@firtoz/socka 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +195 -42
  2. package/dist/SockaWebSocketSession-B1w7RAid.d.ts +209 -0
  3. package/dist/bun/index.d.ts +30 -5
  4. package/dist/bun/index.js +28 -5
  5. package/dist/bun/index.js.map +1 -1
  6. package/dist/{chunk-MZCQHJXY.js → chunk-IFIGKR3W.js} +45 -8
  7. package/dist/chunk-IFIGKR3W.js.map +1 -0
  8. package/dist/{chunk-45D4T232.js → chunk-LVVCHLNW.js} +74 -9
  9. package/dist/chunk-LVVCHLNW.js.map +1 -0
  10. package/dist/{chunk-AM7PB26G.js → chunk-P3JEEOJL.js} +192 -10
  11. package/dist/chunk-P3JEEOJL.js.map +1 -0
  12. package/dist/chunk-QGURL3DJ.js +8 -0
  13. package/dist/chunk-QGURL3DJ.js.map +1 -0
  14. package/dist/client/index.d.ts +59 -3
  15. package/dist/client/index.js +2 -2
  16. package/dist/core/index.d.ts +2 -21
  17. package/dist/core/index.js +1 -1
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/do/index.d.ts +20 -2
  20. package/dist/do/index.js +36 -2
  21. package/dist/do/index.js.map +1 -1
  22. package/dist/hono/cloudflare-workers.d.ts +4 -4
  23. package/dist/hono/cloudflare-workers.js +4 -3
  24. package/dist/hono/cloudflare-workers.js.map +1 -1
  25. package/dist/hono/index.d.ts +22 -6
  26. package/dist/hono/index.js +5 -3
  27. package/dist/hono/index.js.map +1 -1
  28. package/dist/react/index.d.ts +43 -4
  29. package/dist/react/index.js +103 -9
  30. package/dist/react/index.js.map +1 -1
  31. package/dist/server/index.d.ts +18 -5
  32. package/dist/server/index.js +24 -4
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/{socka-report-error-DzFI2Tr7.d.ts → socka-report-error-CXwpAUgl.d.ts} +80 -8
  35. package/dist/test/index.d.ts +11 -0
  36. package/dist/test/index.js +84 -0
  37. package/dist/test/index.js.map +1 -0
  38. package/docs/README.md +16 -7
  39. package/docs/auth.md +27 -0
  40. package/docs/backpressure.md +16 -0
  41. package/docs/client.md +48 -3
  42. package/docs/comparison.md +2 -2
  43. package/docs/durable-objects.md +3 -3
  44. package/docs/getting-started.md +143 -84
  45. package/docs/history.md +26 -0
  46. package/docs/internals.md +56 -0
  47. package/docs/lifecycle.md +3 -3
  48. package/docs/multi-room.md +10 -8
  49. package/docs/peers.md +11 -7
  50. package/docs/presence.md +43 -0
  51. package/docs/{events.md → pushes.md} +1 -1
  52. package/docs/recipes.md +77 -0
  53. package/docs/reconnection.md +44 -0
  54. package/docs/reference.md +27 -32
  55. package/docs/server.md +19 -3
  56. package/docs/testing.md +20 -0
  57. package/docs/wire-format.md +29 -0
  58. package/examples/minimal-socka.ts +56 -3
  59. package/package.json +14 -10
  60. package/roadmap.md +2 -2
  61. package/skills/socka/core-rpc/SKILL.md +2 -2
  62. package/skills/socka/do-session/SKILL.md +2 -2
  63. package/skills/socka/standard-schema/SKILL.md +1 -1
  64. package/dist/SockaWebSocketSession-Bru8yFcK.d.ts +0 -107
  65. package/dist/chunk-45D4T232.js.map +0 -1
  66. package/dist/chunk-AM7PB26G.js.map +0 -1
  67. package/dist/chunk-MZCQHJXY.js.map +0 -1
@@ -0,0 +1,77 @@
1
+ # Recipes (copy-paste)
2
+
3
+ Minimal wiring per runtime. Each assumes a **`defineSocka`** contract and matching client **`SockaSession`**. Full apps: **[chatroom-bun](../../examples/chatroom-bun)** (Bun SQLite), **[chatroom-hono](../../examples/chatroom-hono)** (Hono JSON), **[chatroom-do](../../examples/chatroom-do)** (Durable Object SQLite).
4
+
5
+ ## Multi-room Bun (`Bun.serve`)
6
+
7
+ Use **`createSockaRoomRegistry`** + **`createSockaBunWebSocketHandlers({ resolveScope })`** — see **[chatroom-bun](../../examples/chatroom-bun/src/server.ts)**.
8
+
9
+ ```ts
10
+ import { createSockaBunWebSocketHandlers, sockaBunUpgrade } from "@firtoz/socka/bun";
11
+ import { createSockaRoomRegistry } from "@firtoz/socka/server";
12
+
13
+ const rooms = createSockaRoomRegistry((roomId, _sessionMap) => ({
14
+ contract: myContract,
15
+ createData: (init) => { /* parse init.request */ return { roomId: "…" }; },
16
+ handlers: { /* … */ },
17
+ handleClose: async () => {},
18
+ }));
19
+
20
+ const { websocket } = createSockaBunWebSocketHandlers({
21
+ resolveScope(ws) {
22
+ const room = rooms.get(ws.data.roomId);
23
+ return { sessionMap: room.sessionMap, config: room.config };
24
+ },
25
+ });
26
+
27
+ Bun.serve({
28
+ fetch(req, srv) {
29
+ if (req.url.includes("/ws/")) {
30
+ const roomId = "…";
31
+ if (sockaBunUpgrade(srv, req, { roomId })) return undefined;
32
+ return new Response("upgrade failed", { status: 400 });
33
+ }
34
+ return new Response("OK");
35
+ },
36
+ websocket,
37
+ });
38
+ ```
39
+
40
+ ## Single-room Bun
41
+
42
+ One **`sessionMap`** and one **`config`** — use **`createSockaBunWebSocketHandlers(myConfig)`** without **`resolveScope`**.
43
+
44
+ ## Hono on Node (`@hono/node-ws`)
45
+
46
+ **`createNodeWebSocket`** + **`sockaHonoNodeWs`** — see **[chatroom-hono](../../examples/chatroom-hono/src/server.ts)**.
47
+
48
+ ```ts
49
+ import { sockaHonoNodeWs } from "@firtoz/socka/hono";
50
+ import { createSockaRoomRegistry } from "@firtoz/socka/server";
51
+
52
+ const rooms = createSockaRoomRegistry((roomId) => ({ /* config */ }));
53
+
54
+ app.get("/ws/:roomId", upgradeWebSocket((c) => {
55
+ const room = rooms.get(c.req.param("roomId") ?? "default");
56
+ return sockaHonoNodeWs(room.config, { sessions: room.sessionMap })(c);
57
+ }));
58
+ ```
59
+
60
+ ## Hono on Cloudflare Workers
61
+
62
+ **`upgradeWebSocket`** from **`hono/cloudflare-workers`** + **`sockaHonoCloudflare`** — session often starts on first **`onMessage`**; see **[Server](./server.md#firtoz-socka-hono-cloudflare-workers)**.
63
+
64
+ ## Durable Objects
65
+
66
+ Subclass **`SockaWebSocketDO`**, implement **`createSockaSession`** returning **`SockaDoSession`** — see **[Durable Objects](./durable-objects.md)** and **[chatroom-do](../../examples/chatroom-do/src/do.ts)**.
67
+
68
+ ## Client (browser)
69
+
70
+ ```ts
71
+ import { SockaSession } from "@firtoz/socka/client";
72
+
73
+ const session = new SockaSession({ contract: myContract, url: "wss://…/ws/room" });
74
+ await session.send.list();
75
+ ```
76
+
77
+ React: **`useSockaSession`** / **`useSocka`** / **`useSockaPresence`** — see **[Client](./client.md)**.
@@ -0,0 +1,44 @@
1
+ # Reconnection
2
+
3
+ **`SockaWebSocketClient`** and **`SockaSession`** share the same reconnect options (sessions forward them to the client).
4
+
5
+ ## Defaults
6
+
7
+ - **`url` mode** — Reconnect is **on** with exponential backoff + jitter, **infinite** attempts, and **pause while `document.hidden`** (when **`document`** exists).
8
+ - **Injected `webSocket`** — Reconnect is **off** by default (typical for tests). Pass an explicit **`reconnect`** object to enable it for a mocked socket.
9
+
10
+ Set **`reconnect: false`** to disable entirely.
11
+
12
+ ## Options (`SockaReconnectConfig`)
13
+
14
+ | Field | Default | Notes |
15
+ |-------|---------|--------|
16
+ | **`initialDelayMs`** | `1000` | First delay after a close that triggers reconnect. |
17
+ | **`maxDelayMs`** | `30000` | Cap for the backoff curve. |
18
+ | **`jitter`** | `0.2` | Fraction of the delay to randomize (0–1). |
19
+ | **`maxAttempts`** | *omitted* | Omit for **infinite** attempts. |
20
+ | **`pauseWhenHidden`** | `true` | Wait until the tab is visible again before reconnecting (browser). |
21
+
22
+ Delay grows exponentially from **`initialDelayMs`** up to **`maxDelayMs`**, then jitter is applied.
23
+
24
+ ## Lifecycle callbacks
25
+
26
+ | Callback | When |
27
+ |----------|------|
28
+ | **`onReconnecting`** | Before a delayed attempt is scheduled (**`attempt`**, **`delayMs`**). |
29
+ | **`onReconnected`** | After a **new** socket reaches **`open`** following a reconnect (**`attempt`**). |
30
+
31
+ Use **`onReconnected`** to **re-hydrate** client state: call **`listHistory`**, **`listPresence`**, or your own snapshot RPCs — in-memory UI state may be stale across a new socket.
32
+
33
+ ## Stopping the loop
34
+
35
+ **`session.close()`** / **`client.close()`** performs a **manual** close: the client sets an internal flag so **abnormal-close reconnect** does not run afterward. Use this for user-driven “disconnect” or cleanup.
36
+
37
+ ## Pending RPCs
38
+
39
+ Pending calls at disconnect time **reject** (same as without reconnect). There is no built-in queue across disconnects; re-issue work after **`onReconnected`** or **`waitForOpen()`** if needed.
40
+
41
+ ## See also
42
+
43
+ - **[Client](./client.md)** — lifecycle and React remounting when changing URL/room.
44
+ - **[Testing](./testing.md)** — injected **`WebSocket`** fakes in tests.
package/docs/reference.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Reference
2
2
 
3
+ User-facing **API** and **configuration**. For **wire protocol details** (frame kinds, transport layers, `decodeSockaWire`), see **[Internals](./internals.md)**.
4
+
3
5
  ## Type inference
4
6
 
5
7
  ```ts
@@ -14,52 +16,39 @@ type Handlers = InferSockaHandlers<
14
16
 
15
17
  **`InferSockaSend`** — Call names become methods on **`session.send`**; inputs/outputs follow the contract. **`InferSockaHandlers`** — Server handler arity matches **`calls`** (with or without `input`).
16
18
 
19
+ ### Optional output (fire-and-forget)
20
+
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.
22
+
17
23
  ## Errors and observability
18
24
 
19
25
  | Concern | Hook |
20
26
  |--------|------|
21
27
  | Exceptions inside **RPC handlers** | `onHandlerError` on `SockaWebSocketSessionConfig` / `SockaDoSessionConfig` |
22
28
  | Invalid **inbound wire** payloads (before your handler runs) | `onValidationError` on the same config |
23
- | Everything else ( **`onAttached`** failures, adapter I/O, **client** push listener throws, **client** push payload validation) | Optional **`reportError(event)`** on `SockaWebSocketSessionConfig`, `SockaDoSessionConfig`, or `SockaSession` / `useSockaSession` options |
29
+ | Everything else ( **`onAttached`** failures, adapter I/O, **client** push listener throws, **client** push payload validation, **fire-and-forget RPC errors** without a pending promise) | Optional **`reportError(event)`** on `SockaWebSocketSessionConfig`, `SockaDoSessionConfig`, or `SockaSession` / `useSockaSession` options |
24
30
 
25
- Each **`event`** is **`SockaReportError`**: one discriminated union (`kind` narrows context; **`error`** is the thrown/rejected value; **`eventName`** / **`adapter`** where relevant). Export: **`@firtoz/socka/core`** (`defaultReportError`, `reportSockaError`). If you omit **`reportError`**, socka uses **`console.error`** with the same **`socka:`**-prefixed messages as before.
26
-
27
- ## TypeScript: Durable Objects and session types
28
-
29
- `@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.
31
+ Each **`event`** is **`SockaReportError`**: one discriminated union (`kind` narrows context; **`error`** is the thrown/rejected value where applicable; **`eventName`** / **`adapter`** where relevant). Kinds include **`clientFireAndForgetRpcError`**, **`clientOrphanServerError`**, and **`clientUnexpectedServerResponse`** for client-side RPC edge cases. Export: **`@firtoz/socka/core`** (`defaultReportError`, `reportSockaError`). If you omit **`reportError`**, socka uses **`console.error`** with the same **`socka:`**-prefixed messages as before.
30
32
 
31
33
  ## Wire encoding: JSON and msgpack
32
34
 
33
- Socka has two layers: **transport encoding** (how each WebSocket frame is serialized) and **logical frames** (the socka v1 object inside). Both sides must agree on **`wireFormat`** or decoding fails immediately (wrong frame type or parse errors).
34
-
35
- | `wireFormat` | WebSocket frame | Bytes on the wire |
36
- |--------------|-----------------|-------------------|
37
- | **`"json"`** (default) | **Text** — `send(string)` | UTF-8 JSON of the **whole** envelope (one JSON object per frame). Uses **`serializeJson`** / **`deserializeJson`** when set, otherwise `JSON.stringify` / `JSON.parse`. |
38
- | **`"msgpack"`** | **Binary** — `send(ArrayBuffer)` | [msgpack](https://msgpack.org/) of the same envelope object graph. In the browser, **`SockaWebSocketClient`** sets **`binaryType = "arraybuffer"`** so binary frames decode correctly. |
35
+ - Set **`wireFormat`** to the **same value** on the **client** and on **every server session** for that connection. Default is **`"json"`** (UTF-8 **text** WebSocket frames).
36
+ - **`"msgpack"`** uses **binary** frames; use it only when **both** ends opt in.
37
+ - **RPCs and typed pushes** share one encoding there is no separate “push encoding.”
39
38
 
40
- **Rules**
39
+ Tradeoffs (bundle size, CPU, debuggability): **[Wire format](./wire-format.md)**.
41
40
 
42
- - Set **`wireFormat`** to the **same value** on the **client** (`SockaSession` / `SockaWebSocketClient` / `useSockaSession` options) and on **every server session** that talks to that client (`SockaWebSocketSessionConfig`, `SockaDoSessionConfig`, and the `config` passed to **`createSockaBunWebSocketHandlers`**, **`sockaHonoNodeWs`**, **`sockaHonoCloudflare`**, etc.).
43
- - **RPCs and contract pushes** share one encoding: `clientRequest` / `serverResponse` / `serverError` / `serverEvent` are all wrapped the same way.
44
- - If you use **`dispatchSockaInboundMessage`** manually, pass the same **`wireFormat`** as the peer used to **encode** the frame.
45
- - Optional **`serializeJson`** / **`deserializeJson`** on client or server config only affect **JSON mode** (the outer envelope). Call **`body`** and push **`body`** values are still whatever your **Standard Schema** accepts after JSON/msgpack decode.
41
+ Tables, logical frame kinds (`clientRequest`, `serverResponse`, ), and **`dispatchSockaInboundMessage`** details: **[Internals](./internals.md)**.
46
42
 
47
- ## Logical frames (socka v1)
43
+ ## RPC handler errors
48
44
 
49
- Every decoded payload is one logical socka **v1** object. **`decodeSockaWire`** checks shape after `JSON.parse` (text) or msgpack unpack (binary).
45
+ Throw **`SockaError`** from handlers when you control the **message** sent on the **`serverError`** frame. Pass optional **`code`** and **`data`** so clients can branch without parsing **`message`**:
50
46
 
51
- | Kind | Role |
52
- |------|------|
53
- | `clientRequest` | Client → server RPC (`id`, `rpc`, `body`) |
54
- | `serverResponse` | Success reply (correlated by `id`) |
55
- | `serverError` | Correlated failure (`id`, `error` message string) |
56
- | `serverEvent` | Server push (`event`, `body`) — **not** tied to an RPC `id` |
57
-
58
- Clients generate **`id`** strings per request; servers echo them on **`serverResponse`** and **`serverError`** so concurrent RPCs never mix results. **`serverEvent`** uses the contract **`pushes`** map and **`session.subscribe`** on the client.
59
-
60
- ### Handler errors
47
+ ```ts
48
+ throw new SockaError("Not allowed", { code: "FORBIDDEN", data: { reason: "…" } });
49
+ ```
61
50
 
62
- Throw **`SockaError`** from handlers when you control the **message** sent on the **`serverError`** frame. Any other thrown value is wrapped in **`SockaError`** using the original **`Error.message`** when possible, otherwise **`"Handler failed"`**. The client rejects the matching RPC with **`SockaError`**; the wire carries a string **message** only.
51
+ Any other thrown value is wrapped in **`SockaError`** using the original **`Error.message`** when possible, otherwise **`"Handler failed"`**. For calls **with** **`output`**, the client rejects the matching RPC with **`SockaError`**. For **output-less** (fire-and-forget) calls, there is no pending promise; the client surfaces **`SockaError`** via **`reportError`** (`kind`: **`clientFireAndForgetRpcError`**). The wire carries **`error`** (string) plus optional **`code`**, **`data`**, and **`rpc`** (procedure name). Older peers that only read **`error`** are unchanged.
63
52
 
64
53
  ## Server session configuration
65
54
 
@@ -71,7 +60,8 @@ Throw **`SockaError`** from handlers when you control the **message** sent on th
71
60
  | **`wireFormat`** | `"json"` (default) or `"msgpack"` — must match clients. |
72
61
  | **`handlers`** | Typed call implementations; arity follows input schema (see [Getting started](./getting-started.md)). |
73
62
  | **`handleClose`** | Async per-socket teardown; runs **before** removal from `sessions` (see [Lifecycle](./lifecycle.md)). |
74
- | **`createData`** | Builds **`session.data`**. **`SockaWebSocketSession`**: **`(init: SockaWebSocketInit) => T`** (e.g. **`init.request`** from upgrade). **`SockaDoSession`**: **`(ctx: Context) => T`** — see **[Durable Objects](./durable-objects.md)**. |
63
+ | **`createData`** | Builds **`session.data`**. **`SockaWebSocketSession`**: **`SockaWebSocketSessionConfig`** uses **`(init: SockaStrictWebSocketInit) => T`**; **`SockaWebSocketSessionConfigLoose`** uses **`(init: SockaWebSocketInit) => T`** see **[Server](./server.md)**. **`SockaDoSession`**: **`(ctx: Context) => T`** — see **[Durable Objects](./durable-objects.md)**. |
64
+ | **`SockaWebSocketSessionConfig` / `Loose` / `Union`** | Default **`SockaWebSocketSessionConfig`** is strict ( **`init.request` required** ). **`SockaWebSocketSessionConfigLoose`** sets **`strictUpgradeRequest: false`**. **`SockaWebSocketSessionConfigUnion`** is the union of strict and loose (what **`attachSockaWebSocket`** and Bun/Hono helpers accept). See **[Server — Strict upgrade request](./server.md#strict-upgrade-request)**. |
75
65
  | **`onAttached`** | Optional: after registration in `sessions` (safe for broadcasts). |
76
66
  | **`onHandlerError`** | Observes thrown errors in handlers (after optional `SockaError` wrapping for the wire). |
77
67
  | **`onValidationError`** | Inbound frame failed schema / wire decode before your handler. |
@@ -89,12 +79,16 @@ Throw **`SockaError`** from handlers when you control the **message** sent on th
89
79
  | **`autoConnect`** | Default **`true`**. If **`false`**, call **`connect()`** before **`session.send`** (or rely on implicit open from **`send`**). |
90
80
  | **`serializeJson` / `deserializeJson`** | Same as server — JSON wire mode only. |
91
81
  | **`onOpen` / `onClose` / `onError`** | WebSocket lifecycle. |
82
+ | **`reconnect`** | **`false`** or backoff options — default **on** for **`url`**, **off** for injected **`webSocket`** unless overridden. See **[Reconnection](./reconnection.md)**. |
83
+ | **`onReconnecting` / `onReconnected`** | Reconnect lifecycle (**`SockaWebSocketClient`** / **`SockaSession`**). |
92
84
  | **`onValidationError`** | Inbound frame failed validation (**`SockaWebSocketClient`**). |
93
85
  | **`pushHandlers`** | Initial **`session.subscribe`** subscriptions (**`SockaSession`** only). |
94
86
  | **`reportError`** | Client pipeline failures (listeners, validation); see **Errors and observability**. |
95
87
 
96
88
  **`SockaSession`** passes unrecognized options through to **`SockaWebSocketClient`** except **`pushHandlers`** and **`reportError`** (handled at the session layer). React hooks mirror these options — see **[Client](./client.md)**.
97
89
 
90
+ **`SockaSession`** / **`SockaWebSocketClient`** also expose **`status`** and **`onStatusChange`** (connection lifecycle, reconnect UI).
91
+
98
92
  ## Schema libraries
99
93
 
100
94
  Anything that implements **Standard Schema v1** works — **Zod**, **Valibot**, **ArkType**, or a custom implementation. Pass schemas straight into **`defineSocka`**; no adapter helpers required.
@@ -115,7 +109,8 @@ Anything that implements **Standard Schema v1** works — **Zod**, **Valibot**,
115
109
  | `@firtoz/socka` | Same as **`@firtoz/socka/core`** — `defineSocka`, wire helpers, errors, types (prefer explicit **`/core`** in examples) |
116
110
  | `@firtoz/socka/core` | `defineSocka`, wire helpers, `SockaError`, `SockaReportError`, `reportSockaError`, types |
117
111
  | `@firtoz/socka/client` | `SockaSession`, `SockaWebSocketClient` (also re-exports `SockaReportError`, `reportSockaError`) |
118
- | `@firtoz/socka/react` | `useSocka`, `useSockaSession`, provider + context |
112
+ | `@firtoz/socka/test` | `createFakeWebSocket` for unit tests — see **[Testing](./testing.md)** |
113
+ | `@firtoz/socka/react` | `useSocka`, `useSockaSession`, `useSockaPresence`, provider + context |
119
114
  | `@firtoz/socka/do` | `SockaDoSession`, `SockaWebSocketDO` |
120
115
  | `@firtoz/socka/server` | `SockaWebSocketSession`, `attachSockaWebSocket`, `dispatchSockaInboundMessage`, `broadcastSockaEventToPeers` |
121
116
  | `@firtoz/socka/bun` | `createSockaBunWebSocketHandlers` for **`Bun.serve`** |
package/docs/server.md CHANGED
@@ -42,16 +42,29 @@ attachSockaWebSocket(
42
42
 
43
43
  Optional fourth argument **`{ request }`** is passed to **`createData`** when you define per-connection state. Use **`InferSockaHandlers<typeof myContract, SockaWebSocketSession<typeof myContract, YourData>>`** (or omit the second generic and let inference fill it from your handlers).
44
44
 
45
- **Inbound frames without `attachSockaWebSocket`** — use **`dispatchSockaInboundMessage(session, wireFormat, data)`** with the same `data` shape as a DOM **`MessageEvent`** (`string`, **`ArrayBuffer`**, **`Blob`**, **`ArrayBufferView`**, or **`Buffer`** on Node/Bun). This is what **`attachSockaWebSocket`** uses internally.
45
+ **Inbound frames without `attachSockaWebSocket`** — use **`dispatchSockaInboundMessage(session, wireFormat, data)`** with the same `data` shape as a DOM **`MessageEvent`**. See **[Internals](./internals.md)** for how this fits the wire pipeline.
46
46
 
47
47
  ## `createData` and session-only state
48
48
 
49
49
  | | |
50
50
  |---:|---|
51
- | **`createData`** runs in the **`SockaWebSocketSession`** constructor. | You receive **`SockaWebSocketInit`** (e.g. **`{ request }`** from **`attachSockaWebSocket`**). |
51
+ | **`createData`** runs in the **`SockaWebSocketSession`** constructor. | By default you receive **`SockaStrictWebSocketInit`** ( **`init.request`** is the upgrade **`Request`** ). Set **`strictUpgradeRequest: false`** for **`SockaWebSocketInit`** when **`request`** may be missing. |
52
52
  | **Result** is stored in **`session.data`**. | Lives in **process memory** unless you persist it yourself. |
53
53
 
54
- Calls **with** an input schema use **`(input, session) => output`**. Calls **without** input use **`(session) => output`** only (no `undefined` first argument). The **`session`** argument is the **`SockaWebSocketSession`** instance: read **`session.data`**, call **`await session.emitPush`**, **`await session.broadcastPush`** (payloads are validated against the contract **`pushes`** schemas before send).
54
+ ## Strict upgrade request
55
+
56
+ **Strict vs loose:** **`SockaWebSocketSessionConfig`** (default) requires the upgrade **`Request`**. Use **`SockaWebSocketSessionConfigLoose`** with **`strictUpgradeRequest: false`** when **`init.request`** may be missing — it does not change the wire protocol, only **`createData`** typing and runtime checks.
57
+
58
+ | Mode | Type passed to **`createData`** | When to use it |
59
+ |------|----------------------------------|----------------|
60
+ | **Omitted** (default) | **`SockaStrictWebSocketInit`** — **`init.request` is always a `Request`** | Normal **Bun** / **Hono** upgrades and **`attachSockaWebSocket(..., { request })`**. **`createData`** can use **`new URL(init.request.url)`** and read headers. If the adapter omits **`request`**, socka throws at session construction. |
61
+ | **`false`** | **`SockaWebSocketInit`** — **`init.request` may be `undefined`** | Tests, **Node `ws`** without a **`Request`**, or adapters that only have a bare **`WebSocket`**. Handle a missing **`request`** in **`createData`** or omit **`createData`** usage of **`init`**. |
62
+
63
+ **Typical wiring:** Bun stores **`request`** on **`ServerWebSocket` `data`**; use **`sockaBunInitFromWsData`** (strict is the default). Hono **`sockaHonoNodeWs`** can omit **`sockaInit`** — the default builds a **`Request`** from the Hono context. See JSDoc on **`SockaWebSocketSessionConfig`**, **`SockaWebSocketInit`**, and **`SockaStrictWebSocketInit`** in **`@firtoz/socka/server`**.
64
+
65
+ Calls **with** an input schema use **`(input, session) => …`**. Calls **without** input use **`(session) => …`** only (no `undefined` first argument). When the call has **`output`** in the contract, the handler return value is validated and sent as **`serverResponse`**. When **`output` is omitted** (fire-and-forget), the handler should return **`void`**; socka sends **no** success **`serverResponse`** (failures still become **`serverError`**). See **[Reference — Optional output (fire-and-forget)](./reference.md#optional-output-fire-and-forget)** and **[Client](./client.md)**.
66
+
67
+ The **`session`** argument is the **`SockaWebSocketSession`** instance: read **`session.data`**, call **`await session.emitPush`**, **`await session.broadcastPush`** (payloads are validated against the contract **`pushes`** schemas before send).
55
68
 
56
69
  **`onAttached`** — optional. Runs after the session is registered in the shared **`sessions`** map (safe to broadcast to peers).
57
70
 
@@ -78,6 +91,7 @@ type GameData = { health: number };
78
91
 
79
92
  const session = new SockaWebSocketSession(websocket, sessions, {
80
93
  contract: gameContract,
94
+ strictUpgradeRequest: false,
81
95
  createData: () => ({ health: 100 }),
82
96
  handlers: {
83
97
  getHealth: async (s) => ({ health: s.data.health }),
@@ -111,6 +125,8 @@ Bun.serve({ fetch, websocket });
111
125
 
112
126
  **Multi-room** — use the overload **`createSockaBunWebSocketHandlers({ resolveScope })`** so each **`ServerWebSocket`** picks the correct **`sessionMap`** and shared **`config`** (see [Multi-room](./multi-room.md) and the tic-tac-toe example).
113
127
 
128
+ **Upgrade query params** — `createData` only sees **`init.request`** when the adapter passes it. For Bun, merge the upgrade **`Request`** into **`ServerWebSocket` `data`** so **`?name=`** and other query params are available. Prefer **`sockaBunUpgrade(server, req, { roomId })`** from **`@firtoz/socka/bun`**, which sets **`data: { …extras, request: req }`**. Alternatively call **`server.upgrade(req, { data: { roomId, request: req } })`** yourself. **`sockaBunInitFromWsData`** reads **`data.request`** and builds **`SockaWebSocketInit`** for the session constructor.
129
+
114
130
  ## `@firtoz/socka/hono` (Node — `@hono/node-ws`)
115
131
 
116
132
  Use **`createNodeWebSocket`** from [**`@hono/node-ws`**](https://github.com/honojs/middleware/tree/main/packages/node-ws) with **`serve`** from **`@hono/node-server`**, then **`upgradeWebSocket(sockaHonoNodeWs({ contract, handlers, handleClose }))`**. **`sockaHonoNodeWs`** returns the callback Hono expects for **`upgradeWebSocket`**.
@@ -0,0 +1,20 @@
1
+ # Testing
2
+
3
+ ## Client: inject a `WebSocket`
4
+
5
+ In tests, pass **`webSocket`** into **`SockaSession`** or **`SockaWebSocketClient`** with a **fake implementation** of the **`WebSocket`** API (no real network). Import **`createFakeWebSocket`** from **`@firtoz/socka/test`** (same helper the package tests use). Drive **`send`** / **`subscribe`** by delivering **encoded socka frames** (JSON or msgpack per **`wireFormat`**) through the fake’s **`onmessage`** path.
6
+
7
+ **Reconnect:** With an injected socket, reconnect defaults to **off**; set **`reconnect`** explicitly if you need to test backoff behavior.
8
+
9
+ ## Server: `SockaWebSocketSession` in isolation
10
+
11
+ Construct **`SockaWebSocketSession`** with a **`SockaWebSocketSessionConfig`** and call **`handleRawMessage`** / **`dispatchSockaInboundMessage`** with encoded frames (see package tests under **`packages/socka/src/server/`**).
12
+
13
+ ## Integration-style
14
+
15
+ The monorepo may include **`tests/socka-server-test`** (or similar) for end-to-end handler checks against a real upgrade path — run the workspace test target that applies to your change.
16
+
17
+ ## See also
18
+
19
+ - **[Reference](./reference.md)** — client/server configuration tables.
20
+ - **[Internals](./internals.md)** — frame shapes for manual encoding.
@@ -0,0 +1,29 @@
1
+ # Wire format tradeoffs (JSON vs msgpack)
2
+
3
+ Socka uses one **`wireFormat`** per connection for **both** RPCs and pushes (**default `"json"`**).
4
+
5
+ ## JSON (`"json"`)
6
+
7
+ - **Pros** — Easy to debug in DevTools; text frames; no extra binary codec in the browser.
8
+ - **Cons** — Larger payloads than msgpack for repetitive objects; UTF-8 string overhead.
9
+
10
+ ## MessagePack (`"msgpack"`)
11
+
12
+ Implemented with **msgpackr** (bundled dependency size ~tens of kb — see your bundler report).
13
+
14
+ - **Pros** — Smaller frames for structured data; binary **`ArrayBuffer`** WebSocket frames.
15
+ - **Cons** — Harder to read in network tabs; must set **`wireFormat: "msgpack"`** on **client and server** for that connection.
16
+
17
+ ## Binary payloads
18
+
19
+ If you send **raw binary** application data, msgpack mode is a natural fit; keep contract fields as **byte arrays** or **base64** depending on how you want to validate (Standard Schema still applies to decoded values).
20
+
21
+ ## Switching
22
+
23
+ Set **`wireFormat: "msgpack"`** on **`SockaSession`** / **`SockaWebSocketClient`** and on **`SockaWebSocketSessionConfig`** / **`SockaDoSessionConfig`** for every session that speaks to that client.
24
+
25
+ **Details:** **[Reference — Wire encoding](./reference.md#wire-encoding-json-and-msgpack)** · **[Internals](./internals.md)**.
26
+
27
+ ## `serverError` and `rpc`
28
+
29
+ On **`serverError`** frames, **`rpc`** is an optional string naming the procedure when the failure is tied to a client RPC. Servers built on **`SockaWebSocketSession`** include it on correlated errors so clients can attribute failures without relying on a pending **`Map`** entry (needed for fire-and-forget calls that omit **`output`**). Older servers may omit **`rpc`**; clients still receive **`id`** and **`error`**.
@@ -8,12 +8,59 @@ import {
8
8
  type InferSockaHandlers,
9
9
  } from "../src/core/contract";
10
10
 
11
+ const messageRow = z.object({
12
+ id: z.string(),
13
+ ts: z.number(),
14
+ userId: z.string(),
15
+ displayName: z.string(),
16
+ text: z.string(),
17
+ });
18
+
19
+ const onlineUser = z.object({
20
+ userId: z.string(),
21
+ displayName: z.string(),
22
+ });
23
+
11
24
  const contract = defineSocka({
12
25
  calls: {
13
- echo: {
26
+ sendMessage: {
14
27
  input: z.object({ text: z.string() }),
15
- output: z.object({ text: z.string() }),
28
+ output: z.object({ ok: z.literal(true) }),
16
29
  },
30
+ listHistory: {
31
+ input: z.object({ limit: z.number().optional() }),
32
+ output: z.object({ messages: z.array(messageRow) }),
33
+ },
34
+ listPresence: {
35
+ input: z.object({}).optional(),
36
+ output: z.object({
37
+ selfUserId: z.string(),
38
+ users: z.array(onlineUser),
39
+ }),
40
+ },
41
+ clearHistory: {
42
+ input: z.object({}).optional(),
43
+ output: z.object({ ok: z.literal(true) }),
44
+ },
45
+ },
46
+ pushes: {
47
+ userJoined: z.object({ userId: z.string(), displayName: z.string() }),
48
+ userLeft: z.object({
49
+ userId: z.string(),
50
+ displayName: z.string(),
51
+ }),
52
+ roomMessage: z.object({
53
+ id: z.string(),
54
+ ts: z.number(),
55
+ userId: z.string(),
56
+ displayName: z.string(),
57
+ text: z.string(),
58
+ }),
59
+ historyCleared: z.object({
60
+ ts: z.number(),
61
+ clearedByUserId: z.string(),
62
+ clearedByDisplayName: z.string(),
63
+ }),
17
64
  },
18
65
  });
19
66
 
@@ -23,7 +70,13 @@ type Handlers = InferSockaHandlers<typeof contract, unknown>;
23
70
  void (async () => {
24
71
  const _send: Send = {} as Send;
25
72
  const _handlers: Handlers = {
26
- echo: async (input) => ({ text: input.text }),
73
+ sendMessage: async () => ({ ok: true }),
74
+ listHistory: async () => ({ messages: [] }),
75
+ listPresence: async () => ({
76
+ selfUserId: "demo-user",
77
+ users: [{ userId: "demo-user", displayName: "Demo" }],
78
+ }),
79
+ clearHistory: async () => ({ ok: true }),
27
80
  };
28
81
  void _send;
29
82
  void _handlers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/socka",
3
- "version": "2.0.0",
3
+ "version": "3.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",
@@ -42,6 +42,10 @@
42
42
  "./hono/cloudflare": {
43
43
  "types": "./dist/hono/cloudflare-workers.d.ts",
44
44
  "import": "./dist/hono/cloudflare-workers.js"
45
+ },
46
+ "./test": {
47
+ "types": "./dist/test/index.d.ts",
48
+ "import": "./dist/test/index.js"
45
49
  }
46
50
  },
47
51
  "files": [
@@ -92,17 +96,17 @@
92
96
  "access": "public"
93
97
  },
94
98
  "dependencies": {
95
- "@firtoz/maybe-error": "^1.6.0",
99
+ "@firtoz/maybe-error": "^1.6.1",
96
100
  "@standard-schema/spec": "^1.1.0",
97
101
  "msgpackr": "^1.11.9"
98
102
  },
99
103
  "peerDependencies": {
100
- "@cloudflare/workers-types": "^4.20260329.1",
104
+ "@cloudflare/workers-types": "^4.20260416.2",
101
105
  "@firtoz/websocket-do": "^13.0.0",
102
106
  "@hono/node-server": "^1.19.2",
103
107
  "@hono/node-ws": "^1.3.0",
104
108
  "hono": "^4.12.9",
105
- "react": "^19.2.4",
109
+ "react": "^19.2.5",
106
110
  "ws": "^8.18.0"
107
111
  },
108
112
  "peerDependenciesMeta": {
@@ -129,17 +133,17 @@
129
133
  }
130
134
  },
131
135
  "devDependencies": {
132
- "@cloudflare/workers-types": "^4.20260329.1",
133
- "@happy-dom/global-registrator": "^20.8.9",
134
- "@hono/node-server": "^1.19.2",
136
+ "@cloudflare/workers-types": "^4.20260416.2",
137
+ "@happy-dom/global-registrator": "^20.9.0",
138
+ "@hono/node-server": "^1.19.14",
135
139
  "@hono/node-ws": "^1.3.0",
136
140
  "@tanstack/intent": "^0.0.29",
137
141
  "@testing-library/react": "^16.3.2",
138
142
  "@types/react": "^19.2.14",
139
143
  "@types/ws": "^8.18.1",
140
- "bun-types": "^1.3.11",
141
- "happy-dom": "^20.8.9",
142
- "react-dom": "19.2.4",
144
+ "bun-types": "^1.3.12",
145
+ "happy-dom": "^20.9.0",
146
+ "react-dom": "19.2.5",
143
147
  "tsup": "^8.5.1",
144
148
  "typescript": "^6.0.2",
145
149
  "valibot": "^1.3.1",
package/roadmap.md CHANGED
@@ -1,8 +1,8 @@
1
- # Roadmap (post–socka v1)
1
+ # Roadmap
2
2
 
3
3
  Deferred work and ideas—not a commitment order.
4
4
 
5
5
  - **npm metadata** — Keywords, README polish, and release notes that match positioning (WebSocket RPC, Standard Schema, Durable Objects).
6
- - **Release communication** — Short announcement (optional blog or dev.to), comparison vs hand-rolled protocols and adjacent tools.
6
+ - **Release communication** — Short announcement (optional blog or dev.to), comparison vs custom protocols and adjacent tools.
7
7
  - **`create-socka-app` / `bun create`** — After the public API is stable enough, a scaffold for contract + client + server/DO choice.
8
8
  - **Ecosystem** — Tutorials, Stack Overflow presence, example repos as the community asks.
@@ -7,7 +7,7 @@ description: Standard Schema socka contracts (defineSocka), v1 wire envelopes, S
7
7
 
8
8
  ## Contract
9
9
 
10
- - **`defineSocka`** in **`@firtoz/socka/core`**: pass **`calls`** (and optional **`pushes`**) with **`StandardSchemaV1`** `input` / `output` per call. Types flow from **`InferSockaSend`**, **`InferSockaHandlers`**, **`InferSockaPushHandlers`**.
10
+ - **`defineSocka`** in **`@firtoz/socka/core`**: pass **`calls`** (and optional **`pushes`**) with **`StandardSchemaV1`** `input` and optional **`output`** per call. Omit **`output`** for fire-and-forget (no **`serverResponse`** on success; client **`send`** resolves after send; failures use **`serverError`** + **`reportError`**). Use **`output: z.void()`** for a correlated ACK. Types flow from **`InferSockaSend`**, **`InferSockaHandlers`**, **`InferSockaPushHandlers`**.
11
11
  - There is **no** `defineSockaProtocol` / `defineSockaRpcSpec` in socka—those names belong to other stacks; use **`defineSocka`** only.
12
12
 
13
13
  ## Browser client
@@ -24,7 +24,7 @@ description: Standard Schema socka contracts (defineSocka), v1 wire envelopes, S
24
24
  ## Wire
25
25
 
26
26
  - Every frame is a **socka v1** object validated by **`decodeSockaWire`** (`socka`, **`v`**, discriminators, **`id`**, **`rpc`**, **`body`**, …). Invalid payloads become **`SockaWireError`** (or **`onValidationError`** on the client).
27
- - RPC success → **`serverResponse`**; RPC failure → **`serverError`**; server pushes → **`serverEvent`** with event name + **`body`**.
27
+ - RPC success → **`serverResponse`** (unless the call omits **`output`**, then no success frame); RPC failure → **`serverError`** (optional **`rpc`** field for procedure name); server pushes → **`serverEvent`** with event name + **`body`**.
28
28
  - **JSON vs msgpack** is a transport choice only; the logical shape is identical. **Client and DO must use the same `wireFormat`.**
29
29
 
30
30
  ## Durable Objects
@@ -7,7 +7,7 @@ description: SockaDoSession and SockaWebSocketDO on Cloudflare Durable Objects
7
7
 
8
8
  ## Components
9
9
 
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>`**). Responses use **`encodeServerResponse`** / **`encodeServerError`**; optional **`encodeServerEvent`** for contract pushes.
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
11
  - **`SockaWebSocketDO`**: thin **`BaseWebSocketDO`** wrapper; you supply **`createSockaSession(ctx, websocket)`** returning a **`SockaDoSession`** (or subclass).
12
12
 
13
13
  ## Session config (`SockaDoSessionConfig`)
@@ -15,7 +15,7 @@ description: SockaDoSession and SockaWebSocketDO on Cloudflare Durable Objects
15
15
  - **`contract`**: from **`defineSocka`**.
16
16
  - **`wireFormat`**: **`"json"`** (default) or **`"msgpack"`**—must match the browser **`SockaWebSocketClient`/`SockaSession`** **`wireFormat`**.
17
17
  - **`createData`**: optional when session **`TData`** is empty (**`Record<string, never>`**); defaults to **`{}`**. Otherwise Hono **`Context`** → per-connection state.
18
- - **`handlers`**: call name → async/sync handler; inputs/outputs validated with Standard Schema via **`parseStandardSchema`**.
18
+ - **`handlers`**: call name → async/sync handler; inputs and optional outputs validated with Standard Schema via **`parseStandardSchema`** (omit **`output`** in the contract for fire-and-forget success; handler still runs on the server).
19
19
  - **`handleClose`**: cleanup when the socket closes.
20
20
  - **`onHandlerError`**, **`onValidationError`**: optional hooks (validation and handler failures are also mapped to **`serverError`** frames).
21
21
  - **`serializeJson`**, **`deserializeJson`**: optional; default **`JSON.stringify`/`JSON.parse`** for JSON mode.
@@ -7,7 +7,7 @@ description: Standard Schema v1 for socka contracts and wire validation—Zod, V
7
7
 
8
8
  ## When to use
9
9
 
10
- You define **call** `input` / `output` (and optional **push** payloads) for **`@firtoz/socka/core`**, **`@firtoz/socka/client`**, **`@firtoz/socka/react`**, and **`@firtoz/socka/do`**. Every schema slot expects **`StandardSchemaV1`** from **`@standard-schema/spec`** so callers can use **Zod**, **Valibot**, **ArkType**, etc., without socka-specific adapters.
10
+ You define **call** `input` and optional **`output`** (and optional **push** payloads) for **`@firtoz/socka/core`**, **`@firtoz/socka/client`**, **`@firtoz/socka/react`**, and **`@firtoz/socka/do`**. Omit **`output`** only for intentional fire-and-forget success semantics; **`output`** slots that are present use **`StandardSchemaV1`** from **`@standard-schema/spec`** so callers can use **Zod**, **Valibot**, **ArkType**, etc., without socka-specific adapters.
11
11
 
12
12
  ## Rules
13
13