mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-16 16:14:43 -04:00
refactor: save ydoc state in the database, so it can be restored easier
By storing the ydoc state in the database we can reconnect lost clients easier and enable offline editing because we continue using the crdt data that has been used by the client before the connection loss. Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
4707540237
commit
a826677225
26 changed files with 301 additions and 204 deletions
|
@ -17,7 +17,6 @@ const syncAnnotation = Annotation.define()
|
|||
*/
|
||||
export class YTextSyncViewPlugin implements PluginValue {
|
||||
private readonly observer: YTextSyncViewPlugin['onYTextUpdate']
|
||||
private firstUpdate = true
|
||||
|
||||
constructor(private view: EditorView, private readonly yText: YText, pluginLoaded: () => void) {
|
||||
this.observer = this.onYTextUpdate.bind(this)
|
||||
|
@ -47,16 +46,7 @@ export class YTextSyncViewPlugin implements PluginValue {
|
|||
},
|
||||
[[], 0] as [ChangeSpec[], number]
|
||||
)
|
||||
return this.addDeleteAllChanges(changes)
|
||||
}
|
||||
|
||||
private addDeleteAllChanges(changes: ChangeSpec[]): ChangeSpec[] {
|
||||
if (this.firstUpdate) {
|
||||
this.firstUpdate = false
|
||||
return [{ from: 0, to: this.view.state.doc.length, insert: '' }, ...changes]
|
||||
} else {
|
||||
return changes
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
public update(update: ViewUpdate): void {
|
||||
|
|
|
@ -21,12 +21,11 @@ import { useCursorActivityCallback } from './hooks/use-cursor-activity-callback'
|
|||
import { useUpdateCodeMirrorReference } from './hooks/use-update-code-mirror-reference'
|
||||
import { useBindYTextToRedux } from './hooks/yjs/use-bind-y-text-to-redux'
|
||||
import { useCodeMirrorYjsExtension } from './hooks/yjs/use-code-mirror-yjs-extension'
|
||||
import { useMarkdownContentYText } from './hooks/yjs/use-markdown-content-y-text'
|
||||
import { useOnMetadataUpdated } from './hooks/yjs/use-on-metadata-updated'
|
||||
import { useOnNoteDeleted } from './hooks/yjs/use-on-note-deleted'
|
||||
import { useRealtimeConnection } from './hooks/yjs/use-realtime-connection'
|
||||
import { useRealtimeDoc } from './hooks/yjs/use-realtime-doc'
|
||||
import { useReceiveRealtimeUsers } from './hooks/yjs/use-receive-realtime-users'
|
||||
import { useYDoc } from './hooks/yjs/use-y-doc'
|
||||
import { useYDocSyncClientAdapter } from './hooks/yjs/use-y-doc-sync-client-adapter'
|
||||
import { useLinter } from './linter/linter'
|
||||
import { MaxLengthWarning } from './max-length-warning/max-length-warning'
|
||||
|
@ -57,8 +56,7 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
|
|||
useApplyScrollState(scrollState)
|
||||
|
||||
const messageTransporter = useRealtimeConnection()
|
||||
const yDoc = useYDoc(messageTransporter)
|
||||
const yText = useMarkdownContentYText(yDoc)
|
||||
const realtimeDoc = useRealtimeDoc()
|
||||
const editorScrollExtension = useCodeMirrorScrollWatchExtension(onScroll)
|
||||
const tablePasteExtensions = useCodeMirrorTablePasteExtension()
|
||||
const fileInsertExtension = useCodeMirrorFileInsertExtension()
|
||||
|
@ -70,13 +68,13 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
|
|||
|
||||
const linterExtension = useLinter()
|
||||
|
||||
const syncAdapter = useYDocSyncClientAdapter(messageTransporter, yDoc)
|
||||
const yjsExtension = useCodeMirrorYjsExtension(yText, syncAdapter)
|
||||
const syncAdapter = useYDocSyncClientAdapter(messageTransporter, realtimeDoc)
|
||||
const yjsExtension = useCodeMirrorYjsExtension(realtimeDoc, syncAdapter)
|
||||
|
||||
useOnMetadataUpdated(messageTransporter)
|
||||
useOnNoteDeleted(messageTransporter)
|
||||
|
||||
useBindYTextToRedux(yText)
|
||||
useBindYTextToRedux(realtimeDoc)
|
||||
useReceiveRealtimeUsers(messageTransporter)
|
||||
|
||||
const extensions = useMemo(
|
||||
|
|
|
@ -4,21 +4,19 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { setNoteContent } from '../../../../../redux/note-details/methods'
|
||||
import type { RealtimeDoc } from '@hedgedoc/commons'
|
||||
import { useEffect } from 'react'
|
||||
import type { YText } from 'yjs/dist/src/types/YText'
|
||||
|
||||
/**
|
||||
* One-Way-synchronizes the text of the given {@link YText y-text} into the global application state.
|
||||
* One-Way-synchronizes the text of the markdown content channel from the given {@link RealtimeDoc realtime doc} into the global application state.
|
||||
*
|
||||
* @param yText The source text
|
||||
* @param realtimeDoc The {@link RealtimeDoc realtime doc} that contains the markdown content
|
||||
*/
|
||||
export const useBindYTextToRedux = (yText: YText | undefined): void => {
|
||||
export const useBindYTextToRedux = (realtimeDoc: RealtimeDoc): void => {
|
||||
useEffect(() => {
|
||||
if (!yText) {
|
||||
return
|
||||
}
|
||||
const yText = realtimeDoc.getMarkdownContentChannel()
|
||||
const yTextCallback = () => setNoteContent(yText.toString())
|
||||
yText.observe(yTextCallback)
|
||||
return () => yText.unobserve(yTextCallback)
|
||||
}, [yText])
|
||||
}, [realtimeDoc])
|
||||
}
|
||||
|
|
|
@ -8,30 +8,33 @@ import { YTextSyncViewPlugin } from '../../codemirror-extensions/document-sync/y
|
|||
import type { Extension } from '@codemirror/state'
|
||||
import { ViewPlugin } from '@codemirror/view'
|
||||
import type { YDocSyncClientAdapter } from '@hedgedoc/commons'
|
||||
import type { RealtimeDoc } from '@hedgedoc/commons'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { Text as YText } from 'yjs'
|
||||
|
||||
/**
|
||||
* Creates a {@link Extension code mirror extension} that synchronizes an editor with the given {@link YText ytext}.
|
||||
*
|
||||
* @param yText The source and target for the editor content
|
||||
* @param doc The {@link RealtimeDoc realtime doc} that contains the markdown content text channel
|
||||
* @param syncAdapter The sync adapter that processes the communication for content synchronisation.
|
||||
* @return the created extension
|
||||
*/
|
||||
export const useCodeMirrorYjsExtension = (yText: YText | undefined, syncAdapter: YDocSyncClientAdapter): Extension => {
|
||||
export const useCodeMirrorYjsExtension = (doc: RealtimeDoc, syncAdapter: YDocSyncClientAdapter): Extension => {
|
||||
const [editorReady, setEditorReady] = useState(false)
|
||||
const synchronized = useApplicationState((state) => state.realtimeStatus.isSynced)
|
||||
const connected = useApplicationState((state) => state.realtimeStatus.isConnected)
|
||||
|
||||
useEffect(() => {
|
||||
if (editorReady && connected && !synchronized && yText) {
|
||||
if (editorReady && connected && !synchronized) {
|
||||
syncAdapter.requestDocumentState()
|
||||
}
|
||||
}, [connected, editorReady, syncAdapter, synchronized, yText])
|
||||
}, [connected, editorReady, syncAdapter, synchronized])
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
yText ? [ViewPlugin.define((view) => new YTextSyncViewPlugin(view, yText, () => setEditorReady(true)))] : [],
|
||||
[yText]
|
||||
() => [
|
||||
ViewPlugin.define(
|
||||
(view) => new YTextSyncViewPlugin(view, doc.getMarkdownContentChannel(), () => setEditorReady(true))
|
||||
)
|
||||
],
|
||||
[doc]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { RealtimeDoc } from '@hedgedoc/commons'
|
||||
import { useMemo } from 'react'
|
||||
import type { Text as YText } from 'yjs'
|
||||
|
||||
/**
|
||||
* Extracts the y-text channel that saves the markdown content from the given yDoc.
|
||||
*
|
||||
* @param yDoc The yjs document from which the yText should be extracted
|
||||
* @return the extracted yText channel
|
||||
*/
|
||||
export const useMarkdownContentYText = (yDoc: RealtimeDoc | undefined): YText | undefined => {
|
||||
return useMemo(() => yDoc?.getMarkdownContentChannel(), [yDoc])
|
||||
}
|
|
@ -95,7 +95,7 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
|||
}, [messageTransporter])
|
||||
|
||||
useEffect(() => {
|
||||
const connectedListener = messageTransporter.doAsSoonAsConnected(() => setRealtimeConnectionState(true))
|
||||
const connectedListener = messageTransporter.doAsSoonAsReady(() => setRealtimeConnectionState(true))
|
||||
const disconnectedListener = messageTransporter.on('disconnected', () => setRealtimeConnectionState(false), {
|
||||
objectify: true
|
||||
}) as Listener
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { RealtimeDoc } from '@hedgedoc/commons'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* Creates a new {@link RealtimeDoc y-doc}.
|
||||
*
|
||||
* @return The created {@link RealtimeDoc y-doc}
|
||||
*/
|
||||
export const useRealtimeDoc = (): RealtimeDoc => {
|
||||
const doc = useMemo(() => new RealtimeDoc(), [])
|
||||
|
||||
useEffect(() => () => doc.destroy(), [doc])
|
||||
|
||||
return doc
|
||||
}
|
|
@ -5,11 +5,10 @@
|
|||
*/
|
||||
import { setRealtimeSyncedState } from '../../../../../redux/realtime/methods'
|
||||
import { Logger } from '../../../../../utils/logger'
|
||||
import type { MessageTransporter } from '@hedgedoc/commons'
|
||||
import type { MessageTransporter, RealtimeDoc } from '@hedgedoc/commons'
|
||||
import { YDocSyncClientAdapter } from '@hedgedoc/commons'
|
||||
import type { Listener } from 'eventemitter2'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import type { Doc } from 'yjs'
|
||||
|
||||
const logger = new Logger('useYDocSyncClient')
|
||||
|
||||
|
@ -17,18 +16,14 @@ const logger = new Logger('useYDocSyncClient')
|
|||
* Creates a {@link YDocSyncClientAdapter} and mirrors its sync state to the global application state.
|
||||
*
|
||||
* @param messageTransporter The {@link MessageTransporter message transporter} that sends and receives messages for the synchronisation
|
||||
* @param yDoc The {@link Doc y-doc} that should be synchronized
|
||||
* @param doc The {@link RealtimeDoc realtime doc} that should be synchronized
|
||||
* @return the created adapter
|
||||
*/
|
||||
export const useYDocSyncClientAdapter = (
|
||||
messageTransporter: MessageTransporter,
|
||||
yDoc: Doc | undefined
|
||||
doc: RealtimeDoc
|
||||
): YDocSyncClientAdapter => {
|
||||
const syncAdapter = useMemo(() => new YDocSyncClientAdapter(messageTransporter), [messageTransporter])
|
||||
|
||||
useEffect(() => {
|
||||
syncAdapter.setYDoc(yDoc)
|
||||
}, [syncAdapter, yDoc])
|
||||
const syncAdapter = useMemo(() => new YDocSyncClientAdapter(messageTransporter, doc), [doc, messageTransporter])
|
||||
|
||||
useEffect(() => {
|
||||
const onceSyncedListener = syncAdapter.doAsSoonAsSynced(() => {
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { MessageTransporter } from '@hedgedoc/commons'
|
||||
import { RealtimeDoc } from '@hedgedoc/commons'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Creates a new {@link RealtimeDoc y-doc}.
|
||||
*
|
||||
* @return The created {@link RealtimeDoc y-doc}
|
||||
*/
|
||||
export const useYDoc = (messageTransporter: MessageTransporter): RealtimeDoc | undefined => {
|
||||
const [yDoc, setYDoc] = useState<RealtimeDoc>()
|
||||
|
||||
useEffect(() => {
|
||||
messageTransporter.doAsSoonAsConnected(() => {
|
||||
setYDoc(new RealtimeDoc())
|
||||
})
|
||||
messageTransporter.on('disconnected', () => {
|
||||
setYDoc(undefined)
|
||||
})
|
||||
}, [messageTransporter])
|
||||
|
||||
useEffect(() => () => yDoc?.destroy(), [yDoc])
|
||||
|
||||
return yDoc
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue