request-drain 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/RequestTracker.d.ts +36 -0
- package/dist/RequestTracker.d.ts.map +1 -0
- package/dist/RequestTracker.js +57 -0
- package/dist/RequestTracker.js.map +1 -0
- package/dist/ShutdownHandle.d.ts +48 -0
- package/dist/ShutdownHandle.d.ts.map +1 -0
- package/dist/ShutdownHandle.js +92 -0
- package/dist/ShutdownHandle.js.map +1 -0
- package/dist/ShutdownRegistry.d.ts +26 -0
- package/dist/ShutdownRegistry.d.ts.map +1 -0
- package/dist/ShutdownRegistry.js +54 -0
- package/dist/ShutdownRegistry.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +11 -0
- package/dist/utils.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pascal Pfeifer <pascal@pfeifer.zone>
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# request-drain
|
|
2
|
+
|
|
3
|
+
> Gracefully drain HTTP requests before shutting down a Node.js service.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/request-drain)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](https://codecov.io/gh/pfeiferio/request-drain)
|
|
10
|
+
|
|
11
|
+
`request-drain` tracks active HTTP requests and allows services to wait until all requests are finished before shutting down.
|
|
12
|
+
|
|
13
|
+
It is designed for graceful deployments where a process should stop accepting new requests, finish ongoing ones, and then exit cleanly.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What problem does this solve?
|
|
18
|
+
|
|
19
|
+
When deploying a new version of a service, existing HTTP requests should be allowed to finish before the process exits.
|
|
20
|
+
`request-drain` tracks active requests and allows services to wait until they are completed.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
* ✅ Graceful request draining for Node.js services
|
|
27
|
+
* ✅ Works with Express, Fastify, Koa or any Node.js HTTP framework
|
|
28
|
+
* ✅ No runtime dependencies
|
|
29
|
+
* ✅ Multiple middleware layers can track the same request safely
|
|
30
|
+
* ✅ Predictable shutdown behavior with optional timeout
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install request-drain
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
The recommended pattern is to encapsulate shutdown behavior inside each middleware. The middleware registers itself with the `ShutdownRegistry`, tracks incoming requests, and decides how long to wait for them to finish.
|
|
45
|
+
|
|
46
|
+
The request object must be a Node.js `IncomingMessage`. This is compatible with Express, Fastify, Koa and native HTTP servers.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import {ShutdownRegistry} from "request-drain"
|
|
50
|
+
|
|
51
|
+
const registry = new ShutdownRegistry()
|
|
52
|
+
|
|
53
|
+
const createMiddleware = (registry: ShutdownRegistry) => {
|
|
54
|
+
const handle = registry.register()
|
|
55
|
+
|
|
56
|
+
handle.onAbort(async () => {
|
|
57
|
+
await handle.waitUntilIdle(5_000)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return (req, res, next) => {
|
|
61
|
+
if (handle.isShutdown) {
|
|
62
|
+
res.status(503).end("Server shutting down")
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Registers the request with the handle.
|
|
68
|
+
* The request is automatically released when it closes.
|
|
69
|
+
*/
|
|
70
|
+
handle.request(req)
|
|
71
|
+
next()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
app.use(createMiddleware(registry))
|
|
76
|
+
|
|
77
|
+
const onShutdown = async () => {
|
|
78
|
+
await registry.shutdown()
|
|
79
|
+
process.exit(0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
process.on("SIGTERM", onShutdown)
|
|
83
|
+
process.on("SIGINT", onShutdown)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
In this example the middleware:
|
|
87
|
+
|
|
88
|
+
* tracks incoming requests
|
|
89
|
+
* stops accepting new requests during shutdown
|
|
90
|
+
* waits up to 5 seconds for active requests to finish
|
|
91
|
+
|
|
92
|
+
`registry.shutdown()` calls `onAbort` on all registered handles and waits for them to complete.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Multiple Middlewares
|
|
97
|
+
|
|
98
|
+
Each middleware registers its own handle. The registry ensures that a request passing through multiple middlewares is tracked independently per handle without double-counting within a handle.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const registry = new ShutdownRegistry()
|
|
102
|
+
|
|
103
|
+
app.use(createMiddleware(registry))
|
|
104
|
+
app.use(createSessionMiddleware(registry))
|
|
105
|
+
|
|
106
|
+
const onShutdown = async () => {
|
|
107
|
+
await registry.shutdown()
|
|
108
|
+
process.exit(0)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
process.on("SIGTERM", onShutdown)
|
|
112
|
+
process.on("SIGINT", onShutdown)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Optional: Global Shutdown Timeout
|
|
118
|
+
|
|
119
|
+
`request-drain` does not enforce a global shutdown timeout. Each middleware decides how long it waits via `waitUntilIdle()`.
|
|
120
|
+
|
|
121
|
+
If you want to enforce a maximum shutdown time for the entire process, add a safety timeout in your signal handler:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import {setTimeout as sleep} from "node:timers/promises"
|
|
125
|
+
|
|
126
|
+
const onShutdown = async () => {
|
|
127
|
+
await Promise.race([
|
|
128
|
+
registry.shutdown(),
|
|
129
|
+
sleep(30_000)
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
process.exit(0)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
process.on("SIGTERM", onShutdown)
|
|
136
|
+
process.on("SIGINT", onShutdown)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
This ensures the process exits even if a component fails to shut down in time.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Design Goals
|
|
144
|
+
|
|
145
|
+
* Minimal API surface
|
|
146
|
+
* Predictable shutdown behavior
|
|
147
|
+
* No framework coupling
|
|
148
|
+
* No global state
|
|
149
|
+
* No runtime dependencies
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks active request identifiers and emits an `idle` event
|
|
3
|
+
* when all tracked requests have been released.
|
|
4
|
+
*
|
|
5
|
+
* Used internally by {@link ShutdownHandle} to determine when
|
|
6
|
+
* all requests associated with a handle have completed.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RequestTracker {
|
|
9
|
+
#private;
|
|
10
|
+
/**
|
|
11
|
+
* Registers a callback that is executed once the tracker becomes idle.
|
|
12
|
+
*
|
|
13
|
+
* The callback fires when the last tracked request is released.
|
|
14
|
+
*/
|
|
15
|
+
onIdle(onIdle: () => void): this;
|
|
16
|
+
/**
|
|
17
|
+
* Removes a previously registered idle listener.
|
|
18
|
+
*/
|
|
19
|
+
offIdle(handle: () => void): this;
|
|
20
|
+
/**
|
|
21
|
+
* Releases a previously tracked request identifier.
|
|
22
|
+
*
|
|
23
|
+
* If this call removes the last active request, the tracker
|
|
24
|
+
* emits an `idle` event.
|
|
25
|
+
*/
|
|
26
|
+
release(id: symbol): this;
|
|
27
|
+
/**
|
|
28
|
+
* Marks a request identifier as active.
|
|
29
|
+
*/
|
|
30
|
+
track(id: symbol): this;
|
|
31
|
+
/**
|
|
32
|
+
* Indicates whether no requests are currently tracked.
|
|
33
|
+
*/
|
|
34
|
+
get isEmpty(): boolean;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=RequestTracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RequestTracker.d.ts","sourceRoot":"","sources":["../src/RequestTracker.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,cAAc;;IAKzB;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,IAAI;IAKzB;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,IAAI;IAK1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAUzB;;OAEG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKvB;;OAEG;IACH,IAAI,OAAO,YAEV;CACF"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
/**
|
|
3
|
+
* Tracks active request identifiers and emits an `idle` event
|
|
4
|
+
* when all tracked requests have been released.
|
|
5
|
+
*
|
|
6
|
+
* Used internally by {@link ShutdownHandle} to determine when
|
|
7
|
+
* all requests associated with a handle have completed.
|
|
8
|
+
*/
|
|
9
|
+
export class RequestTracker {
|
|
10
|
+
#store = new Set();
|
|
11
|
+
#event = new EventEmitter();
|
|
12
|
+
/**
|
|
13
|
+
* Registers a callback that is executed once the tracker becomes idle.
|
|
14
|
+
*
|
|
15
|
+
* The callback fires when the last tracked request is released.
|
|
16
|
+
*/
|
|
17
|
+
onIdle(onIdle) {
|
|
18
|
+
this.#event.once('idle', onIdle);
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Removes a previously registered idle listener.
|
|
23
|
+
*/
|
|
24
|
+
offIdle(handle) {
|
|
25
|
+
this.#event.removeListener('idle', handle);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Releases a previously tracked request identifier.
|
|
30
|
+
*
|
|
31
|
+
* If this call removes the last active request, the tracker
|
|
32
|
+
* emits an `idle` event.
|
|
33
|
+
*/
|
|
34
|
+
release(id) {
|
|
35
|
+
if (!this.#store.has(id))
|
|
36
|
+
return this;
|
|
37
|
+
this.#store.delete(id);
|
|
38
|
+
if (this.isEmpty) {
|
|
39
|
+
this.#event.emit('idle');
|
|
40
|
+
}
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Marks a request identifier as active.
|
|
45
|
+
*/
|
|
46
|
+
track(id) {
|
|
47
|
+
this.#store.add(id);
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Indicates whether no requests are currently tracked.
|
|
52
|
+
*/
|
|
53
|
+
get isEmpty() {
|
|
54
|
+
return this.#store.size === 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=RequestTracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RequestTracker.js","sourceRoot":"","sources":["../src/RequestTracker.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,OAAO,cAAc;IAEhB,MAAM,GAAgB,IAAI,GAAG,EAAE,CAAA;IAC/B,MAAM,GAAG,IAAI,YAAY,EAAE,CAAA;IAEpC;;;;OAIG;IACH,MAAM,CAAC,MAAkB;QACvB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;OAEG;IACH,OAAO,CAAC,MAAkB;QACxB,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC1C,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;OAKG;IACH,OAAO,CAAC,EAAU;QAChB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,IAAI,CAAA;QACrC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACtB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC1B,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,EAAU;QACd,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACnB,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;OAEG;IACH,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,CAAA;IAC/B,CAAC;CACF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { OnAbortFn, RequestConfigs } from "./types.js";
|
|
2
|
+
import type { RequestTracker } from "./RequestTracker.js";
|
|
3
|
+
import type { IncomingMessage } from "node:http";
|
|
4
|
+
export declare const SHUTDOWN_KEY: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* Represents a middleware-scoped shutdown controller.
|
|
7
|
+
*
|
|
8
|
+
* A handle tracks requests passing through a middleware and
|
|
9
|
+
* allows the middleware to participate in a coordinated shutdown.
|
|
10
|
+
*
|
|
11
|
+
* Each middleware typically registers its own handle via
|
|
12
|
+
* {@link ShutdownRegistry.register}.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ShutdownHandle {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(requestTracker: RequestTracker, requestConfigs: RequestConfigs, onRequestOpen: (id: symbol) => void, onRequestClose: (id: symbol) => void);
|
|
17
|
+
/**
|
|
18
|
+
* Returns true if no requests are currently tracked by this handle.
|
|
19
|
+
*/
|
|
20
|
+
get isIdle(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Waits until all tracked requests have completed or the timeout expires.
|
|
23
|
+
*
|
|
24
|
+
* @param ttl Maximum time to wait in milliseconds.
|
|
25
|
+
*/
|
|
26
|
+
waitUntilIdle(ttl: number): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Indicates whether the shutdown process has started.
|
|
29
|
+
*
|
|
30
|
+
* Middlewares can use this flag to reject new requests.
|
|
31
|
+
*/
|
|
32
|
+
get isShutdown(): boolean;
|
|
33
|
+
[SHUTDOWN_KEY](): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Registers a function that will be executed when shutdown begins.
|
|
36
|
+
*
|
|
37
|
+
* Typically used to wait for active requests to finish or to
|
|
38
|
+
* release external resources.
|
|
39
|
+
*/
|
|
40
|
+
onAbort(fn: OnAbortFn): void;
|
|
41
|
+
/**
|
|
42
|
+
* Registers a request with this handle.
|
|
43
|
+
*
|
|
44
|
+
* The request is automatically released when its `close` event fires.
|
|
45
|
+
*/
|
|
46
|
+
request(req: IncomingMessage): void;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=ShutdownHandle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShutdownHandle.d.ts","sourceRoot":"","sources":["../src/ShutdownHandle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAE,cAAc,EAAC,MAAM,YAAY,CAAC;AAC1D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,WAAW,CAAC;AAE/C,eAAO,MAAM,YAAY,eAAqB,CAAA;AAE9C;;;;;;;;GAQG;AACH,qBAAa,cAAc;;gBAUvB,cAAc,EAAE,cAAc,EAC9B,cAAc,EAAE,cAAc,EAC9B,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,EACnC,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI;IAQtC;;OAEG;IACH,IAAI,MAAM,YAET;IAED;;;;OAIG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB/C;;;;OAIG;IACH,IAAI,UAAU,YAEb;IAEK,CAAC,YAAY,CAAC;IAOpB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,EAAE,SAAS;IAIrB;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,eAAe;CAY7B"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createRequestConfig } from "./utils.js";
|
|
2
|
+
export const SHUTDOWN_KEY = Symbol('shutdown');
|
|
3
|
+
/**
|
|
4
|
+
* Represents a middleware-scoped shutdown controller.
|
|
5
|
+
*
|
|
6
|
+
* A handle tracks requests passing through a middleware and
|
|
7
|
+
* allows the middleware to participate in a coordinated shutdown.
|
|
8
|
+
*
|
|
9
|
+
* Each middleware typically registers its own handle via
|
|
10
|
+
* {@link ShutdownRegistry.register}.
|
|
11
|
+
*/
|
|
12
|
+
export class ShutdownHandle {
|
|
13
|
+
#abortFns = [];
|
|
14
|
+
#requestConfigs;
|
|
15
|
+
#onRequestClose;
|
|
16
|
+
#onRequestOpen;
|
|
17
|
+
#requestTracker;
|
|
18
|
+
#shutdown = false;
|
|
19
|
+
constructor(requestTracker, requestConfigs, onRequestOpen, onRequestClose) {
|
|
20
|
+
this.#requestConfigs = requestConfigs;
|
|
21
|
+
this.#requestTracker = requestTracker;
|
|
22
|
+
this.#onRequestClose = onRequestClose;
|
|
23
|
+
this.#onRequestOpen = onRequestOpen;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns true if no requests are currently tracked by this handle.
|
|
27
|
+
*/
|
|
28
|
+
get isIdle() {
|
|
29
|
+
return this.#requestTracker.isEmpty;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Waits until all tracked requests have completed or the timeout expires.
|
|
33
|
+
*
|
|
34
|
+
* @param ttl Maximum time to wait in milliseconds.
|
|
35
|
+
*/
|
|
36
|
+
async waitUntilIdle(ttl) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
if (this.isIdle) {
|
|
39
|
+
return resolve();
|
|
40
|
+
}
|
|
41
|
+
const onIdle = () => {
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
resolve();
|
|
44
|
+
};
|
|
45
|
+
const timeout = setTimeout(() => {
|
|
46
|
+
resolve();
|
|
47
|
+
this.#requestTracker.offIdle(onIdle);
|
|
48
|
+
}, ttl).unref();
|
|
49
|
+
this.#requestTracker.onIdle(onIdle);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Indicates whether the shutdown process has started.
|
|
54
|
+
*
|
|
55
|
+
* Middlewares can use this flag to reject new requests.
|
|
56
|
+
*/
|
|
57
|
+
get isShutdown() {
|
|
58
|
+
return this.#shutdown;
|
|
59
|
+
}
|
|
60
|
+
async [SHUTDOWN_KEY]() {
|
|
61
|
+
this.#shutdown = true;
|
|
62
|
+
for (const fn of this.#abortFns) {
|
|
63
|
+
await fn();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Registers a function that will be executed when shutdown begins.
|
|
68
|
+
*
|
|
69
|
+
* Typically used to wait for active requests to finish or to
|
|
70
|
+
* release external resources.
|
|
71
|
+
*/
|
|
72
|
+
onAbort(fn) {
|
|
73
|
+
this.#abortFns.push(fn);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Registers a request with this handle.
|
|
77
|
+
*
|
|
78
|
+
* The request is automatically released when its `close` event fires.
|
|
79
|
+
*/
|
|
80
|
+
request(req) {
|
|
81
|
+
const requestConfig = this.#requestConfigs.get(req) ?? createRequestConfig();
|
|
82
|
+
const id = requestConfig.id;
|
|
83
|
+
this.#requestTracker.track(id);
|
|
84
|
+
this.#onRequestOpen(id);
|
|
85
|
+
if (requestConfig.isNew) {
|
|
86
|
+
this.#requestConfigs.set(req, requestConfig);
|
|
87
|
+
requestConfig.isNew = false;
|
|
88
|
+
req.on('close', () => this.#onRequestClose(id));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=ShutdownHandle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShutdownHandle.js","sourceRoot":"","sources":["../src/ShutdownHandle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,mBAAmB,EAAC,MAAM,YAAY,CAAC;AAG/C,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,UAAU,CAAC,CAAA;AAE9C;;;;;;;;GAQG;AACH,MAAM,OAAO,cAAc;IAEhB,SAAS,GAAgB,EAAE,CAAA;IAC3B,eAAe,CAAgB;IAC/B,eAAe,CAAsB;IACrC,cAAc,CAAsB;IACpC,eAAe,CAAgB;IACxC,SAAS,GAAY,KAAK,CAAA;IAE1B,YACE,cAA8B,EAC9B,cAA8B,EAC9B,aAAmC,EACnC,cAAoC;QAEpC,IAAI,CAAC,eAAe,GAAG,cAAc,CAAA;QACrC,IAAI,CAAC,eAAe,GAAG,cAAc,CAAA;QACrC,IAAI,CAAC,eAAe,GAAG,cAAc,CAAA;QACrC,IAAI,CAAC,cAAc,GAAG,aAAa,CAAA;IACrC,CAAC;IAED;;OAEG;IACH,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,eAAe,CAAC,OAAO,CAAA;IACrC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,aAAa,CAAC,GAAW;QAC7B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChB,OAAO,OAAO,EAAE,CAAA;YAClB,CAAC;YAED,MAAM,MAAM,GAAG,GAAG,EAAE;gBAClB,YAAY,CAAC,OAAO,CAAC,CAAA;gBACrB,OAAO,EAAE,CAAA;YACX,CAAC,CAAA;YAED,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC9B,OAAO,EAAE,CAAA;gBACT,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;YACtC,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,EAAE,CAAA;YAEf,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACrC,CAAC,CAAC,CAAA;IACJ,CAAC;IAED;;;;OAIG;IACH,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,SAAS,CAAA;IACvB,CAAC;IAED,KAAK,CAAC,CAAC,YAAY,CAAC;QAClB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;QACrB,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAChC,MAAM,EAAE,EAAE,CAAA;QACZ,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,OAAO,CAAC,EAAa;QACnB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACzB,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,GAAoB;QAC1B,MAAM,aAAa,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,mBAAmB,EAAE,CAAA;QAC5E,MAAM,EAAE,GAAG,aAAa,CAAC,EAAE,CAAA;QAC3B,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QAC9B,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,CAAA;QAEvB,IAAI,aAAa,CAAC,KAAK,EAAE,CAAC;YACxB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;YAC5C,aAAa,CAAC,KAAK,GAAG,KAAK,CAAA;YAC3B,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC,CAAA;QACjD,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ShutdownHandle } from "./ShutdownHandle.js";
|
|
2
|
+
/**
|
|
3
|
+
* Coordinates shutdown across multiple middleware handles.
|
|
4
|
+
*
|
|
5
|
+
* The registry ensures that requests passing through multiple
|
|
6
|
+
* middlewares are tracked correctly and released across all
|
|
7
|
+
* associated trackers when the request closes.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ShutdownRegistry {
|
|
10
|
+
#private;
|
|
11
|
+
/**
|
|
12
|
+
* Initiates shutdown by invoking all registered abort handlers.
|
|
13
|
+
*
|
|
14
|
+
* Each handle receives the shutdown signal and executes its
|
|
15
|
+
* registered `onAbort` callbacks.
|
|
16
|
+
*/
|
|
17
|
+
shutdown(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Registers a new shutdown handle.
|
|
20
|
+
*
|
|
21
|
+
* Each middleware should create its own handle to track
|
|
22
|
+
* requests and define shutdown behavior.
|
|
23
|
+
*/
|
|
24
|
+
register(): ShutdownHandle;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=ShutdownRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShutdownRegistry.d.ts","sourceRoot":"","sources":["../src/ShutdownRegistry.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,cAAc,EAAC,MAAM,qBAAqB,CAAC;AAGjE;;;;;;GAMG;AACH,qBAAa,gBAAgB;;IAQ3B;;;;;OAKG;IACG,QAAQ;IAQd;;;;;OAKG;IACH,QAAQ,IAAI,cAAc;CAyB3B"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SHUTDOWN_KEY, ShutdownHandle } from "./ShutdownHandle.js";
|
|
2
|
+
import { RequestTracker } from "./RequestTracker.js";
|
|
3
|
+
/**
|
|
4
|
+
* Coordinates shutdown across multiple middleware handles.
|
|
5
|
+
*
|
|
6
|
+
* The registry ensures that requests passing through multiple
|
|
7
|
+
* middlewares are tracked correctly and released across all
|
|
8
|
+
* associated trackers when the request closes.
|
|
9
|
+
*/
|
|
10
|
+
export class ShutdownRegistry {
|
|
11
|
+
#requestConfigs = new WeakMap();
|
|
12
|
+
#handles = [];
|
|
13
|
+
#idRequestTrackerMap = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Initiates shutdown by invoking all registered abort handlers.
|
|
16
|
+
*
|
|
17
|
+
* Each handle receives the shutdown signal and executes its
|
|
18
|
+
* registered `onAbort` callbacks.
|
|
19
|
+
*/
|
|
20
|
+
async shutdown() {
|
|
21
|
+
const promises = [];
|
|
22
|
+
for (const handle of this.#handles) {
|
|
23
|
+
promises.push(handle[SHUTDOWN_KEY]());
|
|
24
|
+
}
|
|
25
|
+
await Promise.allSettled(promises);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Registers a new shutdown handle.
|
|
29
|
+
*
|
|
30
|
+
* Each middleware should create its own handle to track
|
|
31
|
+
* requests and define shutdown behavior.
|
|
32
|
+
*/
|
|
33
|
+
register() {
|
|
34
|
+
const requestTracker = new RequestTracker();
|
|
35
|
+
const handle = new ShutdownHandle(requestTracker, this.#requestConfigs, (id) => {
|
|
36
|
+
const set = this.#idRequestTrackerMap.get(id);
|
|
37
|
+
if (set) {
|
|
38
|
+
set.add(requestTracker);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
this.#idRequestTrackerMap.set(id, new Set([requestTracker]));
|
|
42
|
+
}
|
|
43
|
+
}, (id) => {
|
|
44
|
+
const trackers = this.#idRequestTrackerMap.get(id);
|
|
45
|
+
if (!trackers)
|
|
46
|
+
return;
|
|
47
|
+
trackers.forEach(requestTracker => requestTracker.release(id));
|
|
48
|
+
this.#idRequestTrackerMap.delete(id);
|
|
49
|
+
});
|
|
50
|
+
this.#handles.push(handle);
|
|
51
|
+
return handle;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=ShutdownRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShutdownRegistry.js","sourceRoot":"","sources":["../src/ShutdownRegistry.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,YAAY,EAAE,cAAc,EAAC,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,OAAO,gBAAgB;IAE3B,eAAe,GAAmB,IAAI,OAAO,EAAE,CAAA;IAE/C,QAAQ,GAAqB,EAAE,CAAA;IAE/B,oBAAoB,GAAqC,IAAI,GAAG,EAAE,CAAA;IAElE;;;;;OAKG;IACH,KAAK,CAAC,QAAQ;QACZ,MAAM,QAAQ,GAAG,EAAE,CAAA;QACnB,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;QACvC,CAAC;QACD,MAAM,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAA;IACpC,CAAC;IAED;;;;;OAKG;IACH,QAAQ;QACN,MAAM,cAAc,GAAG,IAAI,cAAc,EAAE,CAAA;QAE3C,MAAM,MAAM,GAAG,IAAI,cAAc,CAC/B,cAAc,EACd,IAAI,CAAC,eAAe,EACpB,CAAC,EAAE,EAAE,EAAE;YACL,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAC7C,IAAI,GAAG,EAAE,CAAC;gBACR,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;YACzB,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,GAAG,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAA;YAC9D,CAAC;QACH,CAAC,EACD,CAAC,EAAE,EAAE,EAAE;YACL,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAClD,IAAI,CAAC,QAAQ;gBAAE,OAAM;YACrB,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;YAC9D,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACtC,CAAC,CACF,CAAA;QAED,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC1B,OAAO,MAAM,CAAA;IACf,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AACvD,YAAY,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
export type RequestConfigs = WeakMap<IncomingMessage, RequestConfig>;
|
|
3
|
+
export type RequestConfig = {
|
|
4
|
+
isNew: boolean;
|
|
5
|
+
id: symbol;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Function executed when shutdown begins.
|
|
9
|
+
* May perform asynchronous cleanup.
|
|
10
|
+
*/
|
|
11
|
+
export type OnAbortFn = () => void | Promise<void>;
|
|
12
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,WAAW,CAAC;AAE/C,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,eAAe,EAAE,aAAa,CAAC,CAAA;AAEpE,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,OAAO,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;CACX,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,YAAY,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,mBAAmB,QAAO,aAKtC,CAAA"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAkB,EAAE;IACrD,OAAO;QACL,KAAK,EAAE,IAAI;QACX,EAAE,EAAE,MAAM,EAAE;KACb,CAAA;AACH,CAAC,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "request-drain",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Gracefully drain HTTP requests before shutting down a Node.js service.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Pascal Pfeifer <pascal@pfeifer.zone>",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"graceful-shutdown",
|
|
24
|
+
"request-drain",
|
|
25
|
+
"http",
|
|
26
|
+
"middleware",
|
|
27
|
+
"nodejs",
|
|
28
|
+
"express",
|
|
29
|
+
"fastify",
|
|
30
|
+
"koa"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/pfeiferio/request-drain.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/pfeiferio/request-drain/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/pfeiferio/request-drain#readme",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"prepublishOnly": "npm test",
|
|
48
|
+
"build": "npm run clean && tsc",
|
|
49
|
+
"clean": "rm -rf dist",
|
|
50
|
+
"test": "npm run build && node --test",
|
|
51
|
+
"test:coverage": "npm run build && node --test --experimental-test-coverage --test-coverage-exclude='test/**'",
|
|
52
|
+
"test:coverage:c8": "npm run build && npx c8 node --test",
|
|
53
|
+
"test:coverage:lcov": "npm run build && npx c8 --src src node --enable-source-maps --test"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^20.17.9",
|
|
57
|
+
"typescript": "^5.9.3"
|
|
58
|
+
}
|
|
59
|
+
}
|