@nifrajs/core 0.1.0-alpha.1

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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/errors.d.ts +21 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/errors.js +26 -0
  6. package/dist/errors.js.map +1 -0
  7. package/dist/index.d.ts +26 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +21 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/internal/brand.d.ts +11 -0
  12. package/dist/internal/brand.d.ts.map +1 -0
  13. package/dist/internal/brand.js +10 -0
  14. package/dist/internal/brand.js.map +1 -0
  15. package/dist/internal/secret.d.ts +9 -0
  16. package/dist/internal/secret.d.ts.map +1 -0
  17. package/dist/internal/secret.js +19 -0
  18. package/dist/internal/secret.js.map +1 -0
  19. package/dist/router/router.d.ts +61 -0
  20. package/dist/router/router.d.ts.map +1 -0
  21. package/dist/router/router.js +324 -0
  22. package/dist/router/router.js.map +1 -0
  23. package/dist/schema/standard.d.ts +46 -0
  24. package/dist/schema/standard.d.ts.map +1 -0
  25. package/dist/schema/standard.js +20 -0
  26. package/dist/schema/standard.js.map +1 -0
  27. package/dist/seo.d.ts +48 -0
  28. package/dist/seo.d.ts.map +1 -0
  29. package/dist/seo.js +109 -0
  30. package/dist/seo.js.map +1 -0
  31. package/dist/server/body.d.ts +39 -0
  32. package/dist/server/body.d.ts.map +1 -0
  33. package/dist/server/body.js +85 -0
  34. package/dist/server/body.js.map +1 -0
  35. package/dist/server/context.d.ts +111 -0
  36. package/dist/server/context.d.ts.map +1 -0
  37. package/dist/server/context.js +2 -0
  38. package/dist/server/context.js.map +1 -0
  39. package/dist/server/contract.d.ts +142 -0
  40. package/dist/server/contract.d.ts.map +1 -0
  41. package/dist/server/contract.js +59 -0
  42. package/dist/server/contract.js.map +1 -0
  43. package/dist/server/cookies.d.ts +41 -0
  44. package/dist/server/cookies.d.ts.map +1 -0
  45. package/dist/server/cookies.js +181 -0
  46. package/dist/server/cookies.js.map +1 -0
  47. package/dist/server/logger.d.ts +49 -0
  48. package/dist/server/logger.d.ts.map +1 -0
  49. package/dist/server/logger.js +125 -0
  50. package/dist/server/logger.js.map +1 -0
  51. package/dist/server/registry.d.ts +46 -0
  52. package/dist/server/registry.d.ts.map +1 -0
  53. package/dist/server/registry.js +2 -0
  54. package/dist/server/registry.js.map +1 -0
  55. package/dist/server/server.d.ts +528 -0
  56. package/dist/server/server.d.ts.map +1 -0
  57. package/dist/server/server.js +1660 -0
  58. package/dist/server/server.js.map +1 -0
  59. package/dist/server/sse.d.ts +54 -0
  60. package/dist/server/sse.d.ts.map +1 -0
  61. package/dist/server/sse.js +126 -0
  62. package/dist/server/sse.js.map +1 -0
  63. package/dist/server/webhook.d.ts +42 -0
  64. package/dist/server/webhook.d.ts.map +1 -0
  65. package/dist/server/webhook.js +149 -0
  66. package/dist/server/webhook.js.map +1 -0
  67. package/dist/server/websocket.d.ts +152 -0
  68. package/dist/server/websocket.d.ts.map +1 -0
  69. package/dist/server/websocket.js +178 -0
  70. package/dist/server/websocket.js.map +1 -0
  71. package/dist/server/ws-bun.d.ts +27 -0
  72. package/dist/server/ws-bun.d.ts.map +1 -0
  73. package/dist/server/ws-bun.js +82 -0
  74. package/dist/server/ws-bun.js.map +1 -0
  75. package/dist/server/ws-hook.d.ts +36 -0
  76. package/dist/server/ws-hook.d.ts.map +1 -0
  77. package/dist/server/ws-hook.js +18 -0
  78. package/dist/server/ws-hook.js.map +1 -0
  79. package/dist/ws.d.ts +19 -0
  80. package/dist/ws.d.ts.map +1 -0
  81. package/dist/ws.js +26 -0
  82. package/dist/ws.js.map +1 -0
  83. package/package.json +63 -0
  84. package/src/errors.ts +41 -0
  85. package/src/index.ts +115 -0
  86. package/src/internal/brand.ts +11 -0
  87. package/src/internal/secret.ts +24 -0
  88. package/src/router/router.ts +429 -0
  89. package/src/schema/standard.ts +76 -0
  90. package/src/seo.ts +155 -0
  91. package/src/server/body.ts +89 -0
  92. package/src/server/context.ts +124 -0
  93. package/src/server/contract.ts +209 -0
  94. package/src/server/cookies.ts +198 -0
  95. package/src/server/logger.ts +167 -0
  96. package/src/server/registry.ts +63 -0
  97. package/src/server/server.ts +2479 -0
  98. package/src/server/sse.ts +161 -0
  99. package/src/server/webhook.ts +207 -0
  100. package/src/server/websocket.ts +308 -0
  101. package/src/server/ws-bun.ts +106 -0
  102. package/src/server/ws-hook.ts +53 -0
  103. package/src/ws.ts +40 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * WebSocket seam — portable types shared by `@nifrajs/core`'s `app.ws()` and every serving adapter
3
+ * (Bun `listen()`, `@nifrajs/node`, `@nifrajs/deno`, `toFetchHandler` on Workers). No runtime code here, so
4
+ * it stays edge-safe and dependency-free; each adapter implements {@link NifraWebSocket} over its
5
+ * runtime's native socket and dispatches to the handler.
6
+ *
7
+ * A WS upgrade can't go through `app.fetch` (which has no socket) — it's adapter-integrated. The
8
+ * adapter calls `app.resolveWebSocketUpgrade(req)` (runs the route's `upgrade()` guard in a real
9
+ * request context), then performs the runtime upgrade with the returned data.
10
+ */
11
+ import type { InferOutput, StandardIssue, StandardSchemaV1 } from "../schema/standard.js";
12
+ type MaybePromise<T> = T | Promise<T>;
13
+ /** A received frame, normalized across runtimes: text → `string`, binary → `Uint8Array`. */
14
+ export type WebSocketData = string | Uint8Array;
15
+ /** The value `message` receives: the schema's validated output when a `messageSchema` is set on the
16
+ * handler, otherwise the raw frame (`string | Uint8Array`). */
17
+ type WsMessageInput<Schema extends StandardSchemaV1 | undefined> = Schema extends StandardSchemaV1 ? InferOutput<Schema> : WebSocketData;
18
+ /** The portable socket handed to WS lifecycle callbacks. Each adapter wraps its native socket. */
19
+ export interface NifraWebSocket<Data = unknown> {
20
+ /** Send a text (`string`) or binary (`ArrayBuffer`/typed-array) frame. No-op once closed. */
21
+ send(data: string | ArrayBufferView | ArrayBuffer): void;
22
+ /** Close the connection (optional code 1000–4999 + short reason). */
23
+ close(code?: number, reason?: string): void;
24
+ /** `WebSocket.readyState` (0 CONNECTING · 1 OPEN · 2 CLOSING · 3 CLOSED). */
25
+ readonly readyState: number;
26
+ /** Join a pub/sub topic — `app.publish(topic, data)` then reaches this connection. Idempotent. */
27
+ subscribe(topic: string): void;
28
+ /** Leave a pub/sub topic. Idempotent. (All topics are dropped automatically on close.) */
29
+ unsubscribe(topic: string): void;
30
+ /** Per-connection state — seeded by `upgrade()`, mutable for the connection's lifetime. */
31
+ data: Data;
32
+ /** Escape hatch to the runtime's native socket (Bun `ServerWebSocket`, Web `WebSocket`, `ws`). */
33
+ readonly raw: unknown;
34
+ }
35
+ /**
36
+ * In-process pub/sub for `ws.subscribe(topic)` + `app.publish(topic, data)`. **Single-instance only** —
37
+ * topics live in this process's memory, so a multi-instance deploy (multiple servers behind a load
38
+ * balancer) needs an external fan-out (Redis pub/sub, a Cloudflare Durable Object, NATS, …) bridged to
39
+ * `app.publish`. The same registry instance is shared by an app's connections and its `publish`.
40
+ */
41
+ export declare class TopicRegistry {
42
+ private readonly topics;
43
+ private readonly memberships;
44
+ subscribe(topic: string, ws: NifraWebSocket): void;
45
+ unsubscribe(topic: string, ws: NifraWebSocket): void;
46
+ /** Drop every subscription for a connection — called on close so a registry never leaks dead sockets. */
47
+ unsubscribeAll(ws: NifraWebSocket): void;
48
+ /** Send `data` to every connection subscribed to `topic`. Per-socket send errors are isolated. */
49
+ publish(topic: string, data: string | ArrayBufferView | ArrayBuffer): void;
50
+ }
51
+ /**
52
+ * The request-context subset the `upgrade()` guard sees — the same lazy accessors a route handler's
53
+ * `c` has (cookies/headers/env are read straight off the upgrade request). Structurally a slice of the
54
+ * core `RawContext`, so the real context object satisfies it.
55
+ */
56
+ export interface WebSocketContext<Env = unknown> {
57
+ readonly req: Request;
58
+ readonly params: Record<string, string>;
59
+ readonly query: unknown;
60
+ readonly cookies: Readonly<Record<string, string>>;
61
+ readonly env: Env;
62
+ readonly signal: AbortSignal;
63
+ readonly waitUntil: (promise: Promise<unknown>) => void;
64
+ boundedBody(maxBytes?: number): Promise<Uint8Array>;
65
+ boundedJson<T = unknown>(maxBytes?: number): Promise<T>;
66
+ }
67
+ /** A WebSocket route's lifecycle. All callbacks optional; only `message` is needed for an echo. */
68
+ export interface WebSocketHandler<Data = unknown, Env = unknown, Schema extends StandardSchemaV1 | undefined = undefined> {
69
+ /**
70
+ * Cross-site WebSocket hijacking (CSWSH) guard — checked BEFORE `upgrade()`. A browser does not
71
+ * apply CORS to WebSocket handshakes and DOES send the page's cookies, so without an Origin check
72
+ * any site can open an authenticated socket to your app. Set this to lock the route to known
73
+ * origins: a string allow-list (exact `Origin` header match) or a predicate. A request whose
74
+ * `Origin` doesn't match (or is absent, for the allow-list form) is rejected with `403` before any
75
+ * per-connection work. Omit for no built-in check (same as today — then enforce it yourself in
76
+ * `upgrade()`). Non-browser clients can spoof `Origin`, so this is a browser-CSWSH defense, not
77
+ * authentication — pair it with auth in `upgrade()`.
78
+ */
79
+ allowedOrigins?: ReadonlyArray<string> | ((origin: string | null) => boolean);
80
+ /**
81
+ * Runs in the HTTP request context **before** the upgrade (and after {@link allowedOrigins}) — the
82
+ * place to authenticate or rate-limit. Return the initial per-connection `data` (→ `ws.data`), or a
83
+ * `Response` to **reject** the upgrade (the client never connects). Omit to accept with
84
+ * `data: undefined`. A thrown error rejects with a flat 500.
85
+ */
86
+ upgrade?(c: WebSocketContext<Env>): MaybePromise<Data | Response>;
87
+ /**
88
+ * Contract-first messages: a Standard Schema (`t`, zod, valibot, …) validating each **inbound** frame.
89
+ * Text frames are parsed as JSON first. When set, `message` receives the validated, typed value; a
90
+ * non-JSON or schema-invalid frame is routed to `onInvalidMessage` (or dropped if that's omitted).
91
+ */
92
+ messageSchema?: Schema;
93
+ open?(ws: NifraWebSocket<Data>): MaybePromise<void>;
94
+ message?(ws: NifraWebSocket<Data>, data: WsMessageInput<Schema>): MaybePromise<void>;
95
+ /** Inbound frame that failed JSON parse or `messageSchema` validation (only fires when a schema is
96
+ * set). `raw` is the original frame; `issues` are the Standard Schema issues (one synthetic issue for
97
+ * a JSON parse failure). Omit to silently drop invalid frames. */
98
+ onInvalidMessage?(ws: NifraWebSocket<Data>, issues: ReadonlyArray<StandardIssue>, raw: WebSocketData): MaybePromise<void>;
99
+ close?(ws: NifraWebSocket<Data>, code: number, reason: string): MaybePromise<void>;
100
+ error?(ws: NifraWebSocket<Data>, error: unknown): MaybePromise<void>;
101
+ }
102
+ /**
103
+ * The outcome of `app.resolveWebSocketUpgrade(req)` — for serving adapters:
104
+ * - `pass` — not a WS upgrade for a registered WS route; handle as a normal HTTP request.
105
+ * - `reject` — a WS route matched but `upgrade()` rejected (or the path was malformed); return `response`.
106
+ * - `upgrade` — perform the runtime upgrade, then dispatch the native socket's events to `handler`,
107
+ * seeding `ws.data` with `data`.
108
+ */
109
+ export type WebSocketUpgradeOutcome = {
110
+ readonly kind: "pass";
111
+ } | {
112
+ readonly kind: "reject";
113
+ readonly response: Response;
114
+ } | {
115
+ readonly kind: "upgrade";
116
+ readonly handler: WebSocketHandler;
117
+ readonly data: unknown;
118
+ /** The app's pub/sub registry — the adapter wires `ws.subscribe` + close-cleanup to it. */
119
+ readonly pubsub: TopicRegistry;
120
+ };
121
+ /**
122
+ * A standard server-side `WebSocket` — the half returned by Deno's `Deno.upgradeWebSocket` and the
123
+ * Workers `WebSocketPair`. {@link attachWebSocket} wires one to a nifra handler, so the Deno and Workers
124
+ * bridges share all the dispatch/normalization/error-isolation logic (only the upgrade call differs).
125
+ */
126
+ export interface StandardWebSocket {
127
+ send(data: string | ArrayBufferView | ArrayBuffer): void;
128
+ close(code?: number, reason?: string): void;
129
+ readonly readyState: number;
130
+ binaryType?: string;
131
+ addEventListener(type: string, listener: (event: never) => void): void;
132
+ }
133
+ /**
134
+ * Wire a standard server-side `WebSocket` to a nifra {@link WebSocketHandler}, returning the portable
135
+ * {@link NifraWebSocket}. Shared by the Deno and Workers bridges. `openNow` fires `open` immediately
136
+ * (Workers, where the socket is already open after `accept()`); otherwise `open` waits for the socket's
137
+ * `open` event (Deno). Lifecycle callbacks are error-isolated — a throw (or async rejection) routes to
138
+ * `error()` and never tears the process down; binary frames are normalized to `Uint8Array`.
139
+ */
140
+ export declare function attachWebSocket(socket: StandardWebSocket, handler: WebSocketHandler, data: unknown, options: {
141
+ openNow: boolean;
142
+ pubsub: TopicRegistry;
143
+ }): NifraWebSocket;
144
+ /**
145
+ * If the handler declares a `messageSchema`, return a copy whose `message` validates each frame —
146
+ * parse as JSON, run the Standard Schema, then call the user's `message` with the typed value, or
147
+ * `onInvalidMessage` on failure. Returns the handler unchanged when no schema is set. Called once at
148
+ * `app.ws()` registration, so every adapter dispatches validated messages with no per-adapter code.
149
+ */
150
+ export declare function wrapWebSocketMessageValidation(handler: WebSocketHandler): WebSocketHandler;
151
+ export {};
152
+ //# sourceMappingURL=websocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/server/websocket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * WebSocket seam — portable types shared by `@nifrajs/core`'s `app.ws()` and every serving adapter
3
+ * (Bun `listen()`, `@nifrajs/node`, `@nifrajs/deno`, `toFetchHandler` on Workers). No runtime code here, so
4
+ * it stays edge-safe and dependency-free; each adapter implements {@link NifraWebSocket} over its
5
+ * runtime's native socket and dispatches to the handler.
6
+ *
7
+ * A WS upgrade can't go through `app.fetch` (which has no socket) — it's adapter-integrated. The
8
+ * adapter calls `app.resolveWebSocketUpgrade(req)` (runs the route's `upgrade()` guard in a real
9
+ * request context), then performs the runtime upgrade with the returned data.
10
+ */
11
+ import { validateStandard } from "../schema/standard.js";
12
+ /**
13
+ * In-process pub/sub for `ws.subscribe(topic)` + `app.publish(topic, data)`. **Single-instance only** —
14
+ * topics live in this process's memory, so a multi-instance deploy (multiple servers behind a load
15
+ * balancer) needs an external fan-out (Redis pub/sub, a Cloudflare Durable Object, NATS, …) bridged to
16
+ * `app.publish`. The same registry instance is shared by an app's connections and its `publish`.
17
+ */
18
+ export class TopicRegistry {
19
+ topics = new Map();
20
+ memberships = new WeakMap();
21
+ subscribe(topic, ws) {
22
+ let subs = this.topics.get(topic);
23
+ if (subs === undefined) {
24
+ subs = new Set();
25
+ this.topics.set(topic, subs);
26
+ }
27
+ subs.add(ws);
28
+ let mine = this.memberships.get(ws);
29
+ if (mine === undefined) {
30
+ mine = new Set();
31
+ this.memberships.set(ws, mine);
32
+ }
33
+ mine.add(topic);
34
+ }
35
+ unsubscribe(topic, ws) {
36
+ const subs = this.topics.get(topic);
37
+ if (subs !== undefined) {
38
+ subs.delete(ws);
39
+ if (subs.size === 0)
40
+ this.topics.delete(topic); // reclaim empty topics — no unbounded growth
41
+ }
42
+ this.memberships.get(ws)?.delete(topic);
43
+ }
44
+ /** Drop every subscription for a connection — called on close so a registry never leaks dead sockets. */
45
+ unsubscribeAll(ws) {
46
+ const mine = this.memberships.get(ws);
47
+ if (mine === undefined)
48
+ return;
49
+ for (const topic of mine) {
50
+ const subs = this.topics.get(topic);
51
+ if (subs !== undefined) {
52
+ subs.delete(ws);
53
+ if (subs.size === 0)
54
+ this.topics.delete(topic);
55
+ }
56
+ }
57
+ this.memberships.delete(ws);
58
+ }
59
+ /** Send `data` to every connection subscribed to `topic`. Per-socket send errors are isolated. */
60
+ publish(topic, data) {
61
+ const subs = this.topics.get(topic);
62
+ if (subs === undefined)
63
+ return;
64
+ for (const ws of subs) {
65
+ try {
66
+ ws.send(data);
67
+ }
68
+ catch {
69
+ /* a single dead/closing socket must not abort the broadcast to the rest */
70
+ }
71
+ }
72
+ }
73
+ }
74
+ /** Normalize a received binary frame to `Uint8Array` (text frames stay `string`). */
75
+ function toBinary(raw) {
76
+ if (raw instanceof Uint8Array)
77
+ return raw;
78
+ if (raw instanceof ArrayBuffer)
79
+ return new Uint8Array(raw);
80
+ if (ArrayBuffer.isView(raw))
81
+ return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
82
+ return new Uint8Array(0); // unknown (e.g. an unconverted Blob) — adapters set binaryType to avoid this
83
+ }
84
+ /**
85
+ * Wire a standard server-side `WebSocket` to a nifra {@link WebSocketHandler}, returning the portable
86
+ * {@link NifraWebSocket}. Shared by the Deno and Workers bridges. `openNow` fires `open` immediately
87
+ * (Workers, where the socket is already open after `accept()`); otherwise `open` waits for the socket's
88
+ * `open` event (Deno). Lifecycle callbacks are error-isolated — a throw (or async rejection) routes to
89
+ * `error()` and never tears the process down; binary frames are normalized to `Uint8Array`.
90
+ */
91
+ export function attachWebSocket(socket, handler, data, options) {
92
+ const { pubsub } = options;
93
+ const ws = {
94
+ send: (payload) => socket.send(payload),
95
+ close: (code, reason) => socket.close(code, reason),
96
+ get readyState() {
97
+ return socket.readyState;
98
+ },
99
+ subscribe: (topic) => pubsub.subscribe(topic, ws),
100
+ unsubscribe: (topic) => pubsub.unsubscribe(topic, ws),
101
+ data,
102
+ raw: socket,
103
+ };
104
+ const reportError = (error) => {
105
+ if (handler.error === undefined)
106
+ return;
107
+ try {
108
+ const r = handler.error(ws, error);
109
+ if (r instanceof Promise)
110
+ r.catch(() => { });
111
+ }
112
+ catch {
113
+ /* the error handler itself failed — last resort, swallow */
114
+ }
115
+ };
116
+ const safe = (call) => {
117
+ try {
118
+ const r = call();
119
+ if (r instanceof Promise)
120
+ r.catch(reportError);
121
+ }
122
+ catch (error) {
123
+ reportError(error);
124
+ }
125
+ };
126
+ // Deliver binary as ArrayBuffer (→ Uint8Array) rather than Blob, where the runtime allows it.
127
+ if ("binaryType" in socket)
128
+ socket.binaryType = "arraybuffer";
129
+ socket.addEventListener("message", (event) => {
130
+ const raw = event.data;
131
+ const payload = typeof raw === "string" ? raw : toBinary(raw);
132
+ safe(() => handler.message?.(ws, payload));
133
+ });
134
+ socket.addEventListener("close", (event) => {
135
+ pubsub.unsubscribeAll(ws); // drop topic subscriptions so the registry never holds a dead socket
136
+ safe(() => handler.close?.(ws, event.code ?? 1005, event.reason ?? ""));
137
+ });
138
+ socket.addEventListener("error", (event) => reportError(event));
139
+ if (options.openNow) {
140
+ safe(() => handler.open?.(ws));
141
+ }
142
+ else {
143
+ socket.addEventListener("open", () => safe(() => handler.open?.(ws)));
144
+ }
145
+ return ws;
146
+ }
147
+ const WS_MESSAGE_DECODER = new TextDecoder();
148
+ /**
149
+ * If the handler declares a `messageSchema`, return a copy whose `message` validates each frame —
150
+ * parse as JSON, run the Standard Schema, then call the user's `message` with the typed value, or
151
+ * `onInvalidMessage` on failure. Returns the handler unchanged when no schema is set. Called once at
152
+ * `app.ws()` registration, so every adapter dispatches validated messages with no per-adapter code.
153
+ */
154
+ export function wrapWebSocketMessageValidation(handler) {
155
+ const schema = handler.messageSchema;
156
+ if (schema === undefined)
157
+ return handler;
158
+ // The handler is type-erased here; the user's `message` really accepts the schema's validated output
159
+ // (typed at the real call site), so widen its param to `unknown` for the internal call.
160
+ const userMessage = handler.message;
161
+ const onInvalid = handler.onInvalidMessage;
162
+ const validatingMessage = (ws, raw) => {
163
+ let parsed;
164
+ try {
165
+ parsed = JSON.parse(typeof raw === "string" ? raw : WS_MESSAGE_DECODER.decode(raw));
166
+ }
167
+ catch {
168
+ return onInvalid?.(ws, [{ message: "invalid JSON" }], raw);
169
+ }
170
+ const outcome = validateStandard(schema, parsed);
171
+ const finish = (o) => o.ok ? userMessage?.(ws, o.value) : onInvalid?.(ws, o.issues, raw);
172
+ return outcome instanceof Promise ? outcome.then(finish) : finish(outcome);
173
+ };
174
+ // The wrapped `message` takes the raw frame (adapters pass that) and produces the typed value
175
+ // internally; the stored handler is type-erased, so the signature is sound.
176
+ return { ...handler, message: validatingMessage };
177
+ }
178
+ //# sourceMappingURL=websocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/server/websocket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Bun WebSocket plumbing — the `websocket` dispatcher config `listen()` hands to Bun's serve when
3
+ * the app has WS routes. Lives behind the `@nifrajs/core/ws` subpath (via `ws-hook.ts`) so the base
4
+ * bundle of a no-WebSocket app never ships it; `server.ts` only imports the **types** from here
5
+ * (erased). No `Bun.*` calls here — the runtime seam stays in `server.ts` (see runtime-boundary.test).
6
+ */
7
+ import type { NifraWebSocket, TopicRegistry, WebSocketHandler } from "./websocket.js";
8
+ import type { BunWsHandlers } from "./ws-hook.js";
9
+ /** Per-connection state Bun's `websocket` callbacks read via `ws.data`: the matched handler, the
10
+ * `upgrade()`-seeded user data, and the memoized portable {@link NifraWebSocket} wrapper. */
11
+ export interface BunWsData {
12
+ readonly handler: WebSocketHandler;
13
+ data: unknown;
14
+ nifra?: NifraWebSocket;
15
+ }
16
+ /** Structural view of Bun's `ServerWebSocket` — keeps `Bun.*` types out of the public `.d.ts`
17
+ * (`listen()` casts for the same reason). The real socket satisfies this. */
18
+ export interface BunSocket {
19
+ send(data: string | ArrayBufferView | ArrayBuffer, compress?: boolean): number;
20
+ close(code?: number, reason?: string): void;
21
+ readonly readyState: number;
22
+ readonly data: BunWsData;
23
+ }
24
+ /** The shared Bun `websocket` dispatcher config for one app — each connection's
25
+ * `ws.data.handler` is the matched route's handler, set by `server.upgrade`. */
26
+ export declare function createBunWsHandlers(topics: TopicRegistry): BunWsHandlers;
27
+ //# sourceMappingURL=ws-bun.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-bun.d.ts","sourceRoot":"","sources":["../../src/server/ws-bun.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAiB,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACpG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAEjD;6FAC6F;AAC7F,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;IAClC,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,CAAC,EAAE,cAAc,CAAA;CACvB;AAED;6EAC6E;AAC7E,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,WAAW,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IAC9E,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAE3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CACzB;AAwDD;gFACgF;AAChF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAqBxE"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Bun WebSocket plumbing — the `websocket` dispatcher config `listen()` hands to Bun's serve when
3
+ * the app has WS routes. Lives behind the `@nifrajs/core/ws` subpath (via `ws-hook.ts`) so the base
4
+ * bundle of a no-WebSocket app never ships it; `server.ts` only imports the **types** from here
5
+ * (erased). No `Bun.*` calls here — the runtime seam stays in `server.ts` (see runtime-boundary.test).
6
+ */
7
+ /** Normalize a received binary frame to `Uint8Array` (Bun hands a `Buffer`, already one; an
8
+ * `ArrayBuffer` is wrapped). Text frames stay `string`. */
9
+ function toBinaryData(message) {
10
+ return message instanceof Uint8Array ? message : new Uint8Array(message);
11
+ }
12
+ /** Wrap a Bun `ServerWebSocket` as the portable {@link NifraWebSocket} handed to WS callbacks. */
13
+ function wrapBunSocket(raw, topics) {
14
+ const ws = {
15
+ send: (data) => {
16
+ raw.send(data);
17
+ },
18
+ close: (code, reason) => raw.close(code, reason),
19
+ get readyState() {
20
+ return raw.readyState;
21
+ },
22
+ subscribe: (topic) => topics.subscribe(topic, ws),
23
+ unsubscribe: (topic) => topics.unsubscribe(topic, ws),
24
+ get data() {
25
+ return raw.data.data;
26
+ },
27
+ set data(value) {
28
+ raw.data.data = value;
29
+ },
30
+ raw,
31
+ };
32
+ return ws;
33
+ }
34
+ function reportWsError(error, ws, handler) {
35
+ if (handler.error === undefined)
36
+ return;
37
+ try {
38
+ const result = handler.error(ws, error);
39
+ if (result instanceof Promise)
40
+ result.catch(() => { }); // a throwing error handler is the last resort
41
+ }
42
+ catch {
43
+ /* swallow: the error handler itself failed; nothing left to do but not crash */
44
+ }
45
+ }
46
+ /** Run a WS lifecycle callback, routing a sync throw or async rejection to `handler.error` so a
47
+ * failing callback never crashes the socket loop. */
48
+ function dispatchWsCallback(call, ws, handler) {
49
+ try {
50
+ const result = call();
51
+ if (result instanceof Promise)
52
+ result.catch((e) => reportWsError(e, ws, handler));
53
+ }
54
+ catch (error) {
55
+ reportWsError(error, ws, handler);
56
+ }
57
+ }
58
+ /** The shared Bun `websocket` dispatcher config for one app — each connection's
59
+ * `ws.data.handler` is the matched route's handler, set by `server.upgrade`. */
60
+ export function createBunWsHandlers(topics) {
61
+ return {
62
+ open: (raw) => {
63
+ const ws = raw;
64
+ const nifra = wrapBunSocket(ws, topics);
65
+ ws.data.nifra = nifra;
66
+ dispatchWsCallback(() => ws.data.handler.open?.(nifra), nifra, ws.data.handler);
67
+ },
68
+ message: (raw, message) => {
69
+ const ws = raw;
70
+ const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics);
71
+ const data = typeof message === "string" ? message : toBinaryData(message);
72
+ dispatchWsCallback(() => ws.data.handler.message?.(nifra, data), nifra, ws.data.handler);
73
+ },
74
+ close: (raw, code, reason) => {
75
+ const ws = raw;
76
+ const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics);
77
+ topics.unsubscribeAll(nifra); // drop topic subscriptions so the registry never holds a dead socket
78
+ dispatchWsCallback(() => ws.data.handler.close?.(nifra, code, reason), nifra, ws.data.handler);
79
+ },
80
+ };
81
+ }
82
+ //# sourceMappingURL=ws-bun.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-bun.js","sourceRoot":"","sources":["../../src/server/ws-bun.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Lazy WebSocket runtime seam. `server.ts` never imports the WS implementation statically — the
3
+ * `@nifrajs/core/ws` subpath entry registers it here on import. That keeps the WS dispatcher, pub/sub
4
+ * registry, Bun socket plumbing, and message-schema validation out of the base server bundle for
5
+ * apps that never call `app.ws()` (~1.5 KB gzipped on the minimal-app benchmark).
6
+ *
7
+ * Only type imports below — this module must stay weightless, it is always in the base bundle.
8
+ */
9
+ import type { NifraWebSocket, StandardWebSocket, TopicRegistry, WebSocketHandler } from "./websocket.js";
10
+ /** The Bun serve `websocket` callback trio, closed over one app's `TopicRegistry`. The `ws`
11
+ * params are `unknown` so `Bun.*` types never leak into the public `.d.ts`; the implementation
12
+ * narrows to its structural `BunSocket` view. */
13
+ export interface BunWsHandlers {
14
+ open(ws: unknown): void;
15
+ message(ws: unknown, message: string | ArrayBuffer | Uint8Array): void;
16
+ close(ws: unknown, code: number, reason: string): void;
17
+ }
18
+ /** What `@nifrajs/core/ws` registers: everything `server.ts` and `toFetchHandler` need at runtime. */
19
+ export interface WsRuntime {
20
+ /** `wrapWebSocketMessageValidation` — applied once at `app.ws()` registration. */
21
+ wrapHandler(handler: WebSocketHandler): WebSocketHandler;
22
+ /** One in-process pub/sub registry per app (backs `ws.subscribe` + `app.publish`). */
23
+ createTopics(): TopicRegistry;
24
+ /** The Bun `websocket` config for `listen()` when the app has WS routes. */
25
+ bunHandlers(topics: TopicRegistry): BunWsHandlers;
26
+ /** `attachWebSocket` — wires a standard server socket (Workers `WebSocketPair`) to a handler. */
27
+ attach(socket: StandardWebSocket, handler: WebSocketHandler, data: unknown, options: {
28
+ openNow: boolean;
29
+ pubsub: TopicRegistry;
30
+ }): NifraWebSocket;
31
+ }
32
+ /** Called by `@nifrajs/core/ws` on import. Last registration wins (idempotent in practice — there is
33
+ * one implementation; double-import is a no-op re-set of the same functions). */
34
+ export declare function setWsRuntime(impl: WsRuntime): void;
35
+ export declare function getWsRuntime(): WsRuntime | undefined;
36
+ //# sourceMappingURL=ws-hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-hook.d.ts","sourceRoot":"","sources":["../../src/server/ws-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,gBAAgB,EACjB,MAAM,gBAAgB,CAAA;AAEvB;;iDAEiD;AACjD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,IAAI,CAAA;IACtE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACvD;AAED,sGAAsG;AACtG,MAAM,WAAW,SAAS;IACxB,kFAAkF;IAClF,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,gBAAgB,CAAA;IACxD,sFAAsF;IACtF,YAAY,IAAI,aAAa,CAAA;IAC7B,4EAA4E;IAC5E,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAAA;IACjD,iGAAiG;IACjG,MAAM,CACJ,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,OAAO,EACb,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,aAAa,CAAA;KAAE,GACnD,cAAc,CAAA;CAClB;AAID;iFACiF;AACjF,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAElD;AAED,wBAAgB,YAAY,IAAI,SAAS,GAAG,SAAS,CAEpD"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Lazy WebSocket runtime seam. `server.ts` never imports the WS implementation statically — the
3
+ * `@nifrajs/core/ws` subpath entry registers it here on import. That keeps the WS dispatcher, pub/sub
4
+ * registry, Bun socket plumbing, and message-schema validation out of the base server bundle for
5
+ * apps that never call `app.ws()` (~1.5 KB gzipped on the minimal-app benchmark).
6
+ *
7
+ * Only type imports below — this module must stay weightless, it is always in the base bundle.
8
+ */
9
+ let runtime;
10
+ /** Called by `@nifrajs/core/ws` on import. Last registration wins (idempotent in practice — there is
11
+ * one implementation; double-import is a no-op re-set of the same functions). */
12
+ export function setWsRuntime(impl) {
13
+ runtime = impl;
14
+ }
15
+ export function getWsRuntime() {
16
+ return runtime;
17
+ }
18
+ //# sourceMappingURL=ws-hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-hook.js","sourceRoot":"","sources":["../../src/server/ws-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmCH,IAAI,OAA8B,CAAA;AAElC;iFACiF;AACjF,MAAM,UAAU,YAAY,CAAC,IAAe;IAC1C,OAAO,GAAG,IAAI,CAAA;AAChB,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,OAAO,CAAA;AAChB,CAAC"}
package/dist/ws.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * WebSocket runtime for `@nifrajs/core` — import once at your server entry to enable `app.ws()`:
3
+ *
4
+ * import "@nifrajs/core/ws"
5
+ *
6
+ * const app = server().ws("/chat", { message: (ws, data) => ws.send(data) })
7
+ *
8
+ * Why a subpath: an app that never uses WebSockets shouldn't ship the WS dispatcher, pub/sub
9
+ * registry, and Bun socket plumbing in its server bundle (~1.5 KB gzipped on the minimal-app size
10
+ * benchmark). Importing this module registers the runtime with the core server (idempotent);
11
+ * calling `app.ws()` without it fails loud at boot with a `WS_RUNTIME_MISSING` error pointing here.
12
+ *
13
+ * Serving adapters that wire standard sockets themselves (`@nifrajs/workers`) import
14
+ * `attachWebSocket` / `TopicRegistry` from here too.
15
+ */
16
+ import { attachWebSocket, TopicRegistry, wrapWebSocketMessageValidation } from "./server/websocket.js";
17
+ export type { NifraWebSocket, StandardWebSocket, WebSocketContext, WebSocketData, WebSocketHandler, WebSocketUpgradeOutcome, } from "./server/websocket.js";
18
+ export { attachWebSocket, TopicRegistry, wrapWebSocketMessageValidation };
19
+ //# sourceMappingURL=ws.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,eAAe,EACf,aAAa,EACb,8BAA8B,EAC/B,MAAM,uBAAuB,CAAA;AAW9B,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,8BAA8B,EAAE,CAAA"}
package/dist/ws.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * WebSocket runtime for `@nifrajs/core` — import once at your server entry to enable `app.ws()`:
3
+ *
4
+ * import "@nifrajs/core/ws"
5
+ *
6
+ * const app = server().ws("/chat", { message: (ws, data) => ws.send(data) })
7
+ *
8
+ * Why a subpath: an app that never uses WebSockets shouldn't ship the WS dispatcher, pub/sub
9
+ * registry, and Bun socket plumbing in its server bundle (~1.5 KB gzipped on the minimal-app size
10
+ * benchmark). Importing this module registers the runtime with the core server (idempotent);
11
+ * calling `app.ws()` without it fails loud at boot with a `WS_RUNTIME_MISSING` error pointing here.
12
+ *
13
+ * Serving adapters that wire standard sockets themselves (`@nifrajs/workers`) import
14
+ * `attachWebSocket` / `TopicRegistry` from here too.
15
+ */
16
+ import { attachWebSocket, TopicRegistry, wrapWebSocketMessageValidation, } from "./server/websocket.js";
17
+ import { createBunWsHandlers } from "./server/ws-bun.js";
18
+ import { setWsRuntime } from "./server/ws-hook.js";
19
+ setWsRuntime({
20
+ wrapHandler: wrapWebSocketMessageValidation,
21
+ createTopics: () => new TopicRegistry(),
22
+ bunHandlers: createBunWsHandlers,
23
+ attach: attachWebSocket,
24
+ });
25
+ export { attachWebSocket, TopicRegistry, wrapWebSocketMessageValidation };
26
+ //# sourceMappingURL=ws.js.map
package/dist/ws.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws.js","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,eAAe,EACf,aAAa,EACb,8BAA8B,GAC/B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAElD,YAAY,CAAC;IACX,WAAW,EAAE,8BAA8B;IAC3C,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,aAAa,EAAE;IACvC,WAAW,EAAE,mBAAmB;IAChC,MAAM,EAAE,eAAe;CACxB,CAAC,CAAA;AAUF,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,8BAA8B,EAAE,CAAA"}