@nifrajs/core 2.14.1 → 3.2.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 (226) hide show
  1. package/README.md +25 -0
  2. package/dist/assurance.d.ts +16 -8
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +33 -0
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/durable-adapters.d.ts.map +1 -1
  7. package/dist/durable-adapters.js +26 -3
  8. package/dist/durable-adapters.js.map +1 -1
  9. package/dist/durable-approval.d.ts +56 -0
  10. package/dist/durable-approval.d.ts.map +1 -0
  11. package/dist/durable-approval.js +233 -0
  12. package/dist/durable-approval.js.map +1 -0
  13. package/dist/durable-effect.d.ts +42 -0
  14. package/dist/durable-effect.d.ts.map +1 -0
  15. package/dist/durable-effect.js +234 -0
  16. package/dist/durable-effect.js.map +1 -0
  17. package/dist/durable-execution.d.ts +8 -346
  18. package/dist/durable-execution.d.ts.map +1 -1
  19. package/dist/durable-execution.js +7 -1150
  20. package/dist/durable-execution.js.map +1 -1
  21. package/dist/durable-saga.d.ts +94 -0
  22. package/dist/durable-saga.d.ts.map +1 -0
  23. package/dist/durable-saga.js +550 -0
  24. package/dist/durable-saga.js.map +1 -0
  25. package/dist/edge-kit.d.ts +27 -0
  26. package/dist/edge-kit.d.ts.map +1 -0
  27. package/dist/edge-kit.js +24 -0
  28. package/dist/edge-kit.js.map +1 -0
  29. package/dist/errors.d.ts +1 -1
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/index.d.ts +23 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +18 -2
  35. package/dist/index.js.map +1 -1
  36. package/dist/internal/durable-approval-protocol.d.ts +1 -1
  37. package/dist/internal/durable-approval-protocol.d.ts.map +1 -1
  38. package/dist/internal/durable-effect-protocol.d.ts +1 -1
  39. package/dist/internal/durable-effect-protocol.d.ts.map +1 -1
  40. package/dist/internal/durable-saga-protocol.d.ts +1 -1
  41. package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
  42. package/dist/internal/durable-shared.d.ts +26 -0
  43. package/dist/internal/durable-shared.d.ts.map +1 -0
  44. package/dist/internal/durable-shared.js +131 -0
  45. package/dist/internal/durable-shared.js.map +1 -0
  46. package/dist/internal/durable-types.d.ts +159 -0
  47. package/dist/internal/durable-types.d.ts.map +1 -0
  48. package/dist/internal/durable-types.js +2 -0
  49. package/dist/internal/durable-types.js.map +1 -0
  50. package/dist/internal/route-compiler.d.ts +61 -0
  51. package/dist/internal/route-compiler.d.ts.map +1 -0
  52. package/dist/internal/route-compiler.js +121 -0
  53. package/dist/internal/route-compiler.js.map +1 -0
  54. package/dist/internal/route-execution.d.ts +17 -9
  55. package/dist/internal/route-execution.d.ts.map +1 -1
  56. package/dist/internal/route-execution.js +4 -18
  57. package/dist/internal/route-execution.js.map +1 -1
  58. package/dist/internal/route-lanes.d.ts +35 -0
  59. package/dist/internal/route-lanes.d.ts.map +1 -0
  60. package/dist/internal/route-lanes.js +150 -0
  61. package/dist/internal/route-lanes.js.map +1 -0
  62. package/dist/internal/route-program.d.ts +67 -0
  63. package/dist/internal/route-program.d.ts.map +1 -0
  64. package/dist/internal/route-program.js +199 -0
  65. package/dist/internal/route-program.js.map +1 -0
  66. package/dist/response-observer.d.ts +25 -0
  67. package/dist/response-observer.d.ts.map +1 -0
  68. package/dist/response-observer.js +40 -0
  69. package/dist/response-observer.js.map +1 -0
  70. package/dist/security/baseline.d.ts +68 -0
  71. package/dist/security/baseline.d.ts.map +1 -0
  72. package/dist/security/baseline.js +118 -0
  73. package/dist/security/baseline.js.map +1 -0
  74. package/dist/server/bare-error-lane.d.ts +26 -0
  75. package/dist/server/bare-error-lane.d.ts.map +1 -0
  76. package/dist/server/bare-error-lane.js +60 -0
  77. package/dist/server/bare-error-lane.js.map +1 -0
  78. package/dist/server/body-lane.d.ts +13 -0
  79. package/dist/server/body-lane.d.ts.map +1 -0
  80. package/dist/server/body-lane.js +134 -0
  81. package/dist/server/body-lane.js.map +1 -0
  82. package/dist/server/body.d.ts +7 -0
  83. package/dist/server/body.d.ts.map +1 -1
  84. package/dist/server/body.js +7 -0
  85. package/dist/server/body.js.map +1 -1
  86. package/dist/server/bridge-markers.d.ts +25 -0
  87. package/dist/server/bridge-markers.d.ts.map +1 -0
  88. package/dist/server/bridge-markers.js +25 -0
  89. package/dist/server/bridge-markers.js.map +1 -0
  90. package/dist/server/context.d.ts +6 -1
  91. package/dist/server/context.d.ts.map +1 -1
  92. package/dist/server/contract.d.ts +40 -14
  93. package/dist/server/contract.d.ts.map +1 -1
  94. package/dist/server/contract.js +7 -0
  95. package/dist/server/contract.js.map +1 -1
  96. package/dist/server/cookies.d.ts.map +1 -1
  97. package/dist/server/cookies.js +16 -2
  98. package/dist/server/cookies.js.map +1 -1
  99. package/dist/server/header-case.d.ts.map +1 -1
  100. package/dist/server/header-case.js +2 -1
  101. package/dist/server/header-case.js.map +1 -1
  102. package/dist/server/http.d.ts +36 -3
  103. package/dist/server/http.d.ts.map +1 -1
  104. package/dist/server/http.js +49 -3
  105. package/dist/server/http.js.map +1 -1
  106. package/dist/server/install.d.ts +6 -8
  107. package/dist/server/install.d.ts.map +1 -1
  108. package/dist/server/install.js +7 -0
  109. package/dist/server/install.js.map +1 -1
  110. package/dist/server/node-outcome-hook.d.ts +2 -1
  111. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  112. package/dist/server/node-outcome-hook.js.map +1 -1
  113. package/dist/server/node-outcome.d.ts +17 -2
  114. package/dist/server/node-outcome.d.ts.map +1 -1
  115. package/dist/server/node-outcome.js +37 -3
  116. package/dist/server/node-outcome.js.map +1 -1
  117. package/dist/server/plugin.d.ts +2 -2
  118. package/dist/server/plugin.d.ts.map +1 -1
  119. package/dist/server/plugin.js +6 -1
  120. package/dist/server/plugin.js.map +1 -1
  121. package/dist/server/query.d.ts +7 -1
  122. package/dist/server/query.d.ts.map +1 -1
  123. package/dist/server/query.js +8 -3
  124. package/dist/server/query.js.map +1 -1
  125. package/dist/server/registry.d.ts +40 -12
  126. package/dist/server/registry.d.ts.map +1 -1
  127. package/dist/server/request-context.d.ts +2 -13
  128. package/dist/server/request-context.d.ts.map +1 -1
  129. package/dist/server/request-context.js +18 -159
  130. package/dist/server/request-context.js.map +1 -1
  131. package/dist/server/respond.d.ts.map +1 -1
  132. package/dist/server/respond.js +58 -4
  133. package/dist/server/respond.js.map +1 -1
  134. package/dist/server/response-contract-lane.d.ts.map +1 -1
  135. package/dist/server/response-contract-lane.js +5 -2
  136. package/dist/server/response-contract-lane.js.map +1 -1
  137. package/dist/server/response-observer-runtime.d.ts +20 -0
  138. package/dist/server/response-observer-runtime.d.ts.map +1 -0
  139. package/dist/server/response-observer-runtime.js +95 -0
  140. package/dist/server/response-observer-runtime.js.map +1 -0
  141. package/dist/server/runtime-core.d.ts +76 -0
  142. package/dist/server/runtime-core.d.ts.map +1 -1
  143. package/dist/server/runtime-core.js +42 -1
  144. package/dist/server/runtime-core.js.map +1 -1
  145. package/dist/server/server-types.d.ts +6 -1
  146. package/dist/server/server-types.d.ts.map +1 -1
  147. package/dist/server/server.d.ts +158 -116
  148. package/dist/server/server.d.ts.map +1 -1
  149. package/dist/server/server.js +707 -844
  150. package/dist/server/server.js.map +1 -1
  151. package/dist/server/validation.d.ts +5 -0
  152. package/dist/server/validation.d.ts.map +1 -0
  153. package/dist/server/validation.js +13 -0
  154. package/dist/server/validation.js.map +1 -0
  155. package/dist/server/websocket.d.ts +20 -4
  156. package/dist/server/websocket.d.ts.map +1 -1
  157. package/dist/server/websocket.js +81 -11
  158. package/dist/server/websocket.js.map +1 -1
  159. package/dist/server/ws-bun.d.ts +12 -4
  160. package/dist/server/ws-bun.d.ts.map +1 -1
  161. package/dist/server/ws-bun.js +50 -16
  162. package/dist/server/ws-bun.js.map +1 -1
  163. package/dist/server/ws-hook.d.ts +3 -2
  164. package/dist/server/ws-hook.d.ts.map +1 -1
  165. package/dist/server.d.ts +3 -2
  166. package/dist/server.d.ts.map +1 -1
  167. package/dist/server.js +2 -1
  168. package/dist/server.js.map +1 -1
  169. package/dist/single-copy-register.d.ts +2 -0
  170. package/dist/single-copy-register.d.ts.map +1 -0
  171. package/dist/single-copy-register.js +24 -0
  172. package/dist/single-copy-register.js.map +1 -0
  173. package/dist/single-copy.d.ts +156 -0
  174. package/dist/single-copy.d.ts.map +1 -0
  175. package/dist/single-copy.js +472 -0
  176. package/dist/single-copy.js.map +1 -0
  177. package/package.json +28 -3
  178. package/src/assurance.ts +58 -1
  179. package/src/durable-adapters.ts +20 -0
  180. package/src/durable-approval.ts +281 -0
  181. package/src/durable-effect.ts +300 -0
  182. package/src/durable-execution.ts +8 -1627
  183. package/src/durable-saga.ts +744 -0
  184. package/src/edge-kit.ts +27 -0
  185. package/src/errors.ts +1 -0
  186. package/src/index.ts +129 -2
  187. package/src/internal/durable-approval-protocol.ts +1 -1
  188. package/src/internal/durable-effect-protocol.ts +1 -1
  189. package/src/internal/durable-saga-protocol.ts +1 -1
  190. package/src/internal/durable-shared.ts +157 -0
  191. package/src/internal/durable-types.ts +194 -0
  192. package/src/internal/route-compiler.ts +234 -0
  193. package/src/internal/route-execution.ts +53 -96
  194. package/src/internal/route-lanes.ts +221 -0
  195. package/src/internal/route-program.ts +404 -0
  196. package/src/response-observer.ts +64 -0
  197. package/src/security/baseline.ts +172 -0
  198. package/src/server/bare-error-lane.ts +77 -0
  199. package/src/server/body-lane.ts +203 -0
  200. package/src/server/body.ts +8 -0
  201. package/src/server/bridge-markers.ts +24 -0
  202. package/src/server/context.ts +6 -1
  203. package/src/server/contract.ts +70 -20
  204. package/src/server/cookies.ts +16 -2
  205. package/src/server/header-case.ts +3 -1
  206. package/src/server/http.ts +53 -3
  207. package/src/server/install.ts +14 -0
  208. package/src/server/node-outcome-hook.ts +2 -1
  209. package/src/server/node-outcome.ts +44 -3
  210. package/src/server/plugin.ts +11 -5
  211. package/src/server/query.ts +10 -4
  212. package/src/server/registry.ts +71 -16
  213. package/src/server/request-context.ts +13 -216
  214. package/src/server/respond.ts +71 -3
  215. package/src/server/response-contract-lane.ts +6 -2
  216. package/src/server/response-observer-runtime.ts +135 -0
  217. package/src/server/runtime-core.ts +124 -1
  218. package/src/server/server-types.ts +6 -1
  219. package/src/server/server.ts +1446 -1485
  220. package/src/server/validation.ts +19 -0
  221. package/src/server/websocket.ts +127 -17
  222. package/src/server/ws-bun.ts +67 -15
  223. package/src/server/ws-hook.ts +3 -2
  224. package/src/server.ts +8 -2
  225. package/src/single-copy-register.ts +24 -0
  226. package/src/single-copy.ts +526 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Registration-time route-lane selection.
3
+ *
4
+ * A route's lane is a property of its registered shape, not of an individual request. Keeping this
5
+ * decision in a small internal module gives the server kernel one vocabulary for bare/body/query/
6
+ * lifecycle execution while preserving the important invariant: this module is never called from the
7
+ * request path. The returned booleans are consumed by the existing compiled execution plan and fused
8
+ * builders; no adapter or extra dispatch hop is introduced at runtime.
9
+ */
10
+ import type { RouteSchema } from "../server/context.ts"
11
+ import type { LifecycleExecutionLane, RouteExecutionLane } from "./route-execution.ts"
12
+
13
+ export interface RouteLaneSelection {
14
+ readonly bare: boolean
15
+ readonly fusedQuery: boolean
16
+ readonly bodyOnly: boolean
17
+ readonly fusedBody: boolean
18
+ readonly lane: RouteExecutionLane
19
+ readonly lifecycleLane: LifecycleExecutionLane
20
+ readonly lifecycleHookLane: "derive-before" | "derive-before-after" | undefined
21
+ readonly fusedLane:
22
+ | "bare"
23
+ | "body"
24
+ | "query"
25
+ | "derive-before"
26
+ | "derive-before-after"
27
+ | "body-derive-before"
28
+ | "body-derive-before-after"
29
+ | undefined
30
+ }
31
+
32
+ export function selectRouteLanes(options: {
33
+ readonly schema: RouteSchema | undefined
34
+ readonly hasIdempotency: boolean
35
+ readonly hasLedger: boolean
36
+ readonly hasResponseContract: boolean
37
+ readonly hasDecorations: boolean
38
+ readonly derives: number
39
+ readonly beforeHandle: number
40
+ readonly afterHandle: number
41
+ readonly onError: number
42
+ readonly around: number
43
+ readonly defaultOnValidationError: boolean
44
+ }): RouteLaneSelection {
45
+ const {
46
+ schema,
47
+ hasIdempotency,
48
+ hasLedger,
49
+ hasResponseContract,
50
+ hasDecorations,
51
+ derives,
52
+ beforeHandle,
53
+ afterHandle,
54
+ onError,
55
+ around,
56
+ defaultOnValidationError,
57
+ } = options
58
+
59
+ const bare =
60
+ schema?.params === undefined &&
61
+ schema?.headers === undefined &&
62
+ schema?.body === undefined &&
63
+ schema?.query === undefined &&
64
+ !hasIdempotency &&
65
+ !hasLedger &&
66
+ !hasResponseContract &&
67
+ derives === 0 &&
68
+ beforeHandle === 0 &&
69
+ afterHandle === 0 &&
70
+ onError === 0
71
+
72
+ // A route whose ONLY lifecycle step is a query schema can fuse too: the parse + validate + handler +
73
+ // respond collapse into one closure with no lifecycle promise on the sync path. The guards mirror the
74
+ // query lane below PLUS everything the fused dispatch skips: around hooks, the idempotency/ledger
75
+ // wrappers, and validation-error recovery (schema or server default) - the fused invalid path is
76
+ // exactly `validationError(issues)`, so any recovery semantics keep the generic lane.
77
+ const fusedQuery =
78
+ !bare &&
79
+ !hasResponseContract &&
80
+ schema?.query !== undefined &&
81
+ schema.body === undefined &&
82
+ schema.params === undefined &&
83
+ schema.headers === undefined &&
84
+ schema.onValidationError === undefined &&
85
+ !defaultOnValidationError &&
86
+ !hasIdempotency &&
87
+ !hasLedger &&
88
+ derives === 0 &&
89
+ beforeHandle === 0 &&
90
+ afterHandle === 0 &&
91
+ onError === 0 &&
92
+ around === 0
93
+
94
+ const bodyOnly =
95
+ !hasResponseContract &&
96
+ schema?.body !== undefined &&
97
+ schema.query === undefined &&
98
+ schema.params === undefined &&
99
+ schema.headers === undefined &&
100
+ derives === 0 &&
101
+ beforeHandle === 0 &&
102
+ afterHandle === 0 &&
103
+ onError === 0
104
+
105
+ const fusedBody =
106
+ !bare &&
107
+ bodyOnly &&
108
+ schema.onValidationError === undefined &&
109
+ !defaultOnValidationError &&
110
+ !hasIdempotency &&
111
+ !hasLedger &&
112
+ around === 0
113
+
114
+ const lane: RouteExecutionLane = bare
115
+ ? "bare"
116
+ : bodyOnly
117
+ ? "body"
118
+ : !hasResponseContract &&
119
+ schema?.body === undefined &&
120
+ schema?.query !== undefined &&
121
+ schema.params === undefined &&
122
+ schema.headers === undefined &&
123
+ derives === 0 &&
124
+ beforeHandle === 0 &&
125
+ afterHandle === 0 &&
126
+ onError === 0
127
+ ? "query"
128
+ : "lifecycle"
129
+
130
+ // Lifecycle routes with no params schema are the common middleware shape: derive/before plus an
131
+ // optional query or body schema. Select their complete validation stage at registration so the
132
+ // request path never re-checks params/body presence. Parameter-schema routes retain the generic
133
+ // lifecycle runner until their more involved recovery matrix is selected explicitly.
134
+ const lifecycleLane: LifecycleExecutionLane =
135
+ lane !== "lifecycle" || schema?.params !== undefined || schema?.headers !== undefined
136
+ ? undefined
137
+ : schema?.body !== undefined
138
+ ? schema.query !== undefined
139
+ ? "body-query"
140
+ : "body"
141
+ : schema?.query !== undefined
142
+ ? "query"
143
+ : "hooks"
144
+
145
+ // The realistic middleware shape is commonly exactly one synchronous-or-async derive followed by
146
+ // one before hook. Keep the generic runner for every route that can observe decorations, after hooks,
147
+ // error hooks, or response contracts; this lane only removes the two per-request hook-loop dispatches
148
+ // and preserves the same async continuations and error handling. A params or headers schema routes
149
+ // back to the generic lane because the fused builder has no `params` / `headers` validation step
150
+ // baked in - adding them would be more code than it saves. A `body` schema is the same story
151
+ // because the fused derive-before / derive-before-after builders don't parse the body: routes with a
152
+ // body schema are routed to the `body-derive-before` / `body-derive-before-after` lane below.
153
+ const lifecycleHookLane =
154
+ lane === "lifecycle" &&
155
+ !hasResponseContract &&
156
+ !hasDecorations &&
157
+ !hasIdempotency &&
158
+ !hasLedger &&
159
+ schema?.onValidationError === undefined &&
160
+ !defaultOnValidationError &&
161
+ schema?.params === undefined &&
162
+ schema?.headers === undefined &&
163
+ schema?.body === undefined &&
164
+ derives === 1 &&
165
+ beforeHandle === 1 &&
166
+ afterHandle <= 1 &&
167
+ onError === 0 &&
168
+ around === 0
169
+ ? afterHandle === 0
170
+ ? "derive-before"
171
+ : "derive-before-after"
172
+ : undefined
173
+
174
+ // Body + lifecycle hooks in a closed shape: a body schema plus the same 1-derive + 1-before +
175
+ // 0-or-1-after + 0-onError shape, with no other body/query/path conflict. The fused closure runs
176
+ // parse → validate → derive → before → handler → after → respond in one function frame, the same
177
+ // win `fusedBody` already gives for the no-hooks body shape.
178
+ const fusedBodyLifecycle =
179
+ lane === "lifecycle" &&
180
+ schema?.body !== undefined &&
181
+ schema?.query === undefined &&
182
+ schema?.params === undefined &&
183
+ schema?.headers === undefined &&
184
+ schema?.onValidationError === undefined &&
185
+ !defaultOnValidationError &&
186
+ !hasResponseContract &&
187
+ !hasDecorations &&
188
+ !hasIdempotency &&
189
+ !hasLedger &&
190
+ derives === 1 &&
191
+ beforeHandle === 1 &&
192
+ afterHandle <= 1 &&
193
+ onError === 0 &&
194
+ around === 0
195
+
196
+ return {
197
+ bare,
198
+ fusedQuery,
199
+ bodyOnly,
200
+ fusedBody,
201
+ lane,
202
+ lifecycleLane,
203
+ lifecycleHookLane,
204
+ fusedLane:
205
+ around === 0
206
+ ? fusedQuery
207
+ ? "query"
208
+ : fusedBody
209
+ ? "body"
210
+ : bare
211
+ ? "bare"
212
+ : lifecycleHookLane !== undefined
213
+ ? (lifecycleHookLane as "derive-before" | "derive-before-after")
214
+ : fusedBodyLifecycle
215
+ ? afterHandle === 0
216
+ ? "body-derive-before"
217
+ : "body-derive-before-after"
218
+ : undefined
219
+ : undefined,
220
+ }
221
+ }
@@ -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
+ }
@@ -0,0 +1,64 @@
1
+ import { INSTALL_RESPONSE_OBSERVER } from "./server/install.ts"
2
+ import {
3
+ createResponseObserverRuntime,
4
+ type ResponseObserverRuntime,
5
+ } from "./server/response-observer-runtime.ts"
6
+ import type { AnyServer } from "./server/server.ts"
7
+
8
+ export type {
9
+ ResponseBodyHook,
10
+ ResponseHeadersHook,
11
+ ResponseObserverMethods,
12
+ ResponseObserverRuntime,
13
+ } from "./server/response-observer-runtime.ts"
14
+
15
+ /** A server shape with the opt-in response observation methods installed. */
16
+ export type ResponseObserverServer<S extends AnyServer> = S &
17
+ import("./server/response-observer-runtime.ts").ResponseObserverMethods
18
+
19
+ /** A plugin that enables response header, body, and raw-response observation methods. */
20
+ export type ResponseObserverPlugin = (<S extends AnyServer>(
21
+ app: S,
22
+ ) => ResponseObserverServer<S>) & { readonly pluginName?: string }
23
+
24
+ /**
25
+ * Enable the response observation methods on a server.
26
+ *
27
+ * The default server does not carry the header, body, or raw-response observation adapters. Apply
28
+ * this plugin before using those methods:
29
+ *
30
+ * ```ts
31
+ * import { responseObserver } from "@nifrajs/core/response-observer"
32
+ * const app = server().use(responseObserver()).onResponseHeaders((headers) => {
33
+ * headers.set("cache-control", "no-store")
34
+ * })
35
+ * ```
36
+ */
37
+ export function responseObserver(): ResponseObserverPlugin {
38
+ const apply = <S extends AnyServer>(app: S): ResponseObserverServer<S> => {
39
+ const install = (app as unknown as Record<symbol, unknown>)[INSTALL_RESPONSE_OBSERVER]
40
+ if (typeof install !== "function") {
41
+ throw new TypeError("[nifra] responseObserver() requires a compatible Server")
42
+ }
43
+ const methods = (install as (runtime: ResponseObserverRuntime) => unknown).call(
44
+ app,
45
+ createResponseObserverRuntime(),
46
+ )
47
+ if (methods === null || typeof methods !== "object") {
48
+ throw new TypeError("[nifra] responseObserver() failed to install its methods")
49
+ }
50
+ Object.assign(app, methods)
51
+ return app as ResponseObserverServer<S>
52
+ }
53
+ return Object.assign(apply, { pluginName: "nifra:response-observer" })
54
+ }
55
+
56
+ /** Attach the opt-in observer runtime to a middleware bundle that uses response observation. */
57
+ export function withResponseObserver<T extends object>(middleware: T): T {
58
+ Object.defineProperty(middleware, INSTALL_RESPONSE_OBSERVER, {
59
+ value: createResponseObserverRuntime(),
60
+ enumerable: false,
61
+ configurable: false,
62
+ })
63
+ return middleware
64
+ }