|
|
@@ -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;
|
|
|
};
|