| "use strict"; |
| |
| // 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; |
| } |