@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.
Files changed (46) hide show
  1. package/README.md +15 -1
  2. package/dist/{SockaWebSocketSession-B1w7RAid.d.ts → SockaWebSocketSession-BaGvSerM.d.ts} +28 -10
  3. package/dist/bun/index.d.ts +6 -6
  4. package/dist/bun/index.js +2 -2
  5. package/dist/bun/index.js.map +1 -1
  6. package/dist/{chunk-5WQTYLIC.js → chunk-JR2GENNT.js} +2 -2
  7. package/dist/chunk-JR2GENNT.js.map +1 -0
  8. package/dist/{chunk-P3JEEOJL.js → chunk-THFUHQJ3.js} +2 -2
  9. package/dist/chunk-THFUHQJ3.js.map +1 -0
  10. package/dist/{chunk-LVVCHLNW.js → chunk-TTXY7O5P.js} +20 -4
  11. package/dist/chunk-TTXY7O5P.js.map +1 -0
  12. package/dist/client/index.d.ts +10 -10
  13. package/dist/client/index.js +1 -1
  14. package/dist/core/index.d.ts +1 -1
  15. package/dist/core/index.js.map +1 -1
  16. package/dist/do/index.d.ts +83 -18
  17. package/dist/do/index.js +62 -6
  18. package/dist/do/index.js.map +1 -1
  19. package/dist/hono/cloudflare-workers.d.ts +4 -4
  20. package/dist/hono/cloudflare-workers.js +2 -2
  21. package/dist/hono/cloudflare-workers.js.map +1 -1
  22. package/dist/hono/index.d.ts +4 -4
  23. package/dist/hono/index.js +2 -2
  24. package/dist/hono/index.js.map +1 -1
  25. package/dist/react/index.d.ts +13 -13
  26. package/dist/react/index.js +1 -1
  27. package/dist/react/index.js.map +1 -1
  28. package/dist/server/index.d.ts +8 -8
  29. package/dist/server/index.js +4 -4
  30. package/dist/server/index.js.map +1 -1
  31. package/dist/{socka-report-error-CXwpAUgl.d.ts → socka-report-error-nTXJIzNb.d.ts} +32 -12
  32. package/docs/README.md +2 -0
  33. package/docs/client.md +17 -0
  34. package/docs/collaborative-realtime.md +61 -0
  35. package/docs/durable-objects.md +84 -35
  36. package/docs/internals.md +1 -1
  37. package/docs/pushes.md +32 -2
  38. package/docs/react-durable-objects.md +96 -0
  39. package/docs/recipes.md +1 -1
  40. package/docs/reference.md +18 -5
  41. package/package.json +8 -8
  42. package/skills/socka/core-rpc/SKILL.md +1 -1
  43. package/skills/socka/do-session/SKILL.md +1 -1
  44. package/dist/chunk-5WQTYLIC.js.map +0 -1
  45. package/dist/chunk-LVVCHLNW.js.map +0 -1
  46. package/dist/chunk-P3JEEOJL.js.map +0 -1
@@ -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`**. You pass **`handlers`**, **`handleClose`**, and optional **`onAttached`**; **`broadcastPush`** uses the shared **`sessions`** map you pass into the constructor.
6
- 2. **`SockaWebSocketDO`** — subclasses **`BaseWebSocketDO`** from **`@firtoz/websocket-do`**. It connects HTTP WebSocket upgrade **`createSockaSession(ctx, websocket)`** so your session class gets the right **`sessions`** map and, when needed, a Hono **`Context`**.
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
- ```ts
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
- new SockaDoSession(websocket, sessions, {
30
- contract: myContract,
31
- // wireFormat: "msgpack", // optional; default JSON text — must match client
32
- handlers: {
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
- **`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
-
46
- ## `SockaWebSocketDO` and routing
99
+ ## Hibernation and `session.data`
47
100
 
48
- Subclass **`SockaWebSocketDO`** and pass **`createSockaSession`** to connect upgrades to your session class. The base class exposes **`getBaseApp()`** for a Hono app that matches **`BaseWebSocketDO`** routing (see **`@firtoz/websocket-do`** for env typing and routes).
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
- Minimal shape (full game example: [`examples/tic-tac-toe-do`](../../../examples/tic-tac-toe-do/src/do.ts)):
103
+ **Hibernation resume** **`@firtoz/websocket-do`** calls **`createSession(undefined, websocket)`** then **`session.resume()`**, **not** **`startFresh`**. So:
51
104
 
52
- ```ts
53
- export class MyDO extends SockaWebSocketDO<MySession, Env> {
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
- constructor(ctx: DurableObjectState, env: Env) {
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
- ## Hibernation and `session.data`
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
- After you mutate **`session.data`**, call **`await session.update()`** (from **`@firtoz/websocket-do`**) so the attachment is rewritten for **hibernation**. 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.
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` **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.
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
- Lower-level helpers (for example **`broadcastSockaEventToPeers`** from **`@firtoz/socka/server`**) exist for advanced cases; prefer **`broadcastPush`** when you already have a session so schemas stay centralized.
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
- Subclass **`SockaWebSocketDO`**, implement **`createSockaSession`** returning **`SockaDoSession`** — see **[Durable Objects](./durable-objects.md)** and **[chatroom-do](../../examples/chatroom-do/src/do.ts)**.
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 { InferSockaSend, InferSockaHandlers } from "@firtoz/socka/core";
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`** — Server handler arity matches **`calls`** (with or without `input`).
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.0.2",
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": "^1.11.10"
101
+ "msgpackr": "^2.0.1"
102
102
  },
103
103
  "peerDependencies": {
104
- "@cloudflare/workers-types": "^4.20260423.1",
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.20260423.1",
136
+ "@cloudflare/workers-types": "^4.20260503.1",
137
137
  "@happy-dom/global-registrator": "^20.9.0",
138
- "@hono/node-server": "^2.0.0",
139
- "@hono/node-ws": "^1.3.0",
140
- "@tanstack/intent": "^0.0.32",
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.3.6"
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`**: thin **`BaseWebSocketDO`** wrapper; you supply **`createSockaSession(ctx, websocket)`** returning a **`SockaDoSession`** (or subclass).
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":";;;AA2CA,SAAS,qBAIR,MAAA,EAC+D;AAC/D,EAAA,OACC,sBAAA,IAA0B,MAAA,IAAU,MAAA,CAAO,oBAAA,KAAyB,KAAA;AAEtE;AAgCO,SAAS,2BACf,QAAA,EACA,IAAA,EACA,KAAA,EACA,IAAA,EACA,cAAc,KAAA,EACP;AACP,EAAA,KAAA,MAAW,CAAC,EAAA,EAAI,OAAO,CAAA,IAAK,QAAA,EAAU;AACrC,IAAA,IAAI,WAAA,IAAe,EAAA,KAAO,IAAA,CAAK,SAAA,EAAW;AAC1C,IAAA,OAAA,CAAQ,aAAA,CAAc,OAAO,IAAI,CAAA;AAAA,EAClC;AACD;AAMO,IAAM,wBAAN,MAIP;AAAA,EAKQ,WAAA,CACU,SAAA,EACG,QAAA,EAInB,MAAA,EACA,IAAA,EACC;AAPe,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AACG,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAOnB,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,UAAA,GAAa,OAAO,UAAA,IAAc,MAAA;AACvC,IAAA,IAAI,oBAAA,CAAqB,MAAM,CAAA,EAAG;AACjC,MAAA,MAAM,aAAa,MAAA,CAAO,UAAA;AAG1B,MAAA,MAAM,MAAA,GAAS,UAAA,KAAe,CAAC,EAAA,MAA4B,EAAC,CAAA,CAAA;AAC5D,MAAA,IAAA,CAAK,KAAA,GAAQ,MAAA,CAAO,IAAA,IAAQ,EAAE,CAAA;AAAA,IAC/B,CAAA,MAAO;AACN,MAAA,IAAI,CAAC,MAAM,OAAA,EAAS;AACnB,QAAA,MAAM,IAAI,KAAA;AAAA,UACT;AAAA,SACD;AAAA,MACD;AACA,MAAA,MAAM,UAAA,GAAuC,EAAE,OAAA,EAAS,IAAA,CAAK,OAAA,EAAQ;AACrE,MAAA,IAAI,OAAO,UAAA,EAAY;AACtB,QAAA,IAAA,CAAK,KAAA,GAAQ,MAAA,CAAO,UAAA,CAAW,UAAU,CAAA;AAAA,MAC1C,CAAA,MAAO;AACN,QAAA,IAAA,CAAK,QAAQ,EAAC;AAAA,MACf;AAAA,IACD;AAAA,EACD;AAAA,EAEA,IAAW,IAAA,GAAc;AACxB,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,UAAU,OAAA,EAA8C;AAC9D,IAAA,MAAM,MAAe,EAAC;AACtB,IAAA,KAAA,MAAW,CAAC,EAAA,EAAI,CAAC,CAAA,IAAK,KAAK,QAAA,EAAU;AACpC,MAAA,IAAI,OAAA,EAAS,WAAA,IAAe,EAAA,KAAO,IAAA,CAAK,SAAA,EAAW;AACnD,MAAA,GAAA,CAAI,IAAA,CAAK,EAAE,IAAI,CAAA;AAAA,IAChB;AACA,IAAA,OAAO,GAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,aAAA,CACN,KACA,OAAA,EACM;AACN,IAAA,MAAM,MAAW,EAAC;AAClB,IAAA,KAAA,MAAW,CAAC,EAAA,EAAI,CAAC,CAAA,IAAK,KAAK,QAAA,EAAU;AACpC,MAAA,IAAI,OAAA,EAAS,WAAA,IAAe,EAAA,KAAO,IAAA,CAAK,SAAA,EAAW;AACnD,MAAA,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAC,CAAA;AAAA,IAChB;AACA,IAAA,OAAO,GAAA;AAAA,EACR;AAAA;AAAA,EAGO,UAAU,OAAA,EAA6C;AAC7D,IAAA,IAAI,CAAA,GAAI,CAAA;AACR,IAAA,KAAA,MAAW,CAAC,EAAE,CAAA,IAAK,IAAA,CAAK,QAAA,EAAU;AACjC,MAAA,IAAI,OAAA,EAAS,WAAA,IAAe,EAAA,KAAO,IAAA,CAAK,SAAA,EAAW;AACnD,MAAA,CAAA,IAAK,CAAA;AAAA,IACN;AACA,IAAA,OAAO,CAAA;AAAA,EACR;AAAA;AAAA,EAGO,SAAS,OAAA,EAA8C;AAC7D,IAAA,OAAO,IAAA,CAAK,SAAA,CAAU,OAAO,CAAA,GAAI,CAAA;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAa,iBAAA,GAAmC;AAC/C,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,WAAA,CAAY,IAAI,CAAA;AAAA,EACnC;AAAA,EAEA,MAAa,iBAAiB,UAAA,EAAmC;AAChE,IAAA,IAAI,IAAA,CAAK,eAAe,MAAA,EAAQ;AAC/B,MAAA,MAAM,IAAA,CAAK,qBAAA;AAAA,QACV,IAAI,MAAM,8CAA8C,CAAA;AAAA,QACxD;AAAA,OACD;AACA,MAAA;AAAA,IACD;AACA,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,MAAA,CAAO,eAAA,IAAmB,IAAA,CAAK,KAAA;AACxD,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACH,MAAA,MAAA,GAAS,YAAY,UAAU,CAAA;AAAA,IAChC,CAAA,CAAA,MAAQ;AACP,MAAA,MAAM,IAAA,CAAK,qBAAA;AAAA,QACV,IAAI,MAAM,qBAAqB,CAAA;AAAA,QAC/B;AAAA,OACD;AACA,MAAA;AAAA,IACD;AACA,IAAA,MAAM,IAAA,CAAK,mBAAA,CAAoB,MAAA,EAAQ,UAAU,CAAA;AAAA,EAClD;AAAA,EAEA,MAAa,oBAAoB,MAAA,EAAoC;AACpE,IAAA,IAAI,IAAA,CAAK,eAAe,SAAA,EAAW;AAClC,MAAA,MAAM,IAAA,CAAK,qBAAA;AAAA,QACV,IAAI,MAAM,6CAA6C,CAAA;AAAA,QACvD;AAAA,OACD;AACA,MAAA;AAAA,IACD;AACA,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACH,MAAA,MAAA,GAAS,gBAAA,CAAiB,QAAQ,SAAS,CAAA;AAAA,IAC5C,SAAS,GAAA,EAAK;AACb,MAAA,MAAM,IAAA,CAAK,qBAAA;AAAA,QACV,GAAA,YAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,MAAM,8BAA8B,CAAA;AAAA,QACrE;AAAA,OACD;AACA,MAAA;AAAA,IACD;AACA,IAAA,MAAM,IAAA,CAAK,mBAAA,CAAoB,MAAA,EAAQ,MAAM,CAAA;AAAA,EAC9C;AAAA,EAEA,MAAc,mBAAA,CACb,MAAA,EACA,YAAA,EACgB;AAChB,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI;AACH,MAAA,OAAA,GAAU,gBAAgB,MAAM,CAAA;AAAA,IACjC,SAAS,GAAA,EAAK;AACb,MAAA,IAAI,eAAe,cAAA,EAAgB;AAClC,QAAA,MAAM,IAAA,CAAK,qBAAA,CAAsB,GAAA,EAAK,YAAY,CAAA;AAClD,QAAA;AAAA,MACD;AACA,MAAA,MAAM,GAAA;AAAA,IACP;AAEA,IAAA,QAAQ,QAAQ,IAAA;AAAM,MACrB,KAAK,eAAA;AACJ,QAAA,MAAM,IAAA,CAAK,qBAAA,CAAsB,OAAA,CAAQ,KAAA,EAAO,YAAY,CAAA;AAC5D,QAAA;AAAA,MACD,KAAK,gBAAA;AAAA,MACL,KAAK,aAAA;AAAA,MACL,KAAK,aAAA;AACJ,QAAA,MAAM,IAAA,CAAK,qBAAA;AAAA,UACV,IAAI,MAAM,uDAAuD,CAAA;AAAA,UACjE;AAAA,SACD;AACA,QAAA;AAAA,MACD;AACC,QAAA,eAAA,CAAgB,OAAO,CAAA;AAAA;AACzB,EACD;AAAA,EAEA,MAAc,qBAAA,CACb,KAAA,EACA,aAAA,EACgB;AAChB,IAAA,MAAM,UAAU,KAAA,CAAM,GAAA;AACtB,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,MAAM,OAAO,CAAA;AAEpD,IAAA,IAAI,CAAC,SAAA,EAAW;AACf,MAAA,MAAM,UAAA,GAAa,iBAAA;AAAA,QAClB,KAAA,CAAM,EAAA;AAAA,QACN,iBAAiB,OAAO,CAAA,CAAA;AAAA,QACxB,EAAE,KAAK,OAAA;AAAQ,OAChB;AACA,MAAA,IAAA,CAAK,cAAc,UAAU,CAAA;AAC7B,MAAA;AAAA,IACD;AAEA,IAAA,IAAI,cAAA;AACJ,IAAA,IAAI,UAAU,KAAA,EAAO;AACpB,MAAA,IAAI;AACH,QAAA,cAAA,GAAiB,MAAM,mBAAA,CAAoB,SAAA,CAAU,KAAA,EAAO,MAAM,IAAI,CAAA;AAAA,MACvE,SAAS,GAAA,EAAK;AACb,QAAA,MAAM,GAAA,GACL,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,yBAAA;AACtC,QAAA,MAAM,UAAA,GAAa,iBAAA,CAAkB,KAAA,CAAM,EAAA,EAAI,GAAA,EAAK;AAAA,UACnD,GAAA,EAAK;AAAA,SACL,CAAA;AACD,QAAA,IAAA,CAAK,cAAc,UAAU,CAAA;AAC7B,QAAA;AAAA,MACD;AAAA,IACD;AAEA,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACH,MAAA,IAAI,UAAU,KAAA,EAAO;AACpB,QAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA;AAI5C,QAAA,MAAA,GAAS,MAAM,OAAA,CAAQ,cAAA,EAAgB,IAAI,CAAA;AAAA,MAC5C,CAAA,MAAO;AACN,QAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA;AAG5C,QAAA,MAAA,GAAS,MAAM,QAAQ,IAAI,CAAA;AAAA,MAC5B;AAAA,IACD,SAAS,GAAA,EAAK;AACb,MAAA,IAAA,CAAK,MAAA,CAAO,cAAA,GAAiB,GAAA,EAAK,OAAA,EAAS,gBAAgB,IAAI,CAAA;AAC/D,MAAA,MAAM,QAAA,GACL,GAAA,YAAe,UAAA,GACZ,GAAA,GACA,IAAI,UAAA;AAAA,QACJ,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,OACtC;AACH,MAAA,MAAM,UAAA,GAAa,iBAAA,CAAkB,KAAA,CAAM,EAAA,EAAI,SAAS,OAAA,EAAS;AAAA,QAChE,GAAA,EAAK,OAAA;AAAA,QACL,MAAM,QAAA,CAAS,IAAA;AAAA,QACf,MAAM,QAAA,CAAS;AAAA,OACf,CAAA;AACD,MAAA,IAAA,CAAK,cAAc,UAAU,CAAA;AAC7B,MAAA;AAAA,IACD;AAEA,IAAA,IAAI,SAAA,CAAU,WAAW,MAAA,EAAW;AACnC,MAAA;AAAA,IACD;AAEA,IAAA,IAAI,eAAA;AACJ,IAAA,IAAI;AACH,MAAA,eAAA,GAAkB,MAAM,mBAAA,CAAoB,SAAA,CAAU,MAAA,EAAQ,MAAM,CAAA;AAAA,IACrE,SAAS,GAAA,EAAK;AACb,MAAA,MAAM,GAAA,GACL,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,0BAAA;AACtC,MAAA,MAAM,UAAA,GAAa,kBAAkB,KAAA,CAAM,EAAA,EAAI,KAAK,EAAE,GAAA,EAAK,SAAS,CAAA;AACpE,MAAA,IAAA,CAAK,cAAc,UAAU,CAAA;AAC7B,MAAA;AAAA,IACD;AAEA,IAAA,MAAM,aAAA,GAAgB,oBAAA;AAAA,MACrB,KAAA,CAAM,EAAA;AAAA,MACN,OAAA;AAAA,MACA;AAAA,KACD;AACA,IAAA,IAAA,CAAK,cAAc,aAAa,CAAA;AAAA,EACjC;AAAA,EAEQ,eAAe,KAAA,EAA4C;AAClE,IAAA,OAAO,eAAA;AAAA,MACN,KAAA;AAAA,MACA,IAAA,CAAK,UAAA;AAAA,MACL,IAAA,CAAK,MAAA,CAAO,aAAA,IAAiB,IAAA,CAAK;AAAA,KACnC;AAAA,EACD;AAAA,EAEQ,cAAc,KAAA,EAA6B;AAClD,IAAA,IAAI,IAAA,CAAK,SAAA,CAAU,UAAA,KAAe,SAAA,CAAU,IAAA,EAAM;AACjD,MAAA;AAAA,IACD;AACA,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,cAAA,CAAe,KAAK,CAAA;AACzC,IAAA,IAAI,OAAO,YAAY,QAAA,EAAU;AAChC,MAAA,IAAA,CAAK,SAAA,CAAU,KAAK,OAAO,CAAA;AAC3B,MAAA;AAAA,IACD;AACA,IAAA,MAAM,IAAA,GAAO,IAAI,UAAA,CAAW,OAAA,CAAQ,UAAU,CAAA;AAC9C,IAAA,IAAA,CAAK,IAAI,OAAO,CAAA;AAChB,IAAA,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,aAAA,CAAc,OAAe,IAAA,EAAqB;AACxD,IAAA,MAAM,KAAA,GAAQ,iBAAA,CAAkB,KAAA,EAAO,IAAI,CAAA;AAC3C,IAAA,IAAA,CAAK,cAAc,KAAK,CAAA;AAAA,EACzB;AAAA,EAEA,MAAa,QAAA,CACZ,IAAA,EACA,IAAA,EACgB;AAChB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,OAAO,IAAI,CAAA;AAC/C,IAAA,IAAI,CAAC,MAAA,EAAQ;AACZ,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,oBAAA,EAAuB,MAAA,CAAO,IAAI,CAAC,CAAA,CAAE,CAAA;AAAA,IACtD;AACA,IAAA,MAAM,YAAY,MAAM,mBAAA;AAAA,MACvB,MAAA;AAAA,MACA;AAAA,KACD;AACA,IAAA,IAAA,CAAK,aAAA,CAAc,MAAM,SAAS,CAAA;AAAA,EACnC;AAAA,EAEA,MAAa,aAAA,CACZ,IAAA,EACA,IAAA,EACA,cAAc,KAAA,EACE;AAChB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,OAAO,IAAI,CAAA;AAC/C,IAAA,IAAI,CAAC,MAAA,EAAQ;AACZ,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,oBAAA,EAAuB,MAAA,CAAO,IAAI,CAAC,CAAA,CAAE,CAAA;AAAA,IACtD;AACA,IAAA,MAAM,YAAY,MAAM,mBAAA;AAAA,MACvB,MAAA;AAAA,MACA;AAAA,KACD;AACA,IAAA,0BAAA;AAAA,MACC,IAAA,CAAK,QAAA;AAAA,MACL,IAAA;AAAA,MACA,IAAA;AAAA,MACA,SAAA;AAAA,MACA;AAAA,KACD;AAAA,EACD;AAAA,EAEA,MAAc,qBAAA,CACb,KAAA,EACA,eAAA,EACgB;AAChB,IAAA,IAAI,IAAA,CAAK,OAAO,iBAAA,EAAmB;AAClC,MAAA,MAAM,IAAA,CAAK,MAAA,CAAO,iBAAA,CAAkB,KAAA,EAAO,eAAe,CAAA;AAAA,IAC3D,CAAA,MAAO;AACN,MAAA,OAAA,CAAQ,KAAA,CAAM,0BAAA,EAA4B,KAAA,EAAO,eAAe,CAAA;AAAA,IACjE;AAAA,EACD;AACD;AAMO,SAAS,yBAAA,CAIf,QACA,OAAA,EACO;AACP,EAAA,MAAM,KAAK,MAAA,CAAO,UAAA;AAClB,EAAA,IAAI,CAAC,EAAA,EAAI;AACT,EAAA,IAAI;AACH,IAAA,MAAM,MAAA,GAAS,GAAG,OAAO,CAAA;AACzB,IAAA,KAAK,QAAQ,OAAA,CAAQ,MAAM,CAAA,CAAE,KAAA,CAAM,CAAC,KAAA,KAAmB;AACtD,MAAA,gBAAA,CAAiB,OAAO,WAAA,EAAa;AAAA,QACpC,IAAA,EAAM,kBAAA;AAAA,QACN;AAAA,OACA,CAAA;AAAA,IACF,CAAC,CAAA;AAAA,EACF,SAAS,KAAA,EAAO;AACf,IAAA,gBAAA,CAAiB,OAAO,WAAA,EAAa,EAAE,IAAA,EAAM,kBAAA,EAAoB,OAAO,CAAA;AAAA,EACzE;AACD","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"]}