forked from sudhirj/simply-deferred
-
Notifications
You must be signed in to change notification settings - Fork 0
/
deferred.coffee
215 lines (181 loc) · 9.6 KB
/
deferred.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# #Simply Deferred
# ###Simplified Deferred Library (jQuery API) for Node and the Browser
# ####MIT Licensed.
# Portions of this code are inspired and borrowed from [underscore.js](http://underscorejs.org/) (MIT License)
# ####[Source (github)](http://github.com/sudhirj/simply-deferred) | [Documentation](https://github.com/sudhirj/simply-deferred#simply-deferred)
# © Sudhir Jonathan [sudhirjonathan.com](http://www.sudhirjonathan.com)
VERSION = '3.1.0'
# First, let's set up the constants that we'll need to signify the state of the `deferred` object. These will be returned from the `state()` method.
PENDING = "pending"
RESOLVED = "resolved"
REJECTED = "rejected"
# `has` and `isArguments` are both workarounds for JS quirks. We use them only to flatten arrays.
# `has` checks if an object natively owns a particular property,
has = (obj, prop) -> obj?.hasOwnProperty prop
# while `isArguments` checks if the given object is a method arguments object (like an array, but not quite).
isArguments = (obj) -> return has(obj, 'length') and has(obj, 'callee')
# jQuery treats anything with a `promise()` function as deferrable
isPromise = (obj) -> has(obj, 'promise') && typeof obj?.promise == 'function'
# Borrowed from the incredibly useful [underscore.js](http://underscorejs.org/), these three utilities help
# flatten argument arrays,
flatten = (array) ->
return flatten Array.prototype.slice.call(array) if isArguments array
return [array] if not Array.isArray array
# > `reduce` requires a modern JS interpreter, or a shim.
return array.reduce (memo, value) ->
return memo.concat flatten value if Array.isArray(value)
memo.push value
return memo
, []
# call functions only after they've been invoked a certain number of times,
after = (times, func) ->
return func() if times <= 0
return -> func.apply(this, arguments) if --times < 1
# and wrap functions so we can run code before and after execution.
wrap = (func, wrapper) ->
return ->
args = [func].concat Array.prototype.slice.call(arguments, 0)
wrapper.apply this, args
# Now we'll need a general callback executor, with optional control over the execution context.
execute = (callbacks, args, context) -> callback.call(context, args...) for callback in flatten callbacks
# Let's start with the Deferred object constructor - it needs no arguments
Deferred = ->
# and all `deferred` objects are in a `'pending'` state when initialized.
state = PENDING
doneCallbacks = []
failCallbacks = []
progressCallbacks = []
closingArguments = {'resolved': {}, 'rejected': {}, 'pending': {}}
# Calling `.promise()` gives you an object that you pass around your code indiscriminately.
# Any code can add callbacks to a `promise`, but none can alter the state of the `deferred` itself.
# You can also transform any candidate object into a promise for this particular deferred object by passing it in.
@promise = (candidate) ->
candidate = candidate || {}
# `.state()` returns the state of the current deferred object. This will be one of `'pending'`, `'resolved'` or `'rejected'`.
candidate.state = -> state
# Let's now create a mechanism to store the callbacks that are added in, or execute them immediately if the deferred has already been resolved or rejected.
storeCallbacks = (shouldExecuteImmediately, holder, holderState) ->
return ->
if state is PENDING then holder.push (flatten arguments)...
if shouldExecuteImmediately() then execute arguments, closingArguments[holderState]
return candidate
# Now we can add success / resolution callbacks using `.done(callback)`,
candidate.done = storeCallbacks((-> state is RESOLVED), doneCallbacks, RESOLVED)
# or failure callbacks using `.fail(callback)`,
candidate.fail = storeCallbacks((-> state is REJECTED), failCallbacks, REJECTED)
# and notification callbacks using `.notify(callback)`,
candidate.progress = storeCallbacks((-> state isnt PENDING), progressCallbacks, PENDING)
# or register a callback to always fire when the deferred is either resolved or rejected - using `.always(callback)`
candidate.always = -> candidate.done(arguments...).fail(arguments...)
# It also makes sense to set up a piper to which can filter the success or failure arguments through the given filter methods.
# Quite useful if you want to transform the results of a promise or log them in some way.
pipe = (doneFilter, failFilter, progressFilter) ->
master = new Deferred()
filter = (source, funnel, callback) ->
if not callback then return candidate[source](master[funnel])
candidate[source] (args...) ->
value = callback(args...)
if isPromise(value) then value.done(master.resolve).fail(master.reject).progress(master.notify) else master[funnel](value)
filter('done', 'resolve', doneFilter)
filter('fail', 'reject', failFilter)
filter('progress', 'notify', progressFilter)
return master
# Expose the `.pipe(doneFilter, failFilter)` method and alias it to `.then()`.
candidate.pipe = pipe
candidate.then = pipe
# soak up references to this promise's promise
candidate.promise ?= -> candidate
return candidate
# Since we now have a way to create all the public methods that this deferred needs on a candidate object, let's use it to create them on itself.
@promise this
# Moving to the methods that exist only on the deferred object itself,
# let's create a generic closing function that stores the final resolution / rejection arguments for future callbacks;
# and then runs all the callbacks that have already been set.
candidate = this
close = (finalState, callbacks, context) ->
return ->
if state is PENDING
state = finalState
closingArguments[finalState] = arguments
execute callbacks, closingArguments[finalState], context
return candidate
return this
# Now we can set up `.resolve([args])` method to close the deferred and call the `done` callbacks,
@resolve = close RESOLVED, doneCallbacks
# and `.reject([args])` to fail it and call the `fail` callbacks,
@reject = close REJECTED, failCallbacks
# and `.notify([args])` to call the `progress` callbacks.
@notify = close PENDING, progressCallbacks
# We can also set up `.resolveWith(context, [args])`, `.rejectWith(context, [args])`, and `.notifyWith(context, [args])` to allow setting an execution scope for the callbacks.
@resolveWith = (context, args) -> close(RESOLVED, doneCallbacks, context)(args...)
@rejectWith = (context, args) -> close(REJECTED, failCallbacks, context)(args...)
@notifyWith = (context, args) -> close(PENDING, progressCallbacks, context)(args...)
return this
# If we're dealing with multiple deferreds, it would be nifty to have a way to run code after all of them succeed (or any of them fail).
# Let's set up a `.when([deferreds])` method to do that. It should be able to take any number or deferreds as arguments.
_when = ->
defs = Array.prototype.slice.apply arguments
if defs.length == 1
# small optimization: pass a single deferred object along
return if isPromise defs[0] then defs[0] else (new Deferred()).resolve(defs[0]).promise()
trigger = new Deferred()
return trigger.resolve().promise() if not defs.length
resolutionArgs = []
finish = after defs.length, -> trigger.resolve(resolutionArgs...)
defs.forEach (def, index) ->
if isPromise def
def.done (args...) ->
# special case deferreds resolved with one or zero arguments
# promote it to a unary value rather than a list of arguments
resolutionArgs[index] = if args.length > 1 then args else args[0]
finish()
else
resolutionArgs[index] = def
finish()
isPromise(def) && def.fail(trigger.reject) for def in defs
trigger.promise()
# Since the core team of [Zepto](http://zeptojs.com/) (and maybe other jQuery compatible libraries) don't seem to like the idea of Deferreds / Promises too much,
# let's put in an easy way to install this library into Zetpo.
installInto = (fw) ->
# Add the `.Deferred()` constructor on to the framework.
fw.Deferred = -> new Deferred()
# And wrap the `.ajax()` method to return a promise instead.
fw.ajax = wrap fw.ajax, (ajax, options = {}) ->
def = new Deferred()
createWrapper = (wrapped, finisher) ->
return wrap wrapped, (func, args...) ->
func(args...) if func
finisher(args...)
# This should let us do `request.done(callback)` instead of passing callbacks in to the options hash.
# Also lets us add as many callbacks as we need at any point in the code.
options.success = createWrapper options.success, def.resolve
# Rinse and repeat for errors. We can now use `request.fail(callback)`.
options.error = createWrapper options.error, def.reject
xhr = ajax(options)
promise = def.promise()
# Provide an abort method to cancel the ongoing request.
promise.abort = -> xhr.abort()
promise
# Let's also alias the `.when()` method, for good measure.
fw.when = _when
# Finally, let's support node by exporting the intersting stuff
if (typeof exports isnt 'undefined')
exports.Deferred = -> new Deferred()
exports.when = _when
exports.installInto = installInto
else if typeof define is 'function' && define.amd
define ()->
if typeof Zepto isnt 'undefined'
installInto(Zepto)
else
Deferred.when = _when
Deferred.installInto = installInto
Deferred
else if typeof Zepto isnt 'undefined'
installInto(Zepto)
else
# and the browser by setting the functions on `window`.
this.Deferred = -> new Deferred();
this.Deferred.when = _when
this.Deferred.installInto = installInto
# That's all, folks. The End.