From 3b0eaaee6cfd1e457db5dfdf04f6691eb207b765 Mon Sep 17 00:00:00 2001 From: SecPan Date: Wed, 8 Jul 2026 14:15:17 -0700 Subject: [PATCH] Port upstream flush before save guard --- src/App.vue | 32 ++++++++---- .../editor/editorMarkdownSnapshot.test.ts | 50 +++++++++++++++++++ src/features/editor/editorMarkdownSnapshot.ts | 21 ++++++++ src/types/muya-core.d.ts | 1 + 4 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 src/features/editor/editorMarkdownSnapshot.test.ts create mode 100644 src/features/editor/editorMarkdownSnapshot.ts diff --git a/src/App.vue b/src/App.vue index 95e65b1..0206720 100644 --- a/src/App.vue +++ b/src/App.vue @@ -102,6 +102,7 @@ import { runEditorToolbarCommandWorkflow, scheduleEditorToolbarSync, } from './features/editor/editorToolbarWorkflow' +import { readEditorMarkdownSnapshot } from './features/editor/editorMarkdownSnapshot' import { getEditorToolbarSettings, } from './features/editor/editorToolbarSettings' @@ -488,12 +489,23 @@ function normalizeEditorMarkdown(markdown: string) { return markdown === '\n' ? '' : markdown } -function syncDocumentFromEditor(markDirty = false) { +function getEditorMarkdownSnapshot(flushPending = false) { + if (!editor) { + return '' + } + + return readEditorMarkdownSnapshot(editor, { + flushPending, + normalizeMarkdown: normalizeEditorMarkdown, + }) +} + +function syncDocumentFromEditor(markDirty = false, flushPending = false) { if (!editor) { return documentState.value } - const value = normalizeEditorMarkdown(editor.getMarkdown()) + const value = getEditorMarkdownSnapshot(flushPending) documentState.value = updateDocumentMarkdown(documentState.value, value, { markDirty }) return documentState.value } @@ -504,7 +516,7 @@ function syncMarkdown(nextStatus: unknown = 'Edited') { } const resolvedStatus = typeof nextStatus === 'string' ? nextStatus : 'Edited' - const nextMarkdown = normalizeEditorMarkdown(editor.getMarkdown()) + const nextMarkdown = getEditorMarkdownSnapshot() const markDirty = resolvedStatus === 'Edited' documentState.value = updateDocumentMarkdown(documentState.value, nextMarkdown, { markDirty }) if (markDirty && documentState.value.autosaveTarget === 'android-document') { @@ -1108,7 +1120,7 @@ async function saveAndroidDocument() { return true } - const value = normalizeEditorMarkdown(editor.getMarkdown()) + const value = getEditorMarkdownSnapshot(true) const startResult = createSaveAndroidDocumentWorkflowStart({ documentState: documentState.value, markdown: value, @@ -1199,7 +1211,7 @@ async function saveLocalDraftToAndroidDocument(options: { returnHomeAfterSave?: } saveDraft() - const draftDocument = syncDocumentFromEditor(false) + const draftDocument = syncDocumentFromEditor(false, true) const reopenPromptOnCancel = draftExitPromptOpen.value && options.returnHomeAfterSave === true draftExitPromptOpen.value = false @@ -1273,7 +1285,7 @@ async function saveAndroidDocumentCopy(options: { returnHomeAfterSave?: boolean } const originalSourceUri = documentState.value.sourceUri - const copySourceDocument = syncDocumentFromEditor(documentState.value.isDirty) + const copySourceDocument = syncDocumentFromEditor(documentState.value.isDirty, true) savingAndroidDocumentCopy.value = true status.value = 'Choose a location' @@ -1338,7 +1350,7 @@ async function shareCurrentMarkdownDocument() { return false } - const currentDocument = syncDocumentFromEditor(documentState.value.isDirty) + const currentDocument = syncDocumentFromEditor(documentState.value.isDirty, true) if (currentDocument.autosaveTarget === 'local-draft') { saveDraft() } @@ -1387,7 +1399,7 @@ function saveDraft() { return } - const value = normalizeEditorMarkdown(editor.getMarkdown()) + const value = getEditorMarkdownSnapshot(true) const localDraftAutosave = createLocalDraftAutosaveResult( documentState.value, value, @@ -1738,7 +1750,7 @@ async function preserveCurrentDocumentBeforeIncomingOpen() { appLog.info('preserve current document before incoming Android document') if (preservationAction.kind === 'save-android-document') { - syncDocumentFromEditor(documentState.value.isDirty) + syncDocumentFromEditor(documentState.value.isDirty, true) const sourceUri = documentState.value.sourceUri const saved = await saveAndroidDocument() if (sourceUri && shouldKeepAndroidRecoveryAfterPreserveFailure(saved, sourceUri, documentState.value.markdown)) { @@ -1969,7 +1981,7 @@ async function showHome() { function keepAndroidRecoveryAndShowHome() { const sourceUri = documentState.value.sourceUri - const currentDocument = syncDocumentFromEditor(documentState.value.isDirty) + const currentDocument = syncDocumentFromEditor(documentState.value.isDirty, true) if (sourceUri && currentDocument.markdown.trim() && canPersistAndroidRecoveryDrafts()) { persistAndroidRecoveryDraft(sourceUri, currentDocument.markdown) homeNotice.value = ANDROID_EXIT_RECOVERY_MESSAGE diff --git a/src/features/editor/editorMarkdownSnapshot.test.ts b/src/features/editor/editorMarkdownSnapshot.test.ts new file mode 100644 index 0000000..ebdb9ca --- /dev/null +++ b/src/features/editor/editorMarkdownSnapshot.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from 'vitest' +import { readEditorMarkdownSnapshot } from './editorMarkdownSnapshot' + +function createEditorSnapshotSource() { + let markdown = 'stale' + const editor = { + flush: vi.fn(() => { + markdown = 'fresh\n' + }), + getMarkdown: vi.fn(() => markdown), + } + return editor +} + +describe('readEditorMarkdownSnapshot', () => { + it('flushes pending Muya edits before reading markdown for save paths', () => { + const editor = createEditorSnapshotSource() + + const markdown = readEditorMarkdownSnapshot(editor, { flushPending: true }) + + expect(markdown).toBe('fresh\n') + expect(editor.flush).toHaveBeenCalledTimes(1) + expect(editor.getMarkdown).toHaveBeenCalledTimes(1) + const flushOrder = vi.mocked(editor.flush).mock.invocationCallOrder[0] + const getMarkdownOrder = vi.mocked(editor.getMarkdown).mock.invocationCallOrder[0] + expect(flushOrder).toBeDefined() + expect(getMarkdownOrder).toBeDefined() + expect(flushOrder as number).toBeLessThan(getMarkdownOrder as number) + }) + + it('keeps non-save reads unflushed', () => { + const editor = createEditorSnapshotSource() + + const markdown = readEditorMarkdownSnapshot(editor) + + expect(markdown).toBe('stale') + expect(editor.flush).not.toHaveBeenCalled() + }) + + it('normalizes the flushed markdown snapshot', () => { + const editor = createEditorSnapshotSource() + + const markdown = readEditorMarkdownSnapshot(editor, { + flushPending: true, + normalizeMarkdown: value => value.trim(), + }) + + expect(markdown).toBe('fresh') + }) +}) diff --git a/src/features/editor/editorMarkdownSnapshot.ts b/src/features/editor/editorMarkdownSnapshot.ts new file mode 100644 index 0000000..1b56cb6 --- /dev/null +++ b/src/features/editor/editorMarkdownSnapshot.ts @@ -0,0 +1,21 @@ +export interface EditorMarkdownSnapshotSource { + flush?: () => void + getMarkdown: () => string +} + +interface ReadEditorMarkdownSnapshotOptions { + flushPending?: boolean + normalizeMarkdown?: (markdown: string) => string +} + +export function readEditorMarkdownSnapshot( + editor: EditorMarkdownSnapshotSource, + options: ReadEditorMarkdownSnapshotOptions = {}, +) { + if (options.flushPending) { + editor.flush?.() + } + + const markdown = editor.getMarkdown() + return options.normalizeMarkdown ? options.normalizeMarkdown(markdown) : markdown +} diff --git a/src/types/muya-core.d.ts b/src/types/muya-core.d.ts index 7ba9878..a0f7d10 100644 --- a/src/types/muya-core.d.ts +++ b/src/types/muya-core.d.ts @@ -64,6 +64,7 @@ declare module '@muyajs/core' { on(event: string, listener: (...args: unknown[]) => void): void off(event: string, listener: (...args: unknown[]) => void): void getMarkdown(): string + flush(): void getTOC(): ITocItem[] getHistory(): unknown setHistory(history: unknown): void