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

@mortenson/lexical-collab-stream

v0.0.1

Published

A "taken too far" proof of concept for how you can achieve collaborative text editing in Lexical by streaming mutations to peers.

Readme

Lexical Collab Stream

A "taken too far" proof of concept for how you can achieve collaborative text editing in Lexical by streaming mutations to peers.

Installation and use

First, install the package:

npm install --save @mortenson/lexical-collab-stream

Then in your frontend framework of choice, create a CollabInstance and pass it your Lexical editor:

const collab = new CollabInstance(
  userId, // Some identifier for the user
  editor, // Some instance of the editor
  new CollabWebSocket("wss://example.com"),
  // or
  new CollabTrystero({ appId: "appId", password: "secret" }, "roomId"),
);
collab.start();

For ease of use, a React plugin has been included which makes it easier to use collab stream. Here's an example of adding it to a LexicalComposer instance, and displaying cursors using a minimal ugly component:

import type { CollabCursor } from "@mortenson/lexical-collab-stream";
import { CollaborationPlugin, CursorElement } from "@mortenson/lexical-collab-stream";
...
const [cursors, setCursors] = useState();
const [desynced, setDesynced] = useState(false);
return (
  <LexicalComposer>
    <CollaborationPlugin
      userId="userId"
      desyncListener={() => setDesynced(true)}
      cursorListener={(cursors) => setCursors(new Map(cursors))}
      network={{
        type: "websocket",
        url: "wss://example.com",
      }}
    />
    {/* or */}
    <CollaborationPlugin
      ...
      network={{
        type: "trystero",
        config: { appId: "appId", password: "secret" },
        roomId: "roomId",
      }}
    />
    ...
    {desynced && <div>You were offline for too long, oh no!</div>}
    {cursors &&
      Array.from(cursors.entries()).map(([userId, cursor]) => {
        return (
          <CursorElement userId={userId} cursor={cursor} key={userId} />
        );
      })}
  </LexicalComposer>
)

If you're using websockets, copy examples/server/server.ts and modify it as needed for your application. If people are interested I can add in some amount of default authentication there and make it a more of a "batteries included" example.

Note: re-implementing the server in another language is surprisingly simple, as it mostly exists as a broker for a Redis stream and doesn't require Lexical

If you're using WebRTC / Trystero, no server is required as public signaling servers are used. That said, the network implementation has less guarantees than websockets and may need some love when it comes to offline editing.

Implementation details

After reading the article "Collaborative Text Editing without CRDTs or OT", I thought that it's be fun to try to build a collaborative editor without Yjs.

Here's how it works:

  1. Ensure that every node has a (unique) UUID by watching for create mutations.
  2. A mapping is (poorly?) maintained between UUIDs and NodeKeys
  3. A custom Node Transform is used to (try to) split TextNodes by word (more nodes == better sync, probably)
  4. Clients connect to a websocket server and receive the current EditorState and the stream ID associated with that document.
  5. A mutation listener sends websocket messages that contain a serialized node and information required to upsert/destroy it. On the server, these messages are added to a Redis stream and later streamed back to peers.
  6. A websocket listener receives messages from other clients and upserts nodes from JSON, or destroys them. Node insertion is always relative to a sibling or parent.

Attempt to diagram

flowchart RL
  Redis@{ shape: cyl, label: "Redis Stream" }
  Client -- "insertAfter" --> EditorState
  EditorState -- "onMutation" --> Client
  Client -- "sendMessage" --> Server
  Server -- "onMessage" --> Client
  Server -- "XADD" --> Redis
  Redis -- "XREAD" --> Server

Running locally

For the websocket server, you'll need Redis running locally on port 6379.

  1. Build the library: npm i && npm run build
  2. In one tab, run the client: cd examples/client && npm i && npm run dev
  3. In another tab: cd examples/server && npm i && npm run server (npm run server-wipe-db will wipe Redis if needed).

If you want to try out Trystero/WebRTC instead of running a websocket server, add the ?trystero query param to the page.

Not implemented yet

  • Accurate server reconciliation (there's no guarantee all clients have the same EditorState, we could have an explicit reconciliation cycle or something like rollback+reapply per the blog linked above)

Not planning to implement

  • Authentication
  • Redis performance magic (seems like you could tell that two websockets are on the same ID and share streams but unsure if that matters)

Why

Probably 60% for fun, 40% because of some things I dislike about Yjs:

  • Yjs being a black box feels weird, if collaboration is important I'd like to be in control of it
  • Persistence is tricky, in general it's easier to just store the binary document forever
  • Making the server authoritative is also tricky
  • Introspecting/modifying the Ydoc in non-JS languages is hard
  • The WebRTC integration is broken between browsers (this doesn't solve that, but if I have to figure out horizontal scaling and websockets anyway may as well go DIY)
  • Due to WebRTC being broken, the distributed promises kind of fall apart

Other thoughts

  • It'd be nice if we synced transforms, not mutations, but the word splitting might make this less necessary.
  • Lexical doesn't really expose the purpose of clone, so it's hard to tell when new UUIDs need to be generated (ex: text split/paste vs. just typing)

Credit

examples/client is cloned from @lexical/react-rich-example, the original LICENSE file is included even though some (minor) modifications have been made.