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

@editneo/sync

v0.1.3

Published

Yjs-based CRDT sync manager for EditNeo with offline and WebSocket support

Readme

@editneo/sync

The synchronization layer for EditNeo. This package provides a SyncManager class that bridges the Zustand editor store with Yjs CRDTs, giving you offline persistence through IndexedDB and real-time multi-user collaboration through WebSockets — with automatic conflict resolution.

Installation

npm install @editneo/sync @editneo/core

How It Works

The SyncManager creates a Yjs document and maps the editor's block structure into two Yjs shared types:

  • A Y.Map for individual blocks (keyed by block ID)
  • A Y.Array for the ordered list of root block IDs

Changes flow bidirectionally and automatically:

  1. Store → Yjs: When you mutate the editor store, the SyncManager detects the change via Zustand's subscribe() and pushes it to Yjs. Only changed blocks are synced, and deletions are tracked. The Yjs providers then propagate changes to IndexedDB and to other connected clients.

  2. Yjs → Store: When a change arrives from another client (or from IndexedDB on page load), the SyncManager's observers detect the Yjs mutation and update the Zustand store.

An isSyncing flag prevents infinite loops between the two directions. All updates are wrapped in Yjs transactions for atomicity.

Because Yjs is a CRDT, conflicting edits from multiple users are merged automatically without a central server making decisions.

Usage

Standalone (offline only)

import { SyncManager } from "@editneo/sync";
import { createEditorStore } from "@editneo/core";

const store = createEditorStore();
const sync = new SyncManager("my-document");
sync.bindStore(store);
// Data is now persisted to IndexedDB under "editneo-document-my-document"

With real-time collaboration

const sync = new SyncManager("my-document", {
  url: "wss://your-yjs-server.com",
  room: "my-document",
});
sync.bindStore(store);

The url should point to a y-websocket server. The room determines which document the client joins — clients in the same room share the same Yjs document.

With <NeoEditor /> (recommended)

When used with @editneo/react, the NeoEditor component creates and binds the SyncManager automatically when you pass syncConfig:

<NeoEditor
  id="shared-doc"
  syncConfig={{ url: "wss://your-server.com", room: "shared-doc" }}
>
  <CursorOverlay />
</NeoEditor>

Cursor awareness

Share cursor positions and user info between collaborators:

// Set your user info
sync.setUser({
  name: "Alice",
  color: "#3b82f6",
  avatar: "https://example.com/alice.jpg", // optional
});

// Update cursor position
sync.setCursor("block-abc", 12); // blockId, character index
sync.setCursor(null); // Clear cursor (e.g. on blur)

// Listen for remote cursor changes
const awareness = sync.awareness;
awareness?.on("change", () => {
  const states = awareness.getStates();
  // Map<clientID, { user: { name, color }, cursor: { blockId, index } }>
});

The CursorOverlay component from @editneo/react consumes this awareness data automatically.

Error handling

The SyncManager listens for connection events and logs status changes:

  • status — connection state changes (connecting, connected, disconnected)
  • connection-error — WebSocket errors
  • connection-close — connection closed (auto-reconnect is handled by y-websocket)

Cleanup

When the editor unmounts or the document changes, destroy the sync manager:

sync.destroy();

This unsubscribes from the store, destroys the IndexedDB provider, the WebSocket provider (if any), and the underlying Yjs document.

API Reference

new SyncManager(docId, syncConfig?)

| Parameter | Type | Description | | ------------ | ------------------------------- | --------------------------------------------------------------------- | | docId | string | Unique document identifier. Used to namespace the IndexedDB database. | | syncConfig | { url: string; room: string } | Optional. WebSocket server URL and room name for real-time sync. |

Instance Methods

| Method | Signature | Description | | ----------- | ------------------------------------------------------------------ | -------------------------------------------------- | | bindStore | (store: EditorStoreInstance) => void | Binds the sync manager to an editor store instance | | setUser | (user: { name: string; color: string; avatar?: string }) => void | Sets local user awareness info | | setCursor | (blockId: string \| null, index?: number) => void | Updates local cursor position in awareness | | destroy | () => void | Tears down all providers and the Yjs document |

Instance Properties

| Property | Type | Description | | ------------ | -------------------------------- | -------------------------------------------------- | | doc | Y.Doc | The underlying Yjs document | | yBlocks | Y.Map<any> | Yjs map of all blocks | | yRoot | Y.Array<string> | Yjs array of root block IDs | | provider | IndexeddbPersistence | The IndexedDB persistence provider | | wsProvider | WebsocketProvider \| undefined | The WebSocket provider, if configured | | awareness | Awareness \| undefined | The awareness instance from the WebSocket provider |

Running a WebSocket Server

The simplest way to run a Yjs WebSocket server for development:

npx y-websocket

This starts a server on ws://localhost:1234. Point your syncConfig.url there:

new SyncManager("doc", { url: "ws://localhost:1234", room: "doc" });

For production, see the y-websocket documentation for deployment options including authentication, scaling, and persistence.

License

MIT