@nifrajs/core 3.1.0 → 3.3.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.
Files changed (146) hide show
  1. package/README.md +21 -0
  2. package/dist/durable-adapters.d.ts.map +1 -1
  3. package/dist/durable-adapters.js +26 -3
  4. package/dist/durable-adapters.js.map +1 -1
  5. package/dist/durable-approval.d.ts +56 -0
  6. package/dist/durable-approval.d.ts.map +1 -0
  7. package/dist/durable-approval.js +233 -0
  8. package/dist/durable-approval.js.map +1 -0
  9. package/dist/durable-effect.d.ts +42 -0
  10. package/dist/durable-effect.d.ts.map +1 -0
  11. package/dist/durable-effect.js +234 -0
  12. package/dist/durable-effect.js.map +1 -0
  13. package/dist/durable-execution.d.ts +8 -346
  14. package/dist/durable-execution.d.ts.map +1 -1
  15. package/dist/durable-execution.js +7 -1150
  16. package/dist/durable-execution.js.map +1 -1
  17. package/dist/durable-saga.d.ts +94 -0
  18. package/dist/durable-saga.d.ts.map +1 -0
  19. package/dist/durable-saga.js +550 -0
  20. package/dist/durable-saga.js.map +1 -0
  21. package/dist/edge-kit.d.ts +2 -2
  22. package/dist/edge-kit.d.ts.map +1 -1
  23. package/dist/edge-kit.js +2 -2
  24. package/dist/edge-kit.js.map +1 -1
  25. package/dist/index.d.ts +23 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +18 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/internal/durable-approval-protocol.d.ts +1 -1
  30. package/dist/internal/durable-approval-protocol.d.ts.map +1 -1
  31. package/dist/internal/durable-effect-protocol.d.ts +1 -1
  32. package/dist/internal/durable-effect-protocol.d.ts.map +1 -1
  33. package/dist/internal/durable-saga-protocol.d.ts +1 -1
  34. package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
  35. package/dist/internal/durable-shared.d.ts +26 -0
  36. package/dist/internal/durable-shared.d.ts.map +1 -0
  37. package/dist/internal/durable-shared.js +131 -0
  38. package/dist/internal/durable-shared.js.map +1 -0
  39. package/dist/internal/durable-types.d.ts +159 -0
  40. package/dist/internal/durable-types.d.ts.map +1 -0
  41. package/dist/internal/durable-types.js +2 -0
  42. package/dist/internal/durable-types.js.map +1 -0
  43. package/dist/internal/route-compiler.d.ts +61 -0
  44. package/dist/internal/route-compiler.d.ts.map +1 -0
  45. package/dist/internal/route-compiler.js +121 -0
  46. package/dist/internal/route-compiler.js.map +1 -0
  47. package/dist/internal/route-execution.d.ts +6 -4
  48. package/dist/internal/route-execution.d.ts.map +1 -1
  49. package/dist/internal/route-execution.js +2 -17
  50. package/dist/internal/route-execution.js.map +1 -1
  51. package/dist/internal/route-lanes.d.ts +35 -0
  52. package/dist/internal/route-lanes.d.ts.map +1 -0
  53. package/dist/internal/route-lanes.js +150 -0
  54. package/dist/internal/route-lanes.js.map +1 -0
  55. package/dist/internal/route-program.d.ts +67 -0
  56. package/dist/internal/route-program.d.ts.map +1 -0
  57. package/dist/internal/route-program.js +199 -0
  58. package/dist/internal/route-program.js.map +1 -0
  59. package/dist/server/bare-error-lane.d.ts +1 -1
  60. package/dist/server/bare-error-lane.d.ts.map +1 -1
  61. package/dist/server/bare-error-lane.js +3 -3
  62. package/dist/server/bare-error-lane.js.map +1 -1
  63. package/dist/server/body-lane.d.ts +13 -0
  64. package/dist/server/body-lane.d.ts.map +1 -0
  65. package/dist/server/body-lane.js +134 -0
  66. package/dist/server/body-lane.js.map +1 -0
  67. package/dist/server/contract.d.ts +40 -14
  68. package/dist/server/contract.d.ts.map +1 -1
  69. package/dist/server/contract.js +7 -0
  70. package/dist/server/contract.js.map +1 -1
  71. package/dist/server/cookies.d.ts.map +1 -1
  72. package/dist/server/cookies.js +16 -2
  73. package/dist/server/cookies.js.map +1 -1
  74. package/dist/server/plugin.d.ts +2 -2
  75. package/dist/server/plugin.d.ts.map +1 -1
  76. package/dist/server/plugin.js +6 -1
  77. package/dist/server/plugin.js.map +1 -1
  78. package/dist/server/registry.d.ts +40 -12
  79. package/dist/server/registry.d.ts.map +1 -1
  80. package/dist/server/request-context.d.ts +4 -28
  81. package/dist/server/request-context.d.ts.map +1 -1
  82. package/dist/server/request-context.js +10 -185
  83. package/dist/server/request-context.js.map +1 -1
  84. package/dist/server/respond.d.ts.map +1 -1
  85. package/dist/server/respond.js +39 -2
  86. package/dist/server/respond.js.map +1 -1
  87. package/dist/server/runtime-core.d.ts +18 -2
  88. package/dist/server/runtime-core.d.ts.map +1 -1
  89. package/dist/server/runtime-core.js +0 -23
  90. package/dist/server/runtime-core.js.map +1 -1
  91. package/dist/server/server-types.d.ts +3 -0
  92. package/dist/server/server-types.d.ts.map +1 -1
  93. package/dist/server/server.d.ts +113 -84
  94. package/dist/server/server.d.ts.map +1 -1
  95. package/dist/server/server.js +506 -639
  96. package/dist/server/server.js.map +1 -1
  97. package/dist/server/validation.d.ts +5 -0
  98. package/dist/server/validation.d.ts.map +1 -0
  99. package/dist/server/validation.js +13 -0
  100. package/dist/server/validation.js.map +1 -0
  101. package/dist/server/ws-bun.d.ts +11 -3
  102. package/dist/server/ws-bun.d.ts.map +1 -1
  103. package/dist/server/ws-bun.js +42 -12
  104. package/dist/server/ws-bun.js.map +1 -1
  105. package/dist/server/ws-hook.d.ts +3 -2
  106. package/dist/server/ws-hook.d.ts.map +1 -1
  107. package/dist/server.d.ts +2 -2
  108. package/dist/server.d.ts.map +1 -1
  109. package/dist/server.js +1 -1
  110. package/dist/server.js.map +1 -1
  111. package/dist/single-copy.d.ts.map +1 -1
  112. package/dist/single-copy.js +3 -1
  113. package/dist/single-copy.js.map +1 -1
  114. package/package.json +6 -3
  115. package/src/durable-adapters.ts +20 -0
  116. package/src/durable-approval.ts +281 -0
  117. package/src/durable-effect.ts +300 -0
  118. package/src/durable-execution.ts +8 -1627
  119. package/src/durable-saga.ts +744 -0
  120. package/src/edge-kit.ts +2 -2
  121. package/src/index.ts +129 -2
  122. package/src/internal/durable-approval-protocol.ts +1 -1
  123. package/src/internal/durable-effect-protocol.ts +1 -1
  124. package/src/internal/durable-saga-protocol.ts +1 -1
  125. package/src/internal/durable-shared.ts +157 -0
  126. package/src/internal/durable-types.ts +194 -0
  127. package/src/internal/route-compiler.ts +234 -0
  128. package/src/internal/route-execution.ts +34 -84
  129. package/src/internal/route-lanes.ts +221 -0
  130. package/src/internal/route-program.ts +404 -0
  131. package/src/server/bare-error-lane.ts +3 -2
  132. package/src/server/body-lane.ts +203 -0
  133. package/src/server/contract.ts +70 -20
  134. package/src/server/cookies.ts +16 -2
  135. package/src/server/plugin.ts +11 -5
  136. package/src/server/registry.ts +71 -16
  137. package/src/server/request-context.ts +7 -262
  138. package/src/server/respond.ts +45 -2
  139. package/src/server/runtime-core.ts +28 -2
  140. package/src/server/server-types.ts +3 -0
  141. package/src/server/server.ts +1152 -1260
  142. package/src/server/validation.ts +19 -0
  143. package/src/server/ws-bun.ts +44 -11
  144. package/src/server/ws-hook.ts +3 -2
  145. package/src/server.ts +7 -2
  146. package/src/single-copy.ts +3 -1
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Registration-compiled general route program.
3
+ *
4
+ * This is deliberately a data-and-control-flow compiler, not source generation: user functions and
5
+ * schemas remain opaque references and no request value is ever converted into executable code. The
6
+ * program is frozen at registration so the request path walks one immutable stage sequence instead
7
+ * of rediscovering which lifecycle features are present.
8
+ */
9
+
10
+ import type { StandardSchemaV1 } from "../schema/standard.ts"
11
+ import type { Registry } from "../server/registry.ts"
12
+ import {
13
+ CONTEXT_SET,
14
+ EMPTY_RESPONSE_CONTROLS,
15
+ isResponseResult,
16
+ type ResponseResult,
17
+ } from "../server/runtime-core.ts"
18
+ import type { CtxSet, MaybePromise, RawContext, RequestSource, Server } from "../server/server.ts"
19
+ import type {
20
+ InternalHandler,
21
+ RawAfterHandle,
22
+ RawBeforeHandle,
23
+ RawDerive,
24
+ RawErrorHandler,
25
+ RouteEntry,
26
+ } from "./route-execution.ts"
27
+
28
+ export type ProgramValidationKind = "headers" | "params" | "body" | "query"
29
+
30
+ export type RouteProgramStage =
31
+ | { readonly kind: ProgramValidationKind; readonly schema: StandardSchemaV1 }
32
+ | { readonly kind: "derive"; readonly run: RawDerive }
33
+ | { readonly kind: "before"; readonly run: RawBeforeHandle }
34
+ | { readonly kind: "handler"; readonly run: InternalHandler }
35
+ | { readonly kind: "after"; readonly run: RawAfterHandle }
36
+
37
+ /** The complete registration-time lifecycle, in semantic execution order. */
38
+ export interface RouteProgram {
39
+ readonly stages: readonly RouteProgramStage[]
40
+ readonly validationStages: readonly Extract<RouteProgramStage, { kind: ProgramValidationKind }>[]
41
+ readonly derives: readonly RawDerive[]
42
+ readonly beforeHandle: readonly RawBeforeHandle[]
43
+ readonly afterHandle: readonly RawAfterHandle[]
44
+ readonly onError: readonly RawErrorHandler[]
45
+ readonly handler: InternalHandler
46
+ readonly decorations: Record<PropertyKey, unknown>
47
+ readonly hasDecorations: boolean
48
+ readonly bodySchema: StandardSchemaV1 | undefined
49
+ readonly bodyLimit: RouteEntry["bodyLimit"]
50
+ readonly responseContract: RouteEntry["responseContract"]
51
+ }
52
+
53
+ export interface RouteProgramInput {
54
+ readonly schema: RouteEntry["schema"]
55
+ readonly handler: InternalHandler
56
+ readonly derives: readonly RawDerive[]
57
+ readonly beforeHandle: readonly RawBeforeHandle[]
58
+ readonly afterHandle: readonly RawAfterHandle[]
59
+ readonly onError: readonly RawErrorHandler[]
60
+ readonly decorations: Record<PropertyKey, unknown>
61
+ readonly hasDecorations: boolean
62
+ readonly bodySchema: StandardSchemaV1 | undefined
63
+ readonly bodyLimit: RouteEntry["bodyLimit"]
64
+ readonly responseContract: RouteEntry["responseContract"]
65
+ }
66
+
67
+ /** Build the immutable stage sequence once, while the server is still configurable. */
68
+ export function compileRouteProgram(input: RouteProgramInput): RouteProgram {
69
+ const stages: RouteProgramStage[] = []
70
+ const validationStages: Extract<RouteProgramStage, { kind: ProgramValidationKind }>[] = []
71
+ const schema = input.schema
72
+ if (schema?.headers !== undefined) {
73
+ const stage = { kind: "headers", schema: schema.headers } as const
74
+ stages.push(stage)
75
+ validationStages.push(stage)
76
+ }
77
+ if (schema?.params !== undefined) {
78
+ const stage = { kind: "params", schema: schema.params } as const
79
+ stages.push(stage)
80
+ validationStages.push(stage)
81
+ }
82
+ if (schema?.body !== undefined) {
83
+ const stage = { kind: "body", schema: schema.body } as const
84
+ stages.push(stage)
85
+ validationStages.push(stage)
86
+ }
87
+ if (schema?.query !== undefined) {
88
+ const stage = { kind: "query", schema: schema.query } as const
89
+ stages.push(stage)
90
+ validationStages.push(stage)
91
+ }
92
+ for (const derive of input.derives) stages.push({ kind: "derive", run: derive })
93
+ for (const before of input.beforeHandle) stages.push({ kind: "before", run: before })
94
+ stages.push({ kind: "handler", run: input.handler })
95
+ for (const after of input.afterHandle) stages.push({ kind: "after", run: after })
96
+
97
+ return Object.freeze({
98
+ stages: Object.freeze(stages),
99
+ validationStages: Object.freeze(validationStages),
100
+ derives: Object.freeze([...input.derives]),
101
+ beforeHandle: Object.freeze([...input.beforeHandle]),
102
+ afterHandle: Object.freeze([...input.afterHandle]),
103
+ onError: Object.freeze([...input.onError]),
104
+ handler: input.handler,
105
+ decorations: Object.freeze({ ...input.decorations }),
106
+ hasDecorations: input.hasDecorations,
107
+ bodySchema: input.bodySchema,
108
+ bodyLimit: input.bodyLimit,
109
+ responseContract: input.responseContract,
110
+ })
111
+ }
112
+
113
+ interface RouteProgramRuntime {
114
+ validateProgramStage(
115
+ entry: RouteEntry,
116
+ stage: Extract<RouteProgramStage, { kind: ProgramValidationKind }>,
117
+ source: RequestSource,
118
+ ctx: RawContext,
119
+ ): MaybePromise<Response | ResponseResult | undefined>
120
+ readProgramBody(
121
+ entry: RouteEntry,
122
+ source: RequestSource,
123
+ ctx: RawContext,
124
+ ): Promise<Response | ResponseResult | undefined>
125
+ handleProgramError<T>(
126
+ entry: RouteEntry,
127
+ error: unknown,
128
+ ctx: RawContext,
129
+ finalize: (result: unknown, set: CtxSet) => T,
130
+ wrapResponse: (response: Response | ResponseResult) => T,
131
+ ): MaybePromise<T>
132
+ finishProgramResult<T>(
133
+ entry: RouteEntry,
134
+ ctx: RawContext,
135
+ result: unknown,
136
+ finalize: (result: unknown, set: CtxSet) => T,
137
+ wrapResponse: (response: Response | ResponseResult) => T,
138
+ ): MaybePromise<T>
139
+ }
140
+
141
+ function responseSet(ctx: RawContext): CtxSet {
142
+ return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS
143
+ }
144
+
145
+ function isEarly(value: unknown): value is Response | ResponseResult {
146
+ return value instanceof Response || isResponseResult(value)
147
+ }
148
+
149
+ type ProgramResult<T> = MaybePromise<T>
150
+
151
+ type PendingPhase =
152
+ | { readonly kind: "validation"; readonly next: number }
153
+ | { readonly kind: "derive"; readonly next: number }
154
+ | { readonly kind: "before"; readonly next: number }
155
+ | { readonly kind: "handler" }
156
+ | { readonly kind: "after"; readonly next: number }
157
+
158
+ /** Execute the general program. The first synchronous stage sequence stays synchronous. */
159
+ export function executeRouteProgram<T, R extends Registry, Ctx>(
160
+ runtime: Server<R, Ctx>,
161
+ entry: RouteEntry,
162
+ program: RouteProgram,
163
+ source: RequestSource,
164
+ ctx: RawContext,
165
+ finalize: (result: unknown, set: CtxSet) => T,
166
+ wrapResponse: (response: Response | ResponseResult) => T,
167
+ ): ProgramResult<T> {
168
+ const host = runtime as unknown as RouteProgramRuntime
169
+ try {
170
+ if (program.hasDecorations) Object.assign(ctx, program.decorations)
171
+ return runSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, undefined)
172
+ } catch (error) {
173
+ return host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
174
+ }
175
+ }
176
+
177
+ function runSync<T>(
178
+ host: RouteProgramRuntime,
179
+ entry: RouteEntry,
180
+ program: RouteProgram,
181
+ source: RequestSource,
182
+ ctx: RawContext,
183
+ finalize: (result: unknown, set: CtxSet) => T,
184
+ wrapResponse: (response: Response | ResponseResult) => T,
185
+ validationIndex: number,
186
+ initial: unknown,
187
+ ): ProgramResult<T> {
188
+ const validations = program.validationStages
189
+ for (let i = validationIndex; i < validations.length; i++) {
190
+ const stage = validations[i]!
191
+ const outcome =
192
+ stage.kind === "body"
193
+ ? host.readProgramBody(entry, source, ctx)
194
+ : host.validateProgramStage(entry, stage, source, ctx)
195
+ if (outcome instanceof Promise) {
196
+ return continueProgram(
197
+ host,
198
+ entry,
199
+ program,
200
+ source,
201
+ ctx,
202
+ finalize,
203
+ wrapResponse,
204
+ { kind: "validation", next: i + 1 },
205
+ outcome,
206
+ initial,
207
+ )
208
+ }
209
+ if (outcome !== undefined) return wrapResponse(outcome)
210
+ }
211
+ return runHooksSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, 0, 0, initial)
212
+ }
213
+
214
+ function runHooksSync<T>(
215
+ host: RouteProgramRuntime,
216
+ entry: RouteEntry,
217
+ program: RouteProgram,
218
+ source: RequestSource,
219
+ ctx: RawContext,
220
+ finalize: (result: unknown, set: CtxSet) => T,
221
+ wrapResponse: (response: Response | ResponseResult) => T,
222
+ deriveIndex: number,
223
+ beforeIndex: number,
224
+ afterIndex: number,
225
+ initial: unknown,
226
+ ): ProgramResult<T> {
227
+ for (let i = deriveIndex; i < program.derives.length; i++) {
228
+ const outcome = program.derives[i]!(ctx)
229
+ if (outcome instanceof Promise) {
230
+ return continueProgram(
231
+ host,
232
+ entry,
233
+ program,
234
+ source,
235
+ ctx,
236
+ finalize,
237
+ wrapResponse,
238
+ { kind: "derive", next: i + 1 },
239
+ outcome,
240
+ initial,
241
+ )
242
+ }
243
+ if (isEarly(outcome)) return finalize(outcome, responseSet(ctx))
244
+ Object.assign(ctx, outcome)
245
+ }
246
+ for (let i = beforeIndex; i < program.beforeHandle.length; i++) {
247
+ const outcome = program.beforeHandle[i]!(ctx)
248
+ if (outcome instanceof Promise) {
249
+ return continueProgram(
250
+ host,
251
+ entry,
252
+ program,
253
+ source,
254
+ ctx,
255
+ finalize,
256
+ wrapResponse,
257
+ { kind: "before", next: i + 1 },
258
+ outcome,
259
+ initial,
260
+ )
261
+ }
262
+ if (outcome !== undefined) return finalize(outcome, responseSet(ctx))
263
+ }
264
+
265
+ const output = program.handler(ctx)
266
+ if (output instanceof Promise) {
267
+ return continueProgram(
268
+ host,
269
+ entry,
270
+ program,
271
+ source,
272
+ ctx,
273
+ finalize,
274
+ wrapResponse,
275
+ { kind: "handler" },
276
+ output,
277
+ initial,
278
+ )
279
+ }
280
+ let result = output
281
+ for (let i = afterIndex; i < program.afterHandle.length; i++) {
282
+ const outcome = program.afterHandle[i]!(result, ctx)
283
+ if (outcome instanceof Promise) {
284
+ return continueProgram(
285
+ host,
286
+ entry,
287
+ program,
288
+ source,
289
+ ctx,
290
+ finalize,
291
+ wrapResponse,
292
+ { kind: "after", next: i + 1 },
293
+ outcome,
294
+ result,
295
+ )
296
+ }
297
+ result = outcome
298
+ }
299
+ return host.finishProgramResult(entry, ctx, result, finalize, wrapResponse)
300
+ }
301
+
302
+ async function continueProgram<T>(
303
+ host: RouteProgramRuntime,
304
+ entry: RouteEntry,
305
+ program: RouteProgram,
306
+ source: RequestSource,
307
+ ctx: RawContext,
308
+ finalize: (result: unknown, set: CtxSet) => T,
309
+ wrapResponse: (response: Response | ResponseResult) => T,
310
+ pendingPhase: PendingPhase,
311
+ pending: Promise<unknown>,
312
+ initial: unknown,
313
+ ): Promise<T> {
314
+ try {
315
+ const settled = await pending
316
+ let result = initial
317
+ if (pendingPhase.kind === "validation") {
318
+ if (settled !== undefined) return wrapResponse(settled as Response | ResponseResult)
319
+ } else if (pendingPhase.kind === "derive") {
320
+ if (isEarly(settled)) return finalize(settled, responseSet(ctx))
321
+ Object.assign(ctx, settled)
322
+ } else if (pendingPhase.kind === "before") {
323
+ if (settled !== undefined) return finalize(settled, responseSet(ctx))
324
+ } else if (pendingPhase.kind === "handler") {
325
+ result = settled
326
+ } else {
327
+ result = settled
328
+ }
329
+ return await runAsyncStages(
330
+ host,
331
+ entry,
332
+ program,
333
+ source,
334
+ ctx,
335
+ finalize,
336
+ wrapResponse,
337
+ pendingPhase,
338
+ result,
339
+ )
340
+ } catch (error) {
341
+ return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
342
+ }
343
+ }
344
+
345
+ async function runAsyncStages<T>(
346
+ host: RouteProgramRuntime,
347
+ entry: RouteEntry,
348
+ program: RouteProgram,
349
+ source: RequestSource,
350
+ ctx: RawContext,
351
+ finalize: (result: unknown, set: CtxSet) => T,
352
+ wrapResponse: (response: Response | ResponseResult) => T,
353
+ pendingPhase: PendingPhase,
354
+ result: unknown,
355
+ ): Promise<T> {
356
+ try {
357
+ if (pendingPhase.kind === "validation") {
358
+ const validations = program.validationStages
359
+ for (let i = pendingPhase.next; i < validations.length; i++) {
360
+ const stage = validations[i]!
361
+ const outcome =
362
+ stage.kind === "body"
363
+ ? await host.readProgramBody(entry, source, ctx)
364
+ : host.validateProgramStage(entry, stage, source, ctx)
365
+ const settled = outcome instanceof Promise ? await outcome : outcome
366
+ if (settled !== undefined) return wrapResponse(settled)
367
+ }
368
+ }
369
+
370
+ const deriveStart = pendingPhase.kind === "derive" ? pendingPhase.next : 0
371
+ if (pendingPhase.kind === "validation" || pendingPhase.kind === "derive") {
372
+ for (let i = deriveStart; i < program.derives.length; i++) {
373
+ const outcome = program.derives[i]!(ctx)
374
+ const settled = outcome instanceof Promise ? await outcome : outcome
375
+ if (isEarly(settled)) return finalize(settled, responseSet(ctx))
376
+ Object.assign(ctx, settled)
377
+ }
378
+ }
379
+
380
+ const beforeStart = pendingPhase.kind === "before" ? pendingPhase.next : 0
381
+ if (
382
+ pendingPhase.kind === "validation" ||
383
+ pendingPhase.kind === "derive" ||
384
+ pendingPhase.kind === "before"
385
+ ) {
386
+ for (let i = beforeStart; i < program.beforeHandle.length; i++) {
387
+ const outcome = program.beforeHandle[i]!(ctx)
388
+ const settled = outcome instanceof Promise ? await outcome : outcome
389
+ if (settled !== undefined) return finalize(settled, responseSet(ctx))
390
+ }
391
+ const output = program.handler(ctx)
392
+ result = output instanceof Promise ? await output : output
393
+ }
394
+
395
+ const afterStart = pendingPhase.kind === "after" ? pendingPhase.next : 0
396
+ for (let i = afterStart; i < program.afterHandle.length; i++) {
397
+ const outcome = program.afterHandle[i]!(result, ctx)
398
+ result = outcome instanceof Promise ? await outcome : outcome
399
+ }
400
+ return await host.finishProgramResult(entry, ctx, result, finalize, wrapResponse)
401
+ } catch (error) {
402
+ return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
403
+ }
404
+ }
@@ -17,7 +17,7 @@
17
17
  * which lane caught it. Both the generic bare lane and the fused body runner route here, so that
