@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/docs/comparison.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Compared to
|
|
1
|
+
# Compared to custom WebSocket RPC (and common alternatives)
|
|
2
2
|
|
|
3
3
|
Most apps model messages as large discriminated unions (`type` + `id`), validate twice (once on the wire, once in the handler), and maintain a **pending `Map<string, Deferred>`** for every RPC. That works, but types drift between client and server, correlation IDs are easy to get wrong, and pushing **server events** becomes a second, parallel protocol.
|
|
4
4
|
|
|
@@ -33,4 +33,4 @@ Most apps model messages as large discriminated unions (`type` + `id`), validate
|
|
|
33
33
|
|
|
34
34
|
## When socka is a good fit
|
|
35
35
|
|
|
36
|
-
You want **schema-first WebSocket RPC** with **correlated request/response** and optional **typed server push** from a **single contract module**—and you are fine with **socka v1** frames (see **[
|
|
36
|
+
You want **schema-first WebSocket RPC** with **correlated request/response** and optional **typed server push** from a **single contract module**—and you are fine with **socka v1** frames (see **[Internals](./internals.md)**) so you can swap runtimes (Bun, Hono, Durable Objects, Node **`ws`**) behind the same procedures.
|
package/docs/durable-objects.md
CHANGED
|
@@ -14,11 +14,11 @@ This is the **Cloudflare** side (bindings, Wrangler, generated types)—not sock
|
|
|
14
14
|
1. **Wrangler** — `wrangler.jsonc` / `wrangler.toml` with a **Durable Object** binding and a **migration** for the DO class (see Cloudflare docs and the runnable **[tic-tac-toe-do example](https://github.com/firtoz/fullstack-toolkit/tree/main/examples/tic-tac-toe-do)** in this repo).
|
|
15
15
|
2. **Typed `Env`** — After you change bindings or `wrangler` config, regenerate env types (**`bun run typegen`** / **`cf-typegen`** via [`@firtoz/worker-helper`](https://github.com/firtoz/fullstack-toolkit/tree/main/packages/worker-helper)); **do not hand-edit** `worker-configuration.d.ts`. Workflow reference: [Cloudflare / Wrangler typegen skill](https://github.com/firtoz/fullstack-toolkit/blob/main/.cursor/skills/cloudflare-wrangler-typegen/SKILL.md) in this monorepo.
|
|
16
16
|
3. **Run locally** — `wrangler dev` (optionally pin a port—e.g. **3463** in the tic-tac-toe example so it does not clash with other apps).
|
|
17
|
-
4. **Peers** — **`@firtoz/socka/do`** needs **`@firtoz/websocket-do
|
|
17
|
+
4. **Peers** — **`@firtoz/socka/do`** needs **`@firtoz/websocket-do`** and **`hono`** — see **[Peers](./peers.md)**. Use **`wrangler types`** (or your app’s typegen) for Cloudflare/DO bindings in TypeScript.
|
|
18
18
|
|
|
19
19
|
### Wire format
|
|
20
20
|
|
|
21
|
-
**`wireFormat`** defaults to **`"json"`** (text frames). Use **`"msgpack"`** only if the client also uses **`msgpack`**. Mismatched **`wireFormat`** between client and session config will fail to decode.
|
|
21
|
+
**`wireFormat`** defaults to **`"json"`** (text frames). Use **`"msgpack"`** only if the client also uses **`msgpack`**. Mismatched **`wireFormat`** between client and session config will fail to decode. See **[Reference](./reference.md#wire-encoding-json-and-msgpack)** and **[Internals](./internals.md)**.
|
|
22
22
|
|
|
23
23
|
## `SockaDoSession`
|
|
24
24
|
|
|
@@ -39,7 +39,7 @@ new SockaDoSession(websocket, sessions, {
|
|
|
39
39
|
});
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Handler types use **`InferSockaHandlers<typeof myContract, SockaDoSession<typeof myContract, …>>`**. Throw **`SockaError`** for expected domain failures
|
|
42
|
+
Handler types use **`InferSockaHandlers<typeof myContract, SockaDoSession<typeof myContract, …>>`**. Same semantics as **`SockaWebSocketSession`**: calls **with** **`output`** get a validated **`serverResponse`**; calls **without** **`output`** are fire-and-forget on success (no success frame). Throw **`SockaError`** for expected domain failures so the client receives a structured **`serverError`** frame (with optional **`rpc`** on the wire). For client-side **`reportError`** when using output-less calls, see **[Reference](./reference.md#optional-output-fire-and-forget)** and **[Client](./client.md#fire-and-forget)**.
|
|
43
43
|
|
|
44
44
|
**`createData`** — If your session needs typed **`session.data`**, provide **`createData: (ctx) => …`** where **`ctx`** is a Hono **`Context`** (bindings, request, etc.). That runs when the DO accepts the socket; data participates in **hibernation** via **`@firtoz/websocket-do`** **`BaseSession`** (see **`session.update()`** below).
|
|
45
45
|
|
package/docs/getting-started.md
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
# Getting started
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This guide walks through a **multi-room chat** on one **`defineSocka`** contract: **typed RPCs** (`listHistory`, `listPresence`, `sendMessage`, `clearHistory`) and **typed pushes** (`userJoined`, `userLeft`, `roomMessage`, `historyCleared`). The **[README](../README.md)** has the shortest runnable **Bun** slice (in-memory history).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Runnable apps** with persistence and a **multi-room browser client**: **[chatroom-bun](../../../examples/chatroom-bun)** (Bun SQLite), **[chatroom-hono](../../../examples/chatroom-hono)** (JSON files), **[chatroom-do](../../../examples/chatroom-do)** (Durable Object SQLite + Drizzle).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
For API tables, see **[Reference](./reference.md)**. For wire details, **[Internals](./internals.md)**.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Step 1 — What you are building
|
|
12
|
+
|
|
13
|
+
1. Clients connect to **`ws://host/ws/<roomId>?name=<displayName>`** (path and query are conventions you control).
|
|
14
|
+
2. Each **room** has its own **`sessionMap`** and **config** — see **[Multi-room](./multi-room.md)**.
|
|
15
|
+
3. **Join/leave** are **`pushes`** to everyone else in the room; **chat lines** are **`pushes`** too (after you persist).
|
|
16
|
+
4. **History** is loaded with a **call** (`listHistory`) so reconnects and new panes can hydrate from storage (SQLite, JSON files, or DO SQLite in the examples).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Step 2 — Shared contract
|
|
21
|
+
|
|
22
|
+
Use one module on the client and every server:
|
|
8
23
|
|
|
9
24
|
**`contract.ts`**
|
|
10
25
|
|
|
@@ -12,127 +27,171 @@ Save three files next to each other, then run **`bun run server.ts`**. Point a c
|
|
|
12
27
|
import { defineSocka } from "@firtoz/socka/core";
|
|
13
28
|
import * as z from "zod";
|
|
14
29
|
|
|
15
|
-
export const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
30
|
+
export const messageRow = z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
ts: z.number(),
|
|
33
|
+
userId: z.string(),
|
|
34
|
+
displayName: z.string(),
|
|
35
|
+
text: z.string(),
|
|
22
36
|
});
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
**`server.ts`**
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
import { createSockaBunWebSocketHandlers } from "@firtoz/socka/bun";
|
|
29
|
-
import { myContract } from "./contract";
|
|
38
|
+
export type ChatMessageRow = z.infer<typeof messageRow>;
|
|
30
39
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
echo: async (input) => ({ text: input.text }),
|
|
35
|
-
},
|
|
36
|
-
handleClose: async () => {},
|
|
40
|
+
const onlineUser = z.object({
|
|
41
|
+
userId: z.string(),
|
|
42
|
+
displayName: z.string(),
|
|
37
43
|
});
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
export const chatContract = defineSocka({
|
|
46
|
+
calls: {
|
|
47
|
+
listHistory: {
|
|
48
|
+
input: z.object({ limit: z.number().int().min(1).max(500).optional() }),
|
|
49
|
+
output: z.object({ messages: z.array(messageRow) }),
|
|
50
|
+
},
|
|
51
|
+
listPresence: {
|
|
52
|
+
input: z.object({}).optional(),
|
|
53
|
+
output: z.object({
|
|
54
|
+
selfUserId: z.string(),
|
|
55
|
+
users: z.array(onlineUser),
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
sendMessage: {
|
|
59
|
+
input: z.object({ text: z.string().min(1) }),
|
|
60
|
+
output: z.object({ ok: z.literal(true) }),
|
|
61
|
+
},
|
|
62
|
+
clearHistory: {
|
|
63
|
+
input: z.object({}).optional(),
|
|
64
|
+
output: z.object({ ok: z.literal(true) }),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
pushes: {
|
|
68
|
+
userJoined: z.object({ userId: z.string(), displayName: z.string() }),
|
|
69
|
+
userLeft: z.object({
|
|
70
|
+
userId: z.string(),
|
|
71
|
+
displayName: z.string(),
|
|
72
|
+
}),
|
|
73
|
+
roomMessage: messageRow,
|
|
74
|
+
historyCleared: z.object({
|
|
75
|
+
ts: z.number(),
|
|
76
|
+
clearedByUserId: z.string(),
|
|
77
|
+
clearedByDisplayName: z.string(),
|
|
78
|
+
}),
|
|
47
79
|
},
|
|
48
|
-
websocket,
|
|
49
80
|
});
|
|
50
81
|
```
|
|
51
82
|
|
|
52
|
-
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Step 3 — Client: subscribe, hydrate, send
|
|
86
|
+
|
|
87
|
+
1. Open **`SockaSession`** with **`url`** pointing at your upgrade path (same **`roomId`** and **`name`** the server parses in **`createData`**).
|
|
88
|
+
2. After the socket is ready, **`await session.send.listHistory({})`** (or `{ limit: 100 }`) and render **`messages`**, then **`await session.send.listPresence({})`** to show **who is online** (compare **`selfUserId`** to highlight the current user).
|
|
89
|
+
3. Register **`session.subscribe.on("userJoined" | "userLeft" | "roomMessage" | "historyCleared", …)`** for live updates (merge joins/leaves into your presence UI; on **`historyCleared`**, drop or redraw stored chat lines for that room).
|
|
90
|
+
4. Send **`await session.send.sendMessage({ text: "…" })`**. Optional: **`await session.send.clearHistory({})`** to wipe persisted messages for the room (server should **`broadcastPush("historyCleared", …)`** so every client updates).
|
|
91
|
+
|
|
92
|
+
**Minimal client**
|
|
53
93
|
|
|
54
94
|
```ts
|
|
55
95
|
import { SockaSession } from "@firtoz/socka/client";
|
|
56
|
-
import {
|
|
96
|
+
import { chatContract } from "./contract";
|
|
57
97
|
|
|
58
98
|
const session = new SockaSession({
|
|
59
|
-
contract:
|
|
60
|
-
url: "ws://localhost:
|
|
99
|
+
contract: chatContract,
|
|
100
|
+
url: "ws://localhost:3464/ws/lobby?name=Ada",
|
|
61
101
|
});
|
|
62
|
-
|
|
102
|
+
|
|
103
|
+
session.subscribe.on("userJoined", (p) => console.log("in", p.displayName));
|
|
104
|
+
session.subscribe.on("userLeft", (p) => console.log("out", p.displayName));
|
|
105
|
+
session.subscribe.on("roomMessage", (m) => console.log(`${m.displayName}: ${m.text}`));
|
|
106
|
+
session.subscribe.on("historyCleared", (p) =>
|
|
107
|
+
console.log("cleared by", p.clearedByDisplayName),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const { messages } = await session.send.listHistory({ limit: 50 });
|
|
111
|
+
for (const m of messages) console.log(`[hist] ${m.displayName}: ${m.text}`);
|
|
112
|
+
|
|
113
|
+
const { selfUserId, users } = await session.send.listPresence({});
|
|
114
|
+
console.log("online", selfUserId, users);
|
|
115
|
+
|
|
116
|
+
await session.send.sendMessage({ text: "hello" });
|
|
63
117
|
```
|
|
64
118
|
|
|
65
|
-
|
|
119
|
+
**Multiple rooms on one page** — use **one `SockaSession` per room** (see the chat example **`public/index.html`** + **`src/client.ts`**): each pane builds its own URL and keeps its own subscriptions.
|
|
66
120
|
|
|
67
|
-
|
|
121
|
+
By default, **`wireFormat`** is JSON — see **[Reference](./reference.md#wire-encoding-json-and-msgpack)** if you use **`msgpack`**.
|
|
68
122
|
|
|
69
|
-
|
|
123
|
+
---
|
|
70
124
|
|
|
71
|
-
##
|
|
125
|
+
## Step 4 — Server behavior (all runtimes)
|
|
72
126
|
|
|
73
|
-
|
|
127
|
+
For each **`SockaWebSocketSessionConfig`** / **`SockaDoSessionConfig`**:
|
|
74
128
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
129
|
+
1. **`createData`** — Parse **`roomId`** from the upgrade URL (path) and **`displayName`** from **`name`** query; set **`userId`** (e.g. **`crypto.randomUUID()`**). Same shape as the README **`SockaWebSocketInit`** / Hono **`Context`** on DO.
|
|
130
|
+
2. **`onAttached`** — `await session.broadcastPush("userJoined", { userId, displayName }, true)` (**`excludeSelf: true`** so only peers see the join).
|
|
131
|
+
3. **`handlers.listHistory`** — Read from your store for **`session.data.roomId`** (Bun: SQLite; Hono: JSON file; DO: SQLite in the object).
|
|
132
|
+
4. **`handlers.listPresence`** — Walk the room’s **`sessionMap`** (or DO **`this.sessions`**) and return **`{ selfUserId: session.data.userId, users: [{ userId, displayName }, …] }`** sorted for display.
|
|
133
|
+
5. **`handlers.sendMessage`** — Persist the line, then **`await session.broadcastPush("roomMessage", row)`** (everyone in the room, including the sender, unless you choose **`excludeSelf`**).
|
|
134
|
+
6. **`handlers.clearHistory`** — Delete persisted messages for **`session.data.roomId`**, then **`await session.broadcastPush("historyCleared", { ts, clearedByUserId, clearedByDisplayName })`** so all clients refresh their UI.
|
|
135
|
+
7. **`handleClose`** — `await session.broadcastPush("userLeft", { userId, displayName: session.data.displayName }, true)` — **`displayName`** is the **session’s name at disconnect** (same field as in **`userJoined`** / messages), so clients can render a leave line without guessing from **`userId`** alone. Session is still in **`sessions`** until **`handleClose`** finishes — see **[Lifecycle](./lifecycle.md)**.
|
|
82
136
|
|
|
83
|
-
|
|
137
|
+
---
|
|
84
138
|
|
|
85
|
-
##
|
|
139
|
+
## Step 5 — Wire the server by runtime
|
|
86
140
|
|
|
87
|
-
|
|
88
|
-
npm install @firtoz/socka
|
|
89
|
-
```
|
|
141
|
+
Pick a row, then follow the numbered steps in that subsection.
|
|
90
142
|
|
|
91
|
-
|
|
143
|
+
| Runtime | Install | Full example |
|
|
144
|
+
|---------|---------|--------------|
|
|
145
|
+
| **Bun** | `npm install @firtoz/socka` (+ **`bun-types`** dev if needed) | [chatroom-bun](../../../examples/chatroom-bun) |
|
|
146
|
+
| **Node + `ws`** | `npm install @firtoz/socka ws` (+ **`@types/ws`** dev) | Same contract; attach pattern below |
|
|
147
|
+
| **Hono (Node)** | `npm install @firtoz/socka hono @hono/node-ws @hono/node-server ws` | [chatroom-hono](../../../examples/chatroom-hono) |
|
|
148
|
+
| **Hono (Workers)** | `npm install @firtoz/socka hono` | **[Server](./server.md#firtoz-socka-hono-cloudflare-workers)** — usually **`sockaHonoCloudflare`**; session often starts on first message |
|
|
149
|
+
| **Durable Objects** | `npm install @firtoz/socka hono @firtoz/websocket-do` | [chatroom-do](../../../examples/chatroom-do) |
|
|
92
150
|
|
|
93
|
-
|
|
151
|
+
More installs: **[Peers](./peers.md)**. Cloudflare typings: **`wrangler types`**.
|
|
94
152
|
|
|
95
|
-
|
|
153
|
+
### Bun (`Bun.serve`)
|
|
96
154
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
155
|
+
1. **Open** a **`Database`** from **`bun:sqlite`** (one file; table keyed by **`room_id`**), **`CREATE TABLE IF NOT EXISTS`** for messages.
|
|
156
|
+
2. **`getOrCreateRoom(roomId)`** returns **`{ sessionMap, config }`** where **`config`** closes over **`roomId`** and **`db`**.
|
|
157
|
+
3. **`createSockaBunWebSocketHandlers({ resolveScope })`** — **`resolveScope(ws)`** reads **`ws.data.roomId`** (set in **`fetch`** via **`server.upgrade(req, { data: { roomId } })`**).
|
|
158
|
+
4. **`fetch`** upgrades **`/ws/:roomId`** (decode the segment).
|
|
100
159
|
|
|
101
|
-
|
|
102
|
-
calls: {
|
|
103
|
-
echo: {
|
|
104
|
-
input: z.object({ text: z.string() }),
|
|
105
|
-
output: z.object({ text: z.string() }),
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
```
|
|
160
|
+
### Node + `ws`
|
|
110
161
|
|
|
111
|
-
|
|
162
|
+
1. **`new WebSocketServer({ port })`**.
|
|
163
|
+
2. On **`connection`**, parse **`roomId`** from **`req.url`**, **`getOrCreateRoom`**, then **`attachSockaWebSocket( ws, room.sessionMap, room.config, { request: req } )`** so **`createData`** sees the URL.
|
|
112
164
|
|
|
113
|
-
|
|
165
|
+
### Hono on Node
|
|
114
166
|
|
|
115
|
-
|
|
167
|
+
1. **`createNodeWebSocket({ app })`** from **`@hono/node-ws`**.
|
|
168
|
+
2. **`app.get("/ws/:roomId", upgradeWebSocket((c) => { const room = getOrCreateRoom(c.req.param("roomId")); return sockaHonoNodeWs(room.config, { sessions: room.sessionMap })(c); }))`**.
|
|
169
|
+
3. **`serve`** + **`injectWebSocket(server)`**.
|
|
116
170
|
|
|
117
|
-
|
|
118
|
-
2. **Client** — Keep **`SockaSession`** (or **`useSockaSession`** / **`SockaSessionProvider`**—**[Client](./client.md)**) with the **same** **`wireFormat`** as the server.
|
|
171
|
+
### Hono on Cloudflare Workers
|
|
119
172
|
|
|
120
|
-
|
|
173
|
+
1. Use **`upgradeWebSocket`** from **`hono/cloudflare-workers`** with **`sockaHonoCloudflare`** — see **[Server](./server.md)**.
|
|
174
|
+
2. For **room routing** without a DO, put **`roomId`** in the path and parse it in **`createData`** from **`init.request`**.
|
|
121
175
|
|
|
122
|
-
|
|
176
|
+
### Cloudflare Durable Objects
|
|
123
177
|
|
|
124
|
-
|
|
178
|
+
1. **Worker** — route **`/ws/:roomId`** to **`env.CHAT_ROOM.idFromName(roomId).get(id).fetch(...)`** (stub forwards WebSocket upgrade to the DO).
|
|
179
|
+
2. **DO class** — extend **`SockaWebSocketDO`**; **`SockaDoSession`** handlers use **Drizzle** on **`drizzle(ctx.storage)`** (see [chatroom-do](../../../examples/chatroom-do)).
|
|
180
|
+
3. **One DO instance per room** — history lives in that object’s SQLite; no **`room_id`** column needed if the table is per-DO.
|
|
125
181
|
|
|
126
|
-
|
|
182
|
+
---
|
|
127
183
|
|
|
128
|
-
|
|
129
|
-
|-------|--------|------|
|
|
130
|
-
| **Bun** | [`tic-tac-toe-bun`](../../../examples/tic-tac-toe-bun) | **3461** |
|
|
131
|
-
| **Hono + Node** | [`tic-tac-toe-hono`](../../../examples/tic-tac-toe-hono) | **3462** |
|
|
132
|
-
| **Durable Objects** | [`tic-tac-toe-do`](../../../examples/tic-tac-toe-do) | **3463** |
|
|
184
|
+
## Full-stack examples (chat + tic-tac-toe)
|
|
133
185
|
|
|
134
|
-
|
|
186
|
+
| Topic | Stack | Folder | Port |
|
|
187
|
+
|-------|--------|--------|------|
|
|
188
|
+
| Chat + history | **Bun** + SQLite | [`chatroom-bun`](../../../examples/chatroom-bun) | **3464** |
|
|
189
|
+
| Chat + history | **Hono + Node** + JSON | [`chatroom-hono`](../../../examples/chatroom-hono) | **3465** |
|
|
190
|
+
| Chat + history | **DO** + Drizzle SQLite | [`chatroom-do`](../../../examples/chatroom-do) | **3466** |
|
|
191
|
+
| Tic-tac-toe | **Bun** | [`tic-tac-toe-bun`](../../../examples/tic-tac-toe-bun) | **3461** |
|
|
192
|
+
| Tic-tac-toe | **Hono + Node** | [`tic-tac-toe-hono`](../../../examples/tic-tac-toe-hono) | **3462** |
|
|
193
|
+
| Tic-tac-toe | **DO** | [`tic-tac-toe-do`](../../../examples/tic-tac-toe-do) | **3463** |
|
|
135
194
|
|
|
136
195
|
---
|
|
137
196
|
|
|
138
|
-
Next: [Peers](./peers.md) · [Server](./server.md) · [Durable Objects](./durable-objects.md) · [Client](./client.md) · [Reference](./reference.md)
|
|
197
|
+
Next: [Peers](./peers.md) · [Multi-room](./multi-room.md) · [Server](./server.md) · [Durable Objects](./durable-objects.md) · [Client](./client.md) · [Reference](./reference.md) · [Internals](./internals.md)
|
package/docs/history.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# History (pagination, retention, invalidation)
|
|
2
|
+
|
|
3
|
+
Long-lived rooms often keep a **message log** on the server. Patterns that work well with socka:
|
|
4
|
+
|
|
5
|
+
## Pagination / cursor
|
|
6
|
+
|
|
7
|
+
Expose an RPC such as **`listHistory({ limit?, before? })`** where **`before`** is an opaque cursor (e.g. oldest **`ts`** or **`id`** already shown). Return **`messages`** newest-first or oldest-first consistently, and document which end **`before`** anchors.
|
|
8
|
+
|
|
9
|
+
Clients load an initial page after connect, then **prepend** older pages when the user scrolls up.
|
|
10
|
+
|
|
11
|
+
## Retention
|
|
12
|
+
|
|
13
|
+
Enforce **max rows per room** or **time-based pruning** in the handler that **writes** history (e.g. after **`sendMessage`**). Truncation stays a **server policy**; clients learn about bulk wipes via a **push**.
|
|
14
|
+
|
|
15
|
+
## `historyCleared` (or equivalent)
|
|
16
|
+
|
|
17
|
+
When one client clears history for everyone, **mutate storage** then **`broadcastPush("historyCleared", { ts, … })`**. Other clients should **drop local message lists** (or refetch **`listHistory`**) so UIs stay consistent.
|
|
18
|
+
|
|
19
|
+
## Reconnect
|
|
20
|
+
|
|
21
|
+
After a reconnect, **re-run** **`listHistory`** (or your snapshot RPC) — see **[Reconnection](./reconnection.md)**.
|
|
22
|
+
|
|
23
|
+
## See also
|
|
24
|
+
|
|
25
|
+
- **[Getting started](./getting-started.md)** — chat flow.
|
|
26
|
+
- **[Pushes](./pushes.md)** — broadcasting invalidation events.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Library internals
|
|
2
|
+
|
|
3
|
+
This page is for **contributors** and readers who want the **wire protocol** and implementation edges. If you only need to **use** socka, start with **[Getting started](./getting-started.md)** and **[Reference](./reference.md)**.
|
|
4
|
+
|
|
5
|
+
**Source (monorepo paths):**
|
|
6
|
+
|
|
7
|
+
- Logical frames and **`decodeSockaWire`** — [`packages/socka/src/core/envelope.ts`](../../src/core/envelope.ts)
|
|
8
|
+
- JSON vs msgpack **transport** (`encodeSockaWire`, **`parseWirePayload`**) — [`packages/socka/src/core/wire-codec.ts`](../../src/core/wire-codec.ts)
|
|
9
|
+
- Inbound dispatch — [`packages/socka/src/server/dispatchSockaInboundMessage.ts`](../../src/server/dispatchSockaInboundMessage.ts)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Wire encoding: JSON and msgpack
|
|
14
|
+
|
|
15
|
+
Socka has two layers: **transport encoding** (how each WebSocket frame is serialized) and **logical frames** (the socka v1 object inside). Both sides must agree on **`wireFormat`** or decoding fails immediately (wrong frame type or parse errors).
|
|
16
|
+
|
|
17
|
+
| `wireFormat` | WebSocket frame | Bytes on the wire |
|
|
18
|
+
|--------------|-----------------|-------------------|
|
|
19
|
+
| **`"json"`** (default) | **Text** — `send(string)` | UTF-8 JSON of the **whole** envelope (one JSON object per frame). Uses **`serializeJson`** / **`deserializeJson`** when set, otherwise `JSON.stringify` / `JSON.parse`. |
|
|
20
|
+
| **`"msgpack"`** | **Binary** — `send(ArrayBuffer)` | [msgpack](https://msgpack.org/) of the same envelope object graph. In the browser, **`SockaWebSocketClient`** sets **`binaryType = "arraybuffer"`** so binary frames decode correctly. |
|
|
21
|
+
|
|
22
|
+
**Rules**
|
|
23
|
+
|
|
24
|
+
- Set **`wireFormat`** to the **same value** on the **client** (`SockaSession` / `SockaWebSocketClient` / `useSockaSession` options) and on **every server session** that talks to that client (`SockaWebSocketSessionConfig`, `SockaDoSessionConfig`, and the `config` passed to **`createSockaBunWebSocketHandlers`**, **`sockaHonoNodeWs`**, **`sockaHonoCloudflare`**, etc.).
|
|
25
|
+
- **RPCs and contract pushes** share one encoding: `clientRequest` / `serverResponse` / `serverError` / `serverEvent` are all wrapped the same way.
|
|
26
|
+
- If you use **`dispatchSockaInboundMessage`** manually, pass the same **`wireFormat`** as the peer used to **encode** the frame.
|
|
27
|
+
- Optional **`serializeJson`** / **`deserializeJson`** on client or server config only affect **JSON mode** (the outer envelope). Call **`body`** and push **`body`** values are still whatever your **Standard Schema** accepts after JSON/msgpack decode.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Logical frames (socka v1)
|
|
32
|
+
|
|
33
|
+
Every decoded payload is one logical socka **v1** object. **`decodeSockaWire`** checks shape after `JSON.parse` (text) or msgpack unpack (binary).
|
|
34
|
+
|
|
35
|
+
| Kind | Role |
|
|
36
|
+
|------|------|
|
|
37
|
+
| `clientRequest` | Client → server RPC (`id`, `rpc`, `body`) |
|
|
38
|
+
| `serverResponse` | Success reply (correlated by `id`) — **omitted** when the contract call has **no** **`output`** (fire-and-forget success); see **[Reference](./reference.md)** |
|
|
39
|
+
| `serverError` | Correlated failure (`id`, **`error`** string; optional **`code`**, **`data`**, **`rpc`**) — **`rpc`** names the procedure when the failure is tied to an RPC |
|
|
40
|
+
| `serverEvent` | Server push (`event`, `body`) — **not** tied to an RPC `id` |
|
|
41
|
+
|
|
42
|
+
Clients generate **`id`** strings per request; servers echo them on **`serverResponse`** (when the call declares **`output`**) and on **`serverError`**. **`serverEvent`** uses the contract **`pushes`** map and **`session.subscribe`** on the client.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## TypeScript: `SockaWebSocketDO` and contract erasure
|
|
47
|
+
|
|
48
|
+
`@firtoz/socka/do` **erases** the contract slot on **`SockaWebSocketDO`** so concrete `defineSocka(...)` contracts stay strict under TypeScript. If a generic base class rejects your session type, keep using **your** contract type from the module where you called **`defineSocka`**—do not expect an unconstrained `SockaContract<SockaContractConfig>` to accept every concrete contract without that erasure.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Inbound path (server)
|
|
53
|
+
|
|
54
|
+
**`attachSockaWebSocket`** uses **`dispatchSockaInboundMessage`** with the same `data` shape as a DOM **`MessageEvent`** (`string`, **`ArrayBuffer`**, **`Blob`**, **`ArrayBufferView`**, or **`Buffer`** on Node/Bun). If you handle **`message`** yourself, call **`dispatchSockaInboundMessage(session, wireFormat, data)`** with matching **`wireFormat`**.
|
|
55
|
+
|
|
56
|
+
See also [Reference](./reference.md) for **`SockaWebSocketSessionConfig`** fields and [Server](./server.md) for adapters.
|
package/docs/lifecycle.md
CHANGED
|
@@ -5,21 +5,21 @@ Join, message, and **leave** ordering for socka sessions—whether you use **`@f
|
|
|
5
5
|
## Registration and `onAttached`
|
|
6
6
|
|
|
7
7
|
1. The adapter accepts or upgrades a **`WebSocket`** and constructs a session (**`SockaWebSocketSession`** or **`SockaDoSession`**).
|
|
8
|
-
2. The session is **registered** in the shared **`sessions`** map (the map **`
|
|
8
|
+
2. The session is **registered** in the shared **`sessions`** map (the map **`broadcastPush`** and peer iteration use).
|
|
9
9
|
3. On the next microtask, **`onAttached`** runs (if you provided it). Other sessions in the map can see this socket—use this for join broadcasts, not the constructor.
|
|
10
10
|
|
|
11
11
|
If **`onAttached`** throws or returns a rejected promise, the failure is reported via **`reportError`** (or **`console.error`** by default) with kind **`serverOnAttached`**.
|
|
12
12
|
|
|
13
13
|
## Inbound RPCs
|
|
14
14
|
|
|
15
|
-
While the socket is open, inbound data is decoded
|
|
15
|
+
While the socket is open, inbound data is decoded and validated, then **`handlers[procedure]`** runs. Handler exceptions → **`onHandlerError`**; bad wire payloads → **`onValidationError`** before your handler—see **[Reference](./reference.md)**.
|
|
16
16
|
|
|
17
17
|
## Close and `handleClose`
|
|
18
18
|
|
|
19
19
|
When the transport closes:
|
|
20
20
|
|
|
21
21
|
1. The adapter calls **`await session.invokeHandleClose()`**, which runs **your** **`handleClose(session)`**.
|
|
22
|
-
2. **Until that finishes, the session remains in **`sessions`**—so peer iteration and **`
|
|
22
|
+
2. **Until that finishes, the session remains in **`sessions`**—so peer iteration and **`broadcastPush`** can still see the closing peer (e.g. “last player left”).
|
|
23
23
|
3. Then the adapter removes the socket from the map.
|
|
24
24
|
|
|
25
25
|
**`SockaDoSession`** delegates teardown through **`@firtoz/websocket-do`**; see **[Durable Objects](./durable-objects.md)** for **`BaseSession`** details.
|
package/docs/multi-room.md
CHANGED
|
@@ -2,30 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
A **room** (channel, game, namespace) is a **scope** where every client shares one **`sessionMap`** and one session **config** (the object you pass to **`attachSockaWebSocket`**, **`sockaHonoNodeWs`**, **`createSockaBunWebSocketHandlers`**, …).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
If you care about **encoding** (`json` vs `msgpack`), everyone in that scope must use the same **`wireFormat`** — see **[Reference](./reference.md#wire-encoding-json-and-msgpack)** (details in **[Internals](./internals.md)**).
|
|
6
6
|
|
|
7
7
|
**Durable Objects** — Often one **Durable Object instance** per room (e.g. **`idFromName(roomId)`**), with one **`sessions`** map per instance. See **[Durable Objects](./durable-objects.md)**.
|
|
8
8
|
|
|
9
9
|
Within a scope:
|
|
10
10
|
|
|
11
11
|
- All **`WebSocket`** instances are keys in the same **`Map<WebSocket, Session>`**.
|
|
12
|
-
- **`
|
|
12
|
+
- **`broadcastPush`** (and anything else that iterates **`sessions`**) only reaches sockets in **that** map — “everyone in this room” means “every session in this scope’s map.”
|
|
13
13
|
- **`handleClose(session)`** runs when a socket leaves; use **`session.websocket`** and **`session.data`** for cleanup. See **[Lifecycle](./lifecycle.md)** for ordering (your handler runs **before** the socket is removed from the map).
|
|
14
14
|
|
|
15
15
|
## Choosing a pattern
|
|
16
16
|
|
|
17
17
|
| Runtime | Pattern | When it fits |
|
|
18
18
|
|--------|---------|----------------|
|
|
19
|
-
| **Bun** | **`createSockaBunWebSocketHandlers({ resolveScope })`** | One **`Bun.serve`** `websocket` handler; **`resolveScope(ws)`** returns **`{ sessionMap, config }`**—often from **`ws.data`**
|
|
19
|
+
| **Bun** | **`createSockaBunWebSocketHandlers({ resolveScope })`** | One **`Bun.serve`** `websocket` handler; **`resolveScope(ws)`** returns **`{ sessionMap, config }`**—often **`registry.get(roomId)`** from **`createSockaRoomRegistry`** plus **`ws.data`** from the HTTP upgrade. |
|
|
20
20
|
| **Hono (Node)** | **A)** One route per room (`/ws/:roomId`) with **`getOrCreateRoom`** and **`sockaHonoNodeWs(room.config, { sessions: room.sessionMap })`**. **B)** Single upgrade route + **`resolveScope(c)`** returning **`{ sessions, config }`**. |
|
|
21
21
|
| **Durable Objects** | **One DO instance per room** via **`idFromName(roomId)`** (or similar). Each instance has its own **`sessions`** map. |
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
**Chat + persisted history (good multi-room reference):** [`chatroom-bun`](../../../examples/chatroom-bun) (SQLite), [`chatroom-hono`](../../../examples/chatroom-hono) (JSON files), [`chatroom-do`](../../../examples/chatroom-do) (Durable Object SQLite). **Games:** [`tic-tac-toe-bun`](../../../examples/tic-tac-toe-bun), [`tic-tac-toe-hono`](../../../examples/tic-tac-toe-hono), [`tic-tac-toe-do`](../../../examples/tic-tac-toe-do).
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
If you persist messages **per room**, keep storage keyed by **room** (or one DO per room) so history cannot leak across scopes.
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- **
|
|
27
|
+
## Pitfalls (for app authors)
|
|
28
|
+
|
|
29
|
+
- **Mixing rooms in one map** — If two logical rooms share a **`sessionMap`**, **`broadcastPush`** and “who’s online” can leak across rooms. Give each room its own map (or its own DO instance).
|
|
30
|
+
- **Mutating shared `config`** — Handlers close over **`config`**; changing a shared object inside it affects every connection using that config. Prefer immutable snapshots or a **per-room** config instance (e.g. one **`Game`** object per room).
|
|
31
|
+
- **Very large rooms on a Durable Object** — One DO is one isolate; huge fan-in can hit CPU or memory limits. Split traffic across multiple DOs (e.g. shard by room id) if needed.
|
|
30
32
|
|
|
31
33
|
See also [Lifecycle](./lifecycle.md) and [Server](./server.md) for **`createData`** and **`session.data`**.
|
package/docs/peers.md
CHANGED
|
@@ -41,15 +41,19 @@ Add **`bun-types`** as a dev dependency for TypeScript if you type-check Bun API
|
|
|
41
41
|
### Cloudflare Workers + Hono upgrade (`@firtoz/socka/hono/cloudflare`)
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
npm install @firtoz/socka hono
|
|
44
|
+
npm install @firtoz/socka hono
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
For **TypeScript** on Workers, run **`wrangler types`** (or your app’s **`cf-typegen`** / equivalent) so globals and bindings match your Worker — see [Cloudflare’s TypeScript guide](https://developers.cloudflare.com/workers/languages/typescript). The legacy **`@cloudflare/workers-types`** package still exists and **`@firtoz/socka`** lists it as an **optional** peer for compatibility, but **generated types from your Wrangler config are preferred**.
|
|
48
|
+
|
|
47
49
|
### Cloudflare Durable Objects (`@firtoz/socka/do`)
|
|
48
50
|
|
|
49
51
|
```bash
|
|
50
|
-
npm install @firtoz/socka hono @firtoz/websocket-do
|
|
52
|
+
npm install @firtoz/socka hono @firtoz/websocket-do
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
Use **`wrangler types`** (or your project’s typegen) for Worker/DO globals — same as above. **`@cloudflare/workers-types`** is optional if you are not using generated types yet.
|
|
56
|
+
|
|
53
57
|
**Version pairing:** `@firtoz/socka/do` subclasses **`@firtoz/websocket-do`** (`BaseSession`, `BaseWebSocketDO`). The two packages use **different** version numbers on npm—there is no rule like “same major as socka.” Use a **websocket-do** version that **socka**’s **`peerDependencies`** (and changelog, if you hit edge cases) allow for your **socka** release. You can upgrade **either** package on its own while the integration stays compatible; coordinate when **`BaseSession` / `BaseWebSocketDO`** or socka’s DO layer changes (often **TypeScript** errors first).
|
|
54
58
|
|
|
55
59
|
### Portable `ws` / `attachSockaWebSocket` only (`@firtoz/socka/server`)
|
|
@@ -62,14 +66,14 @@ Add **`@types/ws`** as a dev dependency when you use **`ws`** on Node. (Omit **`
|
|
|
62
66
|
|
|
63
67
|
---
|
|
64
68
|
|
|
65
|
-
`@firtoz/websocket-do` is marked **optional** in **`@firtoz/socka`’s** `package.json` so browser-only clients do not pull Durable Object code.
|
|
69
|
+
`@firtoz/websocket-do` is marked **optional** in **`@firtoz/socka`’s** `package.json` so browser-only clients do not pull Durable Object code.
|
|
66
70
|
|
|
67
71
|
**Any Worker that imports `@firtoz/socka/do` must add `@firtoz/websocket-do` explicitly:** `npm install @firtoz/websocket-do`. Choose a version that **satisfies socka’s stated peer range** (and your app’s lockfile); you do not need one-off “lockstep” bumps for every unrelated release—only when integration or types break.
|
|
68
72
|
|
|
69
73
|
## Practical notes
|
|
70
74
|
|
|
71
|
-
- **Only install peers for paths you use.** A Vite SPA that only imports `@firtoz/socka/client` does not need `hono
|
|
72
|
-
- **TypeScript:**
|
|
75
|
+
- **Only install peers for paths you use.** A Vite SPA that only imports `@firtoz/socka/client` does not need `hono` or `@firtoz/websocket-do`.
|
|
76
|
+
- **TypeScript on Cloudflare:** Prefer **`wrangler types`** output (or your framework’s generated **`Env`**) over manually installing **`@cloudflare/workers-types`** alone — generated types follow your **bindings** and **compatibility date**. For **`@firtoz/socka/bun`** handlers, add **`bun-types`** when you author against Bun APIs.
|
|
73
77
|
- **socka + websocket-do:** If **`@firtoz/websocket-do`** is **outside** what your **socka** version expects (or websocket-do ships a breaking `BaseSession` / `BaseWebSocketDO` change), you may see **type errors** on `SockaDoSession` / `SockaWebSocketDO` or runtime issues—then bump **one or both** until the pairing in the docs / peer range works again.
|
|
74
78
|
|
|
75
79
|
## By entrypoint (reference)
|
|
@@ -78,8 +82,8 @@ Add **`@types/ws`** as a dev dependency when you use **`ws`** on Node. (Omit **`
|
|
|
78
82
|
|--------|----------------|-----|
|
|
79
83
|
| `@firtoz/socka/core`, `@firtoz/socka/client` | **None** | Uses Standard Schema, **`WebSocket`**, and shared frame types—**`lib: ["DOM"]`** (or your bundler defaults) is enough. |
|
|
80
84
|
| `@firtoz/socka/react` | `react` **≥ 18** | Hooks and provider API. |
|
|
81
|
-
| `@firtoz/socka/do` | **`@firtoz/websocket-do`** (version range per **socka** `peerDependencies` / changelog),
|
|
85
|
+
| `@firtoz/socka/do` | **`@firtoz/websocket-do`** (version range per **socka** `peerDependencies` / changelog), **`hono`** | `SockaDoSession` extends **`BaseSession`** from **websocket-do**; **`SockaWebSocketDO`** uses **Hono**-shaped routing on top of **`BaseWebSocketDO`**. Add Cloudflare typings via **`wrangler types`**, not only the generic **`@cloudflare/workers-types`** package. |
|
|
82
86
|
| `@firtoz/socka/server` | None beyond `@firtoz/socka/core` (standard **`WebSocket`** + same contract types) | Portable **`attachSockaWebSocket`** path. |
|
|
83
87
|
| `@firtoz/socka/bun` | Same as `@firtoz/socka/server` (**`bun-types`** for TypeScript) | **`Bun.serve`** / **`ServerWebSocket`** integration. |
|
|
84
88
|
| `@firtoz/socka/hono` | **`hono`**, **`@hono/node-ws`**, **`@hono/node-server`**, **`ws`** (runtime + types) | Node **`upgradeWebSocket`** pipeline. |
|
|
85
|
-
| `@firtoz/socka/hono/cloudflare` | **`hono
|
|
89
|
+
| `@firtoz/socka/hono/cloudflare` | **`hono`** | Workers WebSocket upgrade via **`hono/cloudflare-workers`** (session often starts on first message—see **[Server](./server.md)**). Use **`wrangler types`** for Worker globals. |
|
package/docs/presence.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Presence (who is online)
|
|
2
|
+
|
|
3
|
+
“Presence” is usually modeled as:
|
|
4
|
+
|
|
5
|
+
1. **Snapshot** — an RPC (e.g. **`listPresence`**) returns the current set of users.
|
|
6
|
+
2. **Pushes** — **`userJoined`** / **`userLeft`** (or similar) update the UI when peers attach or detach.
|
|
7
|
+
|
|
8
|
+
## Server: `listPeers` / `listPeersWith`
|
|
9
|
+
|
|
10
|
+
On **`SockaWebSocketSession`** (and **`SockaDoSession`**), **`session.listPeers()`** returns **`TData[]`** for every connection in the same **`sessions`** map (same room), in **insert order**. Use **`listPeers({ excludeSelf: true })`** to omit the calling socket.
|
|
11
|
+
|
|
12
|
+
**`session.peerCount()`** / **`session.hasPeers()`** are cheap alternatives to **`listPeers().length`** when you only need a count or existence check.
|
|
13
|
+
|
|
14
|
+
**`session.listPeersWith((s) => …)`** maps each **peer session** (not just **`data`**) — useful if you need fields beyond **`TData`**.
|
|
15
|
+
|
|
16
|
+
Map that list to whatever your RPC output needs:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
listPresence: async (_input, session) => {
|
|
20
|
+
const users = session.listPeers().map((d) => ({
|
|
21
|
+
userId: d.userId,
|
|
22
|
+
displayName: d.displayName,
|
|
23
|
+
}));
|
|
24
|
+
users.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
25
|
+
return { selfUserId: session.data.userId, users };
|
|
26
|
+
},
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Pushes
|
|
30
|
+
|
|
31
|
+
In **`onAttached`**, broadcast **`userJoined`**; in **`handleClose`**, broadcast **`userLeft`** so other clients update incrementally. Ordering relative to **`listPresence`** is not guaranteed across reconnects — clients should call **`listPresence`** (or equivalent) after connect and treat pushes as deltas.
|
|
32
|
+
|
|
33
|
+
## Client
|
|
34
|
+
|
|
35
|
+
After **`waitForOpen()`** or in **`onOpen`**, fetch **`listPresence`** once, then apply **`userJoined`** / **`userLeft`** from **`session.subscribe`** for live updates.
|
|
36
|
+
|
|
37
|
+
React: **`useSockaPresence`** — see **[Client](./client.md)**.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## See also
|
|
41
|
+
|
|
42
|
+
- **[Getting started](./getting-started.md)** — chat tutorial.
|
|
43
|
+
- **[Pushes](./pushes.md)** — **`broadcastPush`**, **`subscribe`**.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Contracts can declare **`pushes`** alongside **`calls`**. Each push name maps to a **Standard Schema** payload. The server validates payloads **before** sending; the client decodes and validates **before** your listeners run—so **`InferSockaPushPayload`** stays honest end to end.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Pushes use the same **`wireFormat`** as RPCs for that session (default JSON). **Details:** **[Reference](./reference.md#wire-encoding-json-and-msgpack)** · **[Internals](./internals.md)**.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
8
|
export const myContract = defineSocka({
|