@nifrajs/core 5.8.0 → 5.9.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/CHANGELOG.md +99 -0
- package/README.md +9 -4
- package/dist/assurance.d.ts +26 -1
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +43 -11
- package/dist/assurance.js.map +1 -1
- package/dist/canonical-json.d.ts +11 -0
- package/dist/canonical-json.d.ts.map +1 -0
- package/dist/canonical-json.js +46 -0
- package/dist/canonical-json.js.map +1 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +1 -8
- package/dist/capabilities.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +1 -8
- package/dist/data.js.map +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +2 -12
- package/dist/diff.js.map +1 -1
- package/dist/durable-adapters.d.ts +2 -0
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +41 -49
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-saga.d.ts.map +1 -1
- package/dist/durable-saga.js +14 -20
- package/dist/durable-saga.js.map +1 -1
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +14 -68
- package/dist/evidence.js.map +1 -1
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/idempotency.js +60 -23
- package/dist/idempotency.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/control-character.d.ts +3 -0
- package/dist/internal/control-character.d.ts.map +1 -0
- package/dist/internal/control-character.js +10 -0
- package/dist/internal/control-character.js.map +1 -0
- package/dist/internal/durable-shared.d.ts.map +1 -1
- package/dist/internal/durable-shared.js +4 -1
- package/dist/internal/durable-shared.js.map +1 -1
- package/dist/internal/hex.d.ts +5 -0
- package/dist/internal/hex.d.ts.map +1 -0
- package/dist/internal/hex.js +14 -0
- package/dist/internal/hex.js.map +1 -0
- package/dist/internal/record.d.ts +3 -0
- package/dist/internal/record.d.ts.map +1 -0
- package/dist/internal/record.js +5 -0
- package/dist/internal/record.js.map +1 -0
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +2 -2
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/route-execution.d.ts +4 -4
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +4 -0
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +4 -3
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +16 -33
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/internal/schema-parts.d.ts +13 -0
- package/dist/internal/schema-parts.d.ts.map +1 -0
- package/dist/internal/schema-parts.js +15 -0
- package/dist/internal/schema-parts.js.map +1 -0
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +2 -14
- package/dist/ledger.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +10 -37
- package/dist/manifest.js.map +1 -1
- package/dist/open-resolved.d.ts +18 -0
- package/dist/open-resolved.d.ts.map +1 -0
- package/dist/open-resolved.js +62 -0
- package/dist/open-resolved.js.map +1 -0
- package/dist/range.js +1 -1
- package/dist/range.js.map +1 -1
- package/dist/serve-static.d.ts +22 -0
- package/dist/serve-static.d.ts.map +1 -0
- package/dist/serve-static.js +187 -0
- package/dist/serve-static.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +2 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +12 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/base64-answers.d.ts.map +1 -1
- package/dist/server/base64-answers.js +1 -4
- package/dist/server/base64-answers.js.map +1 -1
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +43 -38
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +4 -1
- package/dist/server/body.js.map +1 -1
- package/dist/server/bridge-markers.d.ts +1 -1
- package/dist/server/bridge-markers.js +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +4 -1
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/http.d.ts +5 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +13 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +2 -0
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/idle-timeout.d.ts +39 -0
- package/dist/server/idle-timeout.d.ts.map +1 -0
- package/dist/server/idle-timeout.js +95 -0
- package/dist/server/idle-timeout.js.map +1 -0
- package/dist/server/json-shared.d.ts +23 -0
- package/dist/server/json-shared.d.ts.map +1 -0
- package/dist/server/json-shared.js +33 -0
- package/dist/server/json-shared.js.map +1 -0
- package/dist/server/json-text.d.ts +1 -1
- package/dist/server/json-text.js +1 -1
- package/dist/server/node-outcome.js +1 -1
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/request-context.d.ts +22 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +44 -4
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/request-local.d.ts +44 -0
- package/dist/server/request-local.d.ts.map +1 -0
- package/dist/server/request-local.js +91 -0
- package/dist/server/request-local.js.map +1 -0
- package/dist/server/respond.js +1 -1
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +10 -2
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +93 -64
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/runtime-core.d.ts +7 -4
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +9 -7
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +23 -8
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +58 -59
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +860 -510
- package/dist/server/server.js.map +1 -1
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js +2 -0
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/webhook.js +1 -1
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +3 -1
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +14 -1
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-adapter-view.d.ts +54 -0
- package/dist/server/ws-adapter-view.d.ts.map +1 -0
- package/dist/server/ws-adapter-view.js +7 -0
- package/dist/server/ws-adapter-view.js.map +1 -0
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +1 -1
- package/dist/single-copy.js.map +1 -1
- package/dist/static-file.d.ts +156 -4
- package/dist/static-file.d.ts.map +1 -1
- package/dist/static-file.js +198 -4
- package/dist/static-file.js.map +1 -1
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +1 -3
- package/dist/wire.js.map +1 -1
- package/package.json +16 -1
- package/src/assurance.ts +66 -16
- package/src/canonical-json.ts +43 -0
- package/src/capabilities.ts +1 -8
- package/src/data.ts +2 -8
- package/src/diff.ts +2 -12
- package/src/durable-adapters.ts +53 -50
- package/src/durable-saga.ts +30 -23
- package/src/evidence.ts +25 -90
- package/src/idempotency.ts +62 -20
- package/src/index.ts +13 -2
- package/src/internal/control-character.ts +8 -0
- package/src/internal/durable-shared.ts +4 -1
- package/src/internal/hex.ts +14 -0
- package/src/internal/record.ts +4 -0
- package/src/internal/route-compiler.ts +2 -2
- package/src/internal/route-execution.ts +7 -20
- package/src/internal/route-lanes.ts +20 -44
- package/src/internal/schema-parts.ts +19 -0
- package/src/ledger.ts +2 -16
- package/src/manifest.ts +10 -38
- package/src/open-resolved.ts +64 -0
- package/src/range.ts +1 -1
- package/src/serve-static.ts +227 -0
- package/src/server/bare-error-lane.ts +11 -2
- package/src/server/base64-answers.ts +2 -6
- package/src/server/body-lane.ts +58 -51
- package/src/server/body.ts +4 -1
- package/src/server/bridge-markers.ts +1 -1
- package/src/server/cookies.ts +4 -1
- package/src/server/core-copies.ts +1 -1
- package/src/server/http.ts +12 -0
- package/src/server/idempotency-lane.ts +2 -0
- package/src/server/idle-timeout.ts +107 -0
- package/src/server/json-shared.ts +38 -0
- package/src/server/json-text.ts +1 -1
- package/src/server/node-outcome.ts +1 -1
- package/src/server/request-context.ts +62 -3
- package/src/server/request-local.ts +109 -0
- package/src/server/respond.ts +1 -1
- package/src/server/response-contract-lane.ts +112 -65
- package/src/server/runtime-core.ts +10 -8
- package/src/server/server-types.ts +23 -8
- package/src/server/server.ts +1220 -798
- package/src/server/transport-lane.ts +2 -0
- package/src/server/webhook.ts +1 -1
- package/src/server/websocket.ts +18 -1
- package/src/server/ws-adapter-view.ts +63 -0
- package/src/server.ts +12 -1
- package/src/single-copy.ts +1 -3
- package/src/static-file.ts +350 -4
- package/src/wire.ts +2 -4
package/dist/server/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import { adoptRouteRuntimes, compileRouteOptions } from "../internal/route-compi
|
|
|
8
8
|
import { compileRouteExecutionPlan, } from "../internal/route-execution.js";
|
|
9
9
|
import { authenticateMount, compileRouteProgram, executeRouteProgram, } from "../internal/route-program.js";
|
|
10
10
|
import { isSameOriginRequest } from "../internal/same-origin.js";
|
|
11
|
-
import { decodeRouteParams, expandOptionalParams, } from "../router/pattern.js";
|
|
11
|
+
import { compileRoutePattern, decodeRouteParams, expandOptionalParams, } from "../router/pattern.js";
|
|
12
12
|
import { EMPTY_PARAMS, METHODS, Router } from "../router/router.js";
|
|
13
13
|
import { withAnswerSteps } from "./answer-step.js";
|
|
14
14
|
import { isAuthenticatedRequest, markAuthenticatedRequest, } from "./auth.js";
|
|
@@ -22,8 +22,9 @@ import { declaredHeaders, headerObjectOf } from "./headers.js";
|
|
|
22
22
|
import { isRoutableMethod, jsonError, pathnameOf, plainError, urlPartsOf, } from "./http.js";
|
|
23
23
|
import { withStaticNodeHeaders } from "./node-outcome.js";
|
|
24
24
|
import { queryObjectOf, searchOf } from "./query.js";
|
|
25
|
-
import { PLATFORM_PEER, RequestContext, readBodyFramed, } from "./request-context.js";
|
|
25
|
+
import { PLATFORM_IDLE_TIMEOUT, PLATFORM_PEER, PLATFORM_REQUEST, RequestContext, readBodyFramed, } from "./request-context.js";
|
|
26
26
|
import { recordRequestReplacement } from "./request-lineage.js";
|
|
27
|
+
import { carryToCopy } from "./request-local.js";
|
|
27
28
|
import { needsResolving, resolveDotSegments } from "./request-target.js";
|
|
28
29
|
import { applyStaticResponseHeaders, buildStaticResponseHeaders, claimHandedOver, fusedRespond, fusedRespondNoSet, handedBackResponse, recordResidentTexts, toResponse, writableResponse, } from "./respond.js";
|
|
29
30
|
import { CONTEXT_REQUEST, CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
|
|
@@ -39,24 +40,32 @@ import { jsonLogger } from "./logger.js";
|
|
|
39
40
|
const BUN_NATIVE_REQUEST_SAFE = Symbol.for("@nifrajs/core/bun-native-request-safe");
|
|
40
41
|
const functionToString = Function.prototype.toString;
|
|
41
42
|
const CONTEXTLESS_ARROW = /^(?:async\s*)?\(\s*\)\s*(?::[\s\S]*?)?=>/;
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The socket peer Bun observed (`undefined` if unknown) and its connection's idle timeout, as a
|
|
45
|
+
* `Platform` for one request. A class, not an object literal: JSC builds a literal holding a getter
|
|
46
|
+
* or a symbol key on a slow path, measured at about 5x the instructions of this instance.
|
|
47
|
+
*/
|
|
48
|
+
class BunPeerPlatform {
|
|
49
|
+
#server;
|
|
50
|
+
#resolved = false;
|
|
51
|
+
#address;
|
|
52
|
+
constructor(server, req, setIdleTimeout) {
|
|
53
|
+
this.#server = server;
|
|
54
|
+
this[PLATFORM_IDLE_TIMEOUT] = setIdleTimeout;
|
|
55
|
+
this[PLATFORM_REQUEST] = req;
|
|
56
|
+
}
|
|
44
57
|
// Bun's requestIP() costs ~0.5 us per call, about 7% of a bare GET's server time. Keep the
|
|
45
58
|
// documented raw-peer c.clientIp behavior, but resolve it lazily: most routes never read c.clientIp,
|
|
46
59
|
// so they should not pay for the socket lookup on every request. A getter also
|
|
47
60
|
// preserves middleware that inspects the platform argument directly and trust-mode routes, which
|
|
48
61
|
// resolve the value in deriveClientIp before the handler runs.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
return address;
|
|
58
|
-
},
|
|
59
|
-
};
|
|
62
|
+
get clientIp() {
|
|
63
|
+
if (!this.#resolved) {
|
|
64
|
+
this.#resolved = true;
|
|
65
|
+
this.#address = this.#server.requestIP(this[PLATFORM_REQUEST])?.address;
|
|
66
|
+
}
|
|
67
|
+
return this.#address;
|
|
68
|
+
}
|
|
60
69
|
}
|
|
61
70
|
// Bun's route table has a slot for each of these; a QUERY entry is ignored and the request falls
|
|
62
71
|
// through to the table's fetch, so QUERY routes stay on the portable dispatcher.
|
|
@@ -87,6 +96,20 @@ function outranks(other, route) {
|
|
|
87
96
|
}
|
|
88
97
|
return ahead && other.length === route.length;
|
|
89
98
|
}
|
|
99
|
+
/** Whether one request can match both patterns: no two static segments differ before either ends
|
|
100
|
+
* in a wildcard, and the lengths agree otherwise. Errs toward `true`. */
|
|
101
|
+
function mayShareRequest(a, b) {
|
|
102
|
+
const shared = Math.min(a.length, b.length);
|
|
103
|
+
for (let i = 0; i < shared; i++) {
|
|
104
|
+
const left = a[i];
|
|
105
|
+
const right = b[i];
|
|
106
|
+
if (left.kind === "wildcard" || right.kind === "wildcard")
|
|
107
|
+
return true;
|
|
108
|
+
if (left.kind === "static" && right.kind === "static" && left.value !== right.value)
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return a.length === b.length;
|
|
112
|
+
}
|
|
90
113
|
/**
|
|
91
114
|
* The key Bun's route table holds `path` under. Bun refuses one with a character outside printable
|
|
92
115
|
* ASCII, so such a path is spelled as the portable router matches it: percent-encoded static
|
|
@@ -138,6 +161,8 @@ const BODY_ERROR = (error) => Promise.reject(error);
|
|
|
138
161
|
const DEFAULT_DRAIN_MS = 10_000;
|
|
139
162
|
const DRAIN_POLL_MS = 10;
|
|
140
163
|
const STOP_HOOK_TIMEOUT_MS = 5_000;
|
|
164
|
+
/** Bun's own `idleTimeout` when `listen()` is given none. */
|
|
165
|
+
const BUN_IDLE_TIMEOUT_SEC = 10;
|
|
141
166
|
/** Same-origin check for a WebSocket handshake (CSWSH default). {@link isSameOriginRequest} is the one
|
|
142
167
|
* owner, shared with the server-function mount in `@nifrajs/web` - the two used to answer differently
|
|
143
168
|
* for the same request, so a browser that could open a socket was told its POST was cross-origin. */
|
|
@@ -173,8 +198,8 @@ function hasUnroutedParam(params) {
|
|
|
173
198
|
return false;
|
|
174
199
|
}
|
|
175
200
|
/**
|
|
176
|
-
* Deno
|
|
177
|
-
* Bun, workerd and
|
|
201
|
+
* Deno's server builds `Request.url` from the Host and target as received, without normalizing the
|
|
202
|
+
* target first. Bun, workerd, and `new Request()` parse it before core sees it.
|
|
178
203
|
*/
|
|
179
204
|
const rawRequestTargets = typeof globalThis.Deno !== "undefined";
|
|
180
205
|
/**
|
|
@@ -200,6 +225,7 @@ function withResolvedTarget(req, url) {
|
|
|
200
225
|
// The same body, so the same framing: a runtime still delimited it.
|
|
201
226
|
if (hasTrustedBodyFraming(req))
|
|
202
227
|
markTrustedBodyFraming(rebuilt);
|
|
228
|
+
carryToCopy(req, rebuilt);
|
|
203
229
|
return rebuilt;
|
|
204
230
|
}
|
|
205
231
|
/**
|
|
@@ -254,26 +280,35 @@ const invalidHost = (host) => {
|
|
|
254
280
|
* Host (400), a URL replaces the target, `false` keeps it. Deno hands the target over as sent, so
|
|
255
281
|
* its dot segments are resolved; so does Bun when HTTP/1.0 leaves the Host out, and its route table
|
|
256
282
|
* matched `/files/..` raw. Such a request is served as `localhost`, as Node and Deno serve it.
|
|
257
|
-
* `headerOf`, not `req.headers`: on Deno
|
|
258
|
-
*
|
|
283
|
+
* `headerOf`, not `req.headers`: on Deno it reads Host only when `validateHost` is set, and dot
|
|
284
|
+
* segments are resolved only when `normalizeTarget` is. On Bun only a target that needs resolving
|
|
285
|
+
* is checked unless `validateHost` is set.
|
|
259
286
|
*/
|
|
260
|
-
const requestTarget = (req, validateHost) => {
|
|
261
|
-
if (
|
|
287
|
+
const requestTarget = (req, validateHost, normalizeTarget) => {
|
|
288
|
+
if (rawRequestTargets) {
|
|
289
|
+
const host = validateHost ? headerOf(req, "host") : null;
|
|
290
|
+
if (host !== null && invalidHost(host))
|
|
291
|
+
return true;
|
|
292
|
+
if (!normalizeTarget || !needsResolving(req.url))
|
|
293
|
+
return false;
|
|
294
|
+
// Deno can preserve an unparseable absolute URL when Host is malformed. Do not route its
|
|
295
|
+
// unresolved dot segments through the raw-target fallback.
|
|
296
|
+
if (!URL.canParse(req.url, "http://localhost"))
|
|
297
|
+
return true;
|
|
298
|
+
return resolveDotSegments(req.url);
|
|
299
|
+
}
|
|
300
|
+
if (!(unparsedBunTargets && (validateHost || needsResolving(req.url))))
|
|
262
301
|
return false;
|
|
263
302
|
const host = headerOf(req, "host");
|
|
264
303
|
if (host !== null && invalidHost(host)) {
|
|
265
|
-
if (rawRequestTargets)
|
|
266
|
-
return true;
|
|
267
304
|
// Bun served it anyway: it could not read the Host, so it left the target unparsed, or it
|
|
268
305
|
// built the URL on it - sent twice (joined with ", ", the URL on the first; RFC 9112 section
|
|
269
|
-
// 3.2 answers 400), percent-encoded (
|
|
270
|
-
// sub-delimiter (
|
|
306
|
+
// 3.2 answers 400), percent-encoded (%61.example routed as a.example), or with a
|
|
307
|
+
// sub-delimiter (a!b). A request built in process keeps the URL its caller built, unless Bun
|
|
271
308
|
// would have built it so.
|
|
272
309
|
const routed = URL.parse(req.url)?.host;
|
|
273
310
|
return routed === undefined || URL.parse(`//${host.split(", ")[0]}`, req.url)?.host === routed;
|
|
274
311
|
}
|
|
275
|
-
if (rawRequestTargets)
|
|
276
|
-
return needsResolving(req.url) && resolveDotSegments(req.url);
|
|
277
312
|
// A request built in process carries an absolute URL whatever its headers say.
|
|
278
313
|
return (host === null &&
|
|
279
314
|
req.url.charCodeAt(0) === 47 /* / */ &&
|
|
@@ -307,7 +342,8 @@ function mountPriorityOf(priority) {
|
|
|
307
342
|
function compareMounts(a, b) {
|
|
308
343
|
return b.priority - a.priority || b.path.length - a.path.length || a.order - b.order;
|
|
309
344
|
}
|
|
310
|
-
/** Rebase a mounted request's path without losing the authentication mark used by child caches
|
|
345
|
+
/** Rebase a mounted request's path without losing the authentication mark used by child caches, or
|
|
346
|
+
* its request locals. */
|
|
311
347
|
function stripMountPrefix(request, prefix) {
|
|
312
348
|
if (prefix === "/")
|
|
313
349
|
return request;
|
|
@@ -317,11 +353,39 @@ function stripMountPrefix(request, prefix) {
|
|
|
317
353
|
const childRequest = new Request(url.href, request);
|
|
318
354
|
if (isAuthenticatedRequest(request))
|
|
319
355
|
markAuthenticatedRequest(childRequest);
|
|
356
|
+
carryToCopy(request, childRequest);
|
|
320
357
|
return childRequest;
|
|
321
358
|
}
|
|
322
359
|
function responseSet(ctx) {
|
|
323
360
|
return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS;
|
|
324
361
|
}
|
|
362
|
+
/** The error hooks of the many fused routes that have none, shared so each does not allocate its own. */
|
|
363
|
+
const NO_ERROR_HOOKS = [];
|
|
364
|
+
const WEB_STAGES = new WeakMap();
|
|
365
|
+
const NODE_STAGES = new WeakMap();
|
|
366
|
+
const hookCount = (hooks) => hooks.length > 1 ? 2 : hooks.length;
|
|
367
|
+
/**
|
|
368
|
+
* The fused stage chain of a hook shape, built once per server and handed to every route with the same
|
|
369
|
+
* number of derive, before and after hooks (none, one or several of each). The chain reads a route's
|
|
370
|
+
* hooks and handler from the `FusedHooks` it is called with, so sharing it changes no answer.
|
|
371
|
+
*/
|
|
372
|
+
function sharedStages(cache, owner, route, build) {
|
|
373
|
+
const derives = hookCount(route.derives);
|
|
374
|
+
const befores = hookCount(route.befores);
|
|
375
|
+
const afters = hookCount(route.afters);
|
|
376
|
+
const key = derives * 9 + befores * 3 + afters;
|
|
377
|
+
let chains = cache.get(owner);
|
|
378
|
+
if (chains === undefined) {
|
|
379
|
+
chains = new Map();
|
|
380
|
+
cache.set(owner, chains);
|
|
381
|
+
}
|
|
382
|
+
let stages = chains.get(key);
|
|
383
|
+
if (stages === undefined) {
|
|
384
|
+
stages = build(derives, befores, afters);
|
|
385
|
+
chains.set(key, stages);
|
|
386
|
+
}
|
|
387
|
+
return stages;
|
|
388
|
+
}
|
|
325
389
|
function isContextlessNoArgArrow(handler) {
|
|
326
390
|
if (handler.length !== 0)
|
|
327
391
|
return false;
|
|
@@ -655,6 +719,7 @@ export class Server {
|
|
|
655
719
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
656
720
|
trustBodyFraming;
|
|
657
721
|
validateHost;
|
|
722
|
+
normalizeTarget;
|
|
658
723
|
wsMaxPayloadBytes;
|
|
659
724
|
requestTimeoutMs;
|
|
660
725
|
wsUpgradeTimeoutMs;
|
|
@@ -788,6 +853,7 @@ export class Server {
|
|
|
788
853
|
this.formOrigins = options.formOrigins;
|
|
789
854
|
this.trustBodyFraming = options.trustBodyFraming ?? false;
|
|
790
855
|
this.validateHost = options.validateHost ?? false;
|
|
856
|
+
this.normalizeTarget = options.normalizeTarget ?? false;
|
|
791
857
|
const wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? maxBodyBytes;
|
|
792
858
|
assertByteLimit(wsMaxPayloadBytes, "wsMaxPayloadBytes");
|
|
793
859
|
this.maxBodyBytes = maxBodyBytes;
|
|
@@ -1642,39 +1708,42 @@ export class Server {
|
|
|
1642
1708
|
handler = this.idempotencyRuntime.wrapHandler(handler);
|
|
1643
1709
|
}
|
|
1644
1710
|
const { bare, fusedQuery, fusedBody } = lanes;
|
|
1645
|
-
//
|
|
1646
|
-
//
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1711
|
+
// `selectRouteLanes` admits the fused hook lanes only for derive, before and after hooks, so the
|
|
1712
|
+
// three arrays below are the route's whole lifecycle.
|
|
1713
|
+
const fusedLifecycle = lanes.fusedLane === "hooks" || lanes.fusedLane === "body-hooks";
|
|
1714
|
+
// The fused chains keep the arrays they are built from, and `beforeHandle()` inserts into the live
|
|
1715
|
+
// ones ahead of `beforeHandler()` guards, so the entry and the fused chains share one snapshot of
|
|
1716
|
+
// this route's.
|
|
1717
|
+
const derivesSnapshot = [...this.derives];
|
|
1718
|
+
const beforesSnapshot = [...beforeHandle];
|
|
1719
|
+
const aftersSnapshot = [...afterHandle];
|
|
1720
|
+
const onErrorSnapshot = onError.length === 0 ? NO_ERROR_HOOKS : [...onError];
|
|
1721
|
+
const fusedHooks = fusedLifecycle
|
|
1722
|
+
? {
|
|
1723
|
+
handler: handler,
|
|
1724
|
+
derives: derivesSnapshot,
|
|
1725
|
+
befores: beforesSnapshot,
|
|
1726
|
+
afters: aftersSnapshot,
|
|
1727
|
+
errors: onErrorSnapshot,
|
|
1728
|
+
decorations: hasDecorations ? routeDecorations : undefined,
|
|
1729
|
+
}
|
|
1730
|
+
: undefined;
|
|
1731
|
+
// The Node-direct dispatcher (`!webFast`) prefers these runners over the generic program. Only the
|
|
1732
|
+
// body-only runner also feeds the Web wrapper; the lifecycle lanes build their own Web closure.
|
|
1656
1733
|
const fusedBodyRunner = fusedBody
|
|
1657
1734
|
? this.buildFusedBodyRunner(handler, schema?.body, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1658
|
-
:
|
|
1659
|
-
? this.
|
|
1660
|
-
:
|
|
1661
|
-
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0], beforeHandle[0], undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1662
|
-
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
1663
|
-
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1664
|
-
: undefined;
|
|
1735
|
+
: fusedHooks !== undefined
|
|
1736
|
+
? this.buildFusedLifecycleRunner(fusedHooks, schema?.query, schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1737
|
+
: undefined;
|
|
1665
1738
|
const fusedWeb = bare && this.aroundHooks.length === 0
|
|
1666
1739
|
? this.buildFusedWeb(handler, hasDecorations ? routeDecorations : undefined, isContextlessNoArgArrow(handler), bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1667
1740
|
: fusedQuery
|
|
1668
1741
|
? this.buildFusedQueryWeb(handler, hasDecorations ? routeDecorations : undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1669
1742
|
: fusedBody
|
|
1670
1743
|
? this.buildFusedBodyWeb(fusedBodyRunner)
|
|
1671
|
-
:
|
|
1672
|
-
? this.
|
|
1673
|
-
:
|
|
1674
|
-
? this.buildFusedDeriveBefore(handler, this.derives[0], beforeHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1675
|
-
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
1676
|
-
? this.buildFusedDeriveBeforeAfter(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1677
|
-
: undefined;
|
|
1744
|
+
: fusedHooks !== undefined
|
|
1745
|
+
? this.buildFusedLifecycleWeb(fusedHooks, schema?.query, schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1746
|
+
: undefined;
|
|
1678
1747
|
const program = compileRouteProgram({
|
|
1679
1748
|
schema,
|
|
1680
1749
|
handler: handler,
|
|
@@ -1706,13 +1775,13 @@ export class Server {
|
|
|
1706
1775
|
idempotent,
|
|
1707
1776
|
ledgered,
|
|
1708
1777
|
responseContract,
|
|
1709
|
-
derives:
|
|
1778
|
+
derives: derivesSnapshot,
|
|
1710
1779
|
authStages: [...compiled.authStages],
|
|
1711
1780
|
decorations: routeDecorations,
|
|
1712
1781
|
hasDecorations,
|
|
1713
|
-
beforeHandle:
|
|
1714
|
-
afterHandle:
|
|
1715
|
-
onError:
|
|
1782
|
+
beforeHandle: beforesSnapshot,
|
|
1783
|
+
afterHandle: aftersSnapshot,
|
|
1784
|
+
onError: onErrorSnapshot,
|
|
1716
1785
|
lifecycleHookLane: lanes.lifecycleHookLane,
|
|
1717
1786
|
around: [...this.aroundHooks],
|
|
1718
1787
|
execution,
|
|
@@ -2113,49 +2182,29 @@ export class Server {
|
|
|
2113
2182
|
const lane = entry.execution.fusedLane;
|
|
2114
2183
|
// Type-erase the lane so each `===` branch sees the full union, not the narrowed remainder.
|
|
2115
2184
|
const laneName = lane;
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2185
|
+
const lifecycle = laneName === "hooks" || laneName === "body-hooks";
|
|
2186
|
+
const hooks = lifecycle
|
|
2187
|
+
? {
|
|
2188
|
+
handler: entry.handler,
|
|
2189
|
+
derives: entry.derives,
|
|
2190
|
+
befores: entry.beforeHandle,
|
|
2191
|
+
afters: entry.afterHandle,
|
|
2192
|
+
errors: entry.onError,
|
|
2193
|
+
decorations: entry.hasDecorations ? entry.decorations : undefined,
|
|
2194
|
+
}
|
|
2195
|
+
: undefined;
|
|
2123
2196
|
const fusedBody = laneName === "body"
|
|
2124
2197
|
? this.buildFusedBodyRunner(entry.handler, entry.schema?.body, entry.hasDecorations ? entry.decorations : undefined, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2125
|
-
:
|
|
2126
|
-
? this.
|
|
2127
|
-
:
|
|
2128
|
-
derive !== undefined &&
|
|
2129
|
-
before !== undefined &&
|
|
2130
|
-
after !== undefined
|
|
2131
|
-
? this.buildFusedBodyDeriveBeforeAfterRunner(entry.handler, derive, before, after, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2132
|
-
: laneName === "derive-before" && derive !== undefined && before !== undefined
|
|
2133
|
-
? this.buildFusedDeriveBeforeRunner(entry.handler, derive, before, undefined, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2134
|
-
: laneName === "derive-before-after" &&
|
|
2135
|
-
derive !== undefined &&
|
|
2136
|
-
before !== undefined &&
|
|
2137
|
-
after !== undefined
|
|
2138
|
-
? this.buildFusedDeriveBeforeRunner(entry.handler, derive, before, after, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2139
|
-
: undefined;
|
|
2198
|
+
: hooks !== undefined
|
|
2199
|
+
? this.buildFusedLifecycleRunner(hooks, entry.schema?.query, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2200
|
+
: undefined;
|
|
2140
2201
|
const fusedWeb = laneName === "body"
|
|
2141
2202
|
? this.buildFusedBodyWeb(fusedBody)
|
|
2142
2203
|
: laneName === "query"
|
|
2143
2204
|
? this.buildFusedQueryWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2144
|
-
:
|
|
2145
|
-
? this.
|
|
2146
|
-
:
|
|
2147
|
-
derive !== undefined &&
|
|
2148
|
-
before !== undefined &&
|
|
2149
|
-
after !== undefined
|
|
2150
|
-
? this.buildFusedBodyDeriveBeforeAfter(entry.handler, derive, before, after, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2151
|
-
: laneName === "derive-before" && derive !== undefined && before !== undefined
|
|
2152
|
-
? this.buildFusedDeriveBefore(entry.handler, derive, before, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2153
|
-
: laneName === "derive-before-after" &&
|
|
2154
|
-
derive !== undefined &&
|
|
2155
|
-
before !== undefined &&
|
|
2156
|
-
after !== undefined
|
|
2157
|
-
? this.buildFusedDeriveBeforeAfter(entry.handler, derive, before, after, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2158
|
-
: this.buildFusedWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
|
|
2205
|
+
: hooks !== undefined
|
|
2206
|
+
? this.buildFusedLifecycleWeb(hooks, entry.schema?.query, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2207
|
+
: this.buildFusedWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
|
|
2159
2208
|
return {
|
|
2160
2209
|
...route,
|
|
2161
2210
|
entry: {
|
|
@@ -2188,7 +2237,7 @@ export class Server {
|
|
|
2188
2237
|
fetch(req, platform) {
|
|
2189
2238
|
// Deno's `Request.url` is the target as sent; Bun, workerd and every `new Request()` parse it.
|
|
2190
2239
|
// Route what the parsed URL says, so `/users/../admin` is `/admin` on every runtime.
|
|
2191
|
-
const target = requestTarget(req, this.validateHost);
|
|
2240
|
+
const target = requestTarget(req, this.validateHost, this.normalizeTarget);
|
|
2192
2241
|
if (target === true)
|
|
2193
2242
|
return jsonError(400, "invalid_host");
|
|
2194
2243
|
if (target !== false) {
|
|
@@ -2238,6 +2287,23 @@ export class Server {
|
|
|
2238
2287
|
}
|
|
2239
2288
|
return this.admitGated(requestOf(source), () => this.fetchMatchedInner(source, entry, params, platform));
|
|
2240
2289
|
}
|
|
2290
|
+
/** {@link fetchMatched} for an app whose `onRequest` hooks run before the route does: the hooks see
|
|
2291
|
+
* the request first, and the route Bun matched runs unless one of them replaced the request. */
|
|
2292
|
+
fetchMatchedHooked(source, entry, params, platform) {
|
|
2293
|
+
if (this.capacityGate === undefined) {
|
|
2294
|
+
return this.fetchMatchedHookedInner(source, entry, params, platform);
|
|
2295
|
+
}
|
|
2296
|
+
return this.admitGated(source, () => this.fetchMatchedHookedInner(source, entry, params, platform));
|
|
2297
|
+
}
|
|
2298
|
+
fetchMatchedHookedInner(source, entry, params, platform) {
|
|
2299
|
+
const outcome = this.runWithOnRequest(source, this.trustedPlatform(source, platform), this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, true, { entry, params });
|
|
2300
|
+
if (this.onResponseHooks.length === 0 && this.onResponseFinalizedHooks.length === 0) {
|
|
2301
|
+
return outcome;
|
|
2302
|
+
}
|
|
2303
|
+
return outcome instanceof Promise
|
|
2304
|
+
? outcome.then((response) => this.applyOnResponseAndFinalize(response, this.takeResponseRequest(source)))
|
|
2305
|
+
: this.applyOnResponseAndFinalize(outcome, this.takeResponseRequest(source));
|
|
2306
|
+
}
|
|
2241
2307
|
fetchMatchedInner(source, entry, params, platform) {
|
|
2242
2308
|
const outcome = this.runMatched(source, platform, entry, params, undefined, this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, true);
|
|
2243
2309
|
if (this.onResponseHooks.length === 0 && this.onResponseFinalizedHooks.length === 0) {
|
|
@@ -2597,7 +2663,7 @@ export class Server {
|
|
|
2597
2663
|
// The adapter answers a pass by handing this same object to `fetch`.
|
|
2598
2664
|
const received = req;
|
|
2599
2665
|
// The handshake routes the path `fetch` would: Host checked, dot segments resolved (see `fetch`).
|
|
2600
|
-
const target = requestTarget(req, this.validateHost);
|
|
2666
|
+
const target = requestTarget(req, this.validateHost, this.normalizeTarget);
|
|
2601
2667
|
if (target === true) {
|
|
2602
2668
|
return { kind: "reject", response: this.wrapWebResponse(jsonError(400, "invalid_host")) };
|
|
2603
2669
|
}
|
|
@@ -2786,10 +2852,10 @@ export class Server {
|
|
|
2786
2852
|
/** Bun `fetch` when WS routes exist: try a WS upgrade first, else run the normal HTTP lifecycle.
|
|
2787
2853
|
* `undefined` ⇒ Bun owns the upgraded socket; a `Response` ⇒ a normal reply or a rejected upgrade.
|
|
2788
2854
|
* (The socket dispatch itself lives in `ws-bun.ts`, loaded via `@nifrajs/core/ws`.) */
|
|
2789
|
-
bunFetchWithWebSocket(req, server) {
|
|
2855
|
+
bunFetchWithWebSocket(req, server, setIdleTimeout) {
|
|
2790
2856
|
const handle = (o) => {
|
|
2791
2857
|
if (o.kind === "pass")
|
|
2792
|
-
return this.fetch(req,
|
|
2858
|
+
return this.fetch(req, new BunPeerPlatform(server, req, setIdleTimeout));
|
|
2793
2859
|
if (o.kind === "reject")
|
|
2794
2860
|
return o.response;
|
|
2795
2861
|
// A copy: the Bun socket keeps per-connection state on it, and a hand-built outcome may be shared.
|
|
@@ -2797,7 +2863,7 @@ export class Server {
|
|
|
2797
2863
|
? undefined
|
|
2798
2864
|
: jsonError(426, "upgrade_required");
|
|
2799
2865
|
};
|
|
2800
|
-
const outcome = this.resolveWebSocketUpgrade(req,
|
|
2866
|
+
const outcome = this.resolveWebSocketUpgrade(req, new BunPeerPlatform(server, req, setIdleTimeout));
|
|
2801
2867
|
return outcome instanceof Promise ? outcome.then(handle) : handle(outcome);
|
|
2802
2868
|
}
|
|
2803
2869
|
/**
|
|
@@ -2898,7 +2964,7 @@ export class Server {
|
|
|
2898
2964
|
* at ~13% of a realistic request. The first hook that returns a Promise hands the REMAINING
|
|
2899
2965
|
* hooks to the async continuation; behavior is identical.
|
|
2900
2966
|
*/
|
|
2901
|
-
runWithOnRequest(source, platform, finalize, wrapResponse, onTimeout, webFast) {
|
|
2967
|
+
runWithOnRequest(source, platform, finalize, wrapResponse, onTimeout, webFast, matched) {
|
|
2902
2968
|
const hooks = this.onRequestHooks;
|
|
2903
2969
|
const originalRequest = requestOf(source);
|
|
2904
2970
|
// A Web Request is already the exact object visible to the hooks and the response walk. Only
|
|
@@ -2917,7 +2983,7 @@ export class Server {
|
|
|
2917
2983
|
return this.onRequestFailure(err, current, platform, finalize, wrapResponse);
|
|
2918
2984
|
}
|
|
2919
2985
|
if (outcome instanceof Promise) {
|
|
2920
|
-
return outcome.then((early) => this.continueOnRequest(early, i + 1, originalRequest, current, platform, finalize, wrapResponse, onTimeout, webFast), (err) => this.onRequestFailure(err, current, platform, finalize, wrapResponse));
|
|
2986
|
+
return outcome.then((early) => this.continueOnRequest(early, i + 1, originalRequest, current, platform, finalize, wrapResponse, onTimeout, webFast, matched), (err) => this.onRequestFailure(err, current, platform, finalize, wrapResponse));
|
|
2921
2987
|
}
|
|
2922
2988
|
if (outcome instanceof Request) {
|
|
2923
2989
|
if (outcome !== seen)
|
|
@@ -2930,11 +2996,11 @@ export class Server {
|
|
|
2930
2996
|
if (outcome !== undefined)
|
|
2931
2997
|
return earlyAnswer(outcome, wrapResponse);
|
|
2932
2998
|
}
|
|
2933
|
-
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast);
|
|
2999
|
+
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast, current === originalRequest ? matched : undefined);
|
|
2934
3000
|
}
|
|
2935
3001
|
/** Async tail of {@link runWithOnRequest}: applies the first awaited hook's outcome, then runs
|
|
2936
3002
|
* the remaining hooks (awaiting freely - we're already async here). */
|
|
2937
|
-
async continueOnRequest(first, nextIndex, originalRequest, sourceAtAwait, platform, finalize, wrapResponse, onTimeout, webFast) {
|
|
3003
|
+
async continueOnRequest(first, nextIndex, originalRequest, sourceAtAwait, platform, finalize, wrapResponse, onTimeout, webFast, matched) {
|
|
2938
3004
|
let current = sourceAtAwait;
|
|
2939
3005
|
let seen = requestOf(sourceAtAwait);
|
|
2940
3006
|
let early = first;
|
|
@@ -2962,7 +3028,7 @@ export class Server {
|
|
|
2962
3028
|
}
|
|
2963
3029
|
index++;
|
|
2964
3030
|
}
|
|
2965
|
-
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast);
|
|
3031
|
+
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast, current === originalRequest ? matched : undefined);
|
|
2966
3032
|
}
|
|
2967
3033
|
takeResponseRequest(source) {
|
|
2968
3034
|
const tracked = this.responseSources.get(source);
|
|
@@ -3031,7 +3097,12 @@ export class Server {
|
|
|
3031
3097
|
* (selected by its compiled plan), so a sync handler produces its result with zero promise allocations;
|
|
3032
3098
|
* routes with validation/hooks keep the full compiled route program, unchanged.
|
|
3033
3099
|
*/
|
|
3034
|
-
routeAndRun(source, platform, finalize, wrapResponse, onTimeout, webFast
|
|
3100
|
+
routeAndRun(source, platform, finalize, wrapResponse, onTimeout, webFast,
|
|
3101
|
+
// The route Bun's table already matched for this very request, which routing would find again.
|
|
3102
|
+
matched) {
|
|
3103
|
+
if (matched !== undefined) {
|
|
3104
|
+
return this.runMatched(source, platform, matched.entry, matched.params, undefined, finalize, wrapResponse, onTimeout, webFast);
|
|
3105
|
+
}
|
|
3035
3106
|
// Native delimiter searches keep path/query splitting out of a character-by-character JS loop.
|
|
3036
3107
|
// Keep the slices in locals instead of allocating the public helper's pathname/search pair.
|
|
3037
3108
|
let pathname;
|
|
@@ -3521,53 +3592,244 @@ export class Server {
|
|
|
3521
3592
|
};
|
|
3522
3593
|
}
|
|
3523
3594
|
/**
|
|
3524
|
-
*
|
|
3525
|
-
*
|
|
3526
|
-
*
|
|
3527
|
-
*
|
|
3528
|
-
* and
|
|
3529
|
-
* hot hidden class. Behavior is byte-identical to the compiled route program: the same thrown-value
|
|
3530
|
-
* contract (`Response` is control flow, anything else logs + 500), the same `c.set` semantics, the
|
|
3531
|
-
* same `fusedRespond` for the rendered body. The `LifecycleExecutionLane` guarantee
|
|
3532
|
-
* (`schema.params === undefined && schema.headers === undefined && derives === 1 &&
|
|
3533
|
-
* beforeHandle === 1 && afterHandle === 0 && onError === 0 && !hasResponseContract &&
|
|
3534
|
-
* !hasDecorations`) is enforced by `selectRouteLanes`; this lane is selected ONLY when all of those
|
|
3535
|
-
* hold. A `query` schema (the route declares `{ query }` alongside the hooks) is validated FIRST,
|
|
3536
|
-
* mirroring the compiled route program's stage order; the fused builder runs validate → derive →
|
|
3537
|
-
* before → handler in one frame.
|
|
3595
|
+
* The stage chain a hook shape runs - derive, before, handler, after, respond - for the Web lane,
|
|
3596
|
+
* built once per server and shared by every route with the same number of hooks in each phase (none,
|
|
3597
|
+
* one or several). The chain reads a route's handler and hooks from its `FusedHooks` per request, so
|
|
3598
|
+
* a route's own closure (see {@link buildFusedLifecycleWeb}) holds only that record and its schemas,
|
|
3599
|
+
* and memory grows with the number of distinct shapes, not routes or hook functions.
|
|
3538
3600
|
*/
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3601
|
+
fusedWebStages(deriveCount, beforeCount, afterCount) {
|
|
3602
|
+
let runAfter;
|
|
3603
|
+
if (afterCount === 0) {
|
|
3604
|
+
runAfter = (result, ctx, route) => this.respondHooks(result, ctx, route, this.onResponseHooks.length === 0);
|
|
3605
|
+
}
|
|
3606
|
+
else if (afterCount === 1) {
|
|
3607
|
+
runAfter = (result, ctx, route) => {
|
|
3608
|
+
let transformed;
|
|
3609
|
+
try {
|
|
3610
|
+
transformed = route.afters[0](result, ctx);
|
|
3611
|
+
}
|
|
3612
|
+
catch (err) {
|
|
3613
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3614
|
+
}
|
|
3615
|
+
if (transformed instanceof Promise) {
|
|
3616
|
+
return transformed.then((value) => this.respondHooks(value, ctx, route, this.onResponseHooks.length === 0), (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3617
|
+
}
|
|
3618
|
+
return this.respondHooks(transformed, ctx, route, this.onResponseHooks.length === 0);
|
|
3619
|
+
};
|
|
3620
|
+
}
|
|
3621
|
+
else {
|
|
3622
|
+
runAfter = (result, ctx, route, index) => {
|
|
3623
|
+
const lastAfter = route.afters.length - 1;
|
|
3624
|
+
for (;;) {
|
|
3625
|
+
let transformed;
|
|
3626
|
+
try {
|
|
3627
|
+
transformed = route.afters[index](result, ctx);
|
|
3628
|
+
}
|
|
3629
|
+
catch (err) {
|
|
3630
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3631
|
+
}
|
|
3632
|
+
if (transformed instanceof Promise) {
|
|
3633
|
+
return transformed.then((value) => index < lastAfter
|
|
3634
|
+
? runAfter(value, ctx, route, index + 1)
|
|
3635
|
+
: this.respondHooks(value, ctx, route, this.onResponseHooks.length === 0), (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3636
|
+
}
|
|
3637
|
+
if (index === lastAfter) {
|
|
3638
|
+
return this.respondHooks(transformed, ctx, route, this.onResponseHooks.length === 0);
|
|
3639
|
+
}
|
|
3640
|
+
result = transformed;
|
|
3641
|
+
index++;
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3645
|
+
const runHandler = (ctx, route) => {
|
|
3646
|
+
let result;
|
|
3550
3647
|
try {
|
|
3551
|
-
|
|
3648
|
+
result = route.handler(ctx);
|
|
3552
3649
|
}
|
|
3553
3650
|
catch (err) {
|
|
3554
|
-
return
|
|
3651
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3555
3652
|
}
|
|
3556
|
-
if (
|
|
3557
|
-
return
|
|
3653
|
+
if (result instanceof Promise) {
|
|
3654
|
+
return result.then((value) => runAfter(value, ctx, route, 0), (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3558
3655
|
}
|
|
3559
|
-
return
|
|
3656
|
+
return runAfter(result, ctx, route, 0);
|
|
3560
3657
|
};
|
|
3658
|
+
let runBefore;
|
|
3659
|
+
if (beforeCount === 0) {
|
|
3660
|
+
runBefore = runHandler;
|
|
3661
|
+
}
|
|
3662
|
+
else if (beforeCount === 1) {
|
|
3663
|
+
runBefore = (ctx, route) => {
|
|
3664
|
+
let early;
|
|
3665
|
+
try {
|
|
3666
|
+
early = route.befores[0](ctx);
|
|
3667
|
+
}
|
|
3668
|
+
catch (err) {
|
|
3669
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3670
|
+
}
|
|
3671
|
+
if (early instanceof Promise) {
|
|
3672
|
+
return early.then((settled) => settled === undefined
|
|
3673
|
+
? runHandler(ctx, route, 0)
|
|
3674
|
+
: this.respondHooks(settled, ctx, route), (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3675
|
+
}
|
|
3676
|
+
if (early === undefined)
|
|
3677
|
+
return runHandler(ctx, route, 0);
|
|
3678
|
+
return this.respondHooks(early, ctx, route);
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
else {
|
|
3682
|
+
runBefore = (ctx, route, index) => {
|
|
3683
|
+
const lastBefore = route.befores.length - 1;
|
|
3684
|
+
for (;;) {
|
|
3685
|
+
let early;
|
|
3686
|
+
try {
|
|
3687
|
+
early = route.befores[index](ctx);
|
|
3688
|
+
}
|
|
3689
|
+
catch (err) {
|
|
3690
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3691
|
+
}
|
|
3692
|
+
if (early instanceof Promise) {
|
|
3693
|
+
return early.then((settled) => {
|
|
3694
|
+
if (settled !== undefined)
|
|
3695
|
+
return this.respondHooks(settled, ctx, route);
|
|
3696
|
+
return index < lastBefore
|
|
3697
|
+
? runBefore(ctx, route, index + 1)
|
|
3698
|
+
: runHandler(ctx, route, 0);
|
|
3699
|
+
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3700
|
+
}
|
|
3701
|
+
if (early !== undefined)
|
|
3702
|
+
return this.respondHooks(early, ctx, route);
|
|
3703
|
+
if (index === lastBefore)
|
|
3704
|
+
return runHandler(ctx, route, 0);
|
|
3705
|
+
index++;
|
|
3706
|
+
}
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
let runDerive;
|
|
3710
|
+
if (deriveCount === 0) {
|
|
3711
|
+
runDerive = runBefore;
|
|
3712
|
+
}
|
|
3713
|
+
else if (deriveCount === 1) {
|
|
3714
|
+
runDerive = (ctx, route) => {
|
|
3715
|
+
let derived;
|
|
3716
|
+
try {
|
|
3717
|
+
derived = route.derives[0](ctx);
|
|
3718
|
+
}
|
|
3719
|
+
catch (err) {
|
|
3720
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3721
|
+
}
|
|
3722
|
+
if (derived instanceof Promise) {
|
|
3723
|
+
return derived.then((settled) => {
|
|
3724
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3725
|
+
return this.respondHooks(settled, ctx, route);
|
|
3726
|
+
}
|
|
3727
|
+
try {
|
|
3728
|
+
Object.assign(ctx, settled);
|
|
3729
|
+
}
|
|
3730
|
+
catch (err) {
|
|
3731
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3732
|
+
}
|
|
3733
|
+
return runBefore(ctx, route, 0);
|
|
3734
|
+
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3735
|
+
}
|
|
3736
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3737
|
+
return this.respondHooks(derived, ctx, route);
|
|
3738
|
+
}
|
|
3739
|
+
try {
|
|
3740
|
+
Object.assign(ctx, derived);
|
|
3741
|
+
}
|
|
3742
|
+
catch (err) {
|
|
3743
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3744
|
+
}
|
|
3745
|
+
return runBefore(ctx, route, 0);
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
else {
|
|
3749
|
+
runDerive = (ctx, route, index) => {
|
|
3750
|
+
const lastDerive = route.derives.length - 1;
|
|
3751
|
+
for (;;) {
|
|
3752
|
+
let derived;
|
|
3753
|
+
try {
|
|
3754
|
+
derived = route.derives[index](ctx);
|
|
3755
|
+
}
|
|
3756
|
+
catch (err) {
|
|
3757
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3758
|
+
}
|
|
3759
|
+
if (derived instanceof Promise) {
|
|
3760
|
+
return derived.then((settled) => {
|
|
3761
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3762
|
+
return this.respondHooks(settled, ctx, route);
|
|
3763
|
+
}
|
|
3764
|
+
try {
|
|
3765
|
+
Object.assign(ctx, settled);
|
|
3766
|
+
}
|
|
3767
|
+
catch (err) {
|
|
3768
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3769
|
+
}
|
|
3770
|
+
return index < lastDerive
|
|
3771
|
+
? runDerive(ctx, route, index + 1)
|
|
3772
|
+
: runBefore(ctx, route, 0);
|
|
3773
|
+
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3774
|
+
}
|
|
3775
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3776
|
+
return this.respondHooks(derived, ctx, route);
|
|
3777
|
+
}
|
|
3778
|
+
try {
|
|
3779
|
+
Object.assign(ctx, derived);
|
|
3780
|
+
}
|
|
3781
|
+
catch (err) {
|
|
3782
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3783
|
+
}
|
|
3784
|
+
if (index === lastDerive)
|
|
3785
|
+
return runBefore(ctx, route, 0);
|
|
3786
|
+
index++;
|
|
3787
|
+
}
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
return runDerive;
|
|
3791
|
+
}
|
|
3792
|
+
/**
|
|
3793
|
+
* Compile a fused lifecycle route's entry closure: parse (body) → validate → the shared stage chain
|
|
3794
|
+
* of its hook shape (derive → before → handler → after → respond, see {@link fusedWebStages}) in a
|
|
3795
|
+
* single frame, where the generic program walks staged hook loops. `selectRouteLanes` admits only
|
|
3796
|
+
* derive, before and after hooks - no params or headers schema, onError, around, decorations,
|
|
3797
|
+
* response contract or recovery hook, and never a query schema beside a body schema - so the behavior
|
|
3798
|
+
* is the compiled program's: the same stage order, the same thrown-value contract (`Response` is
|
|
3799
|
+
* control flow, anything else logs and 500s), the same `c.set` semantics and `fusedRespond` render.
|
|
3800
|
+
* The route's own closure holds only its `FusedHooks` and schemas and takes the request's context as
|
|
3801
|
+
* an argument, so a request allocates no step closures.
|
|
3802
|
+
*/
|
|
3803
|
+
/**
|
|
3804
|
+
* Keep the common one-derive/one-before shape bound to its hooks. Inline its synchronous path to
|
|
3805
|
+
* avoid stage calls; retain helper closures only for promise continuations.
|
|
3806
|
+
*/
|
|
3807
|
+
buildFusedLifecycleWebSingle(handler, derive, before, after, maxBodyBytes) {
|
|
3808
|
+
const runAfter = after === undefined
|
|
3809
|
+
? (result, ctx) => this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
3810
|
+
: (result, ctx) => {
|
|
3811
|
+
let transformed;
|
|
3812
|
+
try {
|
|
3813
|
+
transformed = after(result, ctx);
|
|
3814
|
+
}
|
|
3815
|
+
catch (err) {
|
|
3816
|
+
return this.fusedWebFail(err, ctx);
|
|
3817
|
+
}
|
|
3818
|
+
if (transformed instanceof Promise) {
|
|
3819
|
+
return transformed.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => this.fusedWebFail(err, ctx));
|
|
3820
|
+
}
|
|
3821
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
|
|
3822
|
+
};
|
|
3561
3823
|
const runHandler = (ctx) => {
|
|
3562
3824
|
let result;
|
|
3563
3825
|
try {
|
|
3564
3826
|
result = handler(ctx);
|
|
3565
3827
|
}
|
|
3566
3828
|
catch (err) {
|
|
3567
|
-
return
|
|
3829
|
+
return this.fusedWebFail(err, ctx);
|
|
3568
3830
|
}
|
|
3569
3831
|
if (result instanceof Promise) {
|
|
3570
|
-
return result.then((value) => runAfter(value, ctx), (err) =>
|
|
3832
|
+
return result.then((value) => runAfter(value, ctx), (err) => this.fusedWebFail(err, ctx));
|
|
3571
3833
|
}
|
|
3572
3834
|
return runAfter(result, ctx);
|
|
3573
3835
|
};
|
|
@@ -3577,461 +3839,463 @@ export class Server {
|
|
|
3577
3839
|
early = before(ctx);
|
|
3578
3840
|
}
|
|
3579
3841
|
catch (err) {
|
|
3580
|
-
return
|
|
3842
|
+
return this.fusedWebFail(err, ctx);
|
|
3581
3843
|
}
|
|
3582
3844
|
if (early instanceof Promise) {
|
|
3583
|
-
return early.then((settled) =>
|
|
3584
|
-
if (settled === undefined)
|
|
3585
|
-
return runHandler(ctx);
|
|
3586
|
-
return this.respondFused(settled, ctx);
|
|
3587
|
-
}, (err) => logError(err, ctx));
|
|
3845
|
+
return early.then((settled) => (settled === undefined ? runHandler(ctx) : this.respondFused(settled, ctx)), (err) => this.fusedWebFail(err, ctx));
|
|
3588
3846
|
}
|
|
3589
3847
|
if (early === undefined)
|
|
3590
3848
|
return runHandler(ctx);
|
|
3591
3849
|
return this.respondFused(early, ctx);
|
|
3592
3850
|
};
|
|
3593
|
-
|
|
3851
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3852
|
+
const ctx = nativeContext
|
|
3853
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3854
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3594
3855
|
let derived;
|
|
3595
3856
|
try {
|
|
3596
3857
|
derived = derive(ctx);
|
|
3597
3858
|
}
|
|
3598
3859
|
catch (err) {
|
|
3599
|
-
return
|
|
3860
|
+
return this.fusedWebFail(err, ctx);
|
|
3600
3861
|
}
|
|
3601
3862
|
if (derived instanceof Promise) {
|
|
3602
3863
|
return derived.then((settled) => {
|
|
3603
|
-
if (isResponseResult(settled) || settled instanceof Response)
|
|
3864
|
+
if (isResponseResult(settled) || settled instanceof Response)
|
|
3604
3865
|
return this.respondFused(settled, ctx);
|
|
3866
|
+
try {
|
|
3867
|
+
Object.assign(ctx, settled);
|
|
3868
|
+
}
|
|
3869
|
+
catch (err) {
|
|
3870
|
+
return this.fusedWebFail(err, ctx);
|
|
3605
3871
|
}
|
|
3606
|
-
Object.assign(ctx, settled);
|
|
3607
3872
|
return runBefore(ctx);
|
|
3608
|
-
}, (err) =>
|
|
3873
|
+
}, (err) => this.fusedWebFail(err, ctx));
|
|
3609
3874
|
}
|
|
3610
|
-
if (isResponseResult(derived) || derived instanceof Response)
|
|
3875
|
+
if (isResponseResult(derived) || derived instanceof Response)
|
|
3611
3876
|
return this.respondFused(derived, ctx);
|
|
3877
|
+
let early;
|
|
3878
|
+
let result;
|
|
3879
|
+
let transformed;
|
|
3880
|
+
let handlerPromise;
|
|
3881
|
+
try {
|
|
3882
|
+
Object.assign(ctx, derived);
|
|
3883
|
+
early = before(ctx);
|
|
3884
|
+
if (early === undefined) {
|
|
3885
|
+
result = handler(ctx);
|
|
3886
|
+
if (result instanceof Promise)
|
|
3887
|
+
handlerPromise = result;
|
|
3888
|
+
else if (after !== undefined)
|
|
3889
|
+
transformed = after(result, ctx);
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
catch (err) {
|
|
3893
|
+
return this.fusedWebFail(err, ctx);
|
|
3894
|
+
}
|
|
3895
|
+
if (early instanceof Promise) {
|
|
3896
|
+
return early.then((settled) => (settled === undefined ? runHandler(ctx) : this.respondFused(settled, ctx)), (err) => this.fusedWebFail(err, ctx));
|
|
3612
3897
|
}
|
|
3613
|
-
|
|
3614
|
-
|
|
3898
|
+
if (early !== undefined)
|
|
3899
|
+
return this.respondFused(early, ctx);
|
|
3900
|
+
if (handlerPromise !== undefined) {
|
|
3901
|
+
return handlerPromise.then((value) => after === undefined
|
|
3902
|
+
? this.respondFused(value, ctx, this.onResponseHooks.length === 0)
|
|
3903
|
+
: runAfter(value, ctx), (err) => this.fusedWebFail(err, ctx));
|
|
3904
|
+
}
|
|
3905
|
+
if (after === undefined)
|
|
3906
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0);
|
|
3907
|
+
if (transformed instanceof Promise) {
|
|
3908
|
+
return transformed.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => this.fusedWebFail(err, ctx));
|
|
3909
|
+
}
|
|
3910
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
|
|
3615
3911
|
};
|
|
3912
|
+
}
|
|
3913
|
+
buildFusedLifecycleWeb(route, querySchema, bodySchema, maxBodyBytes) {
|
|
3914
|
+
if (querySchema === undefined &&
|
|
3915
|
+
bodySchema === undefined &&
|
|
3916
|
+
route.errors.length === 0 &&
|
|
3917
|
+
route.decorations === undefined &&
|
|
3918
|
+
route.derives.length === 1 &&
|
|
3919
|
+
route.befores.length === 1 &&
|
|
3920
|
+
route.afters.length <= 1) {
|
|
3921
|
+
const derive = route.derives[0];
|
|
3922
|
+
const before = route.befores[0];
|
|
3923
|
+
if (derive !== undefined && before !== undefined) {
|
|
3924
|
+
return this.buildFusedLifecycleWebSingle(route.handler, derive, before, route.afters[0], maxBodyBytes);
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
const runDerive = sharedStages(WEB_STAGES, this, route, (d, b, a) => this.fusedWebStages(d, b, a));
|
|
3928
|
+
const decorations = route.decorations;
|
|
3929
|
+
if (bodySchema !== undefined) {
|
|
3930
|
+
const onParsed = (parsed, ctx) => {
|
|
3931
|
+
let validation;
|
|
3932
|
+
try {
|
|
3933
|
+
validation = bodySchema["~standard"].validate(parsed);
|
|
3934
|
+
}
|
|
3935
|
+
catch (err) {
|
|
3936
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3937
|
+
}
|
|
3938
|
+
if (validation instanceof Promise) {
|
|
3939
|
+
return validation.then((settled) => {
|
|
3940
|
+
if (settled.issues !== undefined)
|
|
3941
|
+
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
3942
|
+
ctx.body = settled.value;
|
|
3943
|
+
return runDerive(ctx, route, 0);
|
|
3944
|
+
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3945
|
+
}
|
|
3946
|
+
if (validation.issues !== undefined)
|
|
3947
|
+
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
3948
|
+
ctx.body = validation.value;
|
|
3949
|
+
return runDerive(ctx, route, 0);
|
|
3950
|
+
};
|
|
3951
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3952
|
+
const ctx = nativeContext
|
|
3953
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3954
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3955
|
+
if (decorations !== undefined)
|
|
3956
|
+
Object.assign(ctx, decorations);
|
|
3957
|
+
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx), this.wrapWebResponse, (err) => this.fusedWebRouteFail(err, ctx, route), bodySchema, this.formOrigins);
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3616
3960
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3617
3961
|
const ctx = nativeContext
|
|
3618
3962
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3619
3963
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3964
|
+
if (decorations !== undefined)
|
|
3965
|
+
Object.assign(ctx, decorations);
|
|
3620
3966
|
if (querySchema === undefined)
|
|
3621
|
-
return runDerive(ctx);
|
|
3967
|
+
return runDerive(ctx, route, 0);
|
|
3622
3968
|
let validation;
|
|
3623
3969
|
try {
|
|
3624
3970
|
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
3625
3971
|
}
|
|
3626
3972
|
catch (err) {
|
|
3627
|
-
return
|
|
3973
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3628
3974
|
}
|
|
3629
3975
|
if (validation instanceof Promise) {
|
|
3630
3976
|
return validation.then((settled) => {
|
|
3631
3977
|
if (settled.issues !== undefined)
|
|
3632
3978
|
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
3633
3979
|
ctx.query = settled.value;
|
|
3634
|
-
return runDerive(ctx);
|
|
3635
|
-
}, (err) =>
|
|
3980
|
+
return runDerive(ctx, route, 0);
|
|
3981
|
+
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3636
3982
|
}
|
|
3637
3983
|
if (validation.issues !== undefined)
|
|
3638
3984
|
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
3639
3985
|
ctx.query = validation.value;
|
|
3640
|
-
return runDerive(ctx);
|
|
3986
|
+
return runDerive(ctx, route, 0);
|
|
3641
3987
|
};
|
|
3642
3988
|
}
|
|
3643
3989
|
/**
|
|
3644
|
-
*
|
|
3645
|
-
*
|
|
3646
|
-
*
|
|
3647
|
-
* ONLY when `schema.body !== undefined && schema.query/params/headers === undefined &&
|
|
3648
|
-
* schema.onValidationError === undefined && !defaultOnValidationError && !hasIdempotency &&
|
|
3649
|
-
* !hasLedger && derives === 1 && beforeHandle === 1 && afterHandle <= 1 && onError === 0 &&
|
|
3650
|
-
* !hasResponseContract && !hasDecorations && around === 0`. The same single `querySchema === undefined`
|
|
3651
|
-
* trick used in the no-body lane lets the build path stay simple: a query + body route falls back
|
|
3652
|
-
* to the generic program (this is rare in practice - validated query + validated body is usually a
|
|
3653
|
-
* "search results" route with a middleware chain on top, and the generic lane is fine there).
|
|
3990
|
+
* The Node-direct twin of {@link fusedWebStages}: the same chain, finalizing through the caller's
|
|
3991
|
+
* own `finalize`, shared by every route with the same hook shape. A terminal `finalize` throw (an
|
|
3992
|
+
* unstringifiable handler return on the Node lane) is caught as `finishLifecycleContract` catches it.
|
|
3654
3993
|
*/
|
|
3655
|
-
|
|
3656
|
-
const
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3994
|
+
fusedNodeStages(deriveCount, beforeCount, afterCount) {
|
|
3995
|
+
const finishResult = (result, ctx, route, finalize, wrapResponse) => {
|
|
3996
|
+
try {
|
|
3997
|
+
return finalize(result, responseSet(ctx), ctx);
|
|
3998
|
+
}
|
|
3999
|
+
catch (err) {
|
|
4000
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
let runAfter;
|
|
4004
|
+
if (afterCount === 0) {
|
|
4005
|
+
runAfter = (result, ctx, route, finalize, wrapResponse) => finishResult(result, ctx, route, finalize, wrapResponse);
|
|
4006
|
+
}
|
|
4007
|
+
else if (afterCount === 1) {
|
|
4008
|
+
runAfter = (result, ctx, route, finalize, wrapResponse) => {
|
|
4009
|
+
let transformed;
|
|
4010
|
+
try {
|
|
4011
|
+
transformed = route.afters[0](result, ctx);
|
|
4012
|
+
}
|
|
4013
|
+
catch (err) {
|
|
4014
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4015
|
+
}
|
|
4016
|
+
if (transformed instanceof Promise) {
|
|
4017
|
+
return transformed.then((value) => finishResult(value, ctx, route, finalize, wrapResponse), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
4018
|
+
}
|
|
4019
|
+
return finishResult(transformed, ctx, route, finalize, wrapResponse);
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
else {
|
|
4023
|
+
runAfter = (result, ctx, route, finalize, wrapResponse, index) => {
|
|
4024
|
+
const lastAfter = route.afters.length - 1;
|
|
4025
|
+
for (;;) {
|
|
3666
4026
|
let transformed;
|
|
3667
4027
|
try {
|
|
3668
|
-
transformed =
|
|
4028
|
+
transformed = route.afters[index](result, ctx);
|
|
3669
4029
|
}
|
|
3670
4030
|
catch (err) {
|
|
3671
|
-
return
|
|
4031
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
3672
4032
|
}
|
|
3673
4033
|
if (transformed instanceof Promise) {
|
|
3674
|
-
return transformed.then((value) =>
|
|
4034
|
+
return transformed.then((value) => index < lastAfter
|
|
4035
|
+
? runAfter(value, ctx, route, finalize, wrapResponse, index + 1)
|
|
4036
|
+
: finishResult(value, ctx, route, finalize, wrapResponse), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
3675
4037
|
}
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
4038
|
+
if (index === lastAfter)
|
|
4039
|
+
return finishResult(transformed, ctx, route, finalize, wrapResponse);
|
|
4040
|
+
result = transformed;
|
|
4041
|
+
index++;
|
|
4042
|
+
}
|
|
4043
|
+
};
|
|
4044
|
+
}
|
|
4045
|
+
const runHandler = (ctx, route, finalize, wrapResponse) => {
|
|
4046
|
+
let result;
|
|
4047
|
+
try {
|
|
4048
|
+
result = route.handler(ctx);
|
|
4049
|
+
}
|
|
4050
|
+
catch (err) {
|
|
4051
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4052
|
+
}
|
|
4053
|
+
if (result instanceof Promise) {
|
|
4054
|
+
return result.then((value) => runAfter(value, ctx, route, finalize, wrapResponse, 0), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
4055
|
+
}
|
|
4056
|
+
return runAfter(result, ctx, route, finalize, wrapResponse, 0);
|
|
4057
|
+
};
|
|
4058
|
+
let runBefore;
|
|
4059
|
+
if (beforeCount === 0) {
|
|
4060
|
+
runBefore = runHandler;
|
|
4061
|
+
}
|
|
4062
|
+
else if (beforeCount === 1) {
|
|
4063
|
+
runBefore = (ctx, route, finalize, wrapResponse) => {
|
|
4064
|
+
let early;
|
|
4065
|
+
try {
|
|
4066
|
+
early = route.befores[0](ctx);
|
|
4067
|
+
}
|
|
4068
|
+
catch (err) {
|
|
4069
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4070
|
+
}
|
|
4071
|
+
if (early instanceof Promise) {
|
|
4072
|
+
return early.then((settled) => settled === undefined
|
|
4073
|
+
? runHandler(ctx, route, finalize, wrapResponse, 0)
|
|
4074
|
+
: finishResult(settled, ctx, route, finalize, wrapResponse), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
4075
|
+
}
|
|
4076
|
+
if (early === undefined)
|
|
4077
|
+
return runHandler(ctx, route, finalize, wrapResponse, 0);
|
|
4078
|
+
return finishResult(early, ctx, route, finalize, wrapResponse);
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
else {
|
|
4082
|
+
runBefore = (ctx, route, finalize, wrapResponse, index) => {
|
|
4083
|
+
const lastBefore = route.befores.length - 1;
|
|
4084
|
+
for (;;) {
|
|
3692
4085
|
let early;
|
|
3693
4086
|
try {
|
|
3694
|
-
early =
|
|
4087
|
+
early = route.befores[index](ctx);
|
|
3695
4088
|
}
|
|
3696
4089
|
catch (err) {
|
|
3697
|
-
return
|
|
4090
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
3698
4091
|
}
|
|
3699
4092
|
if (early instanceof Promise) {
|
|
3700
4093
|
return early.then((settled) => {
|
|
3701
|
-
if (settled
|
|
3702
|
-
return
|
|
3703
|
-
return
|
|
3704
|
-
|
|
4094
|
+
if (settled !== undefined)
|
|
4095
|
+
return finishResult(settled, ctx, route, finalize, wrapResponse);
|
|
4096
|
+
return index < lastBefore
|
|
4097
|
+
? runBefore(ctx, route, finalize, wrapResponse, index + 1)
|
|
4098
|
+
: runHandler(ctx, route, finalize, wrapResponse, 0);
|
|
4099
|
+
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
3705
4100
|
}
|
|
3706
|
-
if (early
|
|
3707
|
-
return
|
|
3708
|
-
|
|
3709
|
-
|
|
4101
|
+
if (early !== undefined)
|
|
4102
|
+
return finishResult(early, ctx, route, finalize, wrapResponse);
|
|
4103
|
+
if (index === lastBefore)
|
|
4104
|
+
return runHandler(ctx, route, finalize, wrapResponse, 0);
|
|
4105
|
+
index++;
|
|
4106
|
+
}
|
|
4107
|
+
};
|
|
4108
|
+
}
|
|
4109
|
+
let runDerive;
|
|
4110
|
+
if (deriveCount === 0) {
|
|
4111
|
+
runDerive = runBefore;
|
|
4112
|
+
}
|
|
4113
|
+
else if (deriveCount === 1) {
|
|
4114
|
+
runDerive = (ctx, route, finalize, wrapResponse) => {
|
|
3710
4115
|
let derived;
|
|
3711
4116
|
try {
|
|
3712
|
-
derived =
|
|
4117
|
+
derived = route.derives[0](ctx);
|
|
3713
4118
|
}
|
|
3714
4119
|
catch (err) {
|
|
3715
|
-
return
|
|
4120
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
3716
4121
|
}
|
|
3717
4122
|
if (derived instanceof Promise) {
|
|
3718
4123
|
return derived.then((settled) => {
|
|
3719
4124
|
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3720
|
-
return
|
|
4125
|
+
return finishResult(settled, ctx, route, finalize, wrapResponse);
|
|
3721
4126
|
}
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
4127
|
+
try {
|
|
4128
|
+
Object.assign(ctx, settled);
|
|
4129
|
+
}
|
|
4130
|
+
catch (err) {
|
|
4131
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4132
|
+
}
|
|
4133
|
+
return runBefore(ctx, route, finalize, wrapResponse, 0);
|
|
4134
|
+
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
3725
4135
|
}
|
|
3726
4136
|
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3727
|
-
return
|
|
4137
|
+
return finishResult(derived, ctx, route, finalize, wrapResponse);
|
|
3728
4138
|
}
|
|
3729
|
-
|
|
3730
|
-
|
|
4139
|
+
try {
|
|
4140
|
+
Object.assign(ctx, derived);
|
|
4141
|
+
}
|
|
4142
|
+
catch (err) {
|
|
4143
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4144
|
+
}
|
|
4145
|
+
return runBefore(ctx, route, finalize, wrapResponse, 0);
|
|
3731
4146
|
};
|
|
3732
|
-
|
|
4147
|
+
}
|
|
4148
|
+
else {
|
|
4149
|
+
runDerive = (ctx, route, finalize, wrapResponse, index) => {
|
|
4150
|
+
const lastDerive = route.derives.length - 1;
|
|
4151
|
+
for (;;) {
|
|
4152
|
+
let derived;
|
|
4153
|
+
try {
|
|
4154
|
+
derived = route.derives[index](ctx);
|
|
4155
|
+
}
|
|
4156
|
+
catch (err) {
|
|
4157
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4158
|
+
}
|
|
4159
|
+
if (derived instanceof Promise) {
|
|
4160
|
+
return derived.then((settled) => {
|
|
4161
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
4162
|
+
return finishResult(settled, ctx, route, finalize, wrapResponse);
|
|
4163
|
+
}
|
|
4164
|
+
try {
|
|
4165
|
+
Object.assign(ctx, settled);
|
|
4166
|
+
}
|
|
4167
|
+
catch (err) {
|
|
4168
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4169
|
+
}
|
|
4170
|
+
return index < lastDerive
|
|
4171
|
+
? runDerive(ctx, route, finalize, wrapResponse, index + 1)
|
|
4172
|
+
: runBefore(ctx, route, finalize, wrapResponse, 0);
|
|
4173
|
+
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
4174
|
+
}
|
|
4175
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4176
|
+
return finishResult(derived, ctx, route, finalize, wrapResponse);
|
|
4177
|
+
}
|
|
4178
|
+
try {
|
|
4179
|
+
Object.assign(ctx, derived);
|
|
4180
|
+
}
|
|
4181
|
+
catch (err) {
|
|
4182
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4183
|
+
}
|
|
4184
|
+
if (index === lastDerive)
|
|
4185
|
+
return runBefore(ctx, route, finalize, wrapResponse, 0);
|
|
4186
|
+
index++;
|
|
4187
|
+
}
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
return runDerive;
|
|
4191
|
+
}
|
|
4192
|
+
/**
|
|
4193
|
+
* The Node-direct twin of {@link buildFusedLifecycleWeb}: the same steps, finalizing through the
|
|
4194
|
+
* caller's own `finalize` (the Node outcome on the direct lane) instead of building a `Response`, so
|
|
4195
|
+
* the `!webFast` dispatcher runs these routes in one frame rather than the generic program. A
|
|
4196
|
+
* terminal `finalize` throw (an unstringifiable handler return on the Node lane) is caught as
|
|
4197
|
+
* `finishLifecycleContract` catches it; the body's 415/400 renders take `wrapResponse` directly.
|
|
4198
|
+
*/
|
|
4199
|
+
buildFusedLifecycleRunner(route, querySchema, bodySchema, maxBodyBytes) {
|
|
4200
|
+
const runDerive = sharedStages(NODE_STAGES, this, route, (d, b, a) => this.fusedNodeStages(d, b, a));
|
|
4201
|
+
const decorations = route.decorations;
|
|
4202
|
+
if (bodySchema !== undefined) {
|
|
4203
|
+
const onParsed = (parsed, ctx, finalize, wrapResponse) => {
|
|
3733
4204
|
let validation;
|
|
3734
4205
|
try {
|
|
3735
4206
|
validation = bodySchema["~standard"].validate(parsed);
|
|
3736
4207
|
}
|
|
3737
4208
|
catch (err) {
|
|
3738
|
-
return
|
|
4209
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
3739
4210
|
}
|
|
3740
4211
|
if (validation instanceof Promise) {
|
|
3741
4212
|
return validation.then((settled) => {
|
|
3742
4213
|
if (settled.issues !== undefined)
|
|
3743
|
-
return
|
|
4214
|
+
return wrapResponse(plainValidationError(settled.issues));
|
|
3744
4215
|
ctx.body = settled.value;
|
|
3745
|
-
return
|
|
3746
|
-
}, (err) =>
|
|
4216
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
4217
|
+
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
3747
4218
|
}
|
|
3748
4219
|
if (validation.issues !== undefined)
|
|
3749
|
-
return
|
|
4220
|
+
return wrapResponse(plainValidationError(validation.issues));
|
|
3750
4221
|
ctx.body = validation.value;
|
|
3751
|
-
return
|
|
4222
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
3752
4223
|
};
|
|
3753
|
-
return
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
* staged loops, per-stage dispatch, and the async-continuation machinery - while Bun rides the
|
|
3763
|
-
* single closure. That asymmetry is the realistic-route Node deficit against Fastify: this runner
|
|
3764
|
-
* closes it by running validate → derive → before → handler → (after) in one frame and finalizing
|
|
3765
|
-
* through the caller's own `finalize` (the Node outcome on the direct lane, `toResponse` on Web),
|
|
3766
|
-
* so the same registration feeds both runtimes with no per-lane copy of the lifecycle to drift.
|
|
3767
|
-
*
|
|
3768
|
-
* Semantics mirror the generic program stage-for-stage for the shapes this lane admits (exactly
|
|
3769
|
-
* one derive, one beforeHandle, zero-or-one afterHandle, no onError/around/decorations/contracts/
|
|
3770
|
-
* recovery hooks - enforced by `selectRouteLanes`): thrown `Response`/`status(...)` are control
|
|
3771
|
-
* flow through `wrapResponse`/`finalize`, anything else logs + 500s via the shared `bareError`
|
|
3772
|
-
* lane (identical to `handleLifecycleError` with no `onError` hooks), and a terminal `finalize`
|
|
3773
|
-
* throw (e.g. an unstringifiable handler return on the Node lane) is caught exactly as
|
|
3774
|
-
* `finishLifecycleContract` catches it. Step functions close over registration state only and take
|
|
3775
|
-
* the request state as arguments, so the hot path allocates no per-request closures.
|
|
3776
|
-
*/
|
|
3777
|
-
buildFusedDeriveBeforeRunner(handler, derive, before, after, querySchema, maxBodyBytes) {
|
|
3778
|
-
const logError = (err, ctx, finalize, wrapResponse) => this.bareError(err, ctx, finalize, wrapResponse);
|
|
3779
|
-
const runFinish = (result, ctx, finalize, wrapResponse) => {
|
|
3780
|
-
if (after === undefined) {
|
|
3781
|
-
try {
|
|
3782
|
-
return finalize(result, responseSet(ctx), ctx);
|
|
3783
|
-
}
|
|
3784
|
-
catch (err) {
|
|
3785
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3786
|
-
}
|
|
3787
|
-
}
|
|
3788
|
-
let transformed;
|
|
3789
|
-
try {
|
|
3790
|
-
transformed = after(result, ctx);
|
|
3791
|
-
}
|
|
3792
|
-
catch (err) {
|
|
3793
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3794
|
-
}
|
|
3795
|
-
if (transformed instanceof Promise) {
|
|
3796
|
-
return transformed.then((value) => {
|
|
3797
|
-
try {
|
|
3798
|
-
return finalize(value, responseSet(ctx), ctx);
|
|
3799
|
-
}
|
|
3800
|
-
catch (err) {
|
|
3801
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3802
|
-
}
|
|
3803
|
-
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3804
|
-
}
|
|
3805
|
-
try {
|
|
3806
|
-
return finalize(transformed, responseSet(ctx), ctx);
|
|
3807
|
-
}
|
|
3808
|
-
catch (err) {
|
|
3809
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3810
|
-
}
|
|
3811
|
-
};
|
|
3812
|
-
const finalizeEarly = (result, ctx, finalize, wrapResponse) => {
|
|
3813
|
-
try {
|
|
3814
|
-
return finalize(result, responseSet(ctx), ctx);
|
|
3815
|
-
}
|
|
3816
|
-
catch (err) {
|
|
3817
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3818
|
-
}
|
|
3819
|
-
};
|
|
3820
|
-
const runHandler = (ctx, finalize, wrapResponse) => {
|
|
3821
|
-
let result;
|
|
3822
|
-
try {
|
|
3823
|
-
result = handler(ctx);
|
|
3824
|
-
}
|
|
3825
|
-
catch (err) {
|
|
3826
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3827
|
-
}
|
|
3828
|
-
if (result instanceof Promise) {
|
|
3829
|
-
return result.then((value) => runFinish(value, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3830
|
-
}
|
|
3831
|
-
return runFinish(result, ctx, finalize, wrapResponse);
|
|
3832
|
-
};
|
|
3833
|
-
const runBefore = (ctx, finalize, wrapResponse) => {
|
|
3834
|
-
let early;
|
|
3835
|
-
try {
|
|
3836
|
-
early = before(ctx);
|
|
3837
|
-
}
|
|
3838
|
-
catch (err) {
|
|
3839
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3840
|
-
}
|
|
3841
|
-
if (early instanceof Promise) {
|
|
3842
|
-
return early.then((settled) => settled === undefined
|
|
3843
|
-
? runHandler(ctx, finalize, wrapResponse)
|
|
3844
|
-
: finalizeEarly(settled, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3845
|
-
}
|
|
3846
|
-
if (early === undefined)
|
|
3847
|
-
return runHandler(ctx, finalize, wrapResponse);
|
|
3848
|
-
return finalizeEarly(early, ctx, finalize, wrapResponse);
|
|
3849
|
-
};
|
|
3850
|
-
const runDerive = (ctx, finalize, wrapResponse) => {
|
|
3851
|
-
let derived;
|
|
3852
|
-
try {
|
|
3853
|
-
derived = derive(ctx);
|
|
3854
|
-
}
|
|
3855
|
-
catch (err) {
|
|
3856
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3857
|
-
}
|
|
3858
|
-
if (derived instanceof Promise) {
|
|
3859
|
-
return derived.then((settled) => {
|
|
3860
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3861
|
-
return finalizeEarly(settled, ctx, finalize, wrapResponse);
|
|
3862
|
-
}
|
|
3863
|
-
Object.assign(ctx, settled);
|
|
3864
|
-
return runBefore(ctx, finalize, wrapResponse);
|
|
3865
|
-
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3866
|
-
}
|
|
3867
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3868
|
-
return finalizeEarly(derived, ctx, finalize, wrapResponse);
|
|
3869
|
-
}
|
|
3870
|
-
Object.assign(ctx, derived);
|
|
3871
|
-
return runBefore(ctx, finalize, wrapResponse);
|
|
3872
|
-
};
|
|
4224
|
+
return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
|
|
4225
|
+
const ctx = nativeContext
|
|
4226
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
4227
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
4228
|
+
if (decorations !== undefined)
|
|
4229
|
+
Object.assign(ctx, decorations);
|
|
4230
|
+
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse), bodySchema, this.formOrigins);
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
3873
4233
|
const runValidated = (validation, ctx, finalize, wrapResponse) => {
|
|
3874
4234
|
if (validation instanceof Promise) {
|
|
3875
4235
|
return validation.then((settled) => {
|
|
3876
4236
|
if (settled.issues !== undefined)
|
|
3877
4237
|
return wrapResponse(plainValidationError(settled.issues));
|
|
3878
4238
|
ctx.query = settled.value;
|
|
3879
|
-
return runDerive(ctx, finalize, wrapResponse);
|
|
3880
|
-
}, (err) =>
|
|
4239
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
4240
|
+
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
3881
4241
|
}
|
|
3882
4242
|
if (validation.issues !== undefined)
|
|
3883
4243
|
return wrapResponse(plainValidationError(validation.issues));
|
|
3884
4244
|
ctx.query = validation.value;
|
|
3885
|
-
return runDerive(ctx, finalize, wrapResponse);
|
|
4245
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
3886
4246
|
};
|
|
3887
4247
|
return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
|
|
3888
4248
|
const ctx = nativeContext
|
|
3889
4249
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3890
4250
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
4251
|
+
if (decorations !== undefined)
|
|
4252
|
+
Object.assign(ctx, decorations);
|
|
3891
4253
|
if (querySchema === undefined)
|
|
3892
|
-
return runDerive(ctx, finalize, wrapResponse);
|
|
4254
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
3893
4255
|
let validation;
|
|
3894
4256
|
try {
|
|
3895
4257
|
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
3896
4258
|
}
|
|
3897
4259
|
catch (err) {
|
|
3898
|
-
return
|
|
4260
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
3899
4261
|
}
|
|
3900
4262
|
return runValidated(validation, ctx, finalize, wrapResponse);
|
|
3901
4263
|
};
|
|
3902
4264
|
}
|
|
3903
|
-
/**
|
|
3904
|
-
*
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
|
-
let transformed;
|
|
3924
|
-
try {
|
|
3925
|
-
transformed = after(result, ctx);
|
|
3926
|
-
}
|
|
3927
|
-
catch (err) {
|
|
3928
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3929
|
-
}
|
|
3930
|
-
if (transformed instanceof Promise) {
|
|
3931
|
-
return transformed.then((value) => {
|
|
3932
|
-
try {
|
|
3933
|
-
return finalize(value, responseSet(ctx), ctx);
|
|
3934
|
-
}
|
|
3935
|
-
catch (err) {
|
|
3936
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3937
|
-
}
|
|
3938
|
-
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3939
|
-
}
|
|
3940
|
-
try {
|
|
3941
|
-
return finalize(transformed, responseSet(ctx), ctx);
|
|
3942
|
-
}
|
|
3943
|
-
catch (err) {
|
|
3944
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3945
|
-
}
|
|
3946
|
-
};
|
|
3947
|
-
const finalizeEarly = (result, ctx, finalize, wrapResponse) => {
|
|
3948
|
-
try {
|
|
3949
|
-
return finalize(result, responseSet(ctx), ctx);
|
|
3950
|
-
}
|
|
3951
|
-
catch (err) {
|
|
3952
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3953
|
-
}
|
|
3954
|
-
};
|
|
3955
|
-
const runHandler = (ctx, finalize, wrapResponse) => {
|
|
3956
|
-
let result;
|
|
3957
|
-
try {
|
|
3958
|
-
result = handler(ctx);
|
|
3959
|
-
}
|
|
3960
|
-
catch (err) {
|
|
3961
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3962
|
-
}
|
|
3963
|
-
if (result instanceof Promise) {
|
|
3964
|
-
return result.then((value) => runFinish(value, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3965
|
-
}
|
|
3966
|
-
return runFinish(result, ctx, finalize, wrapResponse);
|
|
3967
|
-
};
|
|
3968
|
-
const runBefore = (ctx, finalize, wrapResponse) => {
|
|
3969
|
-
let early;
|
|
3970
|
-
try {
|
|
3971
|
-
early = before(ctx);
|
|
3972
|
-
}
|
|
3973
|
-
catch (err) {
|
|
3974
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3975
|
-
}
|
|
3976
|
-
if (early instanceof Promise) {
|
|
3977
|
-
return early.then((settled) => settled === undefined
|
|
3978
|
-
? runHandler(ctx, finalize, wrapResponse)
|
|
3979
|
-
: finalizeEarly(settled, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3980
|
-
}
|
|
3981
|
-
if (early === undefined)
|
|
3982
|
-
return runHandler(ctx, finalize, wrapResponse);
|
|
3983
|
-
return finalizeEarly(early, ctx, finalize, wrapResponse);
|
|
3984
|
-
};
|
|
3985
|
-
const runDerive = (ctx, finalize, wrapResponse) => {
|
|
3986
|
-
let derived;
|
|
3987
|
-
try {
|
|
3988
|
-
derived = derive(ctx);
|
|
3989
|
-
}
|
|
3990
|
-
catch (err) {
|
|
3991
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3992
|
-
}
|
|
3993
|
-
if (derived instanceof Promise) {
|
|
3994
|
-
return derived.then((settled) => {
|
|
3995
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3996
|
-
return finalizeEarly(settled, ctx, finalize, wrapResponse);
|
|
3997
|
-
}
|
|
3998
|
-
Object.assign(ctx, settled);
|
|
3999
|
-
return runBefore(ctx, finalize, wrapResponse);
|
|
4000
|
-
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
4001
|
-
}
|
|
4002
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4003
|
-
return finalizeEarly(derived, ctx, finalize, wrapResponse);
|
|
4004
|
-
}
|
|
4005
|
-
Object.assign(ctx, derived);
|
|
4006
|
-
return runBefore(ctx, finalize, wrapResponse);
|
|
4007
|
-
};
|
|
4008
|
-
const onParsed = (parsed, ctx, finalize, wrapResponse) => {
|
|
4009
|
-
let validation;
|
|
4265
|
+
/** The thrown-value render of the Web fused lifecycle lanes for a route with no error hooks: a
|
|
4266
|
+
* `Response` is control flow, anything else is logged and answered with a plain 500. */
|
|
4267
|
+
fusedWebFail(err, ctx) {
|
|
4268
|
+
return this.bareError(err, ctx, this.finalizeResponse, this.wrapWebResponse);
|
|
4269
|
+
}
|
|
4270
|
+
/** {@link fusedWebFail} for a fused route that may have error hooks. */
|
|
4271
|
+
fusedWebRouteFail(err, ctx, route) {
|
|
4272
|
+
return this.fusedFail(err, ctx, route, this.finalizeResponse, this.wrapWebResponse);
|
|
4273
|
+
}
|
|
4274
|
+
/** The thrown-value render of a fused lifecycle route. A `Response` or `status(...)` is control flow
|
|
4275
|
+
* that no error hook sees, unless it cannot render, which is a fault like any other. Anything else
|
|
4276
|
+
* goes to the route's error hooks; with none it is logged and answered with a plain 500. */
|
|
4277
|
+
fusedFail(err, ctx, route, finalize, wrapResponse) {
|
|
4278
|
+
if (route.errors.length === 0)
|
|
4279
|
+
return this.bareError(err, ctx, finalize, wrapResponse);
|
|
4280
|
+
let fault = err;
|
|
4281
|
+
if (err instanceof Response || isResponseResult(err)) {
|
|
4010
4282
|
try {
|
|
4011
|
-
|
|
4012
|
-
}
|
|
4013
|
-
catch (err) {
|
|
4014
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
4283
|
+
return finalize(err, responseSet(ctx), ctx);
|
|
4015
4284
|
}
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
if (settled.issues !== undefined)
|
|
4019
|
-
return wrapResponse(plainValidationError(settled.issues));
|
|
4020
|
-
ctx.body = settled.value;
|
|
4021
|
-
return runDerive(ctx, finalize, wrapResponse);
|
|
4022
|
-
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
4285
|
+
catch (renderError) {
|
|
4286
|
+
fault = renderError;
|
|
4023
4287
|
}
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
return
|
|
4034
|
-
}
|
|
4288
|
+
}
|
|
4289
|
+
return this.runErrorHooks(route.errors, undefined, fault, ctx, (result, set) => finalize(result, set, ctx), wrapResponse);
|
|
4290
|
+
}
|
|
4291
|
+
/** {@link respondFused} for a fused lifecycle route, whose failures reach its error hooks. */
|
|
4292
|
+
respondHooks(result, ctx, route, unstamped) {
|
|
4293
|
+
try {
|
|
4294
|
+
return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, unstamped);
|
|
4295
|
+
}
|
|
4296
|
+
catch (err) {
|
|
4297
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
4298
|
+
}
|
|
4035
4299
|
}
|
|
4036
4300
|
bareError(err, ctx, finalize, wrapResponse) {
|
|
4037
4301
|
// The thrown-value contract lives in `bare-error-lane.ts`, one source of truth for both this
|
|
@@ -4557,11 +4821,12 @@ export class Server {
|
|
|
4557
4821
|
}
|
|
4558
4822
|
/** Adapt one route's compiled execution plan to Bun's already-matched request shape. Route
|
|
4559
4823
|
* semantics remain in the plan; this closure only supplies native params and deadline fallback. */
|
|
4560
|
-
compileBunNativeHandler(entry, paramNames, fused, signal, budget, peer, fallback) {
|
|
4824
|
+
compileBunNativeHandler(entry, method, paramNames, fused, signal, budget, peer, fallback, hookedPlatform) {
|
|
4561
4825
|
// This callback is reached only from Bun's compiled native route table. Bun has already parsed
|
|
4562
4826
|
// the HTTP framing, so the JSON lane may retain its native fused `json()` parse without weakening
|
|
4563
|
-
// the portable/adapted source contract (those sources are never marked).
|
|
4564
|
-
|
|
4827
|
+
// the portable/adapted source contract (those sources are never marked). Bun hands a GET or
|
|
4828
|
+
// HEAD slot no body, so it skips the symbol write on every request.
|
|
4829
|
+
const markFramed = method === "GET" || method === "HEAD" ? () => undefined : markTrustedBodyFraming;
|
|
4565
4830
|
// The direct native lanes bypass `runMatched`, so the route's transport byte cap must be
|
|
4566
4831
|
// marked here too - otherwise a Bun `listen()` fused route would leave direct `c.req` body
|
|
4567
4832
|
// reads uncapped. The fallback branches go through `fetchMatched` -> `runMatched`, which marks.
|
|
@@ -4575,7 +4840,9 @@ export class Server {
|
|
|
4575
4840
|
(signal && budget
|
|
4576
4841
|
? (source, params, search, activeSignal, activeBudget, platform, nativeContext) => entry.execution.run(this, entry, source, params, search, activeSignal, activeBudget, platform, nativeContext, this.finalizeResponse, this.wrapWebResponse)
|
|
4577
4842
|
: undefined);
|
|
4578
|
-
|
|
4843
|
+
// The cap is never marked on a GET or HEAD, the only methods a GET or HEAD slot is handed, so those
|
|
4844
|
+
// slots skip the wrapper and its read of the method on every request.
|
|
4845
|
+
const capped = inner === undefined || bodyLimit === undefined || method === "GET" || method === "HEAD"
|
|
4579
4846
|
? inner
|
|
4580
4847
|
: (source, params, search, fusedSignal, fusedBudget, platform, nativeContext) => {
|
|
4581
4848
|
const method = source.method;
|
|
@@ -4603,6 +4870,12 @@ export class Server {
|
|
|
4603
4870
|
: finishNative(request, outcome);
|
|
4604
4871
|
};
|
|
4605
4872
|
if (paramNames.length === 0) {
|
|
4873
|
+
if (hookedPlatform !== undefined) {
|
|
4874
|
+
return (request, server) => {
|
|
4875
|
+
markFramed(request);
|
|
4876
|
+
return this.fetchMatchedHooked(request, entry, EMPTY_PARAMS, hookedPlatform(request, server));
|
|
4877
|
+
};
|
|
4878
|
+
}
|
|
4606
4879
|
if (capped === undefined) {
|
|
4607
4880
|
return (request) => {
|
|
4608
4881
|
markFramed(request);
|
|
@@ -4625,6 +4898,16 @@ export class Server {
|
|
|
4625
4898
|
const malformed = paramNames.length === 1
|
|
4626
4899
|
? (params) => unroutedParam(params[paramNames[0]])
|
|
4627
4900
|
: hasUnroutedParam;
|
|
4901
|
+
if (hookedPlatform !== undefined) {
|
|
4902
|
+
return (request, server) => {
|
|
4903
|
+
markFramed(request);
|
|
4904
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Bun's route table hands each handler a Request carrying the `params` it matched.
|
|
4905
|
+
const params = request.params ?? EMPTY_PARAMS;
|
|
4906
|
+
if (malformed(params))
|
|
4907
|
+
return fallback(request, server);
|
|
4908
|
+
return this.fetchMatchedHooked(request, entry, params, hookedPlatform(request, server));
|
|
4909
|
+
};
|
|
4910
|
+
}
|
|
4628
4911
|
if (capped === undefined) {
|
|
4629
4912
|
return (request, server) => {
|
|
4630
4913
|
markFramed(request);
|
|
@@ -4653,26 +4936,25 @@ export class Server {
|
|
|
4653
4936
|
return runNative(request, params);
|
|
4654
4937
|
};
|
|
4655
4938
|
}
|
|
4656
|
-
/** Compile portable route registrations into Bun's native route table.
|
|
4657
|
-
*
|
|
4658
|
-
* route
|
|
4939
|
+
/** Compile portable route registrations into Bun's native route table. Paths a composed mount or
|
|
4940
|
+
* a WebSocket handshake may reach stay with the portable dispatcher; provably disjoint HTTP routes
|
|
4941
|
+
* keep native matching. Request hooks run before the matched route and rewrites return to dispatch.
|
|
4659
4942
|
*
|
|
4660
4943
|
* Bun and the portable router agree on which of two static-and-`:param` paths wins a request, so
|
|
4661
4944
|
* the table is kept to those paths. Where Bun would otherwise pass a request on to a less specific
|
|
4662
4945
|
* path, the more specific one carries `fallback`, the portable dispatcher, under that method.
|
|
4663
4946
|
* `peer` is the platform every request served from the table shares. */
|
|
4664
|
-
buildBunNativeRoutes(fallback, peer) {
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
this.
|
|
4670
|
-
this.preRouteMountCount > 0 ||
|
|
4671
|
-
this.clientIpTrust !== undefined) {
|
|
4947
|
+
buildBunNativeRoutes(fallback, peer, platformFor) {
|
|
4948
|
+
const hooked = this.onRequestHooks.length > 0 && !this.bunNativeRequestHooksSafe;
|
|
4949
|
+
if ((hooked && platformFor === undefined) ||
|
|
4950
|
+
// A hook may rewrite a handshake onto a socket path, which only the dispatcher's upgrade check follows.
|
|
4951
|
+
(hooked && this.wsRoutes.length > 0) ||
|
|
4952
|
+
this.mountsTakeUpgrades()) {
|
|
4672
4953
|
return undefined;
|
|
4673
4954
|
}
|
|
4674
4955
|
const routes = Object.create(null);
|
|
4675
|
-
const mayUseFusedNative =
|
|
4956
|
+
const mayUseFusedNative = !hooked &&
|
|
4957
|
+
this.requestTimeoutMs === 0 &&
|
|
4676
4958
|
(this.onResponseHooks.length === 0 || this.bunNativeResponseHeadersOnly) &&
|
|
4677
4959
|
this.onResponseFinalizedHooks.length === 0 &&
|
|
4678
4960
|
// The capacity gate must wrap every request; the fused lane bypasses fetchMatched, so enabling
|
|
@@ -4682,28 +4964,49 @@ export class Server {
|
|
|
4682
4964
|
const unboundedBudget = mayUseFusedNative ? getUnboundedRequestBudget() : undefined;
|
|
4683
4965
|
let count = 0;
|
|
4684
4966
|
const all = this.catalog.entries();
|
|
4967
|
+
const preRouteMounts = this.fetchMounts.filter((mount) => mount.beforeRoutes);
|
|
4685
4968
|
// A path with a wildcard or a part-literal segment cannot go in the table: Bun reads `/:name.json`
|
|
4686
4969
|
// as one parameter named `name.json`. These are filed under each run of static segments they
|
|
4687
4970
|
// start with, which is where a table path they could outrank looks for them.
|
|
4688
4971
|
const under = new Map();
|
|
4972
|
+
const fileUnder = (segments) => {
|
|
4973
|
+
let prefix = "";
|
|
4974
|
+
for (const segment of segments) {
|
|
4975
|
+
let bucket = under.get(prefix);
|
|
4976
|
+
if (bucket === undefined) {
|
|
4977
|
+
bucket = [];
|
|
4978
|
+
under.set(prefix, bucket);
|
|
4979
|
+
}
|
|
4980
|
+
bucket.push(segments);
|
|
4981
|
+
if (segment.kind !== "static")
|
|
4982
|
+
break;
|
|
4983
|
+
prefix += `/${segment.value}`;
|
|
4984
|
+
}
|
|
4985
|
+
};
|
|
4986
|
+
// A handshake is a GET the portable dispatcher gives to a socket route before any HTTP route, so a
|
|
4987
|
+
// socket path is outside the table like a wildcard path, and a GET route a request may share with
|
|
4988
|
+
// one stays with the dispatcher.
|
|
4989
|
+
const sockets = this.wsRoutes.map((route) => compileRoutePattern(route.path).segments);
|
|
4990
|
+
for (const segments of sockets)
|
|
4991
|
+
fileUnder(segments);
|
|
4689
4992
|
const lead = all.map((route) => {
|
|
4690
4993
|
const segments = route.pattern.segments;
|
|
4691
4994
|
const plain = segments.every((segment) => segment.kind === "static" || segment.kind === "param");
|
|
4995
|
+
if (!plain)
|
|
4996
|
+
fileUnder(segments);
|
|
4692
4997
|
let prefix = "";
|
|
4693
4998
|
for (const segment of segments) {
|
|
4694
|
-
if (!plain) {
|
|
4695
|
-
let bucket = under.get(prefix);
|
|
4696
|
-
if (bucket === undefined) {
|
|
4697
|
-
bucket = [];
|
|
4698
|
-
under.set(prefix, bucket);
|
|
4699
|
-
}
|
|
4700
|
-
bucket.push(route);
|
|
4701
|
-
}
|
|
4702
4999
|
if (segment.kind !== "static")
|
|
4703
5000
|
break;
|
|
4704
5001
|
prefix += `/${segment.value}`;
|
|
4705
5002
|
}
|
|
4706
|
-
|
|
5003
|
+
// Mounts take every method under their literal prefix before route matching. A parameter may
|
|
5004
|
+
// fill the rest of that prefix, but a static route ending above the mount cannot reach it.
|
|
5005
|
+
// Conservative by design: only the leading static run proves a dynamic route disjoint.
|
|
5006
|
+
const mayHitMount = preRouteMounts.some((mount) => underMountPrefix(prefix, mount.path) ||
|
|
5007
|
+
(route.pattern.paramNames.length > 0 &&
|
|
5008
|
+
(prefix === "" || underMountPrefix(mount.path, prefix))));
|
|
5009
|
+
return plain && !mayHitMount ? prefix : undefined;
|
|
4707
5010
|
});
|
|
4708
5011
|
// A preflight hook is intentionally handled by the fallback fetch path. Keeping OPTIONS out
|
|
4709
5012
|
// of the native lane means Bun dispatches it through CORS's onRequest hook even when the
|
|
@@ -4729,17 +5032,20 @@ export class Server {
|
|
|
4729
5032
|
entry.idempotent !== undefined ||
|
|
4730
5033
|
!BUN_TABLE_METHODS.includes(method) ||
|
|
4731
5034
|
(preflightOnFallback && method === "OPTIONS") ||
|
|
4732
|
-
under.get(run)?.some((other) => outranks(other
|
|
5035
|
+
under.get(run)?.some((other) => outranks(other, pattern.segments)) ||
|
|
5036
|
+
(method === "GET" && sockets.some((socket) => mayShareRequest(socket, pattern.segments)))) {
|
|
4733
5037
|
continue;
|
|
4734
5038
|
}
|
|
4735
5039
|
const paramNames = pattern.paramNames;
|
|
4736
5040
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined;
|
|
4737
|
-
const handler = this.compileBunNativeHandler(entry, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback);
|
|
5041
|
+
const handler = this.compileBunNativeHandler(entry, method, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback, hooked ? platformFor : undefined);
|
|
4738
5042
|
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it. Left
|
|
4739
5043
|
// off, `validateHost` costs nothing here: the table holds no wildcard, a `..` parameter falls
|
|
4740
5044
|
// back, and reading the Host on every request measured about 6% of a bare route.
|
|
4741
5045
|
tableAt(path, pattern.segments)[method] = this.validateHost
|
|
4742
|
-
? (request, server) => requestTarget(request, true
|
|
5046
|
+
? (request, server) => requestTarget(request, true, this.normalizeTarget)
|
|
5047
|
+
? fallback(request, server)
|
|
5048
|
+
: handler(request, server)
|
|
4743
5049
|
: handler;
|
|
4744
5050
|
count += 1;
|
|
4745
5051
|
if (paramNames.length > 0) {
|
|
@@ -4794,11 +5100,17 @@ export class Server {
|
|
|
4794
5100
|
* nothing - so any endpoint slower than 10s (a render, an export, a long upstream call) has its
|
|
4795
5101
|
* connection cut mid-flight regardless of `requestTimeoutMs`. Apps with such endpoints must raise
|
|
4796
5102
|
* this above their slowest expected response, which is why it is a first-class option and not a
|
|
4797
|
-
* reason to drop down to `Bun.serve
|
|
5103
|
+
* reason to drop down to `Bun.serve`, or hold those requests' connections alone with
|
|
5104
|
+
* `withIdleTimeout()` from `@nifrajs/core/idle-timeout`. `0` disables the timeout entirely; max 255.
|
|
4798
5105
|
*
|
|
4799
5106
|
* `tls` serves HTTPS directly: `app.listen(443, { tls: { cert, key } })` with PEM text or bytes.
|
|
4800
5107
|
* Requests then arrive with `https:` URLs. Behind a proxy or a platform that terminates TLS, leave
|
|
4801
5108
|
* it unset.
|
|
5109
|
+
*
|
|
5110
|
+
* `static` sees every request before the app does, the ones Bun's route table serves included, so
|
|
5111
|
+
* a file it answers wins over any route at the same path. It returns the file's response, or
|
|
5112
|
+
* `undefined` to let the app route the request. No hook of the app runs for a request it answers.
|
|
5113
|
+
* `@nifrajs/web`'s Bun build serves its client bundle and public files this way.
|
|
4802
5114
|
*/
|
|
4803
5115
|
listen(port, options) {
|
|
4804
5116
|
if (typeof Bun === "undefined") {
|
|
@@ -4851,6 +5163,10 @@ export class Server {
|
|
|
4851
5163
|
return jsonError(500, "internal_error");
|
|
4852
5164
|
},
|
|
4853
5165
|
};
|
|
5166
|
+
const idleTimeoutSec = options?.idleTimeoutSec ?? BUN_IDLE_TIMEOUT_SEC;
|
|
5167
|
+
const setIdleTimeout = (request, seconds) =>
|
|
5168
|
+
// biome-ignore lint/plugin/noChainedTypeAssertions lint/plugin/requireSafetyCommentForTypeAssertion: `running` is the Bun `Server` this call started (`RunningServer` publishes a subset of it), and every source a Bun listener routes is a `Request`.
|
|
5169
|
+
running.timeout(request, seconds ?? idleTimeoutSec);
|
|
4854
5170
|
const fallback = (req, server) => {
|
|
4855
5171
|
// Bun has already framed and bounded this request body in its HTTP parser. Mark the
|
|
4856
5172
|
// source before the portable fallback runs so body schemas use Bun's native `json()`
|
|
@@ -4858,25 +5174,59 @@ export class Server {
|
|
|
4858
5174
|
// Requests. Native route handlers mark themselves in compileBunNativeHandler; this
|
|
4859
5175
|
// covers every request the native lane does not serve.
|
|
4860
5176
|
markTrustedBodyFraming(req);
|
|
4861
|
-
return this.fetch(req,
|
|
5177
|
+
return this.fetch(req, new BunPeerPlatform(server, req, setIdleTimeout));
|
|
4862
5178
|
};
|
|
4863
5179
|
// Bun names the socket peer of a request when its server is asked. A request served from the
|
|
4864
5180
|
// native table carries this one platform, which asks only when a handler reads `c.clientIp`.
|
|
5181
|
+
const socketPeer = (request) => running.requestIP(request)?.address;
|
|
5182
|
+
// A `clientIp` trust declaration resolves here too, from the request's headers, when a handler reads
|
|
5183
|
+
// the address; only a declaration of zero hops names the socket peer.
|
|
5184
|
+
const trust = this.clientIpTrust;
|
|
4865
5185
|
const peer = {
|
|
4866
|
-
[PLATFORM_PEER]:
|
|
5186
|
+
[PLATFORM_PEER]: trust === undefined
|
|
5187
|
+
? socketPeer
|
|
5188
|
+
: (request) => resolveClientIp(!("header" in trust) && trust.trustedHops === 0 ? socketPeer(request) : undefined,
|
|
5189
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: every source a Bun listener routes is a `Request`.
|
|
5190
|
+
request, trust),
|
|
5191
|
+
[PLATFORM_IDLE_TIMEOUT]: setIdleTimeout,
|
|
4867
5192
|
};
|
|
4868
|
-
const nativeRoutes =
|
|
5193
|
+
const nativeRoutes = this.buildBunNativeRoutes(fallback, peer, (request, server) =>
|
|
5194
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: `fallback` hands `fetch` the same platform, which holds no environment.
|
|
5195
|
+
new BunPeerPlatform(server, request, setIdleTimeout));
|
|
5196
|
+
const first = options?.static;
|
|
5197
|
+
const before = (inner) => first === undefined
|
|
5198
|
+
? inner
|
|
5199
|
+
: (req, server) => {
|
|
5200
|
+
const answer = first(req);
|
|
5201
|
+
if (answer === undefined)
|
|
5202
|
+
return inner(req, server);
|
|
5203
|
+
return answer instanceof Promise
|
|
5204
|
+
? answer.then((response) => response ?? inner(req, server))
|
|
5205
|
+
: answer;
|
|
5206
|
+
};
|
|
5207
|
+
// Bun runs a table handler without `fetch`, so each one gets the same front as the fallback.
|
|
5208
|
+
if (first !== undefined && nativeRoutes !== undefined) {
|
|
5209
|
+
for (const path in nativeRoutes) {
|
|
5210
|
+
const methods = nativeRoutes[path];
|
|
5211
|
+
for (const method of BUN_TABLE_METHODS) {
|
|
5212
|
+
const handler = methods[method];
|
|
5213
|
+
if (handler !== undefined)
|
|
5214
|
+
methods[method] = before(handler);
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
4869
5218
|
const sockets = new Set();
|
|
4870
5219
|
this.bunSockets = sockets;
|
|
4871
5220
|
const running = (wsHandlers === undefined
|
|
4872
5221
|
? Bun.serve({
|
|
4873
5222
|
...listening,
|
|
4874
5223
|
...(nativeRoutes === undefined ? {} : { routes: nativeRoutes }),
|
|
4875
|
-
fetch: fallback,
|
|
5224
|
+
fetch: before(fallback),
|
|
4876
5225
|
})
|
|
4877
5226
|
: Bun.serve({
|
|
4878
5227
|
...listening,
|
|
4879
|
-
|
|
5228
|
+
...(nativeRoutes === undefined ? {} : { routes: nativeRoutes }),
|
|
5229
|
+
fetch: before((req, server) => this.bunFetchWithWebSocket(req, server, setIdleTimeout)),
|
|
4880
5230
|
// Bun's `ServerWebSocket<BunWsData>` is runtime-compatible with the handlers' structural
|
|
4881
5231
|
// `BunSocket` view (kept local so `Bun.*` types never leak into the published .d.ts); the
|
|
4882
5232
|
// `unknown` params bridge a TS structural-variance quirk. Round-trip covered by websocket.test.ts.
|