├── .npmrc ├── .gitattributes ├── eslint.config.js ├── .github ├── dependabot.yml ├── workflows │ └── ci.yml └── stale.yml ├── example.mjs ├── test ├── fixtures │ ├── csr.pem │ ├── cert.pem │ └── key.pem ├── hooks.test.js └── index.test.js ├── LICENSE ├── lib └── server.js ├── package.json ├── types ├── index.test-d.ts └── index.d.ts ├── README.md ├── .gitignore └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /.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: "monthly" 13 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /example.mjs: -------------------------------------------------------------------------------- 1 | import { restartable } from './index.js' 2 | 3 | async function createApp (fastify, opts) { 4 | const app = fastify(opts) 5 | 6 | app.get('/restart', async () => { 7 | await app.restart() 8 | return { status: 'ok' } 9 | }) 10 | 11 | return app 12 | } 13 | 14 | const app = await restartable(createApp, { logger: true }) 15 | const host = await app.listen({ port: 3000 }) 16 | 17 | console.log('server listening on', host) 18 | 19 | // call restart() if you want to restart 20 | process.on('SIGUSR1', () => { 21 | console.log('Restarting the server') 22 | app.restart() 23 | }) 24 | 25 | process.once('SIGINT', () => { 26 | console.log('Stopping the server') 27 | app.close() 28 | }) 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | # This allows a subsequently queued workflow run to interrupt previous runs 18 | concurrency: 19 | group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 20 | cancel-in-progress: true 21 | 22 | permissions: 23 | contents: read 24 | 25 | jobs: 26 | test: 27 | permissions: 28 | contents: write 29 | pull-requests: write 30 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 31 | with: 32 | license-check: true 33 | lint: true 34 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/fixtures/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICijCCAXICAQAwRTELMAkGA1UEBhMCSVQxEzARBgNVBAgMClNvbWUtU3RhdGUx 3 | ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN 4 | AQEBBQADggEPADCCAQoCggEBAO2xXr3ch1/kBvM1Bw1c8lsVq6tyi3wixxiAJHFh 5 | E7EGxqvfMSDTBF2ns3RQ/ZOKfP+bWc8HmEAhf+CbjHRSd40Mbgfrk8awbn55Wt7t 6 | EalaSGWjvDdCh9SOTcBkqQJ5JY85zfzY9Yqz5N97WwSHlAqhzerUKkptAcTtSGvC 7 | +4WbfOSFm7Zop01ig6FTCAPG9jqiSlYcMX2MG1DpLS9CcASEgCOthp6C/r4ogUgK 8 | QlfWloTc8hKPtKYlw/cYdNCYjFJ7rJXtSvFO7k8UpRBJUkKFttrR2GxQzMmUBIry 9 | C/qAQZyaQKRj0RpC95Ab/UN35dbSFVcUKGCnTLDZXI4pzPsCAwEAAaAAMA0GCSqG 10 | SIb3DQEBCwUAA4IBAQCfihmMvkSKacRxO21oKSmxFQ2PhgEB+iXJscxBGCc6wmV9 11 | pdNvssedJIsgAuhheC5utJRSthSHDxOPcRiuxtXuQtMJKv5aqySa/Mh5CWz3Emf7 12 | hBWthtikqcvq/SLIpRKgUoDDKKUUF6bywX0K8uxJ6lE4UN90m18mR9fHeu3vvXz/ 13 | WZFw8+ma5Y+SRZ73FNAJrb5rf0YkOooWa7Q9zD73f1Gl7YIV3FfzqWVeKFfhkVRi 14 | tSvOAkNNg0goBgVQFTlq6GE4XwWj9KW6BZsjuMZ4hJdxVdgbgDLlzUK4kIJ0yefA 15 | 68MkTtPGsUsRnrXJFIiasAOKvM1cxprExC1kNUvO 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /test/fixtures/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDETCCAfkCFBa0rsvwNVU7CUF+1oeEn2ZLSuvGMA0GCSqGSIb3DQEBCwUAMEUx 3 | CzAJBgNVBAYTAklUMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl 4 | cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjIwMjIwMTY0NjUwWhcNMjIwMzIyMTY0 5 | NjUwWjBFMQswCQYDVQQGEwJJVDETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE 6 | CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC 7 | AQ8AMIIBCgKCAQEA7bFevdyHX+QG8zUHDVzyWxWrq3KLfCLHGIAkcWETsQbGq98x 8 | INMEXaezdFD9k4p8/5tZzweYQCF/4JuMdFJ3jQxuB+uTxrBufnla3u0RqVpIZaO8 9 | N0KH1I5NwGSpAnkljznN/Nj1irPk33tbBIeUCqHN6tQqSm0BxO1Ia8L7hZt85IWb 10 | tminTWKDoVMIA8b2OqJKVhwxfYwbUOktL0JwBISAI62GnoL+viiBSApCV9aWhNzy 11 | Eo+0piXD9xh00JiMUnusle1K8U7uTxSlEElSQoW22tHYbFDMyZQEivIL+oBBnJpA 12 | pGPRGkL3kBv9Q3fl1tIVVxQoYKdMsNlcjinM+wIDAQABMA0GCSqGSIb3DQEBCwUA 13 | A4IBAQCauxDLej33WNxP0JDXoSKndqVrf+Jh795WzVdXLlLK6InUfuf9sz9IoD93 14 | unkcumN7WHtDHZ/jX1xrJCq7qLeJPT4hoeuEae9rvlw3qq98cf5I9VO1y0XIj6Tz 15 | eog6fhJYB2LQu5VXY6GUh73wgd7ppPPxPrvmbuHkXikXPfdQG6qOJ8vSue+eg+bU 16 | pZuqBLsxO07Rjj8TMn5q3HM2M9VauIP6XaW/1PoOfoWaMRtJApfeMxNmilIrrAoF 17 | gFTtqqFdCfj30BvqUbB7CeMj5JSzdI/fapHX/C/8bAGjtWh2YKMFbcH4sAGEv6PD 18 | YNJiBnzlD0bHBXLaiND2793iajmn 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present The Fastify team 4 | 5 | The Fastify team members are listed at https://github.com/fastify/fastify#team. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('node:http') 4 | const https = require('node:https') 5 | 6 | const { FST_ERR_HTTP2_INVALID_VERSION } = require('fastify').errorCodes 7 | 8 | function getServerInstance (options, httpHandler) { 9 | let server = null 10 | if (options.http2) { 11 | if (options.https) { 12 | server = http2().createSecureServer(options.https, httpHandler) 13 | } else { 14 | server = http2().createServer(httpHandler) 15 | } 16 | server.on('session', sessionTimeout(options.http2SessionTimeout)) 17 | } else { 18 | // this is http1 19 | if (options.https) { 20 | server = https.createServer(options.https, httpHandler) 21 | } else { 22 | server = http.createServer(options.http, httpHandler) 23 | } 24 | server.keepAliveTimeout = options.keepAliveTimeout 25 | server.requestTimeout = options.requestTimeout 26 | // we treat zero as null 27 | // and null is the default setting from nodejs 28 | // so we do not pass the option to server 29 | if (options.maxRequestsPerSocket > 0) { 30 | server.maxRequestsPerSocket = options.maxRequestsPerSocket 31 | } 32 | } 33 | return server 34 | } 35 | 36 | function http2 () { 37 | try { 38 | return require('node:http2') 39 | } /* c8 ignore start */ catch { 40 | throw new FST_ERR_HTTP2_INVALID_VERSION() 41 | } /* c8 ignore end */ 42 | } 43 | 44 | function sessionTimeout (timeout) { 45 | return function (session) { 46 | session.setTimeout(timeout, close) 47 | } 48 | } 49 | 50 | function close () { 51 | this.close() 52 | } 53 | 54 | module.exports = getServerInstance 55 | -------------------------------------------------------------------------------- /test/fixtures/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA7bFevdyHX+QG8zUHDVzyWxWrq3KLfCLHGIAkcWETsQbGq98x 3 | INMEXaezdFD9k4p8/5tZzweYQCF/4JuMdFJ3jQxuB+uTxrBufnla3u0RqVpIZaO8 4 | N0KH1I5NwGSpAnkljznN/Nj1irPk33tbBIeUCqHN6tQqSm0BxO1Ia8L7hZt85IWb 5 | tminTWKDoVMIA8b2OqJKVhwxfYwbUOktL0JwBISAI62GnoL+viiBSApCV9aWhNzy 6 | Eo+0piXD9xh00JiMUnusle1K8U7uTxSlEElSQoW22tHYbFDMyZQEivIL+oBBnJpA 7 | pGPRGkL3kBv9Q3fl1tIVVxQoYKdMsNlcjinM+wIDAQABAoIBACF7quzz893+MTxx 8 | a9zmCv3pv5UXPa7u9zzsUDXohu2ZFsN/XVxSXXsT9KOeBRqTl6gqKiyP1VKzZWAu 9 | iUqZk127MoTLGrYShH4sejCNFL/Wh/xJZGokZR38Lde8VlKS3keezPWhWnV/ge45 10 | YkjxEFmvEWLTIGH7mRQ0mM8VWHkpeJiCyU0sSKOchwK+EL18+cANWq0mOvOLQd9E 11 | UuFysM3fADPbCNwD9LsrD2MAZ2hvCFpqS1Nr09ZYDTvkQCMBN38uB2zpGfcP9Sxx 12 | 6TDexrU6c1tcdT0EBI7JrpACZhxDF0HOdz81bxyDJNyqUrRH0ZArt1gNGmjDOFP6 13 | F8J5O4ECgYEA/WLCeEhIb6jLVuDdusfFX7h510Kc+ZwkTUKg5vk98uingxMCsERn 14 | 2cVNiHbHPhS3KOLl9Kf2mVp3XmueD/+bQZQOzzY2djeq26kbtsKa/lFjOVYf2TVp 15 | nU8TO7kxKEqBt76RjZASoKmU+IORhOdBS7h6d2bWr5sAHg6NZ2fCdV8CgYEA8CUp 16 | k65NwS9ROTHl5ftLCYChRmrgKIZHtc44r6U5qsqtIAlBxxJRshWzMTfBnmpuYWQ4 17 | fpbEYqq903d4urAeuFID+Xc23LTt078Qu6XATvNSK0cHnAMuITQB95R/8Xp/jNZV 18 | NEDGzzi21Oe9EQXZRLYnfeOxpWunJuRWR8TEkeUCgYAYc1M4sCDtRWh5tbEvuN8+ 19 | 4VpAf1kObRbDrc5A+4QS0Ih5iXgU8kTjKrrUlEGdp/oUo/B0r1CIc8ZZAiF6gbvF 20 | lDfpnt2agryl/aeC9zxllgzxF3JzT0glud+tP62Sqb3isSzycBicEEEjye5c2MPg 21 | PqjypKXKxDY6sETM2aJWGQKBgFJSSvhnLhxlXhCfPFIkXMq7H44GISWDMp3uPZxo 22 | pWYY6FQtGDFn5D2KFs1ucZ8emQwl0QAEKvov0bbmI6rLqRxCcT5ZUaNDGqwVuWS6 23 | IzxtSOOxC7i9llinfW0jqOBcv9DFwJuTARQUOwitEDD/skVNtCgBn+o3Byvb5n/f 24 | wgrJAoGBANvd+vglen3PNImeJgrqvxgcSzMa4T+8QxlmmrhJahnhG/a36guGfHca 25 | 4csCc083by72HiwSv5Q/OILG6+gDi+AcCp+Pj1kmVvNWykdul3Uc1p8C2km5weF0 26 | aH4D33IcdodqPFIzRVG03lZeg4GMl3fBNRq4S9irhraPN1nWIxm1 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/restartable", 3 | "version": "3.0.2", 4 | "description": "Restart Fastify without losing a request", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "scripts": { 8 | "lint": "eslint", 9 | "lint:fix": "eslint --fix", 10 | "test": "npm run test:unit && npm run test:typescript", 11 | "test:typescript": "tsd", 12 | "test:unit": "c8 --100 node --test" 13 | }, 14 | "types": "types/index.d.ts", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/fastify/restartable.git" 18 | }, 19 | "keywords": [ 20 | "fastify", 21 | "restart", 22 | "http", 23 | "graceful" 24 | ], 25 | "author": "Matteo Collina ", 26 | "contributors": [ 27 | { 28 | "name": "Tomas Della Vedova", 29 | "url": "http://delved.org" 30 | }, 31 | { 32 | "name": "Aras Abbasi", 33 | "email": "aras.abbasi@gmail.com" 34 | }, 35 | { 36 | "name": "James Sumners", 37 | "url": "https://james.sumners.info" 38 | }, 39 | { 40 | "name": "Frazer Smith", 41 | "email": "frazer.dev@icloud.com", 42 | "url": "https://github.com/fdawgs" 43 | } 44 | ], 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/fastify/restartable/issues" 48 | }, 49 | "homepage": "https://github.com/fastify/restartable#readme", 50 | "funding": [ 51 | { 52 | "type": "github", 53 | "url": "https://github.com/sponsors/fastify" 54 | }, 55 | { 56 | "type": "opencollective", 57 | "url": "https://opencollective.com/fastify" 58 | } 59 | ], 60 | "devDependencies": { 61 | "@types/node": "^24.0.8", 62 | "c8": "^10.1.3", 63 | "eslint": "^9.17.0", 64 | "neostandard": "^0.12.0", 65 | "split2": "^4.2.0", 66 | "tsd": "^0.33.0", 67 | "undici": "^7.0.0" 68 | }, 69 | "dependencies": { 70 | "fastify": "^5.0.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { fastify, FastifyInstance, FastifyServerOptions } from 'fastify' 2 | import { expectAssignable, expectType } from 'tsd' 3 | // eslint-disable-next-line @typescript-eslint/no-unused-vars -- ApplicationFactory is used in commented out test 4 | import { restartable, ApplicationFactory } from './index' 5 | import type { Http2Server } from 'node:http2' 6 | 7 | type Fastify = typeof fastify 8 | 9 | async function createApplication ( 10 | fastify: Fastify, 11 | opts: FastifyServerOptions, 12 | restartOpts?: unknown 13 | ): Promise { 14 | const app = fastify(opts) 15 | 16 | expectAssignable(app) 17 | expectAssignable(restartOpts) 18 | 19 | expectType(app.restarted) 20 | expectType<(restartOpts?: unknown) => Promise>(app.restart) 21 | 22 | return app 23 | } 24 | 25 | // This currently fails with: 26 | // -------------------------- 27 | // Parameter type ApplicationFactory is not identical to argument type 28 | // (fastify: typeof import("/Users/denchen/git/restartable/node_modules/fastify/fastify.d.ts"), 29 | // opts: FastifyServerOptions, restartOpts?: unknown) 30 | // => Promise, 31 | // FastifyBaseLogger, FastifyTypeProviderDefault>> 32 | // expectType(createApplication) 33 | 34 | { 35 | const app = await restartable(createApplication) 36 | expectType(app) 37 | expectType(app.restarted) 38 | expectType(app.closingRestartable) 39 | expectType<(restartOpts?: unknown) => Promise>(app.restart) 40 | } 41 | 42 | { 43 | const app = await restartable(createApplication, { logger: true }) 44 | expectType(app) 45 | expectType(app.restarted) 46 | expectType(app.closingRestartable) 47 | expectType<(restartOpts?: unknown) => Promise>(app.restart) 48 | } 49 | 50 | { 51 | const app = await restartable(createApplication, { logger: true }, fastify) 52 | expectType(app) 53 | expectType(app.restarted) 54 | expectType(app.closingRestartable) 55 | expectType<(restartOpts?: unknown) => Promise>(app.restart) 56 | } 57 | 58 | { 59 | const app = await restartable( 60 | async (factory, opts) => await factory(opts), 61 | { http2: true }, 62 | fastify 63 | ) 64 | expectType>(app) 65 | expectType(app.restarted) 66 | expectType(app.closingRestartable) 67 | expectType<(restartOpts?: unknown) => Promise>(app.restart) 68 | } 69 | 70 | { 71 | const app = await restartable(createApplication, { logger: true }, fastify) 72 | app.addPreRestartHook(async (_instance: FastifyInstance, _restartOpts: unknown) => {}) 73 | app.addOnRestartHook(async (_instance: FastifyInstance, _restartOpts: unknown) => {}) 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @fastify/restartable 2 | 3 | [![NPM version](https://img.shields.io/npm/v/@fastify/restartable.svg?style=flat)](https://www.npmjs.com/package/@fastify/restartable) 4 | [![CI](https://github.com/fastify/restartable/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/restartable/actions/workflows/ci.yml) 5 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 6 | 7 | Restart Fastify without losing a request. 8 | 9 | This module is useful if you want to compose the 10 | fastify routes dynamically or you need some remote 11 | config. In case of a change, you can restart Fastify. 12 | 13 | ## Install 14 | 15 | ```bash 16 | npm i @fastify/restartable 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```js 22 | import { restartable } from '@fastify/restartable' 23 | 24 | async function createApp (fastify, opts) { 25 | const app = fastify(opts) 26 | 27 | app.get('/restart', async () => { 28 | await app.restart() 29 | return { status: 'ok' } 30 | }) 31 | 32 | app.addHook('onClose', async () => { 33 | if(!app.closingRestartable) { 34 | console.log('closing the app because of restart') 35 | } 36 | else{ 37 | console.log('closing the app because server is stopping') 38 | } 39 | }) 40 | 41 | return app 42 | } 43 | 44 | const app = await restartable(createApp, { logger: true }) 45 | const host = await app.listen({ port: 3000 }) 46 | 47 | console.log('server listening on', host) 48 | 49 | // call restart() if you want to restart 50 | process.on('SIGUSR1', () => { 51 | console.log('Restarting the server') 52 | app.restart() 53 | }) 54 | 55 | process.once('SIGINT', () => { 56 | console.log('Stopping the server') 57 | app.close() 58 | }) 59 | 60 | ``` 61 | 62 | ## Hooks 63 | 64 | - `preRestart` - called before creating a new app instance and closing an 65 | existing one. The hook is called with the current app instance as an argument. 66 | Use it to close any resources that you don't want to be shared between the 67 | app instances. 68 | 69 | - `onRestart` - called after the new app instance is created and the old one 70 | is closed. The hook is called with the new app instance as an argument. 71 | 72 | **Example**: 73 | 74 | ```js 75 | async function createApplication (fastify, opts) { 76 | console.log('creating new app instance') 77 | return fastify(opts) 78 | } 79 | const app = await restartable(createApplication) 80 | 81 | app.addPreRestartHook(async (app) => { 82 | console.log('preRestart hook called') 83 | }) 84 | 85 | app.addOnRestartHook(async (app) => { 86 | console.log('onRestart hook called') 87 | }) 88 | 89 | await app.restart() 90 | ``` 91 | 92 | **Output**: 93 | 94 | ```bash 95 | preRestart hook called 96 | creating new app instance 97 | onRestart hook called 98 | ``` 99 | 100 | ## License 101 | 102 | Licensed under [MIT](./LICENSE). 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | 151 | #tap files 152 | .tap/ 153 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { fastify, FastifyInstance } from 'fastify' 2 | 3 | import type { 4 | FastifyBaseLogger, 5 | FastifyHttp2Options, 6 | FastifyHttp2SecureOptions, 7 | FastifyHttpOptions, 8 | FastifyHttpsOptions, 9 | FastifyServerOptions, 10 | FastifyTypeProvider, 11 | FastifyTypeProviderDefault, 12 | RawReplyDefaultExpression, 13 | RawRequestDefaultExpression, 14 | RawServerBase, 15 | RawServerDefault, 16 | } from 'fastify' 17 | import * as http from 'node:http' 18 | import * as http2 from 'node:http2' 19 | import * as https from 'node:https' 20 | 21 | export type RestartHook = ( 22 | instance: FastifyInstance, 23 | restartOpts?: unknown 24 | ) => Promise 25 | 26 | declare module 'fastify' { 27 | interface FastifyInstance { 28 | restart: (restartOpts?: unknown) => Promise; 29 | addPreRestartHook: (fn: RestartHook) => void; 30 | addOnRestartHook: (fn: RestartHook) => void; 31 | restarted: boolean; 32 | closingRestartable: boolean; 33 | } 34 | } 35 | 36 | type Fastify = typeof fastify 37 | 38 | export type ApplicationFactory< 39 | Server extends 40 | | RawServerBase 41 | | http.Server 42 | | https.Server 43 | | http2.Http2Server 44 | | http2.Http2SecureServer = RawServerDefault, 45 | Request extends RawRequestDefaultExpression = RawRequestDefaultExpression, 46 | Reply extends RawReplyDefaultExpression = RawReplyDefaultExpression, 47 | Logger extends FastifyBaseLogger = FastifyBaseLogger, 48 | TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault 49 | > = ( 50 | fastify: Fastify, 51 | opts: Server extends infer S 52 | ? S extends http2.Http2SecureServer 53 | ? FastifyHttp2SecureOptions 54 | : S extends http2.Http2Server 55 | ? FastifyHttp2Options 56 | : S extends https.Server 57 | ? FastifyHttpsOptions 58 | : S extends http.Server 59 | ? FastifyHttpOptions 60 | : FastifyServerOptions 61 | : FastifyServerOptions, 62 | restartOpts?: unknown 63 | ) => Promise> 64 | 65 | // These overloads follow the same overloads for the fastify factory 66 | 67 | export declare function restartable< 68 | Server extends http2.Http2SecureServer, 69 | Request extends RawRequestDefaultExpression = RawRequestDefaultExpression, 70 | Reply extends RawReplyDefaultExpression = RawReplyDefaultExpression, 71 | Logger extends FastifyBaseLogger = FastifyBaseLogger, 72 | TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault 73 | > ( 74 | factory: ApplicationFactory, 75 | opts?: FastifyHttp2SecureOptions, 76 | fastify?: Fastify 77 | ): Promise> 78 | 79 | export declare function restartable< 80 | Server extends http2.Http2Server, 81 | Request extends RawRequestDefaultExpression = RawRequestDefaultExpression, 82 | Reply extends RawReplyDefaultExpression = RawReplyDefaultExpression, 83 | Logger extends FastifyBaseLogger = FastifyBaseLogger, 84 | TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault 85 | > ( 86 | factory: ApplicationFactory, 87 | opts?: FastifyHttp2Options, 88 | fastify?: Fastify 89 | ): Promise> 90 | 91 | export declare function restartable< 92 | Server extends https.Server, 93 | Request extends RawRequestDefaultExpression = RawRequestDefaultExpression, 94 | Reply extends RawReplyDefaultExpression = RawReplyDefaultExpression, 95 | Logger extends FastifyBaseLogger = FastifyBaseLogger, 96 | TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault 97 | > ( 98 | factory: ApplicationFactory, 99 | opts?: FastifyHttpsOptions, 100 | fastify?: Fastify 101 | ): Promise> 102 | 103 | export declare function restartable< 104 | Server extends http.Server, 105 | Request extends RawRequestDefaultExpression = RawRequestDefaultExpression, 106 | Reply extends RawReplyDefaultExpression = RawReplyDefaultExpression, 107 | Logger extends FastifyBaseLogger = FastifyBaseLogger, 108 | TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault 109 | > ( 110 | factory: ApplicationFactory, 111 | opts?: FastifyHttpOptions, 112 | fastify?: Fastify 113 | ): Promise> 114 | -------------------------------------------------------------------------------- /test/hooks.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { afterEach, test } = require('node:test') 4 | const { restartable } = require('..') 5 | 6 | afterEach(async () => { 7 | await new Promise((resolve) => setTimeout(resolve, 10)) 8 | }) 9 | 10 | test('should trigger preRestartHook', async (t) => { 11 | t.plan(4) 12 | 13 | async function createApplication (fastify, opts) { 14 | return fastify(opts) 15 | } 16 | 17 | const app = await restartable(createApplication, { 18 | keepAliveTimeout: 1 19 | }) 20 | 21 | t.after(async () => { 22 | await app.close() 23 | }) 24 | 25 | const expectedRestartOptions = { foo: 'bar' } 26 | 27 | app.addPreRestartHook(async (app, restartOptions) => { 28 | t.assert.strictEqual(app.restarted, false) 29 | t.assert.deepStrictEqual(restartOptions, expectedRestartOptions) 30 | }) 31 | 32 | app.addPreRestartHook(async (app, restartOptions) => { 33 | t.assert.strictEqual(app.restarted, false) 34 | t.assert.deepStrictEqual(restartOptions, expectedRestartOptions) 35 | }) 36 | 37 | await app.restart(expectedRestartOptions) 38 | }) 39 | 40 | test('should not fail preRestartHook throw an error', async (t) => { 41 | t.plan(3) 42 | 43 | async function createApplication (fastify, opts) { 44 | return fastify(opts) 45 | } 46 | 47 | const app = await restartable(createApplication, { 48 | keepAliveTimeout: 1 49 | }) 50 | 51 | t.after(async () => { 52 | await app.close() 53 | }) 54 | 55 | const expectedRestartOptions = { foo: 'bar' } 56 | 57 | app.addPreRestartHook(async () => { 58 | throw new Error('kaboom') 59 | }) 60 | 61 | app.addPreRestartHook(async (app, restartOptions) => { 62 | t.assert.strictEqual(app.restarted, false) 63 | t.assert.deepStrictEqual(restartOptions, expectedRestartOptions) 64 | }) 65 | 66 | await app.restart(expectedRestartOptions) 67 | 68 | t.assert.strictEqual(app.restarted, true) 69 | }) 70 | 71 | test('should throw if preRestartHook is not a function', async (t) => { 72 | t.plan(1) 73 | 74 | async function createApplication (fastify, opts) { 75 | return fastify(opts) 76 | } 77 | 78 | const app = await restartable(createApplication, { 79 | keepAliveTimeout: 1 80 | }) 81 | 82 | t.after(async () => { 83 | await app.close() 84 | }) 85 | 86 | t.assert.throws(() => { 87 | app.addPreRestartHook('not a function') 88 | }, { message: 'The hook must be a function' }) 89 | }) 90 | 91 | test('should trigger onRestartHook', async (t) => { 92 | t.plan(4) 93 | 94 | async function createApplication (fastify, opts) { 95 | return fastify(opts) 96 | } 97 | 98 | const app = await restartable(createApplication, { 99 | keepAliveTimeout: 1 100 | }) 101 | 102 | t.after(async () => { 103 | await app.close() 104 | }) 105 | 106 | const expectedRestartOptions = { foo: 'bar' } 107 | 108 | app.addOnRestartHook(async (app, restartOptions) => { 109 | t.assert.strictEqual(app.restarted, true) 110 | t.assert.deepStrictEqual(restartOptions, expectedRestartOptions) 111 | }) 112 | 113 | app.addOnRestartHook(async (app, restartOptions) => { 114 | t.assert.strictEqual(app.restarted, true) 115 | t.assert.deepStrictEqual(restartOptions, expectedRestartOptions) 116 | }) 117 | 118 | await app.restart(expectedRestartOptions) 119 | }) 120 | 121 | test('should not fail onRestartHook throw an error', async (t) => { 122 | t.plan(3) 123 | 124 | async function createApplication (fastify, opts) { 125 | return fastify(opts) 126 | } 127 | 128 | const app = await restartable(createApplication, { 129 | keepAliveTimeout: 1 130 | }) 131 | 132 | t.after(async () => { 133 | await app.close() 134 | }) 135 | 136 | const expectedRestartOptions = { foo: 'bar' } 137 | 138 | app.addOnRestartHook(async () => { 139 | throw new Error('kaboom') 140 | }) 141 | 142 | app.addOnRestartHook(async (app, restartOptions) => { 143 | t.assert.strictEqual(app.restarted, true) 144 | t.assert.deepStrictEqual(restartOptions, expectedRestartOptions) 145 | }) 146 | 147 | await app.restart(expectedRestartOptions) 148 | 149 | t.assert.strictEqual(app.restarted, true) 150 | }) 151 | 152 | test('should throw if onRestartHook is not a function', async (t) => { 153 | t.plan(1) 154 | 155 | async function createApplication (fastify, opts) { 156 | return fastify(opts) 157 | } 158 | 159 | const app = await restartable(createApplication, { 160 | keepAliveTimeout: 1 161 | }) 162 | 163 | t.after(async () => { 164 | await app.close() 165 | }) 166 | 167 | t.assert.throws(() => { 168 | app.addOnRestartHook('not a function') 169 | }, { message: 'The hook must be a function' }) 170 | }) 171 | 172 | test('should not throw if onRestartHook is a sync function', async (t) => { 173 | t.plan(3) 174 | 175 | async function createApplication (fastify, opts) { 176 | return fastify(opts) 177 | } 178 | 179 | const app = await restartable(createApplication, { 180 | keepAliveTimeout: 1 181 | }) 182 | 183 | t.after(async () => { 184 | await app.close() 185 | }) 186 | 187 | const expectedRestartOptions = { foo: 'bar' } 188 | 189 | app.addOnRestartHook((app, restartOptions) => { 190 | t.assert.strictEqual(app.restarted, true) 191 | t.assert.deepStrictEqual(restartOptions, expectedRestartOptions) 192 | }) 193 | 194 | await app.restart(expectedRestartOptions) 195 | 196 | t.assert.strictEqual(app.restarted, true) 197 | }) 198 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const defaultFastify = require('fastify') 4 | const getServerInstance = require('./lib/server') 5 | 6 | const closingServer = Symbol('closingServer') 7 | 8 | async function restartable (factory, opts, fastify = defaultFastify) { 9 | const proxy = { then: undefined } 10 | 11 | let app = await factory((opts) => createApplication(opts, false), opts) 12 | const server = wrapServer(app.server) 13 | 14 | let newHandler = null 15 | 16 | const preRestartHooks = [] 17 | const onRestartHooks = [] 18 | 19 | async function restart (restartOptions) { 20 | const requestListeners = server.listeners('request') 21 | const clientErrorListeners = server.listeners('clientError') 22 | 23 | await executeHooks(preRestartHooks, app, restartOptions) 24 | 25 | let newApp = null 26 | try { 27 | newApp = await factory(createApplication, opts, restartOptions) 28 | if (server.listening) { 29 | const { port, address } = server.address() 30 | await newApp.listen({ port, host: address }) 31 | } else { 32 | await newApp.ready() 33 | } 34 | } catch (error) { 35 | restoreClientErrorListeners(server, clientErrorListeners) 36 | 37 | // In case if fastify.listen() would throw an error 38 | /* c8 ignore next 3 */ 39 | if (newApp !== null) { 40 | await closeApplication(newApp) 41 | } 42 | throw error 43 | } 44 | 45 | server.on('request', newHandler) 46 | 47 | removeRequestListeners(server, requestListeners) 48 | removeClientErrorListeners(server, clientErrorListeners) 49 | 50 | Object.setPrototypeOf(proxy, newApp) 51 | await closeApplication(app) 52 | 53 | app = newApp 54 | 55 | executeHooks(onRestartHooks, newApp, restartOptions) 56 | } 57 | 58 | let debounce = null 59 | // TODO: think about queueing restarts with different options 60 | async function debounceRestart (...args) { 61 | if (debounce === null) { 62 | debounce = restart(...args).finally(() => { debounce = null }) 63 | } 64 | return debounce 65 | } 66 | 67 | let serverCloseCounter = 0 68 | let closingRestartable = false 69 | 70 | function createApplication (newOpts, isRestarted = true) { 71 | opts = newOpts 72 | 73 | let createServerCounter = 0 74 | function serverFactory (handler, options) { 75 | // this cause an uncaughtException because of the bug in Fastify 76 | // see: https://github.com/fastify/fastify/issues/4730 77 | /* c8 ignore next 6 */ 78 | if (++createServerCounter > 1) { 79 | throw new Error( 80 | 'Cannot create multiple server bindings for a restartable application. ' + 81 | 'Please specify an IP address as a host parameter to the fastify.listen()' 82 | ) 83 | } 84 | 85 | if (isRestarted) { 86 | newHandler = handler 87 | return server 88 | } 89 | return getServerInstance(options, handler) 90 | } 91 | 92 | const app = fastify({ ...newOpts, serverFactory }) 93 | 94 | if (!isRestarted) { 95 | Object.setPrototypeOf(proxy, app) 96 | } 97 | 98 | app.decorate('restart', debounceRestart) 99 | 100 | app.decorate('addPreRestartHook', (hook) => { 101 | if (typeof hook !== 'function') { 102 | throw new TypeError('The hook must be a function') 103 | } 104 | preRestartHooks.push(hook) 105 | }) 106 | 107 | app.decorate('addOnRestartHook', (hook) => { 108 | if (typeof hook !== 'function') { 109 | throw new TypeError('The hook must be a function') 110 | } 111 | onRestartHooks.push(hook) 112 | }) 113 | 114 | app.decorate('restarted', { 115 | getter: () => isRestarted 116 | }) 117 | app.decorate('persistentRef', { 118 | getter: () => proxy 119 | }) 120 | app.decorate('closingRestartable', { 121 | getter: () => closingRestartable 122 | }) 123 | 124 | app.addHook('preClose', async () => { 125 | if (++serverCloseCounter > 0) { 126 | closingRestartable = true 127 | server[closingServer] = true 128 | } 129 | }) 130 | 131 | return app 132 | } 133 | 134 | async function closeApplication (app) { 135 | serverCloseCounter-- 136 | await app.close() 137 | } 138 | 139 | return proxy 140 | } 141 | 142 | function wrapServer (server) { 143 | const _listen = server.listen.bind(server) 144 | 145 | server.listen = (...args) => { 146 | if (server.listening) { 147 | server.emit('listening') 148 | } else { 149 | return _listen(...args) 150 | } 151 | } 152 | 153 | server[closingServer] = false 154 | 155 | const _close = server.close.bind(server) 156 | server.close = (cb) => server[closingServer] ? _close(cb) : cb() 157 | 158 | /* c8 ignore next 5 */ 159 | // closeAllConnections was added in Nodejs v18.2.0 160 | if (server.closeAllConnections) { 161 | const _closeAllConnections = server.closeAllConnections.bind(server) 162 | server.closeAllConnections = () => server[closingServer] && _closeAllConnections() 163 | } 164 | 165 | /* c8 ignore next 5 */ 166 | // closeIdleConnections was added in Nodejs v18.2.0 167 | if (server.closeIdleConnections) { 168 | const _closeIdleConnections = server.closeIdleConnections.bind(server) 169 | server.closeIdleConnections = () => server[closingServer] && _closeIdleConnections() 170 | } 171 | 172 | return server 173 | } 174 | 175 | function removeRequestListeners (server, listeners) { 176 | for (const listener of listeners) { 177 | server.removeListener('request', listener) 178 | } 179 | } 180 | 181 | function removeClientErrorListeners (server, listeners) { 182 | for (const listener of listeners) { 183 | server.removeListener('clientError', listener) 184 | } 185 | } 186 | 187 | function restoreClientErrorListeners (server, oldListeners) { 188 | // Creating a new Fastify apps adds one clientError listener 189 | // Let's remove all the new ones 190 | const listeners = server.listeners('clientError') 191 | for (const listener of listeners) { 192 | if (!oldListeners.includes(listener)) { 193 | server.removeListener('clientError', listener) 194 | } 195 | } 196 | } 197 | 198 | async function executeHooks (hooks, app, opts) { 199 | for (const hook of hooks) { 200 | await hook(app, opts)?.catch((error) => app.log.error(error)) 201 | } 202 | } 203 | 204 | module.exports = restartable 205 | module.exports.default = restartable 206 | module.exports.restartable = restartable 207 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { join } = require('node:path') 4 | const { once } = require('node:events') 5 | const { readFile } = require('node:fs/promises') 6 | const http2 = require('node:http2') 7 | 8 | const { afterEach, test } = require('node:test') 9 | const split = require('split2') 10 | const { request, setGlobalDispatcher, Agent } = require('undici') 11 | 12 | const { restartable } = require('..') 13 | 14 | setGlobalDispatcher(new Agent({ 15 | keepAliveTimeout: 1, 16 | keepAliveMaxTimeout: 1, 17 | tls: { 18 | rejectUnauthorized: false 19 | } 20 | })) 21 | 22 | const COMMON_PORT = 4242 23 | 24 | afterEach(async () => { 25 | await new Promise((resolve) => setTimeout(resolve, 10)) 26 | }) 27 | 28 | test('should create and restart fastify app', async (t) => { 29 | async function createApplication (fastify, opts) { 30 | const app = fastify(opts) 31 | 32 | app.get('/', async () => { 33 | return { hello: 'world' } 34 | }) 35 | 36 | return app 37 | } 38 | 39 | const app = await restartable(createApplication, { 40 | keepAliveTimeout: 1 41 | }) 42 | 43 | t.after(async () => { 44 | await app.close() 45 | }) 46 | 47 | const host = await app.listen({ host: '127.0.0.1', port: COMMON_PORT }) 48 | t.assert.strictEqual(host, `http://127.0.0.1:${COMMON_PORT}`) 49 | t.assert.strictEqual(app.addresses()[0].address, '127.0.0.1') 50 | t.assert.strictEqual(app.addresses()[0].port, COMMON_PORT) 51 | 52 | t.assert.strictEqual(app.restarted, false) 53 | 54 | { 55 | const res = await request(host) 56 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 57 | } 58 | 59 | await app.restart() 60 | t.assert.deepStrictEqual(app.restarted, true) 61 | 62 | { 63 | const res = await request(host) 64 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 65 | } 66 | }) 67 | 68 | test('should create and restart fastify app twice', async (t) => { 69 | t.plan(15) 70 | 71 | let closingRestartable = false 72 | 73 | async function createApplication (fastify, opts) { 74 | const app = fastify(opts) 75 | 76 | app.get('/', async () => { 77 | return { hello: 'world' } 78 | }) 79 | 80 | let closeCounter = 0 81 | app.addHook('onClose', async () => { 82 | if (++closeCounter > 1) { 83 | t.fail('onClose hook called more than once') 84 | } 85 | t.assert.strictEqual(app.closingRestartable, closingRestartable) 86 | t.assert.ok('onClose hook called') 87 | }) 88 | 89 | return app 90 | } 91 | 92 | const app = await restartable(createApplication, { 93 | keepAliveTimeout: 1 94 | }) 95 | 96 | const host = await app.listen({ host: '127.0.0.1', port: COMMON_PORT }) 97 | t.assert.strictEqual(host, `http://127.0.0.1:${COMMON_PORT}`) 98 | t.assert.strictEqual(app.addresses()[0].address, '127.0.0.1') 99 | t.assert.strictEqual(app.addresses()[0].port, COMMON_PORT) 100 | 101 | t.assert.strictEqual(app.restarted, false) 102 | 103 | { 104 | const res = await request(host) 105 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 106 | } 107 | 108 | await app.restart() 109 | t.assert.deepStrictEqual(app.restarted, true) 110 | 111 | { 112 | const res = await request(host) 113 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 114 | } 115 | 116 | await app.restart() 117 | t.assert.deepStrictEqual(app.restarted, true) 118 | 119 | { 120 | const res = await request(host) 121 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 122 | } 123 | 124 | closingRestartable = true 125 | await app.close() 126 | }) 127 | 128 | test('should create and restart fastify https app', async (t) => { 129 | async function createApplication (fastify, opts) { 130 | const app = fastify(opts) 131 | 132 | app.get('/', async () => { 133 | return { hello: 'world' } 134 | }) 135 | 136 | return app 137 | } 138 | 139 | const opts = { 140 | https: { 141 | key: await readFile(join(__dirname, 'fixtures', 'key.pem')), 142 | cert: await readFile(join(__dirname, 'fixtures', 'cert.pem')) 143 | }, 144 | keepAliveTimeout: 1, 145 | maxRequestsPerSocket: 42 146 | } 147 | const app = await restartable(createApplication, opts) 148 | 149 | t.after(async () => { 150 | await app.close() 151 | }) 152 | 153 | const host = await app.listen({ host: '127.0.0.1', port: COMMON_PORT }) 154 | t.assert.strictEqual(host, `https://127.0.0.1:${COMMON_PORT}`) 155 | t.assert.strictEqual(app.addresses()[0].address, '127.0.0.1') 156 | t.assert.strictEqual(app.addresses()[0].port, COMMON_PORT) 157 | 158 | { 159 | const res = await request(host) 160 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 161 | } 162 | 163 | await app.restart() 164 | t.assert.deepStrictEqual(app.restarted, true) 165 | 166 | { 167 | const res = await request(host) 168 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 169 | } 170 | }) 171 | 172 | test('should create and restart fastify http2 app', async (t) => { 173 | async function createApplication (fastify, opts) { 174 | const app = fastify(opts) 175 | 176 | app.get('/', async () => { 177 | return { hello: 'world' } 178 | }) 179 | 180 | return app 181 | } 182 | 183 | const opts = { 184 | http2: true, 185 | http2SessionTimeout: 1000, 186 | keepAliveTimeout: 1 187 | } 188 | const app = await restartable(createApplication, opts) 189 | 190 | t.after(async () => { 191 | await app.close() 192 | }) 193 | 194 | const host = await app.listen({ host: '127.0.0.1', port: COMMON_PORT }) 195 | t.assert.strictEqual(host, `http://127.0.0.1:${COMMON_PORT}`) 196 | t.assert.strictEqual(app.addresses()[0].address, '127.0.0.1') 197 | t.assert.strictEqual(app.addresses()[0].port, COMMON_PORT) 198 | 199 | const client = http2.connect(host) 200 | 201 | t.after(() => { 202 | client.close() 203 | }) 204 | 205 | { 206 | const req = client.request({ ':path': '/' }) 207 | req.setEncoding('utf8') 208 | 209 | let data = '' 210 | req.on('data', (chunk) => { data += chunk }) 211 | await once(req, 'end') 212 | req.end() 213 | 214 | t.assert.deepStrictEqual(JSON.parse(data), { hello: 'world' }) 215 | } 216 | 217 | await app.restart() 218 | t.assert.deepStrictEqual(app.restarted, true) 219 | 220 | { 221 | const req = client.request({ ':path': '/' }) 222 | req.setEncoding('utf8') 223 | 224 | let data = '' 225 | req.on('data', (chunk) => { data += chunk }) 226 | await once(req, 'end') 227 | req.end() 228 | 229 | t.assert.deepStrictEqual(JSON.parse(data), { hello: 'world' }) 230 | } 231 | }) 232 | 233 | test('should create and restart fastify https2 app', async (t) => { 234 | async function createApplication (fastify, opts) { 235 | const app = fastify(opts) 236 | 237 | app.get('/', async () => { 238 | return { hello: 'world' } 239 | }) 240 | 241 | return app 242 | } 243 | 244 | const opts = { 245 | http2: true, 246 | https: { 247 | key: await readFile(join(__dirname, 'fixtures', 'key.pem')), 248 | cert: await readFile(join(__dirname, 'fixtures', 'cert.pem')) 249 | }, 250 | keepAliveTimeout: 1 251 | } 252 | const app = await restartable(createApplication, opts) 253 | 254 | t.after(async () => { 255 | await app.close() 256 | }) 257 | 258 | const host = await app.listen({ host: '127.0.0.1', port: COMMON_PORT }) 259 | t.assert.strictEqual(host, `https://127.0.0.1:${COMMON_PORT}`) 260 | t.assert.strictEqual(app.addresses()[0].address, '127.0.0.1') 261 | t.assert.strictEqual(app.addresses()[0].port, COMMON_PORT) 262 | 263 | await app.restart() 264 | t.assert.deepStrictEqual(app.restarted, true) 265 | }) 266 | 267 | test('should restart an app from a route handler', async (t) => { 268 | async function createApplication (fastify, opts) { 269 | const app = fastify(opts) 270 | 271 | app.get('/restart', async () => { 272 | await app.restart() 273 | return { hello: 'world' } 274 | }) 275 | 276 | return app 277 | } 278 | 279 | const app = await restartable(createApplication, { 280 | keepAliveTimeout: 1 281 | }) 282 | 283 | t.after(async () => { 284 | await app.close() 285 | }) 286 | 287 | const host = await app.listen({ host: '127.0.0.1', port: 0 }) 288 | 289 | { 290 | const res = await request(`${host}/restart`) 291 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 292 | } 293 | 294 | t.assert.deepStrictEqual(app.restarted, true) 295 | 296 | { 297 | const res = await request(`${host}/restart`) 298 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 299 | } 300 | }) 301 | 302 | test('should restart an app from inject call', async (t) => { 303 | async function createApplication (fastify, opts) { 304 | const app = fastify(opts) 305 | 306 | app.get('/restart', async () => { 307 | await app.restart() 308 | return { hello: 'world' } 309 | }) 310 | 311 | return app 312 | } 313 | 314 | const app = await restartable(createApplication, { 315 | keepAliveTimeout: 1 316 | }) 317 | t.assert.deepStrictEqual(app.server.listening, false) 318 | 319 | { 320 | const res = await app.inject('/restart') 321 | t.assert.deepStrictEqual(res.json(), { hello: 'world' }) 322 | } 323 | 324 | t.assert.deepStrictEqual(app.restarted, true) 325 | t.assert.deepStrictEqual(app.server.listening, false) 326 | 327 | { 328 | const res = await app.inject('/restart') 329 | t.assert.deepStrictEqual(res.json(), { hello: 'world' }) 330 | } 331 | }) 332 | 333 | test('logger', async (t) => { 334 | async function createApplication (fastify, opts) { 335 | const app = fastify(opts) 336 | 337 | app.get('/', async () => { 338 | return { hello: 'world' } 339 | }) 340 | 341 | return app 342 | } 343 | 344 | const stream = split(JSON.parse) 345 | const opts = { 346 | logger: { stream }, 347 | keepAliveTimeout: 1 348 | } 349 | 350 | const app = await restartable(createApplication, opts) 351 | 352 | t.after(async () => { 353 | await app.close() 354 | }) 355 | 356 | const host = await app.listen({ host: '127.0.0.1', port: 0 }) 357 | 358 | { 359 | const [{ level, msg }] = await once(stream, 'data') 360 | t.assert.strictEqual(level, 30) 361 | t.assert.strictEqual(msg, `Server listening at ${host}`) 362 | } 363 | }) 364 | 365 | test('should save new default options after restart', async (t) => { 366 | const opts1 = { 367 | keepAliveTimeout: 1, 368 | requestTimeout: 1000 369 | } 370 | const opts2 = { 371 | keepAliveTimeout: 1, 372 | requestTimeout: 2000 373 | } 374 | 375 | let restartCounter = 0 376 | const expectedOpts = [opts1, opts2] 377 | 378 | async function createApplication (fastify, opts) { 379 | const expected = expectedOpts[restartCounter++] 380 | t.assert.deepStrictEqual(opts, expected) 381 | 382 | const newOpts = expectedOpts[restartCounter] 383 | const app = fastify(newOpts) 384 | 385 | app.get('/', async () => { 386 | return { hello: 'world' } 387 | }) 388 | 389 | return app 390 | } 391 | 392 | const app = await restartable(createApplication, opts1) 393 | 394 | t.after(async () => { 395 | await app.close() 396 | }) 397 | 398 | await app.listen({ host: '127.0.0.1', port: 0 }) 399 | await app.restart() 400 | }) 401 | 402 | test('should send a restart options', async (t) => { 403 | const restartOpts1 = undefined 404 | const restartOpts2 = { foo: 'bar' } 405 | 406 | let restartCounter = 0 407 | const expectedOpts = [restartOpts1, restartOpts2] 408 | 409 | async function createApplication (fastify, opts, restartOpts) { 410 | const expected = expectedOpts[restartCounter++] 411 | t.assert.deepStrictEqual(restartOpts, expected) 412 | 413 | const app = fastify(opts) 414 | 415 | app.get('/', async () => { 416 | return { hello: 'world' } 417 | }) 418 | 419 | return app 420 | } 421 | 422 | const app = await restartable(createApplication, { 423 | keepAliveTimeout: 1 424 | }) 425 | 426 | t.after(async () => { 427 | await app.close() 428 | }) 429 | 430 | await app.listen({ host: '127.0.0.1', port: 0 }) 431 | await app.restart(restartOpts2) 432 | }) 433 | 434 | test('no warnings', async (t) => { 435 | async function createApplication (fastify, opts) { 436 | const app = fastify(opts) 437 | 438 | app.get('/', async () => { 439 | return { hello: 'world' } 440 | }) 441 | 442 | return app 443 | } 444 | 445 | const onWarning = (warning) => { 446 | t.fail(warning.message) 447 | } 448 | 449 | process.on('warning', onWarning) 450 | 451 | t.after(async () => { 452 | process.removeListener('warning', onWarning) 453 | }) 454 | 455 | const app = await restartable(createApplication) 456 | 457 | t.after(async () => { 458 | await app.close() 459 | }) 460 | 461 | await app.listen({ host: '127.0.0.1', port: 0 }) 462 | 463 | for (let i = 0; i < 11; i++) { 464 | await app.restart() 465 | } 466 | }) 467 | 468 | test('should not restart fastify after a failed start', async (t) => { 469 | let count = 0 470 | 471 | async function createApplication (fastify, opts) { 472 | const app = fastify(opts) 473 | 474 | app.register(async function () { 475 | if (count++ % 2) { 476 | throw new Error('kaboom') 477 | } 478 | }) 479 | 480 | app.get('/', async () => { 481 | return { hello: 'world' } 482 | }) 483 | 484 | return app 485 | } 486 | 487 | const app = await restartable(createApplication, { 488 | keepAliveTimeout: 1 489 | }) 490 | 491 | t.assert.deepStrictEqual(app.restarted, false) 492 | 493 | t.after(async () => { 494 | await app.close() 495 | }) 496 | 497 | const host = await app.listen({ host: '127.0.0.1', port: 0 }) 498 | 499 | { 500 | const res = await request(host) 501 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 502 | } 503 | 504 | await t.assert.rejects(app.restart()) 505 | 506 | { 507 | const res = await request(host) 508 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 509 | } 510 | 511 | await app.restart() 512 | 513 | const res = await request(host, { method: 'GET' }) 514 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 515 | }) 516 | 517 | test('should create and restart fastify app with forceCloseConnections', async (t) => { 518 | async function createApplication (fastify, opts) { 519 | const app = fastify(opts) 520 | 521 | app.get('/', async () => { 522 | return { hello: 'world' } 523 | }) 524 | 525 | return app 526 | } 527 | 528 | const app = await restartable(createApplication, { 529 | forceCloseConnections: true, 530 | keepAliveTimeout: 1 531 | }) 532 | 533 | t.after(async () => { 534 | await app.close() 535 | }) 536 | 537 | const host = await app.listen({ host: '127.0.0.1', port: 0 }) 538 | t.assert.strictEqual(app.restarted, false) 539 | 540 | { 541 | const res = await request(host) 542 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 543 | } 544 | 545 | await app.restart() 546 | t.assert.deepStrictEqual(app.restarted, true) 547 | 548 | { 549 | const res = await request(host) 550 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 551 | } 552 | }) 553 | 554 | test('should not set the server handler before application is ready', async (t) => { 555 | let restartCounter = 0 556 | 557 | async function createApplication (fastify, opts) { 558 | const app = fastify(opts) 559 | 560 | if (app.restarted) { 561 | const res = await request(host) 562 | t.assert.deepStrictEqual(await res.body.json(), { version: 1 }) 563 | } 564 | 565 | app.get('/', async () => { 566 | return { version: restartCounter } 567 | }) 568 | 569 | restartCounter++ 570 | return app 571 | } 572 | 573 | const app = await restartable(createApplication, { 574 | keepAliveTimeout: 1 575 | }) 576 | 577 | t.after(async () => { 578 | await app.close() 579 | }) 580 | 581 | const host = await app.listen({ host: '127.0.0.1', port: 0 }) 582 | 583 | { 584 | const res = await request(host) 585 | t.assert.deepStrictEqual(await res.body.json(), { version: 1 }) 586 | } 587 | 588 | await app.restart() 589 | t.assert.deepStrictEqual(app.restarted, true) 590 | 591 | { 592 | const res = await request(host) 593 | t.assert.deepStrictEqual(await res.body.json(), { version: 2 }) 594 | } 595 | }) 596 | 597 | test('should not restart an application multiple times simultaneously', async (t) => { 598 | let startCounter = 0 599 | 600 | async function createApplication (fastify, opts) { 601 | startCounter++ 602 | 603 | const app = fastify(opts) 604 | 605 | app.get('/', async () => { 606 | return { hello: 'world' } 607 | }) 608 | 609 | await new Promise((resolve) => setTimeout(resolve, 500)) 610 | return app 611 | } 612 | 613 | const app = await restartable(createApplication, { 614 | keepAliveTimeout: 1 615 | }) 616 | 617 | t.after(async () => { 618 | await app.close() 619 | }) 620 | 621 | const host = await app.listen({ host: '127.0.0.1', port: 0 }) 622 | 623 | await Promise.all([ 624 | app.restart(), 625 | app.restart(), 626 | app.restart(), 627 | app.restart(), 628 | app.restart() 629 | ]) 630 | 631 | t.assert.deepStrictEqual(app.restarted, true) 632 | t.assert.deepStrictEqual(startCounter, 2) 633 | 634 | { 635 | const res = await request(host) 636 | t.assert.deepStrictEqual(await res.body.json(), { hello: 'world' }) 637 | } 638 | }) 639 | 640 | test('should contain a persistentRef property', async (t) => { 641 | let firstPersistentRef = null 642 | 643 | async function createApplication (fastify, opts) { 644 | const app = fastify(opts) 645 | 646 | if (app.restarted) { 647 | t.assert.strictEqual(app.persistentRef, proxy) 648 | } else { 649 | firstPersistentRef = app.persistentRef 650 | } 651 | 652 | return app 653 | } 654 | 655 | const proxy = await restartable(createApplication, { 656 | keepAliveTimeout: 1 657 | }) 658 | 659 | t.assert.strictEqual(firstPersistentRef, proxy) 660 | 661 | t.after(async () => { 662 | await proxy.close() 663 | }) 664 | 665 | await proxy.listen({ host: '127.0.0.1', port: 0 }) 666 | 667 | t.assert.strictEqual(proxy.persistentRef, proxy) 668 | 669 | await proxy.restart() 670 | 671 | t.assert.strictEqual(proxy.persistentRef, proxy) 672 | }) 673 | 674 | test('server close event should be emitted only when after closing server', async (t) => { 675 | t.plan(2) 676 | 677 | async function createApplication (fastify, opts) { 678 | return fastify(opts) 679 | } 680 | 681 | const app = await restartable(createApplication, { 682 | keepAliveTimeout: 1 683 | }) 684 | await app.listen({ host: '127.0.0.1', port: 0 }) 685 | 686 | t.assert.ok(app.server.listening) 687 | 688 | app.server.on('close', () => { 689 | t.assert.ok('server close event emitted') 690 | }) 691 | 692 | await app.restart() 693 | await app.restart() 694 | await app.restart() 695 | await app.restart() 696 | await app.restart() 697 | 698 | await app.close() 699 | }) 700 | 701 | test('should close application during the restart', async (t) => { 702 | async function createApplication (fastify, opts) { 703 | const app = fastify(opts) 704 | 705 | app.addHook('onClose', async () => { 706 | await new Promise((resolve) => setTimeout(resolve, 1000)) 707 | }) 708 | 709 | return app 710 | } 711 | 712 | const app = await restartable(createApplication, { 713 | keepAliveTimeout: 1 714 | }) 715 | await app.listen({ host: '127.0.0.1', port: 0 }) 716 | 717 | t.assert.ok(app.server.listening) 718 | 719 | app.restart() 720 | await new Promise((resolve) => setTimeout(resolve, 500)) 721 | await app.close() 722 | 723 | t.assert.ok(!app.server.listening) 724 | }) 725 | 726 | test('should restart an app before listening', async (t) => { 727 | async function createApplication (fastify, opts) { 728 | return fastify(opts) 729 | } 730 | 731 | const app = await restartable(createApplication, { 732 | keepAliveTimeout: 1 733 | }) 734 | 735 | await app.restart() 736 | t.assert.ok(app.restarted) 737 | 738 | await app.listen({ host: '127.0.0.1', port: 0 }) 739 | t.assert.ok(app.server.listening) 740 | 741 | await app.close() 742 | t.assert.ok(!app.server.listening) 743 | }) 744 | --------------------------------------------------------------------------------