@firtoz/socka 2.0.0 → 3.0.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 (67) hide show
  1. package/README.md +195 -42
  2. package/dist/SockaWebSocketSession-B1w7RAid.d.ts +209 -0
  3. package/dist/bun/index.d.ts +30 -5
  4. package/dist/bun/index.js +28 -5
  5. package/dist/bun/index.js.map +1 -1
  6. package/dist/{chunk-MZCQHJXY.js → chunk-IFIGKR3W.js} +45 -8
  7. package/dist/chunk-IFIGKR3W.js.map +1 -0
  8. package/dist/{chunk-45D4T232.js → chunk-LVVCHLNW.js} +74 -9
  9. package/dist/chunk-LVVCHLNW.js.map +1 -0
  10. package/dist/{chunk-AM7PB26G.js → chunk-P3JEEOJL.js} +192 -10
  11. package/dist/chunk-P3JEEOJL.js.map +1 -0
  12. package/dist/chunk-QGURL3DJ.js +8 -0
  13. package/dist/chunk-QGURL3DJ.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 +2 -21
  17. package/dist/core/index.js +1 -1
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/do/index.d.ts +20 -2
  20. package/dist/do/index.js +36 -2
  21. package/dist/do/index.js.map +1 -1
  22. package/dist/hono/cloudflare-workers.d.ts +4 -4
  23. package/dist/hono/cloudflare-workers.js +4 -3
  24. package/dist/hono/cloudflare-workers.js.map +1 -1
  25. package/dist/hono/index.d.ts +22 -6
  26. package/dist/hono/index.js +5 -3
  27. package/dist/hono/index.js.map +1 -1
  28. package/dist/react/index.d.ts +43 -4
  29. package/dist/react/index.js +103 -9
  30. package/dist/react/index.js.map +1 -1
  31. package/dist/server/index.d.ts +18 -5
  32. package/dist/server/index.js +24 -4
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/{socka-report-error-DzFI2Tr7.d.ts → socka-report-error-CXwpAUgl.d.ts} +80 -8
  35. package/dist/test/index.d.ts +11 -0
  36. package/dist/test/index.js +84 -0
  37. package/dist/test/index.js.map +1 -0
  38. package/docs/README.md +16 -7
  39. package/docs/auth.md +27 -0
  40. package/docs/backpressure.md +16 -0
  41. package/docs/client.md +48 -3
  42. package/docs/comparison.md +2 -2
  43. package/docs/durable-objects.md +3 -3
  44. package/docs/getting-started.md +143 -84
  45. package/docs/history.md +26 -0
  46. package/docs/internals.md +56 -0
  47. package/docs/lifecycle.md +3 -3
  48. package/docs/multi-room.md +10 -8
  49. package/docs/peers.md +11 -7
  50. package/docs/presence.md +43 -0
  51. package/docs/{events.md → pushes.md} +1 -1
  52. package/docs/recipes.md +77 -0
  53. package/docs/reconnection.md +44 -0
  54. package/docs/reference.md +27 -32
  55. package/docs/server.md +19 -3
  56. package/docs/testing.md +20 -0
  57. package/docs/wire-format.md +29 -0
  58. package/examples/minimal-socka.ts +56 -3
  59. package/package.json +14 -10
  60. package/roadmap.md +2 -2
  61. package/skills/socka/core-rpc/SKILL.md +2 -2
  62. package/skills/socka/do-session/SKILL.md +2 -2
  63. package/skills/socka/standard-schema/SKILL.md +1 -1
  64. package/dist/SockaWebSocketSession-Bru8yFcK.d.ts +0 -107
  65. package/dist/chunk-45D4T232.js.map +0 -1
  66. package/dist/chunk-AM7PB26G.js.map +0 -1
  67. package/dist/chunk-MZCQHJXY.js.map +0 -1
package/README.md CHANGED
@@ -10,11 +10,15 @@
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
+
15
+ **Validation is [Standard Schema](https://standardschema.dev) v1**, not Zod-specific: any compliant library works (e.g. **Zod**, **Valibot**, or others). Examples below use **Zod** for familiarity.
14
16
 
15
17
  **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
18
 
17
- ## 30-second example (Bun)
19
+ ## Minimal example: multi-room chat (Bun)
20
+
21
+ 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
22
 
19
23
  **`contract.ts`** (shared):
20
24
 
@@ -22,35 +26,158 @@
22
26
  import { defineSocka } from "@firtoz/socka/core";
23
27
  import * as z from "zod";
24
28
 
25
- export const myContract = defineSocka({
29
+ export const messageRow = z.object({
30
+ id: z.string(),
31
+ ts: z.number(),
32
+ userId: z.string(),
33
+ displayName: z.string(),
34
+ text: z.string(),
35
+ });
36
+
37
+ export type ChatMessageRow = z.infer<typeof messageRow>;
38
+
39
+ const onlineUser = z.object({
40
+ userId: z.string(),
41
+ displayName: z.string(),
42
+ });
43
+
44
+ export const chatContract = defineSocka({
26
45
  calls: {
27
- echo: {
28
- input: z.object({ text: z.string() }),
29
- output: z.object({ text: z.string() }),
46
+ listHistory: {
47
+ input: z.object({ limit: z.number().int().min(1).max(500).optional() }),
48
+ output: z.object({ messages: z.array(messageRow) }),
49
+ },
50
+ listPresence: {
51
+ input: z.object({}).optional(),
52
+ output: z.object({
53
+ selfUserId: z.string(),
54
+ users: z.array(onlineUser),
55
+ }),
56
+ },
57
+ sendMessage: {
58
+ input: z.object({ text: z.string().min(1) }),
59
+ output: z.object({ ok: z.literal(true) }),
30
60
  },
61
+ clearHistory: {
62
+ input: z.object({}).optional(),
63
+ output: z.object({ ok: z.literal(true) }),
64
+ },
65
+ },
66
+ pushes: {
67
+ userJoined: z.object({ userId: z.string(), displayName: z.string() }),
68
+ userLeft: z.object({
69
+ userId: z.string(),
70
+ displayName: z.string(),
71
+ }),
72
+ roomMessage: messageRow,
73
+ historyCleared: z.object({
74
+ ts: z.number(),
75
+ clearedByUserId: z.string(),
76
+ clearedByDisplayName: z.string(),
77
+ }),
31
78
  },
32
79
  });
33
80
  ```
34
81
 
35
- **`server.ts`**:
82
+ **Fire-and-forget vs `output: z.void()`** — Omit **`output`** on a call when you want one-way success semantics: the server does not send a **`serverResponse`**, and **`await session.send.*` resolves after the frame is sent** (it does not wait for server processing). Server failures still return a correlated **`serverError`**; use **`reportError`** on **`SockaSession`** / **`useSockaSession`** to observe those when using output-less calls. Use **`output: z.void()`** when you still want a normal request/response **`await`** that completes only after the server acknowledges.
83
+
84
+ **`server.ts`** — **`createSockaRoomRegistry`** gives each room its own **`sessionMap`** and **config**. By default **`createData`** receives **`SockaStrictWebSocketInit`**: **`init.request`** is the upgrade **`Request`** (Bun/Hono/adapters pass it through; see **[Server — Strict upgrade request](./docs/server.md#strict-upgrade-request)**). Set **`strictUpgradeRequest: false`** when you have no **`Request`**. **`session.listPeers()`** returns **`session.data`** for other sockets in the same room—use it to implement **`listPresence`**-style calls (see **[Presence](./docs/presence.md)**).
36
85
 
37
86
  ```ts
87
+ import type { ServerWebSocket } from "bun";
38
88
  import { createSockaBunWebSocketHandlers } from "@firtoz/socka/bun";
39
- import { myContract } from "./contract";
89
+ import {
90
+ createSockaRoomRegistry,
91
+ type SockaWebSocketSessionConfig,
92
+ } from "@firtoz/socka/server";
93
+ import { type ChatMessageRow, chatContract } from "./contract";
94
+
95
+ type SessionData = { roomId: string; userId: string; displayName: string };
96
+
97
+ /** In-memory demo store — swap for SQLite / files / DO in real apps. */
98
+ const history = new Map<string, ChatMessageRow[]>();
99
+
100
+ const registry = createSockaRoomRegistry(
101
+ (roomId): SockaWebSocketSessionConfig<typeof chatContract, SessionData> => ({
102
+ contract: chatContract,
103
+ createData: (init) => {
104
+ const u = new URL(init.request.url);
105
+ const displayName = u.searchParams.get("name")?.trim() || "anon";
106
+ return { roomId, userId: crypto.randomUUID(), displayName };
107
+ },
108
+ onAttached: async (session) => {
109
+ await session.broadcastPush(
110
+ "userJoined",
111
+ { userId: session.data.userId, displayName: session.data.displayName },
112
+ true,
113
+ );
114
+ },
115
+ handlers: {
116
+ listHistory: async (input, session) => {
117
+ const lim = input.limit ?? 200;
118
+ const rows = history.get(session.data.roomId) ?? [];
119
+ return { messages: rows.slice(-lim) };
120
+ },
121
+ listPresence: async (_input, session) => {
122
+ const users = session.listPeers().map((d) => ({
123
+ userId: d.userId,
124
+ displayName: d.displayName,
125
+ }));
126
+ users.sort((a, b) => a.displayName.localeCompare(b.displayName));
127
+ return { selfUserId: session.data.userId, users };
128
+ },
129
+ sendMessage: async (input, session) => {
130
+ const row = {
131
+ id: crypto.randomUUID(),
132
+ ts: Date.now(),
133
+ userId: session.data.userId,
134
+ displayName: session.data.displayName,
135
+ text: input.text,
136
+ };
137
+ const list = history.get(session.data.roomId) ?? [];
138
+ list.push(row);
139
+ history.set(session.data.roomId, list);
140
+ await session.broadcastPush("roomMessage", row);
141
+ return { ok: true as const };
142
+ },
143
+ clearHistory: async (_input, session) => {
144
+ history.set(session.data.roomId, []);
145
+ const ts = Date.now();
146
+ await session.broadcastPush("historyCleared", {
147
+ ts,
148
+ clearedByUserId: session.data.userId,
149
+ clearedByDisplayName: session.data.displayName,
150
+ });
151
+ return { ok: true as const };
152
+ },
153
+ },
154
+ handleClose: async (session) => {
155
+ await session.broadcastPush(
156
+ "userLeft",
157
+ { userId: session.data.userId, displayName: session.data.displayName },
158
+ true,
159
+ );
160
+ },
161
+ }),
162
+ );
163
+
164
+ type BunWsData = { roomId: string; request: Request };
40
165
 
41
166
  const { websocket } = createSockaBunWebSocketHandlers({
42
- contract: myContract,
43
- handlers: {
44
- echo: async (input) => ({ text: input.text }),
167
+ resolveScope(ws: ServerWebSocket<BunWsData>) {
168
+ const { roomId } = ws.data;
169
+ const room = registry.get(roomId);
170
+ return { sessionMap: room.sessionMap, config: room.config };
45
171
  },
46
- handleClose: async () => {},
47
172
  });
48
173
 
49
- Bun.serve({
174
+ Bun.serve<BunWsData>({
50
175
  port: 3450,
51
176
  fetch(req, server) {
52
- if (new URL(req.url).pathname === "/ws") {
53
- if (server.upgrade(req)) return undefined;
177
+ const url = new URL(req.url);
178
+ if (url.pathname.startsWith("/ws/")) {
179
+ const roomId = decodeURIComponent(url.pathname.slice(4)) || "default";
180
+ if (server.upgrade(req, { data: { roomId, request: req } })) return undefined;
54
181
  return new Response("WebSocket upgrade failed", { status: 400 });
55
182
  }
56
183
  return new Response("OK");
@@ -59,62 +186,88 @@ Bun.serve({
59
186
  });
60
187
  ```
61
188
 
189
+ *Ports: this minimal snippet listens on **3450**; the [full-stack examples](#full-stack-examples) use **3461–3466**.*
190
+
62
191
  **`client.ts`** (browser or Bun):
63
192
 
64
193
  ```ts
65
194
  import { SockaSession } from "@firtoz/socka/client";
66
- import { myContract } from "./contract";
195
+ import { chatContract } from "./contract";
67
196
 
68
197
  const session = new SockaSession({
69
- contract: myContract,
70
- url: "ws://localhost:3450/ws",
198
+ contract: chatContract,
199
+ url: "ws://localhost:3450/ws/lobby?name=Ada",
71
200
  });
72
- const { text } = await session.send.echo({ text: "hello" });
73
- console.log(text);
201
+
202
+ session.subscribe.on("userJoined", (p) => console.log("joined", p));
203
+ session.subscribe.on("userLeft", (p) => console.log("left", p.displayName));
204
+ session.subscribe.on("roomMessage", (m) => console.log(`${m.displayName}: ${m.text}`));
205
+ session.subscribe.on("historyCleared", (p) =>
206
+ console.log("history cleared by", p.clearedByDisplayName, "at", p.ts),
207
+ );
208
+
209
+ const { messages } = await session.send.listHistory({});
210
+ console.log("history", messages);
211
+ const { selfUserId, users } = await session.send.listPresence({});
212
+ console.log("online", selfUserId, users);
213
+ await session.send.sendMessage({ text: "hello room" });
214
+ await session.send.clearHistory({});
74
215
  ```
75
216
 
76
- Run **`bun run server.ts`**, then point the client at **`ws://localhost:3450/ws`**.
217
+ Run **`bun run server.ts`**, then point the client at the same **`ws://…/ws/<room>?name=…`** path you upgrade in **`fetch`**.
218
+
219
+ **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
220
 
78
221
  ## Install
79
222
 
80
- ```bash
81
- npm install @firtoz/socka
82
- ```
223
+ Always install **`@firtoz/socka`**, then add **only** what your imports need (`npm` / `pnpm` / `bun add` as you prefer):
83
224
 
84
- Also: `pnpm add @firtoz/socka` · `bun add @firtoz/socka`
225
+ | You are building… | Install |
226
+ |-------------------|---------|
227
+ | **Browser / Vite SPA** (client only) | `npm install @firtoz/socka` |
228
+ | **React** (`@firtoz/socka/react`) | `npm install @firtoz/socka react` — add **`@types/react`** as a dev dependency if TypeScript asks |
229
+ | **Bun** (`Bun.serve`, `@firtoz/socka/bun`) | `npm install @firtoz/socka` — add **`bun-types`** as a dev dependency if you type-check Bun APIs |
230
+ | **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 |
231
+ | **Cloudflare Workers + Hono** (`@firtoz/socka/hono/cloudflare`) | `npm install @firtoz/socka hono` |
232
+ | **Cloudflare Durable Objects** (`@firtoz/socka/do`) | `npm install @firtoz/socka hono @firtoz/websocket-do` |
85
233
 
86
- Optional peers depend on which subpath you import—see **[Peers](./docs/peers.md)**.
234
+ 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
235
 
88
236
  ## Other runtimes
89
237
 
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)** |
238
+ Pick how the socket is upgraded, then use the matching import path and guide:
239
+
240
+ | Runtime | Import path | Quick start |
241
+ |---------|-------------|-------------|
242
+ | **Node + [`ws`](https://github.com/websockets/ws)** (or any standard **`WebSocket`** after upgrade) | `@firtoz/socka/server` | [`attachSockaWebSocket`](./docs/server.md) |
243
+ | **Bun** (`Bun.serve` / `ServerWebSocket`) | `@firtoz/socka/bun` | [`createSockaBunWebSocketHandlers`](./docs/server.md#firtoz-socka-bun-bunserve) |
244
+ | **Hono on Node** (`@hono/node-ws`) | `@firtoz/socka/hono` | [`sockaHonoNodeWs`](./docs/server.md#firtoz-socka-hono-node-hono-node-ws) |
245
+ | **Hono on Cloudflare Workers** | `@firtoz/socka/hono/cloudflare` | [`sockaHonoCloudflare`](./docs/server.md#firtoz-socka-hono-cloudflare-workers) |
96
246
  | **Cloudflare Durable Objects** | `@firtoz/socka/do` | **[Durable Objects](./docs/durable-objects.md)** |
97
247
 
98
248
  ## Why not socket.io, tRPC, or DIY?
99
249
 
100
250
  - **Schema-first RPC + push** — one contract; no parallel “event” protocol for server pushes.
101
251
  - **Correlated envelopes** — request/response IDs and validation hooks are built in.
102
- - **Same contract** across Bun, Hono, Node `ws`, and Durable Objects (see **[Comparison](./docs/comparison.md)** for socket.io / tRPC / hand-rolled).
252
+ - **Same contract** across Bun, Hono, Node `ws`, and Durable Objects (see **[Comparison](./docs/comparison.md)** for socket.io, tRPC, and custom WebSocket stacks).
253
+ - **Room registry + presence helpers** — **`createSockaRoomRegistry`** for per-room **`sessionMap`** / config; **`session.listPeers()`** to list other peers in the room (see **[Presence](./docs/presence.md)**).
254
+ - **Strict upgrade typing + optional reconnect** — by default **`createData`** sees **`init.request`** on the upgrade; **`SockaWebSocketClient`** / **`SockaSession`** can **`reconnect`** with exponential backoff (see **[Reconnection](./docs/reconnection.md)**).
103
255
 
104
256
  ## Documentation
105
257
 
106
258
  Hub: **[`docs/README.md`](./docs/README.md)** (getting started, peers, lifecycle, multi-room, reference).
107
259
 
108
- **Roadmap:** [post–v1 and deferred work](./roadmap.md). Agent skills: [`skills/`](./skills/).
260
+ **Roadmap:** [deferred ideas and future work](./roadmap.md). Agent skills: [`skills/`](./skills/).
109
261
 
110
262
  ## Full-stack examples
111
263
 
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** |
264
+ | Topic | Stack | Folder | Port |
265
+ |-------|--------|--------|------|
266
+ | **Chat + history** | **Bun** + SQLite | [`chatroom-bun`](../../examples/chatroom-bun) | **3464** |
267
+ | **Chat + history** | **Hono + Node** + JSON files | [`chatroom-hono`](../../examples/chatroom-hono) | **3465** |
268
+ | **Chat + history** | **Cloudflare DO** + Drizzle SQLite | [`chatroom-do`](../../examples/chatroom-do) | **3466** |
269
+ | Tic-tac-toe | **Bun** | [`tic-tac-toe-bun`](../../examples/tic-tac-toe-bun) | **3461** |
270
+ | Tic-tac-toe | **Hono + Node** | [`tic-tac-toe-hono`](../../examples/tic-tac-toe-hono) | **3462** |
271
+ | Tic-tac-toe | **Cloudflare DO** | [`tic-tac-toe-do`](../../examples/tic-tac-toe-do) | **3463** |
119
272
 
120
- Each app: **`bun run dev`** (or **`wrangler dev`** for the DO example).
273
+ Chat apps: **`bun run dev`** (or **`wrangler dev`** for **`chatroom-do`**). Tic-tac-toe: same.
@@ -0,0 +1,209 @@
1
+ import { S as SockaContract, a as SockaContractConfig, w as SockaWireFormat, c as InferSockaHandlers, z as SockaReportError, f as InferSockaPushPayload } from './socka-report-error-CXwpAUgl.js';
2
+
3
+ /** Session data with no fields — `createData` may be omitted (defaults to `{}`). */
4
+ type EmptySockaSessionData$1 = Record<string, never>;
5
+ /**
6
+ * Upgrade context passed into `createData` for {@link SockaWebSocketSession} when
7
+ * **`strictUpgradeRequest: false`** is set (opt out of the default strict upgrade).
8
+ *
9
+ * **`request` is optional** because some call sites attach a socket without an HTTP upgrade
10
+ * (custom tests, unusual adapters, Node **`ws`** without a **`Request`**, inner DO engine).
11
+ * If you read query params or headers from the upgrade, either handle a missing
12
+ * **`request`** here, or use the default strict mode (omit **`strictUpgradeRequest: false`**)
13
+ * so {@link SockaStrictWebSocketInit} applies.
14
+ */
15
+ type SockaWebSocketInit = {
16
+ /**
17
+ * Original HTTP **`Request`** for the WebSocket upgrade, when the adapter supplies it
18
+ * (e.g. the optional fourth argument to **`attachSockaWebSocket`**).
19
+ */
20
+ request?: Request;
21
+ };
22
+ /**
23
+ * Upgrade context for {@link SockaWebSocketSession} when using the default strict config
24
+ * ({@link SockaWebSocketSessionConfig} — no `strictUpgradeRequest` field).
25
+ *
26
+ * **What this enables:** `createData` is typed so **`init.request` is always defined**.
27
+ * You can use **`new URL(init.request.url)`**, read search params, and use **`Request`**
28
+ * headers without optional chaining or a dummy base URL for **`URL`** parsing.
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
+ };
41
+ type SockaWebSocketCreateData<TData> = [TData] extends [EmptySockaSessionData$1] ? {
42
+ createData?: (init: SockaWebSocketInit) => TData;
43
+ } : {
44
+ createData: (init: SockaWebSocketInit) => TData;
45
+ };
46
+ type SockaWebSocketCreateDataStrict<TData> = [TData] extends [
47
+ EmptySockaSessionData$1
48
+ ] ? {
49
+ createData?: (init: SockaStrictWebSocketInit) => TData;
50
+ } : {
51
+ createData: (init: SockaStrictWebSocketInit) => TData;
52
+ };
53
+ type SockaSessionForHandlers<TContract extends SockaContract<SockaContractConfig>, TData> = SockaWebSocketSession<TContract, TData>;
54
+ type SockaWebSocketSessionConfigBase<TContract extends SockaContract<SockaContractConfig>, TData> = {
55
+ contract: TContract;
56
+ /** Default `"json"`. Use `"msgpack"` for binary frames (must match client). */
57
+ wireFormat?: SockaWireFormat;
58
+ handlers: InferSockaHandlers<TContract, SockaSessionForHandlers<TContract, TData>>;
59
+ /** Called when this WebSocket closes, before the socket is removed from `sessions`. */
60
+ handleClose: (session: SockaSessionForHandlers<TContract, TData>) => Promise<void>;
61
+ onHandlerError?: (error: unknown, rpcName: string, input: unknown, session: SockaSessionForHandlers<TContract, TData>) => void;
62
+ onValidationError?: (error: unknown, originalMessage: unknown) => Promise<void>;
63
+ /**
64
+ * Optional sink for non-RPC failures (onAttached, adapter I/O). Defaults to
65
+ * `console.error` with `socka:` prefixes; see `SockaReportError` in `@firtoz/socka/core`.
66
+ */
67
+ reportError?: (event: SockaReportError) => void;
68
+ serializeJson?: (value: unknown) => string;
69
+ deserializeJson?: (raw: string) => unknown;
70
+ /**
71
+ * Called once after this session is registered in the shared `sessions` map
72
+ * (safe to broadcast to peers). Sync or async; async rejections are logged.
73
+ */
74
+ onAttached?: (session: SockaSessionForHandlers<TContract, TData>) => void | Promise<void>;
75
+ };
76
+ /**
77
+ * Configuration for {@link SockaWebSocketSession} — **default (strict upgrade)**.
78
+ *
79
+ * Handlers receive the session as the second argument, or the only argument when the call
80
+ * has no input schema.
81
+ *
82
+ * **`createData`** receives {@link SockaStrictWebSocketInit}: **`init.request`** is the
83
+ * HTTP upgrade **`Request`**, required in TypeScript and enforced at runtime. Use for
84
+ * normal Bun/Hono apps and **`attachSockaWebSocket(websocket, sessions, config, { request })`**.
85
+ *
86
+ * Bun helpers: store **`request`** on **`ServerWebSocket`** `data` and use
87
+ * **`sockaBunInitFromWsData`** (see **`@firtoz/socka/bun`**). Hono: **`sockaHonoNodeWs`** /
88
+ * **`sockaHonoCloudflare`** can supply **`sockaInit`** from the context so **`createData`**
89
+ * still sees a **`Request`**.
90
+ *
91
+ * To allow a missing upgrade **`Request`** (tests, Node **`ws`**, inner DO engine), use
92
+ * {@link SockaWebSocketSessionConfigLoose} instead. See the **Server** guide section
93
+ * **Strict upgrade request** in the package docs.
94
+ */
95
+ type SockaWebSocketSessionConfig<TContract extends SockaContract<SockaContractConfig>, TData = EmptySockaSessionData$1> = SockaWebSocketSessionConfigBase<TContract, TData> & SockaWebSocketCreateDataStrict<TData>;
96
+ /**
97
+ * Configuration for {@link SockaWebSocketSession} — **loose upgrade** (opt out of strict).
98
+ *
99
+ * Sets **`strictUpgradeRequest: false`**. **`createData`** receives {@link SockaWebSocketInit};
100
+ * **`init.request` may be `undefined`**.
101
+ *
102
+ * Use when:
103
+ *
104
+ * - Custom **`attachSockaWebSocket`** call sites or tests that do not attach an HTTP
105
+ * **`Request`**
106
+ * - Node **`ws`** or other adapters where you only have a **`WebSocket`**
107
+ * - The inner **`SockaWebSocketSession`** constructed inside **`SockaDoSession`** (socka sets
108
+ * this mode for you)
109
+ *
110
+ * If you read query params or headers from the upgrade, guard for a missing **`request`**
111
+ * or switch to {@link SockaWebSocketSessionConfig} and wire the real **`Request`** through.
112
+ */
113
+ type SockaWebSocketSessionConfigLoose<TContract extends SockaContract<SockaContractConfig>, TData = EmptySockaSessionData$1> = SockaWebSocketSessionConfigBase<TContract, TData> & {
114
+ strictUpgradeRequest: false;
115
+ } & SockaWebSocketCreateData<TData>;
116
+ /**
117
+ * Union of {@link SockaWebSocketSessionConfig} (strict) and {@link SockaWebSocketSessionConfigLoose}.
118
+ * This is the type accepted by {@link SockaWebSocketSession}'s constructor,
119
+ * **`attachSockaWebSocket`**, **`createSockaBunWebSocketHandlers`**, and Hono socka helpers.
120
+ */
121
+ type SockaWebSocketSessionConfigUnion<TContract extends SockaContract<SockaContractConfig>, TData = EmptySockaSessionData$1> = SockaWebSocketSessionConfig<TContract, TData> | SockaWebSocketSessionConfigLoose<TContract, TData>;
122
+
123
+ /** Session data with no fields — `createData` may be omitted (defaults to `{}`). */
124
+ type EmptySockaSessionData = Record<string, never>;
125
+
126
+ /** Session that can send a wire-level server event (already validated). */
127
+ type SockaEmitCapable = {
128
+ emitWireEvent(event: string, body: unknown): void;
129
+ };
130
+ /**
131
+ * Contract-typed session surface for handlers that push to clients.
132
+ */
133
+ interface SockaPushSession<TContract extends SockaContract<SockaContractConfig>> {
134
+ emitPush<K extends keyof TContract["pushes"] & string>(name: K, body: InferSockaPushPayload<TContract, K>): Promise<void>;
135
+ broadcastPush<K extends keyof TContract["pushes"] & string>(name: K, body: InferSockaPushPayload<TContract, K>, excludeSelf?: boolean): Promise<void>;
136
+ }
137
+ /**
138
+ * Broadcast a socka server event to every session in the map (optionally
139
+ * excluding the caller). Payload must already be contract-validated.
140
+ *
141
+ * Exclusion uses the **WebSocket** identity (`self.websocket`), not the session
142
+ * object reference, so the same `sessions` map can hold `SockaDoSession` while
143
+ * `broadcastPush` runs on `this.socka` (inner {@link SockaWebSocketSession}).
144
+ */
145
+ declare function broadcastSockaEventToPeers(sessions: Map<WebSocket, SockaEmitCapable>, self: SockaEmitCapable & {
146
+ readonly websocket: WebSocket;
147
+ }, event: string, body: unknown, excludeSelf?: boolean): void;
148
+ /**
149
+ * Runtime-agnostic socka server session: standard {@link WebSocket} wire
150
+ * dispatch without Cloudflare Durable Object APIs.
151
+ */
152
+ declare class SockaWebSocketSession<TContract extends SockaContract<SockaContractConfig>, TData = EmptySockaSessionData> implements SockaPushSession<TContract> {
153
+ readonly websocket: WebSocket;
154
+ protected readonly sessions: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;
155
+ private readonly config;
156
+ private readonly wireFormat;
157
+ private _data;
158
+ constructor(websocket: WebSocket, sessions: Map<WebSocket, SockaWebSocketSession<TContract, TData>>, config: SockaWebSocketSessionConfigUnion<TContract, TData>, init?: SockaWebSocketInit);
159
+ get data(): TData;
160
+ /**
161
+ * Session data for every connection in the same {@link sessions} map (same room),
162
+ * optionally excluding this socket.
163
+ */
164
+ listPeers(options?: {
165
+ excludeSelf?: boolean;
166
+ }): TData[];
167
+ /**
168
+ * Like {@link listPeers} but maps each peer {@link SockaWebSocketSession}
169
+ * (e.g. when you need more than {@link #data}).
170
+ */
171
+ listPeersWith<R>(map: (session: SockaWebSocketSession<TContract, TData>) => R, options?: {
172
+ excludeSelf?: boolean;
173
+ }): R[];
174
+ /** Count of sessions in this room (same {@link sessions} map), optionally excluding self. */
175
+ peerCount(options?: {
176
+ excludeSelf?: boolean;
177
+ }): number;
178
+ /** Whether any peer sessions exist (optionally excluding self). */
179
+ hasPeers(options?: {
180
+ excludeSelf?: boolean;
181
+ }): boolean;
182
+ /**
183
+ * Invokes the user {@link typeof SockaWebSocketSessionConfig.handleClose} callback.
184
+ * Server adapters should call this when the WebSocket closes, **before** deleting
185
+ * this session from the shared `sessions` map.
186
+ */
187
+ invokeHandleClose(): Promise<void>;
188
+ handleRawMessage(rawMessage: string): Promise<void>;
189
+ handleBinaryMessage(buffer: ArrayBuffer): Promise<void>;
190
+ private dispatchAfterParsed;
191
+ private dispatchClientRequest;
192
+ private encodeOutgoing;
193
+ private sendWireFrame;
194
+ /**
195
+ * Send a server event frame (wire). Prefer {@link emitPush} so
196
+ * payloads are validated against the contract.
197
+ */
198
+ emitWireEvent(event: string, body: unknown): void;
199
+ emitPush<K extends keyof TContract["pushes"] & string>(name: K, body: InferSockaPushPayload<TContract, K>): Promise<void>;
200
+ broadcastPush<K extends keyof TContract["pushes"] & string>(name: K, body: InferSockaPushPayload<TContract, K>, excludeSelf?: boolean): Promise<void>;
201
+ private reportValidationError;
202
+ }
203
+ /**
204
+ * Invoke {@link SockaWebSocketSessionConfig.onAttached} after the session is
205
+ * registered in the shared map.
206
+ */
207
+ declare function runSockaSessionOnAttached<TContract extends SockaContract<SockaContractConfig>, TData>(config: SockaWebSocketSessionConfigUnion<TContract, TData>, session: SockaWebSocketSession<TContract, TData>): void;
208
+
209
+ export { type SockaStrictWebSocketInit as S, SockaWebSocketSession as a, type SockaWebSocketSessionConfigUnion as b, type SockaWebSocketInit as c, type SockaPushSession as d, type SockaWebSocketSessionConfig as e, broadcastSockaEventToPeers as f, type SockaEmitCapable as g, type SockaWebSocketSessionConfigLoose as h, runSockaSessionOnAttached as r };
@@ -1,11 +1,36 @@
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, w as SockaWireFormat } from '../socka-report-error-CXwpAUgl.js';
3
+ import { S as SockaStrictWebSocketInit, a as SockaWebSocketSession, b as SockaWebSocketSessionConfigUnion } from '../SockaWebSocketSession-B1w7RAid.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 the default strict upgrade 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 the session constructor will throw — that usually means you forgot
14
+ * to pass **`request`** on upgrade (or set **`strictUpgradeRequest: false`**).
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 the default strict upgrade 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
- config: SockaWebSocketSessionConfig<TContract, TData>;
33
+ config: SockaWebSocketSessionConfigUnion<TContract, TData>;
9
34
  };
10
35
  type SockaBunWebSocketHandlers<TContract extends SockaContract<SockaContractConfig>, TData, TWsData = undefined> = {
11
36
  sessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;
@@ -28,11 +53,11 @@ type SockaBunWebSocketHandlers<TContract extends SockaContract<SockaContractConf
28
53
  * `sessionMap` and `config` for that socket’s scope (e.g. from `ws.data.roomId`).
29
54
  * The returned `sessionMap` is an empty placeholder; real maps come from `resolveScope`.
30
55
  */
31
- declare function createSockaBunWebSocketHandlers<TContract extends SockaContract<SockaContractConfig>, TData>(config: SockaWebSocketSessionConfig<TContract, TData>, options?: {
56
+ declare function createSockaBunWebSocketHandlers<TContract extends SockaContract<SockaContractConfig>, TData>(config: SockaWebSocketSessionConfigUnion<TContract, TData>, options?: {
32
57
  sessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;
33
58
  }): SockaBunWebSocketHandlers<TContract, TData, undefined>;
34
59
  declare function createSockaBunWebSocketHandlers<TContract extends SockaContract<SockaContractConfig>, TData, TWsData>(options: {
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-LVVCHLNW.js';
3
+ import { reportSockaError } from '../chunk-IFIGKR3W.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