| |
| |
| |
| |
| "use strict"; |
|
|
| class HookCodeFactory { |
| constructor(config) { |
| this.config = config; |
| this.options = undefined; |
| this._args = undefined; |
| } |
|
|
| create(options) { |
| this.init(options); |
| let fn; |
| switch (this.options.type) { |
| case "sync": |
| fn = new Function( |
| this.args(), |
| `"use strict";\n${this.header()}${this.contentWithInterceptors({ |
| onError: (err) => `throw ${err};\n`, |
| onResult: (result) => `return ${result};\n`, |
| resultReturns: true, |
| onDone: () => "", |
| rethrowIfPossible: true |
| })}` |
| ); |
| break; |
| case "async": |
| fn = new Function( |
| this.args({ |
| after: "_callback" |
| }), |
| `"use strict";\n${this.header()}${this.contentWithInterceptors({ |
| onError: (err) => `_callback(${err});\n`, |
| onResult: (result) => `_callback(null, ${result});\n`, |
| onDone: () => "_callback();\n" |
| })}` |
| ); |
| break; |
| case "promise": { |
| let errorHelperUsed = false; |
| const content = this.contentWithInterceptors({ |
| onError: (err) => { |
| errorHelperUsed = true; |
| return `_error(${err});\n`; |
| }, |
| onResult: (result) => `_resolve(${result});\n`, |
| onDone: () => "_resolve();\n" |
| }); |
| let code = ""; |
| code += '"use strict";\n'; |
| code += this.header(); |
| code += "return new Promise((function(_resolve, _reject) {\n"; |
| if (errorHelperUsed) { |
| code += "var _sync = true;\n"; |
| code += "function _error(_err) {\n"; |
| code += "if(_sync)\n"; |
| code += |
| "_resolve(Promise.resolve().then((function() { throw _err; })));\n"; |
| code += "else\n"; |
| code += "_reject(_err);\n"; |
| code += "};\n"; |
| } |
| code += content; |
| if (errorHelperUsed) { |
| code += "_sync = false;\n"; |
| } |
| code += "}));\n"; |
| fn = new Function(this.args(), code); |
| break; |
| } |
| } |
| this.deinit(); |
| return fn; |
| } |
|
|
| setup(instance, options) { |
| instance._x = options.taps.map((t) => t.fn); |
| } |
|
|
| |
| |
| |
| init(options) { |
| this.options = options; |
| this._args = [...options.args]; |
| } |
|
|
| deinit() { |
| this.options = undefined; |
| this._args = undefined; |
| } |
|
|
| contentWithInterceptors(options) { |
| if (this.options.interceptors.length > 0) { |
| const { onError, onResult, onDone } = options; |
| let code = ""; |
| for (let i = 0; i < this.options.interceptors.length; i++) { |
| const interceptor = this.options.interceptors[i]; |
| if (interceptor.call) { |
| code += `${this.getInterceptor(i)}.call(${this.args({ |
| before: interceptor.context ? "_context" : undefined |
| })});\n`; |
| } |
| } |
| code += this.content( |
| Object.assign(options, { |
| onError: |
| onError && |
| ((err) => { |
| let code = ""; |
| for (let i = 0; i < this.options.interceptors.length; i++) { |
| const interceptor = this.options.interceptors[i]; |
| if (interceptor.error) { |
| code += `${this.getInterceptor(i)}.error(${err});\n`; |
| } |
| } |
| code += onError(err); |
| return code; |
| }), |
| onResult: |
| onResult && |
| ((result) => { |
| let code = ""; |
| for (let i = 0; i < this.options.interceptors.length; i++) { |
| const interceptor = this.options.interceptors[i]; |
| if (interceptor.result) { |
| code += `${this.getInterceptor(i)}.result(${result});\n`; |
| } |
| } |
| code += onResult(result); |
| return code; |
| }), |
| onDone: |
| onDone && |
| (() => { |
| let code = ""; |
| for (let i = 0; i < this.options.interceptors.length; i++) { |
| const interceptor = this.options.interceptors[i]; |
| if (interceptor.done) { |
| code += `${this.getInterceptor(i)}.done();\n`; |
| } |
| } |
| code += onDone(); |
| return code; |
| }) |
| }) |
| ); |
| return code; |
| } |
| return this.content(options); |
| } |
|
|
| header() { |
| let code = ""; |
| code += this.needContext() ? "var _context = {};\n" : "var _context;\n"; |
| code += "var _x = this._x;\n"; |
| if (this.options.interceptors.length > 0) { |
| code += "var _taps = this.taps;\n"; |
| code += "var _interceptors = this.interceptors;\n"; |
| } |
| return code; |
| } |
|
|
| needContext() { |
| for (const tap of this.options.taps) if (tap.context) return true; |
| return false; |
| } |
|
|
| callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) { |
| let code = ""; |
| let hasTapCached = false; |
| for (let i = 0; i < this.options.interceptors.length; i++) { |
| const interceptor = this.options.interceptors[i]; |
| if (interceptor.tap) { |
| if (!hasTapCached) { |
| code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`; |
| hasTapCached = true; |
| } |
| code += `${this.getInterceptor(i)}.tap(${ |
| interceptor.context ? "_context, " : "" |
| }_tap${tapIndex});\n`; |
| } |
| } |
| code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`; |
| const tap = this.options.taps[tapIndex]; |
| switch (tap.type) { |
| case "sync": |
| if (!rethrowIfPossible) { |
| code += `var _hasError${tapIndex} = false;\n`; |
| code += "try {\n"; |
| } |
| if (onResult) { |
| code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({ |
| before: tap.context ? "_context" : undefined |
| })});\n`; |
| } else { |
| code += `_fn${tapIndex}(${this.args({ |
| before: tap.context ? "_context" : undefined |
| })});\n`; |
| } |
| if (!rethrowIfPossible) { |
| code += "} catch(_err) {\n"; |
| code += `_hasError${tapIndex} = true;\n`; |
| code += onError("_err"); |
| code += "}\n"; |
| code += `if(!_hasError${tapIndex}) {\n`; |
| } |
| if (onResult) { |
| code += onResult(`_result${tapIndex}`); |
| } |
| if (onDone) { |
| code += onDone(); |
| } |
| if (!rethrowIfPossible) { |
| code += "}\n"; |
| } |
| break; |
| case "async": { |
| let cbCode = ""; |
| cbCode += onResult |
| ? `(function(_err${tapIndex}, _result${tapIndex}) {\n` |
| : `(function(_err${tapIndex}) {\n`; |
| cbCode += `if(_err${tapIndex}) {\n`; |
| cbCode += onError(`_err${tapIndex}`); |
| cbCode += "} else {\n"; |
| if (onResult) { |
| cbCode += onResult(`_result${tapIndex}`); |
| } |
| if (onDone) { |
| cbCode += onDone(); |
| } |
| cbCode += "}\n"; |
| cbCode += "})"; |
| code += `_fn${tapIndex}(${this.args({ |
| before: tap.context ? "_context" : undefined, |
| after: cbCode |
| })});\n`; |
| break; |
| } |
| case "promise": |
| code += `var _hasResult${tapIndex} = false;\n`; |
| code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({ |
| before: tap.context ? "_context" : undefined |
| })});\n`; |
| code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`; |
| code += ` throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`; |
| code += `_promise${tapIndex}.then((function(_result${tapIndex}) {\n`; |
| code += `_hasResult${tapIndex} = true;\n`; |
| if (onResult) { |
| code += onResult(`_result${tapIndex}`); |
| } |
| if (onDone) { |
| code += onDone(); |
| } |
| code += `}), function(_err${tapIndex}) {\n`; |
| code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`; |
| code += onError( |
| `!_err${tapIndex} ? new Error('Tap function (tapPromise) rejects "' + _err${tapIndex} + '" value') : _err${tapIndex}` |
| ); |
| code += "});\n"; |
| break; |
| } |
| return code; |
| } |
|
|
| callTapsSeries({ |
| onError, |
| onResult, |
| resultReturns, |
| onDone, |
| doneReturns, |
| rethrowIfPossible |
| }) { |
| if (this.options.taps.length === 0) return onDone(); |
| const firstAsync = this.options.taps.findIndex((t) => t.type !== "sync"); |
| const somethingReturns = resultReturns || doneReturns; |
| let code = ""; |
| let current = onDone; |
| let unrollCounter = 0; |
| for (let j = this.options.taps.length - 1; j >= 0; j--) { |
| const i = j; |
| const unroll = |
| current !== onDone && |
| (this.options.taps[i].type !== "sync" || unrollCounter++ > 20); |
| if (unroll) { |
| unrollCounter = 0; |
| code += `function _next${i}() {\n`; |
| code += current(); |
| code += "}\n"; |
| current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`; |
| } |
| const done = current; |
| const doneBreak = (skipDone) => { |
| if (skipDone) return ""; |
| return onDone(); |
| }; |
| const content = this.callTap(i, { |
| onError: (error) => onError(i, error, done, doneBreak), |
| onResult: |
| onResult && ((result) => onResult(i, result, done, doneBreak)), |
| onDone: !onResult && done, |
| rethrowIfPossible: |
| rethrowIfPossible && (firstAsync < 0 || i < firstAsync) |
| }); |
| current = () => content; |
| } |
| code += current(); |
| return code; |
| } |
|
|
| callTapsLooping({ onError, onDone, rethrowIfPossible }) { |
| if (this.options.taps.length === 0) return onDone(); |
| const syncOnly = this.options.taps.every((t) => t.type === "sync"); |
| let code = ""; |
| if (!syncOnly) { |
| code += "var _looper = (function() {\n"; |
| code += "var _loopAsync = false;\n"; |
| } |
| code += "var _loop;\n"; |
| code += "do {\n"; |
| code += "_loop = false;\n"; |
| for (let i = 0; i < this.options.interceptors.length; i++) { |
| const interceptor = this.options.interceptors[i]; |
| if (interceptor.loop) { |
| code += `${this.getInterceptor(i)}.loop(${this.args({ |
| before: interceptor.context ? "_context" : undefined |
| })});\n`; |
| } |
| } |
| code += this.callTapsSeries({ |
| onError, |
| onResult: (i, result, next, doneBreak) => { |
| let code = ""; |
| code += `if(${result} !== undefined) {\n`; |
| code += "_loop = true;\n"; |
| if (!syncOnly) code += "if(_loopAsync) _looper();\n"; |
| code += doneBreak(true); |
| code += "} else {\n"; |
| code += next(); |
| code += "}\n"; |
| return code; |
| }, |
| onDone: |
| onDone && |
| (() => { |
| let code = ""; |
| code += "if(!_loop) {\n"; |
| code += onDone(); |
| code += "}\n"; |
| return code; |
| }), |
| rethrowIfPossible: rethrowIfPossible && syncOnly |
| }); |
| code += "} while(_loop);\n"; |
| if (!syncOnly) { |
| code += "_loopAsync = true;\n"; |
| code += "});\n"; |
| code += "_looper();\n"; |
| } |
| return code; |
| } |
|
|
| callTapsParallel({ |
| onError, |
| onResult, |
| onDone, |
| rethrowIfPossible, |
| onTap = (i, run) => run() |
| }) { |
| if (this.options.taps.length <= 1) { |
| return this.callTapsSeries({ |
| onError, |
| onResult, |
| onDone, |
| rethrowIfPossible |
| }); |
| } |
| let code = ""; |
| code += "do {\n"; |
| code += `var _counter = ${this.options.taps.length};\n`; |
| if (onDone) { |
| code += "var _done = (function() {\n"; |
| code += onDone(); |
| code += "});\n"; |
| } |
| for (let i = 0; i < this.options.taps.length; i++) { |
| const done = () => { |
| if (onDone) return "if(--_counter === 0) _done();\n"; |
| return "--_counter;"; |
| }; |
| const doneBreak = (skipDone) => { |
| if (skipDone || !onDone) return "_counter = 0;\n"; |
| return "_counter = 0;\n_done();\n"; |
| }; |
| code += "if(_counter <= 0) break;\n"; |
| code += onTap( |
| i, |
| () => |
| this.callTap(i, { |
| onError: (error) => { |
| let code = ""; |
| code += "if(_counter > 0) {\n"; |
| code += onError(i, error, done, doneBreak); |
| code += "}\n"; |
| return code; |
| }, |
| onResult: |
| onResult && |
| ((result) => { |
| let code = ""; |
| code += "if(_counter > 0) {\n"; |
| code += onResult(i, result, done, doneBreak); |
| code += "}\n"; |
| return code; |
| }), |
| onDone: !onResult && (() => done()), |
| rethrowIfPossible |
| }), |
| done, |
| doneBreak |
| ); |
| } |
| code += "} while(false);\n"; |
| return code; |
| } |
|
|
| args({ before, after } = {}) { |
| let allArgs = this._args; |
| if (before) allArgs = [before, ...allArgs]; |
| if (after) allArgs = [...allArgs, after]; |
| if (allArgs.length === 0) { |
| return ""; |
| } |
|
|
| return allArgs.join(", "); |
| } |
|
|
| getTapFn(idx) { |
| return `_x[${idx}]`; |
| } |
|
|
| getTap(idx) { |
| return `_taps[${idx}]`; |
| } |
|
|
| getInterceptor(idx) { |
| return `_interceptors[${idx}]`; |
| } |
| } |
|
|
| module.exports = HookCodeFactory; |
|
|