@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.
Files changed (55) hide show
  1. package/README.md +120 -0
  2. package/assets/banner.png +0 -0
  3. package/dist/SockaWebSocketSession-Bru8yFcK.d.ts +107 -0
  4. package/dist/bun/index.d.ts +38 -0
  5. package/dist/bun/index.js +121 -0
  6. package/dist/bun/index.js.map +1 -0
  7. package/dist/chunk-45D4T232.js +236 -0
  8. package/dist/chunk-45D4T232.js.map +1 -0
  9. package/dist/chunk-5WQTYLIC.js +46 -0
  10. package/dist/chunk-5WQTYLIC.js.map +1 -0
  11. package/dist/chunk-AM7PB26G.js +421 -0
  12. package/dist/chunk-AM7PB26G.js.map +1 -0
  13. package/dist/chunk-MZCQHJXY.js +158 -0
  14. package/dist/chunk-MZCQHJXY.js.map +1 -0
  15. package/dist/chunk-YMT4HAH7.js +20 -0
  16. package/dist/chunk-YMT4HAH7.js.map +1 -0
  17. package/dist/client/index.d.ts +119 -0
  18. package/dist/client/index.js +5 -0
  19. package/dist/client/index.js.map +1 -0
  20. package/dist/core/index.d.ts +29 -0
  21. package/dist/core/index.js +14 -0
  22. package/dist/core/index.js.map +1 -0
  23. package/dist/do/index.d.ts +80 -0
  24. package/dist/do/index.js +110 -0
  25. package/dist/do/index.js.map +1 -0
  26. package/dist/hono/cloudflare-workers.d.ts +21 -0
  27. package/dist/hono/cloudflare-workers.js +68 -0
  28. package/dist/hono/cloudflare-workers.js.map +1 -0
  29. package/dist/hono/index.d.ts +30 -0
  30. package/dist/hono/index.js +74 -0
  31. package/dist/hono/index.js.map +1 -0
  32. package/dist/react/index.d.ts +72 -0
  33. package/dist/react/index.js +126 -0
  34. package/dist/react/index.js.map +1 -0
  35. package/dist/server/index.d.ts +27 -0
  36. package/dist/server/index.js +63 -0
  37. package/dist/server/index.js.map +1 -0
  38. package/dist/socka-report-error-DzFI2Tr7.d.ts +206 -0
  39. package/docs/README.md +18 -0
  40. package/docs/client.md +85 -0
  41. package/docs/comparison.md +36 -0
  42. package/docs/durable-objects.md +74 -0
  43. package/docs/events.md +48 -0
  44. package/docs/getting-started.md +138 -0
  45. package/docs/lifecycle.md +31 -0
  46. package/docs/multi-room.md +31 -0
  47. package/docs/peers.md +85 -0
  48. package/docs/reference.md +123 -0
  49. package/docs/server.md +124 -0
  50. package/examples/minimal-socka.ts +31 -0
  51. package/package.json +148 -0
  52. package/roadmap.md +8 -0
  53. package/skills/socka/core-rpc/SKILL.md +36 -0
  54. package/skills/socka/do-session/SKILL.md +33 -0
  55. package/skills/socka/standard-schema/SKILL.md +26 -0
@@ -0,0 +1,31 @@
1
+ # Multi-room
2
+
3
+ A **room** (channel, game, namespace) is a **scope** where every client shares one **`sessionMap`** and one session **config** (the object you pass to **`attachSockaWebSocket`**, **`sockaHonoNodeWs`**, **`createSockaBunWebSocketHandlers`**, …).
4
+
5
+ That shared **config** includes **`wireFormat`** (`"json"` or `"msgpack"`). Everyone connecting into that scope must use the same encoding—see **[Reference — Wire encoding](./reference.md#wire-encoding-json-and-msgpack)**.
6
+
7
+ **Durable Objects** — Often one **Durable Object instance** per room (e.g. **`idFromName(roomId)`**), with one **`sessions`** map per instance. See **[Durable Objects](./durable-objects.md)**.
8
+
9
+ Within a scope:
10
+
11
+ - All **`WebSocket`** instances are keys in the same **`Map<WebSocket, Session>`**.
12
+ - **`broadcastContractEvent`** walks that map, so “everyone in this room” means “every session in this scope’s map.”
13
+ - **`handleClose(session)`** runs when a socket leaves; use **`session.websocket`** and **`session.data`** for cleanup. See **[Lifecycle](./lifecycle.md)** for ordering (your handler runs **before** the socket is removed from the map).
14
+
15
+ ## Choosing a pattern
16
+
17
+ | Runtime | Pattern | When it fits |
18
+ |--------|---------|----------------|
19
+ | **Bun** | **`createSockaBunWebSocketHandlers({ resolveScope })`** | One **`Bun.serve`** `websocket` handler; **`resolveScope(ws)`** returns **`{ sessionMap, config }`**—often from **`ws.data`** set during the HTTP upgrade. |
20
+ | **Hono (Node)** | **A)** One route per room (`/ws/:roomId`) with **`getOrCreateRoom`** and **`sockaHonoNodeWs(room.config, { sessions: room.sessionMap })`**. **B)** Single upgrade route + **`resolveScope(c)`** returning **`{ sessions, config }`**. |
21
+ | **Durable Objects** | **One DO instance per room** via **`idFromName(roomId)`** (or similar). Each instance has its own **`sessions`** map. |
22
+
23
+ Demos: [`tic-tac-toe-bun`](../../../examples/tic-tac-toe-bun), [`tic-tac-toe-hono`](../../../examples/tic-tac-toe-hono), [`tic-tac-toe-do`](../../../examples/tic-tac-toe-do).
24
+
25
+ ## Pitfalls
26
+
27
+ - **Mixing rooms in one map** — Two logical rooms sharing a **`sessionMap`** leak broadcasts and presence. Partition maps per room or use separate DO instances.
28
+ - **Stale `config`** — Handlers close over **`config`**; mutating shared objects inside it affects every connection using that config. Prefer immutable snapshots or room-scoped instances (e.g. one **`Game`** per room).
29
+ - **Very large rooms on a Durable Object** — One DO is one isolate; huge fan-in can hit limits. Shard by room id (multiple DOs) if needed.
30
+
31
+ See also [Lifecycle](./lifecycle.md) and [Server](./server.md) for **`createData`** and **`session.data`**.
package/docs/peers.md ADDED
@@ -0,0 +1,85 @@
1
+ # Peers
2
+
3
+ Install **`@firtoz/socka`** first, then add **only** the peers for the code paths you import.
4
+
5
+ ## Pick your flow, then install
6
+
7
+ Choose one row and install for your app (`npm install`, `pnpm add`, `bun add`, etc.). Adjust versions to match your stack.
8
+
9
+ ### Browser or Vite SPA (client only)
10
+
11
+ You use **`@firtoz/socka/client`** (and maybe **`@firtoz/socka/core`** for types). No server adapters. **No Cloudflare types required**—standard DOM / `lib` typings are enough for `WebSocket`.
12
+
13
+ ```bash
14
+ npm install @firtoz/socka
15
+ ```
16
+
17
+ ### React client (`@firtoz/socka/react`)
18
+
19
+ ```bash
20
+ npm install @firtoz/socka react
21
+ ```
22
+
23
+ Add **`@types/react`** as a dev dependency if TypeScript asks for them.
24
+
25
+ ### Node + Hono + `@hono/node-ws` (`@firtoz/socka/hono`)
26
+
27
+ ```bash
28
+ npm install @firtoz/socka hono @hono/node-ws @hono/node-server ws
29
+ ```
30
+
31
+ Add **`@types/ws`** as a dev dependency when you use the **`ws`** package on Node.
32
+
33
+ ### Bun + `Bun.serve` (`@firtoz/socka/bun`)
34
+
35
+ ```bash
36
+ npm install @firtoz/socka
37
+ ```
38
+
39
+ Add **`bun-types`** as a dev dependency for TypeScript if you type-check Bun APIs.
40
+
41
+ ### Cloudflare Workers + Hono upgrade (`@firtoz/socka/hono/cloudflare`)
42
+
43
+ ```bash
44
+ npm install @firtoz/socka hono @cloudflare/workers-types
45
+ ```
46
+
47
+ ### Cloudflare Durable Objects (`@firtoz/socka/do`)
48
+
49
+ ```bash
50
+ npm install @firtoz/socka hono @firtoz/websocket-do @cloudflare/workers-types
51
+ ```
52
+
53
+ **Version pairing:** `@firtoz/socka/do` subclasses **`@firtoz/websocket-do`** (`BaseSession`, `BaseWebSocketDO`). The two packages use **different** version numbers on npm—there is no rule like “same major as socka.” Use a **websocket-do** version that **socka**’s **`peerDependencies`** (and changelog, if you hit edge cases) allow for your **socka** release. You can upgrade **either** package on its own while the integration stays compatible; coordinate when **`BaseSession` / `BaseWebSocketDO`** or socka’s DO layer changes (often **TypeScript** errors first).
54
+
55
+ ### Portable `ws` / `attachSockaWebSocket` only (`@firtoz/socka/server`)
56
+
57
+ ```bash
58
+ npm install @firtoz/socka ws
59
+ ```
60
+
61
+ Add **`@types/ws`** as a dev dependency when you use **`ws`** on Node. (Omit **`ws`** if your runtime already provides a typed **`WebSocket`**.)
62
+
63
+ ---
64
+
65
+ `@firtoz/websocket-do` is marked **optional** in **`@firtoz/socka`’s** `package.json` so browser-only clients do not pull Durable Object code. **`@cloudflare/workers-types`** is also **optional** unless you import **`@firtoz/socka/do`** or **`@firtoz/socka/hono/cloudflare`**, where Workers globals are part of the story.
66
+
67
+ **Any Worker that imports `@firtoz/socka/do` must add `@firtoz/websocket-do` explicitly:** `npm install @firtoz/websocket-do`. Choose a version that **satisfies socka’s stated peer range** (and your app’s lockfile); you do not need one-off “lockstep” bumps for every unrelated release—only when integration or types break.
68
+
69
+ ## Practical notes
70
+
71
+ - **Only install peers for paths you use.** A Vite SPA that only imports `@firtoz/socka/client` does not need `hono`, **`@cloudflare/workers-types`**, or `@firtoz/websocket-do`.
72
+ - **TypeScript:** For Workers code, add **`@cloudflare/workers-types`** to `compilerOptions.types` (or use your framework’s defaults). For **`@firtoz/socka/bun`** handlers, add **`bun-types`** when you author against Bun APIs.
73
+ - **socka + websocket-do:** If **`@firtoz/websocket-do`** is **outside** what your **socka** version expects (or websocket-do ships a breaking `BaseSession` / `BaseWebSocketDO` change), you may see **type errors** on `SockaDoSession` / `SockaWebSocketDO` or runtime issues—then bump **one or both** until the pairing in the docs / peer range works again.
74
+
75
+ ## By entrypoint (reference)
76
+
77
+ | Entry | Required peers | Why |
78
+ |--------|----------------|-----|
79
+ | `@firtoz/socka/core`, `@firtoz/socka/client` | **None** | Uses Standard Schema, **`WebSocket`**, and shared frame types—**`lib: ["DOM"]`** (or your bundler defaults) is enough. |
80
+ | `@firtoz/socka/react` | `react` **≥ 18** | Hooks and provider API. |
81
+ | `@firtoz/socka/do` | **`@firtoz/websocket-do`** (version range per **socka** `peerDependencies` / changelog), **`@cloudflare/workers-types`**, **`hono`** | `SockaDoSession` extends **`BaseSession`** from **websocket-do**; **`SockaWebSocketDO`** uses **Hono**-shaped routing on top of **`BaseWebSocketDO`**. |
82
+ | `@firtoz/socka/server` | None beyond `@firtoz/socka/core` (standard **`WebSocket`** + same contract types) | Portable **`attachSockaWebSocket`** path. |
83
+ | `@firtoz/socka/bun` | Same as `@firtoz/socka/server` (**`bun-types`** for TypeScript) | **`Bun.serve`** / **`ServerWebSocket`** integration. |
84
+ | `@firtoz/socka/hono` | **`hono`**, **`@hono/node-ws`**, **`@hono/node-server`**, **`ws`** (runtime + types) | Node **`upgradeWebSocket`** pipeline. |
85
+ | `@firtoz/socka/hono/cloudflare` | **`hono`**, **`@cloudflare/workers-types`** (**`upgradeWebSocket`** from `hono/cloudflare-workers`) | Workers WebSocket upgrade (session often starts on first message—see **[Server](./server.md)**). |
@@ -0,0 +1,123 @@
1
+ # Reference
2
+
3
+ ## Type inference
4
+
5
+ ```ts
6
+ import type { InferSockaSend, InferSockaHandlers } from "@firtoz/socka/core";
7
+
8
+ type Send = InferSockaSend<typeof myContract>;
9
+ type Handlers = InferSockaHandlers<
10
+ typeof myContract,
11
+ SockaWebSocketSession<typeof myContract>
12
+ >;
13
+ ```
14
+
15
+ **`InferSockaSend`** — Call names become methods on **`session.send`**; inputs/outputs follow the contract. **`InferSockaHandlers`** — Server handler arity matches **`calls`** (with or without `input`).
16
+
17
+ ## Errors and observability
18
+
19
+ | Concern | Hook |
20
+ |--------|------|
21
+ | Exceptions inside **RPC handlers** | `onHandlerError` on `SockaWebSocketSessionConfig` / `SockaDoSessionConfig` |
22
+ | 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 |
24
+
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.
30
+
31
+ ## Wire encoding: JSON and msgpack
32
+
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. |
39
+
40
+ **Rules**
41
+
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.
46
+
47
+ ## Logical frames (socka v1)
48
+
49
+ Every decoded payload is one logical socka **v1** object. **`decodeSockaWire`** checks shape after `JSON.parse` (text) or msgpack unpack (binary).
50
+
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
61
+
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.
63
+
64
+ ## Server session configuration
65
+
66
+ **`SockaWebSocketSessionConfig`** (`@firtoz/socka/server`, Bun, Hono) and **`SockaDoSessionConfig`** (`@firtoz/socka/do`) share the same fields except **`createData`** (see below).
67
+
68
+ | Field | Purpose |
69
+ |--------|---------|
70
+ | **`contract`** | `defineSocka(...)` — `calls`, optional `pushes`. |
71
+ | **`wireFormat`** | `"json"` (default) or `"msgpack"` — must match clients. |
72
+ | **`handlers`** | Typed call implementations; arity follows input schema (see [Getting started](./getting-started.md)). |
73
+ | **`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)**. |
75
+ | **`onAttached`** | Optional: after registration in `sessions` (safe for broadcasts). |
76
+ | **`onHandlerError`** | Observes thrown errors in handlers (after optional `SockaError` wrapping for the wire). |
77
+ | **`onValidationError`** | Inbound frame failed schema / wire decode before your handler. |
78
+ | **`reportError`** | Non-RPC failures (`onAttached`, adapters, …); see **Errors and observability** above. |
79
+ | **`serializeJson` / `deserializeJson`** | Replace JSON stringify/parse for **JSON wire mode** only. |
80
+
81
+ ## Client configuration
82
+
83
+ | Field | Purpose |
84
+ |--------|---------|
85
+ | **`contract`** | Same module reference as the server. |
86
+ | **`url`** | `new WebSocket(url)` when **`webSocket`** is omitted. |
87
+ | **`webSocket`** | Inject an existing socket (tests or custom setup); **`url`** not required if set. |
88
+ | **`wireFormat`** | Must match the server session (**default `"json"`**). |
89
+ | **`autoConnect`** | Default **`true`**. If **`false`**, call **`connect()`** before **`session.send`** (or rely on implicit open from **`send`**). |
90
+ | **`serializeJson` / `deserializeJson`** | Same as server — JSON wire mode only. |
91
+ | **`onOpen` / `onClose` / `onError`** | WebSocket lifecycle. |
92
+ | **`onValidationError`** | Inbound frame failed validation (**`SockaWebSocketClient`**). |
93
+ | **`pushHandlers`** | Initial **`session.subscribe`** subscriptions (**`SockaSession`** only). |
94
+ | **`reportError`** | Client pipeline failures (listeners, validation); see **Errors and observability**. |
95
+
96
+ **`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
+
98
+ ## Schema libraries
99
+
100
+ Anything that implements **Standard Schema v1** works — **Zod**, **Valibot**, **ArkType**, or a custom implementation. Pass schemas straight into **`defineSocka`**; no adapter helpers required.
101
+
102
+ ## At a glance
103
+
104
+ | | |
105
+ |---:|---|
106
+ | **Contract** | `defineSocka({ calls, pushes? })` — Zod, Valibot, ArkType, or any [Standard Schema v1](https://standardschema.dev/) |
107
+ | **Client** | `SockaSession` / `useSockaSession` / `SockaSessionProvider` + `useSockaSessionContext` |
108
+ | **Server** | `SockaDoSession` + `SockaWebSocketDO` on Durable Objects, or **`@firtoz/socka/server`** / **`@firtoz/socka/bun`** / **`@firtoz/socka/hono`** on any supported runtime |
109
+ | **Wire** | JSON text frames by default; optional **msgpack** binary — same logical frames, both ends must use the same `wireFormat` |
110
+
111
+ ### Imports
112
+
113
+ | Path | Use for |
114
+ |------|---------|
115
+ | `@firtoz/socka` | Same as **`@firtoz/socka/core`** — `defineSocka`, wire helpers, errors, types (prefer explicit **`/core`** in examples) |
116
+ | `@firtoz/socka/core` | `defineSocka`, wire helpers, `SockaError`, `SockaReportError`, `reportSockaError`, types |
117
+ | `@firtoz/socka/client` | `SockaSession`, `SockaWebSocketClient` (also re-exports `SockaReportError`, `reportSockaError`) |
118
+ | `@firtoz/socka/react` | `useSocka`, `useSockaSession`, provider + context |
119
+ | `@firtoz/socka/do` | `SockaDoSession`, `SockaWebSocketDO` |
120
+ | `@firtoz/socka/server` | `SockaWebSocketSession`, `attachSockaWebSocket`, `dispatchSockaInboundMessage`, `broadcastSockaEventToPeers` |
121
+ | `@firtoz/socka/bun` | `createSockaBunWebSocketHandlers` for **`Bun.serve`** |
122
+ | `@firtoz/socka/hono` | `sockaHonoNodeWs` for **`@hono/node-ws`** |
123
+ | `@firtoz/socka/hono/cloudflare` | `sockaHonoCloudflare` for **`hono/cloudflare-workers`** |
package/docs/server.md ADDED
@@ -0,0 +1,124 @@
1
+ # Server: Node, Bun, Hono, and ws
2
+
3
+ This guide is for a **normal WebSocket** in your own process: **Node** (including the **`ws`** package), **Bun** **`Bun.serve`**, **Hono** on Node (**`@hono/node-ws`**) or on **Cloudflare Workers** (**`hono/cloudflare-workers`**). You supply **`contract`**, **`handlers`**, and **`handleClose`**; socka decodes frames, validates inputs, and dispatches RPCs.
4
+
5
+ **Cloudflare Durable Objects** ( **`SockaDoSession`**, **`SockaWebSocketDO`** ) are covered in **[Durable Objects](./durable-objects.md)**.
6
+
7
+ With **`@firtoz/socka/server`**, pass a **`SockaWebSocketSessionConfig`** and wire the socket with **`attachSockaWebSocket`**, or call **`dispatchSockaInboundMessage`**, **`handleRawMessage`**, or **`handleBinaryMessage`** on **`SockaWebSocketSession`** yourself if you already handle **`message`** events.
8
+
9
+ ### `wireFormat` and session config
10
+
11
+ The third argument to **`attachSockaWebSocket`** is a **`SockaWebSocketSessionConfig`**: at minimum **`contract`**, **`handlers`**, and **`handleClose`**. Optional **`wireFormat`** defaults to **`"json"`** (UTF-8 **text** WebSocket frames). Use **`wireFormat: "msgpack"`** only if clients use the same mode (**binary** frames). Optional **`serializeJson` / `deserializeJson`** customize JSON encoding of the **outer envelope** (not a substitute for matching `wireFormat`). Full field list: **[Reference — Server session configuration](./reference.md#server-session-configuration)**.
12
+
13
+ ```ts
14
+ import {
15
+ attachSockaWebSocket,
16
+ type SockaWebSocketSession,
17
+ } from "@firtoz/socka/server";
18
+ import { myContract } from "./contract";
19
+
20
+ const sessions = new Map<
21
+ WebSocket,
22
+ SockaWebSocketSession<typeof myContract>
23
+ >();
24
+
25
+ // After the upgrade (shape depends on runtime — see below):
26
+ attachSockaWebSocket(
27
+ websocket,
28
+ sessions,
29
+ {
30
+ contract: myContract,
31
+ handlers: {
32
+ list: async (session) => fetchMessages(),
33
+ insert: async (input, session) => saveMessage(input.message),
34
+ },
35
+ handleClose: async (session) => {
36
+ // cleanup for this socket; session is still in `sessions` during the call
37
+ },
38
+ },
39
+ { request: upgradeRequest },
40
+ );
41
+ ```
42
+
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
+
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.
46
+
47
+ ## `createData` and session-only state
48
+
49
+ | | |
50
+ |---:|---|
51
+ | **`createData`** runs in the **`SockaWebSocketSession`** constructor. | You receive **`SockaWebSocketInit`** (e.g. **`{ request }`** from **`attachSockaWebSocket`**). |
52
+ | **Result** is stored in **`session.data`**. | Lives in **process memory** unless you persist it yourself. |
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).
55
+
56
+ **`onAttached`** — optional. Runs after the session is registered in the shared **`sessions`** map (safe to broadcast to peers).
57
+
58
+ **Example — `session.data`:**
59
+
60
+ ```ts
61
+ import { defineSocka } from "@firtoz/socka/core";
62
+ import { SockaWebSocketSession } from "@firtoz/socka/server";
63
+ import * as z from "zod";
64
+
65
+ const gameContract = defineSocka({
66
+ calls: {
67
+ getHealth: {
68
+ output: z.object({ health: z.number() }),
69
+ },
70
+ damage: {
71
+ input: z.object({ amount: z.number() }),
72
+ output: z.object({ health: z.number() }),
73
+ },
74
+ },
75
+ });
76
+
77
+ type GameData = { health: number };
78
+
79
+ const session = new SockaWebSocketSession(websocket, sessions, {
80
+ contract: gameContract,
81
+ createData: () => ({ health: 100 }),
82
+ handlers: {
83
+ getHealth: async (s) => ({ health: s.data.health }),
84
+ damage: async (input, s) => {
85
+ s.data.health = Math.max(0, s.data.health - input.amount);
86
+ return { health: s.data.health };
87
+ },
88
+ },
89
+ handleClose: async (session) => {
90
+ // optional per-socket cleanup
91
+ },
92
+ });
93
+ sessions.set(websocket, session);
94
+ ```
95
+
96
+ ## `@firtoz/socka/bun` (Bun.serve)
97
+
98
+ **Bun** `Bun.serve` uses **`ServerWebSocket`**, which does **not** implement **`addEventListener`**, so **`attachSockaWebSocket`** does not apply. Use **`createSockaBunWebSocketHandlers`** and pass the returned **`websocket`** into **`Bun.serve`**:
99
+
100
+ ```ts
101
+ import { createSockaBunWebSocketHandlers } from "@firtoz/socka/bun";
102
+
103
+ const { websocket } = createSockaBunWebSocketHandlers({
104
+ contract: myContract,
105
+ handlers: { /* ... */ },
106
+ handleClose: async (session) => {},
107
+ });
108
+
109
+ Bun.serve({ fetch, websocket });
110
+ ```
111
+
112
+ **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
+
114
+ ## `@firtoz/socka/hono` (Node — `@hono/node-ws`)
115
+
116
+ 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`**.
117
+
118
+ ## `@firtoz/socka/hono/cloudflare` (Workers)
119
+
120
+ Use **`upgradeWebSocket`** from **`hono/cloudflare-workers`** with **`sockaHonoCloudflare({ contract, handlers, handleClose })`**. The session is created on the first **`onMessage`** (Workers helpers omit **`onOpen`**).
121
+
122
+ **Node with [`ws`](https://github.com/websockets/ws)** — in the **`connection`** handler, pass the socket into **`attachSockaWebSocket`**. The `ws` package’s socket is not always identical to the browser **`WebSocket`** type; if TypeScript complains, cast to the global **`WebSocket`** type your build targets. **`attachSockaWebSocket`** and **`dispatchSockaInboundMessage`** accept JSON frames delivered as UTF-8 **`ArrayBuffer`** slices (some runtimes send text that way) as well as strings.
123
+
124
+ **Integration tests in this repo:** [`tests/socka-server-test`](https://github.com/firtoz/fullstack-toolkit/tree/main/tests/socka-server-test) (Node **`ws`**, **Bun.serve**, **Hono + `@hono/node-ws`**).
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Minimal runnable example: `bun run example:minimal` from `packages/socka`.
3
+ */
4
+ import * as z from "zod";
5
+ import {
6
+ defineSocka,
7
+ type InferSockaSend,
8
+ type InferSockaHandlers,
9
+ } from "../src/core/contract";
10
+
11
+ const contract = defineSocka({
12
+ calls: {
13
+ echo: {
14
+ input: z.object({ text: z.string() }),
15
+ output: z.object({ text: z.string() }),
16
+ },
17
+ },
18
+ });
19
+
20
+ type Send = InferSockaSend<typeof contract>;
21
+ type Handlers = InferSockaHandlers<typeof contract, unknown>;
22
+
23
+ void (async () => {
24
+ const _send: Send = {} as Send;
25
+ const _handlers: Handlers = {
26
+ echo: async (input) => ({ text: input.text }),
27
+ };
28
+ void _send;
29
+ void _handlers;
30
+ console.log("minimal socka types OK");
31
+ })();
package/package.json ADDED
@@ -0,0 +1,148 @@
1
+ {
2
+ "name": "@firtoz/socka",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "description": "Standard Schema–first WebSocket RPC for TypeScript — Bun, Hono, Node ws, Cloudflare Workers, Durable Objects",
6
+ "main": "./dist/core/index.js",
7
+ "module": "./dist/core/index.js",
8
+ "types": "./dist/core/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/core/index.d.ts",
12
+ "import": "./dist/core/index.js"
13
+ },
14
+ "./core": {
15
+ "types": "./dist/core/index.d.ts",
16
+ "import": "./dist/core/index.js"
17
+ },
18
+ "./client": {
19
+ "types": "./dist/client/index.d.ts",
20
+ "import": "./dist/client/index.js"
21
+ },
22
+ "./react": {
23
+ "types": "./dist/react/index.d.ts",
24
+ "import": "./dist/react/index.js"
25
+ },
26
+ "./do": {
27
+ "types": "./dist/do/index.d.ts",
28
+ "import": "./dist/do/index.js"
29
+ },
30
+ "./server": {
31
+ "types": "./dist/server/index.d.ts",
32
+ "import": "./dist/server/index.js"
33
+ },
34
+ "./bun": {
35
+ "types": "./dist/bun/index.d.ts",
36
+ "import": "./dist/bun/index.js"
37
+ },
38
+ "./hono": {
39
+ "types": "./dist/hono/index.d.ts",
40
+ "import": "./dist/hono/index.js"
41
+ },
42
+ "./hono/cloudflare": {
43
+ "types": "./dist/hono/cloudflare-workers.d.ts",
44
+ "import": "./dist/hono/cloudflare-workers.js"
45
+ }
46
+ },
47
+ "files": [
48
+ "dist/**/*.js",
49
+ "dist/**/*.js.map",
50
+ "dist/**/*.d.ts",
51
+ "examples/**/*.ts",
52
+ "skills",
53
+ "docs/**/*.md",
54
+ "assets/banner.png",
55
+ "README.md",
56
+ "roadmap.md"
57
+ ],
58
+ "scripts": {
59
+ "build": "tsup",
60
+ "prepack": "bun run build",
61
+ "typecheck": "tsgo --noEmit -p ./tsconfig.json",
62
+ "example:minimal": "bun ./examples/minimal-socka.ts",
63
+ "test": "bun test",
64
+ "lint": "biome check --write src",
65
+ "lint:ci": "biome ci src && bun run intent:validate",
66
+ "format": "biome format src --write",
67
+ "intent:validate": "intent validate"
68
+ },
69
+ "keywords": [
70
+ "websocket",
71
+ "rpc",
72
+ "standard-schema",
73
+ "durable-objects",
74
+ "cloudflare",
75
+ "tanstack-intent"
76
+ ],
77
+ "author": "Firtina Ozbalikchi <firtoz@github.com>",
78
+ "license": "MIT",
79
+ "homepage": "https://github.com/firtoz/fullstack-toolkit#readme",
80
+ "repository": {
81
+ "type": "git",
82
+ "url": "https://github.com/firtoz/fullstack-toolkit.git",
83
+ "directory": "packages/socka"
84
+ },
85
+ "bugs": {
86
+ "url": "https://github.com/firtoz/fullstack-toolkit/issues"
87
+ },
88
+ "engines": {
89
+ "node": ">=18.0.0"
90
+ },
91
+ "publishConfig": {
92
+ "access": "public"
93
+ },
94
+ "dependencies": {
95
+ "@firtoz/maybe-error": "^1.6.0",
96
+ "@standard-schema/spec": "^1.1.0",
97
+ "msgpackr": "^1.11.9"
98
+ },
99
+ "peerDependencies": {
100
+ "@cloudflare/workers-types": "^4.20260329.1",
101
+ "@firtoz/websocket-do": "^13.0.0",
102
+ "@hono/node-server": "^1.19.2",
103
+ "@hono/node-ws": "^1.3.0",
104
+ "hono": "^4.12.9",
105
+ "react": "^19.2.4",
106
+ "ws": "^8.18.0"
107
+ },
108
+ "peerDependenciesMeta": {
109
+ "@cloudflare/workers-types": {
110
+ "optional": true
111
+ },
112
+ "@firtoz/websocket-do": {
113
+ "optional": true
114
+ },
115
+ "@hono/node-server": {
116
+ "optional": true
117
+ },
118
+ "@hono/node-ws": {
119
+ "optional": true
120
+ },
121
+ "hono": {
122
+ "optional": true
123
+ },
124
+ "react": {
125
+ "optional": true
126
+ },
127
+ "ws": {
128
+ "optional": true
129
+ }
130
+ },
131
+ "devDependencies": {
132
+ "@cloudflare/workers-types": "^4.20260329.1",
133
+ "@happy-dom/global-registrator": "^20.8.9",
134
+ "@hono/node-server": "^1.19.2",
135
+ "@hono/node-ws": "^1.3.0",
136
+ "@tanstack/intent": "^0.0.29",
137
+ "@testing-library/react": "^16.3.2",
138
+ "@types/react": "^19.2.14",
139
+ "@types/ws": "^8.18.1",
140
+ "bun-types": "^1.3.11",
141
+ "happy-dom": "^20.8.9",
142
+ "react-dom": "19.2.4",
143
+ "tsup": "^8.5.1",
144
+ "typescript": "^6.0.2",
145
+ "valibot": "^1.3.1",
146
+ "zod": "^4.3.6"
147
+ }
148
+ }
package/roadmap.md ADDED
@@ -0,0 +1,8 @@
1
+ # Roadmap (post–socka v1)
2
+
3
+ Deferred work and ideas—not a commitment order.
4
+
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.
7
+ - **`create-socka-app` / `bun create`** — After the public API is stable enough, a scaffold for contract + client + server/DO choice.
8
+ - **Ecosystem** — Tutorials, Stack Overflow presence, example repos as the community asks.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: socka/core-rpc
3
+ description: Standard Schema socka contracts (defineSocka), v1 wire envelopes, SockaSession/SockaWebSocketClient, React useSockaSession and SockaSessionProvider, SockaError.
4
+ ---
5
+
6
+ # Socka core: RPC
7
+
8
+ ## Contract
9
+
10
+ - **`defineSocka`** in **`@firtoz/socka/core`**: pass **`calls`** (and optional **`pushes`**) with **`StandardSchemaV1`** `input` / `output` per call. Types flow from **`InferSockaSend`**, **`InferSockaHandlers`**, **`InferSockaPushHandlers`**.
11
+ - There is **no** `defineSockaProtocol` / `defineSockaRpcSpec` in socka—those names belong to other stacks; use **`defineSocka`** only.
12
+
13
+ ## Browser client
14
+
15
+ - **`SockaWebSocketClient`** / **`SockaSession`** in **`@firtoz/socka/client`**: constructed with **`contract`**, **`url`** or **`webSocket`**, optional **`wireFormat`**: **`"json"`** (default, text frames) or **`"msgpack"`** (binary `ArrayBuffer`—must match the server). Optional **`pushHandlers`** for server **`serverEvent`** frames (typed from the contract). On **`SockaSession`**, call contract methods as **`await session.send.echo(...)`** (same shape as **`useSockaSession`**’s **`send`**); use **`session.subscribe`** for push subscriptions.
16
+ - **`SockaError`**: thrown on correlated RPC failures when using **`SockaSession`** (check **`instanceof SockaError`**).
17
+
18
+ ## React
19
+
20
+ - **`useSockaSession(contract, options, deps)`** in **`@firtoz/socka/react`**: returns **`{ ready, send, sessionRef }`**. **`send`** exposes typed call methods (e.g. **`send.list()`**). Pass **`pushHandlers`** in **`options`** for contract **pushes** (not a separate “server push” table).
21
+ - **`useSocka(options, deps)`**: lower level; **`sessionRef`** to **`SockaSession`** if you build **`send`** yourself.
22
+ - **Single shared socket**: wrap the tree with **`SockaSessionProvider`** (same props as **`useSockaSession`** plus **`children`**), then call **`useSockaSessionContext(contract)`** in descendants. Pass the **same `contract` reference** as the provider; the hook checks reference equality.
23
+
24
+ ## Wire
25
+
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`**.
28
+ - **JSON vs msgpack** is a transport choice only; the logical shape is identical. **Client and DO must use the same `wireFormat`.**
29
+
30
+ ## Durable Objects
31
+
32
+ - **`SockaDoSession`** / **`SockaWebSocketDO`** in **`@firtoz/socka/do`**—see **`@firtoz/socka/do-session`** skill.
33
+
34
+ ## Low-level
35
+
36
+ - Use **`@firtoz/socka/core`** helpers (**`encodeClientRequest`**, **`decodeSockaWire`**, **`encodeSockaWire`**, **`parseWirePayload`**) only if you bypass **`SockaSession`** / **`SockaDoSession`**.
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: socka/do-session
3
+ description: SockaDoSession and SockaWebSocketDO on Cloudflare Durable Objects—contract, handlers, wireFormat, SockaError; extends websocket-do BaseSession.
4
+ ---
5
+
6
+ # Socka Durable Objects
7
+
8
+ ## Components
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.
11
+ - **`SockaWebSocketDO`**: thin **`BaseWebSocketDO`** wrapper; you supply **`createSockaSession(ctx, websocket)`** returning a **`SockaDoSession`** (or subclass).
12
+
13
+ ## Session config (`SockaDoSessionConfig`)
14
+
15
+ - **`contract`**: from **`defineSocka`**.
16
+ - **`wireFormat`**: **`"json"`** (default) or **`"msgpack"`**—must match the browser **`SockaWebSocketClient`/`SockaSession`** **`wireFormat`**.
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`**.
19
+ - **`handleClose`**: cleanup when the socket closes.
20
+ - **`onHandlerError`**, **`onValidationError`**: optional hooks (validation and handler failures are also mapped to **`serverError`** frames).
21
+ - **`serializeJson`**, **`deserializeJson`**: optional; default **`JSON.stringify`/`JSON.parse`** for JSON mode.
22
+
23
+ ## Errors
24
+
25
+ - Throw **`SockaError`** from handlers for domain failures; the session maps the message to a **`serverError`** frame so the client can **`instanceof SockaError`** when using **`SockaSession`**.
26
+
27
+ ## Client parity
28
+
29
+ - Same **`defineSocka`** contract on the client: **`useSockaSession`**, **`SockaSessionProvider`**, or **`SockaSession`** with matching **`wireFormat`**.
30
+
31
+ ## Transport
32
+
33
+ - Raw WebSocket lifecycle stays in **`@firtoz/websocket-do`**. Socka adds schema validation and socka v1 framing at the session boundary. Do **not** re-export websocket-do symbols from socka—import **`BaseWebSocketDO`** / **`BaseSession`** from **`@firtoz/websocket-do`** when you need them.