@nage-api/core 1.0.0-beta.2
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 +202 -0
- package/README.md +141 -0
- package/dist/bootstrap/bootstrap.d.ts +48 -0
- package/dist/bootstrap/bootstrap.js +255 -0
- package/dist/bootstrap/drain.d.ts +48 -0
- package/dist/bootstrap/drain.js +113 -0
- package/dist/bootstrap/lifecycle.d.ts +30 -0
- package/dist/bootstrap/lifecycle.js +64 -0
- package/dist/bootstrap/process-guards.d.ts +42 -0
- package/dist/bootstrap/process-guards.js +103 -0
- package/dist/bootstrap/query-parser.d.ts +34 -0
- package/dist/bootstrap/query-parser.js +37 -0
- package/dist/bootstrap/shutdown.d.ts +55 -0
- package/dist/bootstrap/shutdown.js +182 -0
- package/dist/constants.d.ts +32 -0
- package/dist/constants.js +48 -0
- package/dist/context/active-context.d.ts +23 -0
- package/dist/context/active-context.js +34 -0
- package/dist/context/request-context.middleware.d.ts +31 -0
- package/dist/context/request-context.middleware.js +95 -0
- package/dist/context/request-context.service.d.ts +29 -0
- package/dist/context/request-context.service.js +67 -0
- package/dist/decorators/owner.decorator.d.ts +18 -0
- package/dist/decorators/owner.decorator.js +31 -0
- package/dist/decorators/public.decorator.d.ts +13 -0
- package/dist/decorators/public.decorator.js +23 -0
- package/dist/decorators/version.decorators.d.ts +34 -0
- package/dist/decorators/version.decorators.js +40 -0
- package/dist/errors/catalog.d.ts +149 -0
- package/dist/errors/catalog.js +289 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.js +22 -0
- package/dist/errors/nage.error.d.ts +43 -0
- package/dist/errors/nage.error.js +45 -0
- package/dist/guards/api-version.guard.d.ts +20 -0
- package/dist/guards/api-version.guard.js +73 -0
- package/dist/http/all-exceptions.filter.d.ts +25 -0
- package/dist/http/all-exceptions.filter.js +256 -0
- package/dist/http/envelope.d.ts +25 -0
- package/dist/http/envelope.js +44 -0
- package/dist/http/no-envelope.decorator.d.ts +11 -0
- package/dist/http/no-envelope.decorator.js +16 -0
- package/dist/http/request-timeout.decorators.d.ts +23 -0
- package/dist/http/request-timeout.decorators.js +29 -0
- package/dist/http/request-timeout.interceptor.d.ts +28 -0
- package/dist/http/request-timeout.interceptor.js +75 -0
- package/dist/http/response.interceptor.d.ts +19 -0
- package/dist/http/response.interceptor.js +73 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +135 -0
- package/dist/job/job.factory.d.ts +29 -0
- package/dist/job/job.factory.js +50 -0
- package/dist/logging/json.logger.d.ts +23 -0
- package/dist/logging/json.logger.js +136 -0
- package/dist/logging/nest-logger.adapter.d.ts +20 -0
- package/dist/logging/nest-logger.adapter.js +46 -0
- package/dist/module/core.module.d.ts +40 -0
- package/dist/module/core.module.js +112 -0
- package/dist/security/audit.d.ts +42 -0
- package/dist/security/audit.js +399 -0
- package/dist/security/index.d.ts +15 -0
- package/dist/security/index.js +50 -0
- package/dist/security/legacy-scan.d.ts +24 -0
- package/dist/security/legacy-scan.js +98 -0
- package/dist/security/random.d.ts +40 -0
- package/dist/security/random.js +87 -0
- package/dist/security/rate-limit.decorators.d.ts +24 -0
- package/dist/security/rate-limit.decorators.js +25 -0
- package/dist/security/rate-limit.guard.d.ts +44 -0
- package/dist/security/rate-limit.guard.js +130 -0
- package/dist/security/rate-limit.store.d.ts +30 -0
- package/dist/security/rate-limit.store.js +63 -0
- package/dist/security/redaction.d.ts +54 -0
- package/dist/security/redaction.js +146 -0
- package/dist/security/tls.d.ts +29 -0
- package/dist/security/tls.js +48 -0
- package/dist/tokens.d.ts +60 -0
- package/dist/tokens.js +89 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +8 -0
- package/package.json +77 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-flight request accounting, so shutdown can wait for the requests it has
|
|
3
|
+
* (PLAN.md §21).
|
|
4
|
+
*
|
|
5
|
+
* `app.close()` alone cannot do this. Nest runs `onModuleDestroy` *before* it
|
|
6
|
+
* closes the HTTP server, so a handler still executing when the signal arrives
|
|
7
|
+
* finds its connection pool already shut — measured, not assumed: a 600ms
|
|
8
|
+
* handler signalled at 200ms returned `pool is closed` with HTTP 200. Knowing
|
|
9
|
+
* the number of live requests is what lets the sequence be put the right way
|
|
10
|
+
* round.
|
|
11
|
+
*
|
|
12
|
+
* Counted in middleware rather than in an interceptor because an interceptor
|
|
13
|
+
* only sees requests that reach a matched route: a request that 404s, or that
|
|
14
|
+
* dies in body parsing, holds a connection just the same.
|
|
15
|
+
*/
|
|
16
|
+
/** The subset of a Node/Express response the tracker touches. */
|
|
17
|
+
export interface DrainResponseLike {
|
|
18
|
+
once(event: 'close', listener: () => void): unknown;
|
|
19
|
+
setHeader?: (name: string, value: string) => void;
|
|
20
|
+
readonly headersSent?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export type DrainMiddleware = (request: unknown, response: DrainResponseLike, next: () => void) => void;
|
|
23
|
+
export interface RequestDrainOptions {
|
|
24
|
+
/** Consulted per request; `true` once a shutdown signal has been seen. */
|
|
25
|
+
readonly isDraining?: () => boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare class RequestDrain {
|
|
28
|
+
#private;
|
|
29
|
+
constructor(options?: RequestDrainOptions);
|
|
30
|
+
/** Requests that have been accepted and not yet had their response closed. */
|
|
31
|
+
get active(): number;
|
|
32
|
+
/**
|
|
33
|
+
* The middleware. Install it first, ahead of helmet and the body parser, so
|
|
34
|
+
* the count covers the whole time the connection is held.
|
|
35
|
+
*/
|
|
36
|
+
middleware(): DrainMiddleware;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve when no request is in flight, or on the deadline.
|
|
39
|
+
*
|
|
40
|
+
* Returns whether it drained; the caller logs the difference, because "we cut
|
|
41
|
+
* off 3 requests" and "we finished cleanly" must not look the same in a deploy
|
|
42
|
+
* log.
|
|
43
|
+
*/
|
|
44
|
+
whenIdle(timeoutMs: number): Promise<boolean>;
|
|
45
|
+
}
|
|
46
|
+
export declare function registerRequestDrain(app: object, drain: RequestDrain): void;
|
|
47
|
+
export declare function requestDrainFor(app: object): RequestDrain | undefined;
|
|
48
|
+
//# sourceMappingURL=drain.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* In-flight request accounting, so shutdown can wait for the requests it has
|
|
4
|
+
* (PLAN.md §21).
|
|
5
|
+
*
|
|
6
|
+
* `app.close()` alone cannot do this. Nest runs `onModuleDestroy` *before* it
|
|
7
|
+
* closes the HTTP server, so a handler still executing when the signal arrives
|
|
8
|
+
* finds its connection pool already shut — measured, not assumed: a 600ms
|
|
9
|
+
* handler signalled at 200ms returned `pool is closed` with HTTP 200. Knowing
|
|
10
|
+
* the number of live requests is what lets the sequence be put the right way
|
|
11
|
+
* round.
|
|
12
|
+
*
|
|
13
|
+
* Counted in middleware rather than in an interceptor because an interceptor
|
|
14
|
+
* only sees requests that reach a matched route: a request that 404s, or that
|
|
15
|
+
* dies in body parsing, holds a connection just the same.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.RequestDrain = void 0;
|
|
19
|
+
exports.registerRequestDrain = registerRequestDrain;
|
|
20
|
+
exports.requestDrainFor = requestDrainFor;
|
|
21
|
+
class RequestDrain {
|
|
22
|
+
#active = 0;
|
|
23
|
+
#idle = new Set();
|
|
24
|
+
#isDraining;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.#isDraining = options.isDraining ?? (() => false);
|
|
27
|
+
}
|
|
28
|
+
/** Requests that have been accepted and not yet had their response closed. */
|
|
29
|
+
get active() {
|
|
30
|
+
return this.#active;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The middleware. Install it first, ahead of helmet and the body parser, so
|
|
34
|
+
* the count covers the whole time the connection is held.
|
|
35
|
+
*/
|
|
36
|
+
middleware() {
|
|
37
|
+
return (_request, response, next) => {
|
|
38
|
+
this.#active += 1;
|
|
39
|
+
// A draining process keeps serving — the orchestrator may still be
|
|
40
|
+
// routing to it — but tells each client not to reuse the socket, so the
|
|
41
|
+
// connection is released at the end of the response instead of being cut
|
|
42
|
+
// when the listener closes. Requests are *not* refused here: a blanket 503
|
|
43
|
+
// would also refuse `/health/live`, and a liveness probe failing during a
|
|
44
|
+
// drain is what turns a graceful shutdown into a `SIGKILL`.
|
|
45
|
+
if (this.#isDraining() && response.headersSent !== true) {
|
|
46
|
+
response.setHeader?.('connection', 'close');
|
|
47
|
+
}
|
|
48
|
+
// `close` rather than `finish`: an aborted response never finishes, and a
|
|
49
|
+
// counter that only ever goes up makes the drain wait out its full
|
|
50
|
+
// deadline on every deploy.
|
|
51
|
+
let settled = false;
|
|
52
|
+
response.once('close', () => {
|
|
53
|
+
if (settled)
|
|
54
|
+
return;
|
|
55
|
+
settled = true;
|
|
56
|
+
this.#active -= 1;
|
|
57
|
+
if (this.#active <= 0)
|
|
58
|
+
this.#releaseIdleWaiters();
|
|
59
|
+
});
|
|
60
|
+
next();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve when no request is in flight, or on the deadline.
|
|
65
|
+
*
|
|
66
|
+
* Returns whether it drained; the caller logs the difference, because "we cut
|
|
67
|
+
* off 3 requests" and "we finished cleanly" must not look the same in a deploy
|
|
68
|
+
* log.
|
|
69
|
+
*/
|
|
70
|
+
async whenIdle(timeoutMs) {
|
|
71
|
+
if (this.#active <= 0)
|
|
72
|
+
return true;
|
|
73
|
+
if (timeoutMs <= 0)
|
|
74
|
+
return false;
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
this.#idle.delete(waiter);
|
|
78
|
+
resolve(false);
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
// Nothing about a shutdown deadline should itself hold the event loop
|
|
81
|
+
// open; the awaited promise is what keeps the sequence alive.
|
|
82
|
+
timer.unref();
|
|
83
|
+
const waiter = () => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
resolve(true);
|
|
86
|
+
};
|
|
87
|
+
this.#idle.add(waiter);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
#releaseIdleWaiters() {
|
|
91
|
+
const waiters = [...this.#idle];
|
|
92
|
+
this.#idle.clear();
|
|
93
|
+
for (const waiter of waiters)
|
|
94
|
+
waiter();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.RequestDrain = RequestDrain;
|
|
98
|
+
/**
|
|
99
|
+
* The tracker belongs to one application instance.
|
|
100
|
+
*
|
|
101
|
+
* A `WeakMap` rather than a provider: the middleware has to be installed by
|
|
102
|
+
* `createApplication` (before helmet, outside the DI-managed middleware chain),
|
|
103
|
+
* and a module-level singleton would let one test suite's counter leak into the
|
|
104
|
+
* next application built in the same process.
|
|
105
|
+
*/
|
|
106
|
+
const drains = new WeakMap();
|
|
107
|
+
function registerRequestDrain(app, drain) {
|
|
108
|
+
drains.set(app, drain);
|
|
109
|
+
}
|
|
110
|
+
function requestDrainFor(app) {
|
|
111
|
+
return drains.get(app);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=drain.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the process is in its own life (PLAN.md §21).
|
|
3
|
+
*
|
|
4
|
+
* Readiness and shutdown are two views of one fact, and before this they were
|
|
5
|
+
* two facts: `/health/ready` answered from the dependency probes alone, so a
|
|
6
|
+
* process that had received `SIGTERM` and was draining still told the load
|
|
7
|
+
* balancer to send it traffic. Every request routed in that window is a request
|
|
8
|
+
* whose connection is about to be closed.
|
|
9
|
+
*
|
|
10
|
+
* Liveness deliberately does **not** read this. A draining process is healthy;
|
|
11
|
+
* failing liveness while it drains asks the orchestrator to `SIGKILL` the very
|
|
12
|
+
* process that is trying to finish its work.
|
|
13
|
+
*/
|
|
14
|
+
export type LifecyclePhase = 'starting' | 'live' | 'draining';
|
|
15
|
+
export declare class LifecycleState {
|
|
16
|
+
#private;
|
|
17
|
+
get phase(): LifecyclePhase;
|
|
18
|
+
/** True from the moment a shutdown signal is observed. */
|
|
19
|
+
get draining(): boolean;
|
|
20
|
+
markLive(): void;
|
|
21
|
+
markDraining(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Back to `starting`. A deployed process never does this — a signal is not
|
|
24
|
+
* something to undo — but a test runner that builds several applications in one
|
|
25
|
+
* process needs the phase not to leak from one to the next.
|
|
26
|
+
*/
|
|
27
|
+
reset(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function processLifecycle(): LifecycleState;
|
|
30
|
+
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Where the process is in its own life (PLAN.md §21).
|
|
4
|
+
*
|
|
5
|
+
* Readiness and shutdown are two views of one fact, and before this they were
|
|
6
|
+
* two facts: `/health/ready` answered from the dependency probes alone, so a
|
|
7
|
+
* process that had received `SIGTERM` and was draining still told the load
|
|
8
|
+
* balancer to send it traffic. Every request routed in that window is a request
|
|
9
|
+
* whose connection is about to be closed.
|
|
10
|
+
*
|
|
11
|
+
* Liveness deliberately does **not** read this. A draining process is healthy;
|
|
12
|
+
* failing liveness while it drains asks the orchestrator to `SIGKILL` the very
|
|
13
|
+
* process that is trying to finish its work.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.LifecycleState = void 0;
|
|
17
|
+
exports.processLifecycle = processLifecycle;
|
|
18
|
+
class LifecycleState {
|
|
19
|
+
#phase = 'starting';
|
|
20
|
+
get phase() {
|
|
21
|
+
return this.#phase;
|
|
22
|
+
}
|
|
23
|
+
/** True from the moment a shutdown signal is observed. */
|
|
24
|
+
get draining() {
|
|
25
|
+
return this.#phase === 'draining';
|
|
26
|
+
}
|
|
27
|
+
markLive() {
|
|
28
|
+
// A late `markLive()` — a listener callback that fires after a signal
|
|
29
|
+
// already arrived — must not resurrect a draining process.
|
|
30
|
+
if (this.#phase === 'starting')
|
|
31
|
+
this.#phase = 'live';
|
|
32
|
+
}
|
|
33
|
+
markDraining() {
|
|
34
|
+
this.#phase = 'draining';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Back to `starting`. A deployed process never does this — a signal is not
|
|
38
|
+
* something to undo — but a test runner that builds several applications in one
|
|
39
|
+
* process needs the phase not to leak from one to the next.
|
|
40
|
+
*/
|
|
41
|
+
reset() {
|
|
42
|
+
this.#phase = 'starting';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.LifecycleState = LifecycleState;
|
|
46
|
+
/**
|
|
47
|
+
* The phase belongs to the **process**, not to an application object.
|
|
48
|
+
*
|
|
49
|
+
* A signal arrives at the process: if it hosted two applications, both are
|
|
50
|
+
* draining, and there is no meaningful state in which one of them is not. Holding
|
|
51
|
+
* it here rather than resolving it through DI also avoids a trap that cost a
|
|
52
|
+
* boot: `NestFactory.create` returns a Proxy that wraps every method in
|
|
53
|
+
* `ExceptionsZone`, whose default teardown is `process.exit(1)`. So
|
|
54
|
+
* `app.get(TOKEN)` for a token an application has not registered does not throw
|
|
55
|
+
* for a caller to catch — it kills the process. Measured: `createApplication` on a
|
|
56
|
+
* module that does not import `NageCoreModule` exited 1 before the listener was
|
|
57
|
+
* bound.
|
|
58
|
+
*/
|
|
59
|
+
let processState;
|
|
60
|
+
function processLifecycle() {
|
|
61
|
+
processState ??= new LifecycleState();
|
|
62
|
+
return processState;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What happens when an error escapes everything (PLAN.md §21, §18).
|
|
3
|
+
*
|
|
4
|
+
* Node's own policy is nearly right and reports nothing useful. An
|
|
5
|
+
* `uncaughtException` or an unhandled rejection prints a human-readable stack to
|
|
6
|
+
* stderr and exits 1 — so the one line describing why production died is the
|
|
7
|
+
* only line of the day that a JSON log shipper cannot parse, it carries no
|
|
8
|
+
* correlation id, and the pools are dropped rather than closed.
|
|
9
|
+
*
|
|
10
|
+
* The policy here keeps the crash and fixes the reporting:
|
|
11
|
+
*
|
|
12
|
+
* - **Both events are fatal.** Nothing is swallowed. A process that has thrown
|
|
13
|
+
* from outside a request has unknown state, and serving the next request from
|
|
14
|
+
* unknown state is how a bug becomes a data defect.
|
|
15
|
+
* - **One structured `fatal` line first**, through the framework logger, so the
|
|
16
|
+
* reason survives in the same stream as everything else.
|
|
17
|
+
* - **Then a bounded graceful shutdown** (`onFatal`), so in-flight responses get
|
|
18
|
+
* a chance to finish and pools get closed. Bounded because the handler cannot
|
|
19
|
+
* trust the very machinery it is shutting down.
|
|
20
|
+
*
|
|
21
|
+
* Installing a listener for these events also takes them off Node's default
|
|
22
|
+
* path, which is why the exit is explicit: a handler that logs and returns turns
|
|
23
|
+
* a crash into a zombie.
|
|
24
|
+
*/
|
|
25
|
+
import type { LoggerPort } from '@nage-api/contracts';
|
|
26
|
+
export type FatalKind = 'uncaughtException' | 'unhandledRejection';
|
|
27
|
+
export interface ProcessGuardOptions {
|
|
28
|
+
readonly logger: LoggerPort;
|
|
29
|
+
/**
|
|
30
|
+
* Graceful shutdown to attempt before exiting — normally the
|
|
31
|
+
* `ShutdownHandle.run` returned by `installShutdown`.
|
|
32
|
+
*/
|
|
33
|
+
readonly onFatal?: (kind: FatalKind, error: unknown) => unknown;
|
|
34
|
+
/** How long `onFatal` may take before the process exits anyway. */
|
|
35
|
+
readonly gracePeriodMs?: number;
|
|
36
|
+
/** Injected so a test can assert the exit code without exiting the runner. */
|
|
37
|
+
readonly exit?: (code: number) => void;
|
|
38
|
+
}
|
|
39
|
+
/** Removes the listeners this installed. */
|
|
40
|
+
export type DisposeProcessGuards = () => void;
|
|
41
|
+
export declare function installProcessGuards(options: ProcessGuardOptions): DisposeProcessGuards;
|
|
42
|
+
//# sourceMappingURL=process-guards.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What happens when an error escapes everything (PLAN.md §21, §18).
|
|
4
|
+
*
|
|
5
|
+
* Node's own policy is nearly right and reports nothing useful. An
|
|
6
|
+
* `uncaughtException` or an unhandled rejection prints a human-readable stack to
|
|
7
|
+
* stderr and exits 1 — so the one line describing why production died is the
|
|
8
|
+
* only line of the day that a JSON log shipper cannot parse, it carries no
|
|
9
|
+
* correlation id, and the pools are dropped rather than closed.
|
|
10
|
+
*
|
|
11
|
+
* The policy here keeps the crash and fixes the reporting:
|
|
12
|
+
*
|
|
13
|
+
* - **Both events are fatal.** Nothing is swallowed. A process that has thrown
|
|
14
|
+
* from outside a request has unknown state, and serving the next request from
|
|
15
|
+
* unknown state is how a bug becomes a data defect.
|
|
16
|
+
* - **One structured `fatal` line first**, through the framework logger, so the
|
|
17
|
+
* reason survives in the same stream as everything else.
|
|
18
|
+
* - **Then a bounded graceful shutdown** (`onFatal`), so in-flight responses get
|
|
19
|
+
* a chance to finish and pools get closed. Bounded because the handler cannot
|
|
20
|
+
* trust the very machinery it is shutting down.
|
|
21
|
+
*
|
|
22
|
+
* Installing a listener for these events also takes them off Node's default
|
|
23
|
+
* path, which is why the exit is explicit: a handler that logs and returns turns
|
|
24
|
+
* a crash into a zombie.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.installProcessGuards = installProcessGuards;
|
|
28
|
+
const DEFAULT_GRACE_PERIOD_MS = 5000;
|
|
29
|
+
function installProcessGuards(options) {
|
|
30
|
+
const { logger } = options;
|
|
31
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
32
|
+
const gracePeriodMs = options.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS;
|
|
33
|
+
let handling = false;
|
|
34
|
+
const handle = (kind, error) => {
|
|
35
|
+
// A rejection thrown from inside the fatal handler must not restart it.
|
|
36
|
+
if (handling)
|
|
37
|
+
return;
|
|
38
|
+
handling = true;
|
|
39
|
+
logger.fatal('Fatal error; the process will exit', {
|
|
40
|
+
kind,
|
|
41
|
+
error: error instanceof Error ? error.message : String(error),
|
|
42
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
43
|
+
// A rejection with a non-Error reason (`reject('nope')`, or a rejected
|
|
44
|
+
// fetch response) is the case where the stack is absent and the value is
|
|
45
|
+
// the only evidence there is.
|
|
46
|
+
...(error instanceof Error ? {} : { value: safeValue(error) }),
|
|
47
|
+
});
|
|
48
|
+
// Set now, so that even if the event loop empties before either the shutdown
|
|
49
|
+
// or the grace period finishes, the process still reports a failure.
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
let timer;
|
|
52
|
+
const grace = new Promise((resolve) => {
|
|
53
|
+
// Not `unref`ed: the grace period is what keeps the process alive long
|
|
54
|
+
// enough to close its pools, and an unreferenced timer would let it exit
|
|
55
|
+
// the moment the last request's socket closed.
|
|
56
|
+
timer = setTimeout(resolve, gracePeriodMs);
|
|
57
|
+
});
|
|
58
|
+
const attempt = (async () => {
|
|
59
|
+
await options.onFatal?.(kind, error);
|
|
60
|
+
})();
|
|
61
|
+
void Promise.race([attempt, grace]).then(() => {
|
|
62
|
+
if (timer !== undefined)
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
exit(1);
|
|
65
|
+
}, () => {
|
|
66
|
+
// The shutdown itself failed. Still exit: this path exists to guarantee
|
|
67
|
+
// the process does not keep serving.
|
|
68
|
+
if (timer !== undefined)
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
exit(1);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const onUncaught = (error) => {
|
|
74
|
+
handle('uncaughtException', error);
|
|
75
|
+
};
|
|
76
|
+
const onRejection = (reason) => {
|
|
77
|
+
handle('unhandledRejection', reason);
|
|
78
|
+
};
|
|
79
|
+
process.on('uncaughtException', onUncaught);
|
|
80
|
+
process.on('unhandledRejection', onRejection);
|
|
81
|
+
return () => {
|
|
82
|
+
process.removeListener('uncaughtException', onUncaught);
|
|
83
|
+
process.removeListener('unhandledRejection', onRejection);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** A rejection reason can be anything, including something that will not stringify. */
|
|
87
|
+
function safeValue(value) {
|
|
88
|
+
// `Promise.reject()` with no reason is a real case, and `JSON.stringify` returns
|
|
89
|
+
// `undefined` for it however the types describe that.
|
|
90
|
+
if (value === undefined)
|
|
91
|
+
return 'undefined';
|
|
92
|
+
if (typeof value === 'string')
|
|
93
|
+
return value;
|
|
94
|
+
try {
|
|
95
|
+
return JSON.stringify(value);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// A cyclic or getter-throwing reason must not turn the fatal handler into a
|
|
99
|
+
// second fatal error.
|
|
100
|
+
return '[unserializable]';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=process-guards.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The query parser the URL query DSL needs (PLAN.md §14.2).
|
|
3
|
+
*
|
|
4
|
+
* Express 5 changed its default `query parser` from `extended` to `simple`.
|
|
5
|
+
* Under `simple`, `?where[status]=active` does not become
|
|
6
|
+
* `{ where: { status: 'active' } }` — it becomes the single flat key
|
|
7
|
+
* `'where[status]'`. Nothing errors: `parseQuery` sees a top-level key it does
|
|
8
|
+
* not recognise, ignores it, and the request returns **the whole collection with
|
|
9
|
+
* a 200**. A client that asked to filter silently gets everything, which is the
|
|
10
|
+
* same class of failure as a missing allow-list.
|
|
11
|
+
*
|
|
12
|
+
* So the setting is applied explicitly rather than inherited. It is a named
|
|
13
|
+
* export as well as part of `bootstrap`, because an application built with
|
|
14
|
+
* `Test.createTestingModule(...).createNestApplication()` never goes through
|
|
15
|
+
* `bootstrap` and would otherwise test a differently-configured app than it ships.
|
|
16
|
+
*/
|
|
17
|
+
/** The Nest-shaped slice needed to reach that instance. */
|
|
18
|
+
interface AppWithHttpAdapter {
|
|
19
|
+
getHttpAdapter: () => {
|
|
20
|
+
getInstance: () => unknown;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Enable bracketed query parsing on an application.
|
|
25
|
+
*
|
|
26
|
+
* A no-op on a platform whose instance has no `set` — Fastify parses brackets
|
|
27
|
+
* through its own querystring parser, so there is nothing to switch on and
|
|
28
|
+
* nothing to warn about.
|
|
29
|
+
*
|
|
30
|
+
* @returns whether the setting was applied
|
|
31
|
+
*/
|
|
32
|
+
export declare function enableQueryDsl(app: AppWithHttpAdapter): boolean;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=query-parser.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The query parser the URL query DSL needs (PLAN.md §14.2).
|
|
4
|
+
*
|
|
5
|
+
* Express 5 changed its default `query parser` from `extended` to `simple`.
|
|
6
|
+
* Under `simple`, `?where[status]=active` does not become
|
|
7
|
+
* `{ where: { status: 'active' } }` — it becomes the single flat key
|
|
8
|
+
* `'where[status]'`. Nothing errors: `parseQuery` sees a top-level key it does
|
|
9
|
+
* not recognise, ignores it, and the request returns **the whole collection with
|
|
10
|
+
* a 200**. A client that asked to filter silently gets everything, which is the
|
|
11
|
+
* same class of failure as a missing allow-list.
|
|
12
|
+
*
|
|
13
|
+
* So the setting is applied explicitly rather than inherited. It is a named
|
|
14
|
+
* export as well as part of `bootstrap`, because an application built with
|
|
15
|
+
* `Test.createTestingModule(...).createNestApplication()` never goes through
|
|
16
|
+
* `bootstrap` and would otherwise test a differently-configured app than it ships.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.enableQueryDsl = enableQueryDsl;
|
|
20
|
+
/**
|
|
21
|
+
* Enable bracketed query parsing on an application.
|
|
22
|
+
*
|
|
23
|
+
* A no-op on a platform whose instance has no `set` — Fastify parses brackets
|
|
24
|
+
* through its own querystring parser, so there is nothing to switch on and
|
|
25
|
+
* nothing to warn about.
|
|
26
|
+
*
|
|
27
|
+
* @returns whether the setting was applied
|
|
28
|
+
*/
|
|
29
|
+
function enableQueryDsl(app) {
|
|
30
|
+
const instance = app.getHttpAdapter().getInstance();
|
|
31
|
+
const settable = instance;
|
|
32
|
+
if (typeof settable.set !== 'function')
|
|
33
|
+
return false;
|
|
34
|
+
settable.set('query parser', 'extended');
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=query-parser.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graceful shutdown, in the order the stages actually have to happen
|
|
3
|
+
* (PLAN.md §21).
|
|
4
|
+
*
|
|
5
|
+
* `app.enableShutdownHooks()` is not this. Nest 11's signal handler runs
|
|
6
|
+
* `onModuleDestroy` → `beforeApplicationShutdown` → close the HTTP server →
|
|
7
|
+
* `onApplicationShutdown`, so **modules destroy their pools while requests are
|
|
8
|
+
* still running**. Measured against a 600ms handler signalled at 200ms: the
|
|
9
|
+
* handler's query came back `pool is closed` and the client got that as a 200.
|
|
10
|
+
* A second measurement: one handler that never returns kept the process alive
|
|
11
|
+
* past 8 seconds, because nothing in that sequence has a deadline — the
|
|
12
|
+
* container survives until the orchestrator's `SIGKILL`.
|
|
13
|
+
*
|
|
14
|
+
* This runs the stages in the order that makes each one meaningful:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Fail readiness.** The load balancer needs to stop choosing this
|
|
17
|
+
* instance, and endpoint propagation is not instant (`readinessDelayMs`).
|
|
18
|
+
* 2. **Stop accepting connections**, and release keep-alive sockets that have
|
|
19
|
+
* no request on them. Requests already in flight keep their sockets.
|
|
20
|
+
* 3. **Wait for those requests**, bounded by `drainTimeoutMs`. Past the
|
|
21
|
+
* deadline the remaining sockets are cut, because a client that has hung is
|
|
22
|
+
* not a reason to abandon the pools.
|
|
23
|
+
* 4. **`app.close()`** — only now do modules close pools, queues and Redis.
|
|
24
|
+
* 5. **Exit.** Zero if the sequence finished, non-zero if `forceExitAfterMs`
|
|
25
|
+
* expired, so a deploy can tell a clean drain from a broken one.
|
|
26
|
+
*/
|
|
27
|
+
import type { LoggerPort, NageCoreConfig } from '@nage-api/contracts';
|
|
28
|
+
import { type LifecycleState } from './lifecycle.js';
|
|
29
|
+
import { RequestDrain } from './drain.js';
|
|
30
|
+
/** The subset of `INestApplication` this needs; keeps tests free of a real app. */
|
|
31
|
+
export interface ClosableApplication {
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
getHttpServer?: () => unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface ShutdownOptions {
|
|
36
|
+
readonly config?: NageCoreConfig;
|
|
37
|
+
readonly logger?: LoggerPort;
|
|
38
|
+
/** Defaults to `SIGTERM` and `SIGINT`. */
|
|
39
|
+
readonly signals?: readonly NodeJS.Signals[];
|
|
40
|
+
/** Injected so a test can assert the exit code without exiting the runner. */
|
|
41
|
+
readonly exit?: (code: number) => void;
|
|
42
|
+
readonly lifecycle?: LifecycleState;
|
|
43
|
+
readonly drain?: RequestDrain;
|
|
44
|
+
}
|
|
45
|
+
export interface ShutdownHandle {
|
|
46
|
+
readonly draining: boolean;
|
|
47
|
+
/** Run the sequence. Idempotent: a second call awaits the first. */
|
|
48
|
+
run(reason: string): Promise<number>;
|
|
49
|
+
/** Remove the signal listeners. Tests and embedded apps need this. */
|
|
50
|
+
dispose(): void;
|
|
51
|
+
}
|
|
52
|
+
export declare function installShutdown(app: ClosableApplication, options?: ShutdownOptions): ShutdownHandle;
|
|
53
|
+
export declare function registerShutdownHandle(app: object, handle: ShutdownHandle): void;
|
|
54
|
+
export declare function shutdownHandleFor(app: object): ShutdownHandle | undefined;
|
|
55
|
+
//# sourceMappingURL=shutdown.d.ts.map
|