@nifrajs/core 4.0.0 → 5.0.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 (196) hide show
  1. package/README.md +6 -2
  2. package/dist/assurance.d.ts +71 -5
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +90 -25
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/capabilities.d.ts +6 -0
  7. package/dist/capabilities.d.ts.map +1 -1
  8. package/dist/capabilities.js +52 -0
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/durable-effect.js +1 -1
  11. package/dist/durable-effect.js.map +1 -1
  12. package/dist/durable-saga.d.ts +6 -0
  13. package/dist/durable-saga.d.ts.map +1 -1
  14. package/dist/durable-saga.js +83 -10
  15. package/dist/durable-saga.js.map +1 -1
  16. package/dist/edge-kit.d.ts +7 -2
  17. package/dist/edge-kit.d.ts.map +1 -1
  18. package/dist/edge-kit.js +7 -2
  19. package/dist/edge-kit.js.map +1 -1
  20. package/dist/effect-scope.d.ts.map +1 -1
  21. package/dist/effect-scope.js +16 -5
  22. package/dist/effect-scope.js.map +1 -1
  23. package/dist/errors.d.ts +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js.map +1 -1
  26. package/dist/evidence.d.ts +2 -0
  27. package/dist/evidence.d.ts.map +1 -1
  28. package/dist/evidence.js +9 -1
  29. package/dist/evidence.js.map +1 -1
  30. package/dist/index.d.ts +4 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/internal/durable-shared.d.ts +1 -2
  35. package/dist/internal/durable-shared.d.ts.map +1 -1
  36. package/dist/internal/durable-shared.js +25 -44
  37. package/dist/internal/durable-shared.js.map +1 -1
  38. package/dist/internal/durable-types.d.ts +3 -0
  39. package/dist/internal/durable-types.d.ts.map +1 -1
  40. package/dist/internal/hook-origins.d.ts +67 -0
  41. package/dist/internal/hook-origins.d.ts.map +1 -0
  42. package/dist/internal/hook-origins.js +86 -0
  43. package/dist/internal/hook-origins.js.map +1 -0
  44. package/dist/internal/route-assurance.d.ts +6 -3
  45. package/dist/internal/route-assurance.d.ts.map +1 -1
  46. package/dist/internal/route-assurance.js +29 -14
  47. package/dist/internal/route-assurance.js.map +1 -1
  48. package/dist/internal/route-program.d.ts +1 -1
  49. package/dist/internal/route-program.d.ts.map +1 -1
  50. package/dist/internal/route-program.js +59 -16
  51. package/dist/internal/route-program.js.map +1 -1
  52. package/dist/reconciliation-worker.d.ts.map +1 -1
  53. package/dist/reconciliation-worker.js +12 -2
  54. package/dist/reconciliation-worker.js.map +1 -1
  55. package/dist/reflection.d.ts +53 -2
  56. package/dist/reflection.d.ts.map +1 -1
  57. package/dist/reflection.js +352 -5
  58. package/dist/reflection.js.map +1 -1
  59. package/dist/router/pattern.d.ts +1 -1
  60. package/dist/router/pattern.d.ts.map +1 -1
  61. package/dist/router/pattern.js +6 -3
  62. package/dist/router/pattern.js.map +1 -1
  63. package/dist/security/baseline.d.ts +3 -0
  64. package/dist/security/baseline.d.ts.map +1 -1
  65. package/dist/security/baseline.js +9 -5
  66. package/dist/security/baseline.js.map +1 -1
  67. package/dist/server/auth.d.ts +32 -3
  68. package/dist/server/auth.d.ts.map +1 -1
  69. package/dist/server/auth.js +23 -0
  70. package/dist/server/auth.js.map +1 -1
  71. package/dist/server/body.d.ts +7 -0
  72. package/dist/server/body.d.ts.map +1 -1
  73. package/dist/server/body.js +34 -1
  74. package/dist/server/body.js.map +1 -1
  75. package/dist/server/client-ip.d.ts +3 -0
  76. package/dist/server/client-ip.d.ts.map +1 -1
  77. package/dist/server/client-ip.js.map +1 -1
  78. package/dist/server/context.d.ts +2 -1
  79. package/dist/server/context.d.ts.map +1 -1
  80. package/dist/server/idempotency-lane.d.ts +6 -3
  81. package/dist/server/idempotency-lane.d.ts.map +1 -1
  82. package/dist/server/idempotency-lane.js +38 -17
  83. package/dist/server/idempotency-lane.js.map +1 -1
  84. package/dist/server/install.d.ts +2 -0
  85. package/dist/server/install.d.ts.map +1 -1
  86. package/dist/server/install.js +2 -0
  87. package/dist/server/install.js.map +1 -1
  88. package/dist/server/logger.d.ts.map +1 -1
  89. package/dist/server/logger.js +3 -1
  90. package/dist/server/logger.js.map +1 -1
  91. package/dist/server/methods.d.ts +1 -0
  92. package/dist/server/methods.d.ts.map +1 -1
  93. package/dist/server/methods.js +1 -0
  94. package/dist/server/methods.js.map +1 -1
  95. package/dist/server/node-outcome.d.ts.map +1 -1
  96. package/dist/server/node-outcome.js +29 -11
  97. package/dist/server/node-outcome.js.map +1 -1
  98. package/dist/server/platform-adapters.d.ts.map +1 -1
  99. package/dist/server/platform-adapters.js +3 -2
  100. package/dist/server/platform-adapters.js.map +1 -1
  101. package/dist/server/request-context.d.ts +2 -1
  102. package/dist/server/request-context.d.ts.map +1 -1
  103. package/dist/server/request-context.js +4 -1
  104. package/dist/server/request-context.js.map +1 -1
  105. package/dist/server/respond.d.ts +44 -11
  106. package/dist/server/respond.d.ts.map +1 -1
  107. package/dist/server/respond.js +192 -79
  108. package/dist/server/respond.js.map +1 -1
  109. package/dist/server/response-observer-runtime.d.ts.map +1 -1
  110. package/dist/server/response-observer-runtime.js +10 -41
  111. package/dist/server/response-observer-runtime.js.map +1 -1
  112. package/dist/server/runtime-core.d.ts +2 -0
  113. package/dist/server/runtime-core.d.ts.map +1 -1
  114. package/dist/server/runtime-core.js +2 -0
  115. package/dist/server/runtime-core.js.map +1 -1
  116. package/dist/server/server-types.d.ts +29 -5
  117. package/dist/server/server-types.d.ts.map +1 -1
  118. package/dist/server/server.d.ts +44 -31
  119. package/dist/server/server.d.ts.map +1 -1
  120. package/dist/server/server.js +444 -321
  121. package/dist/server/server.js.map +1 -1
  122. package/dist/server/sse.d.ts +11 -1
  123. package/dist/server/sse.d.ts.map +1 -1
  124. package/dist/server/sse.js +18 -3
  125. package/dist/server/sse.js.map +1 -1
  126. package/dist/server/transport-lane.d.ts +7 -1
  127. package/dist/server/transport-lane.d.ts.map +1 -1
  128. package/dist/server/transport-lane.js.map +1 -1
  129. package/dist/server/webhook.d.ts +5 -2
  130. package/dist/server/webhook.d.ts.map +1 -1
  131. package/dist/server/webhook.js +17 -1
  132. package/dist/server/webhook.js.map +1 -1
  133. package/dist/server/websocket.d.ts +22 -14
  134. package/dist/server/websocket.d.ts.map +1 -1
  135. package/dist/server/websocket.js +21 -0
  136. package/dist/server/websocket.js.map +1 -1
  137. package/dist/server/ws-hook.d.ts +7 -1
  138. package/dist/server/ws-hook.d.ts.map +1 -1
  139. package/dist/server.d.ts +3 -2
  140. package/dist/server.d.ts.map +1 -1
  141. package/dist/server.js +2 -1
  142. package/dist/server.js.map +1 -1
  143. package/dist/single-copy.d.ts +6 -0
  144. package/dist/single-copy.d.ts.map +1 -1
  145. package/dist/single-copy.js +20 -1
  146. package/dist/single-copy.js.map +1 -1
  147. package/dist/tool-contract.d.ts.map +1 -1
  148. package/dist/tool-contract.js +71 -40
  149. package/dist/tool-contract.js.map +1 -1
  150. package/dist/ws.d.ts.map +1 -1
  151. package/dist/ws.js +3 -1
  152. package/dist/ws.js.map +1 -1
  153. package/package.json +1 -1
  154. package/src/assurance.ts +180 -31
  155. package/src/capabilities.ts +59 -0
  156. package/src/durable-effect.ts +1 -1
  157. package/src/durable-saga.ts +94 -10
  158. package/src/edge-kit.ts +7 -2
  159. package/src/effect-scope.ts +15 -5
  160. package/src/errors.ts +1 -0
  161. package/src/evidence.ts +12 -3
  162. package/src/index.ts +6 -1
  163. package/src/internal/durable-shared.ts +25 -37
  164. package/src/internal/durable-types.ts +3 -0
  165. package/src/internal/hook-origins.ts +153 -0
  166. package/src/internal/route-assurance.ts +32 -18
  167. package/src/internal/route-program.ts +98 -44
  168. package/src/reconciliation-worker.ts +10 -2
  169. package/src/reflection.ts +455 -4
  170. package/src/router/pattern.ts +6 -3
  171. package/src/security/baseline.ts +10 -5
  172. package/src/server/auth.ts +45 -3
  173. package/src/server/body.ts +34 -1
  174. package/src/server/client-ip.ts +3 -0
  175. package/src/server/context.ts +2 -1
  176. package/src/server/idempotency-lane.ts +64 -22
  177. package/src/server/install.ts +3 -0
  178. package/src/server/logger.ts +3 -1
  179. package/src/server/methods.ts +1 -0
  180. package/src/server/node-outcome.ts +34 -12
  181. package/src/server/platform-adapters.ts +3 -2
  182. package/src/server/request-context.ts +5 -0
  183. package/src/server/respond.ts +222 -69
  184. package/src/server/response-observer-runtime.ts +16 -38
  185. package/src/server/runtime-core.ts +2 -0
  186. package/src/server/server-types.ts +29 -5
  187. package/src/server/server.ts +659 -530
  188. package/src/server/sse.ts +84 -55
  189. package/src/server/transport-lane.ts +7 -1
  190. package/src/server/webhook.ts +22 -3
  191. package/src/server/websocket.ts +44 -5
  192. package/src/server/ws-hook.ts +5 -0
  193. package/src/server.ts +5 -0
  194. package/src/single-copy.ts +20 -1
  195. package/src/tool-contract.ts +67 -39
  196. package/src/ws.ts +4 -0
@@ -8,13 +8,17 @@
8
8
  */
9
9
 
10
10
  import type { StandardSchemaV1 } from "../schema/standard.ts"
11
+ import { markAuthenticatedRequest } from "../server/auth.ts"
11
12
  import { pathnameOf } from "../server/http.ts"
13
+ import { queryObjectOf } from "../server/query.ts"
12
14
  import type { Registry } from "../server/registry.ts"
13
15
  import {
16
+ CONTEXT_SEARCH,
14
17
  CONTEXT_SET,
15
18
  EMPTY_RESPONSE_CONTROLS,
16
19
  isResponseResult,
17
20
  type ResponseResult,
21
+ requestOf,
18
22
  status,
19
23
  } from "../server/runtime-core.ts"
20
24
  import type { CtxSet, MaybePromise, RawContext, RequestSource, Server } from "../server/server.ts"
@@ -136,14 +140,14 @@ interface RouteProgramRuntime {
136
140
  entry: RouteEntry,
137
141
  error: unknown,
138
142
  ctx: RawContext,
139
- finalize: (result: unknown, set: CtxSet) => T,
143
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
140
144
  wrapResponse: (response: Response | ResponseResult) => T,
141
145
  ): MaybePromise<T>
142
146
  finishProgramResult<T>(
143
147
  entry: RouteEntry,
144
148
  ctx: RawContext,
145
149
  result: unknown,
146
- finalize: (result: unknown, set: CtxSet) => T,
150
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
147
151
  wrapResponse: (response: Response | ResponseResult) => T,
148
152
  ): MaybePromise<T>
149
153
  }
@@ -156,6 +160,22 @@ function isEarly(value: unknown): value is Response | ResponseResult {
156
160
  return value instanceof Response || isResponseResult(value)
157
161
  }
158
162
 
163
+ /** A hook's early reply. A `status(...)` or `c.json(...)` one is held to the route's response
164
+ * contract like the handler's. */
165
+ function finishEarly<T>(
166
+ host: RouteProgramRuntime,
167
+ entry: RouteEntry,
168
+ ctx: RawContext,
169
+ value: unknown,
170
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
171
+ wrapResponse: (response: Response | ResponseResult) => T,
172
+ ): ProgramResult<T> {
173
+ return entry.responseContract !== undefined &&
174
+ (value === ctx.jsonReply || isResponseResult(value))
175
+ ? host.finishProgramResult(entry, ctx, value, finalize, wrapResponse)
176
+ : finalize(value, responseSet(ctx), ctx)
177
+ }
178
+
159
179
  function authHeadersOf(source: RequestSource): import("../server/auth.ts").AuthHeaders {
160
180
  return Object.freeze({
161
181
  get: (name: string) => source.headers.get(name),
@@ -166,15 +186,38 @@ function authHeadersOf(source: RequestSource): import("../server/auth.ts").AuthH
166
186
  })
167
187
  }
