[d565449] | 1 | "use strict";
|
---|
| 2 |
|
---|
| 3 | // These use the global symbol registry so that multiple copies of this
|
---|
| 4 | // library can work together in case they are not deduped.
|
---|
| 5 | const GENSYNC_START = Symbol.for("gensync:v1:start");
|
---|
| 6 | const GENSYNC_SUSPEND = Symbol.for("gensync:v1:suspend");
|
---|
| 7 |
|
---|
| 8 | const GENSYNC_EXPECTED_START = "GENSYNC_EXPECTED_START";
|
---|
| 9 | const GENSYNC_EXPECTED_SUSPEND = "GENSYNC_EXPECTED_SUSPEND";
|
---|
| 10 | const GENSYNC_OPTIONS_ERROR = "GENSYNC_OPTIONS_ERROR";
|
---|
| 11 | const GENSYNC_RACE_NONEMPTY = "GENSYNC_RACE_NONEMPTY";
|
---|
| 12 | const GENSYNC_ERRBACK_NO_CALLBACK = "GENSYNC_ERRBACK_NO_CALLBACK";
|
---|
| 13 |
|
---|
| 14 | module.exports = Object.assign(
|
---|
| 15 | function gensync(optsOrFn) {
|
---|
| 16 | let genFn = optsOrFn;
|
---|
| 17 | if (typeof optsOrFn !== "function") {
|
---|
| 18 | genFn = newGenerator(optsOrFn);
|
---|
| 19 | } else {
|
---|
| 20 | genFn = wrapGenerator(optsOrFn);
|
---|
| 21 | }
|
---|
| 22 |
|
---|
| 23 | return Object.assign(genFn, makeFunctionAPI(genFn));
|
---|
| 24 | },
|
---|
| 25 | {
|
---|
| 26 | all: buildOperation({
|
---|
| 27 | name: "all",
|
---|
| 28 | arity: 1,
|
---|
| 29 | sync: function(args) {
|
---|
| 30 | const items = Array.from(args[0]);
|
---|
| 31 | return items.map(item => evaluateSync(item));
|
---|
| 32 | },
|
---|
| 33 | async: function(args, resolve, reject) {
|
---|
| 34 | const items = Array.from(args[0]);
|
---|
| 35 |
|
---|
| 36 | if (items.length === 0) {
|
---|
| 37 | Promise.resolve().then(() => resolve([]));
|
---|
| 38 | return;
|
---|
| 39 | }
|
---|
| 40 |
|
---|
| 41 | let count = 0;
|
---|
| 42 | const results = items.map(() => undefined);
|
---|
| 43 | items.forEach((item, i) => {
|
---|
| 44 | evaluateAsync(
|
---|
| 45 | item,
|
---|
| 46 | val => {
|
---|
| 47 | results[i] = val;
|
---|
| 48 | count += 1;
|
---|
| 49 |
|
---|
| 50 | if (count === results.length) resolve(results);
|
---|
| 51 | },
|
---|
| 52 | reject
|
---|
| 53 | );
|
---|
| 54 | });
|
---|
| 55 | },
|
---|
| 56 | }),
|
---|
| 57 | race: buildOperation({
|
---|
| 58 | name: "race",
|
---|
| 59 | arity: 1,
|
---|
| 60 | sync: function(args) {
|
---|
| 61 | const items = Array.from(args[0]);
|
---|
| 62 | if (items.length === 0) {
|
---|
| 63 | throw makeError("Must race at least 1 item", GENSYNC_RACE_NONEMPTY);
|
---|
| 64 | }
|
---|
| 65 |
|
---|
| 66 | return evaluateSync(items[0]);
|
---|
| 67 | },
|
---|
| 68 | async: function(args, resolve, reject) {
|
---|
| 69 | const items = Array.from(args[0]);
|
---|
| 70 | if (items.length === 0) {
|
---|
| 71 | throw makeError("Must race at least 1 item", GENSYNC_RACE_NONEMPTY);
|
---|
| 72 | }
|
---|
| 73 |
|
---|
| 74 | for (const item of items) {
|
---|
| 75 | evaluateAsync(item, resolve, reject);
|
---|
| 76 | }
|
---|
| 77 | },
|
---|
| 78 | }),
|
---|
| 79 | }
|
---|
| 80 | );
|
---|
| 81 |
|
---|
| 82 | /**
|
---|
| 83 | * Given a generator function, return the standard API object that executes
|
---|
| 84 | * the generator and calls the callbacks.
|
---|
| 85 | */
|
---|
| 86 | function makeFunctionAPI(genFn) {
|
---|
| 87 | const fns = {
|
---|
| 88 | sync: function(...args) {
|
---|
| 89 | return evaluateSync(genFn.apply(this, args));
|
---|
| 90 | },
|
---|
| 91 | async: function(...args) {
|
---|
| 92 | return new Promise((resolve, reject) => {
|
---|
| 93 | evaluateAsync(genFn.apply(this, args), resolve, reject);
|
---|
| 94 | });
|
---|
| 95 | },
|
---|
| 96 | errback: function(...args) {
|
---|
| 97 | const cb = args.pop();
|
---|
| 98 | if (typeof cb !== "function") {
|
---|
| 99 | throw makeError(
|
---|
| 100 | "Asynchronous function called without callback",
|
---|
| 101 | GENSYNC_ERRBACK_NO_CALLBACK
|
---|
| 102 | );
|
---|
| 103 | }
|
---|
| 104 |
|
---|
| 105 | let gen;
|
---|
| 106 | try {
|
---|
| 107 | gen = genFn.apply(this, args);
|
---|
| 108 | } catch (err) {
|
---|
| 109 | cb(err);
|
---|
| 110 | return;
|
---|
| 111 | }
|
---|
| 112 |
|
---|
| 113 | evaluateAsync(gen, val => cb(undefined, val), err => cb(err));
|
---|
| 114 | },
|
---|
| 115 | };
|
---|
| 116 | return fns;
|
---|
| 117 | }
|
---|
| 118 |
|
---|
| 119 | function assertTypeof(type, name, value, allowUndefined) {
|
---|
| 120 | if (
|
---|
| 121 | typeof value === type ||
|
---|
| 122 | (allowUndefined && typeof value === "undefined")
|
---|
| 123 | ) {
|
---|
| 124 | return;
|
---|
| 125 | }
|
---|
| 126 |
|
---|
| 127 | let msg;
|
---|
| 128 | if (allowUndefined) {
|
---|
| 129 | msg = `Expected opts.${name} to be either a ${type}, or undefined.`;
|
---|
| 130 | } else {
|
---|
| 131 | msg = `Expected opts.${name} to be a ${type}.`;
|
---|
| 132 | }
|
---|
| 133 |
|
---|
| 134 | throw makeError(msg, GENSYNC_OPTIONS_ERROR);
|
---|
| 135 | }
|
---|
| 136 | function makeError(msg, code) {
|
---|
| 137 | return Object.assign(new Error(msg), { code });
|
---|
| 138 | }
|
---|
| 139 |
|
---|
| 140 | /**
|
---|
| 141 | * Given an options object, return a new generator that dispatches the
|
---|
| 142 | * correct handler based on sync or async execution.
|
---|
| 143 | */
|
---|
| 144 | function newGenerator({ name, arity, sync, async, errback }) {
|
---|
| 145 | assertTypeof("string", "name", name, true /* allowUndefined */);
|
---|
| 146 | assertTypeof("number", "arity", arity, true /* allowUndefined */);
|
---|
| 147 | assertTypeof("function", "sync", sync);
|
---|
| 148 | assertTypeof("function", "async", async, true /* allowUndefined */);
|
---|
| 149 | assertTypeof("function", "errback", errback, true /* allowUndefined */);
|
---|
| 150 | if (async && errback) {
|
---|
| 151 | throw makeError(
|
---|
| 152 | "Expected one of either opts.async or opts.errback, but got _both_.",
|
---|
| 153 | GENSYNC_OPTIONS_ERROR
|
---|
| 154 | );
|
---|
| 155 | }
|
---|
| 156 |
|
---|
| 157 | if (typeof name !== "string") {
|
---|
| 158 | let fnName;
|
---|
| 159 | if (errback && errback.name && errback.name !== "errback") {
|
---|
| 160 | fnName = errback.name;
|
---|
| 161 | }
|
---|
| 162 | if (async && async.name && async.name !== "async") {
|
---|
| 163 | fnName = async.name.replace(/Async$/, "");
|
---|
| 164 | }
|
---|
| 165 | if (sync && sync.name && sync.name !== "sync") {
|
---|
| 166 | fnName = sync.name.replace(/Sync$/, "");
|
---|
| 167 | }
|
---|
| 168 |
|
---|
| 169 | if (typeof fnName === "string") {
|
---|
| 170 | name = fnName;
|
---|
| 171 | }
|
---|
| 172 | }
|
---|
| 173 |
|
---|
| 174 | if (typeof arity !== "number") {
|
---|
| 175 | arity = sync.length;
|
---|
| 176 | }
|
---|
| 177 |
|
---|
| 178 | return buildOperation({
|
---|
| 179 | name,
|
---|
| 180 | arity,
|
---|
| 181 | sync: function(args) {
|
---|
| 182 | return sync.apply(this, args);
|
---|
| 183 | },
|
---|
| 184 | async: function(args, resolve, reject) {
|
---|
| 185 | if (async) {
|
---|
| 186 | async.apply(this, args).then(resolve, reject);
|
---|
| 187 | } else if (errback) {
|
---|
| 188 | errback.call(this, ...args, (err, value) => {
|
---|
| 189 | if (err == null) resolve(value);
|
---|
| 190 | else reject(err);
|
---|
| 191 | });
|
---|
| 192 | } else {
|
---|
| 193 | resolve(sync.apply(this, args));
|
---|
| 194 | }
|
---|
| 195 | },
|
---|
| 196 | });
|
---|
| 197 | }
|
---|
| 198 |
|
---|
| 199 | function wrapGenerator(genFn) {
|
---|
| 200 | return setFunctionMetadata(genFn.name, genFn.length, function(...args) {
|
---|
| 201 | return genFn.apply(this, args);
|
---|
| 202 | });
|
---|
| 203 | }
|
---|
| 204 |
|
---|
| 205 | function buildOperation({ name, arity, sync, async }) {
|
---|
| 206 | return setFunctionMetadata(name, arity, function*(...args) {
|
---|
| 207 | const resume = yield GENSYNC_START;
|
---|
| 208 | if (!resume) {
|
---|
| 209 | // Break the tail call to avoid a bug in V8 v6.X with --harmony enabled.
|
---|
| 210 | const res = sync.call(this, args);
|
---|
| 211 | return res;
|
---|
| 212 | }
|
---|
| 213 |
|
---|
| 214 | let result;
|
---|
| 215 | try {
|
---|
| 216 | async.call(
|
---|
| 217 | this,
|
---|
| 218 | args,
|
---|
| 219 | value => {
|
---|
| 220 | if (result) return;
|
---|
| 221 |
|
---|
| 222 | result = { value };
|
---|
| 223 | resume();
|
---|
| 224 | },
|
---|
| 225 | err => {
|
---|
| 226 | if (result) return;
|
---|
| 227 |
|
---|
| 228 | result = { err };
|
---|
| 229 | resume();
|
---|
| 230 | }
|
---|
| 231 | );
|
---|
| 232 | } catch (err) {
|
---|
| 233 | result = { err };
|
---|
| 234 | resume();
|
---|
| 235 | }
|
---|
| 236 |
|
---|
| 237 | // Suspend until the callbacks run. Will resume synchronously if the
|
---|
| 238 | // callback was already called.
|
---|
| 239 | yield GENSYNC_SUSPEND;
|
---|
| 240 |
|
---|
| 241 | if (result.hasOwnProperty("err")) {
|
---|
| 242 | throw result.err;
|
---|
| 243 | }
|
---|
| 244 |
|
---|
| 245 | return result.value;
|
---|
| 246 | });
|
---|
| 247 | }
|
---|
| 248 |
|
---|
| 249 | function evaluateSync(gen) {
|
---|
| 250 | let value;
|
---|
| 251 | while (!({ value } = gen.next()).done) {
|
---|
| 252 | assertStart(value, gen);
|
---|
| 253 | }
|
---|
| 254 | return value;
|
---|
| 255 | }
|
---|
| 256 |
|
---|
| 257 | function evaluateAsync(gen, resolve, reject) {
|
---|
| 258 | (function step() {
|
---|
| 259 | try {
|
---|
| 260 | let value;
|
---|
| 261 | while (!({ value } = gen.next()).done) {
|
---|
| 262 | assertStart(value, gen);
|
---|
| 263 |
|
---|
| 264 | // If this throws, it is considered to have broken the contract
|
---|
| 265 | // established for async handlers. If these handlers are called
|
---|
| 266 | // synchronously, it is also considered bad behavior.
|
---|
| 267 | let sync = true;
|
---|
| 268 | let didSyncResume = false;
|
---|
| 269 | const out = gen.next(() => {
|
---|
| 270 | if (sync) {
|
---|
| 271 | didSyncResume = true;
|
---|
| 272 | } else {
|
---|
| 273 | step();
|
---|
| 274 | }
|
---|
| 275 | });
|
---|
| 276 | sync = false;
|
---|
| 277 |
|
---|
| 278 | assertSuspend(out, gen);
|
---|
| 279 |
|
---|
| 280 | if (!didSyncResume) {
|
---|
| 281 | // Callback wasn't called synchronously, so break out of the loop
|
---|
| 282 | // and let it call 'step' later.
|
---|
| 283 | return;
|
---|
| 284 | }
|
---|
| 285 | }
|
---|
| 286 |
|
---|
| 287 | return resolve(value);
|
---|
| 288 | } catch (err) {
|
---|
| 289 | return reject(err);
|
---|
| 290 | }
|
---|
| 291 | })();
|
---|
| 292 | }
|
---|
| 293 |
|
---|
| 294 | function assertStart(value, gen) {
|
---|
| 295 | if (value === GENSYNC_START) return;
|
---|
| 296 |
|
---|
| 297 | throwError(
|
---|
| 298 | gen,
|
---|
| 299 | makeError(
|
---|
| 300 | `Got unexpected yielded value in gensync generator: ${JSON.stringify(
|
---|
| 301 | value
|
---|
| 302 | )}. Did you perhaps mean to use 'yield*' instead of 'yield'?`,
|
---|
| 303 | GENSYNC_EXPECTED_START
|
---|
| 304 | )
|
---|
| 305 | );
|
---|
| 306 | }
|
---|
| 307 | function assertSuspend({ value, done }, gen) {
|
---|
| 308 | if (!done && value === GENSYNC_SUSPEND) return;
|
---|
| 309 |
|
---|
| 310 | throwError(
|
---|
| 311 | gen,
|
---|
| 312 | makeError(
|
---|
| 313 | done
|
---|
| 314 | ? "Unexpected generator completion. If you get this, it is probably a gensync bug."
|
---|
| 315 | : `Expected GENSYNC_SUSPEND, got ${JSON.stringify(
|
---|
| 316 | value
|
---|
| 317 | )}. If you get this, it is probably a gensync bug.`,
|
---|
| 318 | GENSYNC_EXPECTED_SUSPEND
|
---|
| 319 | )
|
---|
| 320 | );
|
---|
| 321 | }
|
---|
| 322 |
|
---|
| 323 | function throwError(gen, err) {
|
---|
| 324 | // Call `.throw` so that users can step in a debugger to easily see which
|
---|
| 325 | // 'yield' passed an unexpected value. If the `.throw` call didn't throw
|
---|
| 326 | // back to the generator, we explicitly do it to stop the error
|
---|
| 327 | // from being swallowed by user code try/catches.
|
---|
| 328 | if (gen.throw) gen.throw(err);
|
---|
| 329 | throw err;
|
---|
| 330 | }
|
---|
| 331 |
|
---|
| 332 | function isIterable(value) {
|
---|
| 333 | return (
|
---|
| 334 | !!value &&
|
---|
| 335 | (typeof value === "object" || typeof value === "function") &&
|
---|
| 336 | !value[Symbol.iterator]
|
---|
| 337 | );
|
---|
| 338 | }
|
---|
| 339 |
|
---|
| 340 | function setFunctionMetadata(name, arity, fn) {
|
---|
| 341 | if (typeof name === "string") {
|
---|
| 342 | // This should always work on the supported Node versions, but for the
|
---|
| 343 | // sake of users that are compiling to older versions, we check for
|
---|
| 344 | // configurability so we don't throw.
|
---|
| 345 | const nameDesc = Object.getOwnPropertyDescriptor(fn, "name");
|
---|
| 346 | if (!nameDesc || nameDesc.configurable) {
|
---|
| 347 | Object.defineProperty(
|
---|
| 348 | fn,
|
---|
| 349 | "name",
|
---|
| 350 | Object.assign(nameDesc || {}, {
|
---|
| 351 | configurable: true,
|
---|
| 352 | value: name,
|
---|
| 353 | })
|
---|
| 354 | );
|
---|
| 355 | }
|
---|
| 356 | }
|
---|
| 357 |
|
---|
| 358 | if (typeof arity === "number") {
|
---|
| 359 | const lengthDesc = Object.getOwnPropertyDescriptor(fn, "length");
|
---|
| 360 | if (!lengthDesc || lengthDesc.configurable) {
|
---|
| 361 | Object.defineProperty(
|
---|
| 362 | fn,
|
---|
| 363 | "length",
|
---|
| 364 | Object.assign(lengthDesc || {}, {
|
---|
| 365 | configurable: true,
|
---|
| 366 | value: arity,
|
---|
| 367 | })
|
---|
| 368 | );
|
---|
| 369 | }
|
---|
| 370 | }
|
---|
| 371 |
|
---|
| 372 | return fn;
|
---|
| 373 | }
|
---|