@firtoz/socka
Version:
Standard Schema–first WebSocket RPC for TypeScript — Bun, Hono, Node ws, Cloudflare Workers, Durable Objects
132 lines (97 loc) • 11.3 kB
Markdown
# Reference
User-facing **API** and **configuration**. For **wire protocol details** (frame kinds, transport layers, `decodeSockaWire`), see **[Internals](./internals.md)**.
## Type inference
```ts
import type {
InferSockaSend,
InferSockaHandlers,
InferSockaPushHandlers,
InferSockaPushPayload,
} from "@firtoz/socka/core";
type Send = InferSockaSend<typeof myContract>;
type Handlers = InferSockaHandlers<
typeof myContract,
SockaWebSocketSession<typeof myContract>
>;
type PushHandlers = Partial<InferSockaPushHandlers<typeof myContract>>;
type RoomMessagePayload = InferSockaPushPayload<typeof myContract, "roomMessage">;
```
**`InferSockaSend`** — Call names become methods on **`session.send`**; inputs/outputs follow the contract. **`InferSockaHandlers`** — server handler arity matches **`calls`** (with or without `input`). **`InferSockaPushPayload`** — payload type for a given push name. Use **`Partial<InferSockaPushHandlers<typeof myContract>>`** (or **`satisfies Partial<...>`** on an object literal) to type **`pushHandlers`** on **`SockaSession`** / **`useSockaSession`** — see **[Pushes — Typing `pushHandlers`](./pushes.md#typing-pushhandlers)**.
### TypeScript and exact optional properties
If you define **hand-written** types next to Zod (or other Standard Schema) objects with **`z.optional()`** (or **`.optional()`** on a field) and you enable TypeScript’s **`exactOptionalPropertyTypes`**, a property that is “optional” in the schema is often **inferred** as **`name?: T | undefined`**, not **`name?: T`**. A plain **`pressure?: number`** in your `type` can therefore **widen** differently from `z.infer<typeof pointSchema>`, and **contract** inference (or assignment to a handler parameter) can fail.
**Remedies:** prefer **`z.infer<typeof pointSchema>`** (or the inferred output type of your field) for the type; or in hand-written types, include **`| undefined`**, e.g. **`pressure?: number | undefined`**, so they match the schema end to end.
### Optional output (fire-and-forget)
If a call omits **`output`**, the server sends **no** **`serverResponse`** on success, and the client **`send`** method returns **`Promise<void>`** that resolves after the request is queued to the socket (not after the server runs the handler). **`output: z.void()`** keeps full request/response: the server still sends **`serverResponse`** and the client **`await`** waits for it. For output-less calls, server **`serverError`** frames include an optional **`rpc`** field so **`reportError`** can attribute failures when there is no pending promise. For **client** observability when you cannot rely on **`.catch`** on **`send`**, see **[Client — Fire-and-forget observability](./client.md#fire-and-forget-observability)**.
## Errors and observability
| Concern | Hook |
|--------|------|
| Exceptions inside **RPC handlers** | `onHandlerError` on `SockaWebSocketSessionConfig` / `SockaDoSessionConfig` |
| Invalid **inbound wire** payloads (before your handler runs) | `onValidationError` on the same config |
| 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 |
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.
## Wire encoding: JSON and msgpack
- 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).
- **`"msgpack"`** uses **binary** frames; use it only when **both** ends opt in.
- **RPCs and typed pushes** share one encoding — there is no separate “push encoding.”
Tradeoffs (bundle size, CPU, debuggability): **[Wire format](./wire-format.md)**.
Tables, logical frame kinds (`clientRequest`, `serverResponse`, …), and **`dispatchSockaInboundMessage`** details: **[Internals](./internals.md)**.
## RPC handler errors
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`**:
```ts
throw new SockaError("Not allowed", { code: "FORBIDDEN", data: { reason: "…" } });
```
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.
## Server session configuration
**`SockaWebSocketSessionConfig`** (`@firtoz/socka/server`, Bun, Hono) and **`SockaDoSessionConfig`** (`@firtoz/socka/do`) share the same fields except **`createData`** (see below).
| Field | Purpose |
|--------|---------|
| **`contract`** | `defineSocka(...)` — `calls`, optional `pushes`. |
| **`wireFormat`** | `"json"` (default) or `"msgpack"` — must match clients. |
| **`handlers`** | Typed call implementations; arity follows input schema (see [Getting started](./getting-started.md)). |
| **`handleClose`** | Async per-socket teardown; runs **before** removal from `sessions` (see [Lifecycle](./lifecycle.md)). |
| **`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)**. |
| **`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)**. |
| **`onAttached`** | Optional: after registration in `sessions` (safe for broadcasts). |
| **`onHandlerError`** | Observes thrown errors in handlers (after optional `SockaError` wrapping for the wire). |
| **`onValidationError`** | Inbound frame failed schema / wire decode before your handler. |
| **`reportError`** | Non-RPC failures (`onAttached`, adapters, …); see **Errors and observability** above. |
| **`serializeJson` / `deserializeJson`** | Replace JSON stringify/parse for **JSON wire mode** only. |
## Client configuration
| Field | Purpose |
|--------|---------|
| **`contract`** | Same module reference as the server. |
| **`url`** | `new WebSocket(url)` when **`webSocket`** is omitted. |
| **`webSocket`** | Inject an existing socket (tests or custom setup); **`url`** not required if set. |
| **`wireFormat`** | Must match the server session (**default `"json"`**). |
| **`autoConnect`** | Default **`true`**. If **`false`**, call **`connect()`** before **`session.send`** (or rely on implicit open from **`send`**). |
| **`serializeJson` / `deserializeJson`** | Same as server — JSON wire mode only. |
| **`onOpen` / `onClose` / `onError`** | WebSocket lifecycle. |
| **`reconnect`** | **`false`** or backoff options — default **on** for **`url`**, **off** for injected **`webSocket`** unless overridden. See **[Reconnection](./reconnection.md)**. |
| **`onReconnecting` / `onReconnected`** | Reconnect lifecycle (**`SockaWebSocketClient`** / **`SockaSession`**). |
| **`onValidationError`** | Inbound frame failed validation (**`SockaWebSocketClient`**). |
| **`pushHandlers`** | Initial **`session.subscribe`** subscriptions (**`SockaSession`** only). |
| **`reportError`** | Client pipeline failures (listeners, validation); see **Errors and observability**. |
**`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)**.
**`SockaSession`** / **`SockaWebSocketClient`** also expose **`status`** and **`onStatusChange`** (connection lifecycle, reconnect UI).
## Schema libraries
Anything that implements **Standard Schema v1** works — **Zod**, **Valibot**, **ArkType**, or a custom implementation. Pass schemas straight into **`defineSocka`**; no adapter helpers required.
## At a glance
| | |
|---:|---|
| **Contract** | `defineSocka({ calls, pushes? })` — Zod, Valibot, ArkType, or any [Standard Schema v1](https://standardschema.dev/) |
| **Client** | `SockaSession` / `useSockaSession` / `SockaSessionProvider` + `useSockaSessionContext` |
| **Server** | `SockaDoSession` + `SockaWebSocketDO` on Durable Objects, or **`@firtoz/socka/server`** / **`@firtoz/socka/bun`** / **`@firtoz/socka/hono`** on any supported runtime |
| **Wire** | JSON text frames by default; optional **msgpack** binary — same logical frames, both ends must use the same `wireFormat` |
### Imports
| Path | Use for |
|------|---------|
| `@firtoz/socka` | Same as **`@firtoz/socka/core`** — `defineSocka`, wire helpers, errors, types (prefer explicit **`/core`** in examples) |
| `@firtoz/socka/core` | `defineSocka`, wire helpers, `SockaError`, `SockaReportError`, `reportSockaError`, types. Export map includes **`require`** / **`default`** for Node tooling (e.g. drizzle-kit) that resolves CJS. Keep **constants-only** modules (no socka import) beside Drizzle schema when possible. |
| `@firtoz/socka/client` | `SockaSession`, `SockaWebSocketClient` (also re-exports `SockaReportError`, `reportSockaError`) |
| `@firtoz/socka/test` | `createFakeWebSocket` for unit tests — see **[Testing](./testing.md)** |
| `@firtoz/socka/react` | `useSocka`, `useSockaSession`, `useSockaPresence`, provider + context |
| `@firtoz/socka/do` | `SockaDoSession`, `SockaDoHost`, `SockaWebSocketDO`, `SockaWebSocketDOBase`, `SockaDoSessionConfigInput`, `broadcastPushToAll` (on DO) |
| `@firtoz/socka/server` | `SockaWebSocketSession`, `attachSockaWebSocket`, `dispatchSockaInboundMessage`, `broadcastContractPushToAll`, `broadcastSockaEventToAll`, `broadcastSockaEventToPeers` |
| `@firtoz/socka/bun` | `createSockaBunWebSocketHandlers` for **`Bun.serve`** |
| `@firtoz/socka/hono` | `sockaHonoNodeWs` for **`@hono/node-ws`** |
| `@firtoz/socka/hono/cloudflare` | `sockaHonoCloudflare` for **`hono/cloudflare-workers`** |