You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
373 lines
9.4 KiB
373 lines
9.4 KiB
"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; |
|
}
|
|
|