├── funding.yml ├── .prettierignore ├── .npmrc ├── .gitignore ├── .editorconfig ├── index.js ├── tsconfig.json ├── .github └── workflows │ └── main.yml ├── license ├── package.json ├── lib └── index.js ├── readme.md └── test.js /funding.yml: -------------------------------------------------------------------------------- 1 | github: wooorm 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts.map 2 | *.d.ts 3 | *.log 4 | .DS_Store 5 | coverage/ 6 | node_modules/ 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/index.js').Callback} Callback 3 | * @typedef {import('./lib/index.js').Middleware} Middleware 4 | * @typedef {import('./lib/index.js').Pipeline} Pipeline 5 | * @typedef {import('./lib/index.js').Run} Run 6 | * @typedef {import('./lib/index.js').Use} Use 7 | */ 8 | 9 | export {trough, wrap} from './lib/index.js' 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2020"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2020" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": ["**/*.js"] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v4 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/hydrogen 21 | - node 22 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trough", 3 | "version": "2.2.0", 4 | "description": "`trough` is middleware", 5 | "license": "MIT", 6 | "keywords": [ 7 | "middleware", 8 | "ware" 9 | ], 10 | "repository": "wooorm/trough", 11 | "bugs": "https://github.com/wooorm/trough/issues", 12 | "funding": { 13 | "type": "github", 14 | "url": "https://github.com/sponsors/wooorm" 15 | }, 16 | "author": "Titus Wormer (https://wooorm.com)", 17 | "contributors": [ 18 | "Titus Wormer (https://wooorm.com)" 19 | ], 20 | "sideEffects": false, 21 | "type": "module", 22 | "exports": "./index.js", 23 | "files": [ 24 | "lib/", 25 | "index.d.ts.map", 26 | "index.d.ts", 27 | "index.js" 28 | ], 29 | "devDependencies": { 30 | "@types/node": "^20.0.0", 31 | "c8": "^9.0.0", 32 | "prettier": "^3.0.0", 33 | "remark-cli": "^11.0.0", 34 | "remark-preset-wooorm": "^9.0.0", 35 | "type-coverage": "^2.0.0", 36 | "typescript": "^5.0.0", 37 | "xo": "^0.56.0" 38 | }, 39 | "scripts": { 40 | "build": "tsc --build --clean && tsc --build && type-coverage", 41 | "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", 42 | "prepack": "npm run build && npm run format", 43 | "test": "npm run build && npm run format && npm run test-coverage", 44 | "test-api": "node --conditions development test.js", 45 | "test-coverage": "c8 --100 --reporter lcov npm run test-api" 46 | }, 47 | "prettier": { 48 | "bracketSpacing": false, 49 | "singleQuote": true, 50 | "semi": false, 51 | "tabWidth": 2, 52 | "trailingComma": "none", 53 | "useTabs": false 54 | }, 55 | "remarkConfig": { 56 | "plugins": [ 57 | "remark-preset-wooorm" 58 | ] 59 | }, 60 | "typeCoverage": { 61 | "atLeast": 100, 62 | "detail": true, 63 | "strict": true, 64 | "ignoreCatch": true, 65 | "#": "some nessecary `any`s", 66 | "ignoreFiles": [ 67 | "lib/index.js", 68 | "lib/index.d.ts" 69 | ] 70 | }, 71 | "xo": { 72 | "prettier": true, 73 | "rules": { 74 | "capitalized-comments": "off" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // To do: remove `void`s 2 | // To do: remove `null` from output of our APIs, allow it as user APIs. 3 | 4 | /** 5 | * @typedef {(error?: Error | null | undefined, ...output: Array) => void} Callback 6 | * Callback. 7 | * 8 | * @typedef {(...input: Array) => any} Middleware 9 | * Ware. 10 | * 11 | * @typedef Pipeline 12 | * Pipeline. 13 | * @property {Run} run 14 | * Run the pipeline. 15 | * @property {Use} use 16 | * Add middleware. 17 | * 18 | * @typedef {(...input: Array) => void} Run 19 | * Call all middleware. 20 | * 21 | * Calls `done` on completion with either an error or the output of the 22 | * last middleware. 23 | * 24 | * > 👉 **Note**: as the length of input defines whether async functions get a 25 | * > `next` function, 26 | * > it’s recommended to keep `input` at one value normally. 27 | 28 | * 29 | * @typedef {(fn: Middleware) => Pipeline} Use 30 | * Add middleware. 31 | */ 32 | 33 | /** 34 | * Create new middleware. 35 | * 36 | * @returns {Pipeline} 37 | * Pipeline. 38 | */ 39 | export function trough() { 40 | /** @type {Array} */ 41 | const fns = [] 42 | /** @type {Pipeline} */ 43 | const pipeline = {run, use} 44 | 45 | return pipeline 46 | 47 | /** @type {Run} */ 48 | function run(...values) { 49 | let middlewareIndex = -1 50 | /** @type {Callback} */ 51 | const callback = values.pop() 52 | 53 | if (typeof callback !== 'function') { 54 | throw new TypeError('Expected function as last argument, not ' + callback) 55 | } 56 | 57 | next(null, ...values) 58 | 59 | /** 60 | * Run the next `fn`, or we’re done. 61 | * 62 | * @param {Error | null | undefined} error 63 | * @param {Array} output 64 | */ 65 | function next(error, ...output) { 66 | const fn = fns[++middlewareIndex] 67 | let index = -1 68 | 69 | if (error) { 70 | callback(error) 71 | return 72 | } 73 | 74 | // Copy non-nullish input into values. 75 | while (++index < values.length) { 76 | if (output[index] === null || output[index] === undefined) { 77 | output[index] = values[index] 78 | } 79 | } 80 | 81 | // Save the newly created `output` for the next call. 82 | values = output 83 | 84 | // Next or done. 85 | if (fn) { 86 | wrap(fn, next)(...output) 87 | } else { 88 | callback(null, ...output) 89 | } 90 | } 91 | } 92 | 93 | /** @type {Use} */ 94 | function use(middelware) { 95 | if (typeof middelware !== 'function') { 96 | throw new TypeError( 97 | 'Expected `middelware` to be a function, not ' + middelware 98 | ) 99 | } 100 | 101 | fns.push(middelware) 102 | return pipeline 103 | } 104 | } 105 | 106 | /** 107 | * Wrap `middleware` into a uniform interface. 108 | * 109 | * You can pass all input to the resulting function. 110 | * `callback` is then called with the output of `middleware`. 111 | * 112 | * If `middleware` accepts more arguments than the later given in input, 113 | * an extra `done` function is passed to it after that input, 114 | * which must be called by `middleware`. 115 | * 116 | * The first value in `input` is the main input value. 117 | * All other input values are the rest input values. 118 | * The values given to `callback` are the input values, 119 | * merged with every non-nullish output value. 120 | * 121 | * * if `middleware` throws an error, 122 | * returns a promise that is rejected, 123 | * or calls the given `done` function with an error, 124 | * `callback` is called with that error 125 | * * if `middleware` returns a value or returns a promise that is resolved, 126 | * that value is the main output value 127 | * * if `middleware` calls `done`, 128 | * all non-nullish values except for the first one (the error) overwrite the 129 | * output values 130 | * 131 | * @param {Middleware} middleware 132 | * Function to wrap. 133 | * @param {Callback} callback 134 | * Callback called with the output of `middleware`. 135 | * @returns {Run} 136 | * Wrapped middleware. 137 | */ 138 | export function wrap(middleware, callback) { 139 | /** @type {boolean} */ 140 | let called 141 | 142 | return wrapped 143 | 144 | /** 145 | * Call `middleware`. 146 | * @this {any} 147 | * @param {Array} parameters 148 | * @returns {void} 149 | */ 150 | function wrapped(...parameters) { 151 | const fnExpectsCallback = middleware.length > parameters.length 152 | /** @type {any} */ 153 | let result 154 | 155 | if (fnExpectsCallback) { 156 | parameters.push(done) 157 | } 158 | 159 | try { 160 | result = middleware.apply(this, parameters) 161 | } catch (error) { 162 | const exception = /** @type {Error} */ (error) 163 | 164 | // Well, this is quite the pickle. 165 | // `middleware` received a callback and called it synchronously, but that 166 | // threw an error. 167 | // The only thing left to do is to throw the thing instead. 168 | if (fnExpectsCallback && called) { 169 | throw exception 170 | } 171 | 172 | return done(exception) 173 | } 174 | 175 | if (!fnExpectsCallback) { 176 | if (result && result.then && typeof result.then === 'function') { 177 | result.then(then, done) 178 | } else if (result instanceof Error) { 179 | done(result) 180 | } else { 181 | then(result) 182 | } 183 | } 184 | } 185 | 186 | /** 187 | * Call `callback`, only once. 188 | * 189 | * @type {Callback} 190 | */ 191 | function done(error, ...output) { 192 | if (!called) { 193 | called = true 194 | callback(error, ...output) 195 | } 196 | } 197 | 198 | /** 199 | * Call `done` with one value. 200 | * 201 | * @param {any} [value] 202 | */ 203 | function then(value) { 204 | done(null, value) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # trough 2 | 3 | [![Build][badge-build-image]][badge-build-url] 4 | [![Coverage][badge-coverage-image]][badge-coverage-url] 5 | [![Downloads][badge-downloads-image]][badge-downloads-url] 6 | [![Size][badge-size-image]][badge-size-url] 7 | 8 | `trough` is middleware. 9 | 10 | ## Contents 11 | 12 | * [What is this?](#what-is-this) 13 | * [When should I use this?](#when-should-i-use-this) 14 | * [Install](#install) 15 | * [Use](#use) 16 | * [API](#api) 17 | * [`trough()`](#trough-1) 18 | * [`wrap(middleware, callback)`](#wrapmiddleware-callback) 19 | * [`Callback`](#callback) 20 | * [`Middleware`](#middleware) 21 | * [`Pipeline`](#pipeline) 22 | * [`Run`](#run) 23 | * [`Use`](#use-1) 24 | * [Compatibility](#compatibility) 25 | * [Security](#security) 26 | * [Contribute](#contribute) 27 | * [License](#license) 28 | 29 | ## What is this? 30 | 31 | `trough` is like [`ware`][github-segmentio-ware] with less sugar. 32 | Middleware functions can also change the input of the next. 33 | 34 | The word **trough** (`/trôf/`) means a channel used to convey a liquid. 35 | 36 | ## When should I use this? 37 | 38 | You can use this package when you’re building something that accepts “plugins”, 39 | which are functions, that can be sync or async, promises or callbacks. 40 | 41 | ## Install 42 | 43 | This package is [ESM only][github-gist-esm]. 44 | In Node.js (version 16+), 45 | install with [npm][npm-install]: 46 | 47 | ```sh 48 | npm install trough 49 | ``` 50 | 51 | In Deno with [`esm.sh`][esm-sh]: 52 | 53 | ```js 54 | import {trough, wrap} from 'https://esm.sh/trough@2' 55 | ``` 56 | 57 | In browsers with [`esm.sh`][esm-sh]: 58 | 59 | ```html 60 | 63 | ``` 64 | 65 | ## Use 66 | 67 | ```js 68 | import fs from 'node:fs' 69 | import path from 'node:path' 70 | import process from 'node:process' 71 | import {trough} from 'trough' 72 | 73 | const pipeline = trough() 74 | .use(function (fileName) { 75 | console.log('Checking… ' + fileName) 76 | }) 77 | .use(function (fileName) { 78 | return path.join(process.cwd(), fileName) 79 | }) 80 | .use(function (filePath, next) { 81 | fs.stat(filePath, function (error, stats) { 82 | next(error, {filePath, stats}) 83 | }) 84 | }) 85 | .use(function (ctx, next) { 86 | if (ctx.stats.isFile()) { 87 | fs.readFile(ctx.filePath, next) 88 | } else { 89 | next(new Error('Expected file')) 90 | } 91 | }) 92 | 93 | pipeline.run('readme.md', console.log) 94 | pipeline.run('node_modules', console.log) 95 | ``` 96 | 97 | Yields: 98 | 99 | ```txt 100 | Checking… readme.md 101 | Checking… node_modules 102 | Error: Expected file 103 | at ~/example.js:22:12 104 | at wrapped (~/node_modules/trough/index.js:111:16) 105 | at next (~/node_modules/trough/index.js:62:23) 106 | at done (~/node_modules/trough/index.js:145:7) 107 | at ~/example.js:15:7 108 | at FSReqCallback.oncomplete (node:fs:199:5) 109 | null 110 | ``` 111 | 112 | ## API 113 | 114 | This package exports the identifiers 115 | [`trough`][api-trough] and 116 | [`wrap`][api-wrap]. 117 | There is no default export. 118 | 119 | It exports the [TypeScript][] types 120 | [`Callback`][api-callback], 121 | [`Middleware`][api-middleware], 122 | [`Pipeline`][api-pipeline], 123 | [`Run`][api-run], 124 | and [`Use`][api-use]. 125 | 126 | ### `trough()` 127 | 128 | Create new middleware. 129 | 130 | ###### Parameters 131 | 132 | There are no parameters. 133 | 134 | ###### Returns 135 | 136 | [`Pipeline`][api-pipeline]. 137 | 138 | ### `wrap(middleware, callback)` 139 | 140 | Wrap `middleware` into a uniform interface. 141 | 142 | You can pass all input to the resulting function. 143 | `callback` is then called with the output of `middleware`. 144 | 145 | If `middleware` accepts more arguments than the later given in input, 146 | an extra `done` function is passed to it after that input, 147 | which must be called by `middleware`. 148 | 149 | The first value in `input` is the main input value. 150 | All other input values are the rest input values. 151 | The values given to `callback` are the input values, 152 | merged with every non-nullish output value. 153 | 154 | * if `middleware` throws an error, 155 | returns a promise that is rejected, 156 | or calls the given `done` function with an error, 157 | `callback` is called with that error 158 | * if `middleware` returns a value or returns a promise that is resolved, 159 | that value is the main output value 160 | * if `middleware` calls `done`, 161 | all non-nullish values except for the first one (the error) overwrite the 162 | output values 163 | 164 | ###### Parameters 165 | 166 | * `middleware` ([`Middleware`][api-middleware]) 167 | — function to wrap 168 | * `callback` ([`Callback`][api-callback]) 169 | — callback called with the output of `middleware` 170 | 171 | ###### Returns 172 | 173 | Wrapped middleware ([`Run`][api-run]). 174 | 175 | ### `Callback` 176 | 177 | Callback function (TypeScript type). 178 | 179 | ###### Parameters 180 | 181 | * `error` (`Error`, optional) 182 | — error, if any 183 | * `...output` (`Array`, optional) 184 | — output values 185 | 186 | ###### Returns 187 | 188 | Nothing (`undefined`). 189 | 190 | ### `Middleware` 191 | 192 | A middleware function called with the output of its predecessor (TypeScript 193 | type). 194 | 195 | ###### Synchronous 196 | 197 | If `fn` returns or throws an error, 198 | the pipeline fails and `done` is called with that error. 199 | 200 | If `fn` returns a value (neither `null` nor `undefined`), 201 | the first `input` of the next function is set to that value 202 | (all other `input` is passed through). 203 | 204 | The following example shows how returning an error stops the pipeline: 205 | 206 | ```js 207 | import {trough} from 'trough' 208 | 209 | trough() 210 | .use(function (thing) { 211 | return new Error('Got: ' + thing) 212 | }) 213 | .run('some value', console.log) 214 | ``` 215 | 216 | Yields: 217 | 218 | ```txt 219 | Error: Got: some value 220 | at ~/example.js:5:12 221 | … 222 | ``` 223 | 224 | The following example shows how throwing an error stops the pipeline: 225 | 226 | ```js 227 | import {trough} from 'trough' 228 | 229 | trough() 230 | .use(function (thing) { 231 | throw new Error('Got: ' + thing) 232 | }) 233 | .run('more value', console.log) 234 | ``` 235 | 236 | Yields: 237 | 238 | ```txt 239 | Error: Got: more value 240 | at ~/example.js:5:11 241 | … 242 | ``` 243 | 244 | The following example shows how the first output can be modified: 245 | 246 | ```js 247 | import {trough} from 'trough' 248 | 249 | trough() 250 | .use(function (thing) { 251 | return 'even ' + thing 252 | }) 253 | .run('more value', 'untouched', console.log) 254 | ``` 255 | 256 | Yields: 257 | 258 | ```txt 259 | null 'even more value' 'untouched' 260 | ``` 261 | 262 | ###### Promise 263 | 264 | If `fn` returns a promise, 265 | and that promise rejects, 266 | the pipeline fails and `done` is called with the rejected value. 267 | 268 | If `fn` returns a promise, 269 | and that promise resolves with a value (neither `null` nor `undefined`), 270 | the first `input` of the next function is set to that value (all other `input` 271 | is passed through). 272 | 273 | The following example shows how rejecting a promise stops the pipeline: 274 | 275 | ```js 276 | import {trough} from 'trough' 277 | 278 | trough() 279 | .use(function (thing) { 280 | return new Promise(function (resolve, reject) { 281 | reject('Got: ' + thing) 282 | }) 283 | }) 284 | .run('thing', console.log) 285 | ``` 286 | 287 | Yields: 288 | 289 | ```txt 290 | Got: thing 291 | ``` 292 | 293 | The following example shows how the input isn’t touched by resolving to `null`. 294 | 295 | ```js 296 | import {trough} from 'trough' 297 | 298 | trough() 299 | .use(function () { 300 | return new Promise(function (resolve) { 301 | setTimeout(function () { 302 | resolve(null) 303 | }, 100) 304 | }) 305 | }) 306 | .run('Input', console.log) 307 | ``` 308 | 309 | Yields: 310 | 311 | ```txt 312 | null 'Input' 313 | ``` 314 | 315 | ###### Asynchronous 316 | 317 | If `fn` accepts one more argument than the given `input`, 318 | a `next` function is given (after the input). 319 | `next` must be called, but doesn’t have to be called async. 320 | 321 | If `next` is given a value (neither `null` nor `undefined`) as its first 322 | argument, 323 | the pipeline fails and `done` is called with that value. 324 | 325 | If `next` is given no value (either `null` or `undefined`) as the first 326 | argument, 327 | all following non-nullish values change the input of the following 328 | function, 329 | and all nullish values default to the `input`. 330 | 331 | The following example shows how passing a first argument stops the pipeline: 332 | 333 | ```js 334 | import {trough} from 'trough' 335 | 336 | trough() 337 | .use(function (thing, next) { 338 | next(new Error('Got: ' + thing)) 339 | }) 340 | .run('thing', console.log) 341 | ``` 342 | 343 | Yields: 344 | 345 | ```txt 346 | Error: Got: thing 347 | at ~/example.js:5:10 348 | ``` 349 | 350 | The following example shows how more values than the input are passed. 351 | 352 | ```js 353 | import {trough} from 'trough' 354 | 355 | trough() 356 | .use(function (thing, next) { 357 | setTimeout(function () { 358 | next(null, null, 'values') 359 | }, 100) 360 | }) 361 | .run('some', console.log) 362 | ``` 363 | 364 | Yields: 365 | 366 | ```txt 367 | null 'some' 'values' 368 | ``` 369 | 370 | ###### Parameters 371 | 372 | * `...input` (`Array`, optional) 373 | — input values 374 | 375 | ###### Returns 376 | 377 | Output, promise, etc (`any`). 378 | 379 | ### `Pipeline` 380 | 381 | Pipeline (TypeScript type). 382 | 383 | ###### Properties 384 | 385 | * `run` ([`Run`][api-run]) 386 | — run the pipeline 387 | * `use` ([`Use`][api-use]) 388 | — add middleware 389 | 390 | ### `Run` 391 | 392 | Call all middleware (TypeScript type). 393 | 394 | Calls `done` on completion with either an error or the output of the 395 | last middleware. 396 | 397 | > 👉 **Note**: as the length of input defines whether async functions get a 398 | > `next` function, 399 | > it’s recommended to keep `input` at one value normally. 400 | 401 | ###### Parameters 402 | 403 | * `...input` (`Array`, optional) 404 | — input values 405 | * `done` ([`Callback`][api-callback]) 406 | — callback called when done 407 | 408 | ###### Returns 409 | 410 | Nothing (`undefined`). 411 | 412 | ### `Use` 413 | 414 | Add middleware (TypeScript type). 415 | 416 | ###### Parameters 417 | 418 | * `middleware` ([`Middleware`][api-middleware]) 419 | — middleware function 420 | 421 | ###### Returns 422 | 423 | Current pipeline ([`Pipeline`][api-pipeline]). 424 | 425 | ## Compatibility 426 | 427 | This projects is compatible with maintained versions of Node.js. 428 | 429 | When we cut a new major release, 430 | we drop support for unmaintained versions of Node. 431 | This means we try to keep the current release line, 432 | `trough@2`, 433 | compatible with Node.js 12. 434 | 435 | ## Security 436 | 437 | This package is safe. 438 | 439 | ## Contribute 440 | 441 | Yes please! 442 | See [How to Contribute to Open Source][open-source-guide-contribute]. 443 | 444 | ## License 445 | 446 | [MIT][file-license] © [Titus Wormer][wooorm] 447 | 448 | 449 | 450 | [api-callback]: #callback 451 | 452 | [api-middleware]: #middleware 453 | 454 | [api-pipeline]: #pipeline 455 | 456 | [api-run]: #run 457 | 458 | [api-trough]: #trough 459 | 460 | [api-use]: #use 461 | 462 | [api-wrap]: #wrapmiddleware-callback 463 | 464 | [badge-build-image]: https://github.com/wooorm/trough/workflows/main/badge.svg 465 | 466 | [badge-build-url]: https://github.com/wooorm/trough/actions 467 | 468 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/wooorm/trough.svg 469 | 470 | [badge-coverage-url]: https://codecov.io/github/wooorm/trough 471 | 472 | [badge-downloads-image]: https://img.shields.io/npm/dm/trough.svg 473 | 474 | [badge-downloads-url]: https://www.npmjs.com/package/trough 475 | 476 | [badge-size-image]: https://img.shields.io/bundlejs/size/trough 477 | 478 | [badge-size-url]: https://bundlejs.com/?q=trough 479 | 480 | [npm-install]: https://docs.npmjs.com/cli/install 481 | 482 | [esm-sh]: https://esm.sh 483 | 484 | [file-license]: license 485 | 486 | [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 487 | 488 | [github-segmentio-ware]: https://github.com/segmentio/ware 489 | 490 | [open-source-guide-contribute]: https://opensource.guide/how-to-contribute/ 491 | 492 | [typescript]: https://www.typescriptlang.org 493 | 494 | [wooorm]: https://wooorm.com 495 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('trough').Callback} Callback 3 | */ 4 | 5 | import assert from 'node:assert/strict' 6 | import process from 'node:process' 7 | import test from 'node:test' 8 | import {trough} from 'trough' 9 | 10 | test('trough', async function (t) { 11 | await t.test('should expose the public api', async function () { 12 | assert.deepEqual(Object.keys(await import('trough')).sort(), [ 13 | 'trough', 14 | 'wrap' 15 | ]) 16 | }) 17 | }) 18 | 19 | test('use()', async function (t) { 20 | await t.test('should throw without `fn`', async function () { 21 | const p = trough() 22 | 23 | assert.throws(function () { 24 | // @ts-expect-error: check how missing value is handled. 25 | p.use() 26 | }, /Expected `middelware` to be a function, not undefined/) 27 | }) 28 | 29 | await t.test('should return self', async function () { 30 | const p = trough() 31 | 32 | assert.equal( 33 | p.use(function () {}), 34 | p 35 | ) 36 | }) 37 | }) 38 | 39 | test('synchronous middleware', async function (t) { 40 | await t.test('should pass returned errors to `done`', async function () { 41 | const value = new Error('Foo') 42 | let calls = 0 43 | 44 | trough() 45 | .use(function () { 46 | return value 47 | }) 48 | .run( 49 | /** 50 | * @param {unknown} [error] 51 | * @returns {undefined} 52 | */ 53 | function (error) { 54 | assert.equal(error, value) 55 | calls++ 56 | } 57 | ) 58 | 59 | assert.equal(calls, 1) 60 | }) 61 | 62 | await t.test('should pass thrown errors to `done`', async function () { 63 | const value = new Error('Foo') 64 | let calls = 0 65 | 66 | trough() 67 | .use(function () { 68 | throw value 69 | }) 70 | .run( 71 | /** 72 | * @param {unknown} [error] 73 | * @returns {undefined} 74 | */ 75 | function (error) { 76 | assert.equal(error, value) 77 | calls++ 78 | } 79 | ) 80 | 81 | assert.equal(calls, 1) 82 | }) 83 | 84 | await t.test('should pass values to `fn`s and `done`', async function () { 85 | let calls = 0 86 | 87 | trough() 88 | .use( 89 | /** 90 | * @param {unknown} value 91 | * @returns {undefined} 92 | */ 93 | function (value) { 94 | assert.equal(value, 'some') 95 | } 96 | ) 97 | .run( 98 | 'some', 99 | /** 100 | * @param {unknown} [error] 101 | * @param {unknown} [value] 102 | * @returns {undefined} 103 | */ 104 | function (error, value) { 105 | assert.ifError(error) 106 | assert.equal(value, 'some') 107 | calls++ 108 | } 109 | ) 110 | 111 | assert.equal(calls, 1) 112 | }) 113 | 114 | await t.test( 115 | 'should pass modified values to `fn`s and `done`', 116 | async function () { 117 | let calls = 0 118 | 119 | trough() 120 | .use( 121 | /** 122 | * @param {unknown} [value] 123 | * @returns {string} 124 | */ 125 | function (value) { 126 | assert.equal(value, 'some') 127 | return value + 'thing' 128 | } 129 | ) 130 | .use( 131 | /** 132 | * @param {unknown} [value] 133 | * @returns {string} 134 | */ function (value) { 135 | assert.equal(value, 'something') 136 | return value + ' more' 137 | } 138 | ) 139 | .run( 140 | 'some', 141 | /** 142 | * @param {unknown} [error] 143 | * @param {unknown} [value] 144 | * @returns {undefined} 145 | */ 146 | function (error, value) { 147 | assert.ifError(error) 148 | assert.equal(value, 'something more') 149 | calls++ 150 | } 151 | ) 152 | 153 | assert.equal(calls, 1) 154 | } 155 | ) 156 | }) 157 | 158 | test('promise middleware', async function (t) { 159 | await t.test('should pass rejected errors to `done`', async function () { 160 | const value = new Error('Foo') 161 | let calls = 0 162 | 163 | /** @type {Promise} */ 164 | const promise = new Promise(function (resolve) { 165 | trough() 166 | .use(function () { 167 | return new Promise(function (_, reject) { 168 | reject(value) 169 | }) 170 | }) 171 | .run( 172 | /** 173 | * @param {unknown} [error] 174 | * @returns {undefined} 175 | */ 176 | function (error) { 177 | assert.equal(error, value) 178 | calls++ 179 | resolve(undefined) 180 | } 181 | ) 182 | }) 183 | 184 | assert.equal(calls, 0) 185 | 186 | await promise 187 | 188 | assert.equal(calls, 1) 189 | }) 190 | 191 | await t.test('should pass values to `fn`s and `done`', async function () { 192 | await new Promise(function (resolve) { 193 | trough() 194 | .use( 195 | /** 196 | * @param {unknown} value 197 | * @returns {Promise} 198 | */ 199 | function (value) { 200 | assert.equal(value, 'some') 201 | 202 | return new Promise(function (resolve) { 203 | resolve(undefined) 204 | }) 205 | } 206 | ) 207 | .run( 208 | 'some', 209 | /** 210 | * @param {unknown} [error] 211 | * @param {unknown} [value] 212 | * @returns {undefined} 213 | */ 214 | function (error, value) { 215 | assert.ifError(error) 216 | assert.equal(value, 'some') 217 | resolve(undefined) 218 | } 219 | ) 220 | }) 221 | }) 222 | 223 | await t.test( 224 | 'should pass modified values to `fn`s and `done`', 225 | async function () { 226 | await new Promise(function (resolve) { 227 | trough() 228 | .use( 229 | /** 230 | * @param {unknown} [value] 231 | * @returns {Promise} 232 | */ 233 | function (value) { 234 | assert.equal(value, 'some') 235 | 236 | return new Promise(function (resolve) { 237 | resolve(value + 'thing') 238 | }) 239 | } 240 | ) 241 | .use( 242 | /** 243 | * @param {unknown} [value] 244 | * @returns {Promise} 245 | */ 246 | function (value) { 247 | assert.equal(value, 'something') 248 | 249 | return new Promise(function (resolve) { 250 | resolve(value + ' more') 251 | }) 252 | } 253 | ) 254 | .run( 255 | 'some', 256 | /** 257 | * @param {unknown} [error] 258 | * @param {unknown} [value] 259 | * @returns {undefined} 260 | */ 261 | function (error, value) { 262 | assert.ifError(error) 263 | assert.equal(value, 'something more') 264 | resolve(undefined) 265 | } 266 | ) 267 | }) 268 | } 269 | ) 270 | 271 | await t.test('should support thenables', async function () { 272 | // See: 273 | let calls = 0 274 | 275 | /** @type {Promise} */ 276 | const promise = new Promise(function (resolve) { 277 | trough() 278 | .use(function () { 279 | return { 280 | /** 281 | * @param {(value: unknown) => void} resolve 282 | */ 283 | // eslint-disable-next-line unicorn/no-thenable 284 | then(resolve) { 285 | setTimeout(function () { 286 | resolve(42) 287 | }) 288 | } 289 | } 290 | }) 291 | .run( 292 | /** 293 | * @param {unknown} [error] 294 | * @param {unknown} [value] 295 | * @returns {undefined} 296 | */ 297 | function (error, value) { 298 | assert.equal(error, null) 299 | assert.equal(value, 42) 300 | calls++ 301 | resolve(undefined) 302 | } 303 | ) 304 | }) 305 | 306 | assert.equal(calls, 0) 307 | 308 | await promise 309 | 310 | assert.equal(calls, 1) 311 | }) 312 | }) 313 | 314 | test('asynchronous middleware', async function (t) { 315 | const value = new Error('Foo') 316 | 317 | await t.test('should pass given errors to `done`', async function () { 318 | await new Promise(function (resolve) { 319 | trough() 320 | .use( 321 | /** 322 | * @param {Callback} next 323 | * @returns {undefined} 324 | */ 325 | function (next) { 326 | next(value) 327 | } 328 | ) 329 | .run( 330 | /** 331 | * @param {unknown} [error] 332 | * @returns {undefined} 333 | */ 334 | function (error) { 335 | assert.equal(error, value) 336 | resolve(undefined) 337 | } 338 | ) 339 | }) 340 | }) 341 | 342 | await t.test('should pass async given errors to `done`', async function () { 343 | await new Promise(function (resolve) { 344 | trough() 345 | .use( 346 | /** 347 | * @param {Callback} next 348 | * @returns {undefined} 349 | */ 350 | function (next) { 351 | setImmediate(function () { 352 | next(value) 353 | }) 354 | } 355 | ) 356 | .run( 357 | /** 358 | * @param {unknown} [error] 359 | * @returns {undefined} 360 | */ 361 | function (error) { 362 | assert.equal(error, value) 363 | resolve(undefined) 364 | } 365 | ) 366 | }) 367 | }) 368 | 369 | await t.test('should ignore multiple sync `next` calls', async function () { 370 | await new Promise(function (resolve) { 371 | trough() 372 | .use( 373 | /** 374 | * @param {Callback} next 375 | * @returns {undefined} 376 | */ 377 | function (next) { 378 | next(value) 379 | next(new Error('Other')) 380 | } 381 | ) 382 | .run( 383 | /** 384 | * @param {unknown} [error] 385 | * @returns {undefined} 386 | */ 387 | function (error) { 388 | assert.equal(error, value) 389 | resolve(undefined) 390 | } 391 | ) 392 | }) 393 | }) 394 | 395 | await t.test('should ignore multiple async `next` calls', async function () { 396 | await new Promise(function (resolve) { 397 | trough() 398 | .use( 399 | /** 400 | * @param {Callback} next 401 | * @returns {undefined} 402 | */ 403 | function (next) { 404 | setImmediate(function () { 405 | next(value) 406 | setImmediate(function () { 407 | next(new Error('Other')) 408 | }) 409 | }) 410 | } 411 | ) 412 | .run( 413 | /** 414 | * @param {unknown} [error] 415 | * @returns {undefined} 416 | */ 417 | function (error) { 418 | assert.equal(error, value) 419 | resolve(undefined) 420 | } 421 | ) 422 | }) 423 | }) 424 | 425 | await t.test('should pass values to `fn`s and `done`', async function () { 426 | await new Promise(function (resolve) { 427 | trough() 428 | .use( 429 | /** 430 | * @param {unknown} value 431 | * @param {Callback} next 432 | * @returns {undefined} 433 | */ 434 | function (value, next) { 435 | assert.equal(value, 'some') 436 | setImmediate(next) 437 | } 438 | ) 439 | .run( 440 | 'some', 441 | /** 442 | * @param {unknown} [error] 443 | * @param {unknown} [value] 444 | * @returns {undefined} 445 | */ 446 | function (error, value) { 447 | assert.ifError(error) 448 | assert.equal(value, 'some') 449 | resolve(undefined) 450 | } 451 | ) 452 | }) 453 | }) 454 | 455 | await t.test( 456 | 'should pass modified values to `fn`s and `done`', 457 | async function () { 458 | await new Promise(function (resolve) { 459 | trough() 460 | .use( 461 | /** 462 | * @param {unknown} value 463 | * @param {Callback} next 464 | * @returns {undefined} 465 | */ 466 | function (value, next) { 467 | assert.equal(value, 'some') 468 | 469 | setImmediate(function () { 470 | next(undefined, value + 'thing') 471 | }) 472 | } 473 | ) 474 | .use( 475 | /** 476 | * @param {unknown} value 477 | * @param {Callback} next 478 | * @returns {undefined} 479 | */ 480 | function (value, next) { 481 | assert.equal(value, 'something') 482 | 483 | setImmediate(function () { 484 | next(undefined, value + ' more') 485 | }) 486 | } 487 | ) 488 | .run( 489 | 'some', 490 | /** 491 | * @param {unknown} [error] 492 | * @param {unknown} [value] 493 | * @returns {undefined} 494 | */ 495 | function (error, value) { 496 | assert.ifError(error) 497 | assert.equal(value, 'something more') 498 | resolve(undefined) 499 | } 500 | ) 501 | }) 502 | } 503 | ) 504 | }) 505 | 506 | test('run()', async function (t) { 507 | // Remove the crash handlers by the test runner. 508 | const before = process.listeners('uncaughtException') 509 | 510 | for (const listener of before) { 511 | process.off('uncaughtException', listener) 512 | } 513 | 514 | await t.test('should throw if `done` is not a function', async function () { 515 | assert.throws(function () { 516 | trough().run() 517 | }, /^TypeError: Expected function as last argument, not undefined$/) 518 | }) 519 | 520 | await t.test('should work all together', async function () { 521 | await new Promise(function (resolve) { 522 | trough() 523 | .use( 524 | /** 525 | * @param {unknown} [value] 526 | * @returns {string} 527 | */ 528 | function (value) { 529 | assert.equal(value, 'some') 530 | 531 | return value + 'thing' 532 | } 533 | ) 534 | .use( 535 | /** 536 | * @param {unknown} [value] 537 | * @returns {Promise} 538 | */ 539 | function (value) { 540 | assert.equal(value, 'something') 541 | 542 | return new Promise(function (resolve) { 543 | resolve(value + ' more') 544 | }) 545 | } 546 | ) 547 | .use( 548 | /** 549 | * @param {unknown} value 550 | * @param {Callback} next 551 | * @returns {undefined} 552 | */ 553 | function (value, next) { 554 | assert.equal(value, 'something more') 555 | 556 | setImmediate(function () { 557 | next(undefined, value + '.') 558 | }) 559 | } 560 | ) 561 | .run( 562 | 'some', 563 | /** 564 | * @param {unknown} [error] 565 | * @param {unknown} [value] 566 | * @returns {undefined} 567 | */ 568 | function (error, value) { 569 | assert.ifError(error) 570 | assert.equal(value, 'something more.') 571 | resolve(undefined) 572 | } 573 | ) 574 | }) 575 | }) 576 | 577 | await t.test('should throw errors thrown from `done` (#1)', function () { 578 | assert.throws(function () { 579 | trough().run(function () { 580 | throw new Error('alpha') 581 | }) 582 | }, /^Error: alpha$/) 583 | }) 584 | 585 | await t.test( 586 | 'should throw errors thrown from `done` (#2)', 587 | async function () { 588 | await new Promise(function (resolve) { 589 | process.once('uncaughtException', function (error) { 590 | assert.equal(String(error), 'Error: bravo') 591 | resolve(undefined) 592 | }) 593 | 594 | trough() 595 | .use( 596 | /** 597 | * @param {Callback} next 598 | * @returns {undefined} 599 | */ 600 | function (next) { 601 | setImmediate(next) 602 | } 603 | ) 604 | .run(function () { 605 | throw new Error('bravo') 606 | }) 607 | }) 608 | } 609 | ) 610 | 611 | await t.test( 612 | 'should rethrow errors thrown from `done` (#1)', 613 | async function () { 614 | await new Promise(function (resolve) { 615 | process.once('uncaughtException', function (error) { 616 | assert.equal(String(error), 'Error: bravo') 617 | resolve(undefined) 618 | }) 619 | 620 | trough() 621 | .use( 622 | /** 623 | * @param {Callback} next 624 | * @returns {undefined} 625 | */ 626 | function (next) { 627 | setImmediate(function () { 628 | next(new Error('bravo')) 629 | }) 630 | } 631 | ) 632 | .run( 633 | /** 634 | * @param {unknown} [error] 635 | * @returns {undefined} 636 | */ 637 | function (error) { 638 | throw error 639 | } 640 | ) 641 | }) 642 | } 643 | ) 644 | 645 | await t.test( 646 | 'should rethrow errors thrown from `done` (#2)', 647 | async function () { 648 | try { 649 | await new Promise(function () { 650 | trough() 651 | .use(function () { 652 | throw new Error('bravo') 653 | }) 654 | .run( 655 | /** 656 | * @param {unknown} [error] 657 | * @returns {undefined} 658 | */ 659 | function (error) { 660 | throw error 661 | } 662 | ) 663 | }) 664 | } catch (error) { 665 | assert.equal(String(error), 'Error: bravo') 666 | } 667 | } 668 | ) 669 | 670 | await t.test( 671 | 'should not swallow uncaught exceptions (#1)', 672 | async function () { 673 | await new Promise(function (resolve) { 674 | process.once('uncaughtException', function (error) { 675 | assert.equal(String(error), 'Error: charlie') 676 | resolve(undefined) 677 | }) 678 | trough() 679 | .use( 680 | /** 681 | * @param {Callback} next 682 | * @returns {undefined} 683 | */ 684 | function (next) { 685 | setImmediate(next) 686 | } 687 | ) 688 | .run(function () { 689 | setImmediate(function () { 690 | throw new Error('charlie') 691 | }) 692 | }) 693 | }) 694 | } 695 | ) 696 | 697 | await t.test( 698 | 'should not swallow uncaught exceptions (#2)', 699 | async function () { 700 | await new Promise(function (resolve) { 701 | process.once('uncaughtException', function (error) { 702 | assert.equal(String(error), 'Error: charlie') 703 | resolve(undefined) 704 | }) 705 | trough() 706 | .use( 707 | /** 708 | * @param {Callback} next 709 | * @returns {undefined} 710 | */ 711 | function (next) { 712 | setImmediate(function () { 713 | next(new Error('charlie')) 714 | }) 715 | } 716 | ) 717 | .run( 718 | /** 719 | * @param {unknown} [error] 720 | * @returns {undefined} 721 | */ 722 | function (error) { 723 | setImmediate(function () { 724 | throw error 725 | }) 726 | } 727 | ) 728 | }) 729 | } 730 | ) 731 | 732 | await t.test( 733 | 'should not swallow errors in the `done` handler', 734 | async function () { 735 | const value = new Error('hotel') 736 | 737 | try { 738 | await new Promise(function () { 739 | trough() 740 | .use( 741 | /** 742 | * @param {Callback} next 743 | * @returns {undefined} 744 | */ 745 | function (next) { 746 | next(value) 747 | } 748 | ) 749 | .run( 750 | /** 751 | * @param {unknown} [error] 752 | * @returns {undefined} 753 | */ 754 | function (error) { 755 | throw error 756 | } 757 | ) 758 | }) 759 | } catch (error) { 760 | assert.equal(error, value, 'should pass the error') 761 | } 762 | } 763 | ) 764 | 765 | // Add the test runner listeners. 766 | for (const listener of before) { 767 | process.on('uncaughtException', listener) 768 | } 769 | }) 770 | --------------------------------------------------------------------------------