Browse Source

refactor YjsService initialization

Yuki Takei 1 năm trước cách đây
mục cha
commit
29d0baa90c

+ 2 - 3
apps/app/src/server/crowi/index.js

@@ -36,7 +36,7 @@ import SearchService from '../service/search';
 import { SlackIntegrationService } from '../service/slack-integration';
 import UserGroupService from '../service/user-group';
 import { UserNotificationService } from '../service/user-notification';
-import { instantiateYjsService } from '../service/yjs';
+import { initializeYjsService } from '../service/yjs';
 import { getMongoUri, mongoOptions } from '../util/mongoose-utils';
 
 
@@ -476,8 +476,7 @@ Crowi.prototype.start = async function() {
   this.socketIoService.attachServer(httpServer);
 
   // Initialization YjsService
-  instantiateYjsService(this.socketIoService.io);
-  this.socketIoService.setupYjsConnection();
+  initializeYjsService(this.socketIoService.io, this.pageService);
 
   await this.autoInstall();
 

+ 0 - 43
apps/app/src/server/service/socket-io.ts

@@ -1,12 +1,10 @@
 import type { IncomingMessage } from 'http';
 
 import type { IUserHasId } from '@growi/core/dist/interfaces';
-import { GlobalSocketEventName } from '@growi/core/dist/interfaces';
 import expressSession from 'express-session';
 import passport from 'passport';
 import type { Namespace } from 'socket.io';
 import { Server } from 'socket.io';
-import type { Document } from 'y-socket.io/dist/server';
 
 import { SocketEventName } from '~/interfaces/websocket';
 import loggerFactory from '~/utils/logger';
@@ -15,7 +13,6 @@ import type Crowi from '../crowi';
 import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
 
 import { configManager } from './config-manager';
-import { getYjsService, extractPageIdFromYdocId } from './yjs';
 
 
 const logger = loggerFactory('growi:service:socket-io');
@@ -179,46 +176,6 @@ class SocketIoService {
     });
   }
 
-  setupYjsConnection() {
-    const yjsService = getYjsService();
-
-    this.io.on('connection', (socket) => {
-
-      yjsService.ysocketioInstance.on('awareness-update', async(doc: Document) => {
-        const pageId = extractPageIdFromYdocId(doc.name);
-
-        if (pageId == null) return;
-
-        const awarenessStateSize = doc.awareness.states.size;
-
-        // Triggered when awareness changes
-        this.io
-          .in(getRoomNameWithId(RoomPrefix.PAGE, pageId))
-          .emit(SocketEventName.YjsAwarenessStateSizeUpdated, awarenessStateSize);
-
-        // Triggered when the last user leaves the editor
-        if (awarenessStateSize === 0) {
-          const currentYdoc = yjsService.getCurrentYdoc(pageId);
-          const yjsDraft = currentYdoc?.getText('codemirror').toString();
-          const hasRevisionBodyDiff = await this.crowi.pageService.hasRevisionBodyDiff(pageId, yjsDraft);
-          this.io
-            .in(getRoomNameWithId(RoomPrefix.PAGE, pageId))
-            .emit(SocketEventName.YjsHasRevisionBodyDiffUpdated, hasRevisionBodyDiff);
-        }
-      });
-
-      socket.on(GlobalSocketEventName.YDocSync, async({ pageId, initialValue }) => {
-        try {
-          await yjsService.handleYDocSync(pageId, initialValue);
-        }
-        catch (error) {
-          logger.warn(error.message);
-          socket.emit(GlobalSocketEventName.YDocSyncError, 'An error occurred during YDoc synchronization.');
-        }
-      });
-    });
-  }
-
   async checkConnectionLimitsForAdmin(socket, next) {
     const namespaceName = socket.nsp.name;
 

+ 69 - 25
apps/app/src/server/service/yjs.ts

@@ -1,13 +1,26 @@
+import { GlobalSocketEventName } from '@growi/core';
 import type { Server } from 'socket.io';
 import { MongodbPersistence } from 'y-mongodb-provider';
+import type { Document } 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 loggerFactory from '~/utils/logger';
+
 import { getMongoUri } from '../util/mongoose-utils';
+import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
+
+import type { IPageService } from './page';
+
 
 const MONGODB_PERSISTENCE_COLLECTION_NAME = 'yjs-writings';
 const MONGODB_PERSISTENCE_FLUSH_SIZE = 100;
 
+
+const logger = loggerFactory('growi:service:yjs');
+
+
 export const extractPageIdFromYdocId = (ydocId: string): string | undefined => {
   const result = ydocId.match(/yjs\/(.*)/);
   return result?.[1];
@@ -15,37 +28,55 @@ export const extractPageIdFromYdocId = (ydocId: string): string | undefined => {
 
 class YjsService {
 
-  private static instance: YjsService;
-
   private ysocketio: YSocketIO;
 
   private mdb: MongodbPersistence;
 
-  get ysocketioInstance(): YSocketIO {
-    return this.ysocketio;
-  }
-
-  private constructor(io: Server) {
-    this.ysocketio = new YSocketIO(io);
-    this.ysocketio.initialize();
+  constructor(io: Server, pageService: IPageService) {
+    const ysocketio = new YSocketIO(io);
+    ysocketio.initialize();
+    this.ysocketio = ysocketio;
 
     this.mdb = new MongodbPersistence(getMongoUri(), {
       collectionName: MONGODB_PERSISTENCE_COLLECTION_NAME,
       flushSize: MONGODB_PERSISTENCE_FLUSH_SIZE,
     });
-  }
-
-  public static getInstance(io?: Server) {
-    if (this.instance != null) {
-      return this.instance;
-    }
-
-    if (io == null) {
-      throw new Error("'io' is required if initialize YjsService");
-    }
 
-    this.instance = new YjsService(io);
-    return this.instance;
+    io.on('connection', (socket) => {
+
+      ysocketio.on('awareness-update', async(doc: Document) => {
+        const pageId = extractPageIdFromYdocId(doc.name);
+
+        if (pageId == null) return;
+
+        const awarenessStateSize = doc.awareness.states.size;
+
+        // Triggered when awareness changes
+        io
+          .in(getRoomNameWithId(RoomPrefix.PAGE, pageId))
+          .emit(SocketEventName.YjsAwarenessStateSizeUpdated, awarenessStateSize);
+
+        // Triggered when the last user leaves the editor
+        if (awarenessStateSize === 0) {
+          const currentYdoc = this.getCurrentYdoc(pageId);
+          const yjsDraft = currentYdoc?.getText('codemirror').toString();
+          const hasRevisionBodyDiff = await pageService.hasRevisionBodyDiff(pageId, yjsDraft);
+          io
+            .in(getRoomNameWithId(RoomPrefix.PAGE, pageId))
+            .emit(SocketEventName.YjsHasRevisionBodyDiffUpdated, hasRevisionBodyDiff);
+        }
+      });
+
+      socket.on(GlobalSocketEventName.YDocSync, async({ pageId, initialValue }) => {
+        try {
+          await this.handleYDocSync(pageId, initialValue);
+        }
+        catch (error) {
+          logger.warn(error.message);
+          socket.emit(GlobalSocketEventName.YDocSyncError, 'An error occurred during YDoc synchronization.');
+        }
+      });
+    });
   }
 
   public async handleYDocSync(pageId: string, initialValue: string): Promise<void> {
@@ -110,11 +141,24 @@ class YjsService {
 
 }
 
-export const instantiateYjsService = (io: Server): YjsService => {
-  return YjsService.getInstance(io);
+let _instance: YjsService;
+
+export const initializeYjsService = (io: Server, pageService: IPageService): void => {
+  if (_instance != null) {
+    throw new Error('YjsService is already initialized');
+  }
+
+  if (io == null) {
+    throw new Error("'io' is required if initialize YjsService");
+  }
+
+  _instance = new YjsService(io, pageService);
 };
 
-// export the singleton instance
 export const getYjsService = (): YjsService => {
-  return YjsService.getInstance();
+  if (_instance == null) {
+    throw new Error('YjsService is not initialized yet');
+  }
+
+  return _instance;
 };