How to consume a surface outside SolidJS
The .use() hooks are SolidJS. A CLI or TUI has no reactive runtime, so it reads
the surface directly off the typed client. The trick that keeps a CLI small is to
hold one transport-blind connection so every command is written once,
whether it runs against a local unix socket or a remote box over ssh.
Hold one connection shape
Model the connection as { client, dispose } and dial it once. Local and remote
differ only in which link you build; the commands downstream never branch.
type Connection = {
client: SurfaceFace;
dispose: () => Promise<void>;
};
// local: dial the daemon's unix socket, then re-nest the flat wire tags into
// `client.surface.<member>.<verb>`. Every link factory returns the same
// `{ dispatch, dispose }` pair, so swapping transport is a one-line change.
const link = await unixSocketLink({ group: surface.group, socketPath });
const client = buildSurfaceFace(surface, link.dispatch);
const dispose = () => link.dispose();
Call a procedure
A procedure call off the client is an Effect — a description of the call, not a
request already in flight. No hooks, no subscription. Compose it into the
command’s own program and run that once, at the CLI’s main; then a Ctrl-C, a
deadline, or a lost race reaches the in-flight call instead of leaving it running
unobserved.
// A unary verb is a lazy `Effect`; a streaming verb is a lazy `Stream`, and a
// snapshot-then-deltas member opens with its snapshot — so `firstFrameOrThrow`
// IS the one-shot read, and it interrupts the subscription as soon as that frame
// lands. Nothing below has dispatched yet: this is one description, built.
const readKeys = Effect.gen(function* () {
yield* kill({ pid });
return yield* firstFrameOrThrow(
processKeys(undefined),
"processes keys yielded no snapshot frame — link failure",
);
});
Iterate a stream
A cell, collection, or stream read is a lazy Stream — snapshot first, then
deltas. Running it is the subscription; loop it and act on each frame.
// Consume every frame. Running the stream IS the subscription; interrupting the
// fiber (or the effect completing) tears the wire subscription down — there is
// no `AbortSignal` to thread and none to forget.
const tailLog = Stream.runForEach(nodeLog(nodeId), (frame) =>
Effect.sync(() => process.stdout.write(frame.text)),
);
For a raw (non-descriptor) stream, reach for unenrolledStreamCall
(@kolu/surface/client) rather than calling the procedure bare — it carries the
STREAM_RETRY reconnect context, so a dropped transport re-subscribes
transparently instead of ending the loop.
// The framework's per-subscription RETRY FENCE, without enrolling the stream in
// any `client.health()` fact: a transport drop re-subscribes transparently
// (forever), a DECLARED error never does. `onRetry` fires between the failed
// attempt's last frame and the next attempt's first — clear the view there.
const fenced = unenrolledStreamCall(nodeLog, nodeId, {
onRetry: () => resetView(),
});
const renderLog = Stream.runForEach(fenced, (frame) =>
Effect.sync(() => render(frame)),
);
Drive a live board with mirrorRemoteSurface
When the CLI is a live dashboard rather than a one-shot query, fold the surface
into plain callbacks with mirrorRemoteSurface (@kolu/surface/mirror) — the
consume-side dual of implementSurface. You supply a sink of collection and
stream callbacks; it keeps them current and hands back the procedures plus a
done promise.
const { procedures, done } = mirrorRemoteSurface(
surface,
client,
{
collections: {
processes: {
upsert: (p, proc) => board.set(p, proc),
remove: (p) => board.delete(p),
},
},
streams: { nodeLog: { input: nodeId, onFrame: (f) => board.appendLog(f) } },
},
// The mirror's own cancellation vocabulary for non-Effect callers — it is
// translated into ONE fiber interrupt at this edge.
{ signal },
);
// The mirrored procedures are the SAME shape the face hands back — an `Effect` —
// so a forwarded call composes into the program exactly like a direct one.
const killThroughMirror = procedures.proc.kill({ pid });