From 63f5d600695e85c4e713abbca0c5ce661cdbc1cd Mon Sep 17 00:00:00 2001 From: Matthew Connelly Date: Sun, 31 May 2026 22:57:53 -0400 Subject: [PATCH] fix(collaboration): sync comments to second collab client on export Second collaboration client was missing comment metadata (text, author) because comments were not being pushed to Y.Array before the client exported. Three fixes: CLI: - Set shouldLoadComments: true so #initComments() emits commentsLoaded - Fix isNewFile logic: when seeding from a document, isNewFile must be false so #initComments() runs immediately (not deferred) Browser: - Queue comment events arriving before collaboration is ready - Flush queued events when provider syncs Co-Authored-By: Claude Opus 4.5 --- apps/cli/src/lib/document.ts | 4 +- apps/cli/src/lib/headless-comment-bridge.ts | 2 + .../src/core/collaboration/helpers.js | 37 ++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/apps/cli/src/lib/document.ts b/apps/cli/src/lib/document.ts index cd2d193d70..2d7ff385a1 100644 --- a/apps/cli/src/lib/document.ts +++ b/apps/cli/src/lib/document.ts @@ -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, editorOpenOptions: options.editorOpenOptions, user: options.user, }); diff --git a/apps/cli/src/lib/headless-comment-bridge.ts b/apps/cli/src/lib/headless-comment-bridge.ts index 247870e647..48028ca246 100644 --- a/apps/cli/src/lib/headless-comment-bridge.ts +++ b/apps/cli/src/lib/headless-comment-bridge.ts @@ -147,6 +147,7 @@ export interface HeadlessCommentBridgeResult { /** Options to spread into Editor.open() call */ editorOptions: { isCommentsEnabled: true; + shouldLoadComments: true; documentMode: 'editing'; onCommentsUpdate: (params: Record) => void; onCommentsLoaded: (params: { editor: unknown; comments: unknown[] }) => void; @@ -348,6 +349,7 @@ export function buildHeadlessCommentBridge(ydoc: unknown, user?: UserIdentity): return { editorOptions: { isCommentsEnabled: true, + shouldLoadComments: true, documentMode: 'editing', onCommentsUpdate: handleCommentsUpdate, onCommentsLoaded: handleCommentsLoaded, diff --git a/packages/superdoc/src/core/collaboration/helpers.js b/packages/superdoc/src/core/collaboration/helpers.js index bdf416f8bc..ac34d967cd 100644 --- a/packages/superdoc/src/core/collaboration/helpers.js +++ b/packages/superdoc/src/core/collaboration/helpers.js @@ -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 = []; + +/** + * 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. * @@ -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); } @@ -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); } @@ -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;