@womp/echo
v0.6.1
Published
Realtime collaborative sync engine — diff-based document sync over WebSocket with presence
Keywords
Readme
@womp/echo
Realtime collaborative sync engine for the Womp 3D platform. Diff-based document sync over WebSocket with presence.
Used by the stage (3D scene) and flow (node graph) editors. Designed to be framework-agnostic — the library has no knowledge of scene or flow shapes.
Install
yarn add @womp/echoEntry points
@womp/echo/client— for processes that originate edits to a document@womp/echo/server— for the central server that fans edits out to room participants@womp/echo/shared— wire-format types, commands, jsondiffpatch factory, logger
Quick start (server side)
import { WebSocketServer } from "ws";
import { SyncServer, EventSocketServer, InMemoryDataAdapter } from "@womp/echo/server";
interface MyDoc {
title: string;
items: string[];
}
const wss = new WebSocketServer({ port: 8080 });
const transport = new EventSocketServer(wss);
const adapter = new InMemoryDataAdapter();
// SyncServer wires all event handlers as a side-effect of construction.
// No auth: every join is accepted. Pass an `auth` function to restrict access.
new SyncServer<MyDoc>({ transport, adapter });With optional auth:
import type { IAuth } from "@womp/echo/server";
const auth: IAuth = async (connection, credentials) => {
return credentials === "secret-token"; // return false to reject the join
};
new SyncServer<MyDoc>({ transport, adapter, auth });Quick start (client side)
EventSocketClient handles WebSocket connection with automatic reconnect. Pass the resulting
client (which implements IEventSocket) directly to SyncClient.
import { EventSocketClient } from "@womp/echo/client";
import { SyncClient } from "@womp/echo/client";
import { produceScene } from "@womp/echo/shared";
interface MyDoc {
title: string;
items: string[];
}
const ROOM = "my-room";
const USER_ID = "user-123";
// 1. Create the transport — EventSocketClient auto-generates an `id` via nanoid
// and appends it as ?id=<id> on the WebSocket URL.
const socket = new EventSocketClient("ws://localhost:8080");
// 2. Wrap in SyncClient, typed to your document shape.
const client = new SyncClient<MyDoc>(socket, ROOM);
// 3. Listen for synced events before joining.
client.on(SyncClient.EVENTS.SYNCED, (operations, isLocal) => {
const doc = client.getData();
console.log("doc updated:", doc);
});
// 4. Join the room. Resolves once the server has sent the initial snapshot.
await client.join(USER_ID);
// getData() is the live local copy after join.
console.log("initial doc:", client.getData());
// 5. Mutate the document using produceScene (immer-based) to create a new
// object reference, then call sync() to diff against shadow and push the edit.
// Direct mutation of getData() mutates the shadow too (same ref initially) —
// always go through produceScene so the diff is non-empty.
const current = client.getData() as MyDoc;
const next = produceScene(current, (draft) => {
draft.title = "Hello world";
draft.items.push("first item");
});
// Replace localCopy with the produced draft before syncing.
(client.getSyncService() as any).doc.localCopy = next;
client.sync();
// 6. Teardown
client.destroy();EventSocketClient options
const socket = new EventSocketClient("ws://localhost:8080", {
// Return true while a backend deployment is in progress.
isDeploying: () => window.__DEPLOYING__ === true,
// Register a callback that fires when the deployment is done.
onDeployEnded: (cb) => window.addEventListener("deploy-ended", cb, { once: true }),
logger: customLogger,
});Without the deploy hooks the client reconnects automatically after 1 second on any unclean disconnect.
Presence
Presence<TCustom> is a generic interface you extend with your own application shape.
The library transports presence payloads opaquely — the server fans them out unchanged.
import type { Presence } from "@womp/echo/shared";
interface FlowPresence {
selection: number[];
cursor?: { x: number; y: number };
}
type MyPresence = Presence<FlowPresence>;
// Example presence object your application constructs and sends:
const presence: MyPresence = {
userId: "user-123",
ts: Date.now(),
custom: {
selection: [1, 2, 3],
cursor: { x: 100, y: 200 },
},
};Architecture notes
- Uses jsondiffpatch for structural diff/patch between document versions. Both sides share the same
diffPatchFactoryfrom@womp/echo/shared. - Uses immer (
produceScene) for structural-sharing mutations on the document. Auto-freeze is disabled outside of production so legacy in-place patches remain possible. - Shadow copy / edit queue: each client maintains a
shadow(last acknowledged server state) and alocalCopy(current working state).sync()diffs localCopy against shadow, queues the delta, and sends it to the server. The server re-broadcasts to all other room participants who then apply the patch on their next sync cycle. - WebSocket transport via
ws. Consumers can provide their own transport by implementing theIEventSocket/IEventSocketServerinterfaces from@womp/echo/server. - Server is single-process. Cross-instance scaling is the consumer's responsibility (e.g., sticky sessions or Redis pub/sub fan-out).
InMemoryDataAdapterstores per-room document state in a plainMap. In production, swap in a persistent adapter that implements the same interface.
Versioning
Wire format is frozen. Breaking changes require a major version bump and a coordinated deploy across all consumers.
License
MIT