168
188
 
189
+ /** A fresh parse of `search`, frozen with its arrays, so a stage never holds what validation reads. */
190
+ function frozenQueryOf(search: string): Readonly<Record<string, string | readonly string[]>> {
191
+ const query = queryObjectOf(search)
192
+ for (const key in query) {
193
+ const value = query[key]
194
+ if (Array.isArray(value)) Object.freeze(value)
195
+ }
196
+ return Object.freeze(query)
197
+ }
198
+
169
199
  function authenticationInputOf(
170
200
  source: RequestSource,
171
201
  ctx: RawContext,
172
202
  ): import("../server/auth.ts").AuthenticationInput<unknown> {
203
+ let query: Readonly<Record<string, string | readonly string[]>> | undefined
204
+ // Accessors, so a stage that reads neither the query nor the request costs nothing for them; the
205
+ // search is the context's own, the one `c.query` and its validation parse.
173
206
  return Object.freeze({
174
207
  method: source.method,
175
208
  pathname: pathnameOf(source.url),
176
209
  headers: authHeadersOf(source),
177
210
  params: Object.freeze({ ...ctx.params }),
211
+ get search() {
212
+ return ctx[CONTEXT_SEARCH]
213
+ },
214
+ get query() {
215
+ query ??= frozenQueryOf(ctx[CONTEXT_SEARCH])
216
+ return query
217
+ },
218
+ get request() {
219
+ return ctx.req
220
+ },
178
221
  env: ctx.env,
179
222
  clientIp: ctx.clientIp,
180
223
  signal: ctx.signal,
@@ -206,7 +249,7 @@ function runAuthenticationStages<T>(
206
249
  program: RouteProgram,
207
250
  source: RequestSource,
208
251
  ctx: RawContext,
209
- finalize: (result: unknown, set: CtxSet) => T,
252
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
210
253
  wrapResponse: (response: Response | ResponseResult) => T,
211
254
  start = 0,
212
255
  validationComplete = false,
@@ -245,24 +288,31 @@ function runAuthenticationStages<T>(
245
288
  try {
246
289
  outcome = stage.run(input)
247
290
  } catch {
248
- return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx))
291
+ return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx), ctx)
249
292
  }
250
293
  if (outcome instanceof Promise) {
251
294
  return outcome.then(
252
- (settled) =>
253
- finishAuthentication(
254
- host,
255
- entry,
256
- program,
257
- source,
258
- ctx,
259
- finalize,
260
- wrapResponse,
261
- settled,
262
- start + 1,
263
- validationComplete,
264
- ),
265
- () => finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx)),
295
+ (settled) => {
296
+ // The rest of the program runs inside this callback: a derive or handler that throws
297
+ // synchronously would otherwise reject the request instead of reaching onError.
298
+ try {
299
+ return finishAuthentication(
300
+ host,
301
+ entry,
302
+ program,
303
+ source,
304
+ ctx,
305
+ finalize,
306
+ wrapResponse,
307
+ settled,
308
+ start + 1,
309
+ validationComplete,
310
+ )
311
+ } catch (error) {
312
+ return host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
313
+ }
314
+ },
315
+ () => finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx), ctx),
266
316
  )
