Yuki Takei 1 год назад
Родитель
Сommit
a37fc07abd

+ 55 - 0
apps/app/src/server/service/yjs/create-mongodb-persistence.ts

@@ -0,0 +1,55 @@
+import type { Persistence } from 'y-socket.io/dist/server';
+import * as Y from 'yjs';
+
+import loggerFactory from '~/utils/logger';
+
+import type { MongodbPersistence } from './extended/mongodb-persistence';
+
+const logger = loggerFactory('growi:service:yjs:create-mongodb-persistence');
+
+/**
+ * Based on the example by https://github.com/MaxNoetzold/y-mongodb-provider?tab=readme-ov-file#an-other-example
+ * @param mdb
+ * @returns
+ */
+export const createMongoDBPersistence = (mdb: MongodbPersistence): Persistence => {
+  const persistece: Persistence = {
+    provider: mdb,
+    bindState: async(docName, ydoc) => {
+      logger.debug('bindState', { docName });
+
+      const persistedYdoc = await mdb.getYDoc(docName);
+
+      // get the state vector so we can just store the diffs between client and server
+      const persistedStateVector = Y.encodeStateVector(persistedYdoc);
+      const diff = Y.encodeStateAsUpdate(ydoc, persistedStateVector);
+
+      // store the new data in db (if there is any: empty update is an array of 0s)
+      if (diff.reduce((previousValue, currentValue) => previousValue + currentValue, 0) > 0) {
+        mdb.storeUpdate(docName, diff);
+        mdb.setTypedMeta(docName, 'updatedAt', Date.now());
+      }
+
+      // send the persisted data to clients
+      Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc));
+
+      // store updates of the document in db
+      ydoc.on('update', async(update) => {
+        mdb.storeUpdate(docName, update);
+        mdb.setTypedMeta(docName, 'updatedAt', Date.now());
+      });
+
+      // cleanup some memory
+      persistedYdoc.destroy();
+    },
+    writeState: async(docName) => {
+      logger.debug('writeState', { docName });
+      // This is called when all connections to the document are closed.
+
+      // flush document on close to have the smallest possible database
+      await mdb.flushDocument(docName);
+    },
+  };
+
+  return persistece;
+};

+ 19 - 0
apps/app/src/server/service/yjs/extended/mongodb-persistence.ts

@@ -0,0 +1,19 @@
+import { MongodbPersistence as Original } from 'y-mongodb-provider';
+
+export type MetadataTypesMap = {
+  updatedAt: number,
+}
+type MetadataKeys = keyof MetadataTypesMap;
+
+
+export class MongodbPersistence extends Original {
+
+  async setTypedMeta<K extends MetadataKeys>(docName: string, key: K, value: MetadataTypesMap[K]): Promise<void> {
+    return this.setMeta(docName, key, value);
+  }
+
+  async getTypedMeta<K extends MetadataKeys>(docName: string, key: K): Promise<MetadataTypesMap[K] | undefined> {
+    return await this.getMeta(docName, key) as MetadataTypesMap[K] | undefined;
+  }
+
+}

+ 1 - 0
apps/app/src/server/service/yjs/index.ts

@@ -0,0 +1 @@
+export * from './yjs';

+ 7 - 7
apps/app/src/server/service/yjs.integ.ts → apps/app/src/server/service/yjs/yjs.integ.ts

@@ -1,11 +1,11 @@
-import { YDocStatus } from '@growi/editor/dist/consts';
+import { YDocStatus } from '@growi/core/dist/consts';
 import { Types } from 'mongoose';
 import { Types } from 'mongoose';
 import type { Server } from 'socket.io';
 import type { Server } from 'socket.io';
 import { mock } from 'vitest-mock-extended';
 import { mock } from 'vitest-mock-extended';
-import type { MongodbPersistence } from 'y-mongodb-provider';
 
 
-import { Revision } from '../models/revision';
+import { Revision } from '../../models/revision';
 
 
+import type { MongodbPersistence } from './extended/mongodb-persistence';
 import type { IYjsService } from './yjs';
 import type { IYjsService } from './yjs';
 import { getYjsService, initializeYjsService } from './yjs';
 import { getYjsService, initializeYjsService } from './yjs';
 
 
@@ -67,7 +67,7 @@ describe('YjsService', () => {
       const pageId = new ObjectId();
       const pageId = new ObjectId();
 
 
       const privateMdb = getPrivateMdbInstance(yjsService);
       const privateMdb = getPrivateMdbInstance(yjsService);
-      await privateMdb.setMeta(pageId.toString(), 'updatedAt', 1000);
+      await privateMdb.setTypedMeta(pageId.toString(), 'updatedAt', 1000);
 
 
       // act
       // act
       const result = await yjsService.getYDocStatus(pageId.toString());
       const result = await yjsService.getYDocStatus(pageId.toString());
@@ -104,7 +104,7 @@ describe('YjsService', () => {
       ]);
       ]);
 
 
       const privateMdb = getPrivateMdbInstance(yjsService);
       const privateMdb = getPrivateMdbInstance(yjsService);
-      await privateMdb.setMeta(pageId.toString(), 'updatedAt', (new Date(2034, 1, 1)).getTime());
+      await privateMdb.setTypedMeta(pageId.toString(), 'updatedAt', (new Date(2034, 1, 1)).getTime());
 
 
       // act
       // act
       const result = await yjsService.getYDocStatus(pageId.toString());
       const result = await yjsService.getYDocStatus(pageId.toString());
@@ -124,7 +124,7 @@ describe('YjsService', () => {
       ]);
       ]);
 
 
       const privateMdb = getPrivateMdbInstance(yjsService);
       const privateMdb = getPrivateMdbInstance(yjsService);
-      await privateMdb.setMeta(pageId.toString(), 'updatedAt', (new Date(2025, 1, 1)).getTime());
+      await privateMdb.setTypedMeta(pageId.toString(), 'updatedAt', (new Date(2025, 1, 1)).getTime());
 
 
       // act
       // act
       const result = await yjsService.getYDocStatus(pageId.toString());
       const result = await yjsService.getYDocStatus(pageId.toString());
@@ -144,7 +144,7 @@ describe('YjsService', () => {
       ]);
       ]);
 
 
       const privateMdb = getPrivateMdbInstance(yjsService);
       const privateMdb = getPrivateMdbInstance(yjsService);
-      await privateMdb.setMeta(pageId.toString(), 'updatedAt', (new Date(2024, 1, 1)).getTime());
+      await privateMdb.setTypedMeta(pageId.toString(), 'updatedAt', (new Date(2024, 1, 1)).getTime());
 
 
       // act
       // act
       const result = await yjsService.getYDocStatus(pageId.toString());
       const result = await yjsService.getYDocStatus(pageId.toString());

+ 8 - 42
apps/app/src/server/service/yjs.ts → apps/app/src/server/service/yjs/yjs.ts

@@ -4,15 +4,17 @@ import type { IPage, IUserHasId } from '@growi/core';
 import { YDocStatus } from '@growi/core/dist/consts';
 import { YDocStatus } from '@growi/core/dist/consts';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 import type { Server } from 'socket.io';
 import type { Server } from 'socket.io';
-import { MongodbPersistence } from 'y-mongodb-provider';
-import type { Document, Persistence } 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 * as Y from 'yjs';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import type { PageModel } from '../models/page';
-import { Revision } from '../models/revision';
+import type { PageModel } from '../../models/page';
+import { Revision } from '../../models/revision';
+
+import { createMongoDBPersistence } from './create-mongodb-persistence';
+import { MongodbPersistence } from './extended/mongodb-persistence';
 
 
 
 
 const MONGODB_PERSISTENCE_COLLECTION_NAME = 'yjs-writings';
 const MONGODB_PERSISTENCE_COLLECTION_NAME = 'yjs-writings';
@@ -156,43 +158,7 @@ class YjsService implements IYjsService {
   }
   }
 
 
   private injectPersistence(ysocketio: YSocketIO, mdb: MongodbPersistence): void {
   private injectPersistence(ysocketio: YSocketIO, mdb: MongodbPersistence): void {
-    const persistece: Persistence = {
-      provider: mdb,
-      bindState: async(docName, ydoc) => {
-        logger.debug('bindState', { docName });
-
-        const persistedYdoc = await mdb.getYDoc(docName);
-
-        // get the state vector so we can just store the diffs between client and server
-        const persistedStateVector = Y.encodeStateVector(persistedYdoc);
-        const diff = Y.encodeStateAsUpdate(ydoc, persistedStateVector);
-
-        // store the new data in db (if there is any: empty update is an array of 0s)
-        if (diff.reduce((previousValue, currentValue) => previousValue + currentValue, 0) > 0) {
-          mdb.storeUpdate(docName, diff);
-          mdb.setMeta(docName, 'updatedAt', Date.now());
-        }
-
-        // send the persisted data to clients
-        Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc));
-
-        // store updates of the document in db
-        ydoc.on('update', async(update) => {
-          mdb.storeUpdate(docName, update);
-          mdb.setMeta(docName, 'updatedAt', Date.now());
-        });
-
-        // cleanup some memory
-        persistedYdoc.destroy();
-      },
-      writeState: async(docName) => {
-        logger.debug('writeState', { docName });
-        // This is called when all connections to the document are closed.
-
-        // flush document on close to have the smallest possible database
-        await mdb.flushDocument(docName);
-      },
-    };
+    const persistece = createMongoDBPersistence(mdb);
 
 
     // foce set to private property
     // foce set to private property
     // eslint-disable-next-line dot-notation
     // eslint-disable-next-line dot-notation
@@ -258,7 +224,7 @@ class YjsService implements IYjsService {
     }
     }
 
 
     // count yjs-writings documents with updatedAt > latestRevision.updatedAt
     // count yjs-writings documents with updatedAt > latestRevision.updatedAt
-    const ydocUpdatedAt: number | undefined = await this.mdb.getMeta(pageId, 'updatedAt');
+    const ydocUpdatedAt = await this.mdb.getTypedMeta(pageId, 'updatedAt');
 
 
     if (ydocUpdatedAt == null) {
     if (ydocUpdatedAt == null) {
       dumpLog(YDocStatus.NEW);
       dumpLog(YDocStatus.NEW);