kolu
Docs

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.