index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. 'use strict';
  2. function Kareem() {
  3. this._pres = new Map();
  4. this._posts = new Map();
  5. }
  6. Kareem.skipWrappedFunction = function skipWrappedFunction() {
  7. if (!(this instanceof Kareem.skipWrappedFunction)) {
  8. return new Kareem.skipWrappedFunction(...arguments);
  9. }
  10. this.args = [...arguments];
  11. };
  12. Kareem.overwriteResult = function overwriteResult() {
  13. if (!(this instanceof Kareem.overwriteResult)) {
  14. return new Kareem.overwriteResult(...arguments);
  15. }
  16. this.args = [...arguments];
  17. };
  18. Kareem.prototype.execPre = function(name, context, args, callback) {
  19. if (arguments.length === 3) {
  20. callback = args;
  21. args = [];
  22. }
  23. var pres = this._pres.get(name) || [];
  24. var numPres = pres.length;
  25. var numAsyncPres = pres.numAsync || 0;
  26. var currentPre = 0;
  27. var asyncPresLeft = numAsyncPres;
  28. var done = false;
  29. var $args = args;
  30. var shouldSkipWrappedFunction = null;
  31. if (!numPres) {
  32. return nextTick(function() {
  33. callback(null);
  34. });
  35. }
  36. var next = function() {
  37. if (currentPre >= numPres) {
  38. return;
  39. }
  40. var pre = pres[currentPre];
  41. if (pre.isAsync) {
  42. var args = [
  43. decorateNextFn(_next),
  44. decorateNextFn(function(error) {
  45. if (error) {
  46. if (done) {
  47. return;
  48. }
  49. if (error instanceof Kareem.skipWrappedFunction) {
  50. shouldSkipWrappedFunction = error;
  51. } else {
  52. done = true;
  53. return callback(error);
  54. }
  55. }
  56. if (--asyncPresLeft === 0 && currentPre >= numPres) {
  57. return callback(shouldSkipWrappedFunction);
  58. }
  59. })
  60. ];
  61. callMiddlewareFunction(pre.fn, context, args, args[0]);
  62. } else if (pre.fn.length > 0) {
  63. var args = [decorateNextFn(_next)];
  64. var _args = arguments.length >= 2 ? arguments : [null].concat($args);
  65. for (var i = 1; i < _args.length; ++i) {
  66. args.push(_args[i]);
  67. }
  68. callMiddlewareFunction(pre.fn, context, args, args[0]);
  69. } else {
  70. let maybePromiseLike = null;
  71. try {
  72. maybePromiseLike = pre.fn.call(context);
  73. } catch (err) {
  74. if (err != null) {
  75. return callback(err);
  76. }
  77. }
  78. if (isPromiseLike(maybePromiseLike)) {
  79. maybePromiseLike.then(() => _next(), err => _next(err));
  80. } else {
  81. if (++currentPre >= numPres) {
  82. if (asyncPresLeft > 0) {
  83. // Leave parallel hooks to run
  84. return;
  85. } else {
  86. return nextTick(function() {
  87. callback(shouldSkipWrappedFunction);
  88. });
  89. }
  90. }
  91. next();
  92. }
  93. }
  94. };
  95. next.apply(null, [null].concat(args));
  96. function _next(error) {
  97. if (error) {
  98. if (done) {
  99. return;
  100. }
  101. if (error instanceof Kareem.skipWrappedFunction) {
  102. shouldSkipWrappedFunction = error;
  103. } else {
  104. done = true;
  105. return callback(error);
  106. }
  107. }
  108. if (++currentPre >= numPres) {
  109. if (asyncPresLeft > 0) {
  110. // Leave parallel hooks to run
  111. return;
  112. } else {
  113. return callback(shouldSkipWrappedFunction);
  114. }
  115. }
  116. next.apply(context, arguments);
  117. }
  118. };
  119. Kareem.prototype.execPreSync = function(name, context, args) {
  120. var pres = this._pres.get(name) || [];
  121. var numPres = pres.length;
  122. for (var i = 0; i < numPres; ++i) {
  123. pres[i].fn.apply(context, args || []);
  124. }
  125. };
  126. Kareem.prototype.execPost = function(name, context, args, options, callback) {
  127. if (arguments.length < 5) {
  128. callback = options;
  129. options = null;
  130. }
  131. var posts = this._posts.get(name) || [];
  132. var numPosts = posts.length;
  133. var currentPost = 0;
  134. var firstError = null;
  135. if (options && options.error) {
  136. firstError = options.error;
  137. }
  138. if (!numPosts) {
  139. return nextTick(function() {
  140. callback.apply(null, [firstError].concat(args));
  141. });
  142. }
  143. var next = function() {
  144. var post = posts[currentPost].fn;
  145. var numArgs = 0;
  146. var argLength = args.length;
  147. var newArgs = [];
  148. for (var i = 0; i < argLength; ++i) {
  149. numArgs += args[i] && args[i]._kareemIgnore ? 0 : 1;
  150. if (!args[i] || !args[i]._kareemIgnore) {
  151. newArgs.push(args[i]);
  152. }
  153. }
  154. if (firstError) {
  155. if (post.length === numArgs + 2) {
  156. const _cb = decorateNextFn(function(error) {
  157. if (error) {
  158. if (error instanceof Kareem.overwriteResult) {
  159. args = error.args;
  160. if (++currentPost >= numPosts) {
  161. return callback.call(null, firstError);
  162. }
  163. return next();
  164. }
  165. firstError = error;
  166. }
  167. if (++currentPost >= numPosts) {
  168. return callback.call(null, firstError);
  169. }
  170. next();
  171. });
  172. callMiddlewareFunction(post, context,
  173. [firstError].concat(newArgs).concat([_cb]), _cb);
  174. } else {
  175. if (++currentPost >= numPosts) {
  176. return callback.call(null, firstError);
  177. }
  178. next();
  179. }
  180. } else {
  181. const _cb = decorateNextFn(function(error) {
  182. if (error) {
  183. if (error instanceof Kareem.overwriteResult) {
  184. args = error.args;
  185. if (++currentPost >= numPosts) {
  186. return callback.apply(null, [null].concat(args));
  187. }
  188. return next();
  189. }
  190. firstError = error;
  191. return next();
  192. }
  193. if (++currentPost >= numPosts) {
  194. return callback.apply(null, [null].concat(args));
  195. }
  196. next();
  197. });
  198. if (post.length === numArgs + 2) {
  199. // Skip error handlers if no error
  200. if (++currentPost >= numPosts) {
  201. return callback.apply(null, [null].concat(args));
  202. }
  203. return next();
  204. }
  205. if (post.length === numArgs + 1) {
  206. callMiddlewareFunction(post, context, newArgs.concat([_cb]), _cb);
  207. } else {
  208. let error;
  209. let maybePromiseLike;
  210. try {
  211. maybePromiseLike = post.apply(context, newArgs);
  212. } catch (err) {
  213. error = err;
  214. firstError = err;
  215. }
  216. if (isPromiseLike(maybePromiseLike)) {
  217. return maybePromiseLike.then(
  218. (res) => {
  219. _cb(res instanceof Kareem.overwriteResult ? res : null);
  220. },
  221. err => _cb(err)
  222. );
  223. }
  224. if (maybePromiseLike instanceof Kareem.overwriteResult) {
  225. args = maybePromiseLike.args;
  226. }
  227. if (++currentPost >= numPosts) {
  228. return callback.apply(null, [error].concat(args));
  229. }
  230. next();
  231. }
  232. }
  233. };
  234. next();
  235. };
  236. Kareem.prototype.execPostSync = function(name, context, args) {
  237. const posts = this._posts.get(name) || [];
  238. const numPosts = posts.length;
  239. for (let i = 0; i < numPosts; ++i) {
  240. const res = posts[i].fn.apply(context, args || []);
  241. if (res instanceof Kareem.overwriteResult) {
  242. args = res.args;
  243. }
  244. }
  245. return args;
  246. };
  247. Kareem.prototype.createWrapperSync = function(name, fn) {
  248. var kareem = this;
  249. return function syncWrapper() {
  250. kareem.execPreSync(name, this, arguments);
  251. var toReturn = fn.apply(this, arguments);
  252. const result = kareem.execPostSync(name, this, [toReturn]);
  253. return result[0];
  254. };
  255. };
  256. function _handleWrapError(instance, error, name, context, args, options, callback) {
  257. if (options.useErrorHandlers) {
  258. return instance.execPost(name, context, args, { error: error }, function(error) {
  259. return typeof callback === 'function' && callback(error);
  260. });
  261. } else {
  262. return typeof callback === 'function' && callback(error);
  263. }
  264. }
  265. Kareem.prototype.wrap = function(name, fn, context, args, options) {
  266. const lastArg = (args.length > 0 ? args[args.length - 1] : null);
  267. let argsWithoutCb = Array.from(args);
  268. typeof lastArg === 'function' && argsWithoutCb.pop();
  269. const _this = this;
  270. options = options || {};
  271. const checkForPromise = options.checkForPromise;
  272. this.execPre(name, context, args, function(error) {
  273. if (error && !(error instanceof Kareem.skipWrappedFunction)) {
  274. const numCallbackParams = options.numCallbackParams || 0;
  275. const errorArgs = options.contextParameter ? [context] : [];
  276. for (var i = errorArgs.length; i < numCallbackParams; ++i) {
  277. errorArgs.push(null);
  278. }
  279. return _handleWrapError(_this, error, name, context, errorArgs,
  280. options, lastArg);
  281. }
  282. const numParameters = fn.length;
  283. let ret;
  284. if (error instanceof Kareem.skipWrappedFunction) {
  285. ret = error.args[0];
  286. return _cb(null, ...error.args);
  287. } else {
  288. try {
  289. ret = fn.apply(context, argsWithoutCb.concat(_cb));
  290. } catch (err) {
  291. return _cb(err);
  292. }
  293. }
  294. if (checkForPromise) {
  295. if (isPromiseLike(ret)) {
  296. // Thenable, use it
  297. return ret.then(
  298. res => _cb(null, res),
  299. err => _cb(err)
  300. );
  301. }
  302. // If `fn()` doesn't have a callback argument and doesn't return a
  303. // promise, assume it is sync
  304. if (numParameters < argsWithoutCb.length + 1) {
  305. return _cb(null, ret);
  306. }
  307. }
  308. function _cb() {
  309. const argsWithoutError = Array.from(arguments);
  310. argsWithoutError.shift();
  311. if (options.nullResultByDefault && argsWithoutError.length === 0) {
  312. argsWithoutError.push(null);
  313. }
  314. if (arguments[0]) {
  315. // Assume error
  316. return _handleWrapError(_this, arguments[0], name, context,
  317. argsWithoutError, options, lastArg);
  318. } else {
  319. _this.execPost(name, context, argsWithoutError, function() {
  320. if (lastArg === null) {
  321. return;
  322. }
  323. arguments[0]
  324. ? lastArg(arguments[0])
  325. : lastArg.apply(context, arguments);
  326. });
  327. }
  328. }
  329. });
  330. };
  331. Kareem.prototype.filter = function(fn) {
  332. const clone = this.clone();
  333. const pres = Array.from(clone._pres.keys());
  334. for (const name of pres) {
  335. const hooks = this._pres.get(name).
  336. map(h => Object.assign({}, h, { name: name })).
  337. filter(fn);
  338. if (hooks.length === 0) {
  339. clone._pres.delete(name);
  340. continue;
  341. }
  342. hooks.numAsync = hooks.filter(h => h.isAsync).length;
  343. clone._pres.set(name, hooks);
  344. }
  345. const posts = Array.from(clone._posts.keys());
  346. for (const name of posts) {
  347. const hooks = this._posts.get(name).
  348. map(h => Object.assign({}, h, { name: name })).
  349. filter(fn);
  350. if (hooks.length === 0) {
  351. clone._posts.delete(name);
  352. continue;
  353. }
  354. clone._posts.set(name, hooks);
  355. }
  356. return clone;
  357. };
  358. Kareem.prototype.hasHooks = function(name) {
  359. return this._pres.has(name) || this._posts.has(name);
  360. };
  361. Kareem.prototype.createWrapper = function(name, fn, context, options) {
  362. var _this = this;
  363. if (!this.hasHooks(name)) {
  364. // Fast path: if there's no hooks for this function, just return the
  365. // function wrapped in a nextTick()
  366. return function() {
  367. nextTick(() => fn.apply(this, arguments));
  368. };
  369. }
  370. return function() {
  371. var _context = context || this;
  372. _this.wrap(name, fn, _context, Array.from(arguments), options);
  373. };
  374. };
  375. Kareem.prototype.pre = function(name, isAsync, fn, error, unshift) {
  376. let options = {};
  377. if (typeof isAsync === 'object' && isAsync !== null) {
  378. options = isAsync;
  379. isAsync = options.isAsync;
  380. } else if (typeof arguments[1] !== 'boolean') {
  381. fn = isAsync;
  382. isAsync = false;
  383. }
  384. const pres = this._pres.get(name) || [];
  385. this._pres.set(name, pres);
  386. if (isAsync) {
  387. pres.numAsync = pres.numAsync || 0;
  388. ++pres.numAsync;
  389. }
  390. if (typeof fn !== 'function') {
  391. throw new Error('pre() requires a function, got "' + typeof fn + '"');
  392. }
  393. if (unshift) {
  394. pres.unshift(Object.assign({}, options, { fn: fn, isAsync: isAsync }));
  395. } else {
  396. pres.push(Object.assign({}, options, { fn: fn, isAsync: isAsync }));
  397. }
  398. return this;
  399. };
  400. Kareem.prototype.post = function(name, options, fn, unshift) {
  401. const hooks = this._posts.get(name) || [];
  402. if (typeof options === 'function') {
  403. unshift = !!fn;
  404. fn = options;
  405. options = {};
  406. }
  407. if (typeof fn !== 'function') {
  408. throw new Error('post() requires a function, got "' + typeof fn + '"');
  409. }
  410. if (unshift) {
  411. hooks.unshift(Object.assign({}, options, { fn: fn }));
  412. } else {
  413. hooks.push(Object.assign({}, options, { fn: fn }));
  414. }
  415. this._posts.set(name, hooks);
  416. return this;
  417. };
  418. Kareem.prototype.clone = function() {
  419. const n = new Kareem();
  420. for (let key of this._pres.keys()) {
  421. const clone = this._pres.get(key).slice();
  422. clone.numAsync = this._pres.get(key).numAsync;
  423. n._pres.set(key, clone);
  424. }
  425. for (let key of this._posts.keys()) {
  426. n._posts.set(key, this._posts.get(key).slice());
  427. }
  428. return n;
  429. };
  430. Kareem.prototype.merge = function(other, clone) {
  431. clone = arguments.length === 1 ? true : clone;
  432. var ret = clone ? this.clone() : this;
  433. for (let key of other._pres.keys()) {
  434. const sourcePres = ret._pres.get(key) || [];
  435. const deduplicated = other._pres.get(key).
  436. // Deduplicate based on `fn`
  437. filter(p => sourcePres.map(_p => _p.fn).indexOf(p.fn) === -1);
  438. const combined = sourcePres.concat(deduplicated);
  439. combined.numAsync = sourcePres.numAsync || 0;
  440. combined.numAsync += deduplicated.filter(p => p.isAsync).length;
  441. ret._pres.set(key, combined);
  442. }
  443. for (let key of other._posts.keys()) {
  444. const sourcePosts = ret._posts.get(key) || [];
  445. const deduplicated = other._posts.get(key).
  446. filter(p => sourcePosts.indexOf(p) === -1);
  447. ret._posts.set(key, sourcePosts.concat(deduplicated));
  448. }
  449. return ret;
  450. };
  451. function callMiddlewareFunction(fn, context, args, next) {
  452. let maybePromiseLike;
  453. try {
  454. maybePromiseLike = fn.apply(context, args);
  455. } catch (error) {
  456. return next(error);
  457. }
  458. if (isPromiseLike(maybePromiseLike)) {
  459. maybePromiseLike.then(() => next(), err => next(err));
  460. }
  461. }
  462. function isPromiseLike(v) {
  463. return (typeof v === 'object' && v !== null && typeof v.then === 'function');
  464. }
  465. function decorateNextFn(fn) {
  466. var called = false;
  467. var _this = this;
  468. return function() {
  469. // Ensure this function can only be called once
  470. if (called) {
  471. return;
  472. }
  473. called = true;
  474. // Make sure to clear the stack so try/catch doesn't catch errors
  475. // in subsequent middleware
  476. return nextTick(() => fn.apply(_this, arguments));
  477. };
  478. }
  479. const nextTick = typeof process === 'object' && process !== null && process.nextTick || function nextTick(cb) {
  480. setTimeout(cb, 0);
  481. };
  482. module.exports = Kareem;