@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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/brand.d.ts +11 -0
- package/dist/internal/brand.d.ts.map +1 -0
- package/dist/internal/brand.js +10 -0
- package/dist/internal/brand.js.map +1 -0
- package/dist/internal/secret.d.ts +9 -0
- package/dist/internal/secret.d.ts.map +1 -0
- package/dist/internal/secret.js +19 -0
- package/dist/internal/secret.js.map +1 -0
- package/dist/router/router.d.ts +61 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/router.js +324 -0
- package/dist/router/router.js.map +1 -0
- package/dist/schema/standard.d.ts +46 -0
- package/dist/schema/standard.d.ts.map +1 -0
- package/dist/schema/standard.js +20 -0
- package/dist/schema/standard.js.map +1 -0
- package/dist/seo.d.ts +48 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +109 -0
- package/dist/seo.js.map +1 -0
- package/dist/server/body.d.ts +39 -0
- package/dist/server/body.d.ts.map +1 -0
- package/dist/server/body.js +85 -0
- package/dist/server/body.js.map +1 -0
- package/dist/server/context.d.ts +111 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +2 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/contract.d.ts +142 -0
- package/dist/server/contract.d.ts.map +1 -0
- package/dist/server/contract.js +59 -0
- package/dist/server/contract.js.map +1 -0
- package/dist/server/cookies.d.ts +41 -0
- package/dist/server/cookies.d.ts.map +1 -0
- package/dist/server/cookies.js +181 -0
- package/dist/server/cookies.js.map +1 -0
- package/dist/server/logger.d.ts +49 -0
- package/dist/server/logger.d.ts.map +1 -0
- package/dist/server/logger.js +125 -0
- package/dist/server/logger.js.map +1 -0
- package/dist/server/registry.d.ts +46 -0
- package/dist/server/registry.d.ts.map +1 -0
- package/dist/server/registry.js +2 -0
- package/dist/server/registry.js.map +1 -0
- package/dist/server/server.d.ts +528 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +1660 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/sse.d.ts +54 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +126 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/server/webhook.d.ts +42 -0
- package/dist/server/webhook.d.ts.map +1 -0
- package/dist/server/webhook.js +149 -0
- package/dist/server/webhook.js.map +1 -0
- package/dist/server/websocket.d.ts +152 -0
- package/dist/server/websocket.d.ts.map +1 -0
- package/dist/server/websocket.js +178 -0
- package/dist/server/websocket.js.map +1 -0
- package/dist/server/ws-bun.d.ts +27 -0
- package/dist/server/ws-bun.d.ts.map +1 -0
- package/dist/server/ws-bun.js +82 -0
- package/dist/server/ws-bun.js.map +1 -0
- package/dist/server/ws-hook.d.ts +36 -0
- package/dist/server/ws-hook.d.ts.map +1 -0
- package/dist/server/ws-hook.js +18 -0
- package/dist/server/ws-hook.js.map +1 -0
- package/dist/ws.d.ts +19 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +26 -0
- package/dist/ws.js.map +1 -0
- package/package.json +63 -0
- package/src/errors.ts +41 -0
- package/src/index.ts +115 -0
- package/src/internal/brand.ts +11 -0
- package/src/internal/secret.ts +24 -0
- package/src/router/router.ts +429 -0
- package/src/schema/standard.ts +76 -0
- package/src/seo.ts +155 -0
- package/src/server/body.ts +89 -0
- package/src/server/context.ts +124 -0
- package/src/server/contract.ts +209 -0
- package/src/server/cookies.ts +198 -0
- package/src/server/logger.ts +167 -0
- package/src/server/registry.ts +63 -0
- package/src/server/server.ts +2479 -0
- package/src/server/sse.ts +161 -0
- package/src/server/webhook.ts +207 -0
- package/src/server/websocket.ts +308 -0
- package/src/server/ws-bun.ts +106 -0
- package/src/server/ws-hook.ts +53 -0
- 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
|
package/dist/ws.d.ts.map
ADDED
|
@@ -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"}
|