@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.
- package/README.md +195 -42
- package/dist/SockaWebSocketSession-B1w7RAid.d.ts +209 -0
- package/dist/bun/index.d.ts +30 -5
- package/dist/bun/index.js +28 -5
- package/dist/bun/index.js.map +1 -1
- package/dist/{chunk-MZCQHJXY.js → chunk-IFIGKR3W.js} +45 -8
- package/dist/chunk-IFIGKR3W.js.map +1 -0
- package/dist/{chunk-45D4T232.js → chunk-LVVCHLNW.js} +74 -9
- package/dist/chunk-LVVCHLNW.js.map +1 -0
- package/dist/{chunk-AM7PB26G.js → chunk-P3JEEOJL.js} +192 -10
- package/dist/chunk-P3JEEOJL.js.map +1 -0
- package/dist/chunk-QGURL3DJ.js +8 -0
- package/dist/chunk-QGURL3DJ.js.map +1 -0
- package/dist/client/index.d.ts +59 -3
- package/dist/client/index.js +2 -2
- package/dist/core/index.d.ts +2 -21
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/do/index.d.ts +20 -2
- package/dist/do/index.js +36 -2
- package/dist/do/index.js.map +1 -1
- package/dist/hono/cloudflare-workers.d.ts +4 -4
- package/dist/hono/cloudflare-workers.js +4 -3
- package/dist/hono/cloudflare-workers.js.map +1 -1
- package/dist/hono/index.d.ts +22 -6
- package/dist/hono/index.js +5 -3
- package/dist/hono/index.js.map +1 -1
- package/dist/react/index.d.ts +43 -4
- package/dist/react/index.js +103 -9
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.d.ts +18 -5
- package/dist/server/index.js +24 -4
- package/dist/server/index.js.map +1 -1
- package/dist/{socka-report-error-DzFI2Tr7.d.ts → socka-report-error-CXwpAUgl.d.ts} +80 -8
- package/dist/test/index.d.ts +11 -0
- package/dist/test/index.js +84 -0
- package/dist/test/index.js.map +1 -0
- package/docs/README.md +16 -7
- package/docs/auth.md +27 -0
- package/docs/backpressure.md +16 -0
- package/docs/client.md +48 -3
- package/docs/comparison.md +2 -2
- package/docs/durable-objects.md +3 -3
- package/docs/getting-started.md +143 -84
- package/docs/history.md +26 -0
- package/docs/internals.md +56 -0
- package/docs/lifecycle.md +3 -3
- package/docs/multi-room.md +10 -8
- package/docs/peers.md +11 -7
- package/docs/presence.md +43 -0
- package/docs/{events.md → pushes.md} +1 -1
- package/docs/recipes.md +77 -0
- package/docs/reconnection.md +44 -0
- package/docs/reference.md +27 -32
- package/docs/server.md +19 -3
- package/docs/testing.md +20 -0
- package/docs/wire-format.md +29 -0
- package/examples/minimal-socka.ts +56 -3
- package/package.json +14 -10
- package/roadmap.md +2 -2
- package/skills/socka/core-rpc/SKILL.md +2 -2
- package/skills/socka/do-session/SKILL.md +2 -2
- package/skills/socka/standard-schema/SKILL.md +1 -1
- package/dist/SockaWebSocketSession-Bru8yFcK.d.ts +0 -107
- package/dist/chunk-45D4T232.js.map +0 -1
- package/dist/chunk-AM7PB26G.js.map +0 -1
- package/dist/chunk-MZCQHJXY.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,11 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|
|
|
13
|
-
**Typed WebSocket RPC for TypeScript.**
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
28
|
-
input: z.object({
|
|
29
|
-
output: z.object({
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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 {
|
|
195
|
+
import { chatContract } from "./contract";
|
|
67
196
|
|
|
68
197
|
const session = new SockaSession({
|
|
69
|
-
contract:
|
|
70
|
-
url: "ws://localhost:3450/ws",
|
|
198
|
+
contract: chatContract,
|
|
199
|
+
url: "ws://localhost:3450/ws/lobby?name=Ada",
|
|
71
200
|
});
|
|
72
|
-
|
|
73
|
-
console.log(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
|
93
|
-
|
|
94
|
-
| **
|
|
95
|
-
| **
|
|
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
|
|
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:** [
|
|
260
|
+
**Roadmap:** [deferred ideas and future work](./roadmap.md). Agent skills: [`skills/`](./skills/).
|
|
109
261
|
|
|
110
262
|
## Full-stack examples
|
|
111
263
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
|
115
|
-
|
|
116
|
-
| **
|
|
117
|
-
|
|
|
118
|
-
| **
|
|
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
|
-
|
|
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 };
|
package/dist/bun/index.d.ts
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import { ServerWebSocket } from 'bun';
|
|
2
|
-
import { S as SockaContract, a as SockaContractConfig,
|
|
3
|
-
import { S as
|
|
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:
|
|
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:
|
|
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-
|
|
3
|
-
import { reportSockaError } from '../chunk-
|
|
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
|
|
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
|
|
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
|