18
18
  * decision lives in exactly one place.
19
19
  */
20
- import { pathnameOf, plainError } from "./http.ts"
20
+ import { pathnameOf } from "./http.ts"
21
21
  import type { Logger } from "./logger.ts"
22
22
  import { isResponseResult, type ResponseResult } from "./runtime-core.ts"
23
23
  import type { CtxSet, RawContext } from "./server.ts"
@@ -43,11 +43,12 @@ export function renderBareError<T>(
43
43
  wrapResponse: (response: Response | ResponseResult) => T,
44
44
  responseSet: (ctx: RawContext) => CtxSet,
45
45
  logError: (err: unknown, ctx: RawContext) => void,
46
+ internalError: () => T,
46
47
  ): T {
47
48
  if (err instanceof Response) return wrapResponse(err)
48
49
  if (isResponseResult(err)) return finalize(err, responseSet(ctx), ctx)
49
50
  logError(err, ctx)
50
- return wrapResponse(plainError(500, "internal_error"))
51
+ return internalError()
51
52
  }
52
53
 
53
54
  /**
@@ -0,0 +1,203 @@
1
+ /**
2
+ * The shared framed body parser. Kept separate from RequestContext so compact edge consumers can
3
+ * reuse the exact trust-boundary lane without bundling the full context implementation (cookies,
4
+ * deferred responses, and request helpers).
5
+ */
6
+ import { drainCapped, hasTrustedBodyFraming, parseContentLength, RAW_BODY_READERS } from "./body.ts"
7
+ import { plainError } from "./http.ts"
8
+ import { guardParsedValue, type ProtoPoisoning, parseJsonGuarded } from "./proto-guard.ts"
9
+ import { isUrlEncodedForm, readBoundedForm } from "./query.ts"
10
+ import {
11
+ headerOf,
12
+ isResponseResult,
13
+ PRE_DECODED_BODY,
14
+ type PreDecodedBody,
15
+ type ResponseResult,
16
+ TEXT_DECODER,
17
+ } from "./runtime-core.ts"
18
+ import type { MaybePromise, RequestSource } from "./server.ts"
19
+
20
+ type JsonResultContinuation<T> = (result: unknown | ResponseResult) => T | Promise<T>
21
+ type JsonErrorContinuation<T> = (error: unknown) => T | Promise<T>
22
+
23
+ type FramedJsonSource = RequestSource & {
24
+ readonly bytes?: () => Promise<Uint8Array>
25
+ readonly jsonWithByteLength?: () => Promise<{
26
+ readonly value: unknown
27
+ readonly byteLength: number
28
+ }>
29
+ }
30
+
31
+ const INVALID_JSON = (): ResponseResult => plainError(400, "invalid_json")
32
+
33
+ /** Read a JSON source with the declared-length precheck, delivered-byte recheck, streaming cap, and
34
+ * prototype-poisoning guard in one lane. */
35
+ export function readBoundedJsonSource(
36
+ req: RequestSource,
37
+ maxBytes: number,
38
+ protoPoisoning?: ProtoPoisoning,
39
+ ): Promise<unknown | ResponseResult>
40
+ export function readBoundedJsonSource<T>(
41
+ req: RequestSource,
42
+ maxBytes: number,
43
+ protoPoisoning: ProtoPoisoning,
44
+ onResult: JsonResultContinuation<T>,
45
+ onError: JsonErrorContinuation<T>,
46
+ ): Promise<T>
47
+ export function readBoundedJsonSource<T>(
48
+ req: RequestSource,
49
+ maxBytes: number,
50
+ protoPoisoning: ProtoPoisoning = "reject",
51
+ onResult?: JsonResultContinuation<T>,
52
+ onError?: JsonErrorContinuation<T>,
53
+ ): Promise<unknown | ResponseResult | T> {
54
+ const preDecoded = (req as { [PRE_DECODED_BODY]?: PreDecodedBody })[PRE_DECODED_BODY]
55
+ if (preDecoded !== undefined) {
56
+ return onResult === undefined
57
+ ? Promise.resolve(preDecoded.value)
58
+ : Promise.resolve(preDecoded.value).then(onResult)
59
+ }
60
+
61
+ const raw = ((req as { [RAW_BODY_READERS]?: RequestSource })[RAW_BODY_READERS] ??
62
+ req) as FramedJsonSource
63
+ const declared = headerOf(req, "content-length")
64
+ if (declared !== null) {
65
+ const length = parseContentLength(declared)
66
+ if (length === undefined)
67
+ return onResult === undefined
68
+ ? Promise.resolve(plainError(400, "invalid_content_length"))
69
+ : Promise.resolve(plainError(400, "invalid_content_length")).then(onResult)
70
+ if (length > maxBytes)
71
+ return onResult === undefined
72
+ ? Promise.resolve(plainError(413, "payload_too_large"))
73
+ : Promise.resolve(plainError(413, "payload_too_large")).then(onResult)
74
+
75
+ if (hasTrustedBodyFraming(req)) {
76
+ return raw.json().then(
77
+ (parsed) => {
78
+ try {
79
+ const guarded = guardParsedValue(parsed, protoPoisoning)
80
+ return onResult === undefined ? guarded : onResult(guarded)
81
+ } catch {
82
+ return onResult === undefined
83
+ ? plainError(400, "invalid_json")
84
+ : onResult(plainError(400, "invalid_json"))
85
+ }
86
+ },
87
+ () => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
88
+ )
89
+ }
90
+
91
+ if (headerOf(req, "transfer-encoding") === null) {
92
+ const jsonWithByteLength = raw.jsonWithByteLength
93
+ if (jsonWithByteLength !== undefined) {
94
+ return jsonWithByteLength.call(raw).then(
95
+ ({ value, byteLength }) => {
96
+ if (byteLength > length || byteLength > maxBytes) {
97
+ return onResult === undefined
98
+ ? plainError(413, "payload_too_large")
99
+ : onResult(plainError(413, "payload_too_large"))
100
+ }
101
+ try {
102
+ const guarded = guardParsedValue(value, protoPoisoning)
103
+ return onResult === undefined ? guarded : onResult(guarded)
104
+ } catch {
105
+ return onResult === undefined
106
+ ? plainError(400, "invalid_json")
107
+ : onResult(plainError(400, "invalid_json"))
108
+ }
109
+ },
110
+ () => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
111
+ )
112
+ }
113
+
114
+ const bytesReader = raw.bytes
115
+ if (bytesReader !== undefined) {
116
+ return bytesReader.call(raw).then(
117
+ (bytes) => {
118
+ if (bytes.byteLength > length || bytes.byteLength > maxBytes) {
119
+ return onResult === undefined
120
+ ? plainError(413, "payload_too_large")
121
+ : onResult(plainError(413, "payload_too_large"))
122
+ }
123
+ try {
124
+ const parsed = parseJsonGuarded(TEXT_DECODER.decode(bytes), protoPoisoning)
125
+ return onResult === undefined ? parsed : onResult(parsed)
126
+ } catch {
127
+ return onResult === undefined
128
+ ? plainError(400, "invalid_json")
129
+ : onResult(plainError(400, "invalid_json"))
130
+ }
131
+ },
132
+ () => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
133
+ )
134
+ }
135
+
136
+ return raw.arrayBuffer().then(
137
+ (buffer) => {
138
+ const bytes = new Uint8Array(buffer)
139
+ if (bytes.byteLength > length || bytes.byteLength > maxBytes) {
140
+ return onResult === undefined
141
+ ? plainError(413, "payload_too_large")
142
+ : onResult(plainError(413, "payload_too_large"))
143
+ }
144
+ try {
145
+ const parsed = parseJsonGuarded(TEXT_DECODER.decode(bytes), protoPoisoning)
146
+ return onResult === undefined ? parsed : onResult(parsed)
147
+ } catch {
148
+ return onResult === undefined
149
+ ? plainError(400, "invalid_json")
150
+ : onResult(plainError(400, "invalid_json"))
151
+ }
152
+ },
153
+ () => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
154
+ )
155
+ }
156
+ }
157
+
158
+ const body = raw.body
159
+ const streamed =
160
+ body === null
161
+ ? Promise.resolve<unknown | ResponseResult>(plainError(400, "invalid_json"))
162
+ : drainCapped(body, maxBytes).then((drained) => {
163
+ if (!drained.ok) return plainError(413, "payload_too_large")
164
+ try {
165
+ return parseJsonGuarded(TEXT_DECODER.decode(drained.bytes), protoPoisoning)
166
+ } catch {
167
+ return plainError(400, "invalid_json")
168
+ }
169
+ })
170
+ return onResult === undefined ? streamed : streamed.then(onResult, onError)
171
+ }
172
+
173
+ /** The shared content-type dispatcher around the JSON and urlencoded lanes. */
174
+ export function readBodyFramed<T>(
175
+ source: RequestSource,
176
+ maxBodyBytes: number,
177
+ protoPoisoning: ProtoPoisoning,
178
+ onParsed: (parsed: unknown) => MaybePromise<T>,
179
+ wrapResponse: (response: Response | ResponseResult) => T,
180
+ onError: (err: unknown) => MaybePromise<T>,
181
+ ): Promise<T> {
182
+ const contentType = headerOf(source, "content-type") ?? ""
183
+ if (contentType !== "application/json" && !contentType.includes("application/json")) {
184
+ if (isUrlEncodedForm(contentType)) {
185
+ return readBoundedForm(source, maxBodyBytes).then(
186
+ (form) => (isResponseResult(form) ? wrapResponse(form) : onParsed(form)),
187
+ onError,
188
+ ) as Promise<T>
189
+ }
190
+ return Promise.resolve(wrapResponse(plainError(415, "unsupported_media_type")))
191
+ }
192
+ try {
193
+ return readBoundedJsonSource(
194
+ source,
195
+ maxBodyBytes,
196
+ protoPoisoning,
197
+ (parsed) => (isResponseResult(parsed) ? wrapResponse(parsed) : onParsed(parsed)),
198
+ onError,
199
+ )
200
+ } catch (err) {
201
+ return Promise.resolve(onError(err))
202
+ }
203
+ }