Spaces:
Running
Running
| /* | |
| MIT License http://www.opensource.org/licenses/mit-license.php | |
| Author Tobias Koppers @sokra | |
| */ | |
| ; | |
| 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); | |
| } | |
| /** | |
| * @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options | |
| */ | |
| 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; | |