@nextrush/core 3.0.7 → 4.0.0-beta.1
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/README.md +322 -493
- package/dist/index.d.ts +226 -182
- package/dist/index.js +501 -311
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import { SECURITY_AUDIT } from '@nextrush/types';
|
|
2
2
|
export { ContentType, HttpStatus } from '@nextrush/types';
|
|
3
|
+
import { getErrorStatus, NextRushError, getHttpStatusMessage } from '@nextrush/errors';
|
|
4
|
+
export { BadRequestError, ForbiddenError, HttpError, InternalServerError, NextRushError, NotFoundError, UnauthorizedError } from '@nextrush/errors';
|
|
5
|
+
|
|
6
|
+
// src/application.ts
|
|
3
7
|
|
|
4
8
|
// src/middleware.ts
|
|
9
|
+
var MULTIPLE_NEXT_MESSAGE = "next() called multiple times";
|
|
10
|
+
var RESOLVED = Promise.resolve();
|
|
11
|
+
function emitDoubleResponseWarning(index) {
|
|
12
|
+
console.warn(
|
|
13
|
+
`[nextrush] Middleware at index ${String(index)} called next() after the response was already committed. Downstream middleware may attempt to write to an already-finished response. Either await next() to delegate downstream, or send a response without calling next().`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
5
16
|
function compose(middleware, options) {
|
|
6
17
|
if (!Array.isArray(middleware)) {
|
|
7
18
|
throw new TypeError("Middleware stack must be an array");
|
|
@@ -11,19 +22,46 @@ function compose(middleware, options) {
|
|
|
11
22
|
throw new TypeError("Middleware must be a function");
|
|
12
23
|
}
|
|
13
24
|
}
|
|
14
|
-
const warnDoubleResponse = options?.warnDoubleResponse ??
|
|
25
|
+
const warnDoubleResponse = options?.warnDoubleResponse ?? false;
|
|
15
26
|
const stack = [...middleware];
|
|
16
27
|
const len = stack.length;
|
|
17
28
|
if (len === 0) {
|
|
18
29
|
return function composedMiddleware(_ctx, next) {
|
|
19
|
-
return next ? next() :
|
|
30
|
+
return next ? next() : RESOLVED;
|
|
20
31
|
};
|
|
21
32
|
}
|
|
33
|
+
if (len === 1) {
|
|
34
|
+
const only = stack[0];
|
|
35
|
+
if (only) {
|
|
36
|
+
return function composedSingle(ctx, next) {
|
|
37
|
+
let called = false;
|
|
38
|
+
const nextFn = () => {
|
|
39
|
+
if (called) {
|
|
40
|
+
return Promise.reject(new Error(MULTIPLE_NEXT_MESSAGE));
|
|
41
|
+
}
|
|
42
|
+
called = true;
|
|
43
|
+
if (warnDoubleResponse && ctx.responded) {
|
|
44
|
+
emitDoubleResponseWarning(0);
|
|
45
|
+
}
|
|
46
|
+
return next ? next() : RESOLVED;
|
|
47
|
+
};
|
|
48
|
+
if (ctx.setNext) {
|
|
49
|
+
ctx.setNext(nextFn);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const result = only(ctx, nextFn);
|
|
53
|
+
return result === void 0 ? RESOLVED : Promise.resolve(result);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
22
60
|
return function composedMiddleware(ctx, next) {
|
|
23
61
|
let index = -1;
|
|
24
62
|
function dispatch(i) {
|
|
25
63
|
if (i <= index) {
|
|
26
|
-
return Promise.reject(new Error(
|
|
64
|
+
return Promise.reject(new Error(MULTIPLE_NEXT_MESSAGE));
|
|
27
65
|
}
|
|
28
66
|
index = i;
|
|
29
67
|
let fn;
|
|
@@ -33,13 +71,11 @@ function compose(middleware, options) {
|
|
|
33
71
|
fn = next;
|
|
34
72
|
}
|
|
35
73
|
if (!fn) {
|
|
36
|
-
return
|
|
74
|
+
return RESOLVED;
|
|
37
75
|
}
|
|
38
76
|
const nextFn = () => {
|
|
39
77
|
if (warnDoubleResponse && ctx.responded) {
|
|
40
|
-
|
|
41
|
-
`[nextrush] Middleware at index ${String(i)} called next() after the response was already committed. Downstream middleware may attempt to write to an already-finished response. Either await next() to delegate downstream, or send a response without calling next().`
|
|
42
|
-
);
|
|
78
|
+
emitDoubleResponseWarning(i);
|
|
43
79
|
}
|
|
44
80
|
return dispatch(i + 1);
|
|
45
81
|
};
|
|
@@ -47,7 +83,8 @@ function compose(middleware, options) {
|
|
|
47
83
|
ctx.setNext(nextFn);
|
|
48
84
|
}
|
|
49
85
|
try {
|
|
50
|
-
|
|
86
|
+
const result = fn(ctx, nextFn);
|
|
87
|
+
return result === void 0 ? RESOLVED : Promise.resolve(result);
|
|
51
88
|
} catch (err) {
|
|
52
89
|
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
53
90
|
}
|
|
@@ -67,10 +104,100 @@ function flattenMiddleware(arr) {
|
|
|
67
104
|
}
|
|
68
105
|
return flattened;
|
|
69
106
|
}
|
|
107
|
+
function writeDefaultErrorResponse(error, ctx, opts) {
|
|
108
|
+
const status = getErrorStatus(error);
|
|
109
|
+
if (status >= 500 || !opts.isProduction) {
|
|
110
|
+
opts.logger.error("Request error:", error);
|
|
111
|
+
}
|
|
112
|
+
ctx.status = status;
|
|
113
|
+
if (error instanceof NextRushError) {
|
|
114
|
+
ctx.json(error.toJSON());
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const expose = status < 500;
|
|
118
|
+
ctx.json({
|
|
119
|
+
error: expose ? error.name : getHttpStatusMessage(status),
|
|
120
|
+
message: expose ? error.message : "Internal Server Error",
|
|
121
|
+
code: "INTERNAL_ERROR",
|
|
122
|
+
status
|
|
123
|
+
});
|
|
124
|
+
}
|
|
70
125
|
|
|
71
|
-
// src/
|
|
126
|
+
// src/route-mount.ts
|
|
127
|
+
var SLASH_CHAR_CODE = 47;
|
|
72
128
|
var ORIGINAL_PATH = /* @__PURE__ */ Symbol.for("nextrush.originalPath");
|
|
73
129
|
var ROUTE_PREFIX = /* @__PURE__ */ Symbol.for("nextrush.routePrefix");
|
|
130
|
+
function resolveMountedPath(currentPath, normalizedPrefix, matchesMountPrefix) {
|
|
131
|
+
if (matchesMountPrefix) {
|
|
132
|
+
return matchesMountPrefix(currentPath, normalizedPrefix);
|
|
133
|
+
}
|
|
134
|
+
const prefixLen = normalizedPrefix.length;
|
|
135
|
+
if (!currentPath.startsWith(normalizedPrefix)) return void 0;
|
|
136
|
+
const hasCharAfterPrefix = prefixLen < currentPath.length;
|
|
137
|
+
if (hasCharAfterPrefix && currentPath.charCodeAt(prefixLen) !== SLASH_CHAR_CODE) return void 0;
|
|
138
|
+
return currentPath.slice(prefixLen) || "/";
|
|
139
|
+
}
|
|
140
|
+
function createPrefixMount(normalizedPrefix, routerMiddleware, matchesMountPrefix) {
|
|
141
|
+
return async (ctx, next) => {
|
|
142
|
+
const currentPath = ctx.path;
|
|
143
|
+
const adjustedPath = resolveMountedPath(currentPath, normalizedPrefix, matchesMountPrefix);
|
|
144
|
+
if (adjustedPath === void 0) {
|
|
145
|
+
return next();
|
|
146
|
+
}
|
|
147
|
+
ctx.path = adjustedPath;
|
|
148
|
+
ctx.state[ORIGINAL_PATH] = currentPath;
|
|
149
|
+
ctx.state[ROUTE_PREFIX] = normalizedPrefix;
|
|
150
|
+
try {
|
|
151
|
+
await routerMiddleware(ctx, async () => {
|
|
152
|
+
ctx.path = currentPath;
|
|
153
|
+
await next();
|
|
154
|
+
ctx.path = adjustedPath;
|
|
155
|
+
});
|
|
156
|
+
} finally {
|
|
157
|
+
ctx.path = currentPath;
|
|
158
|
+
ctx.state[ORIGINAL_PATH] = void 0;
|
|
159
|
+
ctx.state[ROUTE_PREFIX] = void 0;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/application.ts
|
|
165
|
+
function validateProxyTrust(proxy) {
|
|
166
|
+
if (proxy === true) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"createApp({ proxy: true }) is no longer valid \u2014 trusting every proxy header lets a remote client forge its own IP. Use `proxy: <hopCount>` (e.g. `proxy: 1` for one reverse proxy) or `proxy: ['<cidr>', ...]` (a list of trusted proxy IPs/ranges) instead."
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (proxy === 0) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"createApp({ proxy: 0 }) trusts zero hops, which is the same as `proxy: false` but never falls back cleanly \u2014 use `proxy: false` to disable proxy trust, or a positive hop count to trust that many proxies."
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return proxy;
|
|
177
|
+
}
|
|
178
|
+
var TeardownTimeoutError = class extends Error {
|
|
179
|
+
constructor(unitName, timeoutMs) {
|
|
180
|
+
super(`Teardown unit "${unitName}" did not complete within ${String(timeoutMs)}ms`);
|
|
181
|
+
this.name = "TeardownTimeoutError";
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
async function runTeardownUnit(unit, timeoutMs) {
|
|
185
|
+
const settle = Promise.resolve().then(() => unit.run()).then(() => null).catch((err) => err instanceof Error ? err : new Error(String(err)));
|
|
186
|
+
if (timeoutMs === void 0) {
|
|
187
|
+
return settle;
|
|
188
|
+
}
|
|
189
|
+
let timer;
|
|
190
|
+
const timedOut = new Promise((resolve) => {
|
|
191
|
+
timer = setTimeout(() => {
|
|
192
|
+
resolve(new TeardownTimeoutError(unit.name, timeoutMs));
|
|
193
|
+
}, timeoutMs);
|
|
194
|
+
});
|
|
195
|
+
try {
|
|
196
|
+
return await Promise.race([settle, timedOut]);
|
|
197
|
+
} finally {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
74
201
|
var NOOP_LOGGER = {
|
|
75
202
|
error(..._args) {
|
|
76
203
|
},
|
|
@@ -82,130 +209,126 @@ var NOOP_LOGGER = {
|
|
|
82
209
|
}
|
|
83
210
|
};
|
|
84
211
|
var Application = class {
|
|
85
|
-
/**
|
|
86
|
-
* Middleware stack
|
|
87
|
-
*/
|
|
88
212
|
middlewareStack = [];
|
|
89
213
|
/**
|
|
90
|
-
*
|
|
214
|
+
* Boot-time security audit checks contributed by tagged middleware
|
|
215
|
+
* (`security-boundaries` capability, task 8.1). Collected as `use()`
|
|
216
|
+
* registers each middleware; run once by `_boot()`, production-only.
|
|
91
217
|
*/
|
|
92
|
-
|
|
218
|
+
securityAudits = [];
|
|
219
|
+
/** Registered extensions, in registration order (setup runs at ready()) */
|
|
220
|
+
extensions = [];
|
|
221
|
+
/** Registered extension names — enforces uniqueness */
|
|
222
|
+
extensionNames = /* @__PURE__ */ new Set();
|
|
93
223
|
/**
|
|
94
|
-
*
|
|
224
|
+
* Combined teardown-unit registration order: extension `destroy()`s and
|
|
225
|
+
* {@link onClose} hooks interleaved in the exact order they were registered,
|
|
226
|
+
* so `_shutdown()` can run the whole set in one reverse-of-registration pass
|
|
227
|
+
* (RFC-022 §7.3 — "teardown becomes a flat list of independently-isolated,
|
|
228
|
+
* budget-raced units"). Populated lazily by {@link extend} (for extensions
|
|
229
|
+
* declaring `destroy()`) and {@link onClose}.
|
|
95
230
|
*/
|
|
231
|
+
teardownUnits = [];
|
|
232
|
+
/** Whether {@link close} has started — {@link onClose} is a pre-shutdown-only registration point (RFC-022 §8.6). */
|
|
233
|
+
_closeStarted = false;
|
|
234
|
+
/** Names decorated onto the app — enforces collision detection */
|
|
235
|
+
decorations = /* @__PURE__ */ new Set();
|
|
96
236
|
_errorHandler = null;
|
|
97
|
-
/**
|
|
98
|
-
* Pluggable logger
|
|
99
|
-
*/
|
|
100
237
|
logger;
|
|
101
|
-
/**
|
|
102
|
-
* Application options
|
|
103
|
-
*/
|
|
104
238
|
options;
|
|
105
|
-
/**
|
|
106
|
-
* Whether the app is running
|
|
107
|
-
*/
|
|
239
|
+
/** Whether the app is running (server listening) */
|
|
108
240
|
_isRunning = false;
|
|
241
|
+
/** Whether the app has been booted (extensions set up, config frozen) */
|
|
242
|
+
_isReady = false;
|
|
243
|
+
/** Whether ready() mounted the app-owned router (so close() can undo it) */
|
|
244
|
+
_routerMounted = false;
|
|
245
|
+
/** In-flight boot promise — memoized so concurrent ready() calls share one boot (H-1) */
|
|
246
|
+
_readyPromise = null;
|
|
247
|
+
/** In-flight shutdown promise — memoized so concurrent close() calls share one (H-3) */
|
|
248
|
+
_closePromise = null;
|
|
249
|
+
/** The app-owned router (optional). Route methods delegate to it. */
|
|
250
|
+
router;
|
|
251
|
+
/** The app-owned DI container (optional). Exposed to extensions and registrars. */
|
|
252
|
+
container;
|
|
109
253
|
constructor(options = {}) {
|
|
254
|
+
const proxy = validateProxyTrust(options.proxy ?? false);
|
|
110
255
|
this.options = {
|
|
111
256
|
env: options.env ?? "development",
|
|
112
|
-
proxy
|
|
257
|
+
proxy
|
|
113
258
|
};
|
|
114
259
|
this.logger = options.logger ?? NOOP_LOGGER;
|
|
260
|
+
this.router = options.router;
|
|
261
|
+
this.container = options.container;
|
|
115
262
|
}
|
|
116
|
-
/**
|
|
117
|
-
* Check if running in production
|
|
118
|
-
*/
|
|
263
|
+
/** Check if running in production */
|
|
119
264
|
get isProduction() {
|
|
120
265
|
return this.options.env === "production";
|
|
121
266
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Check if app is running
|
|
124
|
-
*/
|
|
267
|
+
/** Check if app is running */
|
|
125
268
|
get isRunning() {
|
|
126
269
|
return this._isRunning;
|
|
127
270
|
}
|
|
128
271
|
/**
|
|
129
|
-
*
|
|
272
|
+
* Whether a graceful shutdown is currently in progress — `true` from the
|
|
273
|
+
* moment {@link close} begins tearing down until it completes (F-12:
|
|
274
|
+
* shutdown observability, RFC-022). A readiness check or operator tooling
|
|
275
|
+
* can read this to reflect an in-progress drain.
|
|
130
276
|
*/
|
|
277
|
+
get isDraining() {
|
|
278
|
+
return this._closeStarted;
|
|
279
|
+
}
|
|
280
|
+
/** Check if app has been booted via ready() */
|
|
281
|
+
get isReady() {
|
|
282
|
+
return this._isReady;
|
|
283
|
+
}
|
|
284
|
+
/** Get middleware count */
|
|
131
285
|
get middlewareCount() {
|
|
132
286
|
return this.middlewareStack.length;
|
|
133
287
|
}
|
|
288
|
+
/** Get registered extension count */
|
|
289
|
+
get extensionCount() {
|
|
290
|
+
return this.extensions.length;
|
|
291
|
+
}
|
|
134
292
|
/**
|
|
135
|
-
* Throws if the app is
|
|
136
|
-
* Prevents
|
|
293
|
+
* Throws if the app configuration is frozen (after ready() or start()).
|
|
294
|
+
* Prevents unsafe mutations once the app has booted or is serving traffic.
|
|
137
295
|
*/
|
|
138
|
-
|
|
139
|
-
if (this._isRunning) {
|
|
140
|
-
throw new Error(
|
|
296
|
+
assertConfigurable(method) {
|
|
297
|
+
if (this._isReady || this._isRunning) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Cannot call ${method}() after the app has booted (ready()) or started \u2014 configuration is frozen`
|
|
300
|
+
);
|
|
141
301
|
}
|
|
142
302
|
}
|
|
143
|
-
// ===========================================================================
|
|
144
|
-
// Middleware Registration
|
|
145
|
-
// ===========================================================================
|
|
146
303
|
/**
|
|
147
304
|
* Register middleware function(s)
|
|
148
305
|
*
|
|
149
|
-
* @param middleware - Middleware function
|
|
306
|
+
* @param middleware - Middleware function(s)
|
|
150
307
|
* @returns this for chaining
|
|
151
|
-
*
|
|
152
|
-
* @example
|
|
153
|
-
* ```typescript
|
|
154
|
-
* // Single middleware
|
|
155
|
-
* app.use(async (ctx) => {
|
|
156
|
-
* console.log(ctx.method, ctx.path);
|
|
157
|
-
* await ctx.next();
|
|
158
|
-
* });
|
|
159
|
-
*
|
|
160
|
-
* // Multiple middleware
|
|
161
|
-
* app.use(cors(), helmet(), json());
|
|
162
|
-
* ```
|
|
163
308
|
*/
|
|
164
309
|
use(...middleware) {
|
|
165
|
-
this.
|
|
310
|
+
this.assertConfigurable("use");
|
|
166
311
|
for (const mw of middleware) {
|
|
167
312
|
if (typeof mw !== "function") {
|
|
168
313
|
throw new TypeError("Middleware must be a function");
|
|
169
314
|
}
|
|
170
315
|
this.middlewareStack.push(mw);
|
|
316
|
+
const audit = mw[SECURITY_AUDIT];
|
|
317
|
+
if (typeof audit === "function") {
|
|
318
|
+
this.securityAudits.push(audit);
|
|
319
|
+
}
|
|
171
320
|
}
|
|
172
321
|
return this;
|
|
173
322
|
}
|
|
174
|
-
// ===========================================================================
|
|
175
|
-
// Router Mounting (Hono-style)
|
|
176
|
-
// ===========================================================================
|
|
177
323
|
/**
|
|
178
|
-
* Mount a router at a path prefix
|
|
179
|
-
*
|
|
180
|
-
* This is the Hono-style API for mounting routers directly on the app.
|
|
181
|
-
* The router's routes will only match requests that start with the given prefix.
|
|
324
|
+
* Mount a router at a path prefix.
|
|
182
325
|
*
|
|
183
|
-
* @param path - Path prefix
|
|
326
|
+
* @param path - Path prefix (e.g. '/api/users')
|
|
184
327
|
* @param router - Router instance to mount
|
|
185
328
|
* @returns this for chaining
|
|
186
|
-
*
|
|
187
|
-
* @example
|
|
188
|
-
* ```typescript
|
|
189
|
-
* // Create modular routers
|
|
190
|
-
* const users = createRouter();
|
|
191
|
-
* users.get('/', listUsers);
|
|
192
|
-
* users.get('/:id', getUser);
|
|
193
|
-
* users.post('/', createUser);
|
|
194
|
-
*
|
|
195
|
-
* const posts = createRouter();
|
|
196
|
-
* posts.get('/', listPosts);
|
|
197
|
-
* posts.get('/:id', getPost);
|
|
198
|
-
*
|
|
199
|
-
* // Mount directly on app - clean like Hono!
|
|
200
|
-
* const app = createApp();
|
|
201
|
-
* app.route('/api/users', users);
|
|
202
|
-
* app.route('/api/posts', posts);
|
|
203
|
-
*
|
|
204
|
-
* listen(app, 3000);
|
|
205
|
-
* ```
|
|
206
329
|
*/
|
|
207
330
|
route(path, router) {
|
|
208
|
-
this.
|
|
331
|
+
this.assertConfigurable("route");
|
|
209
332
|
if (path === "/" || path === "") {
|
|
210
333
|
this.middlewareStack.push(router.routes());
|
|
211
334
|
return this;
|
|
@@ -215,229 +338,261 @@ var Application = class {
|
|
|
215
338
|
if (!normalizedPrefix.startsWith("/")) {
|
|
216
339
|
normalizedPrefix = "/" + normalizedPrefix;
|
|
217
340
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
341
|
+
this.middlewareStack.push(
|
|
342
|
+
createPrefixMount(normalizedPrefix, routerMiddleware, router.matchesMountPrefix?.bind(router))
|
|
343
|
+
);
|
|
344
|
+
return this;
|
|
345
|
+
}
|
|
346
|
+
requireRouter() {
|
|
347
|
+
if (!this.router) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
"No router configured. Create your app with `createApp()` from `nextrush` (batteries-included), or pass one: `createApp({ router: createRouter() })`."
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return this.router;
|
|
353
|
+
}
|
|
354
|
+
/** Register a GET route on the app-owned router. */
|
|
355
|
+
get(path, ...entries) {
|
|
356
|
+
this.assertConfigurable("get");
|
|
357
|
+
this.requireRouter().get(path, ...entries);
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
/** Register a POST route on the app-owned router. */
|
|
361
|
+
post(path, ...entries) {
|
|
362
|
+
this.assertConfigurable("post");
|
|
363
|
+
this.requireRouter().post(path, ...entries);
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
/** Register a PUT route on the app-owned router. */
|
|
367
|
+
put(path, ...entries) {
|
|
368
|
+
this.assertConfigurable("put");
|
|
369
|
+
this.requireRouter().put(path, ...entries);
|
|
370
|
+
return this;
|
|
371
|
+
}
|
|
372
|
+
/** Register a PATCH route on the app-owned router. */
|
|
373
|
+
patch(path, ...entries) {
|
|
374
|
+
this.assertConfigurable("patch");
|
|
375
|
+
this.requireRouter().patch(path, ...entries);
|
|
376
|
+
return this;
|
|
377
|
+
}
|
|
378
|
+
/** Register a DELETE route on the app-owned router. */
|
|
379
|
+
delete(path, ...entries) {
|
|
380
|
+
this.assertConfigurable("delete");
|
|
381
|
+
this.requireRouter().delete(path, ...entries);
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
/** Register a HEAD route on the app-owned router. */
|
|
385
|
+
head(path, ...entries) {
|
|
386
|
+
this.assertConfigurable("head");
|
|
387
|
+
this.requireRouter().head(path, ...entries);
|
|
245
388
|
return this;
|
|
246
389
|
}
|
|
247
|
-
// ===========================================================================
|
|
248
|
-
// Error Handling
|
|
249
|
-
// ===========================================================================
|
|
250
390
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* Replaces any previously set handler. Only one error handler is active
|
|
254
|
-
* at a time. For additive error handling, compose logic within a single
|
|
255
|
-
* handler or use the `errorHandler()` middleware from `@nextrush/errors`.
|
|
256
|
-
*
|
|
257
|
-
* @param handler - Error handler function
|
|
258
|
-
* @returns this for chaining
|
|
259
|
-
*
|
|
260
|
-
* @example
|
|
261
|
-
* ```typescript
|
|
262
|
-
* app.setErrorHandler((error, ctx) => {
|
|
263
|
-
* console.error('Request failed:', error);
|
|
264
|
-
*
|
|
265
|
-
* if (error instanceof ValidationError) {
|
|
266
|
-
* ctx.status = 400;
|
|
267
|
-
* ctx.json({ error: error.message, details: error.details });
|
|
268
|
-
* return;
|
|
269
|
-
* }
|
|
391
|
+
* Register a route for all HTTP methods on the app-owned router.
|
|
270
392
|
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
393
|
+
* @remarks
|
|
394
|
+
* There is intentionally no `app.options()` verb method — it would collide
|
|
395
|
+
* with the `app.options` configuration property. Register OPTIONS routes via
|
|
396
|
+
* `app.all()`, the router directly, or let CORS middleware handle preflight.
|
|
275
397
|
*/
|
|
276
|
-
|
|
277
|
-
this.
|
|
398
|
+
all(path, ...entries) {
|
|
399
|
+
this.assertConfigurable("all");
|
|
400
|
+
this.requireRouter().all(path, ...entries);
|
|
278
401
|
return this;
|
|
279
402
|
}
|
|
280
403
|
/**
|
|
281
|
-
* Set
|
|
404
|
+
* Set the application error handler. Replaces any previously set handler.
|
|
282
405
|
*
|
|
283
406
|
* @param handler - Error handler function
|
|
284
407
|
* @returns this for chaining
|
|
285
|
-
*
|
|
286
|
-
* @deprecated Use `setErrorHandler()` instead — `onError()` implies
|
|
287
|
-
* event subscription (additive), but this is a setter (replaces).
|
|
288
408
|
*/
|
|
289
|
-
|
|
290
|
-
|
|
409
|
+
setErrorHandler(handler) {
|
|
410
|
+
this.assertConfigurable("setErrorHandler");
|
|
411
|
+
this._errorHandler = handler;
|
|
412
|
+
return this;
|
|
291
413
|
}
|
|
292
|
-
//
|
|
293
|
-
// Plugin System
|
|
294
|
-
// ===========================================================================
|
|
414
|
+
// Extension System. See docs/RFC/class-runtime/005-plugin-system.md
|
|
295
415
|
/**
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* Handles both sync and async `install()` methods automatically.
|
|
299
|
-
* Returns `this` for sync plugins and `Promise<this>` for async ones.
|
|
416
|
+
* Register an extension. Queues it — `setup()` runs later, at `ready()`,
|
|
417
|
+
* in registration order. Synchronous and chainable.
|
|
300
418
|
*
|
|
301
|
-
* @param
|
|
302
|
-
*
|
|
419
|
+
* @param extension - Extension to register. If it declares a decorated
|
|
420
|
+
* shape via `Extension<TDecorated>`, the return type carries that shape —
|
|
421
|
+
* `app.extend(events<MyEvents>()).events` is statically known, no
|
|
422
|
+
* `declare module` augmentation required.
|
|
423
|
+
* @returns this, intersected with the extension's declared decorated shape
|
|
303
424
|
*
|
|
304
425
|
* @example
|
|
305
426
|
* ```typescript
|
|
306
|
-
*
|
|
307
|
-
* app.
|
|
308
|
-
*
|
|
309
|
-
* // Async plugin — just await it
|
|
310
|
-
* await app.plugin(databasePlugin({ uri: '...' }));
|
|
427
|
+
* const extended = app.extend(events<MyEvents>());
|
|
428
|
+
* await app.ready(); // adapters call this automatically — runs setup()/decorate()
|
|
429
|
+
* extended.events.emit('user:created', { id: '1' }); // available only AFTER ready()
|
|
311
430
|
* ```
|
|
312
431
|
*/
|
|
313
|
-
|
|
314
|
-
this.
|
|
315
|
-
if (
|
|
316
|
-
throw new
|
|
432
|
+
extend(extension) {
|
|
433
|
+
this.assertConfigurable("extend");
|
|
434
|
+
if (!extension || typeof extension.setup !== "function") {
|
|
435
|
+
throw new TypeError("Extension must have a setup() method");
|
|
436
|
+
}
|
|
437
|
+
if (this.extensionNames.has(extension.name)) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`Extension "${extension.name}" is already registered. Use a different extension name, or check for a duplicate app.extend() call.`
|
|
440
|
+
);
|
|
317
441
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
442
|
+
this.extensionNames.add(extension.name);
|
|
443
|
+
this.extensions.push(extension);
|
|
444
|
+
if (typeof extension.destroy === "function") {
|
|
445
|
+
const destroyableExtension = extension;
|
|
446
|
+
this.teardownUnits.push({
|
|
447
|
+
name: extension.name,
|
|
448
|
+
run: () => destroyableExtension.destroy()
|
|
323
449
|
});
|
|
324
450
|
}
|
|
325
|
-
this.plugins.set(plugin.name, plugin);
|
|
326
451
|
return this;
|
|
327
452
|
}
|
|
328
453
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* @returns Promise that resolves when plugin is installed
|
|
454
|
+
* Boot the application: run every registered extension's `setup()` once, in
|
|
455
|
+
* registration order, awaiting async setups. Idempotent — safe to call twice.
|
|
456
|
+
* Adapters call this automatically before `start()`.
|
|
333
457
|
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
458
|
+
* After `ready()` resolves, the configuration is frozen (`use`/`route`/`extend`
|
|
459
|
+
* throw).
|
|
336
460
|
*
|
|
337
|
-
* @
|
|
338
|
-
*
|
|
339
|
-
* await app.plugin(new DatabasePlugin({ connectionString: '...' }));
|
|
340
|
-
* ```
|
|
461
|
+
* @returns this
|
|
462
|
+
* @throws if an extension declares a `needs` dependency not yet registered
|
|
341
463
|
*/
|
|
342
|
-
async
|
|
343
|
-
|
|
344
|
-
|
|
464
|
+
async ready() {
|
|
465
|
+
if (this._isReady) {
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
this._readyPromise ??= this._boot().catch((err) => {
|
|
469
|
+
this._readyPromise = null;
|
|
470
|
+
throw err;
|
|
471
|
+
});
|
|
472
|
+
return this._readyPromise;
|
|
345
473
|
}
|
|
346
474
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* @param name - Plugin name
|
|
350
|
-
* @returns Plugin instance or undefined
|
|
475
|
+
* Run the boot sequence once (extension setup + router mount). Guarded and
|
|
476
|
+
* memoized by {@link ready}.
|
|
351
477
|
*/
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
478
|
+
async _boot() {
|
|
479
|
+
const allNames = new Set(this.extensions.map((e) => e.name));
|
|
480
|
+
for (const extension of this.extensions) {
|
|
481
|
+
if (extension.needs) {
|
|
482
|
+
for (const dep of extension.needs) {
|
|
483
|
+
if (!allNames.has(dep)) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`Extension "${extension.name}" needs "${dep}", but no extension named "${dep}" was ever registered.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const setupDone = /* @__PURE__ */ new Set();
|
|
492
|
+
for (const extension of this.extensions) {
|
|
493
|
+
if (extension.needs) {
|
|
494
|
+
for (const dep of extension.needs) {
|
|
495
|
+
if (!setupDone.has(dep)) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`Extension "${extension.name}" needs "${dep}", but "${dep}" was not registered before it. Register the "${dep}" extension before "${extension.name}".`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const ctx = {
|
|
503
|
+
app: this,
|
|
504
|
+
logger: this.logger,
|
|
505
|
+
container: this.container,
|
|
506
|
+
env: this.options.env ?? "development",
|
|
507
|
+
name: extension.name,
|
|
508
|
+
decorate: (name, value) => {
|
|
509
|
+
this.decorate(name, value);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
await extension.setup(ctx);
|
|
513
|
+
setupDone.add(extension.name);
|
|
514
|
+
}
|
|
515
|
+
if (this.router) {
|
|
516
|
+
this.middlewareStack.push(this.router.routes());
|
|
517
|
+
this._routerMounted = true;
|
|
518
|
+
}
|
|
519
|
+
if (this.isProduction) {
|
|
520
|
+
this._runSecurityAudits();
|
|
521
|
+
}
|
|
522
|
+
this._isReady = true;
|
|
523
|
+
return this;
|
|
355
524
|
}
|
|
356
525
|
/**
|
|
357
|
-
*
|
|
526
|
+
* Run every registered middleware's boot-time security audit check
|
|
527
|
+
* (`security-boundaries` capability, task 8.1/8.2). `throw`-level verdicts
|
|
528
|
+
* fail boot immediately; `warn`-level verdicts log once each via the app
|
|
529
|
+
* logger and never block boot. Production-only — `_boot()` calls this
|
|
530
|
+
* conditionally so a dev/test app never pays the cost or sees the noise.
|
|
358
531
|
*/
|
|
359
|
-
|
|
360
|
-
|
|
532
|
+
_runSecurityAudits() {
|
|
533
|
+
for (const audit of this.securityAudits) {
|
|
534
|
+
const verdict = audit();
|
|
535
|
+
if (verdict.level === "ok") {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (verdict.level === "throw") {
|
|
539
|
+
throw new Error(`[security-boundaries] ${verdict.message}`);
|
|
540
|
+
}
|
|
541
|
+
this.logger.warn(`[security-boundaries] ${verdict.message}`);
|
|
542
|
+
}
|
|
361
543
|
}
|
|
362
|
-
// ===========================================================================
|
|
363
|
-
// Request Handler
|
|
364
|
-
// ===========================================================================
|
|
365
544
|
/**
|
|
366
|
-
*
|
|
545
|
+
* Attach a value to the app under `name`. Extension-author primitive, invoked
|
|
546
|
+
* through {@link ExtensionContext.decorate} — there is intentionally no public
|
|
547
|
+
* `app.decorate()` (RFC §6.1). Throws on collision.
|
|
367
548
|
*
|
|
368
|
-
*
|
|
369
|
-
|
|
549
|
+
* @internal
|
|
550
|
+
*/
|
|
551
|
+
decorate(name, value) {
|
|
552
|
+
if (this.decorations.has(name)) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Decoration "${name}" already exists on the application. Choose a different name, or check for a duplicate ctx.decorate() call across your extensions.`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
if (name in this) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
`Decoration "${name}" collides with an existing Application member \u2014 choose another name`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
Object.defineProperty(this, name, {
|
|
563
|
+
value,
|
|
564
|
+
enumerable: true,
|
|
565
|
+
writable: false,
|
|
566
|
+
configurable: true
|
|
567
|
+
// allow close() to clean up
|
|
568
|
+
});
|
|
569
|
+
this.decorations.add(name);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Whether a decoration already occupies `name`.
|
|
573
|
+
*/
|
|
574
|
+
hasDecorator(name) {
|
|
575
|
+
return this.decorations.has(name);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Create the request handler callback.
|
|
370
579
|
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* handler that includes subsequent registrations.
|
|
580
|
+
* The middleware stack is snapshot at call time — call `ready()` before
|
|
581
|
+
* `callback()` so extension-registered middleware is included. Adapters do
|
|
582
|
+
* this automatically.
|
|
375
583
|
*
|
|
376
584
|
* @returns Request handler function
|
|
377
585
|
*/
|
|
378
586
|
callback() {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
if ("onResponse" in p && typeof p.onResponse !== "function") {
|
|
389
|
-
throw new TypeError(
|
|
390
|
-
`Plugin "${p.name}": onResponse must be a function, got ${typeof p.onResponse}`
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
if ("onError" in p && typeof p.onError !== "function") {
|
|
394
|
-
throw new TypeError(
|
|
395
|
-
`Plugin "${p.name}": onError must be a function, got ${typeof p.onError}`
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
if ("extendContext" in p && typeof p.extendContext !== "function") {
|
|
399
|
-
throw new TypeError(
|
|
400
|
-
`Plugin "${p.name}": extendContext must be a function, got ${typeof p.extendContext}`
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
return true;
|
|
587
|
+
if (!this._isReady && this.extensions.length > 0) {
|
|
588
|
+
this.logger.warn(
|
|
589
|
+
`callback() was called before ready(), but ${String(this.extensions.length)} extension(s) were registered via extend() \u2014 their setup() will NOT run, and anything they would decorate (e.g. app.events) will be missing. Call \`await app.ready()\` before \`callback()\`. Adapters (listen/serve) do this automatically.`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
const fn = compose(this.middlewareStack, {
|
|
593
|
+
warnDoubleResponse: !this.isProduction
|
|
404
594
|
});
|
|
405
|
-
return
|
|
406
|
-
try {
|
|
407
|
-
for (const p of hookPlugins) {
|
|
408
|
-
if (p.extendContext) p.extendContext(ctx);
|
|
409
|
-
if (p.onRequest) await p.onRequest(ctx);
|
|
410
|
-
}
|
|
411
|
-
await fn(ctx);
|
|
412
|
-
for (const p of hookPlugins) {
|
|
413
|
-
if (p.onResponse) {
|
|
414
|
-
try {
|
|
415
|
-
await p.onResponse(ctx);
|
|
416
|
-
} catch (hookError) {
|
|
417
|
-
this.logger.warn("Plugin onResponse hook threw:", {
|
|
418
|
-
plugin: p.name,
|
|
419
|
-
error: hookError
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
} catch (error) {
|
|
425
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
426
|
-
for (const p of hookPlugins) {
|
|
427
|
-
if (p.onError) {
|
|
428
|
-
try {
|
|
429
|
-
await p.onError(err, ctx);
|
|
430
|
-
} catch (hookError) {
|
|
431
|
-
this.logger.warn("Plugin onError hook threw:", {
|
|
432
|
-
plugin: p.name,
|
|
433
|
-
error: hookError
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
await this.handleError(error, ctx);
|
|
439
|
-
}
|
|
440
|
-
};
|
|
595
|
+
return (ctx) => fn(ctx).then(void 0, (error) => this.handleError(error, ctx));
|
|
441
596
|
}
|
|
442
597
|
/**
|
|
443
598
|
* Handle errors - uses custom handler if set, otherwise default
|
|
@@ -452,69 +607,104 @@ var Application = class {
|
|
|
452
607
|
this.logger.error("Error handler threw:", handlerError);
|
|
453
608
|
}
|
|
454
609
|
}
|
|
455
|
-
|
|
610
|
+
try {
|
|
611
|
+
this.defaultErrorHandler(err, ctx);
|
|
612
|
+
} catch (fatal) {
|
|
613
|
+
this.logger.error("Default error handler threw:", fatal);
|
|
614
|
+
}
|
|
456
615
|
}
|
|
457
616
|
/**
|
|
458
|
-
* Default error handler
|
|
459
|
-
*
|
|
460
|
-
* Uses the `expose` flag on errors (set by HttpError/NextRushError)
|
|
461
|
-
* to decide whether to include the error message in the response.
|
|
462
|
-
* 4xx errors expose messages by default; 5xx errors never do.
|
|
463
|
-
* This prevents leaking internal details (paths, SQL, stack info)
|
|
464
|
-
* regardless of environment.
|
|
617
|
+
* Default error handler — delegates to the shared error serializer so the
|
|
618
|
+
* default response matches `@nextrush/errors`' `errorHandler()` (audit C-1).
|
|
465
619
|
*/
|
|
466
620
|
defaultErrorHandler(error, ctx) {
|
|
467
|
-
|
|
468
|
-
this.logger
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (typeof errorMeta.status === "number") {
|
|
473
|
-
const raw = errorMeta.status;
|
|
474
|
-
status = raw >= 400 && raw < 600 ? raw : 500;
|
|
475
|
-
}
|
|
476
|
-
ctx.status = status;
|
|
477
|
-
const expose = errorMeta.expose === true;
|
|
478
|
-
const message = expose ? error.message : "Internal Server Error";
|
|
479
|
-
ctx.json({ error: message });
|
|
480
|
-
}
|
|
481
|
-
// ===========================================================================
|
|
482
|
-
// Lifecycle
|
|
483
|
-
// ===========================================================================
|
|
621
|
+
writeDefaultErrorResponse(error, ctx, {
|
|
622
|
+
logger: this.logger,
|
|
623
|
+
isProduction: this.isProduction
|
|
624
|
+
});
|
|
625
|
+
}
|
|
484
626
|
/**
|
|
485
627
|
* Mark app as running and freeze configuration.
|
|
486
628
|
*
|
|
487
|
-
* Called by adapters when the server starts listening
|
|
488
|
-
* `use()`, `route()`, `plugin()`, and `pluginAsync()` will throw.
|
|
489
|
-
* This prevents unsafe middleware mutations while requests are in flight.
|
|
490
|
-
*
|
|
491
|
-
* To re-enable registration (e.g. for testing), call `close()` first.
|
|
629
|
+
* Called by adapters when the server starts listening (after `ready()`).
|
|
492
630
|
*/
|
|
493
631
|
start() {
|
|
494
632
|
this._isRunning = true;
|
|
495
633
|
}
|
|
496
634
|
/**
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
635
|
+
* Register a teardown hook for a resource outside the extension system
|
|
636
|
+
* (stateful middleware, a manually-attached service, …). Run during
|
|
637
|
+
* {@link close} under the SAME bounded/isolated guarantee as extension
|
|
638
|
+
* `destroy()` — one throwing/hanging hook never strands the others — in one
|
|
639
|
+
* combined reverse-of-registration order together with every `extend()`ed
|
|
640
|
+
* extension's `destroy()` (RFC-022 §8.1/§8.2).
|
|
641
|
+
*
|
|
642
|
+
* @param hook - Teardown callback. May be sync or async; a thrown/rejected
|
|
643
|
+
* hook is isolated and its error collected in {@link close}'s returned array.
|
|
644
|
+
*
|
|
645
|
+
* @remarks Registration is pre-shutdown only: a hook registered after
|
|
646
|
+
* `close()` has already started is not run in that shutdown (RFC-022 §8.6).
|
|
501
647
|
*
|
|
502
|
-
* @
|
|
648
|
+
* @example
|
|
649
|
+
* ```typescript
|
|
650
|
+
* const store = new MemoryStore(options);
|
|
651
|
+
* app.onClose(() => store.shutdown());
|
|
652
|
+
* ```
|
|
503
653
|
*/
|
|
504
|
-
|
|
654
|
+
onClose(hook) {
|
|
655
|
+
if (this._closeStarted) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
this.teardownUnits.push({ name: "onClose hook", run: hook });
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Graceful shutdown. Destroys extensions in reverse registration order,
|
|
662
|
+
* each isolated from the others' failures — one failing/hanging `destroy()`
|
|
663
|
+
* never strands the rest (RFC-022 / ADR-0012).
|
|
664
|
+
*
|
|
665
|
+
* @param options - Optional teardown budget. Omitted = unbounded, byte-identical
|
|
666
|
+
* to today's behavior (`Promise.allSettled`, no timeout). When `timeout` is
|
|
667
|
+
* given, every teardown unit races against it independently; a unit that
|
|
668
|
+
* does not finish in time is reported as a {@link TeardownTimeoutError} in
|
|
669
|
+
* the returned array instead of blocking `close()` past the budget.
|
|
670
|
+
* @returns Array of errors from units that failed or timed out (empty on success)
|
|
671
|
+
*/
|
|
672
|
+
async close(options) {
|
|
673
|
+
this._closePromise ??= this._shutdown(options);
|
|
674
|
+
return this._closePromise;
|
|
675
|
+
}
|
|
676
|
+
/** Run the shutdown sequence once. Guarded and memoized by {@link close}. */
|
|
677
|
+
async _shutdown(options) {
|
|
505
678
|
this._isRunning = false;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
);
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
679
|
+
this._closeStarted = true;
|
|
680
|
+
this.logger.info("Application is draining: graceful shutdown starting");
|
|
681
|
+
if (!this._isReady && this.extensions.length > 0) {
|
|
682
|
+
this.logger.warn(
|
|
683
|
+
`close() was called before ready(), but ${String(this.extensions.length)} extension(s) were registered via extend() \u2014 their setup() never ran, yet destroy() will still be called on them now. If destroy() assumes setup()-established state, this may throw or behave incorrectly. Call \`await app.ready()\` before \`close()\`.`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
const timeoutMs = options?.timeout;
|
|
687
|
+
const units = [...this.teardownUnits].reverse();
|
|
688
|
+
const errors = (await Promise.all(units.map((unit) => runTeardownUnit(unit, timeoutMs)))).filter((e) => e !== null);
|
|
689
|
+
for (const error of errors) {
|
|
690
|
+
this.logger.error("Teardown unit failed during shutdown:", error);
|
|
691
|
+
}
|
|
692
|
+
for (const name of this.decorations) {
|
|
693
|
+
Reflect.deleteProperty(this, name);
|
|
694
|
+
}
|
|
695
|
+
this.decorations.clear();
|
|
696
|
+
this.extensions.length = 0;
|
|
697
|
+
this.extensionNames.clear();
|
|
698
|
+
this.teardownUnits.length = 0;
|
|
699
|
+
this._isReady = false;
|
|
700
|
+
this._readyPromise = null;
|
|
701
|
+
this._closePromise = null;
|
|
702
|
+
this._closeStarted = false;
|
|
703
|
+
if (this._routerMounted) {
|
|
704
|
+
this.middlewareStack.pop();
|
|
705
|
+
this._routerMounted = false;
|
|
706
|
+
}
|
|
707
|
+
return errors;
|
|
518
708
|
}
|
|
519
709
|
};
|
|
520
710
|
function createApp(options) {
|