index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. 'use strict';
  2. /**
  3. * Create a new instance
  4. */
  5. function Kareem() {
  6. this._pres = new Map();
  7. this._posts = new Map();
  8. }
  9. Kareem.skipWrappedFunction = function skipWrappedFunction() {
  10. if (!(this instanceof Kareem.skipWrappedFunction)) {
  11. return new Kareem.skipWrappedFunction(...arguments);
  12. }
  13. this.args = [...arguments];
  14. };
  15. Kareem.overwriteResult = function overwriteResult() {
  16. if (!(this instanceof Kareem.overwriteResult)) {
  17. return new Kareem.overwriteResult(...arguments);
  18. }
  19. this.args = [...arguments];
  20. };
  21. Kareem.overwriteArguments = function overwriteArguments() {
  22. if (!(this instanceof Kareem.overwriteArguments)) {
  23. return new Kareem.overwriteArguments(...arguments);
  24. }
  25. this.args = [...arguments];
  26. };
  27. /**
  28. * Execute all "pre" hooks for "name"
  29. * @param {String} name The hook name to execute
  30. * @param {*} context Overwrite the "this" for the hook
  31. * @param {Array} args arguments passed to the pre hooks
  32. * @returns {Array} The potentially modified arguments
  33. */
  34. Kareem.prototype.execPre = async function execPre(name, context, args) {
  35. const pres = this._pres.get(name) || [];
  36. const numPres = pres.length;
  37. let $args = args;
  38. let skipWrappedFunction = null;
  39. if (!numPres) {
  40. return $args;
  41. }
  42. for (const pre of pres) {
  43. const args = [];
  44. const _args = [null].concat($args);
  45. for (let i = 1; i < _args.length; ++i) {
  46. if (i === _args.length - 1 && typeof _args[i] === 'function') {
  47. continue; // skip callbacks to avoid accidentally calling the callback from a hook
  48. }
  49. args.push(_args[i]);
  50. }
  51. try {
  52. const maybePromiseLike = pre.fn.apply(context, args);
  53. if (isPromiseLike(maybePromiseLike)) {
  54. const result = await maybePromiseLike;
  55. if (result instanceof Kareem.overwriteArguments) {
  56. $args = result.args;
  57. }
  58. } else if (maybePromiseLike instanceof Kareem.overwriteArguments) {
  59. $args = maybePromiseLike.args;
  60. }
  61. } catch (error) {
  62. if (error instanceof Kareem.skipWrappedFunction) {
  63. skipWrappedFunction = error;
  64. continue;
  65. }
  66. if (error instanceof Kareem.overwriteArguments) {
  67. $args = error.args;
  68. continue;
  69. }
  70. throw error;
  71. }
  72. }
  73. if (skipWrappedFunction) {
  74. throw skipWrappedFunction;
  75. }
  76. return $args;
  77. };
  78. /**
  79. * Execute all "pre" hooks for "name" synchronously
  80. * @param {String} name The hook name to execute
  81. * @param {*} context Overwrite the "this" for the hook
  82. * @param {Array} [args] Apply custom arguments to the hook
  83. * @returns {Array} The potentially modified arguments
  84. */
  85. Kareem.prototype.execPreSync = function(name, context, args) {
  86. const pres = this._pres.get(name) || [];
  87. const numPres = pres.length;
  88. let $args = args || [];
  89. for (let i = 0; i < numPres; ++i) {
  90. const result = pres[i].fn.apply(context, $args);
  91. if (result instanceof Kareem.overwriteArguments) {
  92. $args = result.args;
  93. }
  94. }
  95. return $args;
  96. };
  97. /**
  98. * Execute all "post" hooks for "name"
  99. * @param {String} name The hook name to execute
  100. * @param {*} context Overwrite the "this" for the hook
  101. * @param {Array} args Apply custom arguments to the hook
  102. * @param {*} options Optional options or directly the callback
  103. * @returns {void}
  104. */
  105. Kareem.prototype.execPost = async function execPost(name, context, args, options) {
  106. const posts = this._posts.get(name) || [];
  107. const numPosts = posts.length;
  108. let firstError = null;
  109. if (options && options.error) {
  110. firstError = options.error;
  111. }
  112. if (!numPosts) {
  113. if (firstError != null) {
  114. throw firstError;
  115. }
  116. return args;
  117. }
  118. for (const currentPost of posts) {
  119. const post = currentPost.fn;
  120. let numArgs = 0;
  121. const newArgs = [];
  122. const argLength = args.length;
  123. for (let i = 0; i < argLength; ++i) {
  124. if (!args[i] || !args[i]._kareemIgnore) {
  125. numArgs += 1;
  126. newArgs.push(args[i]);
  127. }
  128. }
  129. // If numCallbackParams set, fill in the rest with null to enforce consistent number of args
  130. if (options?.numCallbackParams != null) {
  131. numArgs = options.numCallbackParams;
  132. for (let i = newArgs.length; i < numArgs; ++i) {
  133. newArgs.push(null);
  134. }
  135. }
  136. let resolve;
  137. let reject;
  138. const cbPromise = new Promise((_resolve, _reject) => {
  139. resolve = _resolve;
  140. reject = _reject;
  141. });
  142. newArgs.push(function nextCallback(err) {
  143. if (err) {
  144. reject(err);
  145. } else {
  146. resolve();
  147. }
  148. });
  149. if (firstError) {
  150. if (isErrorHandlingMiddleware(currentPost, numArgs)) {
  151. try {
  152. const res = post.apply(context, [firstError].concat(newArgs));
  153. if (isPromiseLike(res)) {
  154. await res;
  155. } else if (post.length === numArgs + 2) {
  156. // `numArgs + 2` because we added the error and the callback
  157. await cbPromise;
  158. }
  159. } catch (error) {
  160. if (error instanceof Kareem.overwriteResult) {
  161. args = error.args;
  162. continue;
  163. }
  164. firstError = error;
  165. }
  166. } else {
  167. continue;
  168. }
  169. } else {
  170. if (isErrorHandlingMiddleware(currentPost, numArgs)) {
  171. // Skip error handlers if no error
  172. continue;
  173. } else {
  174. let res = null;
  175. try {
  176. res = post.apply(context, newArgs);
  177. if (isPromiseLike(res)) {
  178. res = await res;
  179. } else if (post.length === numArgs + 1) {
  180. // If post function takes a callback, wait for the post function to call the callback
  181. res = await cbPromise;
  182. }
  183. } catch (error) {
  184. if (error instanceof Kareem.overwriteResult) {
  185. args = error.args;
  186. continue;
  187. }
  188. firstError = error;
  189. continue;
  190. }
  191. if (res instanceof Kareem.overwriteResult) {
  192. args = res.args;
  193. continue;
  194. }
  195. }
  196. }
  197. }
  198. if (firstError != null) {
  199. throw firstError;
  200. }
  201. return args;
  202. };
  203. /**
  204. * Execute all "post" hooks for "name" synchronously
  205. * @param {String} name The hook name to execute
  206. * @param {*} context Overwrite the "this" for the hook
  207. * @param {Array} args Apply custom arguments to the hook
  208. * @returns {Array} The used arguments
  209. */
  210. Kareem.prototype.execPostSync = function(name, context, args) {
  211. const posts = this._posts.get(name) || [];
  212. const numPosts = posts.length;
  213. for (let i = 0; i < numPosts; ++i) {
  214. const res = posts[i].fn.apply(context, args || []);
  215. if (res instanceof Kareem.overwriteResult) {
  216. args = res.args;
  217. }
  218. }
  219. return args;
  220. };
  221. /**
  222. * Create a synchronous wrapper for "fn"
  223. * @param {String} name The name of the hook
  224. * @param {Function} fn The function to wrap
  225. * @returns {Function} The wrapped function
  226. */
  227. Kareem.prototype.createWrapperSync = function(name, fn) {
  228. const _this = this;
  229. return function syncWrapper() {
  230. const modifiedArgs = _this.execPreSync(name, this, Array.from(arguments));
  231. const toReturn = fn.apply(this, modifiedArgs);
  232. const result = _this.execPostSync(name, this, [toReturn]);
  233. return result[0];
  234. };
  235. };
  236. /**
  237. * Executes pre hooks, followed by the wrapped function, followed by post hooks.
  238. * @param {String} name The name of the hook
  239. * @param {Function} fn The function for the hook
  240. * @param {*} context Overwrite the "this" for the hook
  241. * @param {Array} args Apply custom arguments to the hook
  242. * @param {Object} options Additional options for the hook
  243. * @returns {void}
  244. */
  245. Kareem.prototype.wrap = async function wrap(name, fn, context, args, options) {
  246. let ret;
  247. let skipWrappedFunction = false;
  248. let modifiedArgs = args;
  249. try {
  250. modifiedArgs = await this.execPre(name, context, args);
  251. } catch (error) {
  252. if (error instanceof Kareem.skipWrappedFunction) {
  253. ret = error.args;
  254. skipWrappedFunction = true;
  255. } else {
  256. await this.execPost(name, context, args, { ...options, error });
  257. }
  258. }
  259. if (!skipWrappedFunction) {
  260. ret = await fn.apply(context, modifiedArgs);
  261. }
  262. ret = await this.execPost(name, context, [ret], options);
  263. return ret[0];
  264. };
  265. /**
  266. * Filter current instance for something specific and return the filtered clone
  267. * @param {Function} fn The filter function
  268. * @returns {Kareem} The cloned and filtered instance
  269. */
  270. Kareem.prototype.filter = function(fn) {
  271. const clone = this.clone();
  272. const pres = Array.from(clone._pres.keys());
  273. for (const name of pres) {
  274. const hooks = this._pres.get(name).
  275. map(h => Object.assign({}, h, { name: name })).
  276. filter(fn);
  277. if (hooks.length === 0) {
  278. clone._pres.delete(name);
  279. continue;
  280. }
  281. clone._pres.set(name, hooks);
  282. }
  283. const posts = Array.from(clone._posts.keys());
  284. for (const name of posts) {
  285. const hooks = this._posts.get(name).
  286. map(h => Object.assign({}, h, { name: name })).
  287. filter(fn);
  288. if (hooks.length === 0) {
  289. clone._posts.delete(name);
  290. continue;
  291. }
  292. clone._posts.set(name, hooks);
  293. }
  294. return clone;
  295. };
  296. /**
  297. * Check for a "name" to exist either in pre or post hooks
  298. * @param {String} name The name of the hook
  299. * @returns {Boolean} "true" if found, "false" otherwise
  300. */
  301. Kareem.prototype.hasHooks = function(name) {
  302. return this._pres.has(name) || this._posts.has(name);
  303. };
  304. /**
  305. * Create a Wrapper for "fn" on "name" and return the wrapped function
  306. * @param {String} name The name of the hook
  307. * @param {Function} fn The function to wrap
  308. * @param {*} context Overwrite the "this" for the hook
  309. * @param {Object} [options]
  310. * @returns {Function} The wrapped function
  311. */
  312. Kareem.prototype.createWrapper = function(name, fn, context, options) {
  313. const _this = this;
  314. if (!this.hasHooks(name)) {
  315. // Fast path: if there's no hooks for this function, just return the function
  316. return fn;
  317. }
  318. return function kareemWrappedFunction() {
  319. const _context = context || this;
  320. return _this.wrap(name, fn, _context, Array.from(arguments), options);
  321. };
  322. };
  323. /**
  324. * Register a new hook for "pre"
  325. * @param {String} name The name of the hook
  326. * @param {Object} [options]
  327. * @param {Function} fn The function to register for "name"
  328. * @param {never} error Unused
  329. * @param {Boolean} [unshift] Wheter to "push" or to "unshift" the new hook
  330. * @returns {Kareem}
  331. */
  332. Kareem.prototype.pre = function(name, options, fn, error, unshift) {
  333. if (typeof options === 'function') {
  334. fn = options;
  335. options = {};
  336. } else if (options == null) {
  337. options = {};
  338. }
  339. const pres = this._pres.get(name) || [];
  340. this._pres.set(name, pres);
  341. if (typeof fn !== 'function') {
  342. throw new Error('pre() requires a function, got "' + typeof fn + '"');
  343. }
  344. if (unshift) {
  345. pres.unshift(Object.assign({}, options, { fn: fn }));
  346. } else {
  347. pres.push(Object.assign({}, options, { fn: fn }));
  348. }
  349. return this;
  350. };
  351. /**
  352. * Register a new hook for "post"
  353. * @param {String} name The name of the hook
  354. * @param {Object} [options]
  355. * @param {Boolean} [options.errorHandler] Whether this is an error handler
  356. * @param {Function} fn The function to register for "name"
  357. * @param {Boolean} [unshift] Wheter to "push" or to "unshift" the new hook
  358. * @returns {Kareem}
  359. */
  360. Kareem.prototype.post = function(name, options, fn, unshift) {
  361. const posts = this._posts.get(name) || [];
  362. if (typeof options === 'function') {
  363. unshift = !!fn;
  364. fn = options;
  365. options = {};
  366. }
  367. if (typeof fn !== 'function') {
  368. throw new Error('post() requires a function, got "' + typeof fn + '"');
  369. }
  370. if (unshift) {
  371. posts.unshift(Object.assign({}, options, { fn: fn }));
  372. } else {
  373. posts.push(Object.assign({}, options, { fn: fn }));
  374. }
  375. this._posts.set(name, posts);
  376. return this;
  377. };
  378. /**
  379. * Register a new error handler for "name"
  380. * @param {String} name The name of the hook
  381. * @param {Object} [options]
  382. * @param {Function} fn The function to register for "name"
  383. * @param {Boolean} [unshift] Wheter to "push" or to "unshift" the new hook
  384. * @returns {Kareem}
  385. */
  386. Kareem.prototype.postError = function postError(name, options, fn, unshift) {
  387. if (typeof options === 'function') {
  388. unshift = !!fn;
  389. fn = options;
  390. options = {};
  391. }
  392. return this.post(name, { ...options, errorHandler: true }, fn, unshift);
  393. };
  394. /**
  395. * Clone the current instance
  396. * @returns {Kareem} The cloned instance
  397. */
  398. Kareem.prototype.clone = function() {
  399. const n = new Kareem();
  400. for (const key of this._pres.keys()) {
  401. const clone = this._pres.get(key).slice();
  402. n._pres.set(key, clone);
  403. }
  404. for (const key of this._posts.keys()) {
  405. n._posts.set(key, this._posts.get(key).slice());
  406. }
  407. return n;
  408. };
  409. /**
  410. * Merge "other" into self or "clone"
  411. * @param {Kareem} other The instance to merge with
  412. * @param {Kareem} [clone] The instance to merge onto (if not defined, using "this")
  413. * @returns {Kareem} The merged instance
  414. */
  415. Kareem.prototype.merge = function(other, clone) {
  416. clone = arguments.length === 1 ? true : clone;
  417. const ret = clone ? this.clone() : this;
  418. for (const key of other._pres.keys()) {
  419. const sourcePres = ret._pres.get(key) || [];
  420. const deduplicated = other._pres.get(key).
  421. // Deduplicate based on `fn`
  422. filter(p => sourcePres.map(_p => _p.fn).indexOf(p.fn) === -1);
  423. const combined = sourcePres.concat(deduplicated);
  424. ret._pres.set(key, combined);
  425. }
  426. for (const key of other._posts.keys()) {
  427. const sourcePosts = ret._posts.get(key) || [];
  428. const deduplicated = other._posts.get(key).
  429. filter(p => sourcePosts.indexOf(p) === -1);
  430. ret._posts.set(key, sourcePosts.concat(deduplicated));
  431. }
  432. return ret;
  433. };
  434. function isPromiseLike(v) {
  435. return (typeof v === 'object' && v !== null && typeof v.then === 'function');
  436. }
  437. function isErrorHandlingMiddleware(post, numArgs) {
  438. if (post.errorHandler) {
  439. return true;
  440. }
  441. return post.fn.length === numArgs + 2;
  442. }
  443. module.exports = Kareem;