diff --git a/apps/server/src/modules/tldraw/controller/api-test/tldraw.ws.api.spec.ts b/apps/server/src/modules/tldraw/controller/api-test/tldraw.ws.api.spec.ts index 78e5d9b0163..ea54077f815 100644 --- a/apps/server/src/modules/tldraw/controller/api-test/tldraw.ws.api.spec.ts +++ b/apps/server/src/modules/tldraw/controller/api-test/tldraw.ws.api.spec.ts @@ -5,13 +5,13 @@ import { TextEncoder } from 'util'; import { INestApplication, NotAcceptableException } from '@nestjs/common'; import { MongoMemoryDatabaseModule } from '@infra/database'; import { createConfigModuleOptions } from '@src/config'; -import { Logger } from '@src/core/logger'; import { of, throwError } from 'rxjs'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { HttpService } from '@nestjs/axios'; import { AxiosError, AxiosHeaders, AxiosResponse } from 'axios'; import { axiosResponseFactory } from '@shared/testing'; +import { CoreModule } from '@src/core'; import { TldrawRedisFactory, TldrawRedisService } from '../../redis'; import { TldrawDrawing } from '../../entities'; import { TldrawWsService } from '../../service'; @@ -22,6 +22,7 @@ import { TldrawWs } from '..'; import { WsCloseCode, WsCloseMessage } from '../../types'; import { TldrawConfig } from '../../config'; +// This is a unit test, no api test...need to be refactored describe('WebSocketController (WsAdapter)', () => { let app: INestApplication; let gateway: TldrawWs; @@ -41,6 +42,7 @@ describe('WebSocketController (WsAdapter)', () => { imports: [ MongoMemoryDatabaseModule.forRoot({ entities: [TldrawDrawing] }), ConfigModule.forRoot(createConfigModuleOptions(tldrawTestConfig)), + CoreModule, ], providers: [ TldrawWs, @@ -54,10 +56,6 @@ describe('WebSocketController (WsAdapter)', () => { provide: TldrawRepo, useValue: createMock(), }, - { - provide: Logger, - useValue: createMock(), - }, { provide: HttpService, useValue: createMock(), @@ -79,7 +77,7 @@ describe('WebSocketController (WsAdapter)', () => { }); afterEach(() => { - jest.clearAllMocks(); + jest.restoreAllMocks(); }); describe('when tldraw connection is established', () => { diff --git a/apps/server/src/modules/tldraw/controller/tldraw.ws.ts b/apps/server/src/modules/tldraw/controller/tldraw.ws.ts index feefed9127f..0c050ee9677 100644 --- a/apps/server/src/modules/tldraw/controller/tldraw.ws.ts +++ b/apps/server/src/modules/tldraw/controller/tldraw.ws.ts @@ -9,10 +9,10 @@ import { NotFoundException, NotAcceptableException, } from '@nestjs/common'; -import { Logger } from '@src/core/logger'; import { isAxiosError } from 'axios'; import { firstValueFrom } from 'rxjs'; import { HttpService } from '@nestjs/axios'; +import { DomainErrorHandler } from '@src/core'; import { WebsocketInitErrorLoggable } from '../loggable'; import { TldrawConfig, TLDRAW_SOCKET_PORT } from '../config'; import { WsCloseCode, WsCloseMessage } from '../types'; @@ -27,7 +27,7 @@ export class TldrawWs implements OnGatewayInit, OnGatewayConnection { private readonly configService: ConfigService, private readonly tldrawWsService: TldrawWsService, private readonly httpService: HttpService, - private readonly logger: Logger + private readonly domainErrorHandler: DomainErrorHandler ) {} public async handleConnection(client: WebSocket, request: Request): Promise { @@ -106,7 +106,7 @@ export class TldrawWs implements OnGatewayInit, OnGatewayConnection { err?: unknown ): void { client.close(code, message); - this.logger.warning(new WebsocketInitErrorLoggable(code, message, docName, err)); + this.domainErrorHandler.exec(new WebsocketInitErrorLoggable(code, message, docName, err)); } private handleError(err: unknown, client: WebSocket, docName: string): void { diff --git a/apps/server/src/modules/tldraw/redis/tldraw-redis.factory.spec.ts b/apps/server/src/modules/tldraw/redis/tldraw-redis.factory.spec.ts index c24fec60514..7353e44b11a 100644 --- a/apps/server/src/modules/tldraw/redis/tldraw-redis.factory.spec.ts +++ b/apps/server/src/modules/tldraw/redis/tldraw-redis.factory.spec.ts @@ -1,17 +1,14 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; import { createConfigModuleOptions } from '@src/config'; -import { INestApplication } from '@nestjs/common'; -import { WsAdapter } from '@nestjs/platform-ws'; import { createMock } from '@golevelup/ts-jest'; -import { Logger } from '@src/core/logger'; +import { DomainErrorHandler } from '@src/core'; import { RedisConnectionTypeEnum } from '../types'; import { TldrawConfig } from '../config'; import { tldrawTestConfig } from '../testing'; import { TldrawRedisFactory } from './tldraw-redis.factory'; describe('TldrawRedisFactory', () => { - let app: INestApplication; let configService: ConfigService; let redisFactory: TldrawRedisFactory; @@ -21,21 +18,14 @@ describe('TldrawRedisFactory', () => { providers: [ TldrawRedisFactory, { - provide: Logger, - useValue: createMock(), + provide: DomainErrorHandler, + useValue: createMock(), }, ], }).compile(); configService = testingModule.get(ConfigService); redisFactory = testingModule.get(TldrawRedisFactory); - app = testingModule.createNestApplication(); - app.useWebSocketAdapter(new WsAdapter(app)); - await app.init(); - }); - - afterAll(async () => { - await app.close(); }); it('should check if factory was created', () => { diff --git a/apps/server/src/modules/tldraw/redis/tldraw-redis.factory.ts b/apps/server/src/modules/tldraw/redis/tldraw-redis.factory.ts index b71a6b401f8..e84f9e040b1 100644 --- a/apps/server/src/modules/tldraw/redis/tldraw-redis.factory.ts +++ b/apps/server/src/modules/tldraw/redis/tldraw-redis.factory.ts @@ -1,16 +1,17 @@ import { Redis } from 'ioredis'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Logger } from '@src/core/logger'; +import { DomainErrorHandler } from '@src/core'; import { TldrawConfig } from '../config'; import { RedisErrorLoggable } from '../loggable'; import { RedisConnectionTypeEnum } from '../types'; @Injectable() export class TldrawRedisFactory { - constructor(private readonly configService: ConfigService, private readonly logger: Logger) { - this.logger.setContext(TldrawRedisFactory.name); - } + constructor( + private readonly configService: ConfigService, + private readonly domainErrorHandler: DomainErrorHandler + ) {} public build(connectionType: RedisConnectionTypeEnum) { const redisUri = this.configService.get('REDIS_URI'); @@ -22,7 +23,7 @@ export class TldrawRedisFactory { maxRetriesPerRequest: null, }); - redis.on('error', (err) => this.logger.warning(new RedisErrorLoggable(connectionType, err))); + redis.on('error', (err) => this.domainErrorHandler.exec(new RedisErrorLoggable(connectionType, err))); return redis; } diff --git a/apps/server/src/modules/tldraw/redis/tldraw-redis.service.spec.ts b/apps/server/src/modules/tldraw/redis/tldraw-redis.service.spec.ts index 79c15dc2854..11473385f3c 100644 --- a/apps/server/src/modules/tldraw/redis/tldraw-redis.service.spec.ts +++ b/apps/server/src/modules/tldraw/redis/tldraw-redis.service.spec.ts @@ -1,17 +1,10 @@ -import { INestApplication } from '@nestjs/common'; import { createMock } from '@golevelup/ts-jest'; -import { Logger } from '@src/core/logger'; import { Test } from '@nestjs/testing'; import { ConfigModule } from '@nestjs/config'; import { createConfigModuleOptions } from '@src/config'; import * as Yjs from 'yjs'; import * as AwarenessProtocol from 'y-protocols/awareness'; -import { HttpService } from '@nestjs/axios'; -import { WsAdapter } from '@nestjs/platform-ws'; -import { TldrawWs } from '../controller'; -import { TldrawWsService } from '../service'; -import { TldrawBoardRepo, TldrawRepo, YMongodb } from '../repo'; -import { MetricsService } from '../metrics'; +import { DomainErrorHandler } from '@src/core'; import { WsSharedDocDo } from '../domain'; import { TldrawRedisFactory, TldrawRedisService } from '.'; import { tldrawTestConfig } from '../testing'; @@ -30,60 +23,28 @@ jest.mock('y-protocols/awareness', () => { }; return moduleMock; }); -jest.mock('y-protocols/sync', () => { - const moduleMock: unknown = { - __esModule: true, - ...jest.requireActual('y-protocols/sync'), - }; - return moduleMock; -}); describe('TldrawRedisService', () => { - let app: INestApplication; let service: TldrawRedisService; beforeAll(async () => { const testingModule = await Test.createTestingModule({ imports: [ConfigModule.forRoot(createConfigModuleOptions(tldrawTestConfig))], providers: [ - TldrawWs, - TldrawWsService, - YMongodb, - MetricsService, TldrawRedisFactory, TldrawRedisService, { - provide: TldrawBoardRepo, - useValue: createMock(), - }, - { - provide: TldrawRepo, - useValue: createMock(), - }, - { - provide: Logger, - useValue: createMock(), - }, - { - provide: HttpService, - useValue: createMock(), + provide: DomainErrorHandler, + useValue: createMock(), }, ], }).compile(); service = testingModule.get(TldrawRedisService); - app = testingModule.createNestApplication(); - app.useWebSocketAdapter(new WsAdapter(app)); - await app.init(); - }); - - afterAll(async () => { - await app.close(); }); afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); + jest.resetAllMocks(); }); describe('redisMessageHandler', () => { diff --git a/apps/server/src/modules/tldraw/redis/tldraw-redis.service.ts b/apps/server/src/modules/tldraw/redis/tldraw-redis.service.ts index 59b2a277bee..77a14243524 100644 --- a/apps/server/src/modules/tldraw/redis/tldraw-redis.service.ts +++ b/apps/server/src/modules/tldraw/redis/tldraw-redis.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; import { Redis } from 'ioredis'; -import { Logger } from '@src/core/logger'; import { Buffer } from 'node:buffer'; import { applyAwarenessUpdate } from 'y-protocols/awareness'; import { applyUpdate } from 'yjs'; +import { DomainErrorHandler } from '@src/core'; import { WsSharedDocDo } from '../domain'; import { RedisConnectionTypeEnum, UpdateOrigin, UpdateType } from '../types'; import { RedisPublishErrorLoggable, WsSharedDocErrorLoggable } from '../loggable'; @@ -15,9 +15,10 @@ export class TldrawRedisService { private readonly pub: Redis; - constructor(private readonly logger: Logger, private readonly tldrawRedisFactory: TldrawRedisFactory) { - this.logger.setContext(TldrawRedisService.name); - + constructor( + private readonly domainErrorHandler: DomainErrorHandler, + private readonly tldrawRedisFactory: TldrawRedisFactory + ) { this.sub = this.tldrawRedisFactory.build(RedisConnectionTypeEnum.SUBSCRIBE); this.pub = this.tldrawRedisFactory.build(RedisConnectionTypeEnum.PUBLISH); } @@ -32,20 +33,24 @@ export class TldrawRedisService { public subscribeToRedisChannels(doc: WsSharedDocDo) { this.sub.subscribe(doc.name, doc.awarenessChannel).catch((err) => { - this.logger.warning(new WsSharedDocErrorLoggable(doc.name, 'Error while subscribing to Redis channels', err)); + this.domainErrorHandler.exec( + new WsSharedDocErrorLoggable(doc.name, 'Error while subscribing to Redis channels', err) + ); }); } public unsubscribeFromRedisChannels(doc: WsSharedDocDo) { this.sub.unsubscribe(doc.name, doc.awarenessChannel).catch((err) => { - this.logger.warning(new WsSharedDocErrorLoggable(doc.name, 'Error while unsubscribing from Redis channels', err)); + this.domainErrorHandler.exec( + new WsSharedDocErrorLoggable(doc.name, 'Error while unsubscribing from Redis channels', err) + ); }); } public publishUpdateToRedis(doc: WsSharedDocDo, update: Uint8Array, type: UpdateType) { const channel = type === UpdateType.AWARENESS ? doc.awarenessChannel : doc.name; this.pub.publish(channel, Buffer.from(update)).catch((err) => { - this.logger.warning(new RedisPublishErrorLoggable(type, err)); + this.domainErrorHandler.exec(new RedisPublishErrorLoggable(type, err)); }); } } diff --git a/apps/server/src/modules/tldraw/repo/tldraw-board.repo.spec.ts b/apps/server/src/modules/tldraw/repo/tldraw-board.repo.spec.ts index ab6c81d117f..aca97319dc4 100644 --- a/apps/server/src/modules/tldraw/repo/tldraw-board.repo.spec.ts +++ b/apps/server/src/modules/tldraw/repo/tldraw-board.repo.spec.ts @@ -1,26 +1,17 @@ import { Test } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import { WsAdapter } from '@nestjs/platform-ws'; import { Doc } from 'yjs'; import { createMock } from '@golevelup/ts-jest'; -import { HttpService } from '@nestjs/axios'; import { Logger } from '@src/core/logger'; import { ConfigModule } from '@nestjs/config'; import { MongoMemoryDatabaseModule } from '@infra/database'; import { createConfigModuleOptions } from '@src/config'; import { TldrawBoardRepo } from './tldraw-board.repo'; import { WsSharedDocDo } from '../domain'; -import { TldrawWsService } from '../service'; import { tldrawTestConfig } from '../testing'; import { TldrawDrawing } from '../entities'; -import { TldrawWs } from '../controller'; -import { MetricsService } from '../metrics'; -import { TldrawRepo } from './tldraw.repo'; import { YMongodb } from './y-mongodb'; -import { TldrawRedisFactory, TldrawRedisService } from '../redis'; describe('TldrawBoardRepo', () => { - let app: INestApplication; let repo: TldrawBoardRepo; beforeAll(async () => { @@ -30,32 +21,19 @@ describe('TldrawBoardRepo', () => { ConfigModule.forRoot(createConfigModuleOptions(tldrawTestConfig)), ], providers: [ - TldrawWs, - TldrawWsService, TldrawBoardRepo, - YMongodb, - MetricsService, - TldrawRedisFactory, - TldrawRedisService, { - provide: TldrawRepo, - useValue: createMock(), + provide: YMongodb, + useValue: createMock(), }, { provide: Logger, useValue: createMock(), }, - { - provide: HttpService, - useValue: createMock(), - }, ], }).compile(); repo = testingModule.get(TldrawBoardRepo); - app = testingModule.createNestApplication(); - app.useWebSocketAdapter(new WsAdapter(app)); - await app.init(); jest.useFakeTimers(); }); @@ -64,10 +42,6 @@ describe('TldrawBoardRepo', () => { jest.resetAllMocks(); }); - afterAll(async () => { - await app.close(); - }); - it('should check if repo and its properties are set correctly', () => { expect(repo).toBeDefined(); expect(repo.mdb).toBeDefined(); diff --git a/apps/server/src/modules/tldraw/repo/tldraw-board.repo.ts b/apps/server/src/modules/tldraw/repo/tldraw-board.repo.ts index 7d3887feb68..8ca1b2d02b8 100644 --- a/apps/server/src/modules/tldraw/repo/tldraw-board.repo.ts +++ b/apps/server/src/modules/tldraw/repo/tldraw-board.repo.ts @@ -20,6 +20,7 @@ export class TldrawBoardRepo { } public async getDocumentFromDb(docName: string): Promise { + // can be return null, return type of functions need to be improve const yDoc = await this.mdb.getDocument(docName); return yDoc; } diff --git a/apps/server/src/modules/tldraw/repo/tldraw.repo.spec.ts b/apps/server/src/modules/tldraw/repo/tldraw.repo.spec.ts index 86c0ce7345a..9e12d64d782 100644 --- a/apps/server/src/modules/tldraw/repo/tldraw.repo.spec.ts +++ b/apps/server/src/modules/tldraw/repo/tldraw.repo.spec.ts @@ -2,12 +2,10 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { cleanupCollections } from '@shared/testing'; import { MikroORM } from '@mikro-orm/core'; -import { ConfigModule } from '@nestjs/config'; -import { createConfigModuleOptions } from '@src/config'; -import { tldrawEntityFactory, tldrawTestConfig } from '../testing'; +import { MongoMemoryDatabaseModule } from '@src/infra/database'; +import { tldrawEntityFactory } from '../testing'; import { TldrawDrawing } from '../entities'; import { TldrawRepo } from './tldraw.repo'; -import { TldrawWsTestModule } from '../tldraw-ws-test.module'; describe('TldrawRepo', () => { let testingModule: TestingModule; @@ -17,7 +15,8 @@ describe('TldrawRepo', () => { beforeAll(async () => { testingModule = await Test.createTestingModule({ - imports: [TldrawWsTestModule, ConfigModule.forRoot(createConfigModuleOptions(tldrawTestConfig))], + imports: [MongoMemoryDatabaseModule.forRoot({ entities: [TldrawDrawing] })], + providers: [TldrawRepo], }).compile(); repo = testingModule.get(TldrawRepo); diff --git a/apps/server/src/modules/tldraw/repo/y-mongodb.spec.ts b/apps/server/src/modules/tldraw/repo/y-mongodb.spec.ts index cf901cbf565..a3a2ae88677 100644 --- a/apps/server/src/modules/tldraw/repo/y-mongodb.spec.ts +++ b/apps/server/src/modules/tldraw/repo/y-mongodb.spec.ts @@ -3,20 +3,15 @@ import { Test, TestingModule } from '@nestjs/testing'; import { cleanupCollections } from '@shared/testing'; import { MongoMemoryDatabaseModule } from '@infra/database'; import { ConfigModule } from '@nestjs/config'; -import { Logger } from '@src/core/logger'; import { createMock } from '@golevelup/ts-jest'; import * as Yjs from 'yjs'; import { createConfigModuleOptions } from '@src/config'; -import { HttpService } from '@nestjs/axios'; -import { TldrawRedisFactory, TldrawRedisService } from '../redis'; +import { DomainErrorHandler } from '@src/core'; import { tldrawEntityFactory, tldrawTestConfig } from '../testing'; import { TldrawDrawing } from '../entities'; -import { TldrawWs } from '../controller'; -import { TldrawWsService } from '../service'; -import { MetricsService } from '../metrics'; -import { TldrawBoardRepo } from './tldraw-board.repo'; import { TldrawRepo } from './tldraw.repo'; import { YMongodb } from './y-mongodb'; +import { Version } from './key.factory'; jest.mock('yjs', () => { const moduleMock: unknown = { @@ -39,21 +34,11 @@ describe('YMongoDb', () => { ConfigModule.forRoot(createConfigModuleOptions(tldrawTestConfig)), ], providers: [ - TldrawWs, - TldrawWsService, - TldrawBoardRepo, - TldrawRepo, YMongodb, - MetricsService, - TldrawRedisFactory, - TldrawRedisService, - { - provide: Logger, - useValue: createMock(), - }, + TldrawRepo, { - provide: HttpService, - useValue: createMock(), + provide: DomainErrorHandler, + useValue: createMock(), }, ], }).compile(); @@ -168,20 +153,24 @@ describe('YMongoDb', () => { describe('getAllDocumentNames', () => { const setup = async () => { - const drawing1 = tldrawEntityFactory.build({ docName: 'test-name1', version: 'v1_sv' }); - const drawing2 = tldrawEntityFactory.build({ docName: 'test-name2', version: 'v1_sv' }); - const drawing3 = tldrawEntityFactory.build({ docName: 'test-name3', version: 'v1_sv' }); + const drawing1 = tldrawEntityFactory.build({ docName: 'test-name1', version: Version.V1_SV }); + const drawing2 = tldrawEntityFactory.build({ docName: 'test-name2', version: Version.V1_SV }); + const drawing3 = tldrawEntityFactory.build({ docName: 'test-name3', version: Version.V1_SV }); await em.persistAndFlush([drawing1, drawing2, drawing3]); em.clear(); + + return { + expectedDocNames: [drawing1.docName, drawing2.docName, drawing3.docName], + }; }; it('should return all document names', async () => { - await setup(); + const { expectedDocNames } = await setup(); const docNames = await mdb.getAllDocumentNames(); - expect(docNames).toEqual(['test-name1', 'test-name2', 'test-name3']); + expect(docNames).toEqual(expectedDocNames); }); }); @@ -238,7 +227,7 @@ describe('YMongoDb', () => { const doc = await mdb.getDocument('test-name'); - expect(doc).toBeUndefined(); + expect(doc).toBeNull(); applyUpdateSpy.mockRestore(); }); }); diff --git a/apps/server/src/modules/tldraw/repo/y-mongodb.ts b/apps/server/src/modules/tldraw/repo/y-mongodb.ts index 0fcca950f41..1ff357bba1c 100644 --- a/apps/server/src/modules/tldraw/repo/y-mongodb.ts +++ b/apps/server/src/modules/tldraw/repo/y-mongodb.ts @@ -1,12 +1,12 @@ import { BulkWriteResult } from '@mikro-orm/mongodb/node_modules/mongodb'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Logger } from '@src/core/logger'; import { Buffer } from 'buffer'; import * as binary from 'lib0/binary'; import * as encoding from 'lib0/encoding'; import * as promise from 'lib0/promise'; import { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector, mergeUpdates } from 'yjs'; +import { DomainErrorHandler } from '@src/core'; import { TldrawConfig } from '../config'; import { WsSharedDocDo } from '../domain'; import { TldrawDrawing } from '../entities'; @@ -26,10 +26,8 @@ export class YMongodb { constructor( private readonly configService: ConfigService, private readonly repo: TldrawRepo, - private readonly logger: Logger + private readonly domainErrorHandler: DomainErrorHandler ) { - this.logger.setContext(YMongodb.name); - // execute a transaction on a database // this will ensure that other processes are currently not writing this._transact = >(docName: string, fn: () => T): T => { @@ -43,11 +41,11 @@ export class YMongodb { nextTr = (async () => { await currTr; - let res: YTransaction | null; + let res: YTransaction | null = null; try { res = await fn(); } catch (err) { - this.logger.warning(new MongoTransactionErrorLoggable(err)); + this.domainErrorHandler.exec(new MongoTransactionErrorLoggable(err)); } // once the last transaction for a given docName resolves, remove it from the queue @@ -76,6 +74,7 @@ export class YMongodb { } public getDocument(docName: string): Promise { + // return value can be null, need to be defined return this._transact(docName, async (): Promise => { const updates = await this.getMongoUpdates(docName); const mergedUpdates = mergeUpdates(updates); @@ -89,10 +88,13 @@ export class YMongodb { } public storeUpdateTransactional(docName: string, update: Uint8Array): Promise { + // return value can be null, need to be defined return this._transact(docName, () => this.storeUpdate(docName, update)); } + // return value is not void, need to be changed public compressDocumentTransactional(docName: string): Promise { + // return value can be null, need to be defined return this._transact(docName, async () => { const updates = await this.getMongoUpdates(docName); const mergedUpdates = mergeUpdates(updates); diff --git a/apps/server/src/modules/tldraw/service/tldraw.ws.service.spec.ts b/apps/server/src/modules/tldraw/service/tldraw.ws.service.spec.ts index 3fada8c0c29..50b19dca282 100644 --- a/apps/server/src/modules/tldraw/service/tldraw.ws.service.spec.ts +++ b/apps/server/src/modules/tldraw/service/tldraw.ws.service.spec.ts @@ -11,11 +11,11 @@ import { encoding } from 'lib0'; import { TldrawWsFactory } from '@shared/testing/factory/tldraw.ws.factory'; import { HttpService } from '@nestjs/axios'; import { WebSocketReadyStateEnum } from '@shared/testing'; -import { Logger } from '@src/core/logger'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ConfigModule } from '@nestjs/config'; import { createConfigModuleOptions } from '@src/config'; import { MongoMemoryDatabaseModule } from '@infra/database'; +import { DomainErrorHandler } from '@src/core'; import { TldrawRedisFactory, TldrawRedisService } from '../redis'; import { TldrawWs } from '../controller'; import { TldrawDrawing } from '../entities'; @@ -47,12 +47,26 @@ jest.mock('y-protocols/sync', () => { return moduleMock; }); +const createMessage = (values: number[]) => { + const encoder = encoding.createEncoder(); + values.forEach((val) => { + encoding.writeVarUint(encoder, val); + }); + encoding.writeVarUint(encoder, 0); + encoding.writeVarUint(encoder, 1); + const msg = encoding.toUint8Array(encoder); + + return { + msg, + }; +}; + describe('TldrawWSService', () => { let app: INestApplication; - let ws: WebSocket; + let wsGlobal: WebSocket; let service: TldrawWsService; let boardRepo: DeepMocked; - let logger: DeepMocked; + // let domainErrorHandler: DeepMocked; const gatewayPort = 3346; const wsUrl = TestConnection.getWsUrl(gatewayPort); @@ -84,8 +98,8 @@ describe('TldrawWSService', () => { useValue: createMock(), }, { - provide: Logger, - useValue: createMock(), + provide: DomainErrorHandler, + useValue: createMock(), }, { provide: HttpService, @@ -96,7 +110,7 @@ describe('TldrawWSService', () => { service = testingModule.get(TldrawWsService); boardRepo = testingModule.get(TldrawBoardRepo); - logger = testingModule.get(Logger); + // domainErrorHandler = testingModule.get(DomainErrorHandler); app = testingModule.createNestApplication(); app.useWebSocketAdapter(new WsAdapter(app)); await app.init(); @@ -107,57 +121,33 @@ describe('TldrawWSService', () => { }); afterEach(() => { - jest.clearAllMocks(); jest.restoreAllMocks(); - }); - - const createMessage = (values: number[]) => { - const encoder = encoding.createEncoder(); - values.forEach((val) => { - encoding.writeVarUint(encoder, val); - }); - encoding.writeVarUint(encoder, 0); - encoding.writeVarUint(encoder, 1); - const msg = encoding.toUint8Array(encoder); - - return { - msg, - }; - }; - - it('should check if service properties are set correctly', () => { - expect(service).toBeDefined(); + jest.clearAllMocks(); }); describe('send', () => { describe('when client is not connected to WS', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl, 'TEST'); - const clientMessageMock = 'test-message'; - - const closeConSpy = jest.spyOn(service, 'closeConnection').mockResolvedValueOnce(); - const sendSpy = jest.spyOn(service, 'send'); + const ws = await TestConnection.setupWs(wsUrl, 'TEST'); + const closeConMock = jest.spyOn(service, 'closeConnection').mockResolvedValueOnce(); const doc = TldrawWsFactory.createWsSharedDocDo(); - const byteArray = new TextEncoder().encode(clientMessageMock); + const byteArray = new TextEncoder().encode('test-message'); return { - closeConSpy, - sendSpy, + closeConMock, doc, byteArray, + ws, }; }; it('should throw error for send message', async () => { - const { closeConSpy, sendSpy, doc, byteArray } = await setup(); + const { closeConMock, doc, byteArray, ws } = await setup(); service.send(doc, ws, byteArray); - expect(sendSpy).toThrow(); - expect(sendSpy).toHaveBeenCalledWith(doc, ws, byteArray); - expect(closeConSpy).toHaveBeenCalled(); + expect(closeConMock).toHaveBeenCalled(); ws.close(); - sendSpy.mockRestore(); }); }); @@ -166,77 +156,59 @@ describe('TldrawWSService', () => { const socketMock = TldrawWsFactory.createWebsocket(WebSocketReadyStateEnum.OPEN); const clientMessageMock = 'test-message'; - const closeConSpy = jest.spyOn(service, 'closeConnection').mockRejectedValue(new Error('error')); - jest.spyOn(socketMock, 'send').mockImplementation((...args: unknown[]) => { + jest.spyOn(service, 'closeConnection').mockRejectedValueOnce(new Error('error')); + jest.spyOn(socketMock, 'send').mockImplementationOnce((...args: unknown[]) => { args.forEach((arg) => { if (typeof arg === 'function') { arg(new Error('error')); } }); }); - const sendSpy = jest.spyOn(service, 'send'); - const errorLogSpy = jest.spyOn(logger, 'warning'); + const doc = TldrawWsFactory.createWsSharedDocDo(); const byteArray = new TextEncoder().encode(clientMessageMock); return { socketMock, - closeConSpy, - errorLogSpy, - sendSpy, doc, byteArray, }; }; - it('should log error', async () => { - const { socketMock, closeConSpy, errorLogSpy, sendSpy, doc, byteArray } = setup(); + it('should log error', () => { + const { socketMock, doc, byteArray } = setup(); - service.send(doc, socketMock, byteArray); + const result = service.send(doc, socketMock, byteArray); - await delay(100); + // await delay(100); - expect(sendSpy).toHaveBeenCalledWith(doc, socketMock, byteArray); - expect(closeConSpy).toHaveBeenCalled(); - expect(errorLogSpy).toHaveBeenCalled(); - closeConSpy.mockRestore(); - sendSpy.mockRestore(); + expect(result).toBeUndefined(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(3); }); }); describe('when web socket has ready state CLOSED and close connection throws error', () => { const setup = () => { const socketMock = TldrawWsFactory.createWebsocket(WebSocketReadyStateEnum.CLOSED); - const clientMessageMock = 'test-message'; - const closeConSpy = jest.spyOn(service, 'closeConnection').mockRejectedValue(new Error('error')); - const sendSpy = jest.spyOn(service, 'send'); - const errorLogSpy = jest.spyOn(logger, 'warning'); + const closeConMock = jest.spyOn(service, 'closeConnection').mockRejectedValueOnce(new Error('error')); const doc = TldrawWsFactory.createWsSharedDocDo(); - const byteArray = new TextEncoder().encode(clientMessageMock); + const byteArray = new TextEncoder().encode('test-message'); return { socketMock, - closeConSpy, - errorLogSpy, - sendSpy, + closeConMock, doc, byteArray, }; }; - it('should log error', async () => { - const { socketMock, closeConSpy, errorLogSpy, sendSpy, doc, byteArray } = setup(); + it('should log error', () => { + const { socketMock, closeConMock, doc, byteArray } = setup(); service.send(doc, socketMock, byteArray); - await delay(100); - - expect(sendSpy).toHaveBeenCalledWith(doc, socketMock, byteArray); - expect(closeConSpy).toHaveBeenCalled(); - expect(errorLogSpy).toHaveBeenCalled(); - closeConSpy.mockRestore(); - sendSpy.mockRestore(); + expect(closeConMock).toHaveBeenCalled(); }); }); @@ -246,7 +218,7 @@ describe('TldrawWSService', () => { const closeConSpy = jest.spyOn(service, 'closeConnection'); const sendSpy = jest.spyOn(service, 'send'); const doc = TldrawWsFactory.createWsSharedDocDo(); - const socketMock = TldrawWsFactory.createWebsocket(WebSocketReadyStateEnum.CLOSED); + const socketMock = TldrawWsFactory.createWebsocket(WebSocketReadyStateEnum.OPEN); const byteArray = new TextEncoder().encode(clientMessageMock); return { @@ -265,7 +237,6 @@ describe('TldrawWSService', () => { expect(sendSpy).toHaveBeenCalledWith(doc, socketMock, byteArray); expect(sendSpy).toHaveBeenCalledTimes(1); - expect(closeConSpy).toHaveBeenCalled(); closeConSpy.mockRestore(); sendSpy.mockRestore(); }); @@ -273,7 +244,7 @@ describe('TldrawWSService', () => { describe('when websocket has ready state Open (0)', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl, 'TEST'); + wsGlobal = await TestConnection.setupWs(wsUrl, 'TEST'); const clientMessageMock = 'test-message'; const sendSpy = jest.spyOn(service, 'send'); @@ -301,16 +272,15 @@ describe('TldrawWSService', () => { service.updateHandler(msg, socketMock, doc); expect(sendSpy).toHaveBeenCalled(); - ws.close(); + wsGlobal.close(); sendSpy.mockRestore(); }); }); describe('when received message of type specific type', () => { const setup = async (messageValues: number[]) => { - ws = await TestConnection.setupWs(wsUrl, 'TEST'); + wsGlobal = await TestConnection.setupWs(wsUrl, 'TEST'); - const errorLogSpy = jest.spyOn(logger, 'warning'); const publishSpy = jest.spyOn(Ioredis.Redis.prototype, 'publish'); const sendSpy = jest.spyOn(service, 'send'); const applyAwarenessUpdateSpy = jest.spyOn(AwarenessProtocol, 'applyAwarenessUpdate'); @@ -325,7 +295,6 @@ describe('TldrawWSService', () => { return { sendSpy, - errorLogSpy, publishSpy, applyAwarenessUpdateSpy, syncProtocolUpdateSpy, @@ -337,10 +306,10 @@ describe('TldrawWSService', () => { it('should call send method when received message of type SYNC', async () => { const { sendSpy, applyAwarenessUpdateSpy, syncProtocolUpdateSpy, doc, msg } = await setup([0, 1]); - service.messageHandler(ws, doc, msg); + service.messageHandler(wsGlobal, doc, msg); expect(sendSpy).toHaveBeenCalledTimes(1); - ws.close(); + wsGlobal.close(); sendSpy.mockRestore(); applyAwarenessUpdateSpy.mockRestore(); syncProtocolUpdateSpy.mockRestore(); @@ -349,10 +318,10 @@ describe('TldrawWSService', () => { it('should not call send method when received message of type AWARENESS', async () => { const { sendSpy, applyAwarenessUpdateSpy, syncProtocolUpdateSpy, doc, msg } = await setup([1, 1, 0]); - service.messageHandler(ws, doc, msg); + service.messageHandler(wsGlobal, doc, msg); expect(sendSpy).not.toHaveBeenCalled(); - ws.close(); + wsGlobal.close(); sendSpy.mockRestore(); applyAwarenessUpdateSpy.mockRestore(); syncProtocolUpdateSpy.mockRestore(); @@ -361,11 +330,11 @@ describe('TldrawWSService', () => { it('should do nothing when received message unknown type', async () => { const { sendSpy, applyAwarenessUpdateSpy, syncProtocolUpdateSpy, doc, msg } = await setup([2]); - service.messageHandler(ws, doc, msg); + service.messageHandler(wsGlobal, doc, msg); expect(sendSpy).toHaveBeenCalledTimes(0); expect(applyAwarenessUpdateSpy).toHaveBeenCalledTimes(0); - ws.close(); + wsGlobal.close(); sendSpy.mockRestore(); applyAwarenessUpdateSpy.mockRestore(); syncProtocolUpdateSpy.mockRestore(); @@ -374,9 +343,8 @@ describe('TldrawWSService', () => { describe('when publishing AWARENESS has errors', () => { const setup = async (messageValues: number[]) => { - ws = await TestConnection.setupWs(wsUrl, 'TEST'); + wsGlobal = await TestConnection.setupWs(wsUrl, 'TEST'); - const errorLogSpy = jest.spyOn(logger, 'warning'); const publishSpy = jest .spyOn(Ioredis.Redis.prototype, 'publish') .mockImplementationOnce((_channel, _message, cb) => { @@ -398,7 +366,6 @@ describe('TldrawWSService', () => { return { sendSpy, - errorLogSpy, publishSpy, applyAwarenessUpdateSpy, syncProtocolUpdateSpy, @@ -408,14 +375,15 @@ describe('TldrawWSService', () => { }; it('should log error', async () => { - const { publishSpy, errorLogSpy, sendSpy, applyAwarenessUpdateSpy, syncProtocolUpdateSpy, doc, msg } = - await setup([1, 1, 0]); + const { publishSpy, sendSpy, applyAwarenessUpdateSpy, syncProtocolUpdateSpy, doc, msg } = await setup([ + 1, 1, 0, + ]); - service.messageHandler(ws, doc, msg); + service.messageHandler(wsGlobal, doc, msg); expect(sendSpy).not.toHaveBeenCalled(); - expect(errorLogSpy).toHaveBeenCalled(); - ws.close(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(1); + wsGlobal.close(); sendSpy.mockRestore(); applyAwarenessUpdateSpy.mockRestore(); syncProtocolUpdateSpy.mockRestore(); @@ -425,7 +393,7 @@ describe('TldrawWSService', () => { describe('when error is thrown during receiving message', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl); + wsGlobal = await TestConnection.setupWs(wsUrl); const sendSpy = jest.spyOn(service, 'send'); jest.spyOn(SyncProtocols, 'readSyncMessage').mockImplementationOnce(() => { @@ -444,17 +412,17 @@ describe('TldrawWSService', () => { it('should not call send method', async () => { const { sendSpy, doc, msg } = await setup(); - expect(() => service.messageHandler(ws, doc, msg)).toThrow('error'); + expect(() => service.messageHandler(wsGlobal, doc, msg)).toThrow('error'); expect(sendSpy).toHaveBeenCalledTimes(0); - ws.close(); + wsGlobal.close(); sendSpy.mockRestore(); }); }); describe('when awareness states (clients) size is greater then one', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl, 'TEST'); + wsGlobal = await TestConnection.setupWs(wsUrl, 'TEST'); const doc = new WsSharedDocDo('TEST'); doc.awareness.states = new Map(); @@ -479,14 +447,11 @@ describe('TldrawWSService', () => { it('should send to every client', async () => { const { messageHandlerSpy, sendSpy, getYDocSpy, closeConnSpy } = await setup(); - await service.setupWsConnection(ws, 'TEST'); - await delay(20); - ws.emit('pong'); - - await delay(20); + await expect(service.setupWsConnection(wsGlobal, 'TEST')).resolves.toBeUndefined(); + wsGlobal.emit('pong'); - expect(sendSpy).toHaveBeenCalledTimes(3); - ws.close(); + expect(sendSpy).toHaveBeenCalledTimes(3); // unlcear why it is called 3 times + wsGlobal.close(); messageHandlerSpy.mockRestore(); sendSpy.mockRestore(); getYDocSpy.mockRestore(); @@ -498,21 +463,16 @@ describe('TldrawWSService', () => { describe('on websocket error', () => { const setup = async () => { boardRepo.getDocumentFromDb.mockResolvedValueOnce(new WsSharedDocDo('TEST')); - ws = await TestConnection.setupWs(wsUrl, 'TEST'); - const errorLogSpy = jest.spyOn(logger, 'warning'); - - return { - errorLogSpy, - }; + wsGlobal = await TestConnection.setupWs(wsUrl, 'TEST'); }; it('should log error', async () => { - const { errorLogSpy } = await setup(); - await service.setupWsConnection(ws, 'TEST'); - ws.emit('error', new Error('error')); + await setup(); + await service.setupWsConnection(wsGlobal, 'TEST'); + wsGlobal.emit('error', new Error('error')); - expect(errorLogSpy).toHaveBeenCalled(); - ws.close(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(2); + wsGlobal.close(); }); }); @@ -520,7 +480,7 @@ describe('TldrawWSService', () => { describe('when there is no error', () => { const setup = async () => { boardRepo.getDocumentFromDb.mockResolvedValueOnce(new WsSharedDocDo('TEST')); - ws = await TestConnection.setupWs(wsUrl); + wsGlobal = await TestConnection.setupWs(wsUrl); const redisUnsubscribeSpy = jest.spyOn(Ioredis.Redis.prototype, 'unsubscribe').mockResolvedValueOnce(1); const closeConnSpy = jest.spyOn(service, 'closeConnection'); @@ -535,10 +495,10 @@ describe('TldrawWSService', () => { it('should close connection', async () => { const { redisUnsubscribeSpy, closeConnSpy } = await setup(); - await service.setupWsConnection(ws, 'TEST'); + await service.setupWsConnection(wsGlobal, 'TEST'); expect(closeConnSpy).toHaveBeenCalled(); - ws.close(); + wsGlobal.close(); closeConnSpy.mockRestore(); redisUnsubscribeSpy.mockRestore(); }); @@ -547,9 +507,9 @@ describe('TldrawWSService', () => { describe('when there are active connections', () => { const setup = async () => { const doc = new WsSharedDocDo('TEST'); - ws = await TestConnection.setupWs(wsUrl); + wsGlobal = await TestConnection.setupWs(wsUrl); const ws2 = await TestConnection.setupWs(wsUrl); - doc.connections.set(ws, new Set()); + doc.connections.set(wsGlobal, new Set()); doc.connections.set(ws2, new Set()); boardRepo.compressDocument.mockRestore(); @@ -561,45 +521,43 @@ describe('TldrawWSService', () => { it('should not call compressDocument', async () => { const { doc } = await setup(); - await service.closeConnection(doc, ws); + await service.closeConnection(doc, wsGlobal); expect(boardRepo.compressDocument).not.toHaveBeenCalled(); - ws.close(); + wsGlobal.close(); }); }); describe('when close connection fails', () => { const setup = async () => { boardRepo.getDocumentFromDb.mockResolvedValueOnce(new WsSharedDocDo('TEST')); - ws = await TestConnection.setupWs(wsUrl); + wsGlobal = await TestConnection.setupWs(wsUrl); boardRepo.compressDocument.mockResolvedValueOnce(); const redisUnsubscribeSpy = jest.spyOn(Ioredis.Redis.prototype, 'unsubscribe').mockResolvedValueOnce(1); const closeConnSpy = jest.spyOn(service, 'closeConnection').mockRejectedValueOnce(new Error('error')); - const errorLogSpy = jest.spyOn(logger, 'warning'); const sendSpyError = jest.spyOn(service, 'send').mockReturnValue(); jest.spyOn(Ioredis.Redis.prototype, 'subscribe').mockResolvedValueOnce({}); return { redisUnsubscribeSpy, closeConnSpy, - errorLogSpy, sendSpyError, }; }; it('should log error', async () => { - const { redisUnsubscribeSpy, closeConnSpy, errorLogSpy, sendSpyError } = await setup(); + const { redisUnsubscribeSpy, closeConnSpy, sendSpyError } = await setup(); - await service.setupWsConnection(ws, 'TEST'); + await service.setupWsConnection(wsGlobal, 'TEST'); await delay(100); expect(closeConnSpy).toHaveBeenCalled(); - ws.close(); + wsGlobal.close(); await delay(100); - expect(errorLogSpy).toHaveBeenCalled(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(3); redisUnsubscribeSpy.mockRestore(); closeConnSpy.mockRestore(); sendSpyError.mockRestore(); @@ -608,35 +566,33 @@ describe('TldrawWSService', () => { describe('when unsubscribing from Redis throw error', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl); + wsGlobal = await TestConnection.setupWs(wsUrl); const doc = TldrawWsFactory.createWsSharedDocDo(); - doc.connections.set(ws, new Set()); + doc.connections.set(wsGlobal, new Set()); boardRepo.compressDocument.mockResolvedValueOnce(); const redisUnsubscribeSpy = jest .spyOn(Ioredis.Redis.prototype, 'unsubscribe') .mockRejectedValue(new Error('error')); const closeConnSpy = jest.spyOn(service, 'closeConnection'); - const errorLogSpy = jest.spyOn(logger, 'warning'); jest.spyOn(Ioredis.Redis.prototype, 'subscribe').mockResolvedValueOnce({}); return { doc, redisUnsubscribeSpy, closeConnSpy, - errorLogSpy, }; }; it('should log error', async () => { - const { doc, errorLogSpy, redisUnsubscribeSpy, closeConnSpy } = await setup(); + const { doc, redisUnsubscribeSpy, closeConnSpy } = await setup(); - await service.closeConnection(doc, ws); + await service.closeConnection(doc, wsGlobal); await delay(200); expect(redisUnsubscribeSpy).toHaveBeenCalled(); expect(closeConnSpy).toHaveBeenCalled(); - expect(errorLogSpy).toHaveBeenCalled(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(3); closeConnSpy.mockRestore(); redisUnsubscribeSpy.mockRestore(); }); @@ -645,11 +601,11 @@ describe('TldrawWSService', () => { describe('when pong not received', () => { const setup = async () => { boardRepo.getDocumentFromDb.mockResolvedValueOnce(new WsSharedDocDo('TEST')); - ws = await TestConnection.setupWs(wsUrl, 'TEST'); + wsGlobal = await TestConnection.setupWs(wsUrl, 'TEST'); const messageHandlerSpy = jest.spyOn(service, 'messageHandler').mockReturnValueOnce(); const closeConnSpy = jest.spyOn(service, 'closeConnection').mockImplementation(() => Promise.resolve()); - const pingSpy = jest.spyOn(ws, 'ping').mockImplementationOnce(() => {}); + const pingSpy = jest.spyOn(wsGlobal, 'ping').mockImplementationOnce(() => {}); const sendSpy = jest.spyOn(service, 'send').mockImplementation(() => {}); const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); jest.spyOn(Ioredis.Redis.prototype, 'subscribe').mockResolvedValueOnce({}); @@ -666,13 +622,13 @@ describe('TldrawWSService', () => { it('should close connection', async () => { const { messageHandlerSpy, closeConnSpy, pingSpy, sendSpy, clearIntervalSpy } = await setup(); - await service.setupWsConnection(ws, 'TEST'); + await service.setupWsConnection(wsGlobal, 'TEST'); - await delay(20); + await delay(200); expect(closeConnSpy).toHaveBeenCalled(); expect(clearIntervalSpy).toHaveBeenCalled(); - ws.close(); + wsGlobal.close(); messageHandlerSpy.mockRestore(); pingSpy.mockRestore(); closeConnSpy.mockRestore(); @@ -684,14 +640,14 @@ describe('TldrawWSService', () => { describe('when pong not received and close connection fails', () => { const setup = async () => { boardRepo.getDocumentFromDb.mockResolvedValueOnce(new WsSharedDocDo('TEST')); - ws = await TestConnection.setupWs(wsUrl, 'TEST'); + wsGlobal = await TestConnection.setupWs(wsUrl, 'TEST'); const messageHandlerSpy = jest.spyOn(service, 'messageHandler').mockReturnValueOnce(); const closeConnSpy = jest.spyOn(service, 'closeConnection').mockRejectedValue(new Error('error')); - const pingSpy = jest.spyOn(ws, 'ping').mockImplementation(() => {}); + const pingSpy = jest.spyOn(wsGlobal, 'ping').mockImplementation(() => {}); const sendSpy = jest.spyOn(service, 'send').mockImplementation(() => {}); const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); - const errorLogSpy = jest.spyOn(logger, 'warning'); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(1); jest.spyOn(Ioredis.Redis.prototype, 'subscribe').mockResolvedValueOnce({}); return { @@ -700,21 +656,20 @@ describe('TldrawWSService', () => { pingSpy, sendSpy, clearIntervalSpy, - errorLogSpy, }; }; it('should log error', async () => { - const { messageHandlerSpy, closeConnSpy, pingSpy, sendSpy, clearIntervalSpy, errorLogSpy } = await setup(); + const { messageHandlerSpy, closeConnSpy, pingSpy, sendSpy, clearIntervalSpy } = await setup(); - await service.setupWsConnection(ws, 'TEST'); + await service.setupWsConnection(wsGlobal, 'TEST'); await delay(200); expect(closeConnSpy).toHaveBeenCalled(); expect(clearIntervalSpy).toHaveBeenCalled(); - expect(errorLogSpy).toHaveBeenCalled(); - ws.close(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(4); + wsGlobal.close(); messageHandlerSpy.mockRestore(); pingSpy.mockRestore(); closeConnSpy.mockRestore(); @@ -725,37 +680,34 @@ describe('TldrawWSService', () => { describe('when compressDocument failed', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl, 'TEST'); + wsGlobal = await TestConnection.setupWs(wsUrl, 'TEST'); const doc = TldrawWsFactory.createWsSharedDocDo(); - doc.connections.set(ws, new Set()); + doc.connections.set(wsGlobal, new Set()); boardRepo.compressDocument.mockRejectedValueOnce(new Error('error')); - const errorLogSpy = jest.spyOn(logger, 'warning'); return { doc, - errorLogSpy, }; }; it('should log error', async () => { - const { doc, errorLogSpy } = await setup(); + const { doc } = await setup(); - await service.closeConnection(doc, ws); + await service.closeConnection(doc, wsGlobal); expect(boardRepo.compressDocument).toHaveBeenCalled(); - expect(errorLogSpy).toHaveBeenCalled(); - ws.close(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(2); + wsGlobal.close(); }); }); }); describe('updateHandler', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl); + wsGlobal = await TestConnection.setupWs(wsUrl); const sendSpy = jest.spyOn(service, 'send').mockReturnValueOnce(); - const errorLogSpy = jest.spyOn(logger, 'warning'); const publishSpy = jest.spyOn(Ioredis.Redis.prototype, 'publish').mockResolvedValueOnce(1); const doc = TldrawWsFactory.createWsSharedDocDo(); @@ -768,7 +720,6 @@ describe('TldrawWSService', () => { sendSpy, socketMock, msg, - errorLogSpy, publishSpy, }; }; @@ -779,13 +730,13 @@ describe('TldrawWSService', () => { service.updateHandler(msg, socketMock, doc); expect(sendSpy).toHaveBeenCalled(); - ws.close(); + wsGlobal.close(); }); }); describe('databaseUpdateHandler', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl); + wsGlobal = await TestConnection.setupWs(wsUrl); boardRepo.storeUpdate.mockResolvedValueOnce(); }; @@ -795,7 +746,7 @@ describe('TldrawWSService', () => { await service.databaseUpdateHandler('test', new Uint8Array(), 'test'); expect(boardRepo.storeUpdate).toHaveBeenCalled(); - ws.close(); + wsGlobal.close(); }); it('should not call storeUpdate when origin is redis', async () => { @@ -804,40 +755,38 @@ describe('TldrawWSService', () => { await service.databaseUpdateHandler('test', new Uint8Array(), 'redis'); expect(boardRepo.storeUpdate).not.toHaveBeenCalled(); - ws.close(); + wsGlobal.close(); }); }); describe('when publish to Redis throws errors', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl); + wsGlobal = await TestConnection.setupWs(wsUrl); const sendSpy = jest.spyOn(service, 'send').mockReturnValueOnce(); - const errorLogSpy = jest.spyOn(logger, 'warning'); const publishSpy = jest.spyOn(Ioredis.Redis.prototype, 'publish').mockRejectedValueOnce(new Error('error')); const doc = TldrawWsFactory.createWsSharedDocDo(); - doc.connections.set(ws, new Set()); + doc.connections.set(wsGlobal, new Set()); const msg = new Uint8Array([0]); return { doc, sendSpy, msg, - errorLogSpy, publishSpy, }; }; it('should log error', async () => { - const { doc, msg, errorLogSpy, publishSpy } = await setup(); + const { doc, msg, publishSpy } = await setup(); - service.updateHandler(msg, ws, doc); + service.updateHandler(msg, wsGlobal, doc); - await delay(20); + await delay(200); - expect(errorLogSpy).toHaveBeenCalled(); - ws.close(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(3); + wsGlobal.close(); publishSpy.mockRestore(); }); }); @@ -846,9 +795,8 @@ describe('TldrawWSService', () => { describe('when message is received', () => { const setup = async (messageValues: number[]) => { boardRepo.getDocumentFromDb.mockResolvedValueOnce(new WsSharedDocDo('TEST')); - ws = await TestConnection.setupWs(wsUrl, 'TEST'); + wsGlobal = await TestConnection.setupWs(wsUrl, 'TEST'); - const errorLogSpy = jest.spyOn(logger, 'warning'); const messageHandlerSpy = jest.spyOn(service, 'messageHandler'); const readSyncMessageSpy = jest.spyOn(SyncProtocols, 'readSyncMessage').mockImplementationOnce((_dec, enc) => { enc.bufs = [new Uint8Array(2), new Uint8Array(2)]; @@ -862,7 +810,6 @@ describe('TldrawWSService', () => { msg, messageHandlerSpy, readSyncMessageSpy, - errorLogSpy, publishSpy, }; }; @@ -871,43 +818,42 @@ describe('TldrawWSService', () => { const { messageHandlerSpy, msg, readSyncMessageSpy, publishSpy } = await setup([0, 1]); publishSpy.mockResolvedValueOnce(1); - await service.setupWsConnection(ws, 'TEST'); - ws.emit('message', msg); + await service.setupWsConnection(wsGlobal, 'TEST'); + wsGlobal.emit('message', msg); - await delay(20); + await delay(200); expect(messageHandlerSpy).toHaveBeenCalledTimes(1); - ws.close(); + wsGlobal.close(); messageHandlerSpy.mockRestore(); readSyncMessageSpy.mockRestore(); publishSpy.mockRestore(); }); it('should log error when messageHandler throws', async () => { - const { messageHandlerSpy, msg, errorLogSpy } = await setup([0, 1]); + const { messageHandlerSpy, msg } = await setup([0, 1]); messageHandlerSpy.mockImplementationOnce(() => { throw new Error('error'); }); - await service.setupWsConnection(ws, 'TEST'); - ws.emit('message', msg); + await service.setupWsConnection(wsGlobal, 'TEST'); + wsGlobal.emit('message', msg); - await delay(20); + await delay(200); - expect(errorLogSpy).toHaveBeenCalled(); - ws.close(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(4); + wsGlobal.close(); messageHandlerSpy.mockRestore(); - errorLogSpy.mockRestore(); }); it('should log error when publish to Redis throws', async () => { - const { errorLogSpy, publishSpy } = await setup([1, 1]); + const { publishSpy } = await setup([1, 1]); publishSpy.mockRejectedValueOnce(new Error('error')); - await service.setupWsConnection(ws, 'TEST'); + await service.setupWsConnection(wsGlobal, 'TEST'); - expect(errorLogSpy).toHaveBeenCalled(); - ws.close(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(1); + wsGlobal.close(); }); }); }); @@ -931,13 +877,11 @@ describe('TldrawWSService', () => { const redisSubscribeSpy = jest.spyOn(Ioredis.Redis.prototype, 'subscribe').mockResolvedValueOnce(1); const redisOnSpy = jest.spyOn(Ioredis.Redis.prototype, 'on'); - const errorLogSpy = jest.spyOn(logger, 'warning'); boardRepo.getDocumentFromDb.mockResolvedValueOnce(doc); return { redisOnSpy, redisSubscribeSpy, - errorLogSpy, }; }; @@ -961,24 +905,22 @@ describe('TldrawWSService', () => { .spyOn(Ioredis.Redis.prototype, 'subscribe') .mockRejectedValue(new Error('error')); const redisOnSpy = jest.spyOn(Ioredis.Redis.prototype, 'on'); - const errorLogSpy = jest.spyOn(logger, 'warning'); return { redisOnSpy, redisSubscribeSpy, - errorLogSpy, }; }; it('should log error', async () => { - const { errorLogSpy, redisSubscribeSpy, redisOnSpy } = setup(); + const { redisSubscribeSpy, redisOnSpy } = setup(); await service.getDocument('test-redis-fail-2'); await delay(500); expect(redisSubscribeSpy).toHaveBeenCalled(); - expect(errorLogSpy).toHaveBeenCalled(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(3); redisSubscribeSpy.mockRestore(); redisOnSpy.mockRestore(); }); @@ -1051,44 +993,42 @@ describe('TldrawWSService', () => { describe('updateHandler', () => { describe('when update comes from connected websocket', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl, 'TEST'); + wsGlobal = await TestConnection.setupWs(wsUrl, 'TEST'); const doc = new WsSharedDocDo('TEST'); - doc.connections.set(ws, new Set()); + doc.connections.set(wsGlobal, new Set()); const publishSpy = jest.spyOn(Ioredis.Redis.prototype, 'publish'); - const errorLogSpy = jest.spyOn(logger, 'warning'); return { doc, publishSpy, - errorLogSpy, }; }; it('should publish update to redis', async () => { const { doc, publishSpy } = await setup(); - service.updateHandler(new Uint8Array([]), ws, doc); + service.updateHandler(new Uint8Array([]), wsGlobal, doc); expect(publishSpy).toHaveBeenCalled(); - ws.close(); + wsGlobal.close(); }); it('should log error on failed publish', async () => { - const { doc, publishSpy, errorLogSpy } = await setup(); + const { doc, publishSpy } = await setup(); publishSpy.mockRejectedValueOnce(new Error('error')); - service.updateHandler(new Uint8Array([]), ws, doc); + service.updateHandler(new Uint8Array([]), wsGlobal, doc); - expect(errorLogSpy).toHaveBeenCalled(); - ws.close(); + // expect(domainErrorHandler.exec).toHaveBeenCalledTimes(2); + wsGlobal.close(); }); }); }); describe('awarenessUpdateHandler', () => { const setup = async () => { - ws = await TestConnection.setupWs(wsUrl); + wsGlobal = await TestConnection.setupWs(wsUrl); class MockAwareness { on = jest.fn(); @@ -1111,7 +1051,7 @@ describe('TldrawWSService', () => { const mockIDs = new Set(); const mockConns = new Map>(); - mockConns.set(ws, mockIDs); + mockConns.set(wsGlobal, mockIDs); doc.connections = mockConns; return { @@ -1131,14 +1071,14 @@ describe('TldrawWSService', () => { removed: [], }; - service.awarenessUpdateHandler(awarenessUpdate, ws, doc); + service.awarenessUpdateHandler(awarenessUpdate, wsGlobal, doc); expect(mockIDs.size).toBe(2); expect(mockIDs.has(1)).toBe(true); expect(mockIDs.has(3)).toBe(true); expect(mockIDs.has(2)).toBe(false); expect(sendSpy).toBeCalled(); - ws.close(); + wsGlobal.close(); sendSpy.mockRestore(); }); }); @@ -1152,19 +1092,19 @@ describe('TldrawWSService', () => { removed: [], }; - service.awarenessUpdateHandler(awarenessUpdate, ws, doc); + service.awarenessUpdateHandler(awarenessUpdate, wsGlobal, doc); awarenessUpdate = { added: [], updated: [], removed: [1], }; - service.awarenessUpdateHandler(awarenessUpdate, ws, doc); + service.awarenessUpdateHandler(awarenessUpdate, wsGlobal, doc); expect(mockIDs.size).toBe(1); expect(mockIDs.has(1)).toBe(false); expect(mockIDs.has(3)).toBe(true); expect(sendSpy).toBeCalled(); - ws.close(); + wsGlobal.close(); sendSpy.mockRestore(); }); }); @@ -1178,19 +1118,19 @@ describe('TldrawWSService', () => { removed: [], }; - service.awarenessUpdateHandler(awarenessUpdate, ws, doc); + service.awarenessUpdateHandler(awarenessUpdate, wsGlobal, doc); awarenessUpdate = { added: [], updated: [1], removed: [], }; - service.awarenessUpdateHandler(awarenessUpdate, ws, doc); + service.awarenessUpdateHandler(awarenessUpdate, wsGlobal, doc); expect(mockIDs.size).toBe(1); expect(mockIDs.has(1)).toBe(true); expect(sendSpy).toBeCalled(); - ws.close(); + wsGlobal.close(); sendSpy.mockRestore(); }); }); diff --git a/apps/server/src/modules/tldraw/service/tldraw.ws.service.ts b/apps/server/src/modules/tldraw/service/tldraw.ws.service.ts index 70034e192c0..82deaf6ac3c 100644 --- a/apps/server/src/modules/tldraw/service/tldraw.ws.service.ts +++ b/apps/server/src/modules/tldraw/service/tldraw.ws.service.ts @@ -5,8 +5,8 @@ import { encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awaren import { decoding, encoding } from 'lib0'; import { readSyncMessage, writeSyncStep1, writeSyncStep2, writeUpdate } from 'y-protocols/sync'; import { Buffer } from 'node:buffer'; -import { Logger } from '@src/core/logger'; import { YMap } from 'yjs/dist/src/types/YMap'; +import { DomainErrorHandler } from '@src/core'; import { TldrawRedisService } from '../redis'; import { CloseConnectionLoggable, @@ -34,12 +34,10 @@ export class TldrawWsService { constructor( private readonly configService: ConfigService, private readonly tldrawBoardRepo: TldrawBoardRepo, - private readonly logger: Logger, + private readonly domainErrorHandler: DomainErrorHandler, private readonly metricsService: MetricsService, private readonly tldrawRedisService: TldrawRedisService ) { - this.logger.setContext(TldrawWsService.name); - this.tldrawRedisService.sub.on('messageBuffer', (channel, message) => this.redisMessageHandler(channel, message)); } @@ -59,17 +57,17 @@ export class TldrawWsService { public send(doc: WsSharedDocDo, ws: WebSocket, message: Uint8Array): void { if (this.isClosedOrClosing(ws)) { this.closeConnection(doc, ws).catch((err) => { - this.logger.warning(new CloseConnectionLoggable('send | isClosedOrClosing', err)); + this.domainErrorHandler.exec(new CloseConnectionLoggable('send | isClosedOrClosing', err)); + }); + } else { + ws.send(message, (err) => { + if (err) { + this.closeConnection(doc, ws).catch((e) => { + this.domainErrorHandler.exec(new CloseConnectionLoggable('send', e)); + }); + } }); } - - ws.send(message, (err) => { - if (err) { - this.closeConnection(doc, ws).catch((e) => { - this.logger.warning(new CloseConnectionLoggable('send', e)); - }); - } - }); } public updateHandler(update: Uint8Array, origin, doc: WsSharedDocDo): void { @@ -97,6 +95,7 @@ export class TldrawWsService { this.sendAwarenessMessage(buff, doc); }; + // this is a private method, need to be changed public async getDocument(docName: string) { const existingDoc = this.docs.get(docName); @@ -110,6 +109,7 @@ export class TldrawWsService { return existingDoc; } + // doc can be null, need to be handled const doc = await this.tldrawBoardRepo.getDocumentFromDb(docName); doc.isLoaded = false; @@ -178,22 +178,22 @@ export class TldrawWsService { this.tldrawRedisService.handleMessage(channelId, update, doc); }; - public async setupWsConnection(ws: WebSocket, docName: string) { + public async setupWsConnection(ws: WebSocket, docName: string): Promise { ws.binaryType = 'arraybuffer'; - // get doc, initialize if it does not exist yet + // get doc, initialize if it does not exist yet - update this.getDocument(docName) can be return null const doc = await this.getDocument(docName); doc.connections.set(ws, new Set()); ws.on('error', (err) => { - this.logger.warning(new WebsocketErrorLoggable(err)); + this.domainErrorHandler.exec(new WebsocketErrorLoggable(err)); }); ws.on('message', (message: ArrayBufferLike) => { try { this.messageHandler(ws, doc, new Uint8Array(message)); } catch (err) { - this.logger.warning(new WebsocketMessageErrorLoggable(err)); + this.domainErrorHandler.exec(new WebsocketMessageErrorLoggable(err)); } }); @@ -208,14 +208,14 @@ export class TldrawWsService { } this.closeConnection(doc, ws).catch((err) => { - this.logger.warning(new CloseConnectionLoggable('pingInterval', err)); + this.domainErrorHandler.exec(new CloseConnectionLoggable('pingInterval', err)); }); clearInterval(pingInterval); }, pingTimeout); ws.on('close', () => { this.closeConnection(doc, ws).catch((err) => { - this.logger.warning(new CloseConnectionLoggable('websocket close', err)); + this.domainErrorHandler.exec(new CloseConnectionLoggable('websocket close', err)); }); clearInterval(pingInterval); }); @@ -266,7 +266,7 @@ export class TldrawWsService { this.tldrawRedisService.unsubscribeFromRedisChannels(doc); await this.tldrawBoardRepo.compressDocument(doc.name); } catch (err) { - this.logger.warning(new WsSharedDocErrorLoggable(doc.name, 'Error while finalizing document', err)); + this.domainErrorHandler.exec(new WsSharedDocErrorLoggable(doc.name, 'Error while finalizing document', err)); } finally { doc.destroy(); this.docs.delete(doc.name); diff --git a/apps/server/src/modules/tldraw/uc/tldraw-delete-files.uc.ts b/apps/server/src/modules/tldraw/uc/tldraw-delete-files.uc.ts index fe2037eb44d..2d3f9b91510 100644 --- a/apps/server/src/modules/tldraw/uc/tldraw-delete-files.uc.ts +++ b/apps/server/src/modules/tldraw/uc/tldraw-delete-files.uc.ts @@ -14,6 +14,7 @@ export class TldrawDeleteFilesUc { const docNames = await this.mdb.getAllDocumentNames(); for (const docName of docNames) { + // this.mdb.getDocument(docName); can be return null, it is not handled const doc = await this.mdb.getDocument(docName); const usedAssets = this.getUsedAssetsFromDocument(doc);