npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@womp/echo

v0.6.1

Published

Realtime collaborative sync engine — diff-based document sync over WebSocket with presence

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/echo

Entry 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 diffPatchFactory from @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 a localCopy (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 the IEventSocket / IEventSocketServer interfaces 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).
  • InMemoryDataAdapter stores per-room document state in a plain Map. 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