├── .github ├── FUNDING.yaml ├── ISSUE_TEMPLATE │ ├── bug-report---.md │ └── feature-request---.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── pre-release.yml │ └── release.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── lib └── index.js ├── package.json ├── test ├── index.test-d.ts └── index.test.js └── tsconfig.json /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: metcoder95 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report \U0001F41B" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature request \U0001F916" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | node: [18.x, 20.x, 21.x] 10 | name: Node ${{ matrix.node }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup node 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ matrix.node }} 17 | - run: npm install 18 | - run: npm run test:ci 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '30 12 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Code Q Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript', 'typescript' ] 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v3 44 | with: 45 | languages: ${{ matrix.language }} 46 | # If you wish to specify custom queries, you can do so here or in a config file. 47 | # By default, queries listed here will override any specified in a config file. 48 | # Prefix the list here with "+" to use these queries and those in the config file. 49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 50 | 51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 52 | # If this step fails, then you should remove it and run the build manually (see below) 53 | - name: Autobuild 54 | uses: github/codeql-action/autobuild@v3 55 | 56 | # ℹ️ Command-line programs to run using the OS shell. 57 | # 📚 https://git.io/JvXDl 58 | 59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 60 | # and modify them (or add more) to build your code if your project 61 | # uses a compiled language 62 | 63 | #- run: | 64 | # make bootstrap 65 | # make release 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@v3 69 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Pre-Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*-beta**' 6 | - 'v*.*.*-rc**' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | name: Pre-Release Creation 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: ncipollo/release-action@v1 17 | with: 18 | prerelease: true 19 | bodyFile: "CHANGELOG.md" 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | name: Release Creation 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: ncipollo/release-action@v1 16 | with: 17 | bodyFile: "CHANGELOG.md" 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Not necessary for libs 107 | package-lock.json -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run test 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run test:ci 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Not necessary for libs 107 | package-lock.json 108 | 109 | # Husky 110 | # .husky/ 111 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.0.0](https://github.com/metcoder95/fastify-racing/compare/v1.1.0...v2.0.0) (2023-12-24) 6 | 7 | 8 | ### ⚠ BREAKING CHANGES 9 | 10 | * drop support for older Node 11 | 12 | ### Features 13 | 14 | * drop support for older Node ([78662fd](https://github.com/metcoder95/fastify-racing/commit/78662fda1b3f5b4ea093b744dd8b205c237b86d7)) 15 | 16 | ## [1.1.0](https://github.com/metcoder95/fastify-racing/compare/v1.0.3...v1.1.0) (2022-09-28) 17 | 18 | 19 | ### Features 20 | 21 | * update dependencies ([423ea99](https://github.com/metcoder95/fastify-racing/commit/423ea997bc2193b6b4bd64f74b0963500fd88d7d)) 22 | 23 | ### [1.0.3](https://github.com/metcoder95/fastify-racing/compare/v1.0.0...v1.0.3) (2022-06-28) 24 | 25 | ### [1.0.2](https://github.com/metcoder95/fastify-racing/compare/v1.0.0...v1.0.2) (2022-06-28) 26 | 27 | ### [1.0.1](https://github.com/metcoder95/fastify-racing/compare/v1.0.0...v1.0.1) (2022-06-28) 28 | 29 | ## [1.0.0](https://github.com/metcoder95/fastify-racing/compare/v2.0.0...v1.0.0) (2022-05-19) 30 | 31 | ## 1.0.0-rc.0 (2022-05-17) 32 | 33 | 34 | ### ⚠ BREAKING CHANGES 35 | 36 | * v1 (#1) 37 | 38 | ### Features 39 | 40 | * add types ([#3](https://github.com/metcoder95/fastify-racing/issues/3)) ([035c117](https://github.com/metcoder95/fastify-racing/commit/035c11700b229237facff79f7eec46d11a35137c)) 41 | * v1 ([#1](https://github.com/metcoder95/fastify-racing/issues/1)) ([39f342e](https://github.com/metcoder95/fastify-racing/commit/39f342e348224346bc1c2e37306c284e76d43b7e)) 42 | 43 | ## 1.0.0 (2022-05-17) 44 | 45 | 46 | ### ⚠ BREAKING CHANGES 47 | 48 | * v1 (#1) 49 | 50 | ### Features 51 | 52 | * add types ([#3](https://github.com/metcoder95/fastify-racing/issues/3)) ([035c117](https://github.com/metcoder95/fastify-racing/commit/035c11700b229237facff79f7eec46d11a35137c)) 53 | * v1 ([#1](https://github.com/metcoder95/fastify-racing/issues/1)) ([39f342e](https://github.com/metcoder95/fastify-racing/commit/39f342e348224346bc1c2e37306c284e76d43b7e)) 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Carlos Fuentes 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastify-racing 2 | 3 | [![CI](https://github.com/metcoder95/fastify-racing/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/metcoder95/fastify-racing/actions/workflows/ci.yml) [![CodeQL](https://github.com/metcoder95/fastify-racing/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/metcoder95/fastify-racing/actions/workflows/codeql-analysis.yml) [![version](https://badge.fury.io/js/fastify-racing.svg)](https://badge.fury.io/js/fastify-racing) 4 | 5 | --- 6 | 7 | `fastify-racing` is a plugin which allows you handle possible client request abortions by exposing an `AbortSignal` instance that will be aborted just and only when the client has closed the request abruptly (e.g. by closing the browser tab). 8 | 9 | ## How it works? 10 | 11 | On every request and after a first invocation, the plugin well schedule event listeners to the [`close`](https://nodejs.org/api/net.html#event-close_1) event triggered by the [Socket](https://nodejs.org/api/net.html#new-netsocketoptions) instance attached to the request object. 12 | 13 | Along with that, the plugin will instanciate and cache an [`AbortController`](https://nodejs.org/api/globals.html#class-abortcontroller) instance for each request. 14 | 15 | When the `close` event is triggered, the plugin will check if the [`AbortSignal`](https://nodejs.org/api/globals.html#class-abortsignal) instance is already aborted, and if not will abort it using the `AbortController` instance. 16 | 17 | Is guaranteed that one and just one `AbortController` and `AbortSignal` will be made per request. 18 | 19 | If the request was not aborted during its lifetime, the plugin will remove the `AbortController` and `AbortSignal` from the cache. This by scheduling a hook-handler on the hook `onResponse`. 20 | 21 | If the request aborted, the same hook will be used for cleaning resources. 22 | 23 | A [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) is used under the hood for caching, ensuring that the `AbortController` and `AbortSignal` instances can be unlinked if not needed anymore, and for instance GC'ed. 24 | 25 | ## Setup 26 | 27 | Install by running `npm install fastify-racing`. 28 | 29 | Then register the plugin to your fastify instance: 30 | 31 | ```js 32 | const fastify = require('fastify')({ 33 | logger: true 34 | }) 35 | 36 | fastify.register(require('fastify-racing'), { 37 | handleError: true, 38 | }) 39 | ``` 40 | 41 | ### Options 42 | 43 | **On Setup** 44 | 45 | - `handleError`: Indicates to the pluging if an event listener to the Socket [`error`](https://nodejs.org/api/net.html#event-error_1) event should be attached or not. Default `true`. 46 | 47 | - `onRequestClosed`: Default callback to be used of none is passed during runtime It will receive as argument the event object similar to the `abort` event handler. Default `null` 48 | 49 | 50 | ## How to use it? 51 | 52 | There are two ways to use this plugin: 53 | 54 | ### Promise 55 | 56 | It will return a promise that will be resolved when the request is aborted. It will be resolved with the result of the [`abort`](https://nodejs.org/api/globals.html#event-abort) event object of the `AbortSignal` instance. This only if no `cb` has been passed as argument. 57 | 58 | It supports an object as argument: 59 | 60 | - `opts.handleError`: [Optional] Indicates to the plugin to ignore or listen to the Socket [`error`](https://nodejs.org/api/net.html#event-error_1) event. Default to `pluginOption.handleError` passed when registering the pluging or `false`. 61 | 62 | **JavaScript** 63 | 64 | ```js 65 | app.get('/', async (req, _reply) => { 66 | const signal = req.race() 67 | const result = await Promise.race([signal, asyncOp(signal)]) 68 | 69 | if (result.type === 'aborted') return '' 70 | else return `${result}-world` 71 | }) 72 | ``` 73 | 74 | **TypeScript** 75 | ```ts 76 | app.post('/', (request: FastifyRequest, reply: FastifyReply) => { 77 | const signal = req.race() 78 | const result: AbortEvent | unknown = await Promise.race([signal, asyncOp(signal)]) 79 | 80 | if ((result).type === 'aborted') return '' 81 | else return `${result}-world` 82 | }); 83 | ``` 84 | 85 | 86 | ### Callback 87 | 88 | If a callback is provided, no promise will be scheduled/returned during the lifetime of the request. 89 | 90 | - `cb`: Similar signature as `onRequestClosed`. Default `undefined` or to `onRequestClosed` if passed when registering the plugin. 91 | 92 | **JavaScript** 93 | 94 | ```js 95 | app.get('/', (req, reply) => { 96 | const signal = req.race((evt) => { 97 | const result = result.type === 'aborted' ? '' : `${result}-world` 98 | 99 | reply.send(result) 100 | }) 101 | }) 102 | ``` 103 | 104 | **TypeScript** 105 | 106 | ```ts 107 | app.post('/', (request: FastifyRequest, reply: FastifyReply) => { 108 | const signal = req.race((evt: AbortEvent) => { 109 | reply.send('') 110 | }) 111 | }); 112 | ``` 113 | 114 | ## Type Definitions 115 | 116 | ```ts 117 | interface AbortEvent { 118 | type: 'abort' | string; 119 | reason?: FastifyError | Error 120 | } 121 | 122 | interface FastifyRacing { 123 | handleError?: boolean; 124 | onRequestClosed?: (evt: AbortEvent) => void; 125 | } 126 | 127 | interface FastifyInstance { 128 | race(cb: FastifyRacing['onRequestClosed']): void 129 | race(opts: Omit): Promise 130 | race(): Promise 131 | } 132 | ``` 133 | 134 | 135 | > See [test](test/index.test.js) for more examples. -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { FastifyPluginCallback } from 'fastify' 3 | import { FastifyError } from '@fastify/error' 4 | 5 | export interface FastifyRacingSignal extends AbortSignal { 6 | then: ( 7 | onFulfilled?: (value: AbortEvent) => void | PromiseLike, 8 | onRejected?: (reason: Error | FastifyError) => void | PromiseLike 9 | ) => void | Promise 10 | } 11 | 12 | export interface AbortEvent { 13 | type: 'abort' | string 14 | reason?: FastifyError | Error 15 | } 16 | 17 | export interface FastifyRacingOptions { 18 | handleError?: boolean 19 | onRequestClosed?: ((evt: AbortEvent) => void) | null 20 | } 21 | 22 | declare module 'fastify' { 23 | interface FastifyRequest { 24 | race(cb: FastifyRacingOptions['onRequestClosed']): void 25 | race(opts: Omit): FastifyRacingSignal 26 | race(opts: Omit): Promise 27 | race(): FastifyRacingSignal 28 | race(): Promise 29 | } 30 | } 31 | 32 | declare const FastifyRacing: FastifyPluginCallback 33 | 34 | export default FastifyRacing 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fp = require('fastify-plugin') 3 | 4 | const { Errors } = require('./lib/index') 5 | 6 | module.exports = fp( 7 | function fastifyRacePlugin (fastify, globalOpts, next) { 8 | const controllers = new WeakMap() 9 | let error 10 | 11 | if (globalOpts != null && typeof globalOpts !== 'object') { 12 | return next(new Errors.BAD_PARAMS('object', typeof globalOpts)) 13 | } 14 | 15 | globalOpts = Object.assign( 16 | {}, 17 | { handleOnError: true, onRequestClosed: null }, 18 | globalOpts 19 | ) 20 | 21 | if (typeof globalOpts.handleOnError !== 'boolean') { 22 | error = new Errors.BAD_PARAMS('boolean', typeof globalOpts.handleOnError) 23 | } else if ( 24 | globalOpts.onRequestClosed != null && 25 | typeof globalOpts.onRequestClosed !== 'function' 26 | ) { 27 | error = new Errors.BAD_PARAMS( 28 | 'function', 29 | typeof globalOpts.onRequestClosed 30 | ) 31 | } 32 | 33 | fastify.decorateRequest('race', race) 34 | fastify.addHook('onResponse', fastifyRacingCleaner) 35 | 36 | return next(error) 37 | 38 | function fastifyRacingCleaner (request, _reply, done) { 39 | if (controllers.has(request)) { 40 | const { controller, cbs } = controllers.get(request) 41 | 42 | if (controller.signal.aborted === false) { 43 | for (const cb of cbs) { 44 | controller.signal.removeEventListener('abort', cb, { 45 | once: true 46 | }) 47 | } 48 | } 49 | 50 | controllers.delete(request) 51 | } 52 | 53 | done() 54 | } 55 | 56 | function race (opts = globalOpts) { 57 | const { raw, id: reqId } = this 58 | const handleError = typeof opts === 'function' ? true : opts.handleOnError 59 | const cb = typeof opts === 'function' ? opts : opts.onRequestClosed 60 | 61 | if (controllers.has(this)) { 62 | const { controller: ctrl, cbs } = controllers.get(this) 63 | 64 | if (ctrl.signal.aborted === true) { 65 | throw new Errors.ALREADY_ABORTED(reqId) 66 | } 67 | 68 | if (raw.socket.destroyed === true) { 69 | throw new Errors.SOCKET_CLOSED(reqId) 70 | } 71 | 72 | if (cb != null) { 73 | ctrl.signal.addEventListener('abort', cb, { 74 | once: true 75 | }) 76 | 77 | controllers.set(this, { controller: ctrl, cbs: cbs.concat(cb) }) 78 | } 79 | 80 | return ctrl.signal 81 | } else { 82 | // eslint-disable-next-line no-undef 83 | const controller = new AbortController() 84 | 85 | if (cb != null) { 86 | controller.signal.addEventListener('abort', cb, { 87 | once: true 88 | }) 89 | } 90 | 91 | if (cb == null) controller.signal.then = theneable.bind(this) 92 | 93 | if (raw.socket.destroyed) { 94 | throw new Errors.SOCKET_CLOSED(reqId) 95 | } else { 96 | raw.once( 97 | 'close', 98 | function () { 99 | if (controllers.has(this)) { 100 | const { controller: ctrl } = controllers.get(this) 101 | if (ctrl.signal.aborted === false) controller.abort() 102 | } 103 | }.bind(this) 104 | ) 105 | 106 | if (handleError === true || cb != null) { 107 | raw.once( 108 | 'error', 109 | function (err) { 110 | if (controllers.has(this)) { 111 | const { controller: ctrl } = controllers.get(this) 112 | if (ctrl.signal.aborted === false) controller.abort(err) 113 | } 114 | }.bind(this) 115 | ) 116 | } 117 | } 118 | 119 | controllers.set(this, { controller, cbs: cb != null ? [cb] : [] }) 120 | 121 | return controller.signal 122 | } 123 | 124 | function theneable (resolve, reject) { 125 | const { controller, cbs } = controllers.get(this) 126 | 127 | if (raw.socket.destroyed === true) { 128 | return reject(Errors.SOCKET_CLOSED(this.id)) 129 | } 130 | 131 | if (controller.signal.aborted === true) { 132 | return reject(Errors.ALREADY_ABORTED(this.id)) 133 | } 134 | 135 | try { 136 | controller.signal.addEventListener('abort', theneableHandler, { 137 | once: true 138 | }) 139 | 140 | controllers.set(this, { 141 | controller, 142 | cbs: cbs.concat(theneableHandler) 143 | }) 144 | } catch (err) { 145 | reject(err) 146 | } 147 | 148 | function theneableHandler (evt) { 149 | const event = { 150 | type: evt.type, 151 | reason: controller.signal?.reason 152 | } 153 | 154 | resolve(event) 155 | } 156 | } 157 | } 158 | }, 159 | { 160 | fastify: '>=3.24.1', 161 | name: 'fastify-racing' 162 | } 163 | ) 164 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const Errors = require('@fastify/error') 2 | 3 | module.exports = { 4 | Errors: { 5 | BAD_PARAMS: Errors( 6 | 'FST_PLUGIN_RACE_BAD_PARAM', 7 | 'Invalid param, expected %s but received %s' 8 | ), 9 | ALREADY_ABORTED: Errors( 10 | 'FST_PLUGIN_RACE_ALREADY_ABORTED', 11 | "Request with ID '%s' already aborted" 12 | ), 13 | SOCKET_CLOSED: Errors( 14 | 'FST_PLUGIN_RACE_SOCKET_CLOSED', 15 | "Socket for request with ID '%s' already closed" 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-racing", 3 | "version": "2.0.0", 4 | "description": "Cancel any running operation at the right time on your request handler", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "test": "tap --cov test/*.test.js && npm run typescript", 9 | "test:ci": "tap --cov test/*.test.js && npm run typescript && npm run lint", 10 | "test:only": "tap --only", 11 | "test:unit": "tap test/*.test.js", 12 | "lint": "standard | snazzy", 13 | "lint:ci": "standard", 14 | "typescript": "tsd", 15 | "prerelease": "npm run test:ci", 16 | "release": "npx standard-version --no-verify" 17 | }, 18 | "engines": { 19 | "node": ">=18.0.0" 20 | }, 21 | "keywords": [ 22 | "fastify", 23 | "racing", 24 | "fastify-racing", 25 | "http", 26 | "server", 27 | "abortcontroller", 28 | "abort-controller", 29 | "abort-signal" 30 | ], 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/metcoder95/fastify-racing.git" 34 | }, 35 | "readme": "https://github.com/metcoder95/fastify-racing/blob/main/README.md", 36 | "bugs": { 37 | "url": "https://github.com/metcoder95/fastify-racing/issues" 38 | }, 39 | "author": "metcoder95 ", 40 | "license": "MIT", 41 | "devDependencies": { 42 | "@types/node": "^22.0.0", 43 | "fastify": "^5.0.0", 44 | "husky": "^9.0.11", 45 | "nodemon": "^3.0.1", 46 | "snazzy": "^9.0.0", 47 | "standard": "^17.0.0", 48 | "tap": "^16.3.0", 49 | "tsd": "^0.32.0", 50 | "typescript": "^5.0", 51 | "undici": "^7.1.0" 52 | }, 53 | "dependencies": { 54 | "@fastify/error": "^4.0.0", 55 | "fastify-plugin": "^5.0.1" 56 | }, 57 | "tsd": { 58 | "directory": "test" 59 | }, 60 | "tap": { 61 | "check-coverage": false 62 | }, 63 | "standard": { 64 | "ignore": [ 65 | "*.d.ts", 66 | "*.test-d.ts" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd' 2 | 3 | import fastify from 'fastify' 4 | import plugin, { AbortEvent, FastifyRacingSignal } from '..' 5 | 6 | const serverHttp = fastify() 7 | 8 | serverHttp.register(plugin) 9 | 10 | serverHttp.register(plugin, { 11 | handleError: true, 12 | onRequestClosed: null 13 | }) 14 | 15 | serverHttp.get( 16 | '/', 17 | { 18 | preHandler: async (request, _reply) => { 19 | const signal = request.race() 20 | const signal2 = request.race({ 21 | handleError: true 22 | }) 23 | const event = await request.race() 24 | const event2 = await request.race({ 25 | handleError: true 26 | }) 27 | 28 | const asVoid = request.race(evt => { 29 | expectType(evt) 30 | }) 31 | 32 | expectType(asVoid) 33 | expectType(signal) 34 | expectType(event) 35 | expectType(signal2) 36 | expectType(event2) 37 | } 38 | }, 39 | async (request, reply) => { 40 | const signal = request.race() 41 | const signal2 = request.race({ 42 | handleError: true 43 | }) 44 | const event = await request.race() 45 | const event2 = await request.race({ 46 | handleError: true 47 | }) 48 | 49 | const asVoid = request.race(evt => { 50 | expectType(evt) 51 | }) 52 | 53 | expectType(asVoid) 54 | expectType(signal) 55 | expectType(event) 56 | expectType(signal2) 57 | expectType(event2) 58 | } 59 | ) 60 | 61 | // -> Second level 62 | serverHttp.register( 63 | function (fastifyInstance, opts, done) { 64 | fastifyInstance.register(plugin) 65 | 66 | fastifyInstance.get( 67 | '/', 68 | { 69 | preHandler: async (request, _reply) => { 70 | const signal = request.race() 71 | const signal2 = request.race({ 72 | handleError: true 73 | }) 74 | const event = await request.race() 75 | const event2 = await request.race({ 76 | handleError: true 77 | }) 78 | const asVoid = request.race(evt => { 79 | expectType(evt) 80 | }) 81 | 82 | expectType(asVoid) 83 | expectType(signal) 84 | expectType(event) 85 | expectType(signal2) 86 | expectType(event2) 87 | } 88 | }, 89 | async (request, reply) => { 90 | const signal = request.race() 91 | const signal2 = request.race({ 92 | handleError: true 93 | }) 94 | const event = await request.race() 95 | const event2 = await request.race({ 96 | handleError: true 97 | }) 98 | 99 | const asVoid = request.race(evt => { 100 | expectType(evt) 101 | }) 102 | 103 | expectType(asVoid) 104 | expectType(signal) 105 | expectType(event) 106 | expectType(signal2) 107 | expectType(event2) 108 | } 109 | ) 110 | 111 | done() 112 | }, 113 | { prefix: '/api' } 114 | ) 115 | 116 | const serverHttp2 = fastify({ http2: true }) 117 | 118 | serverHttp2.register(plugin, { 119 | handleError: true, 120 | onRequestClosed: null 121 | }) 122 | 123 | serverHttp2.get( 124 | '/', 125 | { 126 | preHandler: async (request, _reply) => { 127 | const signal = request.race() 128 | const signal2 = request.race({ 129 | handleError: true 130 | }) 131 | const event = await request.race() 132 | const event2 = await request.race({ 133 | handleError: true 134 | }) 135 | 136 | const asVoid = request.race(evt => { 137 | expectType(evt) 138 | }) 139 | 140 | expectType(asVoid) 141 | expectType(signal) 142 | expectType(event) 143 | expectType(signal2) 144 | expectType(event2) 145 | } 146 | }, 147 | async (request, reply) => { 148 | const signal = request.race() 149 | const signal2 = request.race({ 150 | handleError: true 151 | }) 152 | const event = await request.race() 153 | const event2 = await request.race({ 154 | handleError: true 155 | }) 156 | 157 | const asVoid = request.race(evt => { 158 | expectType(evt) 159 | }) 160 | 161 | expectType(asVoid) 162 | expectType(signal) 163 | expectType(event) 164 | expectType(signal2) 165 | expectType(event2) 166 | } 167 | ) 168 | 169 | // -> First plugin 170 | serverHttp2.register( 171 | function (fastifyInstance, opts, done) { 172 | fastifyInstance.register(plugin) 173 | 174 | fastifyInstance.get( 175 | '/', 176 | { 177 | preHandler: async (request, _reply) => { 178 | const signal = request.race() 179 | const signal2 = request.race({ 180 | handleError: true 181 | }) 182 | const event = await request.race() 183 | const event2 = await request.race({ 184 | handleError: true 185 | }) 186 | 187 | const asVoid = request.race(evt => { 188 | expectType(evt) 189 | }) 190 | 191 | expectType(asVoid) 192 | expectType(signal) 193 | expectType(event) 194 | expectType(signal2) 195 | expectType(event2) 196 | } 197 | }, 198 | async (request, reply) => { 199 | const signal = request.race() 200 | const signal2 = request.race({ 201 | handleError: true 202 | }) 203 | const event = await request.race() 204 | const event2 = await request.race({ 205 | handleError: true 206 | }) 207 | 208 | const asVoid = request.race(evt => { 209 | expectType(evt) 210 | }) 211 | 212 | expectType(asVoid) 213 | expectType(signal) 214 | expectType(event) 215 | expectType(signal2) 216 | expectType(event2) 217 | } 218 | ) 219 | 220 | done() 221 | }, 222 | { prefix: '/api' } 223 | ) 224 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { promisify } = require('util') 3 | 4 | const tap = require('tap') 5 | const fastify = require('fastify') 6 | const { request, Client } = require('undici') 7 | 8 | const plugin = require('../.') 9 | const { Errors } = require('../lib') 10 | 11 | const sleep = promisify(setTimeout) 12 | 13 | tap.plan(2) 14 | 15 | tap.test('fastify-racing#decoration', subtest => { 16 | subtest.plan(4) 17 | 18 | subtest.test('Should decorate the request properly', async t => { 19 | t.plan(3) 20 | 21 | const app = fastify() 22 | app.register(plugin) 23 | 24 | app.get('/', (req, reply) => { 25 | t.ok(req.race, 'should decorate request object') 26 | t.equal(typeof req.race, 'function', 'should be a function') 27 | 28 | return 'hello' 29 | }) 30 | 31 | const response = await app.inject({ 32 | method: 'GET', 33 | path: '/' 34 | }) 35 | 36 | t.equal(response.body, 'hello') 37 | }) 38 | 39 | subtest.test('Should throw if invalid Global opts', async t => { 40 | t.plan(3) 41 | 42 | const app = fastify() 43 | try { 44 | await app.register(plugin, 'invalid').ready() 45 | } catch (err) { 46 | t.ok(err, 'should throw') 47 | t.equal(err.code, 'FST_PLUGIN_RACE_BAD_PARAM') 48 | t.equal(err.message, 'Invalid param, expected object but received string') 49 | } 50 | }) 51 | 52 | subtest.test('Should throw if invalid Global opts.handleOnError', async t => { 53 | t.plan(3) 54 | 55 | const app = fastify() 56 | try { 57 | await app.register(plugin, { handleOnError: 'invalid' }).ready() 58 | } catch (err) { 59 | t.ok(err, 'should throw') 60 | t.equal(err.code, 'FST_PLUGIN_RACE_BAD_PARAM') 61 | t.equal( 62 | err.message, 63 | 'Invalid param, expected boolean but received string' 64 | ) 65 | } 66 | }) 67 | 68 | subtest.test( 69 | 'Should throw if invalid Global opts.onRequestClosed', 70 | async t => { 71 | t.plan(3) 72 | 73 | const app = fastify() 74 | try { 75 | await app.register(plugin, { onRequestClosed: 1 }).ready() 76 | } catch (err) { 77 | t.ok(err, 'should throw') 78 | t.equal(err.code, 'FST_PLUGIN_RACE_BAD_PARAM') 79 | t.equal( 80 | err.message, 81 | 'Invalid param, expected function but received number' 82 | ) 83 | } 84 | } 85 | ) 86 | }) 87 | 88 | tap.test('fastify-racing#promise', { only: true }, subtest => { 89 | subtest.plan(4) 90 | 91 | subtest.test('Should handle a request aborted', t => { 92 | t.plan(3) 93 | 94 | const app = fastify() 95 | // eslint-disable-next-line no-undef 96 | const abtCtlr = new AbortController() 97 | app.register(plugin) 98 | 99 | t.teardown(() => app.close()) 100 | 101 | app.get('/', async (req, _reply) => { 102 | const signal = req.race() 103 | const result = await Promise.race([signal, dummy(signal)]) 104 | 105 | t.equal(typeof result, 'object') 106 | t.equal(result.type, 'abort') 107 | 108 | if (result.type === 'aborted') return '' 109 | else return `${result}-world` 110 | }) 111 | 112 | app 113 | .ready() 114 | .then(() => app.listen({ port: 0 })) 115 | .then(async () => { 116 | request( 117 | `http://localhost:${app.server.address().port}`, 118 | { 119 | method: 'GET', 120 | path: '/', 121 | signal: abtCtlr.signal 122 | }, 123 | err => { 124 | t.ok(err) 125 | } 126 | ) 127 | 128 | // Allow a full event loop cycle 129 | await sleep(5) 130 | abtCtlr.abort() 131 | }) 132 | }) 133 | 134 | subtest.test( 135 | 'Should be able to handle more than one race check within a request', 136 | t => { 137 | const app = fastify() 138 | // eslint-disable-next-line no-undef 139 | const abtCtlr = new AbortController() 140 | let starter 141 | 142 | t.plan(10) 143 | 144 | app.register(plugin) 145 | 146 | app.get( 147 | '/', 148 | { 149 | preHandler: [ 150 | async (req, _reply) => { 151 | starter = req.race() 152 | const result = await Promise.race([starter, dummy(starter, 10)]) 153 | t.equal(result, 'hello') 154 | }, 155 | async (req, _reply) => { 156 | const second = req.race() 157 | const result = await Promise.race([second, dummy(second, 10)]) 158 | 159 | t.equal(result, 'hello') 160 | t.equal( 161 | starter, 162 | second, 163 | 'Should use the same AbortController instance' 164 | ) 165 | }, 166 | async (req, _reply) => { 167 | const third = req.race() 168 | const result = await Promise.race([third, dummy(third, 10)]) 169 | t.equal(result, 'hello') 170 | t.equal( 171 | starter, 172 | third, 173 | 'Should use the same AbortController instance' 174 | ) 175 | } 176 | ] 177 | }, 178 | async (req, _reply) => { 179 | const final = req.race() 180 | 181 | const result = await Promise.race([final, dummy(final, 2000)]) 182 | 183 | t.ok(final.aborted) 184 | t.equal(final, starter, 'Should reuse the initial controller') 185 | 186 | t.equal(typeof result, 'object') 187 | t.equal(result.type, 'abort') 188 | 189 | return '' 190 | } 191 | ) 192 | 193 | t.teardown(() => app.close()) 194 | 195 | app 196 | .ready() 197 | .then(() => app.listen({ port: 0 })) 198 | .then(async () => { 199 | request( 200 | `http://localhost:${app.server.address().port}`, 201 | { 202 | method: 'GET', 203 | path: '/', 204 | signal: abtCtlr.signal 205 | }, 206 | err => { 207 | t.ok(err) 208 | } 209 | ) 210 | 211 | // Allow a full event loop cycle 212 | await sleep(500) 213 | abtCtlr.abort() 214 | }) 215 | } 216 | ) 217 | 218 | subtest.test( 219 | 'Should reuse AbortController for the single request', 220 | async t => { 221 | // eslint-disable-next-line 222 | let first, client 223 | const app = fastify() 224 | 225 | t.teardown(async () => { 226 | await client.destroy() 227 | await app.close() 228 | }) 229 | t.plan(5) 230 | 231 | app.register(plugin) 232 | 233 | app.get( 234 | '/', 235 | { 236 | preHandler: (req, _reply, done) => { 237 | first = req.race() 238 | 239 | t.ok(first) 240 | done() 241 | } 242 | }, 243 | (req, _reply) => { 244 | const second = req.race() 245 | 246 | t.notOk(second.aborted) 247 | t.equal(second, first, 'Should reuse the initial controller') 248 | 249 | _reply.send('Hello World') 250 | } 251 | ) 252 | 253 | await app.listen({ port: 0 }) 254 | 255 | client = new Client(`http://localhost:${app.server.address().port}`) 256 | 257 | const response = await client.request({ 258 | method: 'GET', 259 | path: '/' 260 | }) 261 | 262 | const responseBody = await response.body.text() 263 | 264 | t.equal(response.statusCode, 200) 265 | t.equal(responseBody, 'Hello World') 266 | } 267 | ) 268 | 269 | subtest.test('Should throw on already closed request', async t => { 270 | // eslint-disable-next-line 271 | let first 272 | const app = fastify() 273 | 274 | t.teardown(app.close.bind(app)) 275 | 276 | t.plan(7) 277 | 278 | app.register(plugin) 279 | 280 | app.get( 281 | '/', 282 | { 283 | onResponse: async (req, _reply, done) => { 284 | req.raw.destroy() 285 | 286 | try { 287 | first = await req.race() 288 | } catch (err) { 289 | t.ok(err) 290 | t.ok(err instanceof Errors.SOCKET_CLOSED) 291 | t.equal(err.code, 'FST_PLUGIN_RACE_SOCKET_CLOSED') 292 | t.equal(err.statusCode, 500) 293 | } 294 | 295 | t.notOk(first) 296 | done() 297 | } 298 | }, 299 | (req, _reply) => { 300 | return 'Hello World' 301 | } 302 | ) 303 | 304 | t.teardown(() => app.close()) 305 | 306 | await app.listen({ port: 0 }) 307 | 308 | const client = new Client(`http://localhost:${app.server.address().port}`, { 309 | pipelining: 0 310 | }) 311 | 312 | const response = await client.request( 313 | { 314 | method: 'GET', 315 | path: '/' 316 | } 317 | ) 318 | 319 | t.equal(response.statusCode, 200) 320 | t.equal(await response.body.text(), 'Hello World') 321 | }) 322 | 323 | async function dummy (signal, ms = 3000) { 324 | await sleep(ms, null, { signal, ref: false }) 325 | return 'hello' 326 | } 327 | }) 328 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | "declarationDir": "./types", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | // "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | --------------------------------------------------------------------------------