Skip to content

Commit

Permalink
BC-5736 - Redis for tldraw (#4542)
Browse files Browse the repository at this point in the history
* Introduce redis for tldraw

---------

Co-authored-by: blazejpass <[email protected]>
Co-authored-by: Tomasz Wiaderek <[email protected]>
Co-authored-by: davwas <[email protected]>
Co-authored-by: WojciechGrancow <[email protected]>
Co-authored-by: Cedric Evers <[email protected]>
  • Loading branch information
6 people authored Jan 26, 2024
1 parent 1c9ae44 commit d2baaef
Show file tree
Hide file tree
Showing 65 changed files with 2,945 additions and 844 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ jobs:
- name: 'Dependency Review'
uses: actions/dependency-review-action@v3
with:
allow-licenses: AGPL-3.0-only, LGPL-3.0, MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, X11, 0BSD, GPL-3.0 AND BSD-3-Clause-Clear, Unlicense
allow-dependencies-licenses: 'pkg:npm/parse-mongo-url'
allow-licenses: AGPL-3.0-only, LGPL-3.0, MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, X11, 0BSD, GPL-3.0, Unlicense
# temporarily ignore dependency error for upgrade mongodb 4.9 to 4.11, remove when mikroORM is upgraded to 5.9
allow-ghsas: 'GHSA-vxvm-qww3-2fh7'
12 changes: 5 additions & 7 deletions apps/server/src/modules/tldraw/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,27 @@ import { Configuration } from '@hpi-schul-cloud/commons';
export interface TldrawConfig {
NEST_LOG_LEVEL: string;
INCOMING_REQUEST_TIMEOUT: number;
TLDRAW_DB_COLLECTION_NAME: string;
TLDRAW_DB_FLUSH_SIZE: string;
TLDRAW_DB_MULTIPLE_COLLECTIONS: boolean;
CONNECTION_STRING: string;
FEATURE_TLDRAW_ENABLED: boolean;
TLDRAW_PING_TIMEOUT: number;
TLDRAW_GC_ENABLED: number;
REDIS_URI: string;
API_HOST: number;
TLDRAW_MAX_DOCUMENT_SIZE: number;
}

const tldrawConnectionString: string = Configuration.get('TLDRAW_DB_URL') as string;

const tldrawConfig = {
NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string,
INCOMING_REQUEST_TIMEOUT: Configuration.get('INCOMING_REQUEST_TIMEOUT_API') as number,
TLDRAW_DB_COLLECTION_NAME: Configuration.get('TLDRAW__DB_COLLECTION_NAME') as string,
TLDRAW_DB_FLUSH_SIZE: Configuration.get('TLDRAW__DB_FLUSH_SIZE') as number,
TLDRAW_DB_MULTIPLE_COLLECTIONS: Configuration.get('TLDRAW__DB_MULTIPLE_COLLECTIONS') as boolean,
FEATURE_TLDRAW_ENABLED: Configuration.get('FEATURE_TLDRAW_ENABLED') as boolean,
CONNECTION_STRING: tldrawConnectionString,
CONNECTION_STRING: Configuration.get('TLDRAW_DB_URL') as string,
TLDRAW_PING_TIMEOUT: Configuration.get('TLDRAW__PING_TIMEOUT') as number,
TLDRAW_GC_ENABLED: Configuration.get('TLDRAW__GC_ENABLED') as boolean,
REDIS_URI: Configuration.has('REDIS_URI') ? (Configuration.get('REDIS_URI') as string) : null,
API_HOST: Configuration.get('API_HOST') as string,
TLDRAW_MAX_DOCUMENT_SIZE: Configuration.get('TLDRAW__MAX_DOCUMENT_SIZE') as number,
};

export const SOCKET_PORT = Configuration.get('TLDRAW__SOCKET_PORT') as number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Logger } from '@src/core/logger';
import { TldrawService } from '../../service';
import { TldrawController } from '..';
import { TldrawRepo } from '../../repo';
import { tldrawEntityFactory } from '../../factory';
import { tldrawEntityFactory } from '../../testing';

const baseRouteName = '/tldraw-document';
describe('tldraw controller (api)', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import { Test } from '@nestjs/testing';
import WebSocket from 'ws';
import { TextEncoder } from 'util';
import { INestApplication } 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 } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { AxiosError, AxiosHeaders, AxiosResponse } from 'axios';
import { axiosResponseFactory } from '@shared/testing';
import { WsCloseCodeEnum, WsCloseMessageEnum } from '../../types';
import { TldrawWsTestModule } from '../../tldraw-ws-test.module';
import { TldrawRedisFactory } from '../../redis';
import { TldrawDrawing } from '../../entities';
import { TldrawWsService } from '../../service';
import { TestConnection } from '../../testing/test-connection';
import { TldrawWs } from '../tldraw.ws';
import { TldrawBoardRepo, TldrawRepo, YMongodb } from '../../repo';
import { TestConnection, tldrawTestConfig } from '../../testing';
import { MetricsService } from '../../metrics';
import { TldrawWs } from '..';
import { WsCloseCodeEnum, WsCloseMessageEnum } from '../../types';

describe('WebSocketController (WsAdapter)', () => {
let app: INestApplication;
Expand All @@ -29,14 +36,32 @@ describe('WebSocketController (WsAdapter)', () => {

beforeAll(async () => {
const testingModule = await Test.createTestingModule({
imports: [TldrawWsTestModule],
imports: [
MongoMemoryDatabaseModule.forRoot({ entities: [TldrawDrawing] }),
ConfigModule.forRoot(createConfigModuleOptions(tldrawTestConfig)),
],
providers: [
TldrawWs,
TldrawWsService,
TldrawBoardRepo,
YMongodb,
MetricsService,
TldrawRedisFactory,
{
provide: TldrawRepo,
useValue: createMock<TldrawRepo>(),
},
{
provide: Logger,
useValue: createMock<Logger>(),
},
{
provide: HttpService,
useValue: createMock<HttpService>(),
},
],
}).compile();

gateway = testingModule.get(TldrawWs);
wsService = testingModule.get(TldrawWsService);
httpService = testingModule.get(HttpService);
Expand All @@ -49,10 +74,6 @@ describe('WebSocketController (WsAdapter)', () => {
await app.close();
});

beforeEach(() => {
jest.useFakeTimers({ advanceTimers: true, doNotFake: ['setInterval', 'clearInterval', 'setTimeout'] });
});

afterEach(() => {
jest.clearAllMocks();
});
Expand All @@ -63,14 +84,14 @@ describe('WebSocketController (WsAdapter)', () => {
jest.spyOn(Uint8Array.prototype, 'reduce').mockReturnValueOnce(1);

ws = await TestConnection.setupWs(wsUrl, 'TEST');

const { buffer } = getMessage();

return { handleConnectionSpy, buffer };
};

it(`should handle connection`, async () => {
const { handleConnectionSpy, buffer } = await setup();

ws.send(buffer, () => {});

expect(handleConnectionSpy).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -110,10 +131,10 @@ describe('WebSocketController (WsAdapter)', () => {

it(`should handle 2 connections at same doc and data transfer`, async () => {
const { handleConnectionSpy, ws2, buffer } = await setup();

ws.send(buffer);
ws2.send(buffer);

expect(handleConnectionSpy).toHaveBeenCalled();
expect(handleConnectionSpy).toHaveBeenCalledTimes(2);

handleConnectionSpy.mockRestore();
Expand All @@ -140,7 +161,7 @@ describe('WebSocketController (WsAdapter)', () => {

expect(wsCloseSpy).toHaveBeenCalledWith(
WsCloseCodeEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_CODE,
WsCloseMessageEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_MESSAGE
Buffer.from(WsCloseMessageEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_MESSAGE)
);

httpGetCallSpy.mockRestore();
Expand All @@ -157,7 +178,7 @@ describe('WebSocketController (WsAdapter)', () => {

expect(wsCloseSpy).toHaveBeenCalledWith(
WsCloseCodeEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_CODE,
WsCloseMessageEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_MESSAGE
Buffer.from(WsCloseMessageEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_MESSAGE)
);

httpGetCallSpy.mockRestore();
Expand All @@ -170,10 +191,12 @@ describe('WebSocketController (WsAdapter)', () => {
const setup = () => {
const setupConnectionSpy = jest.spyOn(wsService, 'setupWSConnection');
const wsCloseSpy = jest.spyOn(WebSocket.prototype, 'close');
const closeConnSpy = jest.spyOn(wsService, 'closeConn').mockRejectedValue(new Error('error'));

return {
setupConnectionSpy,
wsCloseSpy,
closeConnSpy,
};
};

Expand All @@ -186,7 +209,7 @@ describe('WebSocketController (WsAdapter)', () => {

expect(wsCloseSpy).toHaveBeenCalledWith(
WsCloseCodeEnum.WS_CLIENT_BAD_REQUEST_CODE,
WsCloseMessageEnum.WS_CLIENT_BAD_REQUEST_MESSAGE
Buffer.from(WsCloseMessageEnum.WS_CLIENT_BAD_REQUEST_MESSAGE)
);

wsCloseSpy.mockRestore();
Expand All @@ -211,7 +234,7 @@ describe('WebSocketController (WsAdapter)', () => {

expect(wsCloseSpy).toHaveBeenCalledWith(
WsCloseCodeEnum.WS_CLIENT_NOT_FOUND_CODE,
WsCloseMessageEnum.WS_CLIENT_NOT_FOUND_MESSAGE
Buffer.from(WsCloseMessageEnum.WS_CLIENT_NOT_FOUND_MESSAGE)
);

wsCloseSpy.mockRestore();
Expand All @@ -232,7 +255,7 @@ describe('WebSocketController (WsAdapter)', () => {

expect(wsCloseSpy).toHaveBeenCalledWith(
WsCloseCodeEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_CODE,
WsCloseMessageEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_MESSAGE
Buffer.from(WsCloseMessageEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_MESSAGE)
);

wsCloseSpy.mockRestore();
Expand Down Expand Up @@ -281,7 +304,7 @@ describe('WebSocketController (WsAdapter)', () => {
expect(setupConnectionSpy).toHaveBeenCalledWith(expect.anything(), 'TEST');
expect(wsCloseSpy).toHaveBeenCalledWith(
WsCloseCodeEnum.WS_CLIENT_FAILED_CONNECTION_CODE,
WsCloseMessageEnum.WS_CLIENT_FAILED_CONNECTION_MESSAGE
Buffer.from(WsCloseMessageEnum.WS_CLIENT_FAILED_CONNECTION_MESSAGE)
);

wsCloseSpy.mockRestore();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Controller, Delete, ForbiddenException, HttpCode, NotFoundException, Param } from '@nestjs/common';
import { ApiValidationError } from '@shared/common';
import { TldrawService } from '../service/tldraw.service';
import { TldrawService } from '../service';
import { TldrawDeleteParams } from './tldraw.params';

@ApiTags('Tldraw Document')
Expand Down
17 changes: 4 additions & 13 deletions apps/server/src/modules/tldraw/controller/tldraw.ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Logger } from '@src/core/logger';
import { AxiosError } from 'axios';
import { firstValueFrom } from 'rxjs';
import { HttpService } from '@nestjs/axios';
import { WebsocketCloseErrorLoggable } from '../loggable/websocket-close-error.loggable';
import { WebsocketCloseErrorLoggable } from '../loggable';
import { TldrawConfig, SOCKET_PORT } from '../config';
import { WsCloseCodeEnum, WsCloseMessageEnum } from '../types';
import { TldrawWsService } from '../service';
Expand Down Expand Up @@ -68,7 +68,7 @@ export class TldrawWs implements OnGatewayInit, OnGatewayConnection {
}

try {
this.tldrawWsService.setupWSConnection(client, docName);
await this.tldrawWsService.setupWSConnection(client, docName);
} catch (err) {
this.closeClientAndLogError(
client,
Expand All @@ -79,17 +79,8 @@ export class TldrawWs implements OnGatewayInit, OnGatewayConnection {
}
}

public afterInit(): void {
this.tldrawWsService.setPersistence({
bindState: async (docName, ydoc) => {
await this.tldrawWsService.updateDocument(docName, ydoc);
},
writeState: async (docName) => {
// This is called when all connections to the document are closed.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
await this.tldrawWsService.flushDocument(docName);
},
});
public async afterInit(): Promise<void> {
await this.tldrawWsService.createDbIndex();
}

private getDocNameFromRequest(request: Request): string {
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/tldraw/domain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ws-shared-doc.do';
Loading

0 comments on commit d2baaef

Please sign in to comment.