// To do: remove `void`s // To do: remove `null` from output of our APIs, allow it as user APIs. /** * @typedef {(error?: Error | null | undefined, ...output: Array) => void} Callback * Callback. * * @typedef {(...input: Array) => any} Middleware * Ware. * * @typedef Pipeline * Pipeline. * @property {Run} run * Run the pipeline. * @property {Use} use * Add middleware. * * @typedef {(...input: Array) => void} Run * Call all middleware. * * Calls `done` on completion with either an error or the output of the * last middleware. * * > 👉 **Note**: as the length of input defines whether async functions get a * > `next` function, * > it’s recommended to keep `input` at one value normally. * * @typedef {(fn: Middleware) => Pipeline} Use * Add middleware. */ /** * Create new middleware. * * @returns {Pipeline} * Pipeline. */ export function trough() { /** @type {Array} */ const fns = [] /** @type {Pipeline} */ const pipeline = {run, use} return pipeline /** @type {Run} */ function run(...values) { let middlewareIndex = -1 /** @type {Callback} */ const callback = values.pop() if (typeof callback !== 'function') { throw new TypeError('Expected function as last argument, not ' + callback) } next(null, ...values) /** * Run the next `fn`, or we’re done. * * @param {Error | null | undefined} error * @param {Array} output */ function next(error, ...output) { const fn = fns[++middlewareIndex] let index = -1 if (error) { callback(error) return } // Copy non-nullish input into values. while (++index < values.length) { if (output[index] === null || output[index] === undefined) { output[index] = values[index] } } // Save the newly created `output` for the next call. values = output // Next or done. if (fn) { wrap(fn, next)(...output) } else { callback(null, ...output) } } } /** @type {Use} */ function use(middelware) { if (typeof middelware !== 'function') { throw new TypeError( 'Expected `middelware` to be a function, not ' + middelware ) } fns.push(middelware) return pipeline } } /** * Wrap `middleware` into a uniform interface. * * You can pass all input to the resulting function. * `callback` is then called with the output of `middleware`. * * If `middleware` accepts more arguments than the later given in input, * an extra `done` function is passed to it after that input, * which must be called by `middleware`. * * The first value in `input` is the main input value. * All other input values are the rest input values. * The values given to `callback` are the input values, * merged with every non-nullish output value. * * * if `middleware` throws an error, * returns a promise that is rejected, * or calls the given `done` function with an error, * `callback` is called with that error * * if `middleware` returns a value or returns a promise that is resolved, * that value is the main output value * * if `middleware` calls `done`, * all non-nullish values except for the first one (the error) overwrite the * output values * * @param {Middleware} middleware * Function to wrap. * @param {Callback} callback * Callback called with the output of `middleware`. * @returns {Run} * Wrapped middleware. */ export function wrap(middleware, callback) { /** @type {boolean} */ let called return wrapped /** * Call `middleware`. * @this {any} * @param {Array} parameters * @returns {void} */ function wrapped(...parameters) { const fnExpectsCallback = middleware.length > parameters.length /** @type {any} */ let result if (fnExpectsCallback) { parameters.push(done) } try { result = middleware.apply(this, parameters) } catch (error) { const exception = /** @type {Error} */ (error) // Well, this is quite the pickle. // `middleware` received a callback and called it synchronously, but that // threw an error. // The only thing left to do is to throw the thing instead. if (fnExpectsCallback && called) { throw exception } return done(exception) } if (!fnExpectsCallback) { if (result && result.then && typeof result.then === 'function') { result.then(then, done) } else if (result instanceof Error) { done(result) } else { then(result) } } } /** * Call `callback`, only once. * * @type {Callback} */ function done(error, ...output) { if (!called) { called = true callback(error, ...output) } } /** * Call `done` with one value. * * @param {any} [value] */ function then(value) { done(null, value) } }