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

@shashimadushan/docx-editor-collaboration

v0.1.2

Published

Real-time multi-user collaboration for @shashimadushan/docx-editor-editor. Yjs-based live editing, presence/online-users, live cursors, and invite/share-link UI — backend-agnostic, ships a self-hosted Hocuspocus reference server.

Readme

@shashimadushan/docx-editor-collaboration

Real-time multi-user collaboration for @shashimadushan/docx-editor-editor — Yjs-based live editing, a Google-Docs-style "who's online" presence bar with live cursors, and invite-by-email / share-link UI. Ships as a separate, opt-in package: install it only if you need multi-user editing, on top of the same comments/track-changes features that already work for single-user brainstorming.

Install

pnpm add @shashimadushan/docx-editor-collaboration yjs @hocuspocus/provider
# and, for the reference server:
pnpm add @hocuspocus/server

Client: wire into ReactDocxEditor

import { ReactDocxEditor } from '@shashimadushan/docx-editor-editor/react';
import { useCollaboration, PresenceBar, ShareDialog, InMemoryInviteAdapter } from '@shashimadushan/docx-editor-collaboration/react';
import '@shashimadushan/docx-editor-collaboration/styles.css';

const inviteAdapter = new InMemoryInviteAdapter();

function CollaborativeEditor({ documentId, user, role /* CollaboratorRole your app already resolved */ }) {
  const { ready, synced, extension, tiptapExtensions, onlineUsers, mode } = useCollaboration({
    documentId,
    wsUrl: 'ws://localhost:1234',
    user, // { id, name, color, avatar? }
    role, // 'owner' | 'editor' | 'commenter' | 'viewer' — see "Roles & read-only enforcement" below
  });
  const [shareOpen, setShareOpen] = React.useState(false);

  // IMPORTANT: mount `ReactDocxEditor` only once BOTH `ready` and `synced`
  // are true — see "Why both `ready` and `synced`?" below for what goes
  // wrong if you gate on `ready` alone.
  if (!ready || !synced) return <div>Connecting…</div>;

  return (
    <>
      <ReactDocxEditor
        extensions={[extension]}
        tiptapExtensions={tiptapExtensions}
        // Locks a viewer/commenter's own local editor to read-only too —
        // see "Roles & read-only enforcement" below for why this is
        // required, not optional, alongside the server-side check.
        mode={mode}
        titleBarActions={
          <>
            <PresenceBar onlineUsers={onlineUsers} />
            <button onClick={() => setShareOpen(true)}>Share</button>
          </>
        }
      />
      <ShareDialog
        open={shareOpen}
        onClose={() => setShareOpen(false)}
        documentId={documentId}
        currentUser={user}
        inviteAdapter={inviteAdapter}
        permissionsAdapter={inviteAdapter}
      />
    </>
  );
}

Why create the provider in an effect (the ready gate)? new HocuspocusProvider(...) opens a WebSocket immediately — a side effect. Creating it in render/useMemo makes React StrictMode's double-render spawn duplicate connections, and its mount→unmount→remount destroys the instance and hands the editor a dead Y.Doc (no sync, no presence). useCollaboration therefore creates the doc/provider inside an effect and exposes ready so you can mount the editor against a guaranteed-live connection.

Why both ready and synced?

ready only means the Y.Doc/HocuspocusProvider objects exist — it flips to true the instant the provider is constructed, one React tick after mount. It says nothing about whether that doc actually reflects the server's state yet; a freshly-constructed Y.Doc starts out completely empty and only catches up once the WebSocket handshake and Yjs sync protocol finish.

synced is the flag that tracks that: it becomes true only once the provider fires Hocuspocus's synced event with state: true, confirming the local doc is caught up.

Mounting on ready alone is the most common Yjs integration bug there is. If you mount ReactDocxEditor — and let it seed its content prop, or run a loadDocx() call — as soon as ready is true, you're seeding a doc that's still empty. Yjs is a CRDT: when the server's real state arrives moments later, it does not detect "this is the same document, replace mine with theirs" — it merges the two, unioning both edit histories. The result is the entire document body appearing twice, and with every additional client that repeats the mistake, it compounds further (three copies, four, ...). Gating on ready && synced — as in the example above — closes this: the editor never mounts against a doc that hasn't yet been confirmed to match the server.

Seeding initial content

Given the above, the safe places to seed a new (never-before-edited) document are:

  1. Server-side, preferably. Pass loadInitialContent to createCollabServer() (see ../server/persistence.ts) — it runs once, before any client connects, so there is no race to avoid at all. This is the recommended approach.

  2. Browser-side, only if you must, and only guarded by isYDocEmpty(). If your app seeds content from the client (e.g. duplicating a template into a brand-new document), do it only after synced is true, and only when isYDocEmpty(ydoc, field) confirms the synced doc really came back empty:

    import { isYDocEmpty } from '@shashimadushan/docx-editor-collaboration';
    
    React.useEffect(() => {
      if (!synced || !ydoc) return;
      if (isYDocEmpty(ydoc)) {
        // safe: the server confirmed this doc has no content yet.
        seedInitialContentInto(ydoc);
      }
    }, [synced, ydoc]);

    Skipping the isYDocEmpty() check is just as dangerous as skipping the synced gate — a returning client reconnecting to a document that already has real content must not re-seed it either.

Roles & read-only enforcement

Yjs has no concept of per-mark authorization — a writable connection can apply any update to any shared type in the doc, full stop. Enforcement therefore has to happen at the connection level, in two places that both matter:

  1. Server-side (createAuthenticateHook, see ./server/auth.ts): sets Hocuspocus's connection-level readOnly flag for both 'viewer' and 'commenter' roles. This is what actually stops a read-only collaborator's edits from ever reaching other clients.

  2. Client-side (role → mode, this README's example above): the server-side flag alone leaves that collaborator's own local editor instance fully editable — they can type, watch nothing sync (their update is silently rejected/ignored by the read-only connection), and lose the work with no indication anything went wrong. Passing your app's already-resolved role into useCollaboration() computes mode via editorModeForRole() (also exported standalone, for headless/non-React use):

    import { editorModeForRole } from '@shashimadushan/docx-editor-collaboration';
    
    editorModeForRole('viewer');    // 'viewing'
    editorModeForRole('commenter'); // 'viewing' — see note below
    editorModeForRole('editor');    // 'editing'
    editorModeForRole('owner');     // 'editing'

    Forward that into ReactDocxEditor's mode prop (or call editor.setMode(mode) directly against a headless DocxEditor) and the local editor locks in lockstep with what the server will actually accept.

commenter maps to 'viewing', same as viewer — this does not disable commenting. Comments are a separate, non-Yjs feature in @shashimadushan/docx-editor-editor (see "What this package does not touch" below) and are unaffected by EditorMode; a commenter can still add/reply to comments while their body-text editor is locked against edits that would just be dropped anyway.

Server: run the reference Hocuspocus server

// collab-server.ts — run as its own long-running Node process
import { createCollabServer, InMemoryInviteAdapter } from '@shashimadushan/docx-editor-collaboration/server';

const permissionsAdapter = new InMemoryInviteAdapter();

createCollabServer({
  port: 1234,
  permissionsAdapter,
  // Preferred way to seed a new document's initial content — see
  // "Seeding initial content" above. Return a raw Yjs update (e.g. from
  // `Y.encodeStateAsUpdate()` of a doc you built with
  // `prosemirrorJSONToYXmlFragment` from an existing .docx/template), or
  // `null` for a genuinely blank document.
  loadInitialContent: async (documentId) => null,
  onPersist: async (documentId, ydoc) => {
    // e.g.: const json = ydocToProseMirrorJSON(ydoc);
    //       const blob = await saveDocxFromJSON(json);
    //       await myStorage.write(documentId, blob);
  },
}).listen();

A Hocuspocus server is a stateful, bidirectional WebSocket process — it cannot run inside a Next.js API route (or any request/response-scoped serverless function). Run it as its own Node process; in production, host it somewhere with a persistent connection (a small VPS, Fly.io, etc.).

Connection status

usePresence()/useCollaboration() expose a status: ConnectionStatus that now distinguishes five states (widened from three in 0.1.x — existing 'connecting' | 'connected' | 'disconnected' checks keep compiling and behaving the same, since this is purely additive):

| Status | Meaning | | --- | --- | | 'connecting' | WebSocket handshake in progress. | | 'connected' | Socket open, but the Yjs sync handshake hasn't completed — the doc may still be empty/stale. | | 'synced' | The doc is confirmed caught up with the server. Safe to read/seed content — see above. | | 'disconnected' | Socket closed (network drop, server restart, explicit disconnect()); local edits queue and flush on reconnect. | | 'error' | The token was rejected (authenticationFailed). Reconnecting with the same token won't help — fetch a fresh one. |

Session token rotation

useCollaboration({ token }) accepts a token that can change on every render (e.g. refreshed on a timer, or after a 401) without tearing down the connection: internally it's handed to HocuspocusProvider as a callback read from a ref, not captured by value, so a rotating token is picked up on the provider's own next reconnect attempt instead of forcing a destroy-and-recreate of the Y.Doc/provider (which would drop in-flight local edits and flash the editor back to "Connecting…" mid-session).

Suggestion authorship

createCollaborationExtension() now calls the editor's track-changes author command from its onInit, attributing every suggestion (tracked insertion/deletion) made in a collaborative session to the collaborator's user.name instead of leaving it credited to the generic default author. No configuration needed — it's wired automatically from the user you already pass to useCollaboration()/createCollaborationExtension().

Bring your own backend

createCollaborationExtension({ ydoc, provider, user }) accepts any object satisfying the minimal CollaborationProvider interface (a Yjs Awareness instance + optional connect/disconnect) — Hocuspocus is the shipped reference, not a hard requirement. useCollaboration() is a Hocuspocus-flavored convenience hook; for another backend, construct your own Y.Doc/provider and call createCollaborationExtension() + usePresence() directly.

Share links (InMemoryInviteAdapter caveat). The reference InMemoryInviteAdapter/LocalStorageInviteAdapter each hold their own private state. A share-link token minted in the browser must be resolvable by the server's onAuthenticate hook too — so for real share links, both sides must talk to one shared store. Use HttpInviteAdapter in the browser pointed at your own API routes, and the same HttpInviteAdapter (or your DB-backed PermissionsAdapter) on the server. See examples/demo for a complete working setup (app/api/collab/* + collab-server/).

No changes to packages/editor are required — everything above uses ReactDocxEditor's already-public extensions/tiptapExtensions/mode/titleBarActions props.

Invite / permissions adapters

InviteAdapter (email invites, share links) and PermissionsAdapter (roles, access) are pluggable interfaces — this package ships InMemoryInviteAdapter and LocalStorageInviteAdapter as reference implementations (invites are logged to the console, not actually emailed). A production app supplies its own adapter backed by a real user DB and an email provider (Resend, SendGrid, etc.).

What this package does not touch

Text-anchored comments (@mentions) and track-changes/suggestions in @shashimadushan/docx-editor-editor are unrelated, single-user "brainstorming" features and are unaffected by installing this package (aside from EditorMode's effect on the toolbar/editability, and suggestion authorship being wired automatically — see above).