Просмотр исходного кода

Made YjsConnectionManager singleton

Shun Miyazawa 2 лет назад
Родитель
Сommit
c55d2da998

+ 2 - 7
apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -2,7 +2,7 @@ import { useCallback, useEffect } from 'react';
 
 import type EventEmitter from 'events';
 
-import { Origin, GlobalSocketEventName } from '@growi/core';
+import { Origin } from '@growi/core';
 import type { DrawioEditByViewerProps } from '@growi/remark-drawio';
 
 import { extractRemoteRevisionDataFromErrorObj, updatePage as _updatePage } from '~/client/services/update-page';
@@ -11,7 +11,6 @@ import { useShareLinkId } from '~/stores/context';
 import { useConflictDiffModal, useDrawioModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { type RemoteRevisionData, useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
-import { useDefaultSocket } from '~/stores/socket-io';
 import loggerFactory from '~/utils/logger';
 
 
@@ -29,8 +28,6 @@ export const useDrawioModalLauncherForView = (opts?: {
   onSaveError?: (error: any) => void,
 }): void => {
 
-  const { data: socket } = useDefaultSocket();
-
   const { data: shareLinkId } = useShareLinkId();
 
   const { data: currentPage } = useSWRxCurrentPage();
@@ -55,8 +52,6 @@ export const useDrawioModalLauncherForView = (opts?: {
         origin: Origin.View,
       });
 
-      socket?.emit(GlobalSocketEventName.YDocUpdate, { pageId: currentPage._id, newMarkdown });
-
       closeConflictDiffModal();
       opts?.onSaveSuccess?.();
     }
@@ -69,7 +64,7 @@ export const useDrawioModalLauncherForView = (opts?: {
       logger.error('failed to save', error);
       opts?.onSaveError?.(error);
     }
-  }, [closeConflictDiffModal, currentPage, opts, shareLinkId, socket]);
+  }, [closeConflictDiffModal, currentPage, opts, shareLinkId]);
 
   // eslint-disable-next-line max-len
   const generateResolveConflictHandler = useCallback((revisionId: string, onConflict: (conflictData: RemoteRevisionData, newMarkdown: string) => void) => {

+ 2 - 6
apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -2,7 +2,7 @@ import { useCallback, useEffect } from 'react';
 
 import type EventEmitter from 'events';
 
-import { GlobalSocketEventName, Origin } from '@growi/core';
+import { Origin } from '@growi/core';
 
 import type MarkdownTable from '~/client/models/MarkdownTable';
 import { extractRemoteRevisionDataFromErrorObj, updatePage as _updatePage } from '~/client/services/update-page';
@@ -11,7 +11,6 @@ import { useShareLinkId } from '~/stores/context';
 import { useHandsontableModal, useConflictDiffModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { type RemoteRevisionData, useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
-import { useDefaultSocket } from '~/stores/socket-io';
 import loggerFactory from '~/utils/logger';
 
 
@@ -28,7 +27,6 @@ export const useHandsontableModalLauncherForView = (opts?: {
   onSaveSuccess?: () => void,
   onSaveError?: (error: any) => void,
 }): void => {
-  const { data: socket } = useDefaultSocket();
 
   const { data: shareLinkId } = useShareLinkId();
 
@@ -54,8 +52,6 @@ export const useHandsontableModalLauncherForView = (opts?: {
         origin: Origin.View,
       });
 
-      socket?.emit(GlobalSocketEventName.YDocUpdate, { pageId: currentPage._id, newMarkdown });
-
       closeConflictDiffModal();
       opts?.onSaveSuccess?.();
     }
@@ -68,7 +64,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
       logger.error('failed to save', error);
       opts?.onSaveError?.(error);
     }
-  }, [closeConflictDiffModal, currentPage, opts, shareLinkId, socket]);
+  }, [closeConflictDiffModal, currentPage, opts, shareLinkId]);
 
   // eslint-disable-next-line max-len
   const generateResolveConflictHandler = useCallback((revisionId: string, onConflict: (conflictData: RemoteRevisionData, newMarkdown: string) => void) => {

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

@@ -1,4 +1,4 @@
-import { allOrigin } from '@growi/core';
+import { Origin, allOrigin } from '@growi/core';
 import type {
   IPage, IRevisionHasId, IUserHasId,
 } from '@growi/core';
@@ -20,6 +20,7 @@ import {
 import type { PageDocument, PageModel } from '~/server/models/page';
 import { configManager } from '~/server/service/config-manager';
 import { preNotifyService } from '~/server/service/pre-notify';
+import { YjsConnectionManager } from '~/server/service/yjs-connection-manager';
 import Xss from '~/services/xss';
 import XssOption from '~/services/xss/xssOption';
 import loggerFactory from '~/utils/logger';
@@ -79,6 +80,12 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
 
 
   async function postAction(req: UpdatePageRequest, res: ApiV3Response, updatedPage: PageDocument) {
+    // Reflect the updates in ydoc
+    const origin = req.body.origin;
+    if (origin === Origin.View || origin === undefined) {
+      await YjsConnectionManager().handleYDocUpdate(req.body.pageId, req.body.body);
+    }
+
     // persist activity
     const parameters = {
       targetModel: SupportedTargetModel.MODEL_PAGE,

+ 2 - 16
apps/app/src/server/service/socket-io.js

@@ -5,7 +5,7 @@ import loggerFactory from '~/utils/logger';
 
 import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
 
-import YjsConnectionManager from './yjs-connection-manager';
+import { YjsConnectionManager } from './yjs-connection-manager';
 
 const expressSession = require('express-session');
 const passport = require('passport');
@@ -38,7 +38,7 @@ class SocketIoService {
     this.io.attach(server);
 
     // create the YjsConnectionManager instance
-    this.yjsConnectionManager = new YjsConnectionManager(this.io);
+    this.yjsConnectionManager = YjsConnectionManager(this.io);
 
     // create namespace for admin
     this.adminNamespace = this.io.of('/admin');
@@ -55,7 +55,6 @@ class SocketIoService {
     await this.setupLoginedUserRoomsJoinOnConnection();
     await this.setupDefaultSocketJoinRoomsEventHandler();
     await this.setupYjsConnection();
-    await this.setupYjsUpdateEvent();
   }
 
   getDefaultSocket() {
@@ -174,19 +173,6 @@ class SocketIoService {
     });
   }
 
-  setupYjsUpdateEvent() {
-    this.io.on('connection', (socket) => {
-      socket.on(GlobalSocketEventName.YDocUpdate, async({ pageId, newMarkdown }) => {
-        try {
-          await this.yjsConnectionManager.handleYDocUpdate(pageId, newMarkdown);
-        }
-        catch (error) {
-          logger.warn(error.message);
-        }
-      });
-    });
-  }
-
   async checkConnectionLimitsForAdmin(socket, next) {
     const namespaceName = socket.nsp.name;
 

+ 24 - 5
apps/app/src/server/service/yjs-connection-manager.ts

@@ -5,16 +5,27 @@ import * as Y from 'yjs';
 
 import { getMongoUri } from '../util/mongoose-utils';
 
+let instance: YjsConnectionManagerImpl | undefined;
+
 export const MONGODB_PERSISTENCE_COLLECTION_NAME = 'yjs-writings';
 export const MONGODB_PERSISTENCE_FLUSH_SIZE = 100;
 
-class YjsConnectionManager {
+interface YjsConnectionManager {
+  handleYDocSync(pageId: string, initialValue: string): Promise<void>;
+  handleYDocUpdate(pageId: string, newValue: string): Promise<void>;
+}
+
+class YjsConnectionManagerImpl implements YjsConnectionManager {
 
   private ysocketio: YSocketIO;
 
   private mdb: MongodbPersistence;
 
-  constructor(io: Server) {
+  constructor(io?: Server) {
+    if (io == null) {
+      throw new Error('io is required');
+    }
+
     this.ysocketio = new YSocketIO(io);
     this.ysocketio.initialize();
 
@@ -24,6 +35,9 @@ class YjsConnectionManager {
     });
 
     this.getCurrentYdoc = this.getCurrentYdoc.bind(this);
+
+    // eslint-disable-next-line @typescript-eslint/no-this-alias
+    instance = this;
   }
 
   public async handleYDocSync(pageId: string, initialValue: string): Promise<void> {
@@ -60,13 +74,13 @@ class YjsConnectionManager {
     persistedYdoc.destroy();
   }
 
-  public async handleYDocUpdate(pageId: string, newMarkdown: string): Promise<void> {
+  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);
     const currentMarkdownLength = currentYdoc.getText('codemirror').length;
     currentYdoc.getText('codemirror').delete(0, currentMarkdownLength);
-    currentYdoc.getText('codemirror').insert(0, newMarkdown);
+    currentYdoc.getText('codemirror').insert(0, newValue);
     Y.encodeStateAsUpdate(currentYdoc);
   }
 
@@ -80,4 +94,9 @@ class YjsConnectionManager {
 
 }
 
-export default YjsConnectionManager;
+export const YjsConnectionManager = (io?: Server): YjsConnectionManagerImpl => {
+  if (instance != null) {
+    return instance;
+  }
+  return new YjsConnectionManagerImpl(io);
+};

+ 0 - 1
packages/core/src/interfaces/websocket.ts

@@ -2,6 +2,5 @@ export const GlobalSocketEventName = {
   // YDoc
   YDocSync: 'ydoc:sync',
   YDocSyncError: 'ydoc:sync:error',
-  YDocUpdate: 'ydoc:update',
 } as const;
 export type GlobalSocketEventName = typeof GlobalSocketEventName[keyof typeof GlobalSocketEventName];