From 43f7ec8735778b1e7ada0858446076c80d010aec Mon Sep 17 00:00:00 2001 From: Russell Zager Date: Tue, 24 Mar 2026 09:33:09 -0700 Subject: [PATCH] fix: bootstrap YDoc for API-created documents Documents created via POST /documents never bootstrapped a Yjs document, so the share view loaded empty. The Milkdown round-trip normalizes markdown, creating a mismatch that forced mutation_ready: false. Fix: call ensureCanonicalYjsBaselineForDocument after createDocument, and sync the DB row markdown to the YDoc-derived version so content comparisons pass. Also requires COLLAB_EMBEDDED_WS=1 for local dev (WS on same port). Fixes #7 Co-Authored-By: Claude Opus 4.6 (1M context) --- server/collab.ts | 14 ++++++++++++++ server/routes.ts | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/server/collab.ts b/server/collab.ts index 721ffe1..41fe969 100644 --- a/server/collab.ts +++ b/server/collab.ts @@ -5713,6 +5713,20 @@ async function seedLegacyDocumentToPersistedYjsAsync( applyMarksMapDiff(ydoc.getMap('marks'), marks); }, 'legacy-seed-markdown'); await seedFragmentFromLegacyMarkdown(ydoc, markdown); + // Sync the document row's markdown to the YDoc-derived version so that + // sameAuthoritativeContent() comparisons pass. Without this, API-created + // documents have a permanent mismatch (raw vs Milkdown-normalized markdown) + // that forces mutation_ready: false and breaks share view hydration. + const derivedMarkdown = await deriveMarkdownProjectionFromFragment(ydoc); + if (derivedMarkdown && derivedMarkdown.trim()) { + const derivedMarks = canonicalizeStoredMarks(encodeMarksMap(ydoc.getMap('marks'))); + updateDocument(slug, derivedMarkdown, derivedMarks); + // Re-read the row so persistCanonicalYjsBaseline uses the normalized markdown + const updatedRow = getDocumentBySlug(slug); + if (updatedRow) { + return persistCanonicalYjsBaseline(slug, updatedRow, ydoc); + } + } return persistCanonicalYjsBaseline(slug, row, ydoc); } diff --git a/server/routes.ts b/server/routes.ts index 80971cb..fd2eb6a 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -16,6 +16,7 @@ import { syncCanonicalDocumentStateToCollab, stripEphemeralCollabSpans, acquireRewriteLock, + ensureCanonicalYjsBaselineForDocument, } from './collab.js'; import { getSnapshotPublicUrl, refreshSnapshotForSlug } from './snapshot.js'; import { executeCanonicalRewrite, mutateCanonicalDocument } from './canonical-document.js'; @@ -784,7 +785,7 @@ function deriveShareCapabilities(role: ShareRole, shareState: string): { } // Create a shared document -apiRoutes.post('/documents', (req: Request, res: Response) => { +apiRoutes.post('/documents', async (req: Request, res: Response) => { const legacyCreateMode = resolveLegacyCreateMode(getPublicBaseUrl(req)); if (legacyCreateMode === 'disabled') { recordLegacyCreateRouteTelemetry(req, legacyCreateMode, 'blocked_disabled'); @@ -827,6 +828,9 @@ apiRoutes.post('/documents', (req: Request, res: Response) => { const ownerSecret = randomUUID(); const normalizedMarks = canonicalizeStoredMarks(marks ?? {}); const doc = createDocument(slug, sanitizedMarkdown, normalizedMarks, title, ownerId, ownerSecret); + // Bootstrap YDoc so the collab system can hydrate the share view. + // Without this, API-created documents have no YDoc and the share view loads empty. + await ensureCanonicalYjsBaselineForDocument(slug); const defaultAccess = createDocumentAccessToken(slug, 'editor'); const links = buildShareLink(req, doc.slug); const shareUrlWithToken = withShareToken(links.shareUrl, defaultAccess.secret);