267
317
  }
268
318
  return finishAuthentication(
@@ -285,7 +335,7 @@ function finishAuthentication<T>(
285
335
  program: RouteProgram,
286
336
  source: RequestSource,
287
337
  ctx: RawContext,
288
- finalize: (result: unknown, set: CtxSet) => T,
338
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
289
339
  wrapResponse: (response: Response | ResponseResult) => T,
290
340
  result: import("../server/auth.ts").AuthenticationResult<unknown>,
291
341
  next: number,
@@ -296,24 +346,24 @@ function finishAuthentication<T>(
296
346
  typeof result !== "object" ||
297
347
  (result.kind !== "authenticated" && result.kind !== "rejected")
298
348
  ) {
299
- return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx))
349
+ return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx), ctx)
300
350
  }
301
351
  if (result.kind === "rejected") {
302
352
  if (!validAuthenticationFailureReason(result.reason)) {
303
- return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx))
353
+ return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx), ctx)
304
354
  }
305
355
  if (
306
356
  result.response !== undefined &&
307
357
  !(result.response instanceof Response) &&
308
358
  !isResponseResult(result.response)
309
359
  ) {
310
- return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx))
360
+ return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx), ctx)
311
361
  }
312
362
  const response = result.response ?? defaultAuthenticationResponse(result.reason)
313
- return finalize(response, responseSet(ctx))
363
+ return finalize(response, responseSet(ctx), ctx)
314
364
  }
315
365
  if (result.principal === null || result.principal === undefined) {
316
- return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx))
366
+ return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx), ctx)
317
367
  }
318
368
  Object.defineProperty(ctx, "principal", {
319
369
  value: result.principal,
@@ -321,6 +371,7 @@ function finishAuthentication<T>(
321
371
  configurable: true,
322
372
  writable: false,
323
373
  })
374
+ markAuthenticatedRequest(requestOf(source))
324
375
  return runAuthenticationStages(
325
376
  host,
326
377
  entry,
@@ -341,7 +392,7 @@ function runFastAuthenticatedProgram<T>(
341
392
  program: RouteProgram,
342
393
  source: RequestSource,
343
394
  ctx: RawContext,
344
- finalize: (result: unknown, set: CtxSet) => T,
395
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
345
396
  wrapResponse: (response: Response | ResponseResult) => T,
346
397
  validationIndex: number,
347
398
  ): ProgramResult<T> {
@@ -375,7 +426,7 @@ async function continueFastAuthenticatedProgram<T>(
375
426
  program: RouteProgram,
376
427
  source: RequestSource,
377
428
  ctx: RawContext,
378
- finalize: (result: unknown, set: CtxSet) => T,
429
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
379
430
  wrapResponse: (response: Response | ResponseResult) => T,
380
431
  validationIndex: number,
381
432
  pending: Promise<unknown>,
@@ -403,7 +454,7 @@ function runFastAuthenticatedHandler<T>(
403
454
  entry: RouteEntry,
404
455
  program: RouteProgram,
405
456
  ctx: RawContext,
406
- finalize: (result: unknown, set: CtxSet) => T,
457
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
407
458
  wrapResponse: (response: Response | ResponseResult) => T,
408
459
  ): ProgramResult<T> {
409
460
  let result: unknown
@@ -427,7 +478,7 @@ function runValidationBeforeAuthentication<T>(
427
478
  program: RouteProgram,
428
479
  source: RequestSource,
429
480
  ctx: RawContext,
430
- finalize: (result: unknown, set: CtxSet) => T,
481
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
431
482
  wrapResponse: (response: Response | ResponseResult) => T,
432
483
  validationIndex = 0,
433
484
  ): ProgramResult<T> {
@@ -461,7 +512,7 @@ async function continueValidationBeforeAuthentication<T>(
461
512
  program: RouteProgram,
462
513
  source: RequestSource,
463
514
  ctx: RawContext,
464
- finalize: (result: unknown, set: CtxSet) => T,
515
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
465
516
  wrapResponse: (response: Response | ResponseResult) => T,
466
517
  validationIndex: number,
467
518
  pending: Promise<unknown>,
@@ -490,7 +541,7 @@ function runValidatedProgram<T>(
490
541
  program: RouteProgram,
491
542
  source: RequestSource,
492
543
  ctx: RawContext,
493
- finalize: (result: unknown, set: CtxSet) => T,
544
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
494
545
  wrapResponse: (response: Response | ResponseResult) => T,
495
546
  ): ProgramResult<T> {
496
547
  if (program.hasDecorations) Object.assign(ctx, program.decorations)
@@ -506,7 +557,7 @@ async function runAuthBeforeValidation<T>(
506
557
  program: RouteProgram,
507
558
  source: RequestSource,
508
559
  ctx: RawContext,
509
- finalize: (result: unknown, set: CtxSet) => T,
560
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
510
561
  wrapResponse: (response: Response | ResponseResult) => T,
511
562
  ): Promise<T> {
512
563
  try {
@@ -515,14 +566,15 @@ async function runAuthBeforeValidation<T>(
515
566
  for (const derive of program.derives) {
516
567
  const result = derive(ctx)
517
568
  const settled = result instanceof Promise ? await result : result
518
- if (isEarly(settled)) return finalize(settled, responseSet(ctx))
569
+ if (isEarly(settled)) return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
519
570
  Object.assign(ctx, settled)
520
571
  }
521
572
 
522
573
  for (const before of program.beforeHandle) {
523
574
  const result = before(ctx)
524
575
  const settled = result instanceof Promise ? await result : result
525
- if (settled !== undefined) return finalize(settled, responseSet(ctx))
576
+ if (settled !== undefined)
577
+ return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
526
578
  }
527
579
 
528
580
  for (const stage of program.validationStages) {
@@ -559,7 +611,7 @@ export function executeRouteProgram<T, R extends Registry, Ctx>(
559
611
  program: RouteProgram,
560
612
  source: RequestSource,
561
613
  ctx: RawContext,
562
- finalize: (result: unknown, set: CtxSet) => T,
614
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
563
615
  wrapResponse: (response: Response | ResponseResult) => T,
564
616
  ): ProgramResult<T> {
565
617
  const host = runtime as unknown as RouteProgramRuntime
@@ -594,7 +646,7 @@ function runSync<T>(
594
646
  program: RouteProgram,
595
647
  source: RequestSource,
596
648
  ctx: RawContext,
597
- finalize: (result: unknown, set: CtxSet) => T,
649
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
598
650
  wrapResponse: (response: Response | ResponseResult) => T,
599
651
  validationIndex: number,
600
652
  initial: unknown,
@@ -631,7 +683,7 @@ function runHooksSync<T>(
631
683
  program: RouteProgram,
632
684
  source: RequestSource,
633
685
  ctx: RawContext,
634
- finalize: (result: unknown, set: CtxSet) => T,
686
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
635
687
  wrapResponse: (response: Response | ResponseResult) => T,
636
688
  deriveIndex: number,
637
689
  beforeIndex: number,
@@ -654,7 +706,7 @@ function runHooksSync<T>(
654
706
  initial,
655
707
  )
656
708
  }
657
- if (isEarly(outcome)) return finalize(outcome, responseSet(ctx))
709
+ if (isEarly(outcome)) return finishEarly(host, entry, ctx, outcome, finalize, wrapResponse)
658
710
  Object.assign(ctx, outcome)
659
711
  }
660
712
  for (let i = beforeIndex; i < program.beforeHandle.length; i++) {
@@ -673,7 +725,7 @@ function runHooksSync<T>(
673
725
  initial,
674
726
  )
675
727
  }
676
- if (outcome !== undefined) return finalize(outcome, responseSet(ctx))
728
+ if (outcome !== undefined) return finishEarly(host, entry, ctx, outcome, finalize, wrapResponse)
677
729
  }
678
730
 
679
731
  const output = program.handler(ctx)
@@ -719,7 +771,7 @@ async function continueProgram<T>(
719
771
  program: RouteProgram,
720
772
  source: RequestSource,
721
773
  ctx: RawContext,
722
- finalize: (result: unknown, set: CtxSet) => T,
774
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
723
775
  wrapResponse: (response: Response | ResponseResult) => T,
724
776
  pendingPhase: PendingPhase,
725
777
  pending: Promise<unknown>,
@@ -731,10 +783,11 @@ async function continueProgram<T>(
731
783
  if (pendingPhase.kind === "validation") {
732
784
  if (settled !== undefined) return wrapResponse(settled as Response | ResponseResult)
733
785
  } else if (pendingPhase.kind === "derive") {
734
- if (isEarly(settled)) return finalize(settled, responseSet(ctx))
786
+ if (isEarly(settled)) return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
735
787
  Object.assign(ctx, settled)
736
788
  } else if (pendingPhase.kind === "before") {
737
- if (settled !== undefined) return finalize(settled, responseSet(ctx))
789
+ if (settled !== undefined)
790
+ return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
738
791
  } else if (pendingPhase.kind === "handler") {
739
792
  result = settled
740
793
  } else {
@@ -762,7 +815,7 @@ async function runAsyncStages<T>(
762
815
  program: RouteProgram,
763
816
  source: RequestSource,
764
817
  ctx: RawContext,
765
- finalize: (result: unknown, set: CtxSet) => T,
818
+ finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
766
819
  wrapResponse: (response: Response | ResponseResult) => T,
767
820
  pendingPhase: PendingPhase,
768
821
  result: unknown,
@@ -786,7 +839,7 @@ async function runAsyncStages<T>(
786
839
  for (let i = deriveStart; i < program.derives.length; i++) {
787
840
  const outcome = program.derives[i]!(ctx)
788
841
  const settled = outcome instanceof Promise ? await outcome : outcome
789
- if (isEarly(settled)) return finalize(settled, responseSet(ctx))
842
+ if (isEarly(settled)) return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
790
843
  Object.assign(ctx, settled)
791
844
  }
792
845
  }
@@ -800,7 +853,8 @@ async function runAsyncStages<T>(
800
853
  for (let i = beforeStart; i < program.beforeHandle.length; i++) {
801
854
  const outcome = program.beforeHandle[i]!(ctx)
802
855
  const settled = outcome instanceof Promise ? await outcome : outcome
803
- if (settled !== undefined) return finalize(settled, responseSet(ctx))
856
+ if (settled !== undefined)
857
+ return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
804
858
  }
805
859
  const output = program.handler(ctx)
806
860
  result = output instanceof Promise ? await output : output
@@ -116,14 +116,22 @@ async function boundedMap<T>(
116
116
  run: (value: T) => Promise<void>,
117
117
  ): Promise<void> {
118
118
  let next = 0
119
+ let failure: { readonly error: unknown } | undefined
119
120
  const worker = async (): Promise<void> => {
120
- for (;;) {
121
+ while (failure === undefined) {
121
122
  const index = next++
122
123
  if (index >= values.length) return
123
- await run(values[index] as T)
124
+ try {
125
+ await run(values[index] as T)
126
+ } catch (error) {
127
+ failure ??= { error }
128
+ }
124
129
  }
125
130
  }
131
+ // A failure starts no new work but lets every started handler settle: the caller releases the
132
+ // lease next, and a handler still running then overlaps the worker that takes the lease over.
126
133
  await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, () => worker()))
134
+ if (failure !== undefined) throw failure.error
127
135
  }
128
136
 
129
137
  export async function runReconciliationWorker<Finding>(