Skip to content

In-process clients

datadata’s client and server are connected by an event bus abstraction, not by a socket. One implementation routes events over WebSockets; another — the in-process event bus — dispatches them as plain function calls within a single JavaScript process.

connectInProcessClient wires a full client to a full server in the same process. It is not a mock: the same optimistic-update machinery, the same validation, the same event ordering — just with microtask latency instead of network latency.

The motivating use: an AI agent running next to the server (in the same Durable Object or Node process as the folder’s server) as a first-class client. A composite event bus lets in-process clients and WebSocket clients coexist on one server — so the agent stages edits through an in-process session while humans watch the same live documents from their browsers, each seeing the other’s changes in real time.

The server also exposes change callbacks — onDocumentChange, invoked on every create, update and delete, onDocumentRestored, invoked on every restore, and onDocumentsPurged, invoked once per purge — which is how agent-triggering, auditing, and downstream automation attach without polling. The callbacks are synchronous: long work is started from one, not awaited in it, and on Cloudflare it goes through the Durable Object’s runInBackground so the object outlives the event that started it (see Cloudflare deployment).

They report exactly what reached storage. A write succeeds once it is stored: if a later step fails — telling subscribers, say — the server logs it and recovers the way it would from a dropped message, and the call still succeeds and the callback still fires. A call that rejects stored nothing, so it is always safe to retry, and a mirror or audit log fed from the callbacks never misses a stored write.

A folder is the unit of authority — one server owns it and orders its events. Applications routinely need it reconciled with data that lives elsewhere: your main application database, an external API, a system of record that predates the folder.

Getting history out needs no client — that is what portable event streams are for, a one-way replay into another store. Getting changes back in is what needs one. Writes from outside have to enter through a client, so they are validated, ordered and broadcast like anyone else’s.

Where that bridge runs decides what it can see. Outside the folder it connects over a WebSocket, the same way a browser does — and like a browser it subscribes per document, so it only learns about documents it already knew to watch. Inside the folder’s own server process it can register server.onDocumentChange instead: every document write in the folder, subscribed or not, with the document in hand. Restores come through server.onDocumentRestored, which names the document but doesn’t carry it, so a mirror reads the restored document itself. A schema migration isn’t one of those writes: a document is brought forward when it is next read (or, if a client subscribes to it, right after the schema write), and only the schema change fires the hook, so a mirror storing document data re-reads that type’s documents when its schema changes. The folder listings — sys:index and sys:trash, where renames show up — don’t fire it; a mirror that tracks them subscribes an in-process client to them, which keeps them current from patches. That, rather than the absent socket, is why a mirror wants to run in-process.

The mapping between your documents and your tables stays application-specific. datadata supplies the client and the callback, not the bridge.

The in-process bus is also why datadata is easy to exercise: the test suite runs real client/server pairs with no infrastructure, and the live demos run the same way — a complete sync engine, client and server, inside your browser tab. In Contingency, the game’s simulation is an in-process client of its own: it moves the enemies, pays out gold, and writes the waves and lives.