-
Notifications
You must be signed in to change notification settings - Fork 2
/
tillthen.js
311 lines (254 loc) · 14 KB
/
tillthen.js
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
// Tillthen v0.3.5
// https://github.com/biril/tillthen
// Licensed and freely distributed under the MIT License
// Copyright (c) 2013-2015 Alex Lambiris
/*global exports, define, process */
(function (root, createModule) {
"use strict";
var
// Package various utility functions we'll be reusing
_ = {
isObject: function (o) {
return Object.prototype.toString.call(o) === "[object Object]";
},
isFunction: function (f) {
return Object.prototype.toString.call(f) === "[object Function]";
}
},
// Detect the current environment (can be CommonJS, AMD or browser). Tillthen will be
// exposed as a module or global depending on that
env = (function () {
// A global `define` method with an `amd` property signifies the presence of an AMD
// loader (require.js, curl.js)
if (typeof define === "function" && define.amd) { return "AMD"; }
// A global `exports` object signifies CommonJS-like enviroments that support
// `module.exports`, e.g. Node
if (typeof exports !== "undefined" && _.isObject(exports)) { return "CommonJS"; }
// If none of the above, then assume a browser, without AMD
return "browser";
}());
// Create a next-turn-evaluation function: A function that evaluates given function `f` on
// given value `v`, *soon*, i.e. *not* in the same turn of the event loop
//
// (Note that support for CommonJS will be specific to node. So if the detected environment
// is in fact 'CommonJS', the presense of node's `process` object is assumed and the latter
// is used to get a reference to Node's `nextTick` method)
_.evaluateOnNextTurn = (function () {
return env === "CommonJS" ?
function (f /*, arg1, .., argN */) {
var args = Array.prototype.slice.apply(arguments);
args.shift();
process.nextTick(function () { f.apply(null, args); });
} :
function (/* f, arg1, .., argN */) {
Array.prototype.splice.call(arguments, 1, 0, 0);
root.setTimeout.apply(root, arguments);
};
}());
// Expose as a module or global depending on the detected environment
switch (env) {
case "CommonJS":
createModule(_, exports);
break;
case "AMD":
define(["exports"], function (exports) { return createModule(_, exports); });
break;
case "browser":
root.tillthen = createModule(_, {});
// When running in a browser (without AMD modules), attach a `noConflict` onto the
// `tillthen` global
root.tillthen.noConflict = (function () {
// Save a reference to the previous value of 'tillthen', so that it can be restored
// later on, if 'noConflict' is used
var previousTillthen = root.tillthen;
// Run in no-conflict mode, setting the `tillthen` global to to its previous value.
// Returns `tillthen`
return function () {
var tillthen = root.tillthen;
root.tillthen = previousTillthen;
tillthen.noConflict = function () { return tillthen; };
return tillthen;
};
}());
}
}(this, function (_, tillthen) {
"use strict";
var
// Tillthen deferred constructor
TillthenDeferred = function () {},
// Tillthen promise constructor
TillthenPromise = function () {},
// Resolve `deferred`, i.e. transition it to an appropriate state depending on given `x`
resolveDeferred = function (deferred, x) {
var xThen = null;
// If `promise` and `x` refer to the same object, reject promise with a TypeError as
// the reason
if (deferred.promise === x) {
return deferred.reject(new TypeError("Cannot resolve a promise with itself"));
}
// If `x` is a promise, adopt its (future) state
if (x instanceof TillthenPromise) {
if (x.state === "fulfilled") { return deferred.fulfill(x.result); }
if (x.state === "rejected") { return deferred.reject(x.result); }
return x.then(deferred.fulfill, deferred.reject);
}
// if `x` is *not* a thenable, fulfill promise with `x`. If attempting to query `then`
// throws an error, reject promise with that error as the reason
//
// (The procedure of first storing a reference to `x.then`, then testing that reference,
// and then calling that reference, avoids multiple accesses to the `x.then` property
// ensuring consistency in the face of an accessor property, whose value could change
// between retrievals)
try {
if (!(_.isObject(x) || _.isFunction(x)) || !_.isFunction(xThen = x.then)) {
return deferred.fulfill(x);
}
}
catch (error) { deferred.reject(error); }
// If `x` is a thenable adopt its (future) state
xThen.call(x, function (value) {
resolveDeferred(deferred, value);
}, function (reason) {
deferred.reject(reason);
});
},
// Create an evaluator for given `onResulted` function and `deferred` object. When invoked
// with a `result` (value or reason), the evaluator will evaluate `onResulted(result)`
// and will use the returned value to resolve `deferred`
createEvaluator = function (onResulted, deferred) {
return function (result) {
try { resolveDeferred(deferred, onResulted(result)); }
catch (reason) { deferred.reject(reason); }
};
},
// Create a deferred object: A pending promise with `resolve`, `fulfill` and `reject`
// methods
createDeferred = function () {
var
// Promise's current state
state = "pending",
// Value of fulfillment or reason of rejection. Will be set when fulfillment or
// rejection actually occurs
result,
// Queues of fulfillment / rejection handlers. Handlers are added whenever the
// promise's `then` method is invoked
fulfillQueue = [],
rejectQueue = [],
// The actual promise. The deferred will derive from this
promise = new TillthenPromise(),
// The deferred to be returned
deferred = null,
// Queue a handler and a dependant deferred for fulfillment. When (and if) the
// promise is fulfilled, the handler will be evaluated on promise's value and the
// result will be used to resolve the dependant deferred
queueForFulfillment = function (onFulfilled, dependantDeferred) {
// If the promise is already rejected, there's nothing to be done
if (state === "rejected") { return; }
// If given `onFulfilled` is not a function then use a pass-through function in
// its place
_.isFunction(onFulfilled) || (onFulfilled = function (value) { return value; });
// Create an evaluator to do the dirty work and either run it 'now' if the
// promise is already fulfilled or as soon as (and if) that eventually happens
var evaluator = createEvaluator(onFulfilled, dependantDeferred);
state === "fulfilled" ? _.evaluateOnNextTurn(evaluator, result) :
fulfillQueue.push(evaluator);
},
// Queue a handler and a dependant deferred for rejection. When (and if) the promise
// is rejected, the handler will be evaluated on promise's reason and the result
// will be used to resolve the dependant deferred
queueForRejection = function (onRejected, dependantDeferred) {
// If the promise is already fulfilled, there's nothing to be done
if (state === "fulfilled") { return; }
// If given `onRejected` is not a function then use a pass-through function in
// its place
_.isFunction(onRejected) || (onRejected = function (error) { throw error; });
// Create an evaluator to do the dirty work and either run it 'now' if the
// promise is already rejected or as soon as (and if) that eventually happens
var evaluator = createEvaluator(onRejected, dependantDeferred);
state === "rejected" ? _.evaluateOnNextTurn(evaluator, result) :
rejectQueue.push(evaluator);
},
// Fulfil the promise. Will run the queued fulfillment-handlers and resolve
// dependant promises. Note that the `fulfill` method will be exposed on the
// returned deferred *only* - not on any returned promise: not by the deferred's
// underlying promise or those returned by invoking `then`
fulfill = function (value) {
// Dont fulfill the promise unless it's currently in a pending state
if (state !== "pending") { return; }
// Fulfil the promise
state = "fulfilled";
_.evaluateOnNextTurn(function (fq) {
for (var i = 0, l = fq.length; i < l; ++i) { fq[i](value); }
}, fulfillQueue);
fulfillQueue = [];
result = value;
return promise;
},
// Reject the promise. Will run the queued rejection-handlers and resolve
// dependant promises. As with the `fulfill` method, the `reject` method will be
// exposed on the returned deferred *only* - not on any returned promise
reject = function (reason) {
// Dont reject the promise unless it's currently in a pending state
if (state !== "pending") { return; }
// Reject the promise
state = "rejected";
_.evaluateOnNextTurn(function (rq) {
for (var i = 0, l = rq.length; i < l; ++i) { rq[i](reason); }
}, rejectQueue);
rejectQueue = [];
result = reason;
return promise;
};
// Attach `then` method as well as `state` and `result` getters to the promise:
Object.defineProperties(promise, {
// Access the promise's current or eventual fulfillment value or rejection reason.
// As soon as (if ever) the promise is fulfilled, the `onFulfilled` handler will
// be evaluated on the promise's fulfillment value. Similarly, as soon as (if ever)
// the promise is rejected, the `onRejected` handler will be evaluated on the
// rejection reason. Returns a new promise which will be eventually resolved
// with the value / reason / promise returned by `onFulfilled` or `onRejected`
then: {
value: function (onFulfilled, onRejected) {
// Create a new deferred, one which is *dependant* on (and will be resolved
// with) the the value / reason / promise returned by `onFulfilled` or
// `onRejected`
var dependantDeferred = createDeferred();
// Queue `onFulfilled` and `onRejected` for evaluation upon the promise's
// eventual fulfillment or rejection
queueForFulfillment(onFulfilled, dependantDeferred);
queueForRejection(onRejected, dependantDeferred);
// Return the dependant deferred's underlying promise
return dependantDeferred.promise;
}
},
// Get the promise's current state ('pending', 'fulfilled' or 'rejected')
state: { get: function () { return state; } },
// Get the promise's result (value or reason). Valid only after the promise has
// either been fulfilled or rejected
result: { get: function () { return result; } }
});
// Derive a deferred from the promise, attach needed methods and return it
TillthenDeferred.prototype = promise;
deferred = new TillthenDeferred();
Object.defineProperties(deferred, {
// Get the deferred's underlying promise
promise: { get: function () { return promise; } },
// Fulfill the promise with given value
fulfill: { value: fulfill },
// Reject the promise with given reason
reject: { value: reject },
// Resolve the promise with given `result`: Fulfill it if `result` is a _value_, or
// cause it to assume `result`'s (future) state if it's a _promise_ itself
resolve: { value: function (result) { resolveDeferred(this, result); } }
});
return deferred;
};
// Attach the `defer` / `getVersion` methods to Tillthen and return it
Object.defineProperties(tillthen, {
// Get a deferred object: A pending promise with `resolve`, `fulfill` and `reject` methods
defer: { value: createDeferred },
// Get current version of Tillthen
version: { get: function () { return "0.3.5"; } }
});
return tillthen;
}));