Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
95764c6
chore(split): SaveService from SyncService
max-nextcloud Jul 1, 2025
2405e75
chore(type): sync service with typescript
max-nextcloud Jul 2, 2025
8525912
chore(migrate): sync service mixin to composable
max-nextcloud Jul 2, 2025
507a7a5
chore(refactor): watch sync service to create save service
max-nextcloud Jul 3, 2025
594fb1a
chore(refactor): move connectSyncService into useSyncService composable
max-nextcloud Jul 3, 2025
725872c
refactor(compose): migrate save service to composable
max-nextcloud Jul 3, 2025
9fdf878
refactor(editor): detect rich editor based on markdown extension
max-nextcloud Jul 3, 2025
94f7619
refactor(editor): always provide an editor
max-nextcloud Jul 3, 2025
e8d184a
refactor(cleanup): unwrap connection
max-nextcloud Jul 3, 2025
c856f2f
chore(minor): clean up redundant injects
max-nextcloud Jul 3, 2025
00c18c4
chore(refactor): simplify types for props
max-nextcloud Jul 3, 2025
c71503e
chore(refactor): watch sync service in useConnection
max-nextcloud Jul 5, 2025
7265f02
chore(extract): Mentions api from extension
max-nextcloud Jul 5, 2025
f6b76f3
chore(simplify): SyncService.open returns void
max-nextcloud Jul 5, 2025
90ae016
chore(simplify): combine loaded and opened event
max-nextcloud Jul 5, 2025
8a46d28
chore(cleanup): unused getter
max-nextcloud Jul 5, 2025
798c4b8
fix(menu): call base components setup function
max-nextcloud Jul 5, 2025
8b45a52
chore(refactor): connect from useConnection composable
max-nextcloud Jul 5, 2025
7bc34c9
chore(cleanup): sync and save service are always defined now
max-nextcloud Jul 6, 2025
d4a09c4
test(cy): properly close connections
max-nextcloud Jul 6, 2025
e6fc63b
chore(refactor): sync service with new connection
max-nextcloud Jul 7, 2025
c3230d3
chore(refactor): instantiate SessionConnection with plain data
max-nextcloud Jul 7, 2025
7dc8e25
fix(sync): stop autosave when closing connection
max-nextcloud Jul 8, 2025
7c52d1c
chore(cleanup): ? on attributes that are always truthy
max-nextcloud Jul 8, 2025
f370913
chore(cleanup): avoid reuse of isRichEditor name
max-nextcloud Jul 8, 2025
6c76f27
chore(cleanup): remove outdated comment
max-nextcloud Jul 8, 2025
9ea9857
chore(copyright): fix year to 2025
max-nextcloud Jul 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
chore(split): SaveService from SyncService
Signed-off-by: Max <[email protected]>
  • Loading branch information
max-nextcloud committed Jul 7, 2025
commit 95764c68e3d9a33a37d090fa16a3c85e741f0d49
8 changes: 4 additions & 4 deletions src/components/CollisionResolveDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
</template>

