@firtoz/socka 3.0.2 → 4.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 +15 -1
- package/dist/{SockaWebSocketSession-B1w7RAid.d.ts → SockaWebSocketSession-BaGvSerM.d.ts} +28 -10
- package/dist/bun/index.d.ts +6 -6
- package/dist/bun/index.js +2 -2
- package/dist/bun/index.js.map +1 -1
- package/dist/{chunk-5WQTYLIC.js → chunk-JR2GENNT.js} +2 -2
- package/dist/chunk-JR2GENNT.js.map +1 -0
- package/dist/{chunk-P3JEEOJL.js → chunk-THFUHQJ3.js} +2 -2
- package/dist/chunk-THFUHQJ3.js.map +1 -0
- package/dist/{chunk-LVVCHLNW.js → chunk-TTXY7O5P.js} +20 -4
- package/dist/chunk-TTXY7O5P.js.map +1 -0
- package/dist/client/index.d.ts +10 -10
- package/dist/client/index.js +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/do/index.d.ts +83 -18
- package/dist/do/index.js +62 -6
- package/dist/do/index.js.map +1 -1
- package/dist/hono/cloudflare-workers.d.ts +4 -4
- package/dist/hono/cloudflare-workers.js +2 -2
- package/dist/hono/cloudflare-workers.js.map +1 -1
- package/dist/hono/index.d.ts +4 -4
- package/dist/hono/index.js +2 -2
- package/dist/hono/index.js.map +1 -1
- package/dist/react/index.d.ts +13 -13
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.d.ts +8 -8
- package/dist/server/index.js +4 -4
- package/dist/server/index.js.map +1 -1
- package/dist/{socka-report-error-CXwpAUgl.d.ts → socka-report-error-nTXJIzNb.d.ts} +32 -12
- package/docs/README.md +2 -0
- package/docs/client.md +17 -0
- package/docs/collaborative-realtime.md +61 -0
- package/docs/durable-objects.md +84 -35
- package/docs/internals.md +1 -1
- package/docs/pushes.md +32 -2
- package/docs/react-durable-objects.md +96 -0
- package/docs/recipes.md +1 -1
- package/docs/reference.md +18 -5
- package/package.json +8 -8
- package/skills/socka/core-rpc/SKILL.md +1 -1
- package/skills/socka/do-session/SKILL.md +1 -1
- package/dist/chunk-5WQTYLIC.js.map +0 -1
- package/dist/chunk-LVVCHLNW.js.map +0 -1
- package/dist/chunk-P3JEEOJL.js.map +0 -1
package/docs/durable-objects.md
CHANGED
|
@@ -2,11 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
On Cloudflare **Durable Objects**, socka splits into two pieces:
|
|
4
4
|
|
|
5
|
-
1. **`SockaDoSession`** — one instance per connected **`WebSocket`**.
|
|
6
|
-
2. **`SockaWebSocketDO`** — subclasses **`BaseWebSocketDO`** from **`@firtoz/websocket-do`**.
|
|
5
|
+
1. **`SockaDoSession`** — one instance per connected **`WebSocket`**. Handlers, **`handleClose`**, and optional **`onAttached`** live in config; **`broadcastPush`** fans out over the shared **`sessions`** map.
|
|
6
|
+
2. **`SockaWebSocketDO`** — subclasses **`BaseWebSocketDO`** from **`@firtoz/websocket-do`**. You declare the **`contract`** once on the DO and implement **`buildSockaSessionConfig`**; the base wires HTTP → WebSocket upgrade → **`new SockaDoSession(websocket, host)`**.
|
|
7
7
|
|
|
8
8
|
You still define one **`defineSocka`** contract; this page is only about hosting it on a **Durable Object**.
|
|
9
9
|
|
|
10
|
+
## Recommended: `SockaWebSocketDO` (default session)
|
|
11
|
+
|
|
12
|
+
Most apps do **not** need a custom **`SockaDoSession`** subclass. Extend **`SockaWebSocketDO`**, set **`contract`**, and implement **`buildSockaSessionConfig`**. Add a constructor only when you need setup beyond **`super(ctx, env)`** (for example **`ctx.blockConcurrencyWhile`** for SQLite migrations).
|
|
13
|
+
|
|
14
|
+
Runnable example: **[`examples/chatroom-do/src/do.ts`](../../../examples/chatroom-do/src/do.ts)**.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import {
|
|
18
|
+
SockaWebSocketDO,
|
|
19
|
+
type SockaDoSessionConfigInput,
|
|
20
|
+
} from "@firtoz/socka/do";
|
|
21
|
+
import { myContract } from "./contract";
|
|
22
|
+
|
|
23
|
+
type SessionData = { userId: string; displayName: string };
|
|
24
|
+
|
|
25
|
+
export class MyDO extends SockaWebSocketDO<
|
|
26
|
+
typeof myContract,
|
|
27
|
+
SessionData,
|
|
28
|
+
Env
|
|
29
|
+
> {
|
|
30
|
+
protected readonly contract = myContract;
|
|
31
|
+
app = this.getBaseApp();
|
|
32
|
+
|
|
33
|
+
protected buildSockaSessionConfig(
|
|
34
|
+
ctx: Context<{ Bindings: Env }> | undefined,
|
|
35
|
+
): SockaDoSessionConfigInput<typeof myContract, SessionData, Env> {
|
|
36
|
+
return {
|
|
37
|
+
wireFormat: "json",
|
|
38
|
+
createData: (c) => ({ userId: crypto.randomUUID(), displayName: "…" }),
|
|
39
|
+
handlers: {
|
|
40
|
+
sendMessage: async (input, session) => {
|
|
41
|
+
await this.db.insert(…);
|
|
42
|
+
await session.broadcastPush("roomMessage", row);
|
|
43
|
+
return { ok: true as const };
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
handleClose: async (session) => { … },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// HTTP admin — no WebSocket session on this path
|
|
51
|
+
async deleteMessage(id: string) {
|
|
52
|
+
await this.db.delete(…);
|
|
53
|
+
await this.broadcastPushToAll("messageDeleted", { id });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**`buildSockaSessionConfig`** omits **`contract`** (the DO owns it). See **[Hibernation and `session.data`](#hibernation-and-sessiondata)** for when **`ctx`** and **`createData`** run (including after hibernate resume).
|
|
59
|
+
|
|
60
|
+
## Custom `SockaDoSession` subclass (optional)
|
|
61
|
+
|
|
62
|
+
Override **`createSockaSession`** on **`SockaWebSocketDOBase`** (four type parameters) when you need a session subtype. Or construct from the host:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
class MySession extends SockaDoSession<typeof myContract, SessionData, Env> {
|
|
66
|
+
constructor(ws: WebSocket, do: MyDO, ctx?: Context<{ Bindings: Env }>) {
|
|
67
|
+
super(ws, do, ctx);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The legacy **`(websocket, sessions, config)`** constructor remains for tests and non-DO tooling.
|
|
73
|
+
|
|
10
74
|
## Cloudflare Worker checklist
|
|
11
75
|
|
|
12
76
|
This is the **Cloudflare** side (bindings, Wrangler, generated types)—not socka-specific, but you need it before **`SockaWebSocketDO`** can run.
|
|
@@ -20,53 +84,38 @@ This is the **Cloudflare** side (bindings, Wrangler, generated types)—not sock
|
|
|
20
84
|
|
|
21
85
|
**`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
86
|
|
|
23
|
-
## `SockaDoSession`
|
|
87
|
+
## `SockaDoSession` (manual wiring)
|
|
24
88
|
|
|
25
|
-
|
|
26
|
-
import { SockaDoSession } from "@firtoz/socka/do";
|
|
27
|
-
import { myContract } from "./contract";
|
|
89
|
+
For tests or advanced cases, you can still construct a session directly:
|
|
28
90
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
list: async (session) => fetchMessages(),
|
|
34
|
-
insert: async (input, session) => saveMessage(input.message),
|
|
35
|
-
},
|
|
36
|
-
handleClose: async (session) => {
|
|
37
|
-
// e.g. remove session.websocket from your game / presence tables
|
|
38
|
-
},
|
|
39
|
-
});
|
|
91
|
+
```ts
|
|
92
|
+
new SockaDoSession(websocket, doHost, attachCtx);
|
|
93
|
+
// or
|
|
94
|
+
new SockaDoSession(websocket, sessions, { contract, handlers, handleClose, … });
|
|
40
95
|
```
|
|
41
96
|
|
|
42
97
|
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
98
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
## `SockaWebSocketDO` and routing
|
|
99
|
+
## Hibernation and `session.data`
|
|
47
100
|
|
|
48
|
-
|
|
101
|
+
**Fresh WebSocket upgrade** — **`buildSockaSessionConfig(ctx)`** runs with the Hono **`Context`**. **`createData`** runs once via **`BaseSession.startFresh(ctx)`** and can read **`ctx.req`** (query params, headers, etc.).
|
|
49
102
|
|
|
50
|
-
|
|
103
|
+
**Hibernation resume** — **`@firtoz/websocket-do`** calls **`createSession(undefined, websocket)`** then **`session.resume()`**, **not** **`startFresh`**. So:
|
|
51
104
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
app = this.getBaseApp();
|
|
105
|
+
- **`buildSockaSessionConfig(undefined)`** runs again to rebuild handlers (they may close over **`this`** — that is fine).
|
|
106
|
+
- **`createData` is not called on resume.** **`session.data`** is restored from the WebSocket attachment. Do not assume **`ctx`** exists inside **`createData`** on resume — it will not run.
|
|
55
107
|
|
|
56
|
-
|
|
57
|
-
super(ctx, env, {
|
|
58
|
-
createSockaSession: (_ctx, websocket) =>
|
|
59
|
-
new MySession(websocket, this.sessions /*, … */),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
```
|
|
108
|
+
If you mutate **`session.data`** after connect, call **`await session.update()`** so the attachment is rewritten before hibernate. If you skip **`update`**, resume can observe stale **`session.data`**. For large or authoritative state, keep a **stable id** in **`session.data`** and use **D1 / KV / SQLite** as the source of truth—the attachment is for small, session-scoped working state.
|
|
64
109
|
|
|
65
110
|
**One Durable Object instance per room** is a common pattern: derive the DO id from your room key so each instance has its own **`sessions`** map—see **[Multi-room](./multi-room.md)**.
|
|
66
111
|
|
|
67
|
-
##
|
|
112
|
+
## Pushes from HTTP / non-WebSocket handlers
|
|
113
|
+
|
|
114
|
+
Many DO apps expose **Hono HTTP routes** on **`app`** (admin moderation, internal APIs, alarms) in addition to **`/websocket`**. After mutating storage from those handlers, you often need a contract-typed push to **every** connected client — with **no** originating WebSocket session.
|
|
115
|
+
|
|
116
|
+
Use **`await this.broadcastPushToAll("messageDeleted", { id })`** on your DO subclass. The DO **`contract`** is the single source of truth. See **[Pushes — Pushes from HTTP / non-WebSocket handlers](./pushes.md#pushes-from-http--non-websocket-handlers)**.
|
|
68
117
|
|
|
69
|
-
|
|
118
|
+
Without a DO subclass, use **`broadcastContractPushToAll(this.sessions, contract, name, body)`** from **`@firtoz/socka/server`**.
|
|
70
119
|
|
|
71
120
|
## See also
|
|
72
121
|
|
package/docs/internals.md
CHANGED
|
@@ -45,7 +45,7 @@ Clients generate **`id`** strings per request; servers echo them on **`serverRes
|
|
|
45
45
|
|
|
46
46
|
## TypeScript: `SockaWebSocketDO` and contract erasure
|
|
47
47
|
|
|
48
|
-
`@firtoz/socka/do`
|
|
48
|
+
`@firtoz/socka/do` uses **`any` for the contract type parameter on `SockaWebSocketDO`’s session generic** because **`SockaDoSession` is invariant in `TContract`**: `SockaDoSession<typeof myContract, …>` does not extend `SockaDoSession<SockaContractBound, …>` at the class level even when `typeof myContract` satisfies **`SockaContractBound`**. Session *values* and APIs that take **`TContract extends SockaContractBound`** (not a `SockaDoSession` subclass) should use **`typeof myContract`** from the module where you called **`defineSocka`**—that bound is wide enough for contracts **with** server pushes; the old `extends SockaContract<SockaContractConfig>` shape was not.
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
package/docs/pushes.md
CHANGED
|
@@ -13,12 +13,42 @@ export const myContract = defineSocka({
|
|
|
13
13
|
});
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## Typing `pushHandlers`
|
|
17
|
+
|
|
18
|
+
On **`SockaSession`** and **`useSockaSession`**, the **`pushHandlers`** option is **`Partial<InferSockaPushHandlers<typeof myContract>>`**. You can use **`satisfies`** to check an object literal without changing its inferred payload types (payloads for each key still narrow from the contract):
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import type { InferSockaPushHandlers } from "@firtoz/socka/core";
|
|
22
|
+
import { myContract } from "./contract";
|
|
23
|
+
|
|
24
|
+
const pushHandlers = {
|
|
25
|
+
itemsChanged: (payload) => {
|
|
26
|
+
// `payload` is typed from the contract
|
|
27
|
+
},
|
|
28
|
+
} satisfies Partial<InferSockaPushHandlers<typeof myContract>>;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Types are exported from **`@firtoz/socka/core`** (same as **`@firtoz/socka`**) — see **[Reference — Type inference](./reference.md#type-inference)**.
|
|
32
|
+
|
|
16
33
|
## Server: emit and broadcast
|
|
17
34
|
|
|
18
35
|
- **`await session.emitPush("itemsChanged", payload)`** — send one **validated** push to **this** socket (typical for private notifications).
|
|
19
36
|
- **`await session.broadcastPush("itemsChanged", payload, excludeSelf?)`** — send to **every session in the same **`sessions`** map**, optionally skipping the caller.
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
### Pushes from HTTP / non-WebSocket handlers
|
|
39
|
+
|
|
40
|
+
When the origin is **not** a connected client (admin HTTP routes on the DO Hono `app`, alarms, cron, service bindings), there is no WebSocket session to call **`broadcastPush`** on.
|
|
41
|
+
|
|
42
|
+
- **`await this.broadcastPushToAll("itemsChanged", payload)`** on **`SockaWebSocketDO`** — validates against the DO **`contract`** and fans out to **every** session in **`this.sessions`**. No **`excludeSelf`**, no anchor session. No-op when the room is empty.
|
|
43
|
+
- **`await broadcastContractPushToAll(sessions, contract, name, body)`** from **`@firtoz/socka/server`** — same semantics when you have the shared **`sessions`** map and contract but are not inside a **`SockaWebSocketDO`** subclass.
|
|
44
|
+
|
|
45
|
+
Pass **`contract`** on the DO (`protected readonly contract = myContract`) so **`broadcastPushToAll`** stays typed and validated — see **[Durable Objects](./durable-objects.md)**.
|
|
46
|
+
|
|
47
|
+
**Do not** loop over **`sessions`** and call **`broadcastPush`** on each session — **`broadcastPush`** already iterates the whole map once. A loop would multiply traffic if every iteration ran a full fan-out.
|
|
48
|
+
|
|
49
|
+
**Do not** pick an arbitrary session (for example **`sessions.values().next().value`**) as an anchor for room-wide pushes — that reads like a bug and is easy to misuse with **`excludeSelf: true`**.
|
|
50
|
+
|
|
51
|
+
Lower-level helpers (for example **`broadcastSockaEventToPeers`** / **`broadcastSockaEventToAll`** from **`@firtoz/socka/server`**) exist for advanced cases; prefer **`broadcastPushToAll`** or **`broadcastContractPushToAll`** so schemas stay centralized.
|
|
22
52
|
|
|
23
53
|
**Ordering** — Delivery order is per connection; there is no cross-client guarantee beyond your own handler ordering. For causal ordering across clients, include a **version** or **timestamp** in the payload.
|
|
24
54
|
|
|
@@ -36,7 +66,7 @@ const payload = await session.subscribe.waitForPush("itemsChanged", {
|
|
|
36
66
|
});
|
|
37
67
|
```
|
|
38
68
|
|
|
39
|
-
Use **`InferSockaPushPayload<typeof myContract, "itemsChanged">`** (from **`@firtoz/socka/core`**) when typing callbacks or reducers. If the server never emits a push your client subscribed to, **`waitForPush`** can time out—handle **`AbortSignal`** and UI loading states.
|
|
69
|
+
Use **`InferSockaPushPayload<typeof myContract, "itemsChanged">`** (from **`@firtoz/socka/core`**) when typing callbacks or reducers one-off. For tables of handlers, prefer **`InferSockaPushHandlers`** (see [Typing `pushHandlers`](#typing-pushhandlers) above). If the server never emits a push your client subscribed to, **`waitForPush`** can time out—handle **`AbortSignal`** and UI loading states.
|
|
40
70
|
|
|
41
71
|
## Who receives `broadcastPush`?
|
|
42
72
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# React + Cloudflare Durable Objects
|
|
2
|
+
|
|
3
|
+
This page is a **minimal wiring guide** for the common stack: **one shared `defineSocka` contract**, a **Durable Object** extending **`SockaWebSocketDO`**, and a **React** client using **`useSockaSession`**. A full app with routing and persistence is **[chatroom-do](../../examples/chatroom-do)** in this repo.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
You need **`@firtoz/socka`**, **`@firtoz/socka/do`**, **`@firtoz/socka/react`**, and peer deps for Cloudflare and **`@firtoz/websocket-do`**—see **[Peers](./peers.md)** and **[Durable Objects](./durable-objects.md)** (Wrangler, `Env` types, **do not** hand-edit generated worker env `.d.ts` files).
|
|
8
|
+
|
|
9
|
+
## Shared contract (no client casts)
|
|
10
|
+
|
|
11
|
+
Export **one** contract from a shared module and import the **same reference** in the worker and the browser. You should **not** need `as never`, `as unknown`, manual `send` types, or `InferSockaSend` in normal app code—if TypeScript widens or fails to infer, align **Zod (or other Standard Schema) output** with any hand-written types (see **[TypeScript and exact optional properties](./reference.md#typescript-and-exact-optional-properties)**) and re-export **types** from the schema with **`z.infer`** when possible.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// shared/contract.ts
|
|
15
|
+
import { defineSocka } from "@firtoz/socka/core";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
export const roomContract = defineSocka({
|
|
19
|
+
calls: {
|
|
20
|
+
list: {
|
|
21
|
+
input: z.object({}).optional(),
|
|
22
|
+
output: z.object({ items: z.array(z.string()) }),
|
|
23
|
+
},
|
|
24
|
+
sendCursor: {
|
|
25
|
+
input: z.object({ x: z.number(), y: z.number() }),
|
|
26
|
+
// no `output` — fire-and-forget; see README “Call `output` shapes”
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
pushes: {
|
|
30
|
+
cursorBatch: z.object({
|
|
31
|
+
cursors: z.array(z.object({ x: z.number(), y: z.number() })),
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Output shapes** — For high-frequency messages (cursors, live drafts), **omit** `output`. Use **`output: z.void()`** when the client should **await** a server ack. See **[Client — Fire-and-forget](./client.md#fire-and-forget)** and **[Reference — Optional output](./reference.md#optional-output-fire-and-forget)**.
|
|
38
|
+
|
|
39
|
+
## Durable Object
|
|
40
|
+
|
|
41
|
+
Subclass **`SockaWebSocketDO`**. Declare **`protected readonly contract`** and implement **`buildSockaSessionConfig`**. Full patterns: **[Durable Objects](./durable-objects.md)**, example **[`examples/chatroom-do/src/do.ts`](../../examples/chatroom-do/src/do.ts)**.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// do.ts (sketch)
|
|
45
|
+
import {
|
|
46
|
+
SockaWebSocketDO,
|
|
47
|
+
type SockaDoSessionConfigInput,
|
|
48
|
+
} from "@firtoz/socka/do";
|
|
49
|
+
import { roomContract } from "./contract";
|
|
50
|
+
|
|
51
|
+
type SessionData = { userId: string };
|
|
52
|
+
|
|
53
|
+
export class RoomDo extends SockaWebSocketDO<
|
|
54
|
+
typeof roomContract,
|
|
55
|
+
SessionData,
|
|
56
|
+
Env
|
|
57
|
+
> {
|
|
58
|
+
protected readonly contract = roomContract;
|
|
59
|
+
|
|
60
|
+
protected buildSockaSessionConfig(
|
|
61
|
+
ctx: Context<{ Bindings: Env }> | undefined,
|
|
62
|
+
): SockaDoSessionConfigInput<typeof roomContract, SessionData, Env> {
|
|
63
|
+
return { handlers: { … }, handleClose: async () => {} };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## React client
|
|
69
|
+
|
|
70
|
+
Use **`useSockaSession(contract, { url, pushHandlers? }, deps)`** with the **same** `roomContract` import. For **`pushHandlers`**, you can use **`satisfies Partial<InferSockaPushHandlers<typeof roomContract>>`**—see **[Pushes — Typing `pushHandlers`](./pushes.md#typing-pushhandlers)**.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
// RoomClient.tsx (sketch; URL must exist before connecting — see Client “SSR and WebSocket URLs”)
|
|
74
|
+
import { useSockaSession } from "@firtoz/socka/react";
|
|
75
|
+
import { roomContract } from "./contract";
|
|
76
|
+
|
|
77
|
+
function RoomClient({ url }: { url: string }) {
|
|
78
|
+
const { ready, send } = useSockaSession(
|
|
79
|
+
roomContract,
|
|
80
|
+
{ url, pushHandlers: { cursorBatch: (p) => { /* set state */ } } },
|
|
81
|
+
[url],
|
|
82
|
+
);
|
|
83
|
+
// `await send.list({})` — request/response
|
|
84
|
+
// `void send.sendCursor({ x, y })` — fire-and-forget; see observability in Client + Reference
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Whiteboard-style contracts (ops + draft + cursors) are sketched in **[Collaborative realtime](./collaborative-realtime.md)**.
|
|
90
|
+
|
|
91
|
+
## See also
|
|
92
|
+
|
|
93
|
+
- **[Client](./client.md)** — `useSockaSession`, **SSR and WebSocket URLs**, fire-and-forget **observability**
|
|
94
|
+
- **[Reference](./reference.md)** — `InferSocka*` types, `reportError`, `SockaReportError` kinds
|
|
95
|
+
- **[Durable Objects](./durable-objects.md)** — hibernation, `session.update()`, `createData`
|
|
96
|
+
- **[chatroom-do](../../examples/chatroom-do)** — SQLite + Drizzle + UI
|
package/docs/recipes.md
CHANGED
|
@@ -63,7 +63,7 @@ app.get("/ws/:roomId", upgradeWebSocket((c) => {
|
|
|
63
63
|
|
|
64
64
|
## Durable Objects
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
- **`SockaWebSocketDO`**: extend with **`contract`** + **`buildSockaSessionConfig`** — see **[Durable Objects](./durable-objects.md)** and **[chatroom-do](../../examples/chatroom-do/src/do.ts)**.
|
|
67
67
|
|
|
68
68
|
## Client (browser)
|
|
69
69
|
|
package/docs/reference.md
CHANGED
|
@@ -5,20 +5,33 @@ User-facing **API** and **configuration**. For **wire protocol details** (frame
|
|
|
5
5
|
## Type inference
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
InferSockaSend,
|
|
10
|
+
InferSockaHandlers,
|
|
11
|
+
InferSockaPushHandlers,
|
|
12
|
+
InferSockaPushPayload,
|
|
13
|
+
} from "@firtoz/socka/core";
|
|
9
14
|
|
|
10
15
|
type Send = InferSockaSend<typeof myContract>;
|
|
11
16
|
type Handlers = InferSockaHandlers<
|
|
12
17
|
typeof myContract,
|
|
13
18
|
SockaWebSocketSession<typeof myContract>
|
|
14
19
|
>;
|
|
20
|
+
type PushHandlers = Partial<InferSockaPushHandlers<typeof myContract>>;
|
|
21
|
+
type RoomMessagePayload = InferSockaPushPayload<typeof myContract, "roomMessage">;
|
|
15
22
|
```
|
|
16
23
|
|
|
17
|
-
**`InferSockaSend`** — Call names become methods on **`session.send`**; inputs/outputs follow the contract. **`InferSockaHandlers`** —
|
|
24
|
+
**`InferSockaSend`** — Call names become methods on **`session.send`**; inputs/outputs follow the contract. **`InferSockaHandlers`** — server handler arity matches **`calls`** (with or without `input`). **`InferSockaPushPayload`** — payload type for a given push name. Use **`Partial<InferSockaPushHandlers<typeof myContract>>`** (or **`satisfies Partial<...>`** on an object literal) to type **`pushHandlers`** on **`SockaSession`** / **`useSockaSession`** — see **[Pushes — Typing `pushHandlers`](./pushes.md#typing-pushhandlers)**.
|
|
25
|
+
|
|
26
|
+
### TypeScript and exact optional properties
|
|
27
|
+
|
|
28
|
+
If you define **hand-written** types next to Zod (or other Standard Schema) objects with **`z.optional()`** (or **`.optional()`** on a field) and you enable TypeScript’s **`exactOptionalPropertyTypes`**, a property that is “optional” in the schema is often **inferred** as **`name?: T | undefined`**, not **`name?: T`**. A plain **`pressure?: number`** in your `type` can therefore **widen** differently from `z.infer<typeof pointSchema>`, and **contract** inference (or assignment to a handler parameter) can fail.
|
|
29
|
+
|
|
30
|
+
**Remedies:** prefer **`z.infer<typeof pointSchema>`** (or the inferred output type of your field) for the type; or in hand-written types, include **`| undefined`**, e.g. **`pressure?: number | undefined`**, so they match the schema end to end.
|
|
18
31
|
|
|
19
32
|
### Optional output (fire-and-forget)
|
|
20
33
|
|
|
21
|
-
If a call omits **`output`**, the server sends **no** **`serverResponse`** on success, and the client **`send`** method returns **`Promise<void>`** that resolves after the request is queued to the socket (not after the server runs the handler). **`output: z.void()`** keeps full request/response: the server still sends **`serverResponse`** and the client **`await`** waits for it. For output-less calls, server **`serverError`** frames include an optional **`rpc`** field so **`reportError`** can attribute failures when there is no pending promise.
|
|
34
|
+
If a call omits **`output`**, the server sends **no** **`serverResponse`** on success, and the client **`send`** method returns **`Promise<void>`** that resolves after the request is queued to the socket (not after the server runs the handler). **`output: z.void()`** keeps full request/response: the server still sends **`serverResponse`** and the client **`await`** waits for it. For output-less calls, server **`serverError`** frames include an optional **`rpc`** field so **`reportError`** can attribute failures when there is no pending promise. For **client** observability when you cannot rely on **`.catch`** on **`send`**, see **[Client — Fire-and-forget observability](./client.md#fire-and-forget-observability)**.
|
|
22
35
|
|
|
23
36
|
## Errors and observability
|
|
24
37
|
|
|
@@ -111,8 +124,8 @@ Anything that implements **Standard Schema v1** works — **Zod**, **Valibot**,
|
|
|
111
124
|
| `@firtoz/socka/client` | `SockaSession`, `SockaWebSocketClient` (also re-exports `SockaReportError`, `reportSockaError`) |
|
|
112
125
|
| `@firtoz/socka/test` | `createFakeWebSocket` for unit tests — see **[Testing](./testing.md)** |
|
|
113
126
|
| `@firtoz/socka/react` | `useSocka`, `useSockaSession`, `useSockaPresence`, provider + context |
|
|
114
|
-
| `@firtoz/socka/do` | `SockaDoSession`, `SockaWebSocketDO` |
|
|
115
|
-
| `@firtoz/socka/server` | `SockaWebSocketSession`, `attachSockaWebSocket`, `dispatchSockaInboundMessage`, `broadcastSockaEventToPeers` |
|
|
127
|
+
| `@firtoz/socka/do` | `SockaDoSession`, `SockaDoHost`, `SockaWebSocketDO`, `SockaWebSocketDOBase`, `SockaDoSessionConfigInput`, `broadcastPushToAll` (on DO) |
|
|
128
|
+
| `@firtoz/socka/server` | `SockaWebSocketSession`, `attachSockaWebSocket`, `dispatchSockaInboundMessage`, `broadcastContractPushToAll`, `broadcastSockaEventToAll`, `broadcastSockaEventToPeers` |
|
|
116
129
|
| `@firtoz/socka/bun` | `createSockaBunWebSocketHandlers` for **`Bun.serve`** |
|
|
117
130
|
| `@firtoz/socka/hono` | `sockaHonoNodeWs` for **`@hono/node-ws`** |
|
|
118
131
|
| `@firtoz/socka/hono/cloudflare` | `sockaHonoCloudflare` for **`hono/cloudflare-workers`** |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/socka",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Standard Schema–first WebSocket RPC for TypeScript — Bun, Hono, Node ws, Cloudflare Workers, Durable Objects",
|
|
6
6
|
"main": "./dist/core/index.js",
|
|
@@ -98,10 +98,10 @@
|
|
|
98
98
|
"dependencies": {
|
|
99
99
|
"@firtoz/maybe-error": "^1.6.1",
|
|
100
100
|
"@standard-schema/spec": "^1.1.0",
|
|
101
|
-
"msgpackr": "^
|
|
101
|
+
"msgpackr": "^2.0.1"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
|
-
"@cloudflare/workers-types": "^4.
|
|
104
|
+
"@cloudflare/workers-types": "^4.20260503.1",
|
|
105
105
|
"@firtoz/websocket-do": "^13.0.2",
|
|
106
106
|
"@hono/node-server": "^1.19.2",
|
|
107
107
|
"@hono/node-ws": "^1.3.0",
|
|
@@ -133,11 +133,11 @@
|
|
|
133
133
|
}
|
|
134
134
|
},
|
|
135
135
|
"devDependencies": {
|
|
136
|
-
"@cloudflare/workers-types": "^4.
|
|
136
|
+
"@cloudflare/workers-types": "^4.20260503.1",
|
|
137
137
|
"@happy-dom/global-registrator": "^20.9.0",
|
|
138
|
-
"@hono/node-server": "^2.0.
|
|
139
|
-
"@hono/node-ws": "^1.3.
|
|
140
|
-
"@tanstack/intent": "^0.0.
|
|
138
|
+
"@hono/node-server": "^2.0.1",
|
|
139
|
+
"@hono/node-ws": "^1.3.1",
|
|
140
|
+
"@tanstack/intent": "^0.0.39",
|
|
141
141
|
"@testing-library/react": "^16.3.2",
|
|
142
142
|
"@types/react": "^19.2.14",
|
|
143
143
|
"@types/ws": "^8.18.1",
|
|
@@ -147,6 +147,6 @@
|
|
|
147
147
|
"tsup": "^8.5.1",
|
|
148
148
|
"typescript": "^6.0.3",
|
|
149
149
|
"valibot": "^1.3.1",
|
|
150
|
-
"zod": "^4.
|
|
150
|
+
"zod": "^4.4.2"
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -29,7 +29,7 @@ description: Standard Schema socka contracts (defineSocka), v1 wire envelopes, S
|
|
|
29
29
|
|
|
30
30
|
## Durable Objects
|
|
31
31
|
|
|
32
|
-
- **`SockaDoSession`** / **`SockaWebSocketDO`** in **`@firtoz/socka/do`**—see **`@firtoz/socka/do-session`** skill.
|
|
32
|
+
- **`SockaDoSession`** / **`SockaWebSocketDO`** in **`@firtoz/socka/do`**—see **`@firtoz/socka/do-session`** skill. Human-oriented wiring for **React + DO**: [React + Durable Objects](../../../docs/react-durable-objects.md); **canvas / whiteboard** contract sketch: [Collaborative realtime](../../../docs/collaborative-realtime.md).
|
|
33
33
|
|
|
34
34
|
## Low-level
|
|
35
35
|
|
|
@@ -8,7 +8,7 @@ description: SockaDoSession and SockaWebSocketDO on Cloudflare Durable Objects
|
|
|
8
8
|
## Components
|
|
9
9
|
|
|
10
10
|
- **`SockaDoSession`** (**`@firtoz/socka/do`**): extends **`BaseSession`** from **`@firtoz/websocket-do`**. Incoming messages are decoded with **`decodeSockaWire`** after JSON parse (text) or **`parseWirePayload`** (msgpack). Valid **`clientRequest`** frames are dispatched to **`handlers`** (typed **`InferSockaHandlers<typeof contract>`**). Calls **with** **`output`** get **`encodeServerResponse`** on success; calls **without** **`output`** send **`encodeServerError`** only on failure; optional **`encodeServerEvent`** for contract pushes.
|
|
11
|
-
- **`SockaWebSocketDO`**:
|
|
11
|
+
- **`SockaWebSocketDO`**: extend with **`protected readonly contract`**, **`buildSockaSessionConfig(ctx)`** (constructor only when you need extra setup, e.g. DB migrate). Default session wiring is built-in; use **`SockaWebSocketDOBase`** only for a custom **`SockaDoSession`** subclass.
|
|
12
12
|
|
|
13
13
|
## Session config (`SockaDoSessionConfig`)
|
|
14
14
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/dispatchSockaInboundMessage.ts"],"names":[],"mappings":";AAUA,eAAsB,2BAAA,CAIrB,OAAA,EACA,UAAA,EACA,IAAA,EACgB;AAChB,EAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC7B,IAAA,MAAM,OAAA,CAAQ,iBAAiB,IAAI,CAAA;AACnC,IAAA;AAAA,EACD;AACA,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,MAAA,CAAO,QAAA,CAAS,IAAI,CAAA,EAAG;AAC3D,IAAA,IAAI,eAAe,MAAA,EAAQ;AAC1B,MAAA,MAAM,OAAA,CAAQ,gBAAA,CAAiB,IAAA,CAAK,QAAA,CAAS,MAAM,CAAC,CAAA;AACpD,MAAA;AAAA,IACD;AACA,IAAA,MAAM,QAAQ,mBAAA,CAAoB,IAAI,UAAA,CAAW,IAAI,EAAE,MAAM,CAAA;AAC7D,IAAA;AAAA,EACD;AACA,EAAA,IAAI,gBAAgB,WAAA,EAAa;AAChC,IAAA,IAAI,eAAe,MAAA,EAAQ;AAC1B,MAAA,MAAM,QAAQ,gBAAA,CAAiB,IAAI,aAAY,CAAE,MAAA,CAAO,IAAI,CAAC,CAAA;AAC7D,MAAA;AAAA,IACD;AACA,IAAA,MAAM,OAAA,CAAQ,oBAAoB,IAAI,CAAA;AACtC,IAAA;AAAA,EACD;AACA,EAAA,IAAI,gBAAgB,IAAA,EAAM;AACzB,IAAA,IAAI,eAAe,MAAA,EAAQ;AAC1B,MAAA,MAAM,OAAA,CAAQ,gBAAA,CAAiB,MAAM,IAAA,CAAK,MAAM,CAAA;AAAA,IACjD,CAAA,MAAO;AACN,MAAA,MAAM,OAAA,CAAQ,mBAAA,CAAoB,MAAM,IAAA,CAAK,aAAa,CAAA;AAAA,IAC3D;AACA,IAAA;AAAA,EACD;AACA,EAAA,IAAI,WAAA,CAAY,MAAA,CAAO,IAAI,CAAA,EAAG;AAC7B,IAAA,MAAM,CAAA,GAAI,IAAA;AACV,IAAA,MAAM,IAAA,GAAO,IAAI,UAAA,CAAW,CAAA,CAAE,QAAQ,CAAA,CAAE,UAAA,EAAY,EAAE,UAAU,CAAA;AAChE,IAAA,IAAI,eAAe,MAAA,EAAQ;AAC1B,MAAA,MAAM,QAAQ,gBAAA,CAAiB,IAAI,aAAY,CAAE,MAAA,CAAO,IAAI,CAAC,CAAA;AAC7D,MAAA;AAAA,IACD;AACA,IAAA,MAAM,IAAA,GAAO,IAAI,UAAA,CAAW,IAAA,CAAK,MAAM,CAAA;AACvC,IAAA,IAAA,CAAK,IAAI,IAAI,CAAA;AACb,IAAA,MAAM,OAAA,CAAQ,mBAAA,CAAoB,IAAA,CAAK,MAAM,CAAA;AAAA,EAC9C;AACD","file":"chunk-5WQTYLIC.js","sourcesContent":["import type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport type { SockaWireFormat } from \"../core/wire-codec\";\nimport type { SockaWebSocketSession } from \"./SockaWebSocketSession\";\n\n/**\n * Decode a WebSocket `message` payload and dispatch it to the session (same\n * behavior as the `message` handler installed by {@link attachSockaWebSocket}).\n * Use this when the runtime does not support `addEventListener` on the socket\n * (e.g. Bun {@link ServerWebSocket}) or when handling messages manually.\n */\nexport async function dispatchSockaInboundMessage<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tsession: SockaWebSocketSession<TContract, TData>,\n\twireFormat: SockaWireFormat,\n\tdata: MessageEvent[\"data\"],\n): Promise<void> {\n\tif (typeof data === \"string\") {\n\t\tawait session.handleRawMessage(data);\n\t\treturn;\n\t}\n\tif (typeof Buffer !== \"undefined\" && Buffer.isBuffer(data)) {\n\t\tif (wireFormat === \"json\") {\n\t\t\tawait session.handleRawMessage(data.toString(\"utf8\"));\n\t\t\treturn;\n\t\t}\n\t\tawait session.handleBinaryMessage(new Uint8Array(data).buffer);\n\t\treturn;\n\t}\n\tif (data instanceof ArrayBuffer) {\n\t\tif (wireFormat === \"json\") {\n\t\t\tawait session.handleRawMessage(new TextDecoder().decode(data));\n\t\t\treturn;\n\t\t}\n\t\tawait session.handleBinaryMessage(data);\n\t\treturn;\n\t}\n\tif (data instanceof Blob) {\n\t\tif (wireFormat === \"json\") {\n\t\t\tawait session.handleRawMessage(await data.text());\n\t\t} else {\n\t\t\tawait session.handleBinaryMessage(await data.arrayBuffer());\n\t\t}\n\t\treturn;\n\t}\n\tif (ArrayBuffer.isView(data)) {\n\t\tconst v = data;\n\t\tconst view = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);\n\t\tif (wireFormat === \"json\") {\n\t\t\tawait session.handleRawMessage(new TextDecoder().decode(view));\n\t\t\treturn;\n\t\t}\n\t\tconst copy = new Uint8Array(view.length);\n\t\tcopy.set(view);\n\t\tawait session.handleBinaryMessage(copy.buffer);\n\t}\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/SockaWebSocketSession.ts"],"names":[],"mappings":"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","file":"chunk-LVVCHLNW.js","sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { exhaustiveGuard } from \"@firtoz/maybe-error\";\nimport type {\n\tInferSockaPushPayload,\n\tSockaContract,\n\tSockaContractConfig,\n} from \"../core/contract\";\nimport {\n\tSockaWireError,\n\tdecodeSockaWire,\n\tencodeServerResponse,\n\tencodeServerError,\n\tencodeServerEvent,\n\ttype SockaClientRequestFrame,\n\ttype SockaWireFrame,\n} from \"../core/envelope\";\nimport {\n\tencodeSockaWire,\n\tparseWirePayload,\n\ttype SockaWireFormat,\n} from \"../core/wire-codec\";\nimport { reportSockaError } from \"../core/socka-report-error\";\nimport { parseStandardSchema } from \"../core/validate\";\nimport { SockaError } from \"../core/socka-error\";\nimport type {\n\tSockaStrictWebSocketInit,\n\tSockaWebSocketInit,\n\tSockaWebSocketSessionConfig,\n\tSockaWebSocketSessionConfigLoose,\n\tSockaWebSocketSessionConfigUnion,\n} from \"./SockaWebSocketSessionConfig\";\n\n/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */\ntype EmptySockaSessionData = Record<string, never>;\n\nexport type {\n\tSockaStrictWebSocketInit,\n\tSockaWebSocketInit,\n\tSockaWebSocketSessionConfig,\n\tSockaWebSocketSessionConfigLoose,\n\tSockaWebSocketSessionConfigUnion,\n};\n\nfunction isLooseUpgradeConfig<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfig: SockaWebSocketSessionConfigUnion<TContract, TData>,\n): config is SockaWebSocketSessionConfigLoose<TContract, TData> {\n\treturn (\n\t\t\"strictUpgradeRequest\" in config && config.strictUpgradeRequest === false\n\t);\n}\n\n/** Session that can send a wire-level server event (already validated). */\nexport type SockaEmitCapable = {\n\temitWireEvent(event: string, body: unknown): void;\n};\n\n/**\n * Contract-typed session surface for handlers that push to clients.\n */\nexport interface SockaPushSession<\n\tTContract extends SockaContract<SockaContractConfig>,\n> {\n\temitPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t): Promise<void>;\n\tbroadcastPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t\texcludeSelf?: boolean,\n\t): Promise<void>;\n}\n\n/**\n * Broadcast a socka server event to every session in the map (optionally\n * excluding the caller). Payload must already be contract-validated.\n *\n * Exclusion uses the **WebSocket** identity (`self.websocket`), not the session\n * object reference, so the same `sessions` map can hold `SockaDoSession` while\n * `broadcastPush` runs on `this.socka` (inner {@link SockaWebSocketSession}).\n */\nexport function broadcastSockaEventToPeers(\n\tsessions: Map<WebSocket, SockaEmitCapable>,\n\tself: SockaEmitCapable & { readonly websocket: WebSocket },\n\tevent: string,\n\tbody: unknown,\n\texcludeSelf = false,\n): void {\n\tfor (const [ws, session] of sessions) {\n\t\tif (excludeSelf && ws === self.websocket) continue;\n\t\tsession.emitWireEvent(event, body);\n\t}\n}\n\n/**\n * Runtime-agnostic socka server session: standard {@link WebSocket} wire\n * dispatch without Cloudflare Durable Object APIs.\n */\nexport class SockaWebSocketSession<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData = EmptySockaSessionData,\n> implements SockaPushSession<TContract>\n{\n\tprivate readonly config: SockaWebSocketSessionConfigUnion<TContract, TData>;\n\tprivate readonly wireFormat: SockaWireFormat;\n\tprivate _data!: TData;\n\n\tpublic constructor(\n\t\tpublic readonly websocket: WebSocket,\n\t\tprotected readonly sessions: Map<\n\t\t\tWebSocket,\n\t\t\tSockaWebSocketSession<TContract, TData>\n\t\t>,\n\t\tconfig: SockaWebSocketSessionConfigUnion<TContract, TData>,\n\t\tinit?: SockaWebSocketInit,\n\t) {\n\t\tthis.config = config;\n\t\tthis.wireFormat = config.wireFormat ?? \"json\";\n\t\tif (isLooseUpgradeConfig(config)) {\n\t\t\tconst createData = config.createData as\n\t\t\t\t| ((init: SockaWebSocketInit) => TData)\n\t\t\t\t| undefined;\n\t\t\tconst create = createData ?? ((_i: SockaWebSocketInit) => ({}) as TData);\n\t\t\tthis._data = create(init ?? {});\n\t\t} else {\n\t\t\tif (!init?.request) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"socka: strict upgrade (default) requires a Request on the upgrade init (e.g. Bun upgrade with `data: { …, request: req }`, or Hono default sockaInit), or use SockaWebSocketSessionConfigLoose with strictUpgradeRequest: false\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst strictInit: SockaStrictWebSocketInit = { request: init.request };\n\t\t\tif (config.createData) {\n\t\t\t\tthis._data = config.createData(strictInit);\n\t\t\t} else {\n\t\t\t\tthis._data = {} as TData;\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic get data(): TData {\n\t\treturn this._data;\n\t}\n\n\t/**\n\t * Session data for every connection in the same {@link sessions} map (same room),\n\t * optionally excluding this socket.\n\t */\n\tpublic listPeers(options?: { excludeSelf?: boolean }): TData[] {\n\t\tconst out: TData[] = [];\n\t\tfor (const [ws, s] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tout.push(s.data);\n\t\t}\n\t\treturn out;\n\t}\n\n\t/**\n\t * Like {@link listPeers} but maps each peer {@link SockaWebSocketSession}\n\t * (e.g. when you need more than {@link #data}).\n\t */\n\tpublic listPeersWith<R>(\n\t\tmap: (session: SockaWebSocketSession<TContract, TData>) => R,\n\t\toptions?: { excludeSelf?: boolean },\n\t): R[] {\n\t\tconst out: R[] = [];\n\t\tfor (const [ws, s] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tout.push(map(s));\n\t\t}\n\t\treturn out;\n\t}\n\n\t/** Count of sessions in this room (same {@link sessions} map), optionally excluding self. */\n\tpublic peerCount(options?: { excludeSelf?: boolean }): number {\n\t\tlet n = 0;\n\t\tfor (const [ws] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tn += 1;\n\t\t}\n\t\treturn n;\n\t}\n\n\t/** Whether any peer sessions exist (optionally excluding self). */\n\tpublic hasPeers(options?: { excludeSelf?: boolean }): boolean {\n\t\treturn this.peerCount(options) > 0;\n\t}\n\n\t/**\n\t * Invokes the user {@link typeof SockaWebSocketSessionConfig.handleClose} callback.\n\t * Server adapters should call this when the WebSocket closes, **before** deleting\n\t * this session from the shared `sessions` map.\n\t */\n\tpublic async invokeHandleClose(): Promise<void> {\n\t\tawait this.config.handleClose(this);\n\t}\n\n\tpublic async handleRawMessage(rawMessage: string): Promise<void> {\n\t\tif (this.wireFormat !== \"json\") {\n\t\t\tawait this.reportValidationError(\n\t\t\t\tnew Error(\"socka: unexpected JSON frame in msgpack mode\"),\n\t\t\t\trawMessage,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst deserialize = this.config.deserializeJson ?? JSON.parse;\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = deserialize(rawMessage);\n\t\t} catch {\n\t\t\tawait this.reportValidationError(\n\t\t\t\tnew Error(\"socka: invalid JSON\"),\n\t\t\t\trawMessage,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tawait this.dispatchAfterParsed(parsed, rawMessage);\n\t}\n\n\tpublic async handleBinaryMessage(buffer: ArrayBuffer): Promise<void> {\n\t\tif (this.wireFormat !== \"msgpack\") {\n\t\t\tawait this.reportValidationError(\n\t\t\t\tnew Error(\"socka: unexpected binary frame in JSON mode\"),\n\t\t\t\tbuffer,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = parseWirePayload(buffer, \"msgpack\");\n\t\t} catch (err) {\n\t\t\tawait this.reportValidationError(\n\t\t\t\terr instanceof Error ? err : new Error(\"socka: msgpack decode failed\"),\n\t\t\t\tbuffer,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tawait this.dispatchAfterParsed(parsed, buffer);\n\t}\n\n\tprivate async dispatchAfterParsed(\n\t\tparsed: unknown,\n\t\toriginalWire: unknown,\n\t): Promise<void> {\n\t\tlet decoded: ReturnType<typeof decodeSockaWire>;\n\t\ttry {\n\t\t\tdecoded = decodeSockaWire(parsed);\n\t\t} catch (err) {\n\t\t\tif (err instanceof SockaWireError) {\n\t\t\t\tawait this.reportValidationError(err, originalWire);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\n\t\tswitch (decoded.kind) {\n\t\t\tcase \"clientRequest\":\n\t\t\t\tawait this.dispatchClientRequest(decoded.frame, originalWire);\n\t\t\t\treturn;\n\t\t\tcase \"serverResponse\":\n\t\t\tcase \"serverError\":\n\t\t\tcase \"serverEvent\":\n\t\t\t\tawait this.reportValidationError(\n\t\t\t\t\tnew Error(\"socka: unexpected server-originated frame from client\"),\n\t\t\t\t\tparsed,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\tdefault:\n\t\t\t\texhaustiveGuard(decoded);\n\t\t}\n\t}\n\n\tprivate async dispatchClientRequest(\n\t\tframe: SockaClientRequestFrame,\n\t\t_originalWire: unknown,\n\t): Promise<void> {\n\t\tconst rpcName = frame.rpc;\n\t\tconst procedure = this.config.contract.calls[rpcName];\n\n\t\tif (!procedure) {\n\t\t\tconst errorFrame = encodeServerError(\n\t\t\t\tframe.id,\n\t\t\t\t`Unknown call: ${rpcName}`,\n\t\t\t\t{ rpc: rpcName },\n\t\t\t);\n\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\treturn;\n\t\t}\n\n\t\tlet validatedInput: unknown;\n\t\tif (procedure.input) {\n\t\t\ttry {\n\t\t\t\tvalidatedInput = await parseStandardSchema(procedure.input, frame.body);\n\t\t\t} catch (err) {\n\t\t\t\tconst msg =\n\t\t\t\t\terr instanceof Error ? err.message : \"Input validation failed\";\n\t\t\t\tconst errorFrame = encodeServerError(frame.id, msg, {\n\t\t\t\t\trpc: rpcName,\n\t\t\t\t});\n\t\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tlet result: unknown;\n\t\ttry {\n\t\t\tif (procedure.input) {\n\t\t\t\tconst handler = this.config.handlers[rpcName] as (\n\t\t\t\t\tinput: unknown,\n\t\t\t\t\ts: SockaWebSocketSession<TContract, TData>,\n\t\t\t\t) => unknown | Promise<unknown>;\n\t\t\t\tresult = await handler(validatedInput, this);\n\t\t\t} else {\n\t\t\t\tconst handler = this.config.handlers[rpcName] as (\n\t\t\t\t\ts: SockaWebSocketSession<TContract, TData>,\n\t\t\t\t) => unknown | Promise<unknown>;\n\t\t\t\tresult = await handler(this);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.config.onHandlerError?.(err, rpcName, validatedInput, this);\n\t\t\tconst sockaErr =\n\t\t\t\terr instanceof SockaError\n\t\t\t\t\t? err\n\t\t\t\t\t: new SockaError(\n\t\t\t\t\t\t\terr instanceof Error ? err.message : \"Handler failed\",\n\t\t\t\t\t\t);\n\t\t\tconst errorFrame = encodeServerError(frame.id, sockaErr.message, {\n\t\t\t\trpc: rpcName,\n\t\t\t\tcode: sockaErr.code,\n\t\t\t\tdata: sockaErr.data,\n\t\t\t});\n\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\treturn;\n\t\t}\n\n\t\tif (procedure.output === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet validatedOutput: unknown;\n\t\ttry {\n\t\t\tvalidatedOutput = await parseStandardSchema(procedure.output, result);\n\t\t} catch (err) {\n\t\t\tconst msg =\n\t\t\t\terr instanceof Error ? err.message : \"Output validation failed\";\n\t\t\tconst errorFrame = encodeServerError(frame.id, msg, { rpc: rpcName });\n\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\treturn;\n\t\t}\n\n\t\tconst responseFrame = encodeServerResponse(\n\t\t\tframe.id,\n\t\t\trpcName,\n\t\t\tvalidatedOutput,\n\t\t);\n\t\tthis.sendWireFrame(responseFrame);\n\t}\n\n\tprivate encodeOutgoing(frame: SockaWireFrame): string | Uint8Array {\n\t\treturn encodeSockaWire(\n\t\t\tframe,\n\t\t\tthis.wireFormat,\n\t\t\tthis.config.serializeJson ?? JSON.stringify,\n\t\t);\n\t}\n\n\tprivate sendWireFrame(frame: SockaWireFrame): void {\n\t\tif (this.websocket.readyState !== WebSocket.OPEN) {\n\t\t\treturn;\n\t\t}\n\t\tconst encoded = this.encodeOutgoing(frame);\n\t\tif (typeof encoded === \"string\") {\n\t\t\tthis.websocket.send(encoded);\n\t\t\treturn;\n\t\t}\n\t\tconst copy = new Uint8Array(encoded.byteLength);\n\t\tcopy.set(encoded);\n\t\tthis.websocket.send(copy.buffer);\n\t}\n\n\t/**\n\t * Send a server event frame (wire). Prefer {@link emitPush} so\n\t * payloads are validated against the contract.\n\t */\n\tpublic emitWireEvent(event: string, body: unknown): void {\n\t\tconst frame = encodeServerEvent(event, body);\n\t\tthis.sendWireFrame(frame);\n\t}\n\n\tpublic async emitPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t): Promise<void> {\n\t\tconst schema = this.config.contract.pushes[name];\n\t\tif (!schema) {\n\t\t\tthrow new Error(`socka: unknown push ${String(name)}`);\n\t\t}\n\t\tconst validated = await parseStandardSchema(\n\t\t\tschema as StandardSchemaV1<unknown, InferSockaPushPayload<TContract, K>>,\n\t\t\tbody,\n\t\t);\n\t\tthis.emitWireEvent(name, validated);\n\t}\n\n\tpublic async broadcastPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t\texcludeSelf = false,\n\t): Promise<void> {\n\t\tconst schema = this.config.contract.pushes[name];\n\t\tif (!schema) {\n\t\t\tthrow new Error(`socka: unknown push ${String(name)}`);\n\t\t}\n\t\tconst validated = await parseStandardSchema(\n\t\t\tschema as StandardSchemaV1<unknown, InferSockaPushPayload<TContract, K>>,\n\t\t\tbody,\n\t\t);\n\t\tbroadcastSockaEventToPeers(\n\t\t\tthis.sessions,\n\t\t\tthis,\n\t\t\tname,\n\t\t\tvalidated,\n\t\t\texcludeSelf,\n\t\t);\n\t}\n\n\tprivate async reportValidationError(\n\t\terror: unknown,\n\t\toriginalMessage: unknown,\n\t): Promise<void> {\n\t\tif (this.config.onValidationError) {\n\t\t\tawait this.config.onValidationError(error, originalMessage);\n\t\t} else {\n\t\t\tconsole.error(\"socka: validation error:\", error, originalMessage);\n\t\t}\n\t}\n}\n\n/**\n * Invoke {@link SockaWebSocketSessionConfig.onAttached} after the session is\n * registered in the shared map.\n */\nexport function runSockaSessionOnAttached<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfig: SockaWebSocketSessionConfigUnion<TContract, TData>,\n\tsession: SockaWebSocketSession<TContract, TData>,\n): void {\n\tconst cb = config.onAttached;\n\tif (!cb) return;\n\ttry {\n\t\tconst result = cb(session);\n\t\tvoid Promise.resolve(result).catch((error: unknown) => {\n\t\t\treportSockaError(config.reportError, {\n\t\t\t\tkind: \"serverOnAttached\",\n\t\t\t\terror,\n\t\t\t});\n\t\t});\n\t} catch (error) {\n\t\treportSockaError(config.reportError, { kind: \"serverOnAttached\", error });\n\t}\n}\n"]}
|