@nifrajs/core 5.5.1 → 5.7.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 +246 -0
- package/README.md +5 -0
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js.map +1 -1
- package/dist/binary.d.ts +15 -1
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -0
- package/dist/binary.js.map +1 -1
- package/dist/edge-kit.d.ts +1 -0
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +1 -0
- package/dist/edge-kit.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +4 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +4 -0
- package/dist/evidence.js.map +1 -1
- package/dist/file-type.d.ts +30 -0
- package/dist/file-type.d.ts.map +1 -0
- package/dist/file-type.js +118 -0
- package/dist/file-type.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/hook-origins.d.ts +2 -0
- package/dist/internal/hook-origins.d.ts.map +1 -1
- package/dist/internal/hook-origins.js +12 -0
- package/dist/internal/hook-origins.js.map +1 -1
- package/dist/internal/route-program.d.ts +11 -0
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +133 -85
- package/dist/internal/route-program.js.map +1 -1
- package/dist/reflection.d.ts +19 -1
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +46 -7
- package/dist/reflection.js.map +1 -1
- package/dist/response-observer.d.ts +1 -0
- package/dist/response-observer.d.ts.map +1 -1
- package/dist/response-observer.js +1 -0
- package/dist/response-observer.js.map +1 -1
- package/dist/server/answer-step.d.ts +27 -0
- package/dist/server/answer-step.d.ts.map +1 -0
- package/dist/server/answer-step.js +33 -0
- package/dist/server/answer-step.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +4 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +9 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/base64-answers.d.ts +5 -0
- package/dist/server/base64-answers.d.ts.map +1 -0
- package/dist/server/base64-answers.js +132 -0
- package/dist/server/base64-answers.js.map +1 -0
- package/dist/server/before-handler.d.ts +10 -0
- package/dist/server/before-handler.d.ts.map +1 -0
- package/dist/server/before-handler.js +12 -0
- package/dist/server/before-handler.js.map +1 -0
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +8 -2
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body-parser.d.ts.map +1 -1
- package/dist/server/body-parser.js +2 -5
- package/dist/server/body-parser.js.map +1 -1
- package/dist/server/body.d.ts +24 -8
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +86 -33
- package/dist/server/body.js.map +1 -1
- package/dist/server/core-copies.d.ts.map +1 -1
- package/dist/server/core-copies.js +5 -2
- package/dist/server/core-copies.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +6 -17
- package/dist/server/http.js.map +1 -1
- package/dist/server/install.d.ts +2 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +2 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/json-response.d.ts +3 -0
- package/dist/server/json-response.d.ts.map +1 -0
- package/dist/server/json-response.js +14 -0
- package/dist/server/json-response.js.map +1 -0
- package/dist/server/json-text.d.ts +16 -0
- package/dist/server/json-text.d.ts.map +1 -0
- package/dist/server/json-text.js +34 -0
- package/dist/server/json-text.js.map +1 -0
- package/dist/server/multipart.d.ts +3 -0
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/multipart.js +226 -97
- package/dist/server/multipart.js.map +1 -1
- package/dist/server/node-outcome.d.ts +5 -0
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +3 -4
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/proto-guard.d.ts.map +1 -1
- package/dist/server/proto-guard.js +39 -4
- package/dist/server/proto-guard.js.map +1 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -14
- package/dist/server/query.js.map +1 -1
- package/dist/server/repeated-prefix.d.ts +17 -0
- package/dist/server/repeated-prefix.d.ts.map +1 -0
- package/dist/server/repeated-prefix.js +17 -0
- package/dist/server/repeated-prefix.js.map +1 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +5 -5
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +3 -4
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +45 -37
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +12 -2
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +137 -9
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/route-template.d.ts +10 -0
- package/dist/server/route-template.d.ts.map +1 -0
- package/dist/server/route-template.js +21 -0
- package/dist/server/route-template.js.map +1 -0
- package/dist/server/runtime-core.d.ts +7 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +10 -2
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +17 -8
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +27 -12
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +326 -108
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +1 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +7 -5
- package/dist/server/sse.js.map +1 -1
- package/dist/server/text-slots.d.ts +8 -0
- package/dist/server/text-slots.d.ts.map +1 -0
- package/dist/server/text-slots.js +19 -0
- package/dist/server/text-slots.js.map +1 -0
- package/dist/server/webhook.d.ts +1 -1
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +24 -19
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +18 -3
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +45 -5
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +7 -4
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts +5 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +26 -17
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-cli.d.ts +64 -0
- package/dist/tool-cli.d.ts.map +1 -0
- package/dist/tool-cli.js +495 -0
- package/dist/tool-cli.js.map +1 -0
- package/dist/transport-codec.d.ts.map +1 -1
- package/dist/transport-codec.js +2 -1
- package/dist/transport-codec.js.map +1 -1
- package/package.json +11 -1
- package/src/assurance.ts +1 -0
- package/src/binary.ts +17 -0
- package/src/edge-kit.ts +1 -0
- package/src/errors.ts +1 -0
- package/src/evidence.ts +8 -0
- package/src/file-type.ts +130 -0
- package/src/index.ts +3 -1
- package/src/internal/hook-origins.ts +10 -0
- package/src/internal/route-program.ts +261 -93
- package/src/reflection.ts +71 -7
- package/src/response-observer.ts +6 -0
- package/src/server/answer-step.ts +68 -0
- package/src/server/bare-error-lane.ts +9 -4
- package/src/server/base64-answers.ts +138 -0
- package/src/server/before-handler.ts +14 -0
- package/src/server/body-lane.ts +19 -9
- package/src/server/body-parser.ts +1 -5
- package/src/server/body.ts +96 -36
- package/src/server/core-copies.ts +5 -2
- package/src/server/http.ts +6 -15
- package/src/server/install.ts +3 -0
- package/src/server/json-response.ts +13 -0
- package/src/server/json-text.ts +34 -0
- package/src/server/multipart.ts +232 -100
- package/src/server/node-outcome.ts +4 -5
- package/src/server/proto-guard.ts +39 -4
- package/src/server/query.ts +7 -13
- package/src/server/repeated-prefix.ts +25 -0
- package/src/server/request-context.ts +5 -5
- package/src/server/respond.ts +47 -30
- package/src/server/response-contract-lane.ts +132 -9
- package/src/server/route-template.ts +24 -0
- package/src/server/runtime-core.ts +12 -2
- package/src/server/server-types.ts +18 -6
- package/src/server/server.ts +361 -112
- package/src/server/sse.ts +8 -6
- package/src/server/text-slots.ts +21 -0
- package/src/server/webhook.ts +29 -24
- package/src/server/websocket.ts +57 -8
- package/src/server/ws-bun.ts +7 -3
- package/src/server.ts +2 -0
- package/src/single-copy.ts +31 -15
- package/src/tool-cli.ts +584 -0
- package/src/transport-codec.ts +3 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** What {@link markRepeatedPrefix} recorded for a chunk. */
|
|
2
|
+
export interface RepeatedPrefix {
|
|
3
|
+
/** Bytes the producer sends again in later responses and never changes. */
|
|
4
|
+
readonly source: Uint8Array
|
|
5
|
+
/** How many of `source`'s leading bytes the chunk starts with. */
|
|
6
|
+
readonly length: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const prefixes = new WeakMap<Uint8Array, RepeatedPrefix>()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Record that `chunk` starts with the first `length` bytes of `source`: bytes its producer sends again
|
|
13
|
+
* in later responses and never changes, such as a cached page shell. A response observer that
|
|
14
|
+
* transforms streamed bodies may keep its work on those bytes, keyed by `source`, and reuse it. It must
|
|
15
|
+
* check that the chunk still holds them, since a later reader may have written into the chunk.
|
|
16
|
+
*/
|
|
17
|
+
export function markRepeatedPrefix(chunk: Uint8Array, source: Uint8Array, length: number): void {
|
|
18
|
+
if (!Number.isSafeInteger(length) || length <= 0) return
|
|
19
|
+
if (length > chunk.byteLength || length > source.byteLength) return
|
|
20
|
+
prefixes.set(chunk, { source, length })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The repeated prefix {@link markRepeatedPrefix} recorded for `chunk`, if any. */
|
|
24
|
+
export const repeatedPrefixOf = (chunk: Uint8Array): RepeatedPrefix | undefined =>
|
|
25
|
+
prefixes.get(chunk)
|
|
@@ -12,6 +12,8 @@ import type { Platform } from "./context.ts"
|
|
|
12
12
|
import { type CookieOptions, cookieNamePrefix, parseCookies, serializeCookie } from "./cookies.ts"
|
|
13
13
|
import { headerObjectOf } from "./headers.ts"
|
|
14
14
|
import { plainError } from "./http.ts"
|
|
15
|
+
import { jsonResponse } from "./json-response.ts"
|
|
16
|
+
import { jsonText } from "./json-text.ts"
|
|
15
17
|
import type { ProtoPoisoning } from "./proto-guard.ts"
|
|
16
18
|
import { searchOf } from "./query.ts"
|
|
17
19
|
import { residentText, responseJsonContentType } from "./respond.ts"
|
|
@@ -342,14 +344,14 @@ export class RequestContext implements RawContext {
|
|
|
342
344
|
// (`undefined`, a function, a symbol) - exactly the case where `Response.json` throws, so it
|
|
343
345
|
// gets the throw rather than an invented body. The content-type is the runtime's own, read
|
|
344
346
|
// off `Response.json` once, so a deferred response carries the same one byte for byte.
|
|
345
|
-
const text = defer === undefined ? undefined : (
|
|
347
|
+
const text = defer === undefined ? undefined : (jsonText(body) as string | undefined)
|
|
346
348
|
const headers =
|
|
347
349
|
text === undefined ? undefined : ownHeaderRecord(i?.headers, responseJsonContentType())
|
|
348
350
|
if (defer !== undefined && text !== undefined && headers !== undefined) {
|
|
349
351
|
reply = defer(text, i?.status ?? 200, headers)
|
|
350
352
|
}
|
|
351
353
|
}
|
|
352
|
-
reply ??= markResidentBody(
|
|
354
|
+
reply ??= markResidentBody(jsonResponse(body, i))
|
|
353
355
|
// Remembered so a response contract holds the value to the route's schema: this is the
|
|
354
356
|
// framework's JSON helper, not a raw-Response escape hatch.
|
|
355
357
|
this.jsonBody = body
|
|
@@ -433,9 +435,7 @@ async function readBoundedBodyOrThrow(
|
|
|
433
435
|
): Promise<Uint8Array> {
|
|
434
436
|
const r = await readBoundedBytes(req, maxBytes ?? maxBodyBytes)
|
|
435
437
|
if (r.ok) return r.bytes
|
|
436
|
-
throw r.status
|
|
437
|
-
? plainError(413, "payload_too_large")
|
|
438
|
-
: plainError(400, "invalid_content_length")
|
|
438
|
+
throw plainError(r.status, r.error)
|
|
439
439
|
}
|
|
440
440
|
|
|
441
441
|
/** Backs `c.boundedJson`: `readBoundedJsonSource`, throwing its flat 400/413 on failure. */
|
package/src/server/respond.ts
CHANGED
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
import { hasResidentBody, markResidentBody, residentBodiesRecorded } from "../mount.ts"
|
|
8
8
|
import { NODE_BRIDGE_MARKER_KEYS } from "./bridge-markers.ts"
|
|
9
9
|
import { plainError } from "./http.ts"
|
|
10
|
+
import { jsonResponse } from "./json-response.ts"
|
|
11
|
+
import { jsonText } from "./json-text.ts"
|
|
10
12
|
import {
|
|
11
13
|
CONTEXT_REQUEST,
|
|
12
14
|
CONTEXT_SET,
|
|
13
15
|
EMPTY_RESPONSE_CONTROLS,
|
|
14
16
|
type HandlerResult,
|
|
15
17
|
isResponseResult,
|
|
18
|
+
JSON_CONTENT_TYPE,
|
|
16
19
|
type PlainRender,
|
|
17
20
|
plainRenderHeaders,
|
|
18
21
|
type ResponseResult,
|
|
@@ -41,10 +44,9 @@ function plainRenderSet(plain: PlainRender, set: CtxSet): CtxSet {
|
|
|
41
44
|
} as CtxSet
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
// Keep the fast JSON respond path byte-identical to `Response.json`
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
const JSON_CONTENT_TYPE = "application/json;charset=utf-8"
|
|
47
|
+
// Keep the fast JSON respond path byte-identical to `Response.json` (see `JSON_CONTENT_TYPE`). A
|
|
48
|
+
// shared `Headers` is safe to reuse across responses: the Response constructor copies
|
|
49
|
+
// `init.headers` into its own list.
|
|
48
50
|
const JSON_CT_HEADERS = new Headers({ "content-type": JSON_CONTENT_TYPE })
|
|
49
51
|
const JSON_INIT_200: ResponseInit = { status: 200, headers: JSON_CT_HEADERS }
|
|
50
52
|
|
|
@@ -212,13 +214,14 @@ function denoResponseWithJsonBody(
|
|
|
212
214
|
* it exactly as `withJsonContentType` did.
|
|
213
215
|
*/
|
|
214
216
|
function jsonResponseWithOwnHeaders(
|
|
215
|
-
result:
|
|
217
|
+
result: unknown,
|
|
216
218
|
status: number,
|
|
217
219
|
headers: Record<string, string>,
|
|
220
|
+
unstamped: boolean,
|
|
218
221
|
): Response {
|
|
219
|
-
const response = status === 200 ?
|
|
222
|
+
const response = jsonResponse(result, status === 200 ? undefined : { status })
|
|
220
223
|
for (const name of Object.keys(headers)) response.headers.set(name, headers[name]!)
|
|
221
|
-
return withBody(response)
|
|
224
|
+
return withBody(response, unstamped)
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
// The framework-buffered-body marker shared with the Node adapter: a Response carrying it exposes
|
|
@@ -254,9 +257,9 @@ export function rememberRenderedResponse(response: Response, hasBody: boolean):
|
|
|
254
257
|
return response
|
|
255
258
|
}
|
|
256
259
|
|
|
257
|
-
/**
|
|
258
|
-
function withBody(response: Response): Response {
|
|
259
|
-
return markResidentBody(response)
|
|
260
|
+
/** Record serialized bodies when response hooks or an in-process client can consume the mark. */
|
|
261
|
+
function withBody(response: Response, unstamped = false): Response {
|
|
262
|
+
return unstamped && !residentBodiesRecorded ? response : markResidentBody(response)
|
|
260
263
|
}
|
|
261
264
|
|
|
262
265
|
const HEADER_MUTABILITY_PROBE = "x-nifra-header-probe"
|
|
@@ -488,8 +491,9 @@ function tagged(
|
|
|
488
491
|
response: Response,
|
|
489
492
|
body: string,
|
|
490
493
|
tagResponseBody: ResponseBodyTagOption,
|
|
494
|
+
unstamped = false,
|
|
491
495
|
): Response {
|
|
492
|
-
withBody(response)
|
|
496
|
+
withBody(response, unstamped)
|
|
493
497
|
return tagResponseBody === false || tagResponseBody === undefined
|
|
494
498
|
? response
|
|
495
499
|
: markTaggedResponse(
|
|
@@ -499,9 +503,16 @@ function tagged(
|
|
|
499
503
|
)
|
|
500
504
|
}
|
|
501
505
|
|
|
502
|
-
/**
|
|
503
|
-
*
|
|
504
|
-
*
|
|
506
|
+
/** A plain 200 JSON response. On Bun and Deno the native `Response.json` beats a hand-inlined
|
|
507
|
+
* stringify + Response construction; on Node undici's is 18-26% slower than stringify into the
|
|
508
|
+
* prebuilt init. Both keep the exact body/content-type contract. */
|
|
509
|
+
function plainJsonResponse(result: object): Response {
|
|
510
|
+
// `undefined` from stringify (a `toJSON` returning nothing) keeps `Response.json`'s TypeError.
|
|
511
|
+
const body = IS_BUN || IS_DENO ? undefined : (jsonText(result) as string | undefined)
|
|
512
|
+
return body === undefined ? jsonResponse(result) : new Response(body, JSON_INIT_200)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Fused-lane respond when `c.set` is untouched; keep the generic fallback for non-JSON values. */
|
|
505
516
|
export function fusedRespondNoSet(
|
|
506
517
|
result: unknown,
|
|
507
518
|
tagResponseBody: ResponseBodyTagOption = false,
|
|
@@ -527,7 +538,7 @@ export function fusedRespondNoSet(
|
|
|
527
538
|
!(result instanceof Response) &&
|
|
528
539
|
!isResponseResult(result)
|
|
529
540
|
) {
|
|
530
|
-
const response =
|
|
541
|
+
const response = plainJsonResponse(result)
|
|
531
542
|
return residentBodiesRecorded ? markResidentBody(response) : response
|
|
532
543
|
}
|
|
533
544
|
if (
|
|
@@ -543,7 +554,7 @@ export function fusedRespondNoSet(
|
|
|
543
554
|
// Deno the tier's only saving here is the response walk itself; every other runtime keeps the
|
|
544
555
|
// one-call prebuilt init below.
|
|
545
556
|
if (statics !== undefined && IS_DENO && !tagResponseBody) {
|
|
546
|
-
const response =
|
|
557
|
+
const response = jsonResponse(result)
|
|
547
558
|
// Indexed for the same reason as `applyStaticDefaults`: pair destructuring is a second
|
|
548
559
|
// iterator walk per header.
|
|
549
560
|
for (let i = 0; i < statics.entries.length; i++) {
|
|
@@ -556,7 +567,7 @@ export function fusedRespondNoSet(
|
|
|
556
567
|
// content-type, so this lane still constructs the response in one call - the whole point of
|
|
557
568
|
// declaring headers statically instead of writing them per response.
|
|
558
569
|
if (tagResponseBody || statics !== undefined) {
|
|
559
|
-
const body =
|
|
570
|
+
const body = jsonText(result) as string | undefined
|
|
560
571
|
// Each shape carries the content-type of the lane it stands in for: the framework's own for the
|
|
561
572
|
// tagging path this branch already owned, and `Response.json`'s for the construction below that
|
|
562
573
|
// the static tier replaces. They differ on Deno, and a declared header must not change one.
|
|
@@ -579,8 +590,8 @@ export function fusedRespondNoSet(
|
|
|
579
590
|
}
|
|
580
591
|
return withBody(
|
|
581
592
|
statics === undefined
|
|
582
|
-
?
|
|
583
|
-
:
|
|
593
|
+
? plainJsonResponse(result)
|
|
594
|
+
: jsonResponse(result, statics.responseJsonInit200()),
|
|
584
595
|
)
|
|
585
596
|
}
|
|
586
597
|
return toResponse(
|
|
@@ -605,13 +616,14 @@ export function fusedRespond(
|
|
|
605
616
|
const request = unstamped ? undefined : ctx[CONTEXT_REQUEST]
|
|
606
617
|
if (set === undefined)
|
|
607
618
|
return fusedRespondNoSet(result, tagResponseBody, statics, unstamped, request)
|
|
608
|
-
return toResponse(result as HandlerResult, set, tagResponseBody, statics, request)
|
|
619
|
+
return toResponse(result as HandlerResult, set, tagResponseBody, statics, request, unstamped)
|
|
609
620
|
}
|
|
610
621
|
|
|
611
622
|
/**
|
|
612
623
|
* Render a handler's result. `request` is the request being answered, given when response hooks will
|
|
613
624
|
* write next: a copy or body-less response built here is then recorded as that request's own, so the
|
|
614
625
|
* hooks write it in place instead of copying it again.
|
|
626
|
+
* `unstamped` is the caller's proof that no response hook follows; unknown callers keep the mark.
|
|
615
627
|
*/
|
|
616
628
|
export function toResponse(
|
|
617
629
|
result: HandlerResult,
|
|
@@ -619,6 +631,7 @@ export function toResponse(
|
|
|
619
631
|
tagResponseBody: ResponseBodyTagOption = false,
|
|
620
632
|
statics?: StaticResponseHeaders,
|
|
621
633
|
request?: Request,
|
|
634
|
+
unstamped = false,
|
|
622
635
|
): Response {
|
|
623
636
|
if (isResponseResult(result)) {
|
|
624
637
|
const plain = result.plain
|
|
@@ -632,6 +645,7 @@ export function toResponse(
|
|
|
632
645
|
tagResponseBody,
|
|
633
646
|
statics,
|
|
634
647
|
request,
|
|
648
|
+
unstamped,
|
|
635
649
|
)
|
|
636
650
|
}
|
|
637
651
|
// A buffered page is built from the body it reports, so the bytes its hooks see are the ones sent.
|
|
@@ -664,7 +678,7 @@ export function toResponse(
|
|
|
664
678
|
typeof tagResponseBody === "object" ? tagResponseBody : undefined,
|
|
665
679
|
)
|
|
666
680
|
}
|
|
667
|
-
return renderValue(result, set, tagResponseBody, statics, request)
|
|
681
|
+
return renderValue(result, set, tagResponseBody, statics, request, unstamped)
|
|
668
682
|
}
|
|
669
683
|
|
|
670
684
|
/**
|
|
@@ -694,6 +708,7 @@ function renderValue(
|
|
|
694
708
|
tagResponseBody: ResponseBodyTagOption,
|
|
695
709
|
statics: StaticResponseHeaders | undefined,
|
|
696
710
|
request: Request | undefined,
|
|
711
|
+
unstamped = false,
|
|
697
712
|
): Response {
|
|
698
713
|
// The static tier only changes WHICH prebuilt header shape the branches below reach for: the
|
|
699
714
|
// request's own headers are folded on top of the declared defaults once, here.
|
|
@@ -723,14 +738,14 @@ function renderValue(
|
|
|
723
738
|
typeof result === "object" &&
|
|
724
739
|
result !== null
|
|
725
740
|
) {
|
|
726
|
-
return jsonResponseWithOwnHeaders(result, status, own)
|
|
741
|
+
return jsonResponseWithOwnHeaders(result, status, own, unstamped)
|
|
727
742
|
}
|
|
728
743
|
if (own === undefined && result !== undefined) {
|
|
729
744
|
// Fast respond (profiled ~50 ns/req faster on every plain-JSON return): `JSON.stringify` + a
|
|
730
745
|
// prebuilt init beats `Response.json`'s internal init handling. Output is byte-identical -
|
|
731
746
|
// same body bytes, same probed content-type. `undefined` from stringify (a function/symbol
|
|
732
747
|
// result) delegates to Response.json so its TypeError contract stays the single source.
|
|
733
|
-
const body =
|
|
748
|
+
const body = jsonText(result) as string | undefined
|
|
734
749
|
if (body !== undefined) {
|
|
735
750
|
const init: ResponseInit =
|
|
736
751
|
status === 200
|
|
@@ -743,13 +758,14 @@ function renderValue(
|
|
|
743
758
|
: staticJsonResponse(body, status, statics, JSON_CONTENT_TYPE, init),
|
|
744
759
|
body,
|
|
745
760
|
tagResponseBody,
|
|
761
|
+
unstamped,
|
|
746
762
|
)
|
|
747
763
|
}
|
|
748
764
|
}
|
|
749
765
|
const init: ResponseInit = headers === undefined ? { status } : { status, headers }
|
|
750
766
|
if (result === undefined) return ownedBy(new Response(null, init), request)
|
|
751
767
|
if (tagResponseBody) {
|
|
752
|
-
const body =
|
|
768
|
+
const body = jsonText(result) as string | undefined
|
|
753
769
|
if (body !== undefined) {
|
|
754
770
|
const response =
|
|
755
771
|
IS_DENO && !(headers instanceof Headers)
|
|
@@ -757,7 +773,7 @@ function renderValue(
|
|
|
757
773
|
body,
|
|
758
774
|
status,
|
|
759
775
|
headers as Record<string, string>,
|
|
760
|
-
|
|
776
|
+
JSON_CONTENT_TYPE,
|
|
761
777
|
)
|
|
762
778
|
: // lgtm [js/stack-trace-exposure] the serialized body is not a caught error stack.
|
|
763
779
|
new Response(body, { status, headers: withJsonContentType(headers) })
|
|
@@ -769,10 +785,11 @@ function renderValue(
|
|
|
769
785
|
}
|
|
770
786
|
}
|
|
771
787
|
if (IS_DENO && headers !== undefined && !(headers instanceof Headers)) {
|
|
772
|
-
|
|
773
|
-
|
|
788
|
+
// Deno's own `Response.json` then header writes measured ~10% under stringify, construction and
|
|
789
|
+
// the same writes plus a content-type one; a declared content-type still overrides the default.
|
|
790
|
+
return jsonResponseWithOwnHeaders(result, status, headers, unstamped)
|
|
774
791
|
}
|
|
775
|
-
return withBody(
|
|
792
|
+
return withBody(jsonResponse(result, init), unstamped)
|
|
776
793
|
}
|
|
777
794
|
|
|
778
795
|
// Add a JSON content-type without pre-building a `Headers` instance just to check-and-set one key:
|
|
@@ -787,12 +804,12 @@ function withJsonContentType(
|
|
|
787
804
|
): Record<string, string> | Headers {
|
|
788
805
|
if (headers === undefined) return JSON_CT_HEADERS
|
|
789
806
|
if (headers instanceof Headers) {
|
|
790
|
-
if (!headers.has("content-type")) headers.set("content-type",
|
|
807
|
+
if (!headers.has("content-type")) headers.set("content-type", JSON_CONTENT_TYPE)
|
|
791
808
|
return headers
|
|
792
809
|
}
|
|
793
810
|
return Object.hasOwn(headers, "content-type")
|
|
794
811
|
? headers
|
|
795
|
-
: { ...headers, "content-type":
|
|
812
|
+
: { ...headers, "content-type": JSON_CONTENT_TYPE }
|
|
796
813
|
}
|
|
797
814
|
|
|
798
815
|
/** Headers that describe a Response's body or its framing: a `c.set` value for one describes a body
|
|
@@ -32,13 +32,23 @@
|
|
|
32
32
|
* carries its JSON Schema as `jsonSchema` (every `t` schema does) CAN list its declared keys, so for
|
|
33
33
|
* those the payload is projected onto the declared shape before it is validated.
|
|
34
34
|
*
|
|
35
|
+
* ## What is judged is what is written
|
|
36
|
+
*
|
|
37
|
+
* `JSON.stringify` writes what a `toJSON` hook returns, not the fields a validator reads off the
|
|
38
|
+
* object. So a value's hooks are applied first, as the serializer would, and that result is validated
|
|
39
|
+
* and sent: a class whose `toJSON` adds a private field fails a closed schema instead of passing on its
|
|
40
|
+
* public ones. `strip` keeps the declared fields of what the hook writes, so a hook that writes `id`
|
|
41
|
+
* from private state keeps it. A `Date` is the exception; it stays a `Date` for a date schema and is
|
|
42
|
+
* written as its ISO string.
|
|
43
|
+
*
|
|
35
44
|
* ## What enforcement costs
|
|
36
45
|
*
|
|
37
46
|
* Less than it looks like it should. With a compiled validator (`@nifrajs/schema`'s `t` compiles at
|
|
38
47
|
* construction; zod/valibot equivalents are similarly cheap), the check itself measures in the
|
|
39
48
|
* ~100ns-per-response range - on a realistic middleware-carrying route, enforce mode benchmarks
|
|
40
|
-
* within measurement noise of the same route with no contract at all, on Bun and Node alike.
|
|
41
|
-
*
|
|
49
|
+
* within measurement noise of the same route with no contract at all, on Bun and Node alike. Looking
|
|
50
|
+
* for `toJSON` hooks visits every object in the payload, about 13 ns each, so a 50-row list pays near
|
|
51
|
+
* 0.7 us. The other cost is structural: a contracted route cannot take the bare-route fused lane, because
|
|
42
52
|
* the check needs the handler's VALUE before it becomes bytes. A route with any middleware, derive,
|
|
43
53
|
* or lifecycle hook has already left that lane, so for the routes that look like production the
|
|
44
54
|
* contract is effectively free - declare it.
|
|
@@ -49,6 +59,7 @@ import {
|
|
|
49
59
|
type StandardSchemaV1,
|
|
50
60
|
} from "../schema/standard.ts"
|
|
51
61
|
import { INSTALL_RESPONSE_CONTRACT } from "./install.ts"
|
|
62
|
+
import { jsonText } from "./json-text.ts"
|
|
52
63
|
import type { IdentityPlugin } from "./plugin.ts"
|
|
53
64
|
import { isResponseResult, status } from "./runtime-core.ts"
|
|
54
65
|
import type { AnyServer } from "./server.ts"
|
|
@@ -166,7 +177,12 @@ function compileProjector(node: unknown): Projector | undefined {
|
|
|
166
177
|
const item = Array.isArray(items) ? undefined : compileProjector(items)
|
|
167
178
|
return item === undefined
|
|
168
179
|
? undefined
|
|
169
|
-
: (value) =>
|
|
180
|
+
: (value) => {
|
|
181
|
+
if (!Array.isArray(value)) return value
|
|
182
|
+
const out: unknown[] = []
|
|
183
|
+
for (let i = 0; i < value.length; i++) out.push(item(writtenAt(value[i], i)))
|
|
184
|
+
return out
|
|
185
|
+
}
|
|
170
186
|
}
|
|
171
187
|
if (type !== "object") return undefined
|
|
172
188
|
const properties = objectField(schema, "properties") ?? {}
|
|
@@ -196,7 +212,7 @@ function compileProjector(node: unknown): Projector | undefined {
|
|
|
196
212
|
for (const key of closed ? keys : nested.keys()) {
|
|
197
213
|
if (!hasField(value, key)) continue
|
|
198
214
|
const projector = nested.get(key)
|
|
199
|
-
define(out, key, projector === undefined ? value[key] : projector(value[key]))
|
|
215
|
+
define(out, key, projector === undefined ? value[key] : projector(writtenAt(value[key], key)))
|
|
200
216
|
}
|
|
201
217
|
if (patterns.length > 0) projectPatternKeys(value, out, declared, patterns, closed)
|
|
202
218
|
return out
|
|
@@ -340,7 +356,8 @@ function projectPatternKeys(
|
|
|
340
356
|
matches++
|
|
341
357
|
}
|
|
342
358
|
if (matches === 0) continue
|
|
343
|
-
if (matches === 1 && projector !== undefined)
|
|
359
|
+
if (matches === 1 && projector !== undefined)
|
|
360
|
+
define(out, key, projector(writtenAt(value[key], key)))
|
|
344
361
|
else if (closed) define(out, key, value[key])
|
|
345
362
|
}
|
|
346
363
|
}
|
|
@@ -352,6 +369,101 @@ function define(out: Record<string, unknown>, key: string, value: unknown): void
|
|
|
352
369
|
else out[key] = value
|
|
353
370
|
}
|
|
354
371
|
|
|
372
|
+
const DATE_TO_JSON = Date.prototype.toJSON
|
|
373
|
+
|
|
374
|
+
// Deeper than this, the copying walk takes over with its own cycle check.
|
|
375
|
+
const MAX_HOOK_SCAN_DEPTH = 64
|
|
376
|
+
|
|
377
|
+
/** Whether `value` holds a `toJSON` hook other than a `Date`'s. Allocates nothing: most payloads
|
|
378
|
+
* have none, and this is the price every contracted response pays. */
|
|
379
|
+
function hasHook(value: object, depth: number): boolean {
|
|
380
|
+
if (depth > MAX_HOOK_SCAN_DEPTH) return true
|
|
381
|
+
const hook: unknown = Reflect.get(value, "toJSON")
|
|
382
|
+
if (typeof hook === "function") return !(hook === DATE_TO_JSON && value instanceof Date)
|
|
383
|
+
if (Array.isArray(value)) {
|
|
384
|
+
for (let i = 0; i < value.length; i++) {
|
|
385
|
+
const item: unknown = value[i]
|
|
386
|
+
if (typeof item === "object" && item !== null && hasHook(item, depth + 1)) return true
|
|
387
|
+
}
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
// Inherited enumerable keys are scanned too; that only ever sends a value to the copying walk.
|
|
391
|
+
for (const key in value) {
|
|
392
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: `key` was enumerated from `value`; `Reflect.get` here halves the speed of this for-in.
|
|
393
|
+
const field: unknown = (value as Record<string, unknown>)[key]
|
|
394
|
+
if (typeof field === "object" && field !== null && hasHook(field, depth + 1)) return true
|
|
395
|
+
}
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* What `JSON.stringify` writes for `value`: every `toJSON` hook applied as the serializer applies it,
|
|
401
|
+
* so the contract judges the bytes that go out rather than fields a hook then replaces. Copies only the
|
|
402
|
+
* containers above a hook. A `Date` keeps its object, since its own hook writes nothing but the ISO
|
|
403
|
+
* string a date schema declares.
|
|
404
|
+
*/
|
|
405
|
+
const wireValueOf = (value: unknown): unknown =>
|
|
406
|
+
typeof value === "object" && value !== null && hasHook(value, 0)
|
|
407
|
+
? wireValue(value, "", [])
|
|
408
|
+
: value
|
|
409
|
+
|
|
410
|
+
const wireValue = (value: unknown, key: string | number, path: object[]): unknown =>
|
|
411
|
+
wireChildren(writtenAt(value, key), path)
|
|
412
|
+
|
|
413
|
+
/** `value` as the serializer takes it under `key`: its own hook applied, its fields still as they are. */
|
|
414
|
+
function writtenAt(value: unknown, key: string | number): unknown {
|
|
415
|
+
if (typeof value !== "object" || value === null) return value
|
|
416
|
+
const hook: unknown = Reflect.get(value, "toJSON")
|
|
417
|
+
if (typeof hook !== "function" || (hook === DATE_TO_JSON && value instanceof Date)) return value
|
|
418
|
+
return fieldsOf(Reflect.apply(hook, value, [String(key)]))
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The serializer calls one hook per value, so a hook's result that has a hook of its own is written
|
|
423
|
+
* by its fields. Copying them out keeps that second hook from running when the checked value is sent.
|
|
424
|
+
*/
|
|
425
|
+
function fieldsOf(value: unknown): unknown {
|
|
426
|
+
if (typeof value !== "object" || value === null) return value
|
|
427
|
+
if (typeof Reflect.get(value, "toJSON") !== "function") return value
|
|
428
|
+
if (Array.isArray(value)) return Array.from({ length: value.length }, (_, i): unknown => value[i])
|
|
429
|
+
const fields: Record<string, unknown> = {}
|
|
430
|
+
for (const name of Object.keys(value))
|
|
431
|
+
if (name !== "toJSON") define(fields, name, Reflect.get(value, name))
|
|
432
|
+
return fields
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function wireChildren(value: unknown, path: object[]): unknown {
|
|
436
|
+
// A cycle is left as it is for `JSON.stringify` to refuse.
|
|
437
|
+
if (typeof value !== "object" || value === null || path.includes(value)) return value
|
|
438
|
+
path.push(value)
|
|
439
|
+
if (Array.isArray(value)) {
|
|
440
|
+
let items: unknown[] | undefined
|
|
441
|
+
for (let i = 0; i < value.length; i++) {
|
|
442
|
+
const item: unknown = value[i]
|
|
443
|
+
const written = wireValue(item, i, path)
|
|
444
|
+
if (written !== item) items ??= value.slice(0, i)
|
|
445
|
+
if (items !== undefined) items[i] = written
|
|
446
|
+
}
|
|
447
|
+
path.pop()
|
|
448
|
+
return items ?? value
|
|
449
|
+
}
|
|
450
|
+
const keys = Object.keys(value)
|
|
451
|
+
let out: Record<string, unknown> | undefined
|
|
452
|
+
let index = 0
|
|
453
|
+
for (const name of keys) {
|
|
454
|
+
const field: unknown = Reflect.get(value, name)
|
|
455
|
+
const written = wireValue(field, name, path)
|
|
456
|
+
if (written !== field && out === undefined) {
|
|
457
|
+
out = {}
|
|
458
|
+
for (const earlier of keys.slice(0, index)) define(out, earlier, Reflect.get(value, earlier))
|
|
459
|
+
}
|
|
460
|
+
if (out !== undefined) define(out, name, written)
|
|
461
|
+
index++
|
|
462
|
+
}
|
|
463
|
+
path.pop()
|
|
464
|
+
return out ?? value
|
|
465
|
+
}
|
|
466
|
+
|
|
355
467
|
const MAX_STRIPPED_NAMES = 20
|
|
356
468
|
const MAX_NAME_LENGTH = 64
|
|
357
469
|
const MAX_REPORTED_SETS = 32
|
|
@@ -435,7 +547,7 @@ const schemaFor = (definition: ResponseContractDefinition, code: number) =>
|
|
|
435
547
|
function reserialize(reply: Response, value: unknown): Response {
|
|
436
548
|
const headers = new Headers(reply.headers)
|
|
437
549
|
headers.delete("content-length")
|
|
438
|
-
return new Response(
|
|
550
|
+
return new Response(jsonText(value), {
|
|
439
551
|
status: reply.status,
|
|
440
552
|
statusText: reply.statusText,
|
|
441
553
|
headers,
|
|
@@ -523,10 +635,21 @@ function validateContract(
|
|
|
523
635
|
replace: (value: unknown) => unknown,
|
|
524
636
|
): ResponseContractOutcome | Promise<ResponseContractOutcome> {
|
|
525
637
|
const project = policy.strip ? projectorOf(schema) : undefined
|
|
526
|
-
|
|
638
|
+
// Naming what was stripped compares against everything the payload writes, so it walks it whole.
|
|
639
|
+
const before =
|
|
640
|
+
project === undefined || policy.report !== undefined || mode === "warn"
|
|
641
|
+
? wireValueOf(payload)
|
|
642
|
+
: payload
|
|
643
|
+
// The projection applies the hook of each value it descends into and reads only declared fields,
|
|
644
|
+
// so a quiet strip never enumerates the payload's own keys. What it copies whole is walked after.
|
|
645
|
+
const sent = project === undefined ? before : wireValueOf(project(writtenAt(before, "")))
|
|
646
|
+
const settled = schema["~standard"].validate(sent)
|
|
647
|
+
// When hooks wrote the payload, `warn` sends what they wrote here rather than calling them again,
|
|
648
|
+
// so a hook that answers differently on a second call cannot send what was not judged.
|
|
649
|
+
const shown = mode === "warn" && before !== payload ? replace(before) : served
|
|
527
650
|
return settled instanceof Promise
|
|
528
|
-
? settled.then((r) => interpret(r,
|
|
529
|
-
: interpret(settled,
|
|
651
|
+
? settled.then((r) => interpret(r, before, shown, mode, replace, policy.report))
|
|
652
|
+
: interpret(settled, before, shown, mode, replace, policy.report)
|
|
530
653
|
}
|
|
531
654
|
|
|
532
655
|
function interpret(
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Method, Router } from "../router/router.ts"
|
|
2
|
+
import type { RouteDescriptor } from "./server-types.ts"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A request's matched route template (`/users/:id`, never `/users/42`), or `undefined` when no route
|
|
6
|
+
* serves it. The matcher is built on first use: by serving time every route is registered, so
|
|
7
|
+
* `app.routes()` is complete, where building it when a plugin is applied would miss later routes.
|
|
8
|
+
*/
|
|
9
|
+
export function routeTemplateOf(app: {
|
|
10
|
+
routes(): ReadonlyArray<RouteDescriptor>
|
|
11
|
+
}): (method: string, path: string) => string | undefined {
|
|
12
|
+
let matcher: Router<string> | undefined
|
|
13
|
+
return (method, path) => {
|
|
14
|
+
if (matcher === undefined) {
|
|
15
|
+
matcher = new Router<string>()
|
|
16
|
+
for (const route of app.routes()) {
|
|
17
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: every registered route's method is one this router already accepted when the app registered it.
|
|
18
|
+
matcher.add(route.method as Method, route.path, route.path)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const found = matcher.find(method, path)
|
|
22
|
+
return found.found ? found.payload : undefined
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -6,11 +6,21 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createUnboundedRequestBudget, type RequestBudget } from "../budget.ts"
|
|
8
8
|
import { NODE_BRIDGE_MARKER_KEYS } from "./bridge-markers.ts"
|
|
9
|
+
import { jsonText } from "./json-text.ts"
|
|
9
10
|
import type { CtxSet, RequestSource } from "./server.ts"
|
|
10
11
|
|
|
11
12
|
/** A handler returns a `Response` (used as-is) or any value (serialized to JSON). */
|
|
12
13
|
export type HandlerResult = Response | unknown
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The Content-Type the runtime's own `Response.json` writes, which every JSON nifra renders on the Web
|
|
17
|
+
* lanes carries too, so a handler's JSON and the framework's envelopes (built with `Response.json`)
|
|
18
|
+
* agree on one runtime. Bun adds `;charset=utf-8`; Deno, workerd and undici do not. Named rather than
|
|
19
|
+
* probed: workerd forbids `Response.json()` while a module loads.
|
|
20
|
+
*/
|
|
21
|
+
export const JSON_CONTENT_TYPE =
|
|
22
|
+
"Bun" in globalThis ? "application/json;charset=utf-8" : "application/json"
|
|
23
|
+
|
|
14
24
|
export const RESPONSE_RESULT = Symbol.for(NODE_BRIDGE_MARKER_KEYS.responseResult)
|
|
15
25
|
export const CONTEXT_SET = Symbol("nifra.context.set")
|
|
16
26
|
export const CONTEXT_SEARCH = Symbol("nifra.context.search")
|
|
@@ -178,8 +188,8 @@ export function status(
|
|
|
178
188
|
toResponse(): Response {
|
|
179
189
|
const headers: Record<string, string> = { ...init?.headers }
|
|
180
190
|
if (body === undefined) return new Response(null, { status: code, headers })
|
|
181
|
-
headers["content-type"] ??=
|
|
182
|
-
return new Response(
|
|
191
|
+
headers["content-type"] ??= JSON_CONTENT_TYPE
|
|
192
|
+
return new Response(jsonText(body), { status: code, headers })
|
|
183
193
|
},
|
|
184
194
|
} as StatusResponse<number, unknown>
|
|
185
195
|
}
|
|
@@ -91,8 +91,9 @@ export interface ServerOptions {
|
|
|
91
91
|
readonly trustBodyFraming?: boolean
|
|
92
92
|
/**
|
|
93
93
|
* Max inbound WebSocket message size (bytes). A larger message never reaches the handler and the
|
|
94
|
-
* socket closes with `1009`; when Bun's own frame limit refuses it, Bun
|
|
95
|
-
* client sees `1006`, while the `close` handler
|
|
94
|
+
* socket closes with `1009`; when Bun's own frame limit refuses it, Bun up to 1.4.3 drops the
|
|
95
|
+
* connection (the client sees `1006`) and Bun 1.4.4 sends `1009`, while the `close` handler gets
|
|
96
|
+
* `1009` either way. Default: `maxBodyBytes` (1 MB).
|
|
96
97
|
* Bun and Node stop buffering at the cap, so a huge frame is never held in memory; on Bun a mounted
|
|
97
98
|
* app's smaller cap, and on Deno and Workers every cap, is checked after the runtime has buffered
|
|
98
99
|
* the whole message.
|
|
@@ -124,10 +125,9 @@ export interface ServerOptions {
|
|
|
124
125
|
readonly maxInboundDeadlineMs?: number
|
|
125
126
|
/**
|
|
126
127
|
* When `listen()`ing, install SIGTERM/SIGINT handlers that gracefully `stop()`. Default false.
|
|
127
|
-
*
|
|
128
|
-
* (default 10_000) - keep it under the platform's kill grace period (`docker stop` waits 10 s).
|
|
128
|
+
* The object form is passed to `stop()`: see {@link StopOptions}.
|
|
129
129
|
*/
|
|
130
|
-
readonly gracefulSignals?: boolean |
|
|
130
|
+
readonly gracefulSignals?: boolean | StopOptions
|
|
131
131
|
/**
|
|
132
132
|
* Capacity-admission gate (see {@link AdmissionController}). Bounds concurrent in-flight work so a
|
|
133
133
|
* healthy instance sheds load (`429` + `Retry-After`) instead of accepting more than it can finish.
|
|
@@ -253,9 +253,21 @@ export interface MountFetchOptions {
|
|
|
253
253
|
readonly opaque?: string | OpaqueMountDeclaration
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
/** A callback
|
|
256
|
+
/** A callback a server awaits while it stops: `onStopping` when it begins, `onStop` after the drain. */
|
|
257
257
|
export type StopHook = () => MaybePromise<void>
|
|
258
258
|
|
|
259
|
+
/** How `stop()` winds a listening server down. */
|
|
260
|
+
export interface StopOptions {
|
|
261
|
+
/** How long in-flight requests get to finish before the stop forces them closed. Default 10_000. */
|
|
262
|
+
readonly drainMs?: number
|
|
263
|
+
/**
|
|
264
|
+
* How long the server keeps serving after `stop()` began, before it drains: the time a load
|
|
265
|
+
* balancer takes to see readiness fail and route elsewhere. Default 0. Keep `delayMs + drainMs`
|
|
266
|
+
* under the platform's kill grace period (`docker stop` waits 10 s, Kubernetes 30 s).
|
|
267
|
+
*/
|
|
268
|
+
readonly delayMs?: number
|
|
269
|
+
}
|
|
270
|
+
|
|
259
271
|
/**
|
|
260
272
|
* MCP tool safety hints, surfaced in `tools/list`, that tell an agent how risky a `.tool()` call is - so it
|
|
261
273
|
* can decide whether to auto-invoke or confirm first. All optional; an omitted hint means "unknown". Mirrors
|