Spaces:
Running
Running
| ; | |
| // These use the global symbol registry so that multiple copies of this | |
| // library can work together in case they are not deduped. | |
| const GENSYNC_START = Symbol.for("gensync:v1:start"); | |
| const GENSYNC_SUSPEND = Symbol.for("gensync:v1:suspend"); | |
| const GENSYNC_EXPECTED_START = "GENSYNC_EXPECTED_START"; | |
| const GENSYNC_EXPECTED_SUSPEND = "GENSYNC_EXPECTED_SUSPEND"; | |
| const GENSYNC_OPTIONS_ERROR = "GENSYNC_OPTIONS_ERROR"; | |
| const GENSYNC_RACE_NONEMPTY = "GENSYNC_RACE_NONEMPTY"; | |
| const GENSYNC_ERRBACK_NO_CALLBACK = "GENSYNC_ERRBACK_NO_CALLBACK"; | |
| module.exports = Object.assign( | |
| function gensync(optsOrFn) { | |
| let genFn = optsOrFn; | |
| if (typeof optsOrFn !== "function") { | |
| genFn = newGenerator(optsOrFn); | |
| } else { | |
| genFn = wrapGenerator(optsOrFn); | |
| } | |
| return Object.assign(genFn, makeFunctionAPI(genFn)); | |
| }, | |
| { | |
| all: buildOperation({ | |
| name: "all", | |
| arity: 1, | |
| sync: function(args) { | |
| const items = Array.from(args[0]); | |
| return items.map(item => evaluateSync(item)); | |
| }, | |
| async: function(args, resolve, reject) { | |
| const items = Array.from(args[0]); | |
| if (items.length === 0) { | |
| Promise.resolve().then(() => resolve([])); | |
| return; | |
| } | |
| let count = 0; | |
| const results = items.map(() => undefined); | |
| items.forEach((item, i) => { | |
| evaluateAsync( | |
| item, | |
| val => { | |
| results[i] = val; | |
| count += 1; | |
| if (count === results.length) resolve(results); | |
| }, | |
| reject | |
| ); | |
| }); | |
| }, | |
| }), | |
| race: buildOperation({ | |
| name: "race", | |
| arity: 1, | |
| sync: function(args) { | |
| const items = Array.from(args[0]); | |
| if (items.length === 0) { | |
| throw makeError("Must race at least 1 item", GENSYNC_RACE_NONEMPTY); | |
| } | |
| return evaluateSync(items[0]); | |
| }, | |
| async: function(args, resolve, reject) { | |
| const items = Array.from(args[0]); | |
| if (items.length === 0) { | |
| throw makeError("Must race at least 1 item", GENSYNC_RACE_NONEMPTY); | |
| } | |
| for (const item of items) { | |
| evaluateAsync(item, resolve, reject); | |
| } | |
| }, | |
| }), | |
| } | |
| ); | |
| /** | |
| * Given a generator function, return the standard API object that executes | |
| * the generator and calls the callbacks. | |
| */ | |
| function makeFunctionAPI(genFn) { | |
| const fns = { | |
| sync: function(...args) { | |
| return evaluateSync(genFn.apply(this, args)); | |
| }, | |
| async: function(...args) { | |
| return new Promise((resolve, reject) => { | |
| evaluateAsync(genFn.apply(this, args), resolve, reject); | |
| }); | |
| }, | |
| errback: function(...args) { | |
| const cb = args.pop(); | |
| if (typeof cb !== "function") { | |
| throw makeError( | |
| "Asynchronous function called without callback", | |
| GENSYNC_ERRBACK_NO_CALLBACK | |
| ); | |
| } | |
| let gen; | |
| try { | |
| gen = genFn.apply(this, args); | |
| } catch (err) { | |
| cb(err); | |
| return; | |
| } | |
| evaluateAsync(gen, val => cb(undefined, val), err => cb(err)); | |
| }, | |
| }; | |
| return fns; | |
| } | |
| function assertTypeof(type, name, value, allowUndefined) { | |
| if ( | |
| typeof value === type || | |
| (allowUndefined && typeof value === "undefined") | |
| ) { | |
| return; | |
| } | |
| let msg; | |
| if (allowUndefined) { | |
| msg = `Expected opts.${name} to be either a ${type}, or undefined.`; | |
| } else { | |
| msg = `Expected opts.${name} to be a ${type}.`; | |
| } | |
| throw makeError(msg, GENSYNC_OPTIONS_ERROR); | |
| } | |
| function makeError(msg, code) { | |
| return Object.assign(new Error(msg), { code }); | |
| } | |
| /** | |
| * Given an options object, return a new generator that dispatches the | |
| * correct handler based on sync or async execution. | |
| */ | |
| function newGenerator({ name, arity, sync, async, errback }) { | |
| assertTypeof("string", "name", name, true /* allowUndefined */); | |
| assertTypeof("number", "arity", arity, true /* allowUndefined */); | |
| assertTypeof("function", "sync", sync); | |
| assertTypeof("function", "async", async, true /* allowUndefined */); | |
| assertTypeof("function", "errback", errback, true /* allowUndefined */); | |
| if (async && errback) { | |
| throw makeError( | |
| "Expected one of either opts.async or opts.errback, but got _both_.", | |
| GENSYNC_OPTIONS_ERROR | |
| ); | |
| } | |
| if (typeof name !== "string") { | |
| let fnName; | |
| if (errback && errback.name && errback.name !== "errback") { | |
| fnName = errback.name; | |
| } | |
| if (async && async.name && async.name !== "async") { | |
| fnName = async.name.replace(/Async$/, ""); | |
| } | |
| if (sync && sync.name && sync.name !== "sync") { | |
| fnName = sync.name.replace(/Sync$/, ""); | |
| } | |
| if (typeof fnName === "string") { | |
| name = fnName; | |
| } | |
| } | |
| if (typeof arity !== "number") { | |
| arity = sync.length; | |
| } | |
| return buildOperation({ | |
| name, | |
| arity, | |
| sync: function(args) { | |
| return sync.apply(this, args); | |
| }, | |
| async: function(args, resolve, reject) { | |
| if (async) { | |
| async.apply(this, args).then(resolve, reject); | |
| } else if (errback) { | |
| errback.call(this, ...args, (err, value) => { | |
| if (err == null) resolve(value); | |
| else reject(err); | |
| }); | |
| } else { | |
| resolve(sync.apply(this, args)); | |
| } | |
| }, | |
| }); | |
| } | |
| function wrapGenerator(genFn) { | |
| return setFunctionMetadata(genFn.name, genFn.length, function(...args) { | |
| return genFn.apply(this, args); | |
| }); | |
| } | |
| function buildOperation({ name, arity, sync, async }) { | |
| return setFunctionMetadata(name, arity, function*(...args) { | |
| const resume = yield GENSYNC_START; | |
| if (!resume) { | |
| // Break the tail call to avoid a bug in V8 v6.X with --harmony enabled. | |
| const res = sync.call(this, args); | |
| return res; | |
| } | |
| let result; | |
| try { | |
| async.call( | |
| this, | |
| args, | |
| value => { | |
| if (result) return; | |
| result = { value }; | |
| resume(); | |
| }, | |
| err => { | |
| if (result) return; | |
| result = { err }; | |
| resume(); | |
| } | |
| ); | |
| } catch (err) { | |
| result = { err }; | |
| resume(); | |
| } | |
| // Suspend until the callbacks run. Will resume synchronously if the | |
| // callback was already called. | |
| yield GENSYNC_SUSPEND; | |
| if (result.hasOwnProperty("err")) { | |
| throw result.err; | |
| } | |
| return result.value; | |
| }); | |
| } | |
| function evaluateSync(gen) { | |
| let value; | |
| while (!({ value } = gen.next()).done) { | |
| assertStart(value, gen); | |
| } | |
| return value; | |
| } | |
| function evaluateAsync(gen, resolve, reject) { | |
| (function step() { | |
| try { | |
| let value; | |
| while (!({ value } = gen.next()).done) { | |
| assertStart(value, gen); | |
| // If this throws, it is considered to have broken the contract | |
| // established for async handlers. If these handlers are called | |
| // synchronously, it is also considered bad behavior. | |
| let sync = true; | |
| let didSyncResume = false; | |
| const out = gen.next(() => { | |
| if (sync) { | |
| didSyncResume = true; | |
| } else { | |
| step(); | |
| } | |
| }); | |
| sync = false; | |
| assertSuspend(out, gen); | |
| if (!didSyncResume) { | |
| // Callback wasn't called synchronously, so break out of the loop | |
| // and let it call 'step' later. | |
| return; | |
| } | |
| } | |
| return resolve(value); | |
| } catch (err) { | |
| return reject(err); | |
| } | |
| })(); | |
| } | |
| function assertStart(value, gen) { | |
| if (value === GENSYNC_START) return; | |
| throwError( | |
| gen, | |
| makeError( | |
| `Got unexpected yielded value in gensync generator: ${JSON.stringify( | |
| value | |
| )}. Did you perhaps mean to use 'yield*' instead of 'yield'?`, | |
| GENSYNC_EXPECTED_START | |
| ) | |
| ); | |
| } | |
| function assertSuspend({ value, done }, gen) { | |
| if (!done && value === GENSYNC_SUSPEND) return; | |
| throwError( | |
| gen, | |
| makeError( | |
| done | |
| ? "Unexpected generator completion. If you get this, it is probably a gensync bug." | |
| : `Expected GENSYNC_SUSPEND, got ${JSON.stringify( | |
| value | |
| )}. If you get this, it is probably a gensync bug.`, | |
| GENSYNC_EXPECTED_SUSPEND | |
| ) | |
| ); | |
| } | |
| function throwError(gen, err) { | |
| // Call `.throw` so that users can step in a debugger to easily see which | |
| // 'yield' passed an unexpected value. If the `.throw` call didn't throw | |
| // back to the generator, we explicitly do it to stop the error | |
| // from being swallowed by user code try/catches. | |
| if (gen.throw) gen.throw(err); | |
| throw err; | |
| } | |
| function isIterable(value) { | |
| return ( | |
| !!value && | |
| (typeof value === "object" || typeof value === "function") && | |
| !value[Symbol.iterator] | |
| ); | |
| } | |
| function setFunctionMetadata(name, arity, fn) { | |
| if (typeof name === "string") { | |
| // This should always work on the supported Node versions, but for the | |
| // sake of users that are compiling to older versions, we check for | |
| // configurability so we don't throw. | |
| const nameDesc = Object.getOwnPropertyDescriptor(fn, "name"); | |
| if (!nameDesc || nameDesc.configurable) { | |
| Object.defineProperty( | |
| fn, | |
| "name", | |
| Object.assign(nameDesc || {}, { | |
| configurable: true, | |
| value: name, | |
| }) | |
| ); | |
| } | |
| } | |
| if (typeof arity === "number") { | |
| const lengthDesc = Object.getOwnPropertyDescriptor(fn, "length"); | |
| if (!lengthDesc || lengthDesc.configurable) { | |
| Object.defineProperty( | |
| fn, | |
| "length", | |
| Object.assign(lengthDesc || {}, { | |
| configurable: true, | |
| value: arity, | |
| }) | |
| ); | |
| } | |
| } | |
| return fn; | |
| } | |