kolu
Docs

How to choose a link

A link is how a client reaches a served contract. Every link produces the same typed client, so the choice is purely about transport — and you can change it later without touching a single call site.

Four links, one clientFour transports — directLink, websocketLink, unixSocketLink, stdioLink — all produce the same typed client. The three wire links must pass through a watchdog that tells live from dead; directLink is always-live and may be used bare.directLinkin-process · no wirealways-live · usable barewebsocketLinkbrowser ⇄ NodeunixSocketLinksame-machine daemonstdioLinksubprocess / sshwatchdogconnectSurface /makeSessiontells live from deadno watchdog neededsurfaceClientthe SAME typed clientapp.cells.load.use(…)app.rpc.surface.proc.kill(…)byte-identical across linksFour different pipes, one identical client shape. Choose the nearest transport now; swap it later without touching a call site.A wire link can silently half-open, so it needs the watchdog. directLink is honestly always-live and may go bare.
Every link produces the same typed client, so the choice is only about transport. The three wire links route through a watchdog that can tell live from dead; directLink is in-process and always-live, so it may be passed bare.

Pick by where the server is

Where the server runs Link Import
In the same process (tests, single-process apps) directLink(router) @kolu/surface/links/direct
In a browser, talking to a Node server websocketLink(ws) @kolu/surface/links/websocket
A daemon on the same machine unixSocketLink({ socketPath }) @kolu/surface/links/unix-socket
A subprocess, or a remote box over ssh stdioLink({ read, write }) @kolu/surface/links/stdio

Match each to its serve side: nothing for directLink (it wraps the router directly), RPCHandler(...).upgrade(ws) for the browser, serveOverUnixSocket for the daemon, serveOverStdio for the subprocess or ssh peer.

  • directLink is the identity element: feed it the router from implementSurface(surface, deps).router and every call runs the handler in-process. Use it for tests and for the in-process phase of a service you will later put behind a socket.
  • unixSocketLink dials the socket it opens, so it is async — it returns a Promise<{ client, dispose }>. The others hand back the client synchronously.
  • stdioLink carries both a local subprocess and an ssh peer; over ssh you usually drive it through makeSession (see Mirror over ssh) rather than wiring read/write by hand.

Because the client type is identical across links, a swap is a one-line change at the wiring seam. A surface that started in-process for tests:

// Was: in-process
const app = surfaceClient(
  surface,
  directLink<typeof surface.contract>(fragment.router),
);

moves to the browser by changing only the link:

// Now: over a WebSocket, via the app layer's watchdog-backed connect
const { client } = connectSurface({ surface, url });
const app = client; // same bound hooks, same call sites

Everything downstream — app.cells.load.use(...), app.rpc.surface.proc.kill(...) — is byte-identical. That is the point of the link abstraction: choose the nearest transport now, and let the surface follow the code out to a socket or a fleet later.