@nifrajs/core 2.13.0 → 3.1.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 (177) hide show
  1. package/README.md +4 -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/binary.d.ts +32 -0
  7. package/dist/binary.d.ts.map +1 -1
  8. package/dist/binary.js +17 -0
  9. package/dist/binary.js.map +1 -1
  10. package/dist/durable-adapters.d.ts.map +1 -1
  11. package/dist/durable-adapters.js +21 -50
  12. package/dist/durable-adapters.js.map +1 -1
  13. package/dist/durable-execution.d.ts.map +1 -1
  14. package/dist/durable-execution.js +49 -69
  15. package/dist/durable-execution.js.map +1 -1
  16. package/dist/edge-kit.d.ts +27 -0
  17. package/dist/edge-kit.d.ts.map +1 -0
  18. package/dist/edge-kit.js +24 -0
  19. package/dist/edge-kit.js.map +1 -0
  20. package/dist/errors.d.ts +1 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/internal/durable-approval-protocol.d.ts +13 -0
  28. package/dist/internal/durable-approval-protocol.d.ts.map +1 -0
  29. package/dist/internal/durable-approval-protocol.js +66 -0
  30. package/dist/internal/durable-approval-protocol.js.map +1 -0
  31. package/dist/internal/durable-effect-protocol.d.ts +14 -0
  32. package/dist/internal/durable-effect-protocol.d.ts.map +1 -0
  33. package/dist/internal/durable-effect-protocol.js +16 -0
  34. package/dist/internal/durable-effect-protocol.js.map +1 -0
  35. package/dist/internal/durable-saga-protocol.d.ts +8 -0
  36. package/dist/internal/durable-saga-protocol.d.ts.map +1 -0
  37. package/dist/internal/durable-saga-protocol.js +21 -0
  38. package/dist/internal/durable-saga-protocol.js.map +1 -0
  39. package/dist/internal/route-catalog.d.ts +3 -0
  40. package/dist/internal/route-catalog.d.ts.map +1 -1
  41. package/dist/internal/route-catalog.js +5 -0
  42. package/dist/internal/route-catalog.js.map +1 -1
  43. package/dist/internal/route-execution.d.ts +11 -5
  44. package/dist/internal/route-execution.d.ts.map +1 -1
  45. package/dist/internal/route-execution.js +2 -1
  46. package/dist/internal/route-execution.js.map +1 -1
  47. package/dist/response-observer.d.ts +25 -0
  48. package/dist/response-observer.d.ts.map +1 -0
  49. package/dist/response-observer.js +40 -0
  50. package/dist/response-observer.js.map +1 -0
  51. package/dist/security/baseline.d.ts +68 -0
  52. package/dist/security/baseline.d.ts.map +1 -0
  53. package/dist/security/baseline.js +118 -0
  54. package/dist/security/baseline.js.map +1 -0
  55. package/dist/server/bare-error-lane.d.ts +26 -0
  56. package/dist/server/bare-error-lane.d.ts.map +1 -0
  57. package/dist/server/bare-error-lane.js +60 -0
  58. package/dist/server/bare-error-lane.js.map +1 -0
  59. package/dist/server/body.d.ts +7 -0
  60. package/dist/server/body.d.ts.map +1 -1
  61. package/dist/server/body.js +7 -0
  62. package/dist/server/body.js.map +1 -1
  63. package/dist/server/bridge-markers.d.ts +25 -0
  64. package/dist/server/bridge-markers.d.ts.map +1 -0
  65. package/dist/server/bridge-markers.js +25 -0
  66. package/dist/server/bridge-markers.js.map +1 -0
  67. package/dist/server/context.d.ts +6 -1
  68. package/dist/server/context.d.ts.map +1 -1
  69. package/dist/server/header-case.d.ts.map +1 -1
  70. package/dist/server/header-case.js +2 -1
  71. package/dist/server/header-case.js.map +1 -1
  72. package/dist/server/http.d.ts +36 -3
  73. package/dist/server/http.d.ts.map +1 -1
  74. package/dist/server/http.js +49 -3
  75. package/dist/server/http.js.map +1 -1
  76. package/dist/server/install.d.ts +6 -8
  77. package/dist/server/install.d.ts.map +1 -1
  78. package/dist/server/install.js +7 -0
  79. package/dist/server/install.js.map +1 -1
  80. package/dist/server/node-outcome-hook.d.ts +2 -1
  81. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  82. package/dist/server/node-outcome-hook.js.map +1 -1
  83. package/dist/server/node-outcome.d.ts +17 -2
  84. package/dist/server/node-outcome.d.ts.map +1 -1
  85. package/dist/server/node-outcome.js +37 -3
  86. package/dist/server/node-outcome.js.map +1 -1
  87. package/dist/server/plugin.d.ts +1 -1
  88. package/dist/server/plugin.d.ts.map +1 -1
  89. package/dist/server/query.d.ts +7 -1
  90. package/dist/server/query.d.ts.map +1 -1
  91. package/dist/server/query.js +8 -3
  92. package/dist/server/query.js.map +1 -1
  93. package/dist/server/registry.d.ts +11 -2
  94. package/dist/server/registry.d.ts.map +1 -1
  95. package/dist/server/request-context.d.ts +17 -4
  96. package/dist/server/request-context.d.ts.map +1 -1
  97. package/dist/server/request-context.js +82 -45
  98. package/dist/server/request-context.js.map +1 -1
  99. package/dist/server/respond.d.ts.map +1 -1
  100. package/dist/server/respond.js +19 -2
  101. package/dist/server/respond.js.map +1 -1
  102. package/dist/server/response-contract-lane.d.ts.map +1 -1
  103. package/dist/server/response-contract-lane.js +5 -2
  104. package/dist/server/response-contract-lane.js.map +1 -1
  105. package/dist/server/response-observer-runtime.d.ts +20 -0
  106. package/dist/server/response-observer-runtime.d.ts.map +1 -0
  107. package/dist/server/response-observer-runtime.js +95 -0
  108. package/dist/server/response-observer-runtime.js.map +1 -0
  109. package/dist/server/runtime-core.d.ts +60 -0
  110. package/dist/server/runtime-core.d.ts.map +1 -1
  111. package/dist/server/runtime-core.js +65 -1
  112. package/dist/server/runtime-core.js.map +1 -1
  113. package/dist/server/server-types.d.ts +18 -1
  114. package/dist/server/server-types.d.ts.map +1 -1
  115. package/dist/server/server.d.ts +89 -48
  116. package/dist/server/server.d.ts.map +1 -1
  117. package/dist/server/server.js +397 -276
  118. package/dist/server/server.js.map +1 -1
  119. package/dist/server/websocket.d.ts +20 -4
  120. package/dist/server/websocket.d.ts.map +1 -1
  121. package/dist/server/websocket.js +81 -11
  122. package/dist/server/websocket.js.map +1 -1
  123. package/dist/server/ws-bun.d.ts +1 -1
  124. package/dist/server/ws-bun.d.ts.map +1 -1
  125. package/dist/server/ws-bun.js +12 -8
  126. package/dist/server/ws-bun.js.map +1 -1
  127. package/dist/server.d.ts +2 -1
  128. package/dist/server.d.ts.map +1 -1
  129. package/dist/server.js +2 -1
  130. package/dist/server.js.map +1 -1
  131. package/dist/single-copy-register.d.ts +2 -0
  132. package/dist/single-copy-register.d.ts.map +1 -0
  133. package/dist/single-copy-register.js +24 -0
  134. package/dist/single-copy-register.js.map +1 -0
  135. package/dist/single-copy.d.ts +156 -0
  136. package/dist/single-copy.d.ts.map +1 -0
  137. package/dist/single-copy.js +470 -0
  138. package/dist/single-copy.js.map +1 -0
  139. package/package.json +24 -2
  140. package/src/assurance.ts +58 -1
  141. package/src/binary.ts +35 -0
  142. package/src/durable-adapters.ts +26 -51
  143. package/src/durable-execution.ts +46 -88
  144. package/src/edge-kit.ts +27 -0
  145. package/src/errors.ts +1 -0
  146. package/src/index.ts +1 -1
  147. package/src/internal/durable-approval-protocol.ts +92 -0
  148. package/src/internal/durable-effect-protocol.ts +37 -0
  149. package/src/internal/durable-saga-protocol.ts +55 -0
  150. package/src/internal/route-catalog.ts +6 -0
  151. package/src/internal/route-execution.ts +23 -16
  152. package/src/response-observer.ts +64 -0
  153. package/src/security/baseline.ts +172 -0
  154. package/src/server/bare-error-lane.ts +76 -0
  155. package/src/server/body.ts +8 -0
  156. package/src/server/bridge-markers.ts +24 -0
  157. package/src/server/context.ts +6 -1
  158. package/src/server/header-case.ts +3 -1
  159. package/src/server/http.ts +53 -3
  160. package/src/server/install.ts +14 -0
  161. package/src/server/node-outcome-hook.ts +2 -1
  162. package/src/server/node-outcome.ts +44 -3
  163. package/src/server/plugin.ts +1 -1
  164. package/src/server/query.ts +10 -4
  165. package/src/server/registry.ts +19 -4
  166. package/src/server/request-context.ts +106 -51
  167. package/src/server/respond.ts +26 -1
  168. package/src/server/response-contract-lane.ts +6 -2
  169. package/src/server/response-observer-runtime.ts +135 -0
  170. package/src/server/runtime-core.ts +98 -1
  171. package/src/server/server-types.ts +18 -1
  172. package/src/server/server.ts +607 -376
  173. package/src/server/websocket.ts +127 -17
  174. package/src/server/ws-bun.ts +26 -7
  175. package/src/server.ts +2 -1
  176. package/src/single-copy-register.ts +24 -0
  177. package/src/single-copy.ts +524 -0
@@ -5,12 +5,13 @@
5
5
  * its values) so `request-context`, `respond`, `node-outcome`, and the kernel form a cycle-free graph.
6
6
  */
7
7
  import { createUnboundedRequestBudget, type RequestBudget } from "../budget.ts"
8
+ import { NODE_BRIDGE_MARKER_KEYS } from "./bridge-markers.ts"
8
9
  import type { CtxSet, RequestSource } from "./server.ts"
9
10
 
10
11
  /** A handler returns a `Response` (used as-is) or any value (serialized to JSON). */
11
12
  export type HandlerResult = Response | unknown
12
13
 
13
- export const RESPONSE_RESULT = Symbol.for("nifra.response.result")
14
+ export const RESPONSE_RESULT = Symbol.for(NODE_BRIDGE_MARKER_KEYS.responseResult)
14
15
  export const CONTEXT_SET = Symbol("nifra.context.set")
15
16
  export const CONTEXT_SEARCH = Symbol("nifra.context.search")
16
17
 
@@ -28,6 +29,28 @@ export interface PreDecodedBody {
28
29
  readonly value: unknown
29
30
  }
30
31
 
32
+ /**
33
+ * A response described as plain data - the status, any headers of its own, and a body still in value
34
+ * form. A `ResponseResult` carrying one is rendered on the SAME lane a handler's plain return takes:
35
+ * `JSON.stringify` straight into the node writer's `kind: "json"` outcome, or the web lane's prebuilt
36
+ * JSON init. No `Response` is constructed anywhere on the node path.
37
+ *
38
+ * That matters because building one is the dominant cost of answering early. Measured on the rig, one
39
+ * server per shape, all five answering the same 401: `c.set.status` + a plain object 82802 req/s,
40
+ * `beforeHandle` returning that same object 81819, `return new Response(...)` 49668, `throw new
41
+ * Response(...)` 47505, the same throw from a `derive` 48746. The `Response` costs 40%; the throw
42
+ * around it costs another 4%; unwinding a lifecycle stage instead of a handler costs nothing
43
+ * measurable. So the fix for a slow rejection is not a faster throw - it is not allocating the
44
+ * `Response`.
45
+ */
46
+ export interface PlainRender {
47
+ readonly status: number
48
+ /** Headers belonging to this render. They win over anything the request left in `c.set.headers`. */
49
+ readonly headers?: Readonly<Record<string, string>>
50
+ /** The body as a value, serialized by the lane that renders it. `undefined` means no body. */
51
+ readonly body: unknown
52
+ }
53
+
31
54
  export interface ResponseResult {
32
55
  readonly [RESPONSE_RESULT]: true
33
56
  toResponse(): Response
@@ -36,6 +59,9 @@ export interface ResponseResult {
36
59
  readonly headers: Readonly<Record<string, string | readonly string[]>> | undefined
37
60
  readonly body: string | Uint8Array
38
61
  }
62
+ /** Present when this result can be rendered as plain data - see {@link PlainRender}. Every lane
63
+ * checks it before `toNodeBody`/`toResponse`, so a carrier that has one never builds a `Response`. */
64
+ readonly plain?: PlainRender
39
65
  }
40
66
 
41
67
  export function isResponseResult(value: unknown): value is ResponseResult {
@@ -47,6 +73,77 @@ export function isResponseResult(value: unknown): value is ResponseResult {
47
73
  )
48
74
  }
49
75
 
76
+ /**
77
+ * The headers a plain render ships: its own on top of whatever the request left in `c.set.headers`,
78
+ * so an ambient header (a request id, say) survives an early exit the way it survives an ordinary
79
+ * return, and a header named at the exit site still wins.
80
+ *
81
+ * Always a fresh object when the render has headers of its own: the node writers mutate the record
82
+ * they are handed (content-type, content-length, cookies), and a `status(...)` value is commonly
83
+ * hoisted to module scope and answered from on every request.
84
+ */
85
+ export function plainRenderHeaders(
86
+ plain: PlainRender,
87
+ set: CtxSet,
88
+ ): Record<string, string> | undefined {
89
+ const own = plain.headers
90
+ if (own === undefined) return set._headers
91
+ return set._headers === undefined ? { ...own } : { ...set._headers, ...own }
92
+ }
93
+
94
+ /**
95
+ * Finish the request here, with this status and body, without building a `Response`.
96
+ *
97
+ * Returned or thrown, from a handler or from any lifecycle stage:
98
+ *
99
+ * ```ts
100
+ * app.derive((c) => {
101
+ * const user = sessionOf(c)
102
+ * if (user === undefined) return status(401, { error: "unauthorized" })
103
+ * return { user }
104
+ * })
105
+ * ```
106
+ *
107
+ * A `derive` is the reason this exists as a value rather than as a rule about `beforeHandle`: a
108
+ * `beforeHandle` already short-circuits by returning a value, but a `derive`'s return IS the context
109
+ * extension, so before this its only exit was `throw new Response(...)` - the most expensive way to
110
+ * say 401 (see {@link PlainRender} for the measurements). Returning it is preferred; throwing it
111
+ * carries the same cost as any other throw and stays supported so a guard helper called for effect
112
+ * (`requireSession(c)`) can still end the request from inside a call it makes.
113
+ *
114
+ * The body is serialized by the lane that renders it, exactly like a handler's plain return, so the
115
+ * response carries a `content-length` rather than falling to chunked, and queued cookies still apply.
116
+ */
117
+ export function status(
118
+ code: number,
119
+ body?: unknown,
120
+ init?: { readonly headers?: Readonly<Record<string, string>> },
121
+ ): ResponseResult {
122
+ // The range `Response` accepts, enforced HERE so the plain lane cannot outrun it. A plain render
123
+ // is written straight to the socket by the Node adapter, where `writeHead` takes 100-999 - so a
124
+ // status this rejects would otherwise ship on Node and throw on every Web runtime, from the same
125
+ // code. One integer comparison against the value already in a register; the divergence it closes
126
+ // is the kind that only shows up in the runtime an app did not test on.
127
+ if (!Number.isInteger(code) || code < 200 || code > 599) {
128
+ throw new RangeError(`[nifra] status(${String(code)}): HTTP status must be an integer 200-599`)
129
+ }
130
+ const headers = init?.headers
131
+ const plain: PlainRender =
132
+ headers === undefined ? { status: code, body } : { status: code, headers, body }
133
+ return {
134
+ [RESPONSE_RESULT]: true,
135
+ plain,
136
+ // Only the lanes that cannot take plain data reach this - an edge runtime handed the value by a
137
+ // caller outside the server, say. Built on demand, never on the lanes that matter.
138
+ toResponse(): Response {
139
+ const headers: Record<string, string> = { ...init?.headers }
140
+ if (body === undefined) return new Response(null, { status: code, headers })
141
+ headers["content-type"] ??= "application/json;charset=utf-8"
142
+ return new Response(JSON.stringify(body), { status: code, headers })
143
+ },
144
+ }
145
+ }
146
+
50
147
  /** The concrete `Request` for a source - itself when a real `Request` was passed (the Web path), or the
51
148
  * lazily-built one (the Node adapter). A real `Request` IS a `RequestSource`, so no wrapper is allocated
52
149
  * on the Web hot path. */
@@ -144,6 +144,21 @@ export interface ServerOptions {
144
144
  * Fastify's `setErrorHandler`) instead of repeating a formatter per route.
145
145
  */
146
146
  readonly onValidationError?: RouteSchema["onValidationError"]
147
+ /**
148
+ * What to do when an order-scoped hook (`derive`/`beforeHandle`/`around`/`aroundCapability`/
149
+ * `afterHandle`/`onError`/`decorate`) covers no route because it was added after the last route was
150
+ * registered. `register()` snapshots the current chain into each route as it is declared, so a hook
151
+ * appended after every route reaches nothing and is silently dead. `"warn"` (default) logs once at
152
+ * seal (first `listen()`/`fetch`/`resolveNode`); `"error"` throws a `FrameworkError`
153
+ * (`UNUSED_SCOPED_HOOKS`); `"off"` skips the bookkeeping entirely, including the one-frame stack
154
+ * capture. App-global hooks (`onRequest`/`onResponse`/...) are unaffected in every mode.
155
+ *
156
+ * This is a development-time diagnostic: the whole audit is guarded by `process.env.NODE_ENV !==
157
+ * "production"`, so any bundler that defines `NODE_ENV` (vite/esbuild/webpack/next, `bun build
158
+ * --production`) dead-code-eliminates it, leaving zero bytes and zero per-request cost in the shipped
159
+ * bundle. It fires in development, test, and CI - where you catch the mistake - not in production.
160
+ */
161
+ readonly unusedScopedHooks?: "warn" | "error" | "off"
147
162
  }
148
163
 
149
164
  /** A fetch-compatible handler used by {@link Server.mountFetch}. */
@@ -246,7 +261,9 @@ export interface RunningServer {
246
261
  /**
247
262
  * A bundle of lifecycle hooks applied together via {@link Server.use} - the unit
248
263
  * `@nifrajs/middleware` ships (cors, security headers, rate-limit). Every hook is
249
- * optional and wired to its lifecycle point. Middleware is context-agnostic (sees
264
+ * optional and wired to its lifecycle point. Middleware carrying a portable response tier must be
265
+ * marked with `withResponseObserver()` from `@nifrajs/core/response-observer`, or the server must
266
+ * install `responseObserver()` before the bundle is applied. Middleware is context-agnostic (sees
250
267
  * the base `Context`); `use` does no context-type merging - the full type-merging
251
268
  * plugin system is deferred, and `.use` is reserved as its future entry point.
252
269
  */