diff --git a/src/components/CollaborativeEditor.vue b/src/components/CollaborativeEditor.vue index da0b2f86d22..321a068ef4c 100644 --- a/src/components/CollaborativeEditor.vue +++ b/src/components/CollaborativeEditor.vue @@ -650,6 +650,10 @@ export default defineComponent({ content, isRichEditor: this.isRichEditor, }) + // The resulting push still needs to reach the server like any + // other step, but should not autosave a document nobody has + // edited yet. + this.saveService.skipNextAutosaveTrigger() setInitialYjsState(this.ydoc, content, { isRichEditor: this.isRichEditor, }) diff --git a/src/composables/useSaveService.ts b/src/composables/useSaveService.ts index 7d1b3bfa9d0..0b14de6cc53 100644 --- a/src/composables/useSaveService.ts +++ b/src/composables/useSaveService.ts @@ -49,10 +49,10 @@ export function provideSaveService( getSaveData, }) - syncService.bus.on('changesPushed', saveService.autosave) + syncService.bus.on('changesPushed', saveService.autosaveOnChangesPushed) syncService.bus.on('close', saveService.clear) onUnmounted(() => { - syncService.bus.off('changesPushed', saveService.autosave) + syncService.bus.off('changesPushed', saveService.autosaveOnChangesPushed) syncService.bus.off('close', saveService.clear) }) diff --git a/src/services/SaveService.ts b/src/services/SaveService.ts index 0992ea12d71..2a1aa66b22d 100644 --- a/src/services/SaveService.ts +++ b/src/services/SaveService.ts @@ -39,7 +39,9 @@ class SaveService { pendingAutosave = 0 getSaveData autosave + autosaveOnChangesPushed clear + #skipNextAutosaveTrigger = false constructor({ connection, @@ -54,9 +56,22 @@ class SaveService { this.document = document this.getSaveData = getSaveData this.autosave = debounce(this._autosave.bind(this), AUTOSAVE_DEBOUNCE * 1000) + this.autosaveOnChangesPushed = this._autosaveOnChangesPushed.bind(this) this.clear = this.clearAutosave.bind(this) } + skipNextAutosaveTrigger() { + this.#skipNextAutosaveTrigger = true + } + + _autosaveOnChangesPushed() { + if (this.#skipNextAutosaveTrigger) { + this.#skipNextAutosaveTrigger = false + return + } + this.autosave() + } + /** * Save the current state * diff --git a/src/tests/services/SaveService.spec.ts b/src/tests/services/SaveService.spec.ts new file mode 100644 index 00000000000..f2bf08e6a68 --- /dev/null +++ b/src/tests/services/SaveService.spec.ts @@ -0,0 +1,60 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { describe, expect, it, vi } from 'vitest' +import { shallowRef } from 'vue' +import { SaveService } from '../../services/SaveService.ts' + +function createSaveService() { + return new SaveService({ + connection: shallowRef(undefined), + document: shallowRef(undefined), + getSaveData: vi.fn(), + }) +} + +describe('SaveService.autosaveOnChangesPushed', () => { + it('autosaves for a normal changesPushed trigger', () => { + const saveService = createSaveService() + const autosave = vi.spyOn(saveService, 'autosave') + + saveService.autosaveOnChangesPushed() + + expect(autosave).toHaveBeenCalledOnce() + }) + + it('does not autosave right after skipNextAutosaveTrigger', () => { + const saveService = createSaveService() + const autosave = vi.spyOn(saveService, 'autosave') + + saveService.skipNextAutosaveTrigger() + saveService.autosaveOnChangesPushed() + + expect(autosave).not.toHaveBeenCalled() + }) + + it('only skips once - the next trigger autosaves normally', () => { + const saveService = createSaveService() + const autosave = vi.spyOn(saveService, 'autosave') + + saveService.skipNextAutosaveTrigger() + saveService.autosaveOnChangesPushed() + saveService.autosaveOnChangesPushed() + + expect(autosave).toHaveBeenCalledOnce() + }) + + it('is unaffected by unrelated triggers before the skip is armed', () => { + const saveService = createSaveService() + const autosave = vi.spyOn(saveService, 'autosave') + + saveService.autosaveOnChangesPushed() + saveService.skipNextAutosaveTrigger() + saveService.autosaveOnChangesPushed() + saveService.autosaveOnChangesPushed() + + expect(autosave).toHaveBeenCalledTimes(2) + }) +})