UNPKG

@firtoz/socka

Version:

Standard Schema–first WebSocket RPC for TypeScript — Bun, Hono, Node ws, Cloudflare Workers, Durable Objects

156 lines (106 loc) • 10.6 kB
# Client ## Vanilla (`SockaSession`) Outside React, construct **`SockaSession`** from **`@firtoz/socka/client`** with the same **`contract`** as the server and a **`url`** (or a prebuilt **`webSocket`**). **`wireFormat`** defaults to **`"json"`** and must match every server session that receives this connection. | Option | Typical use | |--------|-------------| | **`contract`**, **`url`** / **`webSocket`** | Required pairing: open socket to your upgrade URL, or inject a socket for tests. | | **`wireFormat`** | **`"json"`** (text frames) vs **`"msgpack"`** (binary); must match the **server** session config for this connection. | | **`autoConnect: false`** | Defer opening until **`await session.connect()`** (or first **`send`**). | | **`serializeJson` / `deserializeJson`** | Custom JSON handling for the **outer** frame in JSON mode (e.g. replacers). | | **`onOpen` / `onClose` / `onError`** | Lifecycle and telemetry. | | **`pushHandlers`** | Up-front subscriptions for contract **`pushes`** (same as **`session.subscribe.on`**). | | **`reportError`** | Non-RPC client pipeline failures; defaults to **`console.error`**. | Full list: **[Reference — Client configuration](./reference.md#client-configuration)**. ### Fire-and-forget Calls that **omit** **`output`** resolve **`await session.send.*`** as soon as the request is sent; they do **not** wait for **`serverResponse`**. If the server returns **`serverError`**, handle it with **`reportError`** (see **[Reference — Errors](./reference.md#errors-and-observability)**) — there is no rejected promise for that call. Prefer **`output: z.void()`** when you still want **`await`** to track success or failure through the returned promise. ### Fire-and-forget observability For **output-less** calls, the **`send`** method returns a **`Promise<void>`** that resolves when the **outbound request is queued**; it does **not** reject if the server later answers with **`serverError`**. Patterns like **`void send.sendCursor({ ... }).catch(() => undefined)`** therefore **do not** observe **server-side** handler failures, validation errors on the response path, or structured **`SockaError`** for that procedure. - **Client** — pass **`reportError`** to **`SockaSession`** or **`useSockaSession`** / **`useSocka`** (and **`SockaSessionProvider`**, which forwards session options). Inspect **`event.kind`**; fire-and-forget server failures often surface as **`clientFireAndForgetRpcError`**. See **[Reference — Optional output (fire-and-forget)](./reference.md#optional-output-fire-and-forget)** and **[Reference — RPC handler errors](./reference.md#rpc-handler-errors)**. - **Server** — use **`onHandlerError`** and **`onValidationError`** on **`SockaWebSocketSessionConfig`** / **`SockaDoSessionConfig`** for logs and metrics when a handler throws or inbound frames fail before your handler. **Call names** — For literal `calls` objects, **`defineSocka`** rejects names that would make **`session.send`** Promise-like or clash with object shape (e.g. **`then`**, **`toString`**). If you use a wide **`Record<string, SockaProcedureDef>`**, TypeScript cannot apply that check; **`SockaSession`** still validates at construction (see **`RESERVED_SOCKA_PROCEDURE_NAMES`** in **`@firtoz/socka/core`**). ```ts import { SockaSession } from "@firtoz/socka/client"; import { myContract } from "./contract"; const session = new SockaSession({ contract: myContract, url: "wss://example.com/ws" }); const rows = await session.send.list(); ``` Use **`SockaWebSocketClient`** directly if you need **`onResponse` / `onServerError` / `onEvent`** frame hooks without **`SockaSession`**’s typed **`send`** / **`subscribe`**; most apps use **`SockaSession`**. **Connection status** — **`SockaWebSocketClient`** and **`SockaSession`** expose **`status`** (`"idle" | "connecting" | "open" | "reconnecting" | "closed"`) and **`onStatusChange`** for UI (e.g. “Reconnecting…”). Same fields power the React hooks below. ## React ### `useSockaSession` — typed `send` Use **`useSockaSession(contract, options, deps)`** when you want **`send.*`** RPC methods directly (same shape as **`session.send`**). **`ready`** flips to **`true`** after the socket opens; **`deps`** remount the connection when identity (e.g. room id) changes. Also returns **`status`**, **`reconnecting`**, and **`reconnectAttempt`** (see **`useSocka`**). ```ts import { useSockaSession } from "@firtoz/socka/react"; import { myContract } from "./contract"; function App() { const { ready, send } = useSockaSession(myContract, { url: "ws://..." }, []); // After `ready`, call `send.list()` / `send.insert(...)` from effects or event handlers (not during render). return null; } // Binary frames — set the same `wireFormat` on the server session useSockaSession(myContract, { url: "wss://...", wireFormat: "msgpack" }, []); ``` Optional **`pushHandlers`** in the second argument matches **`Partial<InferSockaPushHandlers<typeof myContract>>`** — see **[Pushes — Typing `pushHandlers`](./pushes.md#typing-pushhandlers)**. ### SSR and WebSocket URLs `useSockaSession` and **`SockaSessionProvider`** need a real **`url`** for **`new WebSocket(url)`**. In **React Router SSR** (or any **SSR** tree), `window` is missing on the server, and derived **`ws:` / `wss:`** URLs must not run during **render** with a **fake** placeholder like `ws://127.0.0.1/...` (it can connect to the wrong place or open before you know the page origin). **Recommended pattern:** In the route module, set **`const [url, setUrl] = useState<string | null>(null)`**, and in **`useEffect`** (browser-only), build a **`wss://` / `ws://`** URL from **`window.location`** (protocol, host, path to your Worker’s WebSocket route). If **`url === null`**, render a **loading** shell; only render a child component that calls **`useSockaSession(myContract, { url, pushHandlers? }, [url])`** when **`url`** is set. Keep **`url`** in the hook **`deps`** so identity changes re-open the right socket. **SSR checklist:** 1. **Never** call **`useSockaSession`** with a placeholder URL during SSR — the hook connects in **`useEffect`** and will throw if **`url`** is missing at runtime. 2. **Parent gates the child** — render **`ClientOnly`** / **`url === null ? <Loading /> : <ChatWithSocket url={url} />`** so the hook runs only in the browser with a real URL. 3. **Build the URL in `useEffect`** — derive **`wss:`** from **`window.location`**, room id from route params, and optional auth query params there (not during render). 4. **Pass `[url]` in hook deps** — room or tenant changes should remount the connection. 5. **`SockaSessionProvider`** follows the same rules — wrap only after **`url`** is known. **React + Durable Object** end-to-end wiring: **[React + Durable Objects](./react-durable-objects.md)**. ### `useSocka` — hold a `SockaSession` ref Use **`useSocka(options, deps)`** when you need the full **`SockaSession`** (e.g. **`session.subscribe`**, **`session.client`**, **`waitForPush`**, or passing the session into non-React helpers). It returns **`{ ready, sessionRef, status, reconnecting, reconnectAttempt }`** — read **`sessionRef.current`** in effects or callbacks (it is **`null`** until the effect runs). Use **`reconnecting`** or **`status === "reconnecting"`** for banners; **`reconnectAttempt`** counts backoff attempts. | | **`useSockaSession`** | **`useSocka`** | |--|------------------------|----------------| | **Returns** | **`{ ready, send, status, reconnecting, reconnectAttempt }`** | **`{ ready, sessionRef, status, reconnecting, reconnectAttempt }`** | | **Best for** | Most React UIs that only call RPCs | Subscriptions, low-level client access, imperative APIs | ```tsx import { useEffect } from "react"; import { useSocka } from "@firtoz/socka/react"; import { myContract } from "./contract"; function App() { const { ready, sessionRef } = useSocka({ contract: myContract, url: "ws://..." }, []); useEffect(() => { const s = sessionRef.current; if (!ready || !s) return; const onNotify = (p: { msg: string }) => console.log(p.msg); s.subscribe.on("notify", onNotify); return () => s.subscribe.off("notify", onNotify); }, [ready, sessionRef]); return null; } ``` *(Example assumes your contract defines a **`notify`** push; use the real push names from **`myContract`**.)* ### `useSockaPresence` — snapshot + join/leave deltas Call **`useSockaPresence(sessionRef, ready, options, deps)`** after **`useSocka`** / **`useSockaSession`**: it runs your **`snapshot`** RPC once, subscribes to **`joinPush`** / **`leavePush`**, and returns **`{ users, selfUserId, loading }`**. Pass the same **`deps`** as the connection when room identity changes. ### One WebSocket for the whole tree If many components need **`send`**, avoid calling **`useSockaSession`** in each one (each call owns a connection). Mount a provider once and read the session from context: ```tsx import { SockaSessionProvider, useSockaSessionContext } from "@firtoz/socka/react"; import { myContract } from "./contract"; function Layout({ roomId }: { roomId: string }) { return ( <SockaSessionProvider contract={myContract} deps={[roomId]} url={`wss://example.com/ws/${roomId}`} > <Child /> </SockaSessionProvider> ); } function Child() { const { ready, send, status, reconnecting } = useSockaSessionContext(myContract); // ... } ``` Use the **same `contract` reference** on the provider and in **`useSockaSessionContext`** (checked at runtime). ## Deferred WebSocket connect Use **`autoConnect: false`** on **`SockaWebSocketClient`** / **`SockaSession`** when you want to open the socket later (e.g. after user action). Call **`await session.connect()`** or **`await client.connect()`** before calls; each **`send`** call also awaits connect when the socket is not open yet. ## Client lifecycle Treat each **`SockaSession`** / **`SockaWebSocketClient`** as bound to **one** underlying **`WebSocket`**. When the socket closes, pending calls reject on that instance unless you opt into client-side reconnect (see **[Reconnection](./reconnection.md)**). For a deliberate room/url change, construct a **new** client (in React, remount **`useSockaSession`** / **`SockaSessionProvider`** when **`url`** or identity **`deps`** change). Use **`ready`** / **`waitForOpen()`** before assuming the connection is usable; use **`onClose`** / **`onError`** (or **`reportError`**) for telemetry. ## Pushes Server push and client subscriptions are covered in **[Pushes](./pushes.md)**.