@firtoz/socka 2.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 +120 -0
- package/assets/banner.png +0 -0
- package/dist/SockaWebSocketSession-Bru8yFcK.d.ts +107 -0
- package/dist/bun/index.d.ts +38 -0
- package/dist/bun/index.js +121 -0
- package/dist/bun/index.js.map +1 -0
- package/dist/chunk-45D4T232.js +236 -0
- package/dist/chunk-45D4T232.js.map +1 -0
- package/dist/chunk-5WQTYLIC.js +46 -0
- package/dist/chunk-5WQTYLIC.js.map +1 -0
- package/dist/chunk-AM7PB26G.js +421 -0
- package/dist/chunk-AM7PB26G.js.map +1 -0
- package/dist/chunk-MZCQHJXY.js +158 -0
- package/dist/chunk-MZCQHJXY.js.map +1 -0
- package/dist/chunk-YMT4HAH7.js +20 -0
- package/dist/chunk-YMT4HAH7.js.map +1 -0
- package/dist/client/index.d.ts +119 -0
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -0
- package/dist/do/index.d.ts +80 -0
- package/dist/do/index.js +110 -0
- package/dist/do/index.js.map +1 -0
- package/dist/hono/cloudflare-workers.d.ts +21 -0
- package/dist/hono/cloudflare-workers.js +68 -0
- package/dist/hono/cloudflare-workers.js.map +1 -0
- package/dist/hono/index.d.ts +30 -0
- package/dist/hono/index.js +74 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/react/index.d.ts +72 -0
- package/dist/react/index.js +126 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/index.d.ts +27 -0
- package/dist/server/index.js +63 -0
- package/dist/server/index.js.map +1 -0
- package/dist/socka-report-error-DzFI2Tr7.d.ts +206 -0
- package/docs/README.md +18 -0
- package/docs/client.md +85 -0
- package/docs/comparison.md +36 -0
- package/docs/durable-objects.md +74 -0
- package/docs/events.md +48 -0
- package/docs/getting-started.md +138 -0
- package/docs/lifecycle.md +31 -0
- package/docs/multi-room.md +31 -0
- package/docs/peers.md +85 -0
- package/docs/reference.md +123 -0
- package/docs/server.md +124 -0
- package/examples/minimal-socka.ts +31 -0
- package/package.json +148 -0
- package/roadmap.md +8 -0
- package/skills/socka/core-rpc/SKILL.md +36 -0
- package/skills/socka/do-session/SKILL.md +33 -0
- package/skills/socka/standard-schema/SKILL.md +26 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Call names that cannot be used in {@link defineSocka} `calls` because they would
|
|
5
|
+
* make {@link SockaSession} `send` look Promise-like or clash with ordinary object
|
|
6
|
+
* shape (`constructor`, `Object.prototype`). Call names live only under `send`, so
|
|
7
|
+
* session fields like `client` / `close` need not be reserved.
|
|
8
|
+
*/
|
|
9
|
+
declare const RESERVED_SOCKA_PROCEDURE_NAMES: readonly ["then", "catch", "finally", "constructor", "toString", "valueOf", "toLocaleString", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable"];
|
|
10
|
+
/** Union of {@link RESERVED_SOCKA_PROCEDURE_NAMES}. */
|
|
11
|
+
type ReservedSockaProcedureName = (typeof RESERVED_SOCKA_PROCEDURE_NAMES)[number];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Defines one client-initiated call: an optional input schema and a required output schema.
|
|
15
|
+
* Both must be Standard Schema v1 compliant (Zod v4, Valibot, ArkType, etc.).
|
|
16
|
+
*/
|
|
17
|
+
type SockaProcedureDef = {
|
|
18
|
+
readonly input?: StandardSchemaV1;
|
|
19
|
+
readonly output: StandardSchemaV1;
|
|
20
|
+
};
|
|
21
|
+
/** Configuration object accepted by {@link defineSocka}. */
|
|
22
|
+
type SockaContractConfig = {
|
|
23
|
+
readonly calls: Record<string, SockaProcedureDef>;
|
|
24
|
+
readonly pushes?: Record<string, StandardSchemaV1>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* When call keys are a **narrow** object type, rejects keys in
|
|
28
|
+
* {@link ReservedSockaProcedureName} (thenable / `Object.prototype` hazards on
|
|
29
|
+
* `session.send`). Wide `Record<string, SockaProcedureDef>` is unchanged so
|
|
30
|
+
* dynamic maps still typecheck; use runtime validation (see {@link SockaSession}
|
|
31
|
+
* `send`).
|
|
32
|
+
*/
|
|
33
|
+
type ValidateSockaCallKeys<P extends Record<string, SockaProcedureDef>> = string extends keyof P ? P : keyof P & ReservedSockaProcedureName extends never ? P : never;
|
|
34
|
+
/** Runtime contract returned by {@link defineSocka}, preserving full generic types. */
|
|
35
|
+
type SockaContract<T extends SockaContractConfig = SockaContractConfig> = {
|
|
36
|
+
readonly calls: T["calls"];
|
|
37
|
+
readonly pushes: T extends {
|
|
38
|
+
pushes: Record<string, StandardSchemaV1>;
|
|
39
|
+
} ? T["pushes"] : Record<string, never>;
|
|
40
|
+
};
|
|
41
|
+
type CallFn<P extends SockaProcedureDef> = P extends {
|
|
42
|
+
input: infer I extends StandardSchemaV1;
|
|
43
|
+
} ? (input: StandardSchemaV1.InferInput<I>) => Promise<StandardSchemaV1.InferOutput<P["output"]>> : () => Promise<StandardSchemaV1.InferOutput<P["output"]>>;
|
|
44
|
+
/**
|
|
45
|
+
* Infers the typed `session.send.*` method map for a contract.
|
|
46
|
+
*/
|
|
47
|
+
type InferSockaSend<C extends SockaContract> = {
|
|
48
|
+
[K in keyof C["calls"]]: CallFn<C["calls"][K]>;
|
|
49
|
+
};
|
|
50
|
+
type HandlerOut<P extends SockaProcedureDef> = StandardSchemaV1.InferOutput<P["output"]> | Promise<StandardSchemaV1.InferOutput<P["output"]>>;
|
|
51
|
+
type HandlerFn<P extends SockaProcedureDef, TSession> = P extends {
|
|
52
|
+
input: infer I extends StandardSchemaV1;
|
|
53
|
+
} ? (input: StandardSchemaV1.InferInput<I>, session: TSession) => HandlerOut<P> : (session: TSession) => HandlerOut<P>;
|
|
54
|
+
/**
|
|
55
|
+
* Infers the typed server handler map for a contract. Handlers with an input
|
|
56
|
+
* schema take `(input, session)`; calls without input take `(session)` only.
|
|
57
|
+
* Each handler returns the output that will be validated before sending.
|
|
58
|
+
*/
|
|
59
|
+
type InferSockaHandlers<C extends SockaContract, TSession> = {
|
|
60
|
+
[K in keyof C["calls"]]: HandlerFn<C["calls"][K], TSession>;
|
|
61
|
+
};
|
|
62
|
+
type InferPushPayload<S extends StandardSchemaV1> = StandardSchemaV1.InferOutput<S>;
|
|
63
|
+
/**
|
|
64
|
+
* Payload type for a contract push (output of the push's Standard Schema).
|
|
65
|
+
*/
|
|
66
|
+
type InferSockaPushPayload<C extends SockaContract<SockaContractConfig>, K extends keyof C["pushes"]> = C["pushes"][K] extends StandardSchemaV1 ? InferPushPayload<C["pushes"][K]> : never;
|
|
67
|
+
/**
|
|
68
|
+
* Infers the typed push subscription handler map for a contract's `pushes`.
|
|
69
|
+
*/
|
|
70
|
+
type InferSockaPushHandlers<C extends SockaContract> = {
|
|
71
|
+
[K in keyof C["pushes"]]: C["pushes"][K] extends StandardSchemaV1 ? (payload: InferPushPayload<C["pushes"][K]>) => void | Promise<void> : never;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Creates a socka contract from call and push definitions. Pass Zod, Valibot,
|
|
75
|
+
* ArkType, or any Standard Schema v1 schemas directly — no adapters needed.
|
|
76
|
+
*
|
|
77
|
+
* ```ts
|
|
78
|
+
* export const myContract = defineSocka({
|
|
79
|
+
* calls: {
|
|
80
|
+
* list: { output: z.array(itemSchema) },
|
|
81
|
+
* insert: { input: z.object({ item: itemSchema }), output: z.void() },
|
|
82
|
+
* },
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* Call names must not be {@link ReservedSockaProcedureName} — they would make
|
|
87
|
+
* `session.send` thenable or unsafe as a plain method bag.
|
|
88
|
+
*/
|
|
89
|
+
declare function defineSocka<const T extends SockaContractConfig>(config: T & {
|
|
90
|
+
calls: ValidateSockaCallKeys<T["calls"]>;
|
|
91
|
+
}): SockaContract<T>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Versioned socka wire framing. After JSON parse or msgpack unpack, every frame
|
|
95
|
+
* must satisfy {@link decodeSockaWire}; procedure bodies are validated with Standard Schema on each side.
|
|
96
|
+
*/
|
|
97
|
+
declare const SOCKA_WIRE_VERSION: 1;
|
|
98
|
+
declare class SockaWireError extends Error {
|
|
99
|
+
readonly name = "SockaWireError";
|
|
100
|
+
}
|
|
101
|
+
type SockaClientRequestFrame = {
|
|
102
|
+
readonly socka: "clientRequest";
|
|
103
|
+
readonly v: typeof SOCKA_WIRE_VERSION;
|
|
104
|
+
readonly id: string;
|
|
105
|
+
readonly rpc: string;
|
|
106
|
+
readonly body: Record<string, unknown>;
|
|
107
|
+
};
|
|
108
|
+
type SockaServerResponseFrame = {
|
|
109
|
+
readonly socka: "serverResponse";
|
|
110
|
+
readonly v: typeof SOCKA_WIRE_VERSION;
|
|
111
|
+
readonly id: string;
|
|
112
|
+
readonly rpc: string;
|
|
113
|
+
readonly body: unknown;
|
|
114
|
+
};
|
|
115
|
+
type SockaServerErrorFrame = {
|
|
116
|
+
readonly socka: "serverError";
|
|
117
|
+
readonly v: typeof SOCKA_WIRE_VERSION;
|
|
118
|
+
readonly id: string;
|
|
119
|
+
readonly error: string;
|
|
120
|
+
};
|
|
121
|
+
type SockaServerEventFrame = {
|
|
122
|
+
readonly socka: "serverEvent";
|
|
123
|
+
readonly v: typeof SOCKA_WIRE_VERSION;
|
|
124
|
+
readonly event: string;
|
|
125
|
+
readonly body: unknown;
|
|
126
|
+
};
|
|
127
|
+
type SockaWireFrame = SockaClientRequestFrame | SockaServerResponseFrame | SockaServerErrorFrame | SockaServerEventFrame;
|
|
128
|
+
type DecodedSockaWire = {
|
|
129
|
+
readonly kind: "clientRequest";
|
|
130
|
+
readonly frame: SockaClientRequestFrame;
|
|
131
|
+
} | {
|
|
132
|
+
readonly kind: "serverResponse";
|
|
133
|
+
readonly frame: SockaServerResponseFrame;
|
|
134
|
+
} | {
|
|
135
|
+
readonly kind: "serverError";
|
|
136
|
+
readonly frame: SockaServerErrorFrame;
|
|
137
|
+
} | {
|
|
138
|
+
readonly kind: "serverEvent";
|
|
139
|
+
readonly frame: SockaServerEventFrame;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Decodes a parsed wire object (from JSON or msgpack). Throws {@link SockaWireError}
|
|
143
|
+
* if the payload is not a valid socka v1 frame.
|
|
144
|
+
*/
|
|
145
|
+
declare function decodeSockaWire(parsed: unknown): DecodedSockaWire;
|
|
146
|
+
/** Builds a socka v1 client request frame. */
|
|
147
|
+
declare function encodeClientRequest(id: string, rpc: string, body: Record<string, unknown>): SockaClientRequestFrame;
|
|
148
|
+
/** Builds a socka v1 server response frame. */
|
|
149
|
+
declare function encodeServerResponse(id: string, rpc: string, body: unknown): SockaServerResponseFrame;
|
|
150
|
+
/** Builds a socka v1 server error frame. */
|
|
151
|
+
declare function encodeServerError(id: string, error: string): SockaServerErrorFrame;
|
|
152
|
+
/** Builds a socka v1 server event frame. */
|
|
153
|
+
declare function encodeServerEvent(event: string, body: unknown): SockaServerEventFrame;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* JSON text frames vs msgpack binary frames for the same socka v1 object graph.
|
|
157
|
+
* Matches {@link decodeSockaWire} after parse/unpack.
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
/** Wire encoding: UTF-8 JSON strings (default) or msgpack `ArrayBuffer` frames. */
|
|
161
|
+
type SockaWireFormat = "json" | "msgpack";
|
|
162
|
+
/**
|
|
163
|
+
* Encodes a socka frame for the wire. JSON returns a string; msgpack returns bytes
|
|
164
|
+
* suitable for `WebSocket.send`.
|
|
165
|
+
*/
|
|
166
|
+
declare function encodeSockaWire(frame: SockaWireFrame, format: SockaWireFormat, serializeJson?: (value: unknown) => string): string | Uint8Array;
|
|
167
|
+
/**
|
|
168
|
+
* Decodes a wire payload to a plain object before {@link decodeSockaWire}.
|
|
169
|
+
* Msgpack mode accepts `ArrayBuffer` or `Uint8Array` (e.g. from `msgpackr` / `WebSocket`).
|
|
170
|
+
*/
|
|
171
|
+
declare function parseWirePayload(data: string | ArrayBuffer | Uint8Array, format: SockaWireFormat, deserializeJson?: (raw: string) => unknown): unknown;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Single discriminated union for optional `reportError` on session config and
|
|
175
|
+
* `SockaSession` options: `kind` narrows context; `error` is what was thrown or rejected.
|
|
176
|
+
*/
|
|
177
|
+
type SockaReportError = {
|
|
178
|
+
kind: "clientEventListener";
|
|
179
|
+
eventName: string;
|
|
180
|
+
error: unknown;
|
|
181
|
+
} | {
|
|
182
|
+
kind: "clientEventValidation";
|
|
183
|
+
eventName: string;
|
|
184
|
+
error: unknown;
|
|
185
|
+
} | {
|
|
186
|
+
kind: "serverOnAttached";
|
|
187
|
+
error: unknown;
|
|
188
|
+
} | {
|
|
189
|
+
kind: "serverInboundMessage";
|
|
190
|
+
/** `hono` uses the same log line as the Hono adapters; others use attach-style. */
|
|
191
|
+
adapter: "attach" | "hono" | "bun";
|
|
192
|
+
error: unknown;
|
|
193
|
+
} | {
|
|
194
|
+
kind: "serverHandleClose";
|
|
195
|
+
error: unknown;
|
|
196
|
+
} | {
|
|
197
|
+
kind: "serverShutdown";
|
|
198
|
+
adapter: "attach" | "hono";
|
|
199
|
+
error: unknown;
|
|
200
|
+
};
|
|
201
|
+
/** Default `console.error` behavior; same messages as pre–`reportError` socka. */
|
|
202
|
+
declare function defaultReportError(event: SockaReportError): void;
|
|
203
|
+
/** Invokes the optional `reportError` callback when provided, otherwise `defaultReportError`. */
|
|
204
|
+
declare function reportSockaError(reportError: ((event: SockaReportError) => void) | undefined, event: SockaReportError): void;
|
|
205
|
+
|
|
206
|
+
export { type DecodedSockaWire as D, type InferSockaSend as I, RESERVED_SOCKA_PROCEDURE_NAMES as R, type SockaContract as S, type ValidateSockaCallKeys as V, type SockaContractConfig as a, type SockaWireFormat as b, type SockaProcedureDef as c, defineSocka as d, type InferSockaHandlers as e, type InferSockaPushHandlers as f, type InferSockaPushPayload as g, type ReservedSockaProcedureName as h, SOCKA_WIRE_VERSION as i, SockaWireError as j, type SockaClientRequestFrame as k, type SockaServerErrorFrame as l, type SockaServerEventFrame as m, type SockaServerResponseFrame as n, type SockaWireFrame as o, decodeSockaWire as p, encodeClientRequest as q, encodeServerResponse as r, encodeServerError as s, encodeServerEvent as t, encodeSockaWire as u, parseWirePayload as v, defaultReportError as w, reportSockaError as x, type SockaReportError as y };
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @firtoz/socka — documentation
|
|
2
|
+
|
|
3
|
+
In-repo guides for the **[Socka](../README.md)** library (**npm** [`@firtoz/socka`](https://www.npmjs.com/package/@firtoz/socka)). For Cursor agents, see also [`../skills/`](../skills/).
|
|
4
|
+
|
|
5
|
+
| Doc | Description |
|
|
6
|
+
|-----|-------------|
|
|
7
|
+
| [Getting started](./getting-started.md) | Quickest Bun path, other runtimes, install, wire-up, tic-tac-toe demos |
|
|
8
|
+
| [Peers](./peers.md) | Which peers to install per import path and why |
|
|
9
|
+
| [Multi-room](./multi-room.md) | Scopes, patterns per runtime, pitfalls |
|
|
10
|
+
| [Lifecycle](./lifecycle.md) | `onAttached`, inbound RPCs, `handleClose` ordering |
|
|
11
|
+
| [Server](./server.md) | Node `ws`, Bun, Hono, `attachSockaWebSocket`, `createData`, `session.data` |
|
|
12
|
+
| [Durable Objects](./durable-objects.md) | `SockaDoSession`, `SockaWebSocketDO`, routing, hibernation |
|
|
13
|
+
| [Client](./client.md) | `SockaSession`, React, deferred connect, reconnect |
|
|
14
|
+
| [Pushes](./events.md) | `emitPush` / `broadcastPush`, `session.subscribe`, ordering notes |
|
|
15
|
+
| [Reference](./reference.md) | Wire encoding (JSON/msgpack), frame kinds, server/client config tables, types, errors, imports |
|
|
16
|
+
| [Comparison](./comparison.md) | vs DIY WS, **socket.io**, **tRPC** |
|
|
17
|
+
|
|
18
|
+
**Roadmap** — [Deferred and post–v1 ideas](../roadmap.md).
|
package/docs/client.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Client
|
|
2
|
+
|
|
3
|
+
## Vanilla (`SockaSession`)
|
|
4
|
+
|
|
5
|
+
Outside React, construct **`SockaSession`** from **`@firtoz/socka/client`** with the same **`contract`** as the server and a **`url`** (or a prebuilt **`webSocket`**). **`wireFormat`** defaults to **`"json"`** and must match every server session that receives this connection.
|
|
6
|
+
|
|
7
|
+
| Option | Typical use |
|
|
8
|
+
|--------|-------------|
|
|
9
|
+
| **`contract`**, **`url`** / **`webSocket`** | Required pairing: open socket to your upgrade URL, or inject a socket for tests. |
|
|
10
|
+
| **`wireFormat`** | **`"json"`** (text frames) vs **`"msgpack"`** (binary); must match the **server** session config for this connection. |
|
|
11
|
+
| **`autoConnect: false`** | Defer opening until **`await session.connect()`** (or first **`send`**). |
|
|
12
|
+
| **`serializeJson` / `deserializeJson`** | Custom JSON handling for the **outer** frame in JSON mode (e.g. replacers). |
|
|
13
|
+
| **`onOpen` / `onClose` / `onError`** | Lifecycle and telemetry. |
|
|
14
|
+
| **`pushHandlers`** | Up-front subscriptions for contract **`pushes`** (same as **`session.subscribe.on`**). |
|
|
15
|
+
| **`reportError`** | Non-RPC client pipeline failures; defaults to **`console.error`**. |
|
|
16
|
+
|
|
17
|
+
Full list: **[Reference — Client configuration](./reference.md#client-configuration)**.
|
|
18
|
+
|
|
19
|
+
**Call names** — For literal `calls` objects, **`defineSocka`** rejects names that would make **`session.send`** Promise-like or clash with object shape (e.g. **`then`**, **`toString`**). If you use a wide **`Record<string, SockaProcedureDef>`**, TypeScript cannot apply that check; **`SockaSession`** still validates at construction (see **`RESERVED_SOCKA_PROCEDURE_NAMES`** in **`@firtoz/socka/core`**).
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { SockaSession } from "@firtoz/socka/client";
|
|
23
|
+
import { myContract } from "./contract";
|
|
24
|
+
|
|
25
|
+
const session = new SockaSession({ contract: myContract, url: "wss://example.com/ws" });
|
|
26
|
+
const rows = await session.send.list();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use **`SockaWebSocketClient`** directly if you need **`onResponse` / `onServerError` / `onEvent`** frame hooks without **`SockaSession`**’s typed **`send`** / **`subscribe`**; most apps use **`SockaSession`**.
|
|
30
|
+
|
|
31
|
+
## React
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { useSockaSession } from "@firtoz/socka/react";
|
|
35
|
+
import { myContract } from "./contract";
|
|
36
|
+
|
|
37
|
+
function App() {
|
|
38
|
+
const { ready, send } = useSockaSession(myContract, { url: "ws://..." }, []);
|
|
39
|
+
// After `ready`, call `send.list()` / `send.insert(...)` from effects or event handlers (not during render).
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Binary frames — set the same `wireFormat` on the server session
|
|
44
|
+
useSockaSession(myContract, { url: "wss://...", wireFormat: "msgpack" }, []);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### One WebSocket for the whole tree
|
|
48
|
+
|
|
49
|
+
If many components need **`send`**, avoid calling **`useSockaSession`** in each one (each call owns a connection). Mount a provider once and read the session from context:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { SockaSessionProvider, useSockaSessionContext } from "@firtoz/socka/react";
|
|
53
|
+
import { myContract } from "./contract";
|
|
54
|
+
|
|
55
|
+
function Layout({ roomId }: { roomId: string }) {
|
|
56
|
+
return (
|
|
57
|
+
<SockaSessionProvider
|
|
58
|
+
contract={myContract}
|
|
59
|
+
deps={[roomId]}
|
|
60
|
+
url={`wss://example.com/ws/${roomId}`}
|
|
61
|
+
>
|
|
62
|
+
<Child />
|
|
63
|
+
</SockaSessionProvider>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Child() {
|
|
68
|
+
const { ready, send } = useSockaSessionContext(myContract);
|
|
69
|
+
// ...
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use the **same `contract` reference** on the provider and in **`useSockaSessionContext`** (checked at runtime).
|
|
74
|
+
|
|
75
|
+
## Deferred WebSocket connect
|
|
76
|
+
|
|
77
|
+
Use **`autoConnect: false`** on **`SockaWebSocketClient`** / **`SockaSession`** when you want to open the socket later (e.g. after user action). Call **`await session.connect()`** or **`await client.connect()`** before calls; each **`send`** call also awaits connect when the socket is not open yet.
|
|
78
|
+
|
|
79
|
+
## Client lifecycle
|
|
80
|
+
|
|
81
|
+
Treat each **`SockaSession`** / **`SockaWebSocketClient`** as bound to **one** underlying **`WebSocket`**. When the socket closes, pending calls reject and should not be retried on the same instance. For reconnect or room changes, construct a **new** client (in React, remount **`useSockaSession`** / **`SockaSessionProvider`** when **`url`** or identity **`deps`** change). Use **`ready`** / **`waitForOpen()`** before assuming the connection is usable; use **`onClose`** / **`onError`** (or **`reportError`**) for backoff, toasts, or logging.
|
|
82
|
+
|
|
83
|
+
## Pushes
|
|
84
|
+
|
|
85
|
+
Server push and client subscriptions are covered in [Pushes](./events.md).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Compared to hand-rolled WebSocket RPC (and common alternatives)
|
|
2
|
+
|
|
3
|
+
Most apps model messages as large discriminated unions (`type` + `id`), validate twice (once on the wire, once in the handler), and maintain a **pending `Map<string, Deferred>`** for every RPC. That works, but types drift between client and server, correlation IDs are easy to get wrong, and pushing **server events** becomes a second, parallel protocol.
|
|
4
|
+
|
|
5
|
+
## socka vs typical custom protocol
|
|
6
|
+
|
|
7
|
+
| | Typical custom protocol | socka |
|
|
8
|
+
|---|------------------------|--------|
|
|
9
|
+
| **Strengths** | Total control; any framing; no shared spec | One contract drives **client + server** types; **Standard Schema** everywhere; socka v1 **envelopes** + correlation built in; inferred **`rpc`** / **`handlers`** |
|
|
10
|
+
| **Tradeoffs** | Boilerplate, duplicated schemas, `Promise<unknown>` unless you invest | Opinionated **socka v1** frame shape; **named procedures** (not a free-form message bus) |
|
|
11
|
+
|
|
12
|
+
## socket.io
|
|
13
|
+
|
|
14
|
+
| | socket.io | socka |
|
|
15
|
+
|---|-----------|--------|
|
|
16
|
+
| **Model** | Named events + optional ack callbacks; rooms/namespaces are first-class | **Schema-first RPC**: one `defineSocka` contract drives typed **`session.send.*`** and **`handlers`**; optional typed **pushes** |
|
|
17
|
+
| **Typing** | Largely **string-based** event names; no built-in shared input/output schema layer across client and server | **Standard Schema v1** on every procedure; **no duplicate schema** layer |
|
|
18
|
+
| **When socket.io wins** | Broad ecosystem, Redis adapters, fallbacks, “emit anything” ergonomics | **When you want** correlated request/response + typed contracts in TypeScript on **both** ends |
|
|
19
|
+
|
|
20
|
+
## tRPC
|
|
21
|
+
|
|
22
|
+
| | tRPC | socka |
|
|
23
|
+
|---|------|--------|
|
|
24
|
+
| **Transport** | **HTTP-first** (batching, subscriptions via adapters); WebSocket is **not** the core story | **WebSocket-first** RPC: socka v1 frames on the wire |
|
|
25
|
+
| **Contract** | **Router** + procedures; great for HTTP/JSON | **Single shared contract** (`defineSocka`) for **WS** frames (JSON or msgpack) |
|
|
26
|
+
| **When tRPC wins** | Same-process or HTTP-first APIs, huge React ecosystem | **When the real transport is WebSocket** (including Durable Objects, Bun, Hono, Node `ws`) and you want **one schema** for the socket |
|
|
27
|
+
|
|
28
|
+
## When a custom protocol still wins
|
|
29
|
+
|
|
30
|
+
- You must match **legacy bytes**, a **binary codec** outside JSON/msgpack, or a **non-JSON** framing already deployed in the field.
|
|
31
|
+
- You need a **generic pub/sub** bus where message types are not known at compile time.
|
|
32
|
+
- You are **not** using TypeScript on both ends and do not benefit from shared inference.
|
|
33
|
+
|
|
34
|
+
## When socka is a good fit
|
|
35
|
+
|
|
36
|
+
You want **schema-first WebSocket RPC** with **correlated request/response** and optional **typed server push** from a **single contract module**—and you are fine with **socka v1** frames (see **[Reference](./reference.md)**) so you can swap runtimes (Bun, Hono, Durable Objects, Node **`ws`**) behind the same procedures.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Durable Objects
|
|
2
|
+
|
|
3
|
+
On Cloudflare **Durable Objects**, socka splits into two pieces:
|
|
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`**.
|
|
7
|
+
|
|
8
|
+
You still define one **`defineSocka`** contract; this page is only about hosting it on a **Durable Object**.
|
|
9
|
+
|
|
10
|
+
## Cloudflare Worker checklist
|
|
11
|
+
|
|
12
|
+
This is the **Cloudflare** side (bindings, Wrangler, generated types)—not socka-specific, but you need it before **`SockaWebSocketDO`** can run.
|
|
13
|
+
|
|
14
|
+
1. **Wrangler** — `wrangler.jsonc` / `wrangler.toml` with a **Durable Object** binding and a **migration** for the DO class (see Cloudflare docs and the runnable **[tic-tac-toe-do example](https://github.com/firtoz/fullstack-toolkit/tree/main/examples/tic-tac-toe-do)** in this repo).
|
|
15
|
+
2. **Typed `Env`** — After you change bindings or `wrangler` config, regenerate env types (**`bun run typegen`** / **`cf-typegen`** via [`@firtoz/worker-helper`](https://github.com/firtoz/fullstack-toolkit/tree/main/packages/worker-helper)); **do not hand-edit** `worker-configuration.d.ts`. Workflow reference: [Cloudflare / Wrangler typegen skill](https://github.com/firtoz/fullstack-toolkit/blob/main/.cursor/skills/cloudflare-wrangler-typegen/SKILL.md) in this monorepo.
|
|
16
|
+
3. **Run locally** — `wrangler dev` (optionally pin a port—e.g. **3463** in the tic-tac-toe example so it does not clash with other apps).
|
|
17
|
+
4. **Peers** — **`@firtoz/socka/do`** needs **`@firtoz/websocket-do`**, **`hono`**, **`@cloudflare/workers-types`**—see **[Peers](./peers.md)**.
|
|
18
|
+
|
|
19
|
+
### Wire format
|
|
20
|
+
|
|
21
|
+
**`wireFormat`** defaults to **`"json"`** (text frames). Use **`"msgpack"`** only if the client also uses **`msgpack`**. Mismatched **`wireFormat`** between client and session config will fail to decode. Details: **[Reference — Wire encoding](./reference.md#wire-encoding-json-and-msgpack)**.
|
|
22
|
+
|
|
23
|
+
## `SockaDoSession`
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { SockaDoSession } from "@firtoz/socka/do";
|
|
27
|
+
import { myContract } from "./contract";
|
|
28
|
+
|
|
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
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Handler types use **`InferSockaHandlers<typeof myContract, SockaDoSession<typeof myContract, …>>`**. Throw **`SockaError`** for expected domain failures (bad move, permission denied) so the client receives a structured **`serverError`** frame; see **[Reference](./reference.md)** for other failure paths.
|
|
43
|
+
|
|
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
|
|
47
|
+
|
|
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).
|
|
49
|
+
|
|
50
|
+
Minimal shape (full game example: [`examples/tic-tac-toe-do`](../../../examples/tic-tac-toe-do/src/do.ts)):
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
export class MyDO extends SockaWebSocketDO<MySession, Env> {
|
|
54
|
+
app = this.getBaseApp();
|
|
55
|
+
|
|
56
|
+
constructor(ctx: DurableObjectState, env: Env) {
|
|
57
|
+
super(ctx, env, {
|
|
58
|
+
createSockaSession: (_ctx, websocket) =>
|
|
59
|
+
new MySession(websocket, this.sessions /*, … */),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**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
|
+
|
|
67
|
+
## Hibernation and `session.data`
|
|
68
|
+
|
|
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.
|
|
70
|
+
|
|
71
|
+
## See also
|
|
72
|
+
|
|
73
|
+
- **[Lifecycle](./lifecycle.md)** — **`onAttached`** and **`handleClose`** ordering.
|
|
74
|
+
- **[Server](./server.md)** — **`attachSockaWebSocket`**, Bun, and Hono when the socket is **not** on a Durable Object.
|
package/docs/events.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Pushes (server-initiated)
|
|
2
|
+
|
|
3
|
+
Contracts can declare **`pushes`** alongside **`calls`**. Each push name maps to a **Standard Schema** payload. The server validates payloads **before** sending; the client decodes and validates **before** your listeners run—so **`InferSockaPushPayload`** stays honest end to end.
|
|
4
|
+
|
|
5
|
+
**Wire format** — Push uses the **`serverEvent`** logical frame type. It is encoded with the session’s **`wireFormat`** (**JSON text** or **msgpack binary**) like RPC traffic. Switching to msgpack affects **calls and pushes** together—there is no separate “push encoding.”
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
export const myContract = defineSocka({
|
|
9
|
+
calls: { /* ... */ },
|
|
10
|
+
pushes: {
|
|
11
|
+
itemsChanged: z.array(messageSchema),
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Server: emit and broadcast
|
|
17
|
+
|
|
18
|
+
- **`await session.emitPush("itemsChanged", payload)`** — send one **validated** push to **this** socket (typical for private notifications).
|
|
19
|
+
- **`await session.broadcastPush("itemsChanged", payload, excludeSelf?)`** — send to **every session in the same **`sessions`** map**, optionally skipping the caller.
|
|
20
|
+
|
|
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.
|
|
22
|
+
|
|
23
|
+
**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
|
+
|
|
25
|
+
## Client: `session.subscribe`
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// Optional `pushHandlers` on SockaSession / useSockaSession (same as session.subscribe.on(...))
|
|
29
|
+
// Or subscribe imperatively:
|
|
30
|
+
session.subscribe.on("itemsChanged", (payload) => { /* ... */ });
|
|
31
|
+
session.subscribe.once("itemsChanged", (payload) => { /* ... */ });
|
|
32
|
+
const payload = await session.subscribe.waitForPush("itemsChanged", {
|
|
33
|
+
timeoutMs: 5000,
|
|
34
|
+
signal: ac.signal,
|
|
35
|
+
predicate: (p) => p.length > 0,
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
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.
|
|
40
|
+
|
|
41
|
+
## Who receives `broadcastPush`?
|
|
42
|
+
|
|
43
|
+
Only sessions in the **`sessions`** map you passed into that session’s constructor.
|
|
44
|
+
|
|
45
|
+
- **Bun / Hono multi-room** — **`resolveScope`** (or per-room routes) must put each socket in the map for the right room—see **[Multi-room](./multi-room.md)**.
|
|
46
|
+
- **Durable Objects** — Everyone connected to **that** Durable Object instance shares one map—see **[Durable Objects](./durable-objects.md)**.
|
|
47
|
+
|
|
48
|
+
See also [Client](./client.md) and [Reference](./reference.md) for observability when push validation fails on the client.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
The **[README](../README.md)** opens with a **complete Bun example**: shared contract, **`createSockaBunWebSocketHandlers`**, and **`SockaSession`**. Start there if you want something runnable in one minute.
|
|
4
|
+
|
|
5
|
+
## Quickest path (Bun)
|
|
6
|
+
|
|
7
|
+
Save three files next to each other, then run **`bun run server.ts`**. Point a client at **`ws://localhost:3450/ws`** (same contract as the README).
|
|
8
|
+
|
|
9
|
+
**`contract.ts`**
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { defineSocka } from "@firtoz/socka/core";
|
|
13
|
+
import * as z from "zod";
|
|
14
|
+
|
|
15
|
+
export const myContract = defineSocka({
|
|
16
|
+
calls: {
|
|
17
|
+
echo: {
|
|
18
|
+
input: z.object({ text: z.string() }),
|
|
19
|
+
output: z.object({ text: z.string() }),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**`server.ts`**
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createSockaBunWebSocketHandlers } from "@firtoz/socka/bun";
|
|
29
|
+
import { myContract } from "./contract";
|
|
30
|
+
|
|
31
|
+
const { websocket } = createSockaBunWebSocketHandlers({
|
|
32
|
+
contract: myContract,
|
|
33
|
+
handlers: {
|
|
34
|
+
echo: async (input) => ({ text: input.text }),
|
|
35
|
+
},
|
|
36
|
+
handleClose: async () => {},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
Bun.serve({
|
|
40
|
+
port: 3450,
|
|
41
|
+
fetch(req, server) {
|
|
42
|
+
if (new URL(req.url).pathname === "/ws") {
|
|
43
|
+
if (server.upgrade(req)) return undefined;
|
|
44
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
45
|
+
}
|
|
46
|
+
return new Response("OK");
|
|
47
|
+
},
|
|
48
|
+
websocket,
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**`client.ts`**
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { SockaSession } from "@firtoz/socka/client";
|
|
56
|
+
import { myContract } from "./contract";
|
|
57
|
+
|
|
58
|
+
const session = new SockaSession({
|
|
59
|
+
contract: myContract,
|
|
60
|
+
url: "ws://localhost:3450/ws",
|
|
61
|
+
});
|
|
62
|
+
const { text } = await session.send.echo({ text: "hello" });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## What socka is
|
|
66
|
+
|
|
67
|
+
**Socka** is the library; the **npm package name is [`@firtoz/socka`](https://www.npmjs.com/package/@firtoz/socka)** (scoped). It is **schema-first WebSocket RPC**: one **`defineSocka`** contract gives you typed **`session.send.*`** in the browser and **`handlers`** on the server, with Socka **v1** frames on the wire.
|
|
68
|
+
|
|
69
|
+
For frame shapes and options, see **[Reference](./reference.md)**.
|
|
70
|
+
|
|
71
|
+
## Other runtimes
|
|
72
|
+
|
|
73
|
+
Pick **how** the socket is upgraded, then use the matching subpath:
|
|
74
|
+
|
|
75
|
+
| You want to… | Read this first | Import path |
|
|
76
|
+
|--------------|-----------------|-------------|
|
|
77
|
+
| **Node** + **`ws`**, or any standard **`WebSocket`** after upgrade | **[Server](./server.md)** — **`attachSockaWebSocket`** | `@firtoz/socka/server` |
|
|
78
|
+
| **Bun** **`Bun.serve`** / **`ServerWebSocket`** | **[Server](./server.md)** — **`@firtoz/socka/bun`** | `@firtoz/socka/bun` |
|
|
79
|
+
| **Hono** on **Node** (`@hono/node-ws`) | **[Server](./server.md)** — **`sockaHonoNodeWs`** | `@firtoz/socka/hono` |
|
|
80
|
+
| **Hono** on **Cloudflare Workers** | **[Server](./server.md)** — **`sockaHonoCloudflare`** | `@firtoz/socka/hono/cloudflare` |
|
|
81
|
+
| **Cloudflare Durable Objects** | **[Durable Objects](./durable-objects.md)** | `@firtoz/socka/do` |
|
|
82
|
+
|
|
83
|
+
**Multiple rooms or scopes?** See **[Multi-room](./multi-room.md)**.
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm install @firtoz/socka
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Add **only** the peers for the subpaths you import—**[Peers](./peers.md)**.
|
|
92
|
+
|
|
93
|
+
## Shared contract
|
|
94
|
+
|
|
95
|
+
Use one module for client and server (same as the README):
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { defineSocka } from "@firtoz/socka/core";
|
|
99
|
+
import * as z from "zod";
|
|
100
|
+
|
|
101
|
+
export const myContract = defineSocka({
|
|
102
|
+
calls: {
|
|
103
|
+
echo: {
|
|
104
|
+
input: z.object({ text: z.string() }),
|
|
105
|
+
output: z.object({ text: z.string() }),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For richer examples (list/insert, optional inputs), see **[Server](./server.md)** and the tic-tac-toe apps under `examples/`.
|
|
112
|
+
|
|
113
|
+
## Wire the server, then the client
|
|
114
|
+
|
|
115
|
+
You already have the **client** shape (`SockaSession` with the same contract). Now:
|
|
116
|
+
|
|
117
|
+
1. **Server** — Open the guide from **Other runtimes** and implement **`handlers`** + **`handleClose`** for **`myContract`**. Use **`SockaWebSocketSession`** / **`attachSockaWebSocket`** (Node/Bun/Hono) or **`SockaDoSession`** / **`SockaWebSocketDO`** (Durable Objects).
|
|
118
|
+
2. **Client** — Keep **`SockaSession`** (or **`useSockaSession`** / **`SockaSessionProvider`**—**[Client](./client.md)**) with the **same** **`wireFormat`** as the server.
|
|
119
|
+
|
|
120
|
+
### Wire format (short)
|
|
121
|
+
|
|
122
|
+
Default is **JSON text** frames. **`wireFormat: "msgpack"`** must match on **both** sides. Details: **[Reference — Wire encoding](./reference.md#wire-encoding-json-and-msgpack)**.
|
|
123
|
+
|
|
124
|
+
## Run a full-stack demo
|
|
125
|
+
|
|
126
|
+
Same **tic-tac-toe** contract and game logic, three servers in this repo:
|
|
127
|
+
|
|
128
|
+
| Stack | Folder | Port |
|
|
129
|
+
|-------|--------|------|
|
|
130
|
+
| **Bun** | [`tic-tac-toe-bun`](../../../examples/tic-tac-toe-bun) | **3461** |
|
|
131
|
+
| **Hono + Node** | [`tic-tac-toe-hono`](../../../examples/tic-tac-toe-hono) | **3462** |
|
|
132
|
+
| **Durable Objects** | [`tic-tac-toe-do`](../../../examples/tic-tac-toe-do) | **3463** |
|
|
133
|
+
|
|
134
|
+
From the folder: **`bun run dev`**. The DO example uses **`wrangler dev`**.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
Next: [Peers](./peers.md) · [Server](./server.md) · [Durable Objects](./durable-objects.md) · [Client](./client.md) · [Reference](./reference.md)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Lifecycle
|
|
2
|
+
|
|
3
|
+
Join, message, and **leave** ordering for socka sessions—whether you use **`@firtoz/socka/server`**, **`@firtoz/socka/bun`**, **`@firtoz/socka/hono`**, or **`@firtoz/socka/do`**.
|
|
4
|
+
|
|
5
|
+
## Registration and `onAttached`
|
|
6
|
+
|
|
7
|
+
1. The adapter accepts or upgrades a **`WebSocket`** and constructs a session (**`SockaWebSocketSession`** or **`SockaDoSession`**).
|
|
8
|
+
2. The session is **registered** in the shared **`sessions`** map (the map **`broadcastContractEvent`** uses).
|
|
9
|
+
3. On the next microtask, **`onAttached`** runs (if you provided it). Other sessions in the map can see this socket—use this for join broadcasts, not the constructor.
|
|
10
|
+
|
|
11
|
+
If **`onAttached`** throws or returns a rejected promise, the failure is reported via **`reportError`** (or **`console.error`** by default) with kind **`serverOnAttached`**.
|
|
12
|
+
|
|
13
|
+
## Inbound RPCs
|
|
14
|
+
|
|
15
|
+
While the socket is open, inbound data is decoded ( **`handleRawMessage`**, **`dispatchSockaInboundMessage`**, or Bun/Hono wrappers), inputs are validated, and **`handlers[procedure]`** runs. Handler exceptions → **`onHandlerError`**; bad wire payloads → **`onValidationError`** before your handler—see **[Reference](./reference.md)**.
|
|
16
|
+
|
|
17
|
+
## Close and `handleClose`
|
|
18
|
+
|
|
19
|
+
When the transport closes:
|
|
20
|
+
|
|
21
|
+
1. The adapter calls **`await session.invokeHandleClose()`**, which runs **your** **`handleClose(session)`**.
|
|
22
|
+
2. **Until that finishes, the session remains in **`sessions`**—so peer iteration and **`broadcastContractEvent`** can still see the closing peer (e.g. “last player left”).
|
|
23
|
+
3. Then the adapter removes the socket from the map.
|
|
24
|
+
|
|
25
|
+
**`SockaDoSession`** delegates teardown through **`@firtoz/websocket-do`**; see **[Durable Objects](./durable-objects.md)** for **`BaseSession`** details.
|
|
26
|
+
|
|
27
|
+
## Hibernation (Durable Objects only)
|
|
28
|
+
|
|
29
|
+
If you use **`SockaDoSession`**, mutating **`session.data`** may require **`await session.update()`** so hibernation attachments stay consistent. See **[Durable Objects](./durable-objects.md)**.
|
|
30
|
+
|
|
31
|
+
See also [Multi-room](./multi-room.md).
|