-
Notifications
You must be signed in to change notification settings - Fork 7
/
cadence.js
402 lines (376 loc) · 15.2 KB
/
cadence.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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
var stack = [], JUMP = {}
function Cadence (parent, self, steps, vargs, callback, loop, cadence) {
this.parent = parent
this.self = self
this.finalizers = []
this.steps = steps
this.callback = callback
this.loop = loop
this.cadence = cadence || this
this.cadences = []
this.results = []
this.errors = []
this.called = 0
this.index = 0
this.sync = true
this.waiting = false
this.vargs = vargs
}
// TODO Expand on this. You keep coming back here and saying, oh, no, I need to
// give up on a step if there is an error. I'm not returning from this because
// there is an error and all the callbacks are not returning. I need to return
// immediately if there is an error, and then have a lot more code to deal with
// the stragglers that return.
//
// Go back to your code. Try to explain to me why one error first callback
// function returning an error is preventing another, completely different
// callback function from returning an error. Pretend the are calls to open two
// separate files and tell me why the orderly error reporting of the inability
// to open one file should prevent the completion of the opening of another.
// You're probably doing something complicated, a callback is deferred, and
// neglecting to notify the deferred callback of an error.
//
// In short, this code is fine. If you were not using Cadence either you would
// not have noticed the problem, or else you've have some sort of straggler
// issue causing your code to continue after you've responded to an error.
//
// TODO Update. Yes, good point. This is rare in production code, but I do
// encounter it a lot in testing where I'm testing race conditions in concurrent
// code, the kind of code that Cadence has made it easy for me to write.
//
// Because this is rare in production, it's not all that difficult to accept
// that Cadence should return on the first error, then silently swallow all
// subsequent errors. That seems ugly, but not advancing is also ugly, and in
// both cases the ugliness is avoided by writing code that runs serially.
// (Parallel code using the Node.js event loop is a boondoggle.)
//
// The logic isn't that much more difficult.
//
// It does present challenges when you consider what it means to run finalizers
// early.
//
function createCallback (cadence) {
var index = cadence.results.length
cadence.results.push([])
cadence.sync = false
return function (error) {
if (error == null) {
var I = arguments.length
var vargs = cadence.results[index]
for (var i = 1; i < I; i++) {
vargs[i - 1] = arguments[i]
}
} else {
cadence.errors.push(error)
}
if (++cadence.called === cadence.results.length) {
if (cadence.waiting) {
invoke(cadence)
} else {
cadence.sync = true
}
}
}
}
function invoke (cadence) {
var vargs, fn
for (;;) {
if (cadence.errors.length) { // Critical path.
cadence.results.length = 0
// Break on error cadence is frustrated further by catch blocks that
// would restore forward motion. I suppose you'd only short-circuit
// cadences subordinate to this cadence.
if (cadence.catcher) {
var catcher = cadence.catcher, errors = cadence.errors.splice(0)
fn = function () {
return catcher.call(cadence.self, errors[0], errors)
}
} else {
fn = null
cadence.loop = false
}
} else {
if (cadence.results.length == 0) { // Critical path.
// We had no async callbacks, so use the return value.
vargs = cadence.vargs
// Check for a loop controller in the return values.
if (vargs[0] && vargs[0].jump === JUMP) {
var jump = vargs.shift()
var iterator = cadence
// Walk up to the jumping cadence setting all the
// sub-cadences along the way to their last step. We
// continue with the current cadence, not the destination.
// We don't skip finalizers. When we continue, if the
// current cadence is not the jumping cadence, we're going
// to run the exit procedures for each sub-cadence.
if (!jump.immediate) {
var destination = jump.cadence || cadence.cadence
while (destination !== iterator) {
iterator.loop = false
iterator.index = iterator.steps.length
iterator = iterator.parent
}
}
// Set the index and stop looping if this is a `break`.
iterator.index = Math.min(jump.index, iterator.steps.length)
iterator.loop = iterator.loop && ! jump.break
}
} else {
// Combine the results of all the callbacks into an single array
// of arguments that will be used to invoke the next step.
cadence.vargs = vargs = cadence.results.shift()
// Neither `vargs.push.apply(vargs, vargs_)` nor `vargs_.shift()` is faster.
while (cadence.results.length != 0) {
var vargs_ = cadence.results.shift()
for (var j = 0, J = vargs_.length; j < J; j++) {
vargs.push(vargs_[j])
}
}
}
// On to the next step.
fn = cadence.steps[cadence.index++]
}
if (fn == null) { // Critical path.
if (cadence.finalizers.length) {
// We're going to continue to loop until all the finalizers have
// executed. The step index is going to go beyond length of the
// step array, but that's okay.
var finalizer = cadence.finalizers.pop(), errors = cadence.errors.splice(0)
fn = function () {
async(function () {
return finalizer.vargs
}, [finalizer.steps[0], function (error) {
if (errors.length) throw errors[0]
throw error
}], function () {
if (errors.length) throw errors[0]
return vargs
})
}
} else if (cadence.loop) {
// Go back to the first step.
fn = cadence.steps[0]
cadence.index = 1
} else if (cadence.errors.length) {
// Return the first error we received.
cadence.callback.apply(null, [ cadence.errors[0] ])
break
} else {
if (vargs.length !== 0) {
vargs.unshift(null)
}
cadence.callback.apply(null, vargs)
break
}
}
cadence.called = 0
cadence.sync = true
cadence.waiting = false
cadence.catcher = null
if (Array.isArray(fn)) { // Critical path.
if (fn.length === 1) {
cadence.finalizers.push({ steps: fn, vargs: vargs })
continue
} else if (fn.length === 2) {
cadence.catcher = fn[1]
fn = fn[0]
} else if (fn.length === 3) {
var filter = fn
cadence.catcher = function (error) {
if (filter[1].test(error.code || error.message)) {
return filter[2].call(cadence, error)
} else {
throw error
}
}
fn = fn[0]
} else {
cadence.vargs = [ vargs ]
continue
}
}
stack.push(cadence)
try {
var ret = fn.apply(cadence.self, vargs)
if (ret !== void(0)) {
if (ret !== null && typeof ret.then == 'function') {
var resolver = createCallback(cadence)
ret.then(function (result) {
resolver(null, result)
}, function (error) {
resolver(error)
})
} else {
cadence.vargs = Array.isArray(ret) ? ret : [ ret ]
}
}
// The only one that could be removed if we where to invoke cadences
// directly and immediately when created. It would change loop
// labeling so that the loop label was always passed in as a final
// argument to the variadic arguments. This would in cause a gotcha
// where the user needs to make sure that each loop gets the same
// arguments, ah, and that's surprising because often times we're
// not thinking about the return at the end.
while (cadence.cadences.length != 0) {
invoke(cadence.cadences.shift())
}
} catch (error) {
cadence.errors.push(error)
cadence.sync = true
}
stack.pop()
if (!cadence.sync) {
cadence.waiting = true
break
}
}
}
function async () {
var cadence = stack[stack.length - 1]
var I = arguments.length
if (I) {
var vargs = new Array(I)
for (var i = 0; i < I; i++) {
vargs[i] = arguments[i]
}
invoke(new Cadence(cadence, cadence.self, vargs, [], createCallback(cadence), false, cadence.cadence))
} else {
return createCallback(cadence)
}
}
async.continue = { jump: JUMP, index: 0, break: false, immediate: false }
async.break = { jump: JUMP, index: Infinity, break: true, immediate: false }
async.return = { jump: JUMP, index: Infinity, break: true, immediate: true }
function variadic (f) {
return function () {
var I = arguments.length
var vargs = new Array
for (var i = 0; i < I; i++) {
vargs.push(arguments[i])
}
return f(vargs)
}
}
async.loop = variadic(function (steps) {
var cadence = stack[stack.length - 1]
var vargs = steps.shift()
var looper = new Cadence(cadence, cadence.self, steps, vargs, createCallback(cadence), true, null)
cadence.cadences.push(looper)
return {
continue: { jump: JUMP, index: 0, break: false, cadence: looper, immediate: false },
break: { jump: JUMP, index: Infinity, break: true, cadence: looper, immediate: false }
}
})
async.block = variadic(function (steps) {
steps.unshift([])
steps.push(variadic(function (vargs) {
return [ async.break ].concat(vargs)
}))
return async.loop.apply(async, steps)
})
async.forEach = variadic(function (steps) {
var vargs = steps.shift(), array = vargs.shift(), index = -1
steps.unshift(vargs, variadic(function (vargs) {
index++
if (index === array.length) return [ async.break ].concat(vargs)
return [ array[index], index ].concat(vargs)
}))
return async.loop.apply(this, steps)
})
async.map = variadic(function (steps) {
var vargs = steps.shift(), array = vargs.shift(), index = -1, gather = []
steps.unshift(vargs, variadic(function (vargs) {
index++
if (index === array.length) return [ async.break, gather ]
return [ array[index], index ].concat(vargs)
}))
steps.push(variadic(function (vargs) {
gather.push.apply(gather, vargs)
}))
return async.loop.apply(this, steps)
})
var builders = []
function cadence () {
var I = arguments.length
var steps = new Array
for (var i = 0; i < I; i++) {
steps.push(arguments[i])
}
function execute () {
var I = arguments.length - 1
var vargs = new Array(I + 1)
vargs[0] = async
for (var i = 0; i < I; i++) {
vargs[i + 1] = arguments[i]
}
invoke(new Cadence(null, this, steps, vargs, arguments[i], false, null))
}
// Preserving arity costs next to nothing; the call to `execute` in these
// functions will be inlined. The airty function itself will never be
// inlined because it is in a different context than that of our dear user,
// but it will be compiled.
//
// We put back the switch statement that creates a shim without using
// `new Function` because in doing so we can skip through Cadence using the
// Chrome debugger's blackbox framework features. If we go through the
// generated function, we're not able to pattern match the specific file
// name since it is randomly generated. We could match it, but we'd skip
// over any generated function, not just the ones generated by Cadence.
//
// Obviously, we'll have to step into our shim if it has more arguments then
// the options in the following switch statement. Note that I'm allowing the
// builder array to contain builders that will never get called because the
// switch statement will intercept the request. It keeps the code readable
// for now.
var f
switch (steps[0].length) {
case 0:
f = function () { execute.apply(this, arguments) }
break
case 1:
f = function (one) { execute.apply(this, arguments) }
break
case 2:
f = function (one, two) { execute.apply(this, arguments) }
break
case 3:
f = function (one, two, three) { execute.apply(this, arguments) }
break
case 4:
f = function (one, two, three, four) { execute.apply(this, arguments) }
break
case 5:
f = function (one, two, three, four, five) { execute.apply(this, arguments) }
break
case 6:
f = function (one, two, three, four, five, six) { execute.apply(this, arguments) }
break
case 7:
f = function (one, two, three, four, five, six, seven) { execute.apply(this, arguments) }
break
case 8:
f = function (one, two, three, four, five, six, seven, eight) { execute.apply(this, arguments) }
break
case 9:
f = function (one, two, three, four, five, six, seven, eight, nine) { execute.apply(this, arguments) }
break
default:
while (builders.length < steps[0].length + 1) {
var args = []
for (var i = 0, I = builders.length; i < I; i++) {
args[i] = '_' + i
}
builders.push(new Function (' \n\
return function (execute) { \n\
return function (' + args.join(',') + ') { \n\
execute.apply(this, arguments) \n\
} \n\
} \n\
')())
}
f = builders[steps[0].length](execute)
}
f.toString = function () { return steps[0].toString() }
return f
}
module.exports = cadence