@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.
- package/README.md +25 -0
- package/dist/assurance.d.ts +16 -8
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +33 -0
- package/dist/assurance.js.map +1 -1
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +26 -3
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-approval.d.ts +56 -0
- package/dist/durable-approval.d.ts.map +1 -0
- package/dist/durable-approval.js +233 -0
- package/dist/durable-approval.js.map +1 -0
- package/dist/durable-effect.d.ts +42 -0
- package/dist/durable-effect.d.ts.map +1 -0
- package/dist/durable-effect.js +234 -0
- package/dist/durable-effect.js.map +1 -0
- package/dist/durable-execution.d.ts +8 -346
- package/dist/durable-execution.d.ts.map +1 -1
- package/dist/durable-execution.js +7 -1150
- package/dist/durable-execution.js.map +1 -1
- package/dist/durable-saga.d.ts +94 -0
- package/dist/durable-saga.d.ts.map +1 -0
- package/dist/durable-saga.js +550 -0
- package/dist/durable-saga.js.map +1 -0
- package/dist/edge-kit.d.ts +27 -0
- package/dist/edge-kit.d.ts.map +1 -0
- package/dist/edge-kit.js +24 -0
- package/dist/edge-kit.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +23 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-approval-protocol.d.ts +1 -1
- package/dist/internal/durable-approval-protocol.d.ts.map +1 -1
- package/dist/internal/durable-effect-protocol.d.ts +1 -1
- package/dist/internal/durable-effect-protocol.d.ts.map +1 -1
- package/dist/internal/durable-saga-protocol.d.ts +1 -1
- package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
- package/dist/internal/durable-shared.d.ts +26 -0
- package/dist/internal/durable-shared.d.ts.map +1 -0
- package/dist/internal/durable-shared.js +131 -0
- package/dist/internal/durable-shared.js.map +1 -0
- package/dist/internal/durable-types.d.ts +159 -0
- package/dist/internal/durable-types.d.ts.map +1 -0
- package/dist/internal/durable-types.js +2 -0
- package/dist/internal/durable-types.js.map +1 -0
- package/dist/internal/route-compiler.d.ts +61 -0
- package/dist/internal/route-compiler.d.ts.map +1 -0
- package/dist/internal/route-compiler.js +121 -0
- package/dist/internal/route-compiler.js.map +1 -0
- package/dist/internal/route-execution.d.ts +17 -9
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +4 -18
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +35 -0
- package/dist/internal/route-lanes.d.ts.map +1 -0
- package/dist/internal/route-lanes.js +150 -0
- package/dist/internal/route-lanes.js.map +1 -0
- package/dist/internal/route-program.d.ts +67 -0
- package/dist/internal/route-program.d.ts.map +1 -0
- package/dist/internal/route-program.js +199 -0
- package/dist/internal/route-program.js.map +1 -0
- package/dist/response-observer.d.ts +25 -0
- package/dist/response-observer.d.ts.map +1 -0
- package/dist/response-observer.js +40 -0
- package/dist/response-observer.js.map +1 -0
- package/dist/security/baseline.d.ts +68 -0
- package/dist/security/baseline.d.ts.map +1 -0
- package/dist/security/baseline.js +118 -0
- package/dist/security/baseline.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +26 -0
- package/dist/server/bare-error-lane.d.ts.map +1 -0
- package/dist/server/bare-error-lane.js +60 -0
- package/dist/server/bare-error-lane.js.map +1 -0
- package/dist/server/body-lane.d.ts +13 -0
- package/dist/server/body-lane.d.ts.map +1 -0
- package/dist/server/body-lane.js +134 -0
- package/dist/server/body-lane.js.map +1 -0
- package/dist/server/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +7 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/bridge-markers.d.ts +25 -0
- package/dist/server/bridge-markers.d.ts.map +1 -0
- package/dist/server/bridge-markers.js +25 -0
- package/dist/server/bridge-markers.js.map +1 -0
- package/dist/server/context.d.ts +6 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +40 -14
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +7 -0
- package/dist/server/contract.js.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +16 -2
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/header-case.d.ts.map +1 -1
- package/dist/server/header-case.js +2 -1
- package/dist/server/header-case.js.map +1 -1
- package/dist/server/http.d.ts +36 -3
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +49 -3
- package/dist/server/http.js.map +1 -1
- package/dist/server/install.d.ts +6 -8
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +7 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/node-outcome-hook.d.ts +2 -1
- package/dist/server/node-outcome-hook.d.ts.map +1 -1
- package/dist/server/node-outcome-hook.js.map +1 -1
- package/dist/server/node-outcome.d.ts +17 -2
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +37 -3
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/plugin.d.ts +2 -2
- package/dist/server/plugin.d.ts.map +1 -1
- package/dist/server/plugin.js +6 -1
- package/dist/server/plugin.js.map +1 -1
- package/dist/server/query.d.ts +7 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -3
- package/dist/server/query.js.map +1 -1
- package/dist/server/registry.d.ts +40 -12
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +2 -13
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +18 -159
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +58 -4
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +5 -2
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts +20 -0
- package/dist/server/response-observer-runtime.d.ts.map +1 -0
- package/dist/server/response-observer-runtime.js +95 -0
- package/dist/server/response-observer-runtime.js.map +1 -0
- package/dist/server/runtime-core.d.ts +76 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +42 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +6 -1
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +158 -116
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +707 -844
- package/dist/server/server.js.map +1 -1
- package/dist/server/validation.d.ts +5 -0
- package/dist/server/validation.d.ts.map +1 -0
- package/dist/server/validation.js +13 -0
- package/dist/server/validation.js.map +1 -0
- package/dist/server/websocket.d.ts +20 -4
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +81 -11
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +12 -4
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +50 -16
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +3 -2
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy-register.d.ts +2 -0
- package/dist/single-copy-register.d.ts.map +1 -0
- package/dist/single-copy-register.js +24 -0
- package/dist/single-copy-register.js.map +1 -0
- package/dist/single-copy.d.ts +156 -0
- package/dist/single-copy.d.ts.map +1 -0
- package/dist/single-copy.js +472 -0
- package/dist/single-copy.js.map +1 -0
- package/package.json +28 -3
- package/src/assurance.ts +58 -1
- package/src/durable-adapters.ts +20 -0
- package/src/durable-approval.ts +281 -0
- package/src/durable-effect.ts +300 -0
- package/src/durable-execution.ts +8 -1627
- package/src/durable-saga.ts +744 -0
- package/src/edge-kit.ts +27 -0
- package/src/errors.ts +1 -0
- package/src/index.ts +129 -2
- package/src/internal/durable-approval-protocol.ts +1 -1
- package/src/internal/durable-effect-protocol.ts +1 -1
- package/src/internal/durable-saga-protocol.ts +1 -1
- package/src/internal/durable-shared.ts +157 -0
- package/src/internal/durable-types.ts +194 -0
- package/src/internal/route-compiler.ts +234 -0
- package/src/internal/route-execution.ts +53 -96
- package/src/internal/route-lanes.ts +221 -0
- package/src/internal/route-program.ts +404 -0
- package/src/response-observer.ts +64 -0
- package/src/security/baseline.ts +172 -0
- package/src/server/bare-error-lane.ts +77 -0
- package/src/server/body-lane.ts +203 -0
- package/src/server/body.ts +8 -0
- package/src/server/bridge-markers.ts +24 -0
- package/src/server/context.ts +6 -1
- package/src/server/contract.ts +70 -20
- package/src/server/cookies.ts +16 -2
- package/src/server/header-case.ts +3 -1
- package/src/server/http.ts +53 -3
- package/src/server/install.ts +14 -0
- package/src/server/node-outcome-hook.ts +2 -1
- package/src/server/node-outcome.ts +44 -3
- package/src/server/plugin.ts +11 -5
- package/src/server/query.ts +10 -4
- package/src/server/registry.ts +71 -16
- package/src/server/request-context.ts +13 -216
- package/src/server/respond.ts +71 -3
- package/src/server/response-contract-lane.ts +6 -2
- package/src/server/response-observer-runtime.ts +135 -0
- package/src/server/runtime-core.ts +124 -1
- package/src/server/server-types.ts +6 -1
- package/src/server/server.ts +1446 -1485
- package/src/server/validation.ts +19 -0
- package/src/server/websocket.ts +127 -17
- package/src/server/ws-bun.ts +67 -15
- package/src/server/ws-hook.ts +3 -2
- package/src/server.ts +8 -2
- package/src/single-copy-register.ts +24 -0
- package/src/single-copy.ts +526 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StandardIssue } from "../schema/standard.ts"
|
|
2
|
+
import { type ResponseResult, status } from "./runtime-core.ts"
|
|
3
|
+
|
|
4
|
+
function validationIssues(issues: ReadonlyArray<StandardIssue>): {
|
|
5
|
+
ok: false
|
|
6
|
+
error: string
|
|
7
|
+
issues: unknown[]
|
|
8
|
+
} {
|
|
9
|
+
const serialized = issues.map((issue) => {
|
|
10
|
+
const path = issue.path?.map((seg) => String(typeof seg === "object" ? seg.key : seg))
|
|
11
|
+
return path !== undefined ? { message: issue.message, path } : { message: issue.message }
|
|
12
|
+
})
|
|
13
|
+
return { ok: false, error: "validation", issues: serialized }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The shared 422 response result used by every body-validation lane. */
|
|
17
|
+
export function plainValidationError(issues: ReadonlyArray<StandardIssue>): ResponseResult {
|
|
18
|
+
return status(422, validationIssues(issues))
|
|
19
|
+
}
|
package/src/server/websocket.ts
CHANGED
|
@@ -164,10 +164,18 @@ export interface WebSocketHandler<
|
|
|
164
164
|
/**
|
|
165
165
|
* The **outbound** frame contract (server → client), a Standard Schema. Purely type-level: it types
|
|
166
166
|
* the frames the typed client's `.ws()` handle receives, and documents what this route pushes. The
|
|
167
|
-
* server does NOT runtime-validate its own sends - the inbound `messageSchema` guards the
|
|
168
|
-
* boundary; outbound honesty is the handler author's code
|
|
167
|
+
* server does NOT runtime-validate its own sends by default - the inbound `messageSchema` guards the
|
|
168
|
+
* trust boundary; outbound honesty is the handler author's code. Set {@link validateSend} to opt into
|
|
169
|
+
* a synchronous runtime check of JSON text/UTF-8 binary frames.
|
|
169
170
|
*/
|
|
170
171
|
sendSchema?: Send
|
|
172
|
+
/**
|
|
173
|
+
* Opt-in runtime enforcement for {@link sendSchema}. Invalid JSON, schema failures, and async
|
|
174
|
+
* validators are dropped before the native socket's `send()` runs and reported to `error()` when it
|
|
175
|
+
* exists. This is a development/contract check for the server's own output, not an inbound security
|
|
176
|
+
* boundary. It is off by default and requires `sendSchema` at registration.
|
|
177
|
+
*/
|
|
178
|
+
validateSend?: boolean
|
|
171
179
|
/** Decode versioned transport frames before inbound schema validation. Omit for legacy JSON. */
|
|
172
180
|
transport?: {
|
|
173
181
|
readonly registry: TransportCodecRegistry
|
|
@@ -246,24 +254,19 @@ function toBinary(raw: unknown): Uint8Array {
|
|
|
246
254
|
* `open` event (Deno). Lifecycle callbacks are error-isolated - a throw (or async rejection) routes to
|
|
247
255
|
* `error()` and never tears the process down; binary frames are normalized to `Uint8Array`.
|
|
248
256
|
*/
|
|
249
|
-
export function attachWebSocket
|
|
257
|
+
export function attachWebSocket<
|
|
258
|
+
Data = unknown,
|
|
259
|
+
Env = unknown,
|
|
260
|
+
Schema extends StandardSchemaV1 | undefined = undefined,
|
|
261
|
+
Send extends StandardSchemaV1 | undefined = undefined,
|
|
262
|
+
>(
|
|
250
263
|
socket: StandardWebSocket,
|
|
251
|
-
handler: WebSocketHandler,
|
|
264
|
+
handler: WebSocketHandler<Data, Env, Schema, Send>,
|
|
252
265
|
data: unknown,
|
|
253
266
|
options: { openNow: boolean; pubsub: TopicRegistry; maxPayloadBytes?: number },
|
|
254
|
-
): NifraWebSocket {
|
|
267
|
+
): NifraWebSocket<Data> {
|
|
255
268
|
const { pubsub } = options
|
|
256
|
-
|
|
257
|
-
send: (payload) => socket.send(payload),
|
|
258
|
-
close: (code, reason) => socket.close(code, reason),
|
|
259
|
-
get readyState() {
|
|
260
|
-
return socket.readyState
|
|
261
|
-
},
|
|
262
|
-
subscribe: (topic) => pubsub.subscribe(topic, ws),
|
|
263
|
-
unsubscribe: (topic) => pubsub.unsubscribe(topic, ws),
|
|
264
|
-
data,
|
|
265
|
-
raw: socket,
|
|
266
|
-
}
|
|
269
|
+
let ws!: NifraWebSocket<Data>
|
|
267
270
|
const reportError = (error: unknown): void => {
|
|
268
271
|
if (handler.error === undefined) return
|
|
269
272
|
try {
|
|
@@ -273,6 +276,24 @@ export function attachWebSocket(
|
|
|
273
276
|
/* the error handler itself failed - last resort, swallow */
|
|
274
277
|
}
|
|
275
278
|
}
|
|
279
|
+
const send = createWebSocketSender(
|
|
280
|
+
(payload) => socket.send(payload),
|
|
281
|
+
handler.sendSchema,
|
|
282
|
+
handler.validateSend,
|
|
283
|
+
handler.transport,
|
|
284
|
+
reportError,
|
|
285
|
+
)
|
|
286
|
+
ws = {
|
|
287
|
+
send,
|
|
288
|
+
close: (code, reason) => socket.close(code, reason),
|
|
289
|
+
get readyState() {
|
|
290
|
+
return socket.readyState
|
|
291
|
+
},
|
|
292
|
+
subscribe: (topic) => pubsub.subscribe(topic, ws),
|
|
293
|
+
unsubscribe: (topic) => pubsub.unsubscribe(topic, ws),
|
|
294
|
+
data: data as Data,
|
|
295
|
+
raw: socket,
|
|
296
|
+
}
|
|
276
297
|
const safe = (call: () => MaybePromise<void>): void => {
|
|
277
298
|
try {
|
|
278
299
|
const r = call()
|
|
@@ -300,7 +321,13 @@ export function attachWebSocket(
|
|
|
300
321
|
return
|
|
301
322
|
}
|
|
302
323
|
}
|
|
303
|
-
safe(() =>
|
|
324
|
+
safe(() =>
|
|
325
|
+
(
|
|
326
|
+
handler.message as
|
|
327
|
+
| ((socket: NifraWebSocket<Data>, value: WebSocketData) => MaybePromise<void>)
|
|
328
|
+
| undefined
|
|
329
|
+
)?.(ws, payload),
|
|
330
|
+
)
|
|
304
331
|
})
|
|
305
332
|
socket.addEventListener(
|
|
306
333
|
"close",
|
|
@@ -321,6 +348,89 @@ export function attachWebSocket(
|
|
|
321
348
|
const WS_MESSAGE_DECODER = new TextDecoder()
|
|
322
349
|
const WS_PAYLOAD_ENCODER = new TextEncoder()
|
|
323
350
|
|
|
351
|
+
type WebSocketSendPayload = string | ArrayBufferView | ArrayBuffer
|
|
352
|
+
|
|
353
|
+
const OUTBOUND_VALIDATION_ERROR = "outbound WebSocket frame failed sendSchema validation"
|
|
354
|
+
|
|
355
|
+
function outboundValueOf(
|
|
356
|
+
payload: WebSocketSendPayload,
|
|
357
|
+
transport: WebSocketHandler["transport"],
|
|
358
|
+
): unknown {
|
|
359
|
+
const text =
|
|
360
|
+
typeof payload === "string"
|
|
361
|
+
? payload
|
|
362
|
+
: WS_MESSAGE_DECODER.decode(
|
|
363
|
+
payload instanceof ArrayBuffer
|
|
364
|
+
? new Uint8Array(payload)
|
|
365
|
+
: new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength),
|
|
366
|
+
)
|
|
367
|
+
return transport === undefined
|
|
368
|
+
? JSON.parse(text)
|
|
369
|
+
: decodeTransportFrame(text, transport.registry, {
|
|
370
|
+
...(transport.maxBytes === undefined ? {} : { maxBytes: transport.maxBytes }),
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Build the socket's send function once. The disabled path returns the native sender directly, so
|
|
376
|
+
* the default type-only contract adds no per-frame branch or schema work. Runtime validation is
|
|
377
|
+
* deliberately synchronous: a socket `send()` cannot await an async Standard Schema without changing
|
|
378
|
+
* the portable API, so an async result fails closed and is never sent.
|
|
379
|
+
*/
|
|
380
|
+
export function createWebSocketSender(
|
|
381
|
+
send: (payload: WebSocketSendPayload) => void,
|
|
382
|
+
schema: StandardSchemaV1 | undefined,
|
|
383
|
+
enabled: boolean | undefined,
|
|
384
|
+
transport: WebSocketHandler["transport"],
|
|
385
|
+
onError: (error: unknown) => void,
|
|
386
|
+
): (payload: WebSocketSendPayload) => void {
|
|
387
|
+
if (enabled !== true || schema === undefined) return send
|
|
388
|
+
let reporting = false
|
|
389
|
+
const reportFailure = (error: unknown): void => {
|
|
390
|
+
// An error handler may try to send a diagnostic. Do not recurse forever when that diagnostic also
|
|
391
|
+
// violates the same outbound contract. EVERY failure path reports through here - a path that calls
|
|
392
|
+
// `onError` directly reopens that recursion for the one rejection shape it happens to cover.
|
|
393
|
+
if (reporting) return
|
|
394
|
+
reporting = true
|
|
395
|
+
try {
|
|
396
|
+
onError(error)
|
|
397
|
+
} finally {
|
|
398
|
+
reporting = false
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return (payload) => {
|
|
402
|
+
let value: unknown
|
|
403
|
+
try {
|
|
404
|
+
value = outboundValueOf(payload, transport)
|
|
405
|
+
} catch {
|
|
406
|
+
reportFailure(new Error(OUTBOUND_VALIDATION_ERROR))
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let outcome: ValidationOutcome<unknown> | Promise<ValidationOutcome<unknown>>
|
|
411
|
+
try {
|
|
412
|
+
outcome = validateStandard(schema, value) as
|
|
413
|
+
| ValidationOutcome<unknown>
|
|
414
|
+
| Promise<ValidationOutcome<unknown>>
|
|
415
|
+
} catch {
|
|
416
|
+
reportFailure(new Error(OUTBOUND_VALIDATION_ERROR))
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
if (outcome instanceof Promise) {
|
|
420
|
+
// Do not await or send later: the portable `send()` contract is synchronous. Attach a rejection
|
|
421
|
+
// handler so a validator that rejects cannot become an unhandled process-level rejection.
|
|
422
|
+
outcome.catch(() => {})
|
|
423
|
+
reportFailure(new Error(`${OUTBOUND_VALIDATION_ERROR}; validator must be synchronous`))
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
if (!outcome.ok) {
|
|
427
|
+
reportFailure(new Error(OUTBOUND_VALIDATION_ERROR))
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
send(payload)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
324
434
|
/**
|
|
325
435
|
* If the handler declares a `messageSchema`, return a copy whose `message` validates each frame -
|
|
326
436
|
* parse as JSON, run the Standard Schema, then call the user's `message` with the typed value, or
|
package/src/server/ws-bun.ts
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
* (erased). No `Bun.*` calls here - the runtime seam stays in `server.ts` (see runtime-boundary.test).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
createWebSocketSender,
|
|
10
|
+
type NifraWebSocket,
|
|
11
|
+
type TopicRegistry,
|
|
12
|
+
type WebSocketData,
|
|
13
|
+
type WebSocketHandler,
|
|
14
|
+
} from "./websocket.ts"
|
|
9
15
|
import type { BunWsHandlers } from "./ws-hook.ts"
|
|
10
16
|
|
|
11
17
|
/** Per-connection state Bun's `websocket` callbacks read via `ws.data`: the matched handler, the
|
|
@@ -22,6 +28,10 @@ export interface BunSocket {
|
|
|
22
28
|
send(data: string | ArrayBufferView | ArrayBuffer, compress?: boolean): number
|
|
23
29
|
close(code?: number, reason?: string): void
|
|
24
30
|
readonly readyState: number
|
|
31
|
+
// Bun's native (uWebSockets) topic pub/sub - used only in native-pubsub mode (no per-socket
|
|
32
|
+
// outbound validation), where `app.publish` routes through the Bun server's `publish`.
|
|
33
|
+
subscribe(topic: string): void
|
|
34
|
+
unsubscribe(topic: string): void
|
|
25
35
|
// readonly: we never reassign `ws.data` wholesale, only mutate its fields (`.nifra`, `.data`).
|
|
26
36
|
readonly data: BunWsData
|
|
27
37
|
}
|
|
@@ -32,18 +42,43 @@ function toBinaryData(message: ArrayBuffer | Uint8Array): Uint8Array {
|
|
|
32
42
|
return message instanceof Uint8Array ? message : new Uint8Array(message)
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Wrap a Bun `ServerWebSocket` as the portable {@link NifraWebSocket} handed to WS callbacks.
|
|
47
|
+
*
|
|
48
|
+
* `nativePubsub` selects the topic backend: in native mode `subscribe`/`unsubscribe` go straight to
|
|
49
|
+
* Bun's own (uWebSockets) pub/sub so a broadcast via the Bun server's `publish` reaches this socket
|
|
50
|
+
* with no per-connection JS loop; otherwise they use the runtime-neutral {@link TopicRegistry}. Native
|
|
51
|
+
* mode is used only when no route validates outbound frames, so raw and validated sends are identical.
|
|
52
|
+
*/
|
|
53
|
+
function wrapBunSocket(
|
|
54
|
+
raw: BunSocket,
|
|
55
|
+
topics: TopicRegistry,
|
|
56
|
+
handler: WebSocketHandler,
|
|
57
|
+
nativePubsub: boolean,
|
|
58
|
+
): NifraWebSocket {
|
|
59
|
+
let ws!: NifraWebSocket
|
|
60
|
+
const reportError = (error: unknown): void => reportWsError(error, ws, handler)
|
|
61
|
+
const send = createWebSocketSender(
|
|
62
|
+
(data) => {
|
|
39
63
|
raw.send(data)
|
|
40
64
|
},
|
|
65
|
+
handler.sendSchema,
|
|
66
|
+
handler.validateSend,
|
|
67
|
+
handler.transport,
|
|
68
|
+
reportError,
|
|
69
|
+
)
|
|
70
|
+
ws = {
|
|
71
|
+
send,
|
|
41
72
|
close: (code, reason) => raw.close(code, reason),
|
|
42
73
|
get readyState() {
|
|
43
74
|
return raw.readyState
|
|
44
75
|
},
|
|
45
|
-
subscribe:
|
|
46
|
-
|
|
76
|
+
subscribe: nativePubsub
|
|
77
|
+
? (topic) => raw.subscribe(topic)
|
|
78
|
+
: (topic) => topics.subscribe(topic, ws),
|
|
79
|
+
unsubscribe: nativePubsub
|
|
80
|
+
? (topic) => raw.unsubscribe(topic)
|
|
81
|
+
: (topic) => topics.unsubscribe(topic, ws),
|
|
47
82
|
get data() {
|
|
48
83
|
return raw.data.data
|
|
49
84
|
},
|
|
@@ -80,26 +115,43 @@ function dispatchWsCallback(
|
|
|
80
115
|
}
|
|
81
116
|
}
|
|
82
117
|
|
|
83
|
-
/**
|
|
84
|
-
* `
|
|
85
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The shared Bun `websocket` dispatcher config for one app - each connection's `ws.data.handler` is
|
|
120
|
+
* the matched route's handler, set by `server.upgrade`.
|
|
121
|
+
*
|
|
122
|
+
* `nativePubsub` (set by `listen()` when the app has no validated-send route) makes subscriptions use
|
|
123
|
+
* Bun's own pub/sub; there the registry holds nothing and Bun auto-drops native subscriptions on
|
|
124
|
+
* close, so the `unsubscribeAll` sweep is skipped.
|
|
125
|
+
*/
|
|
126
|
+
export function createBunWsHandlers(topics: TopicRegistry, nativePubsub = false): BunWsHandlers {
|
|
86
127
|
return {
|
|
87
128
|
open: (raw) => {
|
|
88
129
|
const ws = raw as BunSocket
|
|
89
|
-
const nifra = wrapBunSocket(ws, topics)
|
|
130
|
+
const nifra = wrapBunSocket(ws, topics, ws.data.handler, nativePubsub)
|
|
90
131
|
ws.data.nifra = nifra
|
|
91
132
|
dispatchWsCallback(() => ws.data.handler.open?.(nifra), nifra, ws.data.handler)
|
|
92
133
|
},
|
|
93
134
|
message: (raw, message) => {
|
|
94
135
|
const ws = raw as BunSocket
|
|
95
|
-
const
|
|
136
|
+
const handler = ws.data.handler
|
|
137
|
+
const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics, handler, nativePubsub)
|
|
96
138
|
const data: WebSocketData = typeof message === "string" ? message : toBinaryData(message)
|
|
97
|
-
|
|
139
|
+
// Hottest WS path - dispatch inline rather than through `dispatchWsCallback` so no per-frame
|
|
140
|
+
// closure is allocated. Same contract: a sync throw or async rejection routes to `handler.error`.
|
|
141
|
+
try {
|
|
142
|
+
const result = handler.message?.(nifra, data)
|
|
143
|
+
if (result instanceof Promise)
|
|
144
|
+
result.catch((e: unknown) => reportWsError(e, nifra, handler))
|
|
145
|
+
} catch (error) {
|
|
146
|
+
reportWsError(error, nifra, handler)
|
|
147
|
+
}
|
|
98
148
|
},
|
|
99
149
|
close: (raw, code, reason) => {
|
|
100
150
|
const ws = raw as BunSocket
|
|
101
|
-
const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics)
|
|
102
|
-
|
|
151
|
+
const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics, ws.data.handler, nativePubsub)
|
|
152
|
+
// Registry mode: drop topic subscriptions so it never holds a dead socket. Native mode: Bun
|
|
153
|
+
// unsubscribes the socket from every topic on close, so there is nothing to sweep here.
|
|
154
|
+
if (!nativePubsub) topics.unsubscribeAll(nifra)
|
|
103
155
|
dispatchWsCallback(() => ws.data.handler.close?.(nifra, code, reason), nifra, ws.data.handler)
|
|
104
156
|
},
|
|
105
157
|
}
|
package/src/server/ws-hook.ts
CHANGED
|
@@ -30,8 +30,9 @@ export interface WsRuntime {
|
|
|
30
30
|
wrapHandler(handler: WebSocketHandler): WebSocketHandler
|
|
31
31
|
/** One in-process pub/sub registry per app (backs `ws.subscribe` + `app.publish`). */
|
|
32
32
|
createTopics(): TopicRegistry
|
|
33
|
-
/** The Bun `websocket` config for `listen()` when the app has WS routes.
|
|
34
|
-
|
|
33
|
+
/** The Bun `websocket` config for `listen()` when the app has WS routes. `nativePubsub` routes
|
|
34
|
+
* `ws.subscribe` through Bun's own pub/sub (set when no route validates outbound frames). */
|
|
35
|
+
bunHandlers(topics: TopicRegistry, nativePubsub?: boolean): BunWsHandlers
|
|
35
36
|
/** `attachWebSocket` - wires a standard server socket (Workers `WebSocketPair`) to a handler. */
|
|
36
37
|
attach(
|
|
37
38
|
socket: StandardWebSocket,
|
package/src/server.ts
CHANGED
|
@@ -45,7 +45,7 @@ export {
|
|
|
45
45
|
type ScheduledHandler,
|
|
46
46
|
toFetchHandler,
|
|
47
47
|
} from "./server/edge.ts"
|
|
48
|
-
export { pathnameOf, type UrlParts, urlPartsOf } from "./server/http.ts"
|
|
48
|
+
export { isSameOriginPath, pathnameOf, type UrlParts, urlPartsOf } from "./server/http.ts"
|
|
49
49
|
export {
|
|
50
50
|
commonSecretPatterns,
|
|
51
51
|
jsonLogger,
|
|
@@ -70,7 +70,13 @@ export {
|
|
|
70
70
|
toVercelHandler,
|
|
71
71
|
type VercelHandler,
|
|
72
72
|
} from "./server/platform-adapters.ts"
|
|
73
|
-
export type { Registry, RouteInfo } from "./server/registry.ts"
|
|
73
|
+
export type { Registry, ResponseMapFor, RouteInfo, RouteInfoFor } from "./server/registry.ts"
|
|
74
|
+
export {
|
|
75
|
+
type PlainRender,
|
|
76
|
+
type ResponseResult,
|
|
77
|
+
type StatusResponse,
|
|
78
|
+
status,
|
|
79
|
+
} from "./server/runtime-core.ts"
|
|
74
80
|
export {
|
|
75
81
|
type AdmissionController,
|
|
76
82
|
type AdmissionDecision,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-effect entry (`@nifrajs/core/single-copy/register`): install the single-copy resolver into the
|
|
3
|
+
* Bun runtime.
|
|
4
|
+
*
|
|
5
|
+
* Preload it - never import it from application code:
|
|
6
|
+
*
|
|
7
|
+
* ```toml
|
|
8
|
+
* # bunfig.toml
|
|
9
|
+
* preload = ["@nifrajs/core/single-copy/register"]
|
|
10
|
+
* [test]
|
|
11
|
+
* preload = ["@nifrajs/core/single-copy/register"]
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A resolver only affects imports that have not resolved yet, and a module's own imports resolve
|
|
15
|
+
* before its body runs. Registering from inside application code would therefore miss exactly the
|
|
16
|
+
* imports it exists to redirect. `preload` runs before the entry point, which is the only moment that
|
|
17
|
+
* covers the whole graph.
|
|
18
|
+
*
|
|
19
|
+
* `nifra check` reads this specifier out of `bunfig.toml` as the proof that the runtime arm is armed,
|
|
20
|
+
* so the string above is a contract rather than a suggestion.
|
|
21
|
+
*/
|
|
22
|
+
import { registerSingleCopy } from "./single-copy"
|
|
23
|
+
|
|
24
|
+
registerSingleCopy()
|