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.
		
		
		
		
		
			
		
			
				
					374 lines
				
				9.4 KiB
			
		
		
			
		
	
	
					374 lines
				
				9.4 KiB
			| 
											3 years ago
										 | "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; | ||
|  | } |