@firtoz/socka 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +191 -40
  2. package/dist/{SockaWebSocketSession-Bru8yFcK.d.ts → SockaWebSocketSession-Cza7Fti-.d.ts} +87 -5
  3. package/dist/bun/index.d.ts +28 -3
  4. package/dist/bun/index.js +28 -5
  5. package/dist/bun/index.js.map +1 -1
  6. package/dist/{chunk-MZCQHJXY.js → chunk-2FNWVCP3.js} +27 -8
  7. package/dist/chunk-2FNWVCP3.js.map +1 -0
  8. package/dist/{chunk-AM7PB26G.js → chunk-H3S3435J.js} +125 -3
  9. package/dist/chunk-H3S3435J.js.map +1 -0
  10. package/dist/{chunk-45D4T232.js → chunk-JVLUA3Q5.js} +64 -6
  11. package/dist/chunk-JVLUA3Q5.js.map +1 -0
  12. package/dist/chunk-KQO5AVKA.js +8 -0
  13. package/dist/chunk-KQO5AVKA.js.map +1 -0
  14. package/dist/client/index.d.ts +59 -3
  15. package/dist/client/index.js +2 -2
  16. package/dist/core/index.d.ts +5 -1
  17. package/dist/core/index.js +1 -1
  18. package/dist/do/index.d.ts +20 -2
  19. package/dist/do/index.js +35 -2
  20. package/dist/do/index.js.map +1 -1
  21. package/dist/hono/cloudflare-workers.d.ts +2 -2
  22. package/dist/hono/cloudflare-workers.js +4 -3
  23. package/dist/hono/cloudflare-workers.js.map +1 -1
  24. package/dist/hono/index.d.ts +20 -4
  25. package/dist/hono/index.js +5 -3
  26. package/dist/hono/index.js.map +1 -1
  27. package/dist/react/index.d.ts +43 -4
  28. package/dist/react/index.js +103 -9
  29. package/dist/react/index.js.map +1 -1
  30. package/dist/server/index.d.ts +17 -4
  31. package/dist/server/index.js +24 -4
  32. package/dist/server/index.js.map +1 -1
  33. package/dist/{socka-report-error-DzFI2Tr7.d.ts → socka-report-error-ixTynx4w.d.ts} +8 -1
  34. package/dist/test/index.d.ts +11 -0
  35. package/dist/test/index.js +84 -0
  36. package/dist/test/index.js.map +1 -0
  37. package/docs/README.md +15 -6
  38. package/docs/auth.md +27 -0
  39. package/docs/backpressure.md +16 -0
  40. package/docs/client.md +44 -3
  41. package/docs/comparison.md +1 -1
  42. package/docs/durable-objects.md +2 -2
  43. package/docs/getting-started.md +143 -84
  44. package/docs/history.md +26 -0
  45. package/docs/internals.md +56 -0
  46. package/docs/lifecycle.md +3 -3
  47. package/docs/multi-room.md +10 -8
  48. package/docs/peers.md +11 -7
  49. package/docs/presence.md +43 -0
  50. package/docs/{events.md → pushes.md} +1 -1
  51. package/docs/recipes.md +78 -0
  52. package/docs/reconnection.md +44 -0
  53. package/docs/reference.md +21 -30
  54. package/docs/server.md +14 -1
  55. package/docs/testing.md +20 -0
  56. package/docs/wire-format.md +25 -0
  57. package/examples/minimal-socka.ts +56 -3
  58. package/package.json +14 -10
  59. package/dist/chunk-45D4T232.js.map +0 -1
  60. package/dist/chunk-AM7PB26G.js.map +0 -1
  61. package/dist/chunk-MZCQHJXY.js.map +0 -1
package/README.md CHANGED
@@ -10,11 +10,13 @@
10
10
 
11
11
  ![Socka — WebSocket RPC, Standard Schema](./assets/banner.png)
12
12
 
13
- **Typed WebSocket RPC for TypeScript.** Define one contract, get **`session.send.*`** in the client and **`handlers`** on the servervalidated, correlated, done.
13
+ **Typed WebSocket RPC and pushes for TypeScript.** One **`defineSocka`** contract gives you **`session.send.*`** for RPCs and **`session.subscribe`** for **typed server pushes**—validated, correlated, same schema on client and server.
14
14
 
15
15
  **npm:** [`@firtoz/socka`](https://www.npmjs.com/package/@firtoz/socka). *Socka* is the project name in prose; **install and `import` paths always use `@firtoz/socka` or `@firtoz/socka/...`**. The published artifact is **compiled ESM + `.d.ts` in `dist/`** (see `package.json` `exports`).
16
16
 
17
- ## 30-second example (Bun)
17
+ ## Minimal example: multi-room chat (Bun)
18
+
19
+ Join/leave and live messages use **`pushes`**; persisted lines use **`listHistory`**; who is connected uses **`listPresence`**; **`clearHistory`** wipes stored messages and **`historyCleared`** notifies the room (examples also show presence in the UI). This snippet keeps **history in memory** so it stays short—see **[chatroom-bun](../../examples/chatroom-bun)** for **SQLite**, **[chatroom-hono](../../examples/chatroom-hono)** for **file JSON**, and **[chatroom-do](../../examples/chatroom-do)** for **Durable Object SQLite**.
18
20
 
19
21
  **`contract.ts`** (shared):
20
22
 
@@ -22,35 +24,158 @@
22
24
  import { defineSocka } from "@firtoz/socka/core";
23
25
  import * as z from "zod";
24
26
 
25
- export const myContract = defineSocka({
27
+ export const messageRow = z.object({
28
+ id: z.string(),
29
+ ts: z.number(),
30
+ userId: z.string(),
31
+ displayName: z.string(),
32
+ text: z.string(),
33
+ });
34
+
35
+ export type ChatMessageRow = z.infer<typeof messageRow>;
36
+
37
+ const onlineUser = z.object({
38
+ userId: z.string(),
39
+ displayName: z.string(),
40
+ });
41
+
42
+ export const chatContract = defineSocka({
26
43
  calls: {
27
- echo: {
28
- input: z.object({ text: z.string() }),
29
- output: z.object({ text: z.string() }),
44
+ listHistory: {
45
+ input: z.object({ limit: z.number().int().min(1).max(500).optional() }),
46
+ output: z.object({ messages: z.array(messageRow) }),
47
+ },
48
+ listPresence: {
49
+ input: z.object({}).optional(),
50
+ output: z.object({
51
+ selfUserId: z.string(),
52
+ users: z.array(onlineUser),
53
+ }),
54
+ },
55
+ sendMessage: {
56
+ input: z.object({ text: z.string().min(1) }),
57
+ output: z.object({ ok: z.literal(true) }),
58
+ },
59
+ clearHistory: {
60
+ input: z.object({}).optional(),
61
+ output: z.object({ ok: z.literal(true) }),
30
62
  },
31
63
  },
64
+ pushes: {
65
+ userJoined: z.object({ userId: z.string(), displayName: z.string() }),
66
+ userLeft: z.object({
67
+ userId: z.string(),
68
+ displayName: z.string(),
69
+ }),
70
+ roomMessage: messageRow,
71
+ historyCleared: z.object({
72
+ ts: z.number(),
73
+ clearedByUserId: z.string(),
74
+ clearedByDisplayName: z.string(),
75
+ }),
76
+ },
32
77
  });
33
78
  ```
34
79
 
35
- **`server.ts`**:
80
+ **`server.ts`** — **`createSockaRoomRegistry`** holds one **`sessionMap` + config per room**; **`strictUpgradeRequest: true`** types **`createData`** with a real **`Request`** (no `http://_/` placeholder). **`session.listPeers()`** replaces hand-rolled **`sessionMap`** walks for presence.
36
81
 
37
82
  ```ts
83
+ import type { ServerWebSocket } from "bun";
38
84
  import { createSockaBunWebSocketHandlers } from "@firtoz/socka/bun";
39
- import { myContract } from "./contract";
85
+ import {
86
+ createSockaRoomRegistry,
87
+ type SockaStrictWebSocketInit,
88
+ type SockaWebSocketSessionConfig,
89
+ } from "@firtoz/socka/server";
90
+ import { type ChatMessageRow, chatContract } from "./contract";
91
+
92
+ type SessionData = { roomId: string; userId: string; displayName: string };
93
+
94
+ /** In-memory demo store — swap for SQLite / files / DO in real apps. */
95
+ const history = new Map<string, ChatMessageRow[]>();
96
+
97
+ const registry = createSockaRoomRegistry(
98
+ (roomId): SockaWebSocketSessionConfig<typeof chatContract, SessionData> => ({
99
+ contract: chatContract,
100
+ strictUpgradeRequest: true,
101
+ createData: (init: SockaStrictWebSocketInit) => {
102
+ const u = new URL(init.request.url);
103
+ const displayName = u.searchParams.get("name")?.trim() || "anon";
104
+ return { roomId, userId: crypto.randomUUID(), displayName };
105
+ },
106
+ onAttached: async (session) => {
107
+ await session.broadcastPush(
108
+ "userJoined",
109
+ { userId: session.data.userId, displayName: session.data.displayName },
110
+ true,
111
+ );
112
+ },
113
+ handlers: {
114
+ listHistory: async (input, session) => {
115
+ const lim = input.limit ?? 200;
116
+ const rows = history.get(session.data.roomId) ?? [];
117
+ return { messages: rows.slice(-lim) };
118
+ },
119
+ listPresence: async (_input, session) => {
120
+ const users = session.listPeers().map((d) => ({
121
+ userId: d.userId,
122
+ displayName: d.displayName,
123
+ }));
124
+ users.sort((a, b) => a.displayName.localeCompare(b.displayName));
125
+ return { selfUserId: session.data.userId, users };
126
+ },
127
+ sendMessage: async (input, session) => {
128
+ const row = {
129
+ id: crypto.randomUUID(),
130
+ ts: Date.now(),
131
+ userId: session.data.userId,
132
+ displayName: session.data.displayName,
133
+ text: input.text,
134
+ };
135
+ const list = history.get(session.data.roomId) ?? [];
136
+ list.push(row);
137
+ history.set(session.data.roomId, list);
138
+ await session.broadcastPush("roomMessage", row);
139
+ return { ok: true as const };
140
+ },
141
+ clearHistory: async (_input, session) => {
142
+ history.set(session.data.roomId, []);
143
+ const ts = Date.now();
144
+ await session.broadcastPush("historyCleared", {
145
+ ts,
146
+ clearedByUserId: session.data.userId,
147
+ clearedByDisplayName: session.data.displayName,
148
+ });
149
+ return { ok: true as const };
150
+ },
151
+ },
152
+ handleClose: async (session) => {
153
+ await session.broadcastPush(
154
+ "userLeft",
155
+ { userId: session.data.userId, displayName: session.data.displayName },
156
+ true,
157
+ );
158
+ },
159
+ }),
160
+ );
161
+
162
+ type BunWsData = { roomId: string; request: Request };
40
163
 
41
164
  const { websocket } = createSockaBunWebSocketHandlers({
42
- contract: myContract,
43
- handlers: {
44
- echo: async (input) => ({ text: input.text }),
165
+ resolveScope(ws: ServerWebSocket<BunWsData>) {
166
+ const { roomId } = ws.data;
167
+ const room = registry.get(roomId);
168
+ return { sessionMap: room.sessionMap, config: room.config };
45
169
  },
46
- handleClose: async () => {},
47
170
  });
48
171
 
49
- Bun.serve({
172
+ Bun.serve<BunWsData>({
50
173
  port: 3450,
51
174
  fetch(req, server) {
52
- if (new URL(req.url).pathname === "/ws") {
53
- if (server.upgrade(req)) return undefined;
175
+ const url = new URL(req.url);
176
+ if (url.pathname.startsWith("/ws/")) {
177
+ const roomId = decodeURIComponent(url.pathname.slice(4)) || "default";
178
+ if (server.upgrade(req, { data: { roomId, request: req } })) return undefined;
54
179
  return new Response("WebSocket upgrade failed", { status: 400 });
55
180
  }
56
181
  return new Response("OK");
@@ -59,40 +184,63 @@ Bun.serve({
59
184
  });
60
185
  ```
61
186
 
187
+ *Ports: this minimal snippet listens on **3450**; the [full-stack examples](#full-stack-examples) use **3461–3466**.*
188
+
62
189
  **`client.ts`** (browser or Bun):
63
190
 
64
191
  ```ts
65
192
  import { SockaSession } from "@firtoz/socka/client";
66
- import { myContract } from "./contract";
193
+ import { chatContract } from "./contract";
67
194
 
68
195
  const session = new SockaSession({
69
- contract: myContract,
70
- url: "ws://localhost:3450/ws",
196
+ contract: chatContract,
197
+ url: "ws://localhost:3450/ws/lobby?name=Ada",
71
198
  });
72
- const { text } = await session.send.echo({ text: "hello" });
73
- console.log(text);
199
+
200
+ session.subscribe.on("userJoined", (p) => console.log("joined", p));
201
+ session.subscribe.on("userLeft", (p) => console.log("left", p.displayName));
202
+ session.subscribe.on("roomMessage", (m) => console.log(`${m.displayName}: ${m.text}`));
203
+ session.subscribe.on("historyCleared", (p) =>
204
+ console.log("history cleared by", p.clearedByDisplayName, "at", p.ts),
205
+ );
206
+
207
+ const { messages } = await session.send.listHistory({});
208
+ console.log("history", messages);
209
+ const { selfUserId, users } = await session.send.listPresence({});
210
+ console.log("online", selfUserId, users);
211
+ await session.send.sendMessage({ text: "hello room" });
212
+ await session.send.clearHistory({});
74
213
  ```
75
214
 
76
- Run **`bun run server.ts`**, then point the client at **`ws://localhost:3450/ws`**.
215
+ Run **`bun run server.ts`**, then point the client at the same **`ws://…/ws/<room>?name=…`** path you upgrade in **`fetch`**.
216
+
217
+ **More examples:** **[chatroom-bun](../../examples/chatroom-bun)** (SQLite + multi-room UI) · **[chatroom-hono](../../examples/chatroom-hono)** · **[chatroom-do](../../examples/chatroom-do)** · tic-tac-toe **[Bun](../../examples/tic-tac-toe-bun)** · **[Hono + Node](../../examples/tic-tac-toe-hono)** · **[Cloudflare DO](../../examples/tic-tac-toe-do)**.
77
218
 
78
219
  ## Install
79
220
 
80
- ```bash
81
- npm install @firtoz/socka
82
- ```
221
+ Always install **`@firtoz/socka`**, then add **only** what your imports need (`npm` / `pnpm` / `bun add` as you prefer):
83
222
 
84
- Also: `pnpm add @firtoz/socka` · `bun add @firtoz/socka`
223
+ | You are building… | Install |
224
+ |-------------------|---------|
225
+ | **Browser / Vite SPA** (client only) | `npm install @firtoz/socka` |
226
+ | **React** (`@firtoz/socka/react`) | `npm install @firtoz/socka react` — add **`@types/react`** as a dev dependency if TypeScript asks |
227
+ | **Bun** (`Bun.serve`, `@firtoz/socka/bun`) | `npm install @firtoz/socka` — add **`bun-types`** as a dev dependency if you type-check Bun APIs |
228
+ | **Node + Hono + `@hono/node-ws`** | `npm install @firtoz/socka hono @hono/node-ws @hono/node-server ws` — add **`@types/ws`** as a dev dependency when you use **`ws`** on Node |
229
+ | **Cloudflare Workers + Hono** (`@firtoz/socka/hono/cloudflare`) | `npm install @firtoz/socka hono` |
230
+ | **Cloudflare Durable Objects** (`@firtoz/socka/do`) | `npm install @firtoz/socka hono @firtoz/websocket-do` |
85
231
 
86
- Optional peers depend on which subpath you import—see **[Peers](./docs/peers.md)**.
232
+ For **Cloudflare TypeScript types**, prefer **`wrangler types`** (or your app’s typegen) so globals and bindings match your Worker see [Cloudflare’s TypeScript guide](https://developers.cloudflare.com/workers/languages/typescript). More detail: **[Peers](./docs/peers.md)**.
87
233
 
88
234
  ## Other runtimes
89
235
 
90
- | Runtime | Subpath | Guide |
91
- |--------|---------|--------|
92
- | **Node** + [`ws`](https://github.com/websockets/ws), or any standard **`WebSocket`** | `@firtoz/socka/server` | **[Server](./docs/server.md)** — `attachSockaWebSocket` |
93
- | **Bun** `Bun.serve` / `ServerWebSocket` | `@firtoz/socka/bun` | **[Server](./docs/server.md)** |
94
- | **Hono** on Node (`@hono/node-ws`) | `@firtoz/socka/hono` | **[Server](./docs/server.md)** |
95
- | **Hono** on Cloudflare Workers | `@firtoz/socka/hono/cloudflare` | **[Server](./docs/server.md)** |
236
+ Pick how the socket is upgraded, then use the matching import path and guide:
237
+
238
+ | Runtime | Import path | Quick start |
239
+ |---------|-------------|-------------|
240
+ | **Node + [`ws`](https://github.com/websockets/ws)** (or any standard **`WebSocket`** after upgrade) | `@firtoz/socka/server` | [`attachSockaWebSocket`](./docs/server.md) |
241
+ | **Bun** (`Bun.serve` / `ServerWebSocket`) | `@firtoz/socka/bun` | [`createSockaBunWebSocketHandlers`](./docs/server.md#firtoz-socka-bun-bunserve) |
242
+ | **Hono on Node** (`@hono/node-ws`) | `@firtoz/socka/hono` | [`sockaHonoNodeWs`](./docs/server.md#firtoz-socka-hono-node-hono-node-ws) |
243
+ | **Hono on Cloudflare Workers** | `@firtoz/socka/hono/cloudflare` | [`sockaHonoCloudflare`](./docs/server.md#firtoz-socka-hono-cloudflare-workers) |
96
244
  | **Cloudflare Durable Objects** | `@firtoz/socka/do` | **[Durable Objects](./docs/durable-objects.md)** |
97
245
 
98
246
  ## Why not socket.io, tRPC, or DIY?
@@ -100,6 +248,8 @@ Optional peers depend on which subpath you import—see **[Peers](./docs/peers.m
100
248
  - **Schema-first RPC + push** — one contract; no parallel “event” protocol for server pushes.
101
249
  - **Correlated envelopes** — request/response IDs and validation hooks are built in.
102
250
  - **Same contract** across Bun, Hono, Node `ws`, and Durable Objects (see **[Comparison](./docs/comparison.md)** for socket.io / tRPC / hand-rolled).
251
+ - **Room registry + presence helpers** — **`createSockaRoomRegistry`** for per-room **`sessionMap`** / config; **`session.listPeers()`** for who is in the room without walking maps by hand.
252
+ - **Strict upgrade typing + optional reconnect** — Bun/Hono can set **`strictUpgradeRequest: true`** so **`createData`** sees **`init.request`**; **`SockaWebSocketClient`** / **`SockaSession`** can **`reconnect`** with exponential backoff (see **[Reconnection](./docs/reconnection.md)**).
103
253
 
104
254
  ## Documentation
105
255
 
@@ -109,12 +259,13 @@ Hub: **[`docs/README.md`](./docs/README.md)** (getting started, peers, lifecycle
109
259
 
110
260
  ## Full-stack examples
111
261
 
112
- Self-contained **tic-tac-toe** apps in the monorepo [`examples/`](../../examples/) (same game, different servers):
113
-
114
- | Stack | Folder | Port |
115
- |--------|--------|------|
116
- | **Bun** (`@firtoz/socka/bun`) | [`tic-tac-toe-bun`](../../examples/tic-tac-toe-bun) | **3461** |
117
- | **Hono + Node** (`@firtoz/socka/hono`) | [`tic-tac-toe-hono`](../../examples/tic-tac-toe-hono) | **3462** |
118
- | **Cloudflare DO** (`@firtoz/socka/do`) | [`tic-tac-toe-do`](../../examples/tic-tac-toe-do) | **3463** |
262
+ | Topic | Stack | Folder | Port |
263
+ |-------|--------|--------|------|
264
+ | **Chat + history** | **Bun** + SQLite | [`chatroom-bun`](../../examples/chatroom-bun) | **3464** |
265
+ | **Chat + history** | **Hono + Node** + JSON files | [`chatroom-hono`](../../examples/chatroom-hono) | **3465** |
266
+ | **Chat + history** | **Cloudflare DO** + Drizzle SQLite | [`chatroom-do`](../../examples/chatroom-do) | **3466** |
267
+ | Tic-tac-toe | **Bun** | [`tic-tac-toe-bun`](../../examples/tic-tac-toe-bun) | **3461** |
268
+ | Tic-tac-toe | **Hono + Node** | [`tic-tac-toe-hono`](../../examples/tic-tac-toe-hono) | **3462** |
269
+ | Tic-tac-toe | **Cloudflare DO** | [`tic-tac-toe-do`](../../examples/tic-tac-toe-do) | **3463** |
119
270
 
120
- Each app: **`bun run dev`** (or **`wrangler dev`** for the DO example).
271
+ Chat apps: **`bun run dev`** (or **`wrangler dev`** for **`chatroom-do`**). Tic-tac-toe: same.
@@ -1,21 +1,77 @@
1
- import { S as SockaContract, a as SockaContractConfig, b as SockaWireFormat, e as InferSockaHandlers, y as SockaReportError, g as InferSockaPushPayload } from './socka-report-error-DzFI2Tr7.js';
1
+ import { S as SockaContract, a as SockaContractConfig, b as SockaWireFormat, e as InferSockaHandlers, y as SockaReportError, g as InferSockaPushPayload } from './socka-report-error-ixTynx4w.js';
2
2
 
3
3
  /** Session data with no fields — `createData` may be omitted (defaults to `{}`). */
4
4
  type EmptySockaSessionData$1 = Record<string, never>;
5
- /** Optional upgrade context for {@link SockaWebSocketSession}. */
5
+ /**
6
+ * Upgrade context passed into `createData` for {@link SockaWebSocketSession} when
7
+ * **`strictUpgradeRequest` is not set to `true`** (the default).
8
+ *
9
+ * **`request` is optional** because some call sites attach a socket without an HTTP upgrade
10
+ * (custom tests, unusual adapters). If you read query params or headers from the upgrade,
11
+ * you must handle a missing `request` (e.g. optional chaining and a fallback URL), or set
12
+ * **`strictUpgradeRequest: true`** instead so {@link SockaStrictWebSocketInit} applies.
13
+ */
6
14
  type SockaWebSocketInit = {
7
- /** Original HTTP request for the WebSocket upgrade, when available. */
15
+ /**
16
+ * Original HTTP **`Request`** for the WebSocket upgrade, when the adapter supplies it
17
+ * (e.g. the optional fourth argument to **`attachSockaWebSocket`**).
18
+ */
8
19
  request?: Request;
9
20
  };
21
+ /**
22
+ * Upgrade context when **`strictUpgradeRequest: true`** is set on
23
+ * {@link SockaWebSocketSessionConfig}.
24
+ *
25
+ * **What this enables:** `createData` is typed so **`init.request` is always defined**.
26
+ * You can use **`new URL(init.request.url)`** and read search params without
27
+ * `init.request?.url ?? "http://_/"` placeholders, and TypeScript will catch mistakes if you
28
+ * treat the request as optional.
29
+ *
30
+ * **Runtime behavior:** If the adapter does not pass a `Request` while strict mode is on,
31
+ * socka throws an error explaining how to wire the upgrade (e.g. Bun `data: { request: req }`,
32
+ * or Hono default `sockaInit`).
33
+ *
34
+ * **Typical use:** Bun **`Bun.serve`** upgrades and Hono **`sockaHonoNodeWs`** /
35
+ * **`sockaHonoCloudflare`** where the incoming HTTP request is available and should drive
36
+ * `session.data` (names, cookies, etc.).
37
+ */
38
+ type SockaStrictWebSocketInit = {
39
+ request: Request;
40
+ };
10
41
  type SockaWebSocketCreateData<TData> = [TData] extends [EmptySockaSessionData$1] ? {
11
42
  createData?: (init: SockaWebSocketInit) => TData;
12
43
  } : {
13
44
  createData: (init: SockaWebSocketInit) => TData;
14
45
  };
46
+ type SockaWebSocketCreateDataStrict<TData> = [TData] extends [
47
+ EmptySockaSessionData$1
48
+ ] ? {
49
+ createData?: (init: SockaStrictWebSocketInit) => TData;
50
+ } : {
51
+ createData: (init: SockaStrictWebSocketInit) => TData;
52
+ };
15
53
  type SockaSessionForHandlers<TContract extends SockaContract<SockaContractConfig>, TData> = SockaWebSocketSession<TContract, TData>;
16
54
  /**
17
55
  * Configuration for {@link SockaWebSocketSession}. Handlers receive the session
18
56
  * instance as the second argument (or the only argument when the procedure has no input).
57
+ *
58
+ * ## `strictUpgradeRequest` (optional flag)
59
+ *
60
+ * Controls how **`createData`** is typed and validated for the **HTTP upgrade**:
61
+ *
62
+ * - **Omitted or `undefined` (default)** — `createData` receives {@link SockaWebSocketInit}.
63
+ * **`init.request` may be missing.** Use this for adapters or tests that construct sessions
64
+ * without a real upgrade request, or when you intentionally support both cases and handle
65
+ * optional `request` in code.
66
+ *
67
+ * - **`true`** — Opt in to **strict** upgrade typing: `createData` receives
68
+ * {@link SockaStrictWebSocketInit}, so **`init.request` is required** in TypeScript and
69
+ * enforced at runtime. Prefer this for normal Bun/Hono apps that always have an upgrade
70
+ * `Request`, so you avoid placeholder URLs and get clearer errors if wiring is wrong.
71
+ *
72
+ * Bun and Hono helpers document how they populate init (e.g. **`sockaBunInitFromWsData`**,
73
+ * default **`sockaInit`** from Hono context). The published **Server** guide includes a
74
+ * **Strict upgrade request** section with examples.
19
75
  */
20
76
  type SockaWebSocketSessionConfig<TContract extends SockaContract<SockaContractConfig>, TData = EmptySockaSessionData$1> = {
21
77
  contract: TContract;
@@ -38,7 +94,11 @@ type SockaWebSocketSessionConfig<TContract extends SockaContract<SockaContractCo
38
94
  * (safe to broadcast to peers). Sync or async; async rejections are logged.
39
95
  */
40
96
  onAttached?: (session: SockaSessionForHandlers<TContract, TData>) => void | Promise<void>;
41
- } & SockaWebSocketCreateData<TData>;
97
+ } & (({
98
+ strictUpgradeRequest?: undefined;
99
+ } & SockaWebSocketCreateData<TData>) | ({
100
+ strictUpgradeRequest: true;
101
+ } & SockaWebSocketCreateDataStrict<TData>));
42
102
 
43
103
  /** Session data with no fields — `createData` may be omitted (defaults to `{}`). */
44
104
  type EmptySockaSessionData = Record<string, never>;
@@ -77,6 +137,28 @@ declare class SockaWebSocketSession<TContract extends SockaContract<SockaContrac
77
137
  private _data;
78
138
  constructor(websocket: WebSocket, sessions: Map<WebSocket, SockaWebSocketSession<TContract, TData>>, config: SockaWebSocketSessionConfig<TContract, TData>, init?: SockaWebSocketInit);
79
139
  get data(): TData;
140
+ /**
141
+ * Session data for every connection in the same {@link sessions} map (same room),
142
+ * optionally excluding this socket.
143
+ */
144
+ listPeers(options?: {
145
+ excludeSelf?: boolean;
146
+ }): TData[];
147
+ /**
148
+ * Like {@link listPeers} but maps each peer {@link SockaWebSocketSession}
149
+ * (e.g. when you need more than {@link #data}).
150
+ */
151
+ listPeersWith<R>(map: (session: SockaWebSocketSession<TContract, TData>) => R, options?: {
152
+ excludeSelf?: boolean;
153
+ }): R[];
154
+ /** Count of sessions in this room (same {@link sessions} map), optionally excluding self. */
155
+ peerCount(options?: {
156
+ excludeSelf?: boolean;
157
+ }): number;
158
+ /** Whether any peer sessions exist (optionally excluding self). */
159
+ hasPeers(options?: {
160
+ excludeSelf?: boolean;
161
+ }): boolean;
80
162
  /**
81
163
  * Invokes the user {@link typeof SockaWebSocketSessionConfig.handleClose} callback.
82
164
  * Server adapters should call this when the WebSocket closes, **before** deleting
@@ -104,4 +186,4 @@ declare class SockaWebSocketSession<TContract extends SockaContract<SockaContrac
104
186
  */
105
187
  declare function runSockaSessionOnAttached<TContract extends SockaContract<SockaContractConfig>, TData>(config: SockaWebSocketSessionConfig<TContract, TData>, session: SockaWebSocketSession<TContract, TData>): void;
106
188
 
107
- export { SockaWebSocketSession as S, type SockaWebSocketSessionConfig as a, type SockaWebSocketInit as b, broadcastSockaEventToPeers as c, type SockaEmitCapable as d, type SockaPushSession as e, runSockaSessionOnAttached as r };
189
+ export { type SockaStrictWebSocketInit as S, SockaWebSocketSession as a, type SockaWebSocketSessionConfig as b, type SockaWebSocketInit as c, type SockaPushSession as d, broadcastSockaEventToPeers as e, type SockaEmitCapable as f, runSockaSessionOnAttached as r };
@@ -1,8 +1,33 @@
1
1
  import { ServerWebSocket } from 'bun';
2
- import { S as SockaContract, a as SockaContractConfig, b as SockaWireFormat } from '../socka-report-error-DzFI2Tr7.js';
3
- import { S as SockaWebSocketSession, a as SockaWebSocketSessionConfig } from '../SockaWebSocketSession-Bru8yFcK.js';
2
+ import { S as SockaContract, a as SockaContractConfig, b as SockaWireFormat } from '../socka-report-error-ixTynx4w.js';
3
+ import { S as SockaStrictWebSocketInit, a as SockaWebSocketSession, b as SockaWebSocketSessionConfig } from '../SockaWebSocketSession-Cza7Fti-.js';
4
4
  import '@standard-schema/spec';
5
5
 
6
+ /**
7
+ * Reads the upgrade {@link Request} from Bun **`ServerWebSocket.data`** when your
8
+ * **`fetch`** handler stored it there (e.g. **`server.upgrade(req, { data: { roomId, request: req } })`**).
9
+ *
10
+ * Pair with **`strictUpgradeRequest: true`** on {@link SockaWebSocketSessionConfig} so
11
+ * **`createData`** is typed with {@link SockaStrictWebSocketInit} and **`init.request`**
12
+ * is always defined. If **`request`** is missing from **`data`**, this returns **`undefined`**
13
+ * and strict mode will throw when constructing the session — that usually means you forgot
14
+ * to pass **`request`** on upgrade.
15
+ */
16
+ declare function sockaBunInitFromWsData(ws: ServerWebSocket<unknown>): SockaStrictWebSocketInit | undefined;
17
+ /** `ServerWebSocket.data` shape after {@link sockaBunUpgrade}. */
18
+ type SockaBunUpgradeData<TExtra> = TExtra & {
19
+ request: Request;
20
+ };
21
+ /**
22
+ * Calls {@link Bun.Server.upgrade} with **`request: req`** merged into **`data`**, so
23
+ * {@link sockaBunInitFromWsData} and **`strictUpgradeRequest: true`** always see the HTTP
24
+ * upgrade request (query params, cookies path).
25
+ */
26
+ declare function sockaBunUpgrade<TExtra extends Record<string, unknown>>(server: {
27
+ upgrade: (req: Request, opts: {
28
+ data: SockaBunUpgradeData<TExtra>;
29
+ }) => boolean;
30
+ }, req: Request, data?: TExtra): boolean;
6
31
  type SockaBunResolveScope<TContract extends SockaContract<SockaContractConfig>, TData, TWsData = undefined> = (ws: ServerWebSocket<TWsData>) => {
7
32
  sessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;
8
33
  config: SockaWebSocketSessionConfig<TContract, TData>;
@@ -35,4 +60,4 @@ declare function createSockaBunWebSocketHandlers<TContract extends SockaContract
35
60
  resolveScope: SockaBunResolveScope<TContract, TData, TWsData>;
36
61
  }): SockaBunWebSocketHandlers<TContract, TData, TWsData>;
37
62
 
38
- export { type SockaBunResolveScope, type SockaBunWebSocketHandlers, createSockaBunWebSocketHandlers };
63
+ export { type SockaBunResolveScope, type SockaBunUpgradeData, type SockaBunWebSocketHandlers, createSockaBunWebSocketHandlers, sockaBunInitFromWsData, sockaBunUpgrade };
package/dist/bun/index.js CHANGED
@@ -1,14 +1,31 @@
1
1
  import { dispatchSockaInboundMessage } from '../chunk-5WQTYLIC.js';
2
- import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-45D4T232.js';
3
- import { reportSockaError } from '../chunk-MZCQHJXY.js';
2
+ import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-JVLUA3Q5.js';
3
+ import { reportSockaError } from '../chunk-2FNWVCP3.js';
4
4
 
5
5
  // src/bun/index.ts
6
+ function sockaBunInitFromWsData(ws) {
7
+ const d = ws.data;
8
+ if (d && "request" in d && d.request instanceof Request) {
9
+ return { request: d.request };
10
+ }
11
+ return void 0;
12
+ }
13
+ function sockaBunUpgrade(server, req, data) {
14
+ const extra = data ?? {};
15
+ return server.upgrade(req, { data: { ...extra, request: req } });
16
+ }
6
17
  function bunHandlersFromResolveScope(resolveScope) {
7
18
  const websocket = {
8
19
  open(ws) {
9
20
  const { sessionMap, config } = resolveScope(ws);
10
21
  const domWs = ws;
11
- const session = new SockaWebSocketSession(domWs, sessionMap, config);
22
+ const init = sockaBunInitFromWsData(ws);
23
+ const session = new SockaWebSocketSession(
24
+ domWs,
25
+ sessionMap,
26
+ config,
27
+ init
28
+ );
12
29
  sessionMap.set(domWs, session);
13
30
  runSockaSessionOnAttached(config, session);
14
31
  },
@@ -62,7 +79,13 @@ function bunHandlersFromConfig(config, maybeOptions) {
62
79
  const websocket = {
63
80
  open(ws) {
64
81
  const domWs = ws;
65
- const session = new SockaWebSocketSession(domWs, sessionMap, config);
82
+ const init = sockaBunInitFromWsData(ws);
83
+ const session = new SockaWebSocketSession(
84
+ domWs,
85
+ sessionMap,
86
+ config,
87
+ init
88
+ );
66
89
  sessionMap.set(domWs, session);
67
90
  runSockaSessionOnAttached(config, session);
68
91
  },
@@ -116,6 +139,6 @@ function createSockaBunWebSocketHandlers(configOrOptions, maybeOptions) {
116
139
  );
117
140
  }
118
141
 
119
- export { createSockaBunWebSocketHandlers };
142
+ export { createSockaBunWebSocketHandlers, sockaBunInitFromWsData, sockaBunUpgrade };
120
143
  //# sourceMappingURL=index.js.map
121
144
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bun/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { ServerWebSocket } from \"bun\";\nimport type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport { reportSockaError } from \"../core/socka-report-error\";\nimport type { SockaWireFormat } from \"../core/wire-codec\";\nimport { dispatchSockaInboundMessage } from \"../server/dispatchSockaInboundMessage\";\nimport {\n\tSockaWebSocketSession,\n\trunSockaSessionOnAttached,\n\ttype SockaWebSocketSessionConfig,\n} from \"../server/SockaWebSocketSession\";\n\nexport type SockaBunResolveScope<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData = undefined,\n> = (ws: ServerWebSocket<TWsData>) => {\n\tsessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>;\n};\n\nexport type SockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData = undefined,\n> = {\n\tsessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t/** Pass into `Bun.serve({ ..., websocket })`. */\n\twebsocket: {\n\t\topen: (ws: ServerWebSocket<TWsData>) => void;\n\t\tmessage: (\n\t\t\tws: ServerWebSocket<TWsData>,\n\t\t\tmessage: unknown,\n\t\t) => void | Promise<void>;\n\t\tclose: (ws: ServerWebSocket<TWsData>) => void | Promise<void>;\n\t};\n\twireFormat: SockaWireFormat;\n};\n\nfunction bunHandlersFromResolveScope<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData,\n>(\n\tresolveScope: SockaBunResolveScope<TContract, TData, TWsData>,\n): SockaBunWebSocketHandlers<TContract, TData, TWsData> {\n\tconst websocket: SockaBunWebSocketHandlers<\n\t\tTContract,\n\t\tTData,\n\t\tTWsData\n\t>[\"websocket\"] = {\n\t\topen(ws: ServerWebSocket<TWsData>) {\n\t\t\tconst { sessionMap, config } = resolveScope(ws);\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = new SockaWebSocketSession(domWs, sessionMap, config);\n\t\t\tsessionMap.set(domWs, session);\n\t\t\trunSockaSessionOnAttached(config, session);\n\t\t},\n\t\tasync message(ws: ServerWebSocket<TWsData>, message: unknown) {\n\t\t\tconst { sessionMap, config } = resolveScope(ws);\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\tif (!session) return;\n\t\t\tconst wireFormat = config.wireFormat ?? \"json\";\n\t\t\ttry {\n\t\t\t\tawait dispatchSockaInboundMessage(\n\t\t\t\t\tsession,\n\t\t\t\t\twireFormat,\n\t\t\t\t\tmessage as MessageEvent[\"data\"],\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverInboundMessage\",\n\t\t\t\t\tadapter: \"bun\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tasync close(ws: ServerWebSocket<TWsData>) {\n\t\t\tconst { sessionMap, config } = resolveScope(ws);\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\ttry {\n\t\t\t\tif (session) {\n\t\t\t\t\tawait session.invokeHandleClose();\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverHandleClose\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t} finally {\n\t\t\t\tsessionMap.delete(domWs);\n\t\t\t}\n\t\t},\n\t};\n\n\treturn {\n\t\tsessionMap: new Map(),\n\t\twebsocket,\n\t\twireFormat: \"json\",\n\t};\n}\n\nfunction bunHandlersFromConfig<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>,\n\tmaybeOptions?: {\n\t\tsessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t},\n): SockaBunWebSocketHandlers<TContract, TData, undefined> {\n\tconst sessionMap =\n\t\tmaybeOptions?.sessionMap ??\n\t\tnew Map<WebSocket, SockaWebSocketSession<TContract, TData>>();\n\tconst wireFormat = config.wireFormat ?? \"json\";\n\n\tconst websocket: SockaBunWebSocketHandlers<\n\t\tTContract,\n\t\tTData,\n\t\tundefined\n\t>[\"websocket\"] = {\n\t\topen(ws: ServerWebSocket<undefined>) {\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = new SockaWebSocketSession(domWs, sessionMap, config);\n\t\t\tsessionMap.set(domWs, session);\n\t\t\trunSockaSessionOnAttached(config, session);\n\t\t},\n\t\tasync message(ws: ServerWebSocket<undefined>, message: unknown) {\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\tif (!session) return;\n\t\t\ttry {\n\t\t\t\tawait dispatchSockaInboundMessage(\n\t\t\t\t\tsession,\n\t\t\t\t\twireFormat,\n\t\t\t\t\tmessage as MessageEvent[\"data\"],\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverInboundMessage\",\n\t\t\t\t\tadapter: \"bun\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tasync close(ws: ServerWebSocket<undefined>) {\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\ttry {\n\t\t\t\tif (session) {\n\t\t\t\t\tawait session.invokeHandleClose();\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverHandleClose\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t} finally {\n\t\t\t\tsessionMap.delete(domWs);\n\t\t\t}\n\t\t},\n\t};\n\n\treturn { sessionMap, websocket, wireFormat };\n}\n\n/**\n * WebSocket handlers for `Bun.serve` when using `ServerWebSocket` (no\n * `addEventListener`). Inbound frames are dispatched with the same logic as\n * `attachSockaWebSocket`.\n *\n * **Single-room:** pass a {@link SockaWebSocketSessionConfig} and optional shared `sessionMap`.\n *\n * **Multi-room:** pass `{ resolveScope }` where `resolveScope(ws)` returns the\n * `sessionMap` and `config` for that socket’s scope (e.g. from `ws.data.roomId`).\n * The returned `sessionMap` is an empty placeholder; real maps come from `resolveScope`.\n */\nexport function createSockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>,\n\toptions?: {\n\t\tsessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t},\n): SockaBunWebSocketHandlers<TContract, TData, undefined>;\n\nexport function createSockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData,\n>(options: {\n\tresolveScope: SockaBunResolveScope<TContract, TData, TWsData>;\n}): SockaBunWebSocketHandlers<TContract, TData, TWsData>;\n\nexport function createSockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfigOrOptions:\n\t\t| SockaWebSocketSessionConfig<TContract, TData>\n\t\t| { resolveScope: SockaBunResolveScope<TContract, TData, unknown> },\n\tmaybeOptions?: {\n\t\tsessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t},\n):\n\t| SockaBunWebSocketHandlers<TContract, TData, undefined>\n\t| SockaBunWebSocketHandlers<TContract, TData, unknown> {\n\tconst isResolveScope =\n\t\ttypeof configOrOptions === \"object\" &&\n\t\tconfigOrOptions !== null &&\n\t\t\"resolveScope\" in configOrOptions &&\n\t\ttypeof (configOrOptions as { resolveScope: unknown }).resolveScope ===\n\t\t\t\"function\";\n\n\tif (isResolveScope) {\n\t\treturn bunHandlersFromResolveScope(\n\t\t\t(\n\t\t\t\tconfigOrOptions as {\n\t\t\t\t\tresolveScope: SockaBunResolveScope<TContract, TData, unknown>;\n\t\t\t\t}\n\t\t\t).resolveScope,\n\t\t);\n\t}\n\treturn bunHandlersFromConfig(\n\t\tconfigOrOptions as SockaWebSocketSessionConfig<TContract, TData>,\n\t\tmaybeOptions,\n\t);\n}\n"]}
1
+ {"version":3,"sources":["../../src/bun/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { ServerWebSocket } from \"bun\";\nimport type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport { reportSockaError } from \"../core/socka-report-error\";\nimport type { SockaWireFormat } from \"../core/wire-codec\";\nimport { dispatchSockaInboundMessage } from \"../server/dispatchSockaInboundMessage\";\nimport {\n\tSockaWebSocketSession,\n\trunSockaSessionOnAttached,\n\ttype SockaStrictWebSocketInit,\n\ttype SockaWebSocketSessionConfig,\n} from \"../server/SockaWebSocketSession\";\n\n/**\n * Reads the upgrade {@link Request} from Bun **`ServerWebSocket.data`** when your\n * **`fetch`** handler stored it there (e.g. **`server.upgrade(req, { data: { roomId, request: req } })`**).\n *\n * Pair with **`strictUpgradeRequest: true`** on {@link SockaWebSocketSessionConfig} so\n * **`createData`** is typed with {@link SockaStrictWebSocketInit} and **`init.request`**\n * is always defined. If **`request`** is missing from **`data`**, this returns **`undefined`**\n * and strict mode will throw when constructing the session — that usually means you forgot\n * to pass **`request`** on upgrade.\n */\nexport function sockaBunInitFromWsData(\n\tws: ServerWebSocket<unknown>,\n): SockaStrictWebSocketInit | undefined {\n\tconst d = ws.data as Record<string, unknown> | undefined;\n\tif (d && \"request\" in d && d.request instanceof Request) {\n\t\treturn { request: d.request };\n\t}\n\treturn undefined;\n}\n\n/** `ServerWebSocket.data` shape after {@link sockaBunUpgrade}. */\nexport type SockaBunUpgradeData<TExtra> = TExtra & { request: Request };\n\n/**\n * Calls {@link Bun.Server.upgrade} with **`request: req`** merged into **`data`**, so\n * {@link sockaBunInitFromWsData} and **`strictUpgradeRequest: true`** always see the HTTP\n * upgrade request (query params, cookies path).\n */\nexport function sockaBunUpgrade<TExtra extends Record<string, unknown>>(\n\tserver: {\n\t\tupgrade: (\n\t\t\treq: Request,\n\t\t\topts: { data: SockaBunUpgradeData<TExtra> },\n\t\t) => boolean;\n\t},\n\treq: Request,\n\tdata?: TExtra,\n): boolean {\n\tconst extra = (data ?? ({} as TExtra)) as TExtra;\n\treturn server.upgrade(req, { data: { ...extra, request: req } });\n}\n\nexport type SockaBunResolveScope<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData = undefined,\n> = (ws: ServerWebSocket<TWsData>) => {\n\tsessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>;\n};\n\nexport type SockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData = undefined,\n> = {\n\tsessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t/** Pass into `Bun.serve({ ..., websocket })`. */\n\twebsocket: {\n\t\topen: (ws: ServerWebSocket<TWsData>) => void;\n\t\tmessage: (\n\t\t\tws: ServerWebSocket<TWsData>,\n\t\t\tmessage: unknown,\n\t\t) => void | Promise<void>;\n\t\tclose: (ws: ServerWebSocket<TWsData>) => void | Promise<void>;\n\t};\n\twireFormat: SockaWireFormat;\n};\n\nfunction bunHandlersFromResolveScope<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData,\n>(\n\tresolveScope: SockaBunResolveScope<TContract, TData, TWsData>,\n): SockaBunWebSocketHandlers<TContract, TData, TWsData> {\n\tconst websocket: SockaBunWebSocketHandlers<\n\t\tTContract,\n\t\tTData,\n\t\tTWsData\n\t>[\"websocket\"] = {\n\t\topen(ws: ServerWebSocket<TWsData>) {\n\t\t\tconst { sessionMap, config } = resolveScope(ws);\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst init = sockaBunInitFromWsData(ws);\n\t\t\tconst session = new SockaWebSocketSession(\n\t\t\t\tdomWs,\n\t\t\t\tsessionMap,\n\t\t\t\tconfig,\n\t\t\t\tinit,\n\t\t\t);\n\t\t\tsessionMap.set(domWs, session);\n\t\t\trunSockaSessionOnAttached(config, session);\n\t\t},\n\t\tasync message(ws: ServerWebSocket<TWsData>, message: unknown) {\n\t\t\tconst { sessionMap, config } = resolveScope(ws);\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\tif (!session) return;\n\t\t\tconst wireFormat = config.wireFormat ?? \"json\";\n\t\t\ttry {\n\t\t\t\tawait dispatchSockaInboundMessage(\n\t\t\t\t\tsession,\n\t\t\t\t\twireFormat,\n\t\t\t\t\tmessage as MessageEvent[\"data\"],\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverInboundMessage\",\n\t\t\t\t\tadapter: \"bun\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tasync close(ws: ServerWebSocket<TWsData>) {\n\t\t\tconst { sessionMap, config } = resolveScope(ws);\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\ttry {\n\t\t\t\tif (session) {\n\t\t\t\t\tawait session.invokeHandleClose();\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverHandleClose\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t} finally {\n\t\t\t\tsessionMap.delete(domWs);\n\t\t\t}\n\t\t},\n\t};\n\n\treturn {\n\t\tsessionMap: new Map(),\n\t\twebsocket,\n\t\twireFormat: \"json\",\n\t};\n}\n\nfunction bunHandlersFromConfig<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>,\n\tmaybeOptions?: {\n\t\tsessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t},\n): SockaBunWebSocketHandlers<TContract, TData, undefined> {\n\tconst sessionMap =\n\t\tmaybeOptions?.sessionMap ??\n\t\tnew Map<WebSocket, SockaWebSocketSession<TContract, TData>>();\n\tconst wireFormat = config.wireFormat ?? \"json\";\n\n\tconst websocket: SockaBunWebSocketHandlers<\n\t\tTContract,\n\t\tTData,\n\t\tundefined\n\t>[\"websocket\"] = {\n\t\topen(ws: ServerWebSocket<undefined>) {\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst init = sockaBunInitFromWsData(ws);\n\t\t\tconst session = new SockaWebSocketSession(\n\t\t\t\tdomWs,\n\t\t\t\tsessionMap,\n\t\t\t\tconfig,\n\t\t\t\tinit,\n\t\t\t);\n\t\t\tsessionMap.set(domWs, session);\n\t\t\trunSockaSessionOnAttached(config, session);\n\t\t},\n\t\tasync message(ws: ServerWebSocket<undefined>, message: unknown) {\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\tif (!session) return;\n\t\t\ttry {\n\t\t\t\tawait dispatchSockaInboundMessage(\n\t\t\t\t\tsession,\n\t\t\t\t\twireFormat,\n\t\t\t\t\tmessage as MessageEvent[\"data\"],\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverInboundMessage\",\n\t\t\t\t\tadapter: \"bun\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tasync close(ws: ServerWebSocket<undefined>) {\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\ttry {\n\t\t\t\tif (session) {\n\t\t\t\t\tawait session.invokeHandleClose();\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverHandleClose\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t} finally {\n\t\t\t\tsessionMap.delete(domWs);\n\t\t\t}\n\t\t},\n\t};\n\n\treturn { sessionMap, websocket, wireFormat };\n}\n\n/**\n * WebSocket handlers for `Bun.serve` when using `ServerWebSocket` (no\n * `addEventListener`). Inbound frames are dispatched with the same logic as\n * `attachSockaWebSocket`.\n *\n * **Single-room:** pass a {@link SockaWebSocketSessionConfig} and optional shared `sessionMap`.\n *\n * **Multi-room:** pass `{ resolveScope }` where `resolveScope(ws)` returns the\n * `sessionMap` and `config` for that socket’s scope (e.g. from `ws.data.roomId`).\n * The returned `sessionMap` is an empty placeholder; real maps come from `resolveScope`.\n */\nexport function createSockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>,\n\toptions?: {\n\t\tsessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t},\n): SockaBunWebSocketHandlers<TContract, TData, undefined>;\n\nexport function createSockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData,\n>(options: {\n\tresolveScope: SockaBunResolveScope<TContract, TData, TWsData>;\n}): SockaBunWebSocketHandlers<TContract, TData, TWsData>;\n\nexport function createSockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfigOrOptions:\n\t\t| SockaWebSocketSessionConfig<TContract, TData>\n\t\t| { resolveScope: SockaBunResolveScope<TContract, TData, unknown> },\n\tmaybeOptions?: {\n\t\tsessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t},\n):\n\t| SockaBunWebSocketHandlers<TContract, TData, undefined>\n\t| SockaBunWebSocketHandlers<TContract, TData, unknown> {\n\tconst isResolveScope =\n\t\ttypeof configOrOptions === \"object\" &&\n\t\tconfigOrOptions !== null &&\n\t\t\"resolveScope\" in configOrOptions &&\n\t\ttypeof (configOrOptions as { resolveScope: unknown }).resolveScope ===\n\t\t\t\"function\";\n\n\tif (isResolveScope) {\n\t\treturn bunHandlersFromResolveScope(\n\t\t\t(\n\t\t\t\tconfigOrOptions as {\n\t\t\t\t\tresolveScope: SockaBunResolveScope<TContract, TData, unknown>;\n\t\t\t\t}\n\t\t\t).resolveScope,\n\t\t);\n\t}\n\treturn bunHandlersFromConfig(\n\t\tconfigOrOptions as SockaWebSocketSessionConfig<TContract, TData>,\n\t\tmaybeOptions,\n\t);\n}\n"]}