Procházet zdrojové kódy

refactor useSecondaryYdocs

Yuki Takei před 1 rokem
rodič
revize
62673a9b35

+ 1 - 1
packages/editor/src/client/components/CodeMirrorEditorMain.tsx

@@ -47,7 +47,7 @@ export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
     reviewMode: enableUnifiedMergeView,
   });
 
-  useUnifiedMergeView(enableUnifiedMergeView, codeMirrorEditor);
+  useUnifiedMergeView(enableUnifiedMergeView, codeMirrorEditor, { pageId });
 
   // setup additional extensions
   useEffect(() => {

+ 67 - 57
packages/editor/src/client/services-internal/unified-merge-view/use-unified-merge-view.ts

@@ -2,15 +2,13 @@ import { useEffect } from 'react';
 
 import {
   unifiedMergeView,
-  // originalDocChangeEffect,
-  // getOriginalDoc,
+  originalDocChangeEffect,
+  getOriginalDoc,
 } from '@codemirror/merge';
-// import { StateField, ChangeSet } from '@codemirror/state';
-
+import { ChangeSet } from '@codemirror/state';
 import * as Y from 'yjs';
 
-// import { CollaborativeChange } from '../../../consts/collaborative-change';
-// import { deltaToChangeSpecs } from '../../../utils/delta-to-changespecs';
+import { deltaToChangeSpecs } from '../../../utils/delta-to-changespecs';
 import type { UseCodeMirrorEditor } from '../../services';
 import { useSecondaryYdocs } from '../../stores/use-secondary-ydocs';
 
@@ -20,62 +18,74 @@ type Configuration = {
 }
 
 export const useUnifiedMergeView = (
-    isEnabled = false,
+    isEnabled: boolean,
     codeMirrorEditor?: UseCodeMirrorEditor,
     configuration?: Configuration,
 ): void => {
 
-  // const { pageId } = configuration ?? {};
+  const { pageId } = configuration ?? {};
 
-  // const { primaryDoc, secondaryDoc } = useSecondaryYdocs({
-  //   isEnabled,
-  //   pageId,
-  //   useSecondary: isEnabled,
-  // }) ?? {};
+  const { primaryDoc, secondaryDoc } = useSecondaryYdocs(isEnabled, {
+    pageId,
+    useSecondary: isEnabled,
+  }) ?? {};
 
   // setup unifiedMergeView
-  // useEffect(() => {
-  //   if (!isEnabled || primaryDoc == null || secondaryDoc == null || codeMirrorEditor == null) {
-  //     return;
-  //   }
-
-  //   const extension = isEnabled ? [
-  //     unifiedMergeView({
-  //       original: primaryDoc.getText('codemirror').toString(),
-  //     }),
-  //   ] : [];
-
-  //   const cleanupFunction = codeMirrorEditor?.appendExtensions(extension);
-  //   return cleanupFunction;
-  // }, [isEnabled, pageId, codeMirrorEditor, primaryDoc, secondaryDoc]);
-
-  // effect for updating orignal document by collaborative changes
-  // useEffect(() => {
-  //   if (!isEnabled || codeMirrorEditor == null) {
-  //     return;
-  //   }
-
-  //   const extension = StateField.define({
-  //     create: () => null,
-  //     update(value, tr) {
-  //       for (const e of tr.effects) {
-  //         if (e.is(CollaborativeChange)) {
-  //           const changeSpecs = deltaToChangeSpecs(e.value);
-  //           const changeSet = ChangeSet.of(changeSpecs, getOriginalDoc(tr.state).length);
-  //           const effect = originalDocChangeEffect(tr.state, changeSet);
-  //           setTimeout(() => {
-  //             codeMirrorEditor.view?.dispatch({
-  //               effects: effect,
-  //             });
-  //           }, 0);
-  //         }
-  //       }
-
-  //       return value;
-  //     },
-  //   });
-
-  //   const cleanupFunction = codeMirrorEditor.appendExtensions(extension);
-  //   return cleanupFunction;
-  // }, [codeMirrorEditor, isEnabled]);
+  useEffect(() => {
+    if (!isEnabled || primaryDoc == null || secondaryDoc == null || codeMirrorEditor == null) {
+      return;
+    }
+
+    const extension = isEnabled ? [
+      unifiedMergeView({
+        original: codeMirrorEditor.getDoc(),
+      }),
+    ] : [];
+
+    const cleanupFunction = codeMirrorEditor?.appendExtensions(extension);
+    return cleanupFunction;
+  }, [isEnabled, pageId, codeMirrorEditor, primaryDoc, secondaryDoc]);
+
+  // Setup sync from primaryDoc to secondaryDoc
+  useEffect(() => {
+    if (!isEnabled || primaryDoc == null || secondaryDoc == null || codeMirrorEditor == null) {
+      return;
+    }
+
+    const primaryYText = primaryDoc.getText('codemirror');
+
+    const sync = (event: Y.YTextEvent) => {
+      if (event.transaction.local) return;
+
+      if (codeMirrorEditor?.view?.state == null) {
+        return;
+      }
+
+      // sync from primaryDoc to secondaryDoc
+      Y.applyUpdate(secondaryDoc, Y.encodeStateAsUpdate(primaryDoc));
+
+      // sync from primaryDoc to original document
+      if (codeMirrorEditor?.view?.state != null) {
+        const changeSpecs = deltaToChangeSpecs(event.delta);
+        const originalDoc = getOriginalDoc(codeMirrorEditor.view.state);
+        const changeSet = ChangeSet.of(changeSpecs, originalDoc.length);
+        const effect = originalDocChangeEffect(codeMirrorEditor.view.state, changeSet);
+
+        // Dispatch in next tick to ensure state is updated
+        setTimeout(() => {
+          codeMirrorEditor.view?.dispatch({
+            effects: effect,
+          });
+        }, 0);
+      }
+    };
+
+    primaryYText.observe(sync);
+
+    // cleanup
+    return () => {
+      primaryYText.unobserve(sync);
+    };
+  }, [codeMirrorEditor, isEnabled, primaryDoc, secondaryDoc]);
+
 };

+ 1 - 11
packages/editor/src/client/stores/use-collaborative-editor-mode.ts

@@ -7,7 +7,6 @@ import { SocketIOProvider } from 'y-socket.io';
 import * as Y from 'yjs';
 
 import { userColor } from '../../consts';
-// import { CollaborativeChange } from '../../consts/collaborative-change';
 import type { EditingClient } from '../../interfaces';
 import type { UseCodeMirrorEditor } from '../services';
 
@@ -30,8 +29,7 @@ export const useCollaborativeEditorMode = (
     user, pageId, onEditorsUpdated, reviewMode,
   } = configuration ?? {};
 
-  const { primaryDoc, activeDoc } = useSecondaryYdocs({
-    isEnabled,
+  const { primaryDoc, activeDoc } = useSecondaryYdocs(isEnabled, {
     pageId,
     useSecondary: reviewMode,
   }) ?? {};
@@ -125,14 +123,6 @@ export const useCollaborativeEditorMode = (
 
     const activeText = activeDoc.getText('codemirror');
 
-    // setup observer to mark collaborative changes
-    // primaryDoc.getText('codemirror').observe((event) => {
-    //   if (event.transaction.local) return;
-    //   codeMirrorEditor.view?.dispatch({
-    //     effects: CollaborativeChange.of(event.delta),
-    //   });
-    // });
-
     const undoManager = new Y.UndoManager(activeText);
 
     // initialize document with activeDoc text

+ 56 - 100
packages/editor/src/client/stores/use-secondary-ydocs.ts

@@ -1,129 +1,85 @@
-import { useEffect, useState } from 'react';
+import { useEffect } from 'react';
 
+import useSWRImmutable from 'swr/immutable';
 import * as Y from 'yjs';
 
-type UseDocumentStateProps = {
-  isEnabled: boolean;
+type Configuration = {
   pageId?: string;
   useSecondary?: boolean;
 }
 
-type DocumentState = {
-  activeDoc: Y.Doc,
-  primaryDoc: Y.Doc,
-  secondaryDoc?: Y.Doc,
-}
-
-export const useSecondaryYdocs = ({ isEnabled, pageId, useSecondary }: UseDocumentStateProps): DocumentState | null => {
 
-  const [currentPageId, setCurrentPageId] = useState(pageId);
+type StoredYDocs = {
+  primaryDoc: Y.Doc;
+  secondaryDoc: Y.Doc | undefined;
+}
 
-  const [primaryDoc, setPrimaryDoc] = useState<Y.Doc>();
-  const [secondaryDoc, setSecondaryDoc] = useState<Y.Doc>();
+type YDocsState = StoredYDocs & {
+  activeDoc: Y.Doc,
+}
 
-  // Setup primaryDoc
-  useEffect(() => {
+const docsCache = new Map<string, StoredYDocs>();
 
-    let _primaryDoc: Y.Doc;
-
-    setPrimaryDoc((prevPrimaryDoc) => {
-      // keep the current ydoc if the conditions are met
-      if (isEnabled
-          // the given page ID is not null
-          && pageId != null
-          // the current page ID matches the given page ID,
-          && currentPageId === pageId
-          // the main document is already initialized
-          && prevPrimaryDoc != null
-      ) {
-        return prevPrimaryDoc;
-      }
+export const useSecondaryYdocs = (isEnabled: boolean, configuration?: Configuration): YDocsState | null => {
+  const { pageId, useSecondary = false } = configuration ?? {};
 
-      setCurrentPageId(pageId);
+  const cacheKey = `ydocs:${pageId}`;
 
-      // set undefined
-      if (!isEnabled) {
-        return undefined;
+  const { data: docs, mutate } = useSWRImmutable<StoredYDocs>(
+    isEnabled && pageId ? cacheKey : null,
+    () => {
+      // Return cached docs if they exist
+      const cached = docsCache.get(cacheKey);
+      if (cached) {
+        return cached;
       }
 
-      _primaryDoc = new Y.Doc();
-
-      return _primaryDoc;
-    });
-
-    // cleanup
-    return () => {
-      _primaryDoc?.destroy();
-    };
-  }, [isEnabled, currentPageId, pageId]);
+      // Create new docs
+      const primaryDoc = new Y.Doc();
+      const storedYdocs: StoredYDocs = { primaryDoc, secondaryDoc: undefined };
+      docsCache.set(cacheKey, storedYdocs);
+      return storedYdocs;
+    },
+  );
 
-  // Setup secondaryDoc
+  // Setup or cleanup secondaryDoc based on useSecondary flag
   useEffect(() => {
+    if (!docs) return;
 
-    let _secondaryDoc: Y.Doc;
-
-    setSecondaryDoc((prevSecondaryDoc) => {
-      // keep the current ydoc if the conditions are met
-      if (isEnabled
-          // the given page ID is not null
-          && pageId != null
-          // the current page ID matches the given page ID,
-          && currentPageId === pageId
-          // the main document is already initialized
-          && prevSecondaryDoc != null
-          // the review mode status matches the presence of the review document
-          && useSecondary === (prevSecondaryDoc != null)) {
-
-        return prevSecondaryDoc;
-      }
-
-      // set undefined
-      if (!isEnabled || primaryDoc == null || !useSecondary) {
-        return undefined;
-      }
-
-      _secondaryDoc = new Y.Doc();
-
-      const text = primaryDoc.getText('codemirror');
+    // Create secondaryDoc
+    if (useSecondary && docs.secondaryDoc == null) {
+      const secondaryDoc = new Y.Doc();
+      docsCache.set(cacheKey, { ...docs, secondaryDoc });
+      mutate({ ...docs, secondaryDoc }, false);
 
       // initialize secondaryDoc with primaryDoc state
-      Y.applyUpdate(_secondaryDoc, Y.encodeStateAsUpdate(primaryDoc));
-      // Setup sync from primaryDoc to secondaryDoc
-      text.observe((event) => {
-        if (event.transaction.local) return;
-        Y.applyUpdate(_secondaryDoc, Y.encodeStateAsUpdate(primaryDoc));
-      });
-
-      return _secondaryDoc;
-    });
-
-    // cleanup
+      Y.applyUpdate(secondaryDoc, Y.encodeStateAsUpdate(docs.primaryDoc));
+    }
+    // Cleanup secondaryDoc
+    else if (!useSecondary && docs.secondaryDoc != null) {
+      docs.secondaryDoc.destroy();
+      docsCache.set(cacheKey, { ...docs, secondaryDoc: undefined });
+      mutate({ ...docs, secondaryDoc: undefined }, false);
+    }
+
+    // Cleanup on unmount or when isEnabled becomes false
     return () => {
-      _secondaryDoc?.destroy();
+      if (!isEnabled && docsCache.has(cacheKey)) {
+        const state = docsCache.get(cacheKey);
+        state?.primaryDoc.destroy();
+        state?.secondaryDoc?.destroy();
+        docsCache.delete(cacheKey);
+      }
     };
+  }, [cacheKey, docs, isEnabled, useSecondary, mutate]);
 
-  }, [isEnabled, currentPageId, pageId, primaryDoc, useSecondary]);
-
-  // Handle secondaryDoc to primaryDoc sync when exiting review mode
-  // useEffect(() => {
-  //   if (!isEnabled || reviewMode || !secondaryDoc || !primaryDoc) {
-  //     return;
-  //   }
-
-  //   Y.applyUpdate(primaryDoc, Y.encodeStateAsUpdate(secondaryDoc));
-  //   secondaryDoc.destroy();
-  //   setsecondaryDoc(null);
-  // }, [isEnabled, reviewMode, secondaryDoc, primaryDoc]);
-
-
-  if (primaryDoc == null || (useSecondary && secondaryDoc == null)) {
+  if (!docs?.primaryDoc || (useSecondary && !docs?.secondaryDoc)) {
     return null;
   }
 
   return {
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    activeDoc: useSecondary ? secondaryDoc! : primaryDoc,
-    primaryDoc,
-    secondaryDoc,
+    activeDoc: docs.secondaryDoc ?? docs.primaryDoc,
+    primaryDoc: docs.primaryDoc,
+    secondaryDoc: docs.secondaryDoc,
   };
 };

+ 0 - 5
packages/editor/src/consts/collaborative-change.ts

@@ -1,5 +0,0 @@
-import { StateEffect } from '@codemirror/state';
-
-import type { Delta } from '../interfaces';
-
-export const CollaborativeChange = StateEffect.define<Delta>();