소스 검색

implement syncYDoc method

Yuki Takei 1 년 전
부모
커밋
817e325a2d
3개의 변경된 파일91개의 추가작업 그리고 44개의 파일을 삭제
  1. 1 1
      apps/app/src/server/routes/apiv3/page/update-page.ts
  2. 81 0
      apps/app/src/server/service/yjs/sync-ydoc.ts
  3. 9 43
      apps/app/src/server/service/yjs/yjs.ts

+ 1 - 1
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -68,7 +68,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     const origin = req.body.origin;
     const origin = req.body.origin;
     if (origin === Origin.View || origin === undefined) {
     if (origin === Origin.View || origin === undefined) {
       const yjsService = getYjsService();
       const yjsService = getYjsService();
-      await yjsService.handleYDocUpdate(req.body.pageId, req.body.body);
+      await yjsService.syncWithTheLatestRevisionForce(req.body.pageId);
     }
     }
 
 
     // persist activity
     // persist activity

+ 81 - 0
apps/app/src/server/service/yjs/sync-ydoc.ts

@@ -0,0 +1,81 @@
+import { Origin, YDocStatus } from '@growi/core';
+import type { Document } from 'y-socket.io/dist/server';
+
+import loggerFactory from '~/utils/logger';
+
+import { Revision } from '../../models/revision';
+
+import type { MongodbPersistence } from './extended/mongodb-persistence';
+
+const logger = loggerFactory('growi:service:yjs:sync-ydoc');
+
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type Delta = Array<{insert?:Array<any>|string, delete?:number, retain?:number}>;
+
+type Context = {
+  ydocStatus: YDocStatus,
+}
+
+/**
+ * Sync the text and the meta data with the latest revision body
+ * @param mdb
+ * @param doc
+ * @param context true to force sync
+ */
+export const syncYDoc = async(mdb: MongodbPersistence, doc: Document, context: true | Context): Promise<void> => {
+  const pageId = doc.name;
+
+  const revision = await Revision
+    .findOne(
+      // filter
+      { pageId },
+      // projection
+      { body: 1, createdAt: 1, origin: 1 },
+      // options
+      { sort: { createdAt: -1 } },
+    )
+    .lean();
+
+  if (revision == null) {
+    logger.warn(`Synchronization has been canceled since the revision of the page ('${pageId}') could not be found`);
+    return;
+  }
+
+  const shouldSync = context === true
+    || (() => {
+      switch (context.ydocStatus) {
+        case YDocStatus.NEW:
+          return true;
+        case YDocStatus.OUTDATED:
+          // should skip when the YDoc is outdated and the latest revision is created by the editor
+          return revision.origin !== Origin.Editor;
+        default:
+          return false;
+      }
+    })();
+
+  if (shouldSync) {
+    logger.debug(`YDoc for the page ('${pageId}') is synced with the latest revision body`);
+
+    const ytext = doc.getText('codemirror');
+    const delta: Delta = [];
+
+    if (ytext.length > 0) {
+      delta.push({ delete: ytext.length });
+    }
+    if (revision.body != null) {
+      delta.push({ insert: revision.body });
+    }
+
+    ytext.applyDelta(delta, { sanitize: false });
+  }
+
+  const shouldSyncMeta = context === true
+    || context.ydocStatus === YDocStatus.NEW
+    || context.ydocStatus === YDocStatus.OUTDATED;
+
+  if (shouldSyncMeta) {
+    mdb.setMeta(doc.name, 'updatedAt', revision.createdAt.getTime() ?? Date.now());
+  }
+};

+ 9 - 43
apps/app/src/server/service/yjs/yjs.ts

@@ -6,7 +6,6 @@ import mongoose from 'mongoose';
 import type { Server } from 'socket.io';
 import type { Server } from 'socket.io';
 import type { Document } from 'y-socket.io/dist/server';
 import type { Document } from 'y-socket.io/dist/server';
 import { YSocketIO, type Document as Ydoc } from 'y-socket.io/dist/server';
 import { YSocketIO, type Document as Ydoc } from 'y-socket.io/dist/server';
-import * as Y from 'yjs';
 
 
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import { RoomPrefix, getRoomNameWithId } from '~/server/util/socket-io-helpers';
 import { RoomPrefix, getRoomNameWithId } from '~/server/util/socket-io-helpers';
@@ -18,6 +17,7 @@ import { Revision } from '../../models/revision';
 import { createIndexes } from './create-indexes';
 import { createIndexes } from './create-indexes';
 import { createMongoDBPersistence } from './create-mongodb-persistence';
 import { createMongoDBPersistence } from './create-mongodb-persistence';
 import { MongodbPersistence } from './extended/mongodb-persistence';
 import { MongodbPersistence } from './extended/mongodb-persistence';
+import { syncYDoc } from './sync-ydoc';
 
 
 
 
 const MONGODB_PERSISTENCE_COLLECTION_NAME = 'yjs-writings';
 const MONGODB_PERSISTENCE_COLLECTION_NAME = 'yjs-writings';
@@ -27,14 +27,12 @@ const MONGODB_PERSISTENCE_FLUSH_SIZE = 100;
 const logger = loggerFactory('growi:service:yjs');
 const logger = loggerFactory('growi:service:yjs');
 
 
 
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type Delta = Array<{insert?:Array<any>|string, delete?:number, retain?:number}>;
 type RequestWithUser = IncomingMessage & { user: IUserHasId };
 type RequestWithUser = IncomingMessage & { user: IUserHasId };
 
 
 
 
 export interface IYjsService {
 export interface IYjsService {
   getYDocStatus(pageId: string): Promise<YDocStatus>;
   getYDocStatus(pageId: string): Promise<YDocStatus>;
-  handleYDocUpdate(pageId: string, newValue: string): Promise<void>;
+  syncWithTheLatestRevisionForce(pageId: string): Promise<void>;
   getCurrentYdoc(pageId: string): Ydoc | undefined;
   getCurrentYdoc(pageId: string): Ydoc | undefined;
 }
 }
 
 
@@ -80,37 +78,9 @@ class YjsService implements IYjsService {
     ysocketio.on('document-loaded', async(doc: Document) => {
     ysocketio.on('document-loaded', async(doc: Document) => {
       const pageId = doc.name;
       const pageId = doc.name;
 
 
-      if (pageId == null) {
-        return;
-      }
-
       const ydocStatus = await this.getYDocStatus(pageId);
       const ydocStatus = await this.getYDocStatus(pageId);
-      const shouldSync = ydocStatus === YDocStatus.NEW || ydocStatus === YDocStatus.OUTDATED;
-
-      if (shouldSync) {
-        logger.debug(`Initialize the page ('${pageId}') with the latest revision body`);
-
-        const revision = await Revision
-          .findOne({ pageId })
-          .sort({ createdAt: -1 })
-          .lean();
-
-        if (revision?.body != null) {
-          const ytext = doc.getText('codemirror');
-          const delta: Delta = (ydocStatus === YDocStatus.OUTDATED && ytext.length > 0)
-            ? [
-              { delete: ytext.length },
-              { insert: revision.body },
-            ]
-            : [
-              { insert: revision.body },
-            ];
-
-          ytext.applyDelta(delta, { sanitize: false });
-        }
-
-        mdb.setMeta(doc.name, 'updatedAt', revision?.createdAt.getTime() ?? Date.now());
-      }
+
+      syncYDoc(mdb, doc, { ydocStatus });
     });
     });
 
 
     ysocketio.on('awareness-update', async(doc: Document) => {
     ysocketio.on('awareness-update', async(doc: Document) => {
@@ -210,18 +180,14 @@ class YjsService implements IYjsService {
     return YDocStatus.OUTDATED;
     return YDocStatus.OUTDATED;
   }
   }
 
 
-  public async handleYDocUpdate(pageId: string, newValue: string): Promise<void> {
-    // TODO: https://redmine.weseek.co.jp/issues/132775
-    // It's necessary to confirm that the user is not editing the target page in the Editor
-    const currentYdoc = this.getCurrentYdoc(pageId);
-    if (currentYdoc == null) {
+  public async syncWithTheLatestRevisionForce(pageId: string): Promise<void> {
+    const doc = this.ysocketio.documents.get(pageId);
+
+    if (doc == null) {
       return;
       return;
     }
     }
 
 
-    const currentMarkdownLength = currentYdoc.getText('codemirror').length;
-    currentYdoc.getText('codemirror').delete(0, currentMarkdownLength);
-    currentYdoc.getText('codemirror').insert(0, newValue);
-    Y.encodeStateAsUpdate(currentYdoc);
+    syncYDoc(this.mdb, doc, true);
   }
   }
 
 
   public getCurrentYdoc(pageId: string): Ydoc | undefined {
   public getCurrentYdoc(pageId: string): Ydoc | undefined {