@wwind/client
v0.2.0
Published
Browser-side WebSocket client and wire types for @wwind/native runtimes. Zero-dep core, optional React bindings.
Maintainers
Readme
@wwind/client
Browser-side WebSocket client and wire types for @wwind/native runtimes.
Zero dependencies. Framework-agnostic. Targets ES2022 + DOM. Use it from React, Vue, Svelte, Solid, vanilla, anything.
Install
npm install @wwind/clientUse
import { WarmwindClient, type ViewFrame } from "@wwind/client";
const client = new WarmwindClient({
wsUrl: "wss://your-app.warmwind.space/ws",
httpBase: "https://your-app.warmwind.space",
});
client.onConnect((connected) => console.log("ws", connected));
client.onFrame((frame: ViewFrame) => render(frame));
client.connect();
// Invoke an action exposed by the runtime.
await client.invoke("pdf-viewer.open", { path: "/files/manual.pdf" });
// History controls.
await client.undo();
await client.redo();
await client.jump(3);When the app is served from the same origin as the runtime (the
common case), both wsUrl and httpBase can be omitted. The client
will derive ws(s)://<host>/ws and use relative HTTP paths.
Auth-validating proxies
Pass getToken when the runtime sits behind an auth-validating
proxy. It runs fresh before every WebSocket open and every HTTP
call, so an upstream refresher can rotate the token without
reconnecting the client:
const client = new WarmwindClient({
wsUrl: "wss://gateway.warmwind.space/app/<id>/ws",
httpBase: "https://gateway.warmwind.space/app/<id>",
getToken: () => session.fetchAccessToken(),
});- WebSocket: appended as
?token=<value>(&token=if the URL already has a query string). - HTTP: sent as
Authorization: Bearer <value>on every request.
React
import {
useWarmwindClient, useWarmwindFrame,
useWarmwindConnection, useWarmwindAction,
} from "@wwind/client/react";
function App() {
const client = useWarmwindClient({ getToken });
const frame = useWarmwindFrame(client);
const connected = useWarmwindConnection(client);
const open = useWarmwindAction(client, "pdf-viewer.open");
if (!connected) return <Spinner />;
if (!frame) return null;
return <Renderer view={frame.view} onOpen={(p) => open({ path: p })} />;
}react is an optional peer dependency; the vanilla WarmwindClient
import has no React in its closure.
Wire protocol
The runtime is the source of truth. The client subscribes to its view tree over a single WebSocket and posts user intents back over plain HTTP. No bespoke RPC, no client-side state machine.
Server → client (over WS)
{ kind: "frame"; rev: number; frame: ViewFrame }
{ kind: "patch"; baseRev: number; rev: number; patches: ViewPatchOp[] }
{ kind: "reload"; reason?: string }framecarries a full view snapshot (view,stack,tools,history). Sent on connect and after any change the runtime decides is too large to send as a delta.patchis a batch of RFC 6901 JSON-Patch ops applied on top of the previous frame. Drop the patch and refetch viaGET /api/viewifbaseRev !== latest_rev.reloadis a dev-mode signal that the renderer bundle changed.revis monotonic per session.
Client → server (over HTTP)
POST /api/invoke { action_id: string, inputs: object } → InvokeResult
POST /api/undo → 204
POST /api/redo → 204
POST /api/jump { index: number } → 204
GET /api/view → ViewFrame
(response header: X-Warmwind-Rev)InvokeResult is { ok: true, result? } | { ok: false, error: { code, message } }.
That's the entire surface.
Types
All wire types are exported and stable across SDK minor versions:
import type {
ViewFrame, ViewNode, ViewAction, ActionInput,
DerivedView, ToolDescriptor, HistoryFrame,
PageView, PageMode,
ViewPatchOp, WireMessage,
} from "@wwind/client";applyPatch(frame, op) and applyPatches(frame, ops) are exposed
if you need to apply deltas yourself.
License
MIT
