How to serve a map of surfaces
Mirroring one surface fronts a single host. A fleet
app fronts N of them, of the same shape, with the set changing at runtime. This
guide serves that fleet as one @kolu/surface-map map: define the entry surface
once, hold a binding per host, and bridge those bindings into the map’s
MapRegistry. drishti — a browser fronting an ssh fleet — is the worked example
throughout.
1. Define the map
defineSurfaceMap(keySchema, entry, codec) takes the per-host entry surface and
produces its map form: every member’s input gains a key so one socket carries all
instances. The codec bridges your Key to the wire string every channel is
keyed on; for a plain-string key it is the identity pair.
const HostKeySchema = z.string();
const identityCodec: KeyCodec<string> = { encode: (k) => k, decode: (s) => s };
const hostMap = defineSurfaceMap(HostKeySchema, entry, identityCodec);
2. Build one binding per host
Hold one binding per host: a makeSession over sshConnector (per
Mirror over ssh), a pump that folds the agent’s frames
into a local mirror, and an in-process link the map forwards member calls to.
Its state() projects the session’s connection state onto the map’s per-entry
state, so a fault on one box surfaces on exactly that box’s chip — warming while
it retries, failed only on a standing fault — never as a fleet-wide crash.
function buildHostBinding(host: string, agentDrv: string): HostBinding {
const session: Session<
AgentClient<typeof entry.contract>,
SshProv
> = makeSession({
initialConnection: "probing",
connectOnce: sshConnector<typeof entry.contract>({
host,
binary: "fleet-top-agent",
resolveDrvPath: () => Promise.resolve(agentDrv), // deferred per dial
}),
});
const processes = new Map<Pid, Proc>();
const fragment = implementSurface(entry, {
channel: inMemoryChannelByName(),
cells: { load: { store: inMemoryStore(DEFAULT_LOAD) } },
collections: {
processes: {
readAll: () => processes,
upsert: (k, v) => {
processes.set(k, v);
},
remove: (k) => {
processes.delete(k);
},
},
},
procedures: {
proc: {
// `kill` forwards to the CURRENT live agent client — a kill can land
// across a reconnect, so never a per-spawn stub.
kill: async ({ input }) => {
const pending = session.currentClient();
if (pending === null) throw new Error("no live agent link");
return (await pending).surface.proc.kill(input);
},
},
},
});
// Fold the agent's frames into the local fragment; the first `load` frame is
// the handshake that flips the session to `connected`.
let firstLoad = true;
void pumpRemoteSurface({
source: entry,
session,
makeSink: ({ seq: _seq }) => {
firstLoad = true;
return {
cells: {
load: (v) => {
if (firstLoad) {
firstLoad = false;
session.markConnected();
}
fragment.ctx.cells.load.set(v);
},
},
collections: {
processes: {
upsert: (k, v) => fragment.ctx.collections.processes.upsert(k, v),
remove: (k) => fragment.ctx.collections.processes.remove(k),
},
},
};
},
});
const router = implement(entry.contract).router({ ...fragment.router });
const link = directLink<typeof entry.contract>(router);
let latest: SessionState<SshProv> = { phase: "probing", log: [], sinceMs: 0 };
const unsub = session.onState((s) => {
latest = s;
});
return {
link,
state: () => projectState(latest),
onStateChange: (cb) => session.onState(() => cb()),
destroy: () => {
unsub();
session.destroy();
},
};
}
3. Bridge the bindings into a MapRegistry
serveSurfaceMap takes a MapRegistry — the one writer of membership. Build it
over your bindings: members()/has() answer from the binding set, subscribe
fires on any host’s state change, and resolve(host) hands the map that host’s
link + projected state (or a fault for an unknown key). There is no second
writer of membership.
// The hand-built MapRegistry is the ONE writer of membership; `resolve(host)`
// hands the map each host's link + projected connection state.
const registry: MapRegistry<string, "copying", string> = {
members: () => [...bindings.keys()],
subscribe: (onChange) => {
changeCbs.add(onChange);
return () => changeCbs.delete(onChange);
},
has: (k) => bindings.has(k),
resolve: (k): EntrySession<"copying"> | EntryFault => {
const b = bindings.get(k);
if (b === undefined)
return { kind: "fault", failed: `unknown host: ${k}` };
return { kind: "session", link: b.link, state: b.state() };
},
};
4. Serve the map
serveSurfaceMap(map, registry) publishes entries and derives each entry’s
EntryStatus by projecting its session’s connection state — you never write
status yourself. It returns a finalized top-level router — hand it straight to
a transport (a directLink in-process, or an HTTP+WebSocket serve path); do not
flatten it further with implement(...).router({...}) (that flatten is only for
an implementSurface fragment).
// `serveSurfaceMap` returns a FINALIZED top-level router — hand it straight to
// a transport (here an in-process `directLink`); do NOT flatten it further.
const { router, dispose } = serveSurfaceMap(hostMap, registry);
The client half — reading this map back as chips and a switchable canvas — is The client half; the exact registry and wire types are in the @kolu/surface-map reference.