Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/cli/src/lib/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,9 @@ export async function openCollaborativeDocument(
documentId: profile.documentId,
ydoc: runtime.ydoc,
collaborationProvider: runtime.provider,
isNewFile: shouldSeed,
// When seeding from a document, we need isNewFile: false so that
// #initComments() runs and emits commentsLoaded, pushing comments to Y.Array.
isNewFile: shouldSeed && !docForEditor,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep isNewFile true when seeding from a doc

When decision.action === 'seed' and a document path is provided, docForEditor is the same truthy doc, so this now passes isNewFile: false for the normal seed-from-file path. The editor's collaboration bootstrap only inserts the parsed document into the shared Yjs fragment when options.isNewFile is true (initializeCollaborationData() returns early otherwise), so an empty room seeded from a real DOCX can be marked bootstrapped without actually publishing the document content to other clients.

Useful? React with 👍 / 👎.

editorOpenOptions: options.editorOpenOptions,
user: options.user,
});
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/lib/headless-comment-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export interface HeadlessCommentBridgeResult {
/** Options to spread into Editor.open() call */
editorOptions: {
isCommentsEnabled: true;
shouldLoadComments: true;
documentMode: 'editing';
onCommentsUpdate: (params: Record<string, unknown>) => void;
onCommentsLoaded: (params: { editor: unknown; comments: unknown[] }) => void;
Expand Down Expand Up @@ -348,6 +349,7 @@ export function buildHeadlessCommentBridge(ydoc: unknown, user?: UserIdentity):
return {
editorOptions: {
isCommentsEnabled: true,
shouldLoadComments: true,
documentMode: 'editing',
onCommentsUpdate: handleCommentsUpdate,
onCommentsLoaded: handleCommentsLoaded,
Expand Down
37 changes: 36 additions & 1 deletion packages/superdoc/src/core/collaboration/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ import { actorIdentitiesMatch } from '@superdoc/common';

import { addYComment, updateYComment, deleteYComment } from './collaboration-comments';

/**
* Queue for comment events that arrive before collaboration is ready.
* Flushed when provider syncs via flushPendingCommentEvents().
*/
let pendingCommentEvents = [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope queued comment events per SuperDoc instance

This module-level queue is shared by every SuperDoc instance loaded in the page/process. If one non-collaborative or not-yet-collaborative editor emits a comment event, then another editor later reaches provider sync, flushPendingCommentEvents(superdoc) replays all queued events into that other editor's Y.Array, leaking or corrupting comments across documents; the queued events need to be tied to the originating instance or discarded when that instance is not actually being upgraded to collaboration.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Jun 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Module-level pendingCommentEvents queue is shared across all SuperDoc instances. If multiple instances exist (e.g., multi-document workspace, or CLI processing multiple files), events queued by one instance will be flushed into a different instance's Y.Array, corrupting comment sync. The queue should be scoped per SuperDoc instance (e.g., stored on superdoc itself or in a WeakMap keyed by instance).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/superdoc/src/core/collaboration/helpers.js, line 11:

<comment>Module-level `pendingCommentEvents` queue is shared across all SuperDoc instances. If multiple instances exist (e.g., multi-document workspace, or CLI processing multiple files), events queued by one instance will be flushed into a different instance's Y.Array, corrupting comment sync. The queue should be scoped per SuperDoc instance (e.g., stored on `superdoc` itself or in a WeakMap keyed by instance).</comment>

<file context>
@@ -4,6 +4,25 @@ import { actorIdentitiesMatch } from '@superdoc/common';
+ * Queue for comment events that arrive before collaboration is ready.
+ * Flushed when provider syncs via flushPendingCommentEvents().
+ */
+let pendingCommentEvents = [];
+
+/**
</file context>
Fix with Cubic


/**
* Flush any queued comment events to Y.Array.
* Called when collaboration becomes ready (provider synced).
*
* @param {Object} superdoc The SuperDoc instance
*/
const flushPendingCommentEvents = (superdoc) => {
if (!pendingCommentEvents.length) return;
const events = pendingCommentEvents;
pendingCommentEvents = [];
events.forEach((event) => syncCommentsToClients(superdoc, event));
};

/**
* Load comments from the ydoc into the comments store.
*
Expand Down Expand Up @@ -68,6 +87,12 @@ export const initCollaborationComments = (superdoc) => {
const updateCommentsStore = () => loadCommentsFromYdoc(superdoc);

const onSuperDocYdocSynced = () => {
// Flush queued comment events to Y.Array BEFORE loading.
// When comments are imported before collaboration is fully wired
// (isCollaborative is false), they get queued. Flushing now ensures
// the Y.Array is seeded so other clients receive them.
flushPendingCommentEvents(superdoc);

if (!updateCommentsStore()) {
setTimeout(updateCommentsStore, 0);
}
Expand All @@ -86,6 +111,10 @@ export const initCollaborationComments = (superdoc) => {
superdoc.provider.on('synced', onSuperDocYdocSynced);

// Load any existing comments immediately (in case provider synced before we subscribed)
// If provider is already synced, flush queued events first
if (superdoc.provider?.synced) {
flushPendingCommentEvents(superdoc);
}
if (!updateCommentsStore()) {
setTimeout(updateCommentsStore, 0);
}
Expand Down Expand Up @@ -169,7 +198,13 @@ export const makeDocumentsCollaborative = (superdoc) => {
* @returns {void}
*/
export const syncCommentsToClients = (superdoc, event) => {
if (!superdoc.isCollaborative || !superdoc.config.modules.comments) return;
if (!superdoc.config.modules.comments) return;

// Queue events until collaboration is ready
if (!superdoc.isCollaborative) {
pendingCommentEvents.push(event);
return;
}

const yArray = superdoc.ydoc.getArray('comments');
const user = superdoc.config.user;
Expand Down
Loading