<script>
import { useSyncServiceMixin } from './Editor.provider.ts'
import { useSaveServiceMixin, useSyncServiceMixin } from './Editor.provider.ts'
import { useEditorFlags } from '../composables/useEditorFlags.ts'
import { useEditor } from '../composables/useEditor.ts'
import NcButton from '@nextcloud/vue/components/NcButton'
Expand All @@ -39,7 +39,7 @@ export default {
components: {
NcButton,
},
mixins: [useSyncServiceMixin],
mixins: [useSaveServiceMixin, useSyncServiceMixin],
props: {
syncError: {
type: Object,
Expand All @@ -60,15 +60,15 @@ export default {
methods: {
resolveThisVersion() {
this.clicked = true
this.$syncService.forceSave().then(() => this.$syncService.syncUp())
this.$saveService.forceSave().then(() => this.$syncService.syncUp())
this.setEditable(!this.readOnly)
},
resolveServerVersion() {
const { outsideChange } = this.syncError.data
this.clicked = true
this.setEditable(!this.readOnly)
this.setContent(outsideChange, { isRichEditor: this.isRichEditor })
this.$syncService.forceSave().then(() => this.$syncService.syncUp())
this.$saveService.forceSave().then(() => this.$syncService.syncUp())
},
},
}
Expand Down
7 changes: 7 additions & 0 deletions src/components/Editor.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const FILE = Symbol('editor:file')
export const ATTACHMENT_RESOLVER = Symbol('attachment:resolver')
export const IS_MOBILE = Symbol('editor:is-mobile')
export const SYNC_SERVICE = Symbol('sync:service')
export const SAVE_SERVICE = Symbol('save:service')
export const EDITOR_UPLOAD = Symbol('editor:upload')
export const HOOK_MENTION_SEARCH = Symbol('hook:mention-search')
export const HOOK_MENTION_INSERT = Symbol('hook:mention-insert')
Expand All @@ -19,6 +20,12 @@ export const useSyncServiceMixin = {
},
}

export const useSaveServiceMixin = {
inject: {
$saveService: { from: SAVE_SERVICE, default: null },
},
}

export const useIsMobileMixin = {
inject: {
$isMobile: { from: IS_MOBILE, default: false },
Expand Down
32 changes: 21 additions & 11 deletions src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import {
FILE,
ATTACHMENT_RESOLVER,
IS_MOBILE,
SAVE_SERVICE,
SYNC_SERVICE,
} from './Editor.provider.ts'
import { provideEditorFlags } from '../composables/useEditorFlags.ts'
Expand All @@ -103,7 +104,8 @@ import ReadonlyBar from './Menu/ReadonlyBar.vue'

import { logger } from '../helpers/logger.js'
import { getDocumentState } from '../helpers/yjs.js'
import { SyncService, ERROR_TYPE, IDLE_TIMEOUT } from './../services/SyncService.js'
import { SaveService } from '../services/SaveService.js'
import { SyncService, ERROR_TYPE, IDLE_TIMEOUT } from '../services/SyncService.js'
import SessionApi from '../services/SessionApi.js'
import createSyncServiceProvider from './../services/SyncServiceProvider.js'
import AttachmentResolver from './../services/AttachmentResolver.js'
Expand Down Expand Up @@ -163,6 +165,9 @@ export default {
// using getters we can always provide the
// actual values without being reactive
Object.defineProperties(val, {
[SAVE_SERVICE]: {
get: () => this.saveService,
},
[SYNC_SERVICE]: {
get: () => this.syncService,
},
Expand Down Expand Up @@ -252,6 +257,7 @@ export default {
)
const baseVersionEtag = shallowRef(null)
const syncService = shallowRef(null)
const saveService = shallowRef(null)
const connectSyncService = () => {
const guestName = localStorage.getItem('nick') ?? ''
const api = new SessionApi({
Expand All @@ -262,6 +268,9 @@ export default {
syncService.value = new SyncService({
api,
baseVersionEtag: baseVersionEtag.value,
})
saveService.value = new SaveService({
syncService: syncService.value,
serialize: isRichEditor.value
? (content) =>
createMarkdownSerializer(editor.value?.schema).serialize(
Expand Down Expand Up @@ -301,6 +310,7 @@ export default {
language,
lowlightLoaded,
requireReconnect,
saveService,
setEditable,
syncProvider,
syncService,
Expand Down Expand Up @@ -450,7 +460,7 @@ export default {
unsubscribe('text:translate-modal:show', this.showTranslateModal)
if (this.dirty) {
const timeout = new Promise((resolve) => setTimeout(resolve, 2000))
await Promise.any([timeout, this.syncService.save()])
await Promise.any([timeout, this.saveService.save()])
}
await this.close()
removeFromDebugging(this)
Expand Down Expand Up @@ -631,15 +641,15 @@ export default {
},

onCreate({ editor }) {
const proseMirrorMarkdown = this.syncService.serialize(editor.state.doc)
const proseMirrorMarkdown = this.saveService.serialize(editor.state.doc)
this.emit('create:content', {
markdown: proseMirrorMarkdown,
})
},

onUpdate({ editor }) {
// this.debugContent(editor)
const proseMirrorMarkdown = this.syncService.serialize(editor.state.doc)
const proseMirrorMarkdown = this.saveService.serialize(editor.state.doc)
this.emit('update:content', {
markdown: proseMirrorMarkdown,
})
Expand Down Expand Up @@ -715,7 +725,7 @@ export default {
if (this.editor.can().undo() || this.editor.can().redo()) {
this.dirty = state.dirty
if (this.dirty) {
this.syncService.autosave()
this.saveService.autosave()
}
}
}
Expand Down Expand Up @@ -752,7 +762,7 @@ export default {
},

onKeyboardSave() {
this.syncService.save()
this.saveService.save()
},

onAddImageNode() {
Expand All @@ -764,7 +774,7 @@ export default {
},

async save() {
await this.syncService.save()
await this.saveService.save()
},

async disconnect() {
Expand Down Expand Up @@ -828,7 +838,7 @@ export default {
* @param {object} editor The Tiptap editor
*/
debugContent(editor) {
const proseMirrorMarkdown = this.syncService.serialize(editor.state.doc)
const proseMirrorMarkdown = this.saveService.serialize(editor.state.doc)
const markdownItHtml = markdownit.render(proseMirrorMarkdown)

logger.debug(
Expand All @@ -852,7 +862,7 @@ export default {
pendingStructs: this.ydoc.store.pendingStructs,
pendingStructsRemote: this.syncProvider?.remote.store.pendingStructs,
clientVectors: [],
documentState: this.syncService?.getDocumentState(),
documentState: this.saveService?.getDocumentState(),
}
for (const client of this.ydoc.store.clients.values()) {
yjsData.clientVectors.push(client.at(-1).id)
Expand All @@ -867,7 +877,7 @@ export default {

readOnlyToggled() {
if (this.editMode) {
this.syncService.save()
this.saveService.save()
}
this.editMode = !this.editMode
this.setEditable(this.editMode)
Expand Down Expand Up @@ -916,7 +926,7 @@ export default {
},

saveBeforeUnload() {
this.syncService?.saveViaSendBeacon()
this.saveService?.saveViaSendBeacon()
},
},
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/Editor/Status.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import NcButton from '@nextcloud/vue/components/NcButton'
import NcSavingIndicatorIcon from '@nextcloud/vue/components/NcSavingIndicatorIcon'
import {
useIsMobileMixin,
useSyncServiceMixin,
useSaveServiceMixin,
} from '../Editor.provider.ts'
import { useEditorFlags } from '../../composables/useEditorFlags.ts'
import refreshMoment from '../../mixins/refreshMoment.js'
Expand All @@ -50,7 +50,7 @@ export default {

mixins: [
useIsMobileMixin,
useSyncServiceMixin,
useSaveServiceMixin,
refreshMoment,
],

Expand Down Expand Up @@ -130,7 +130,7 @@ export default {
methods: {
onClickSave() {
if (this.dirtyStateIndicator) {
this.$syncService.forceSave()
this.$saveService.forceSave()
}
},
onEditorWidthChange(newWidth) {
Expand Down
91 changes: 91 additions & 0 deletions src/services/SaveService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/* eslint-disable jsdoc/valid-types */

import debounce from 'debounce'

import { logger } from '../helpers/logger.js'

/**
* Interval to save the serialized document and the document state
*
* @type {number} time in ms
*/
const AUTOSAVE_INTERVAL = 30000

class SaveService {
syncService
serialize
getDocumentState

constructor({ syncService, serialize, getDocumentState }) {
this.syncService = syncService
this.serialize = serialize
this.getDocumentState = getDocumentState
this.autosave = debounce(this._autosave.bind(this), AUTOSAVE_INTERVAL)
}

get connection() {
return this.syncService.connection
}

get emit() {
return this.syncService.emit.bind(this.syncService)
}

get hasActiveConnection() {
return this.connection && !this.connection.isClosed
}

_getContent() {
return this.serialize()
}

async save({ force = false, manualSave = true } = {}) {
logger.debug('[SaveService] saving', arguments[0])
try {
const response = await this.connection.save({
version: this.version,
autosaveContent: this._getContent(),
documentState: this.getDocumentState(),
force,
manualSave,
})
this.emit('stateChange', { dirty: false })
this.connection.document.lastSavedVersionTime = Date.now() / 1000
logger.debug('[SaveService] saved', response)
const { document, sessions } = response.data
this.emit('save', { document, sessions })
this.autosave.clear()
} catch (e) {
logger.error('Failed to save document.', { error: e })
throw e
}
}

saveViaSendBeacon() {
this.connection.saveViaSendBeacon({
version: this.version,
autosaveContent: this._getContent(),
documentState: this.getDocumentState(),
force: false,
manualSave: true,
})
logger.debug('[SaveService] saved using sendBeacon')
}

forceSave() {
return this.save({ force: true })
}

_autosave() {
return this.save({ manualSave: false }).catch((error) => {
logger.error('Failed to autosave document.', { error })
})
}
}

export { SaveService }
Loading