index.js 9.4 KB

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