index.js 9.2 KB

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