Skip to content

Commit 9476b21

Browse files
committed
fix(collab-doc): match client markdown post-processing on server persist
Cursor review: yDocToFileMarkdown serialized the body with yDocToMarkdown only, but the editor save path runs postProcessSerializedMarkdown before applyFrontmatter. Server persist could therefore write markdown differing from a client save (empty list markers, callout un-escaping) — spurious blob churn / round-trip drift despite the byte-identical claim. Apply postProcessSerializedMarkdown in yDocToFileMarkdown so a server persist is byte-identical to a client save and the client's dirty-check baseline. Add a regression test guarding the composition.
1 parent 2f1933c commit 9476b21

2 files changed

Lines changed: 45 additions & 6 deletions

File tree

apps/sim/lib/collab-doc/converter.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
/**
22
* @vitest-environment jsdom
33
*/
4+
import { FILE_DOC_SEED } from '@sim/realtime-protocol/file-doc'
45
import { describe, expect, it } from 'vitest'
56
import * as Y from 'yjs'
7+
import {
8+
applyFrontmatter,
9+
postProcessSerializedMarkdown,
10+
} from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-fidelity'
611
import { serializeMarkdownBody } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-parse'
7-
import { applyMarkdownToYDoc, markdownToYDoc, yDocToMarkdown } from './converter'
12+
import {
13+
applyMarkdownToYDoc,
14+
markdownToYDoc,
15+
yDocToFileMarkdown,
16+
yDocToMarkdown,
17+
} from './converter'
818

919
/** Representative markdown covering the custom-fidelity constructs (tables, code, lists, marks). */
1020
const SAMPLES = [
@@ -74,4 +84,25 @@ describe('collab-doc converter', () => {
7484
expect(merged).toContain('Alpha paragraph. EDITED')
7585
expect(merged).toContain('expanded by the agent')
7686
})
87+
88+
it('yDocToFileMarkdown matches the client save composition (frontmatter + postProcess body pass)', () => {
89+
// A server-side persist must be byte-identical to the editor save — `applyFrontmatter(frontmatter,
90+
// postProcessSerializedMarkdown(body))` — or it drifts from a client save / the dirty-check baseline
91+
// and churns the blob. This guards against the postProcess pass being dropped from the server path.
92+
const frontmatter = '---\ntitle: Doc\n---\n'
93+
const doc = markdownToYDoc('- one\n- two\n\n> [!NOTE]\n> hi')
94+
doc.getMap(FILE_DOC_SEED.configMap).set(FILE_DOC_SEED.frontmatterKey, frontmatter)
95+
const expected = applyFrontmatter(
96+
frontmatter,
97+
postProcessSerializedMarkdown(yDocToMarkdown(doc))
98+
)
99+
expect(yDocToFileMarkdown(doc)).toBe(expected)
100+
doc.destroy()
101+
})
102+
103+
it('yDocToFileMarkdown re-attaches empty frontmatter when the config map has none', () => {
104+
const doc = markdownToYDoc('plain body\n')
105+
expect(yDocToFileMarkdown(doc)).toBe(postProcessSerializedMarkdown(yDocToMarkdown(doc)))
106+
doc.destroy()
107+
})
77108
})

apps/sim/lib/collab-doc/converter.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99
} from '@tiptap/y-tiptap'
1010
import type * as Y from 'yjs'
1111
import { createMarkdownContentExtensions } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions'
12-
import { applyFrontmatter } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-fidelity'
12+
import {
13+
applyFrontmatter,
14+
postProcessSerializedMarkdown,
15+
} from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-fidelity'
1316
import {
1417
parseMarkdownToDoc,
1518
serializeDocToMarkdown,
@@ -87,13 +90,18 @@ export function yDocToMarkdown(ydoc: Y.Doc): string {
8790

8891
/**
8992
* Project a collaborative {@link Y.Doc} back to the file's FULL canonical markdown — the body from the
90-
* CRDT re-joined with the frontmatter carried in the config map. This is exactly what the editor
91-
* writes on save (`applyFrontmatter(resolveSaveFrontmatter(), body)`), so a server-side persist of the
92-
* live doc is byte-identical to a client save — no spurious churn on the round-trip.
93+
* CRDT re-joined with the frontmatter carried in the config map. Mirrors the editor's save path EXACTLY
94+
* (`applyFrontmatter(resolveSaveFrontmatter(), postProcessSerializedMarkdown(editor.getMarkdown()))`),
95+
* INCLUDING the `postProcessSerializedMarkdown` body fidelity pass (empty list markers, callout
96+
* un-escaping, trailing whitespace) — so a server-side persist is byte-identical to a client save and
97+
* matches the client's dirty-check baseline, with no spurious blob churn on the round-trip.
9398
*/
9499
export function yDocToFileMarkdown(ydoc: Y.Doc): string {
95100
const frontmatter = ydoc.getMap(FILE_DOC_SEED.configMap).get(FILE_DOC_SEED.frontmatterKey)
96-
return applyFrontmatter(typeof frontmatter === 'string' ? frontmatter : '', yDocToMarkdown(ydoc))
101+
return applyFrontmatter(
102+
typeof frontmatter === 'string' ? frontmatter : '',
103+
postProcessSerializedMarkdown(yDocToMarkdown(ydoc))
104+
)
97105
}
98106

99107
/**

0 commit comments

Comments
 (0)