@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.
- package/README.md +195 -42
- package/dist/SockaWebSocketSession-B1w7RAid.d.ts +209 -0
- package/dist/bun/index.d.ts +30 -5
- package/dist/bun/index.js +28 -5
- package/dist/bun/index.js.map +1 -1
- package/dist/{chunk-MZCQHJXY.js → chunk-IFIGKR3W.js} +45 -8
- package/dist/chunk-IFIGKR3W.js.map +1 -0
- package/dist/{chunk-45D4T232.js → chunk-LVVCHLNW.js} +74 -9
- package/dist/chunk-LVVCHLNW.js.map +1 -0
- package/dist/{chunk-AM7PB26G.js → chunk-P3JEEOJL.js} +192 -10
- package/dist/chunk-P3JEEOJL.js.map +1 -0
- package/dist/chunk-QGURL3DJ.js +8 -0
- package/dist/chunk-QGURL3DJ.js.map +1 -0
- package/dist/client/index.d.ts +59 -3
- package/dist/client/index.js +2 -2
- package/dist/core/index.d.ts +2 -21
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/do/index.d.ts +20 -2
- package/dist/do/index.js +36 -2
- package/dist/do/index.js.map +1 -1
- package/dist/hono/cloudflare-workers.d.ts +4 -4
- package/dist/hono/cloudflare-workers.js +4 -3
- package/dist/hono/cloudflare-workers.js.map +1 -1
- package/dist/hono/index.d.ts +22 -6
- package/dist/hono/index.js +5 -3
- package/dist/hono/index.js.map +1 -1
- package/dist/react/index.d.ts +43 -4
- package/dist/react/index.js +103 -9
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.d.ts +18 -5
- package/dist/server/index.js +24 -4
- package/dist/server/index.js.map +1 -1
- package/dist/{socka-report-error-DzFI2Tr7.d.ts → socka-report-error-CXwpAUgl.d.ts} +80 -8
- package/dist/test/index.d.ts +11 -0
- package/dist/test/index.js +84 -0
- package/dist/test/index.js.map +1 -0
- package/docs/README.md +16 -7
- package/docs/auth.md +27 -0
- package/docs/backpressure.md +16 -0
- package/docs/client.md +48 -3
- package/docs/comparison.md +2 -2
- package/docs/durable-objects.md +3 -3
- package/docs/getting-started.md +143 -84
- package/docs/history.md +26 -0
- package/docs/internals.md +56 -0
- package/docs/lifecycle.md +3 -3
- package/docs/multi-room.md +10 -8
- package/docs/peers.md +11 -7
- package/docs/presence.md +43 -0
- package/docs/{events.md → pushes.md} +1 -1
- package/docs/recipes.md +77 -0
- package/docs/reconnection.md +44 -0
- package/docs/reference.md +27 -32
- package/docs/server.md +19 -3
- package/docs/testing.md +20 -0
- package/docs/wire-format.md +29 -0
- package/examples/minimal-socka.ts +56 -3
- package/package.json +14 -10
- package/roadmap.md +2 -2
- package/skills/socka/core-rpc/SKILL.md +2 -2
- package/skills/socka/do-session/SKILL.md +2 -2
- package/skills/socka/standard-schema/SKILL.md +1 -1
- package/dist/SockaWebSocketSession-Bru8yFcK.d.ts +0 -107
- package/dist/chunk-45D4T232.js.map +0 -1
- package/dist/chunk-AM7PB26G.js.map +0 -1
- package/dist/chunk-MZCQHJXY.js.map +0 -1
package/docs/recipes.md
ADDED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
**
|
|
39
|
+
Tradeoffs (bundle size, CPU, debuggability): **[Wire format](./wire-format.md)**.
|
|
41
40
|
|
|
42
|
-
|
|
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
|
-
##
|
|
43
|
+
## RPC handler errors
|
|
48
44
|
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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:
|
|
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/
|
|
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
|
|
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. |
|
|
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
|
-
|
|
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`**.
|
package/docs/testing.md
ADDED
|
@@ -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
|
-
|
|
26
|
+
sendMessage: {
|
|
14
27
|
input: z.object({ text: z.string() }),
|
|
15
|
-
output: z.object({
|
|
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
|
-
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
133
|
-
"@happy-dom/global-registrator": "^20.
|
|
134
|
-
"@hono/node-server": "^1.19.
|
|
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.
|
|
141
|
-
"happy-dom": "^20.
|
|
142
|
-
"react-dom": "19.2.
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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>`**).
|
|
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
|
|
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`
|
|
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
|
|