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 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
+ [![npm version](https://img.shields.io/npm/v/request-drain.svg)](https://www.npmjs.com/package/request-drain)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org/)
9
+ [![codecov](https://codecov.io/gh/pfeiferio/request-drain/branch/main/graph/badge.svg)](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"}
@@ -0,0 +1,4 @@
1
+ export type { OnAbortFn } from "./types.js";
2
+ export { ShutdownRegistry } from "./ShutdownRegistry.js";
3
+ export type { ShutdownHandle } from "./ShutdownHandle.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { ShutdownRegistry } from "./ShutdownRegistry.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAC"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ import type { RequestConfig } from "./types.js";
2
+ /**
3
+ * Creates a new request configuration used to track a request
4
+ * across multiple middleware handles.
5
+ */
6
+ export declare const createRequestConfig: () => RequestConfig;
7
+ //# sourceMappingURL=utils.d.ts.map
@@ -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,11 @@
1
+ /**
2
+ * Creates a new request configuration used to track a request
3
+ * across multiple middleware handles.
4
+ */
5
+ export const createRequestConfig = () => {
6
+ return {
7
+ isNew: true,
8
+ id: Symbol()
9
+ };
10
+ };
11
+ //# sourceMappingURL=utils.js.map
@@ -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
+ }