From e3a3690b58a6babb5ed90c0ed9dd6c7cd93aff9f Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Sun, 14 May 2023 13:16:39 +0200 Subject: [PATCH] refactor(realtime): solve circle dependencies in realtime-user-status-adapter.ts Signed-off-by: Tilman Vatteroth --- .../realtime-note/realtime-connection.spec.ts | 53 +++- .../realtime-note/realtime-connection.ts | 6 +- .../realtime-user-status-adapter.spec.ts | 231 +++++++++++------- .../realtime-user-status-adapter.ts | 133 +++++----- .../test-utils/mock-connection.ts | 27 +- 5 files changed, 259 insertions(+), 191 deletions(-) diff --git a/backend/src/realtime/realtime-note/realtime-connection.spec.ts b/backend/src/realtime/realtime-note/realtime-connection.spec.ts index b850f577f..3738a9d13 100644 --- a/backend/src/realtime/realtime-note/realtime-connection.spec.ts +++ b/backend/src/realtime/realtime-note/realtime-connection.spec.ts @@ -16,7 +16,10 @@ import { User } from '../../users/user.entity'; import * as NameRandomizerModule from './random-word-lists/name-randomizer'; import { RealtimeConnection } from './realtime-connection'; import { RealtimeNote } from './realtime-note'; -import { RealtimeUserStatusAdapter } from './realtime-user-status-adapter'; +import { + OtherAdapterCollector, + RealtimeUserStatusAdapter, +} from './realtime-user-status-adapter'; import * as RealtimeUserStatusModule from './realtime-user-status-adapter'; jest.mock('./random-word-lists/name-randomizer'); @@ -77,18 +80,46 @@ describe('websocket connection', () => { it.each([true, false])( 'returns the correct realtime user status with acceptEdits %s', (acceptEdits) => { - const realtimeUserStatus = Mock.of(); - let usedConnection: RealtimeConnection | undefined = undefined; + const realtimeUserStatus1 = Mock.of(); + const realtimeUserStatus2 = Mock.of(); + const realtimeUserStatus3 = Mock.of(); + + const realtimeConnections = [ + Mock.of({ + getRealtimeUserStateAdapter: () => realtimeUserStatus1, + }), + Mock.of({ + getRealtimeUserStateAdapter: () => realtimeUserStatus2, + }), + Mock.of({ + getRealtimeUserStateAdapter: () => realtimeUserStatus3, + }), + ]; jest + .spyOn(mockedRealtimeNote, 'getConnections') + .mockImplementation(() => realtimeConnections); + + const constructor = jest .spyOn(RealtimeUserStatusModule, 'RealtimeUserStatusAdapter') .mockImplementation( - (username, displayName, connection, acceptCursorUpdateProvider) => { + ( + username, + displayName, + otherAdapterCollector: OtherAdapterCollector, + messageTransporter, + acceptCursorUpdateProvider, + ) => { expect(username).toBe(mockedUserName); expect(displayName).toBe(mockedDisplayName); - usedConnection = connection; + expect(otherAdapterCollector()).toStrictEqual([ + realtimeUserStatus1, + realtimeUserStatus2, + realtimeUserStatus3, + ]); + expect(messageTransporter).toBe(mockedMessageTransporter); expect(acceptCursorUpdateProvider()).toBe(acceptEdits); - return realtimeUserStatus; + return realtimeUserStatus1; }, ); @@ -99,8 +130,14 @@ describe('websocket connection', () => { acceptEdits, ); - expect(usedConnection).toBe(sut); - expect(sut.getRealtimeUserStateAdapter()).toBe(realtimeUserStatus); + expect(constructor).toHaveBeenCalledWith( + mockedUserName, + mockedDisplayName, + expect.anything(), + mockedMessageTransporter, + expect.anything(), + ); + expect(sut.getRealtimeUserStateAdapter()).toBe(realtimeUserStatus1); }, ); diff --git a/backend/src/realtime/realtime-note/realtime-connection.ts b/backend/src/realtime/realtime-note/realtime-connection.ts index fc0799b9f..414f786c4 100644 --- a/backend/src/realtime/realtime-note/realtime-connection.ts +++ b/backend/src/realtime/realtime-note/realtime-connection.ts @@ -50,7 +50,11 @@ export class RealtimeConnection { this.realtimeUserStateAdapter = new RealtimeUserStatusAdapter( this.user?.username ?? null, this.getDisplayName(), - this, + () => + this.realtimeNote + .getConnections() + .map((connection) => connection.getRealtimeUserStateAdapter()), + this.getTransporter(), () => acceptEdits, ); } diff --git a/backend/src/realtime/realtime-note/realtime-user-status-adapter.spec.ts b/backend/src/realtime/realtime-note/realtime-user-status-adapter.spec.ts index a2202e290..1c50164c7 100644 --- a/backend/src/realtime/realtime-note/realtime-user-status-adapter.spec.ts +++ b/backend/src/realtime/realtime-note/realtime-user-status-adapter.spec.ts @@ -3,25 +3,25 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { Message, MessageTransporter, MessageType } from '@hedgedoc/commons'; -import { Mock } from 'ts-mockery'; +import { + Message, + MessageType, + MockedBackendMessageTransporter, +} from '@hedgedoc/commons'; -import { Note } from '../../notes/note.entity'; -import { RealtimeConnection } from './realtime-connection'; -import { RealtimeNote } from './realtime-note'; -import { MockConnectionBuilder } from './test-utils/mock-connection'; +import { RealtimeUserStatusAdapter } from './realtime-user-status-adapter'; type SendMessageSpy = jest.SpyInstance< void, - [Required] + [Required] >; describe('realtime user status adapter', () => { - let clientLoggedIn1: RealtimeConnection; - let clientLoggedIn2: RealtimeConnection; - let clientGuest: RealtimeConnection; - let clientNotReady: RealtimeConnection; - let clientDecline: RealtimeConnection; + let clientLoggedIn1: RealtimeUserStatusAdapter | undefined; + let clientLoggedIn2: RealtimeUserStatusAdapter | undefined; + let clientGuest: RealtimeUserStatusAdapter | undefined; + let clientNotReady: RealtimeUserStatusAdapter | undefined; + let clientDecline: RealtimeUserStatusAdapter | undefined; let clientLoggedIn1SendMessageSpy: SendMessageSpy; let clientLoggedIn2SendMessageSpy: SendMessageSpy; @@ -29,8 +29,6 @@ describe('realtime user status adapter', () => { let clientNotReadySendMessageSpy: SendMessageSpy; let clientDeclineSendMessageSpy: SendMessageSpy; - let realtimeNote: RealtimeNote; - const clientLoggedIn1Username = 'logged.in1'; const clientLoggedIn2Username = 'logged.in2'; const clientNotReadyUsername = 'not.ready'; @@ -38,46 +36,96 @@ describe('realtime user status adapter', () => { const guestDisplayName = 'Virtuous Mockingbird'; - function spyOnSendMessage(connection: RealtimeConnection): jest.SpyInstance { - return jest.spyOn(connection.getTransporter(), 'sendMessage'); - } + let messageTransporterLoggedIn1: MockedBackendMessageTransporter; + let messageTransporterLoggedIn2: MockedBackendMessageTransporter; + let messageTransporterGuest: MockedBackendMessageTransporter; + let messageTransporterNotReady: MockedBackendMessageTransporter; + let messageTransporterDecline: MockedBackendMessageTransporter; beforeEach(() => { - realtimeNote = new RealtimeNote( - Mock.of({ id: 9876 }), - 'mockedContent', + clientLoggedIn1 = undefined; + clientLoggedIn2 = undefined; + clientGuest = undefined; + clientNotReady = undefined; + clientDecline = undefined; + + messageTransporterLoggedIn1 = new MockedBackendMessageTransporter(''); + messageTransporterLoggedIn2 = new MockedBackendMessageTransporter(''); + messageTransporterGuest = new MockedBackendMessageTransporter(''); + messageTransporterNotReady = new MockedBackendMessageTransporter(''); + messageTransporterDecline = new MockedBackendMessageTransporter(''); + + function otherAdapterCollector(): RealtimeUserStatusAdapter[] { + return [ + clientLoggedIn1, + clientLoggedIn2, + clientGuest, + clientNotReady, + clientDecline, + ].filter((value) => value !== undefined) as RealtimeUserStatusAdapter[]; + } + + clientLoggedIn1 = new RealtimeUserStatusAdapter( + clientLoggedIn1Username, + clientLoggedIn1Username, + otherAdapterCollector, + messageTransporterLoggedIn1, + () => true, + ); + clientLoggedIn2 = new RealtimeUserStatusAdapter( + clientLoggedIn2Username, + clientLoggedIn2Username, + otherAdapterCollector, + messageTransporterLoggedIn2, + () => true, + ); + clientGuest = new RealtimeUserStatusAdapter( + null, + guestDisplayName, + otherAdapterCollector, + messageTransporterGuest, + () => true, + ); + clientNotReady = new RealtimeUserStatusAdapter( + clientNotReadyUsername, + clientNotReadyUsername, + otherAdapterCollector, + messageTransporterNotReady, + () => true, + ); + clientDecline = new RealtimeUserStatusAdapter( + clientDeclineUsername, + clientDeclineUsername, + otherAdapterCollector, + messageTransporterDecline, + () => false, ); - clientLoggedIn1 = new MockConnectionBuilder(realtimeNote) - .withAcceptingRealtimeUserStatus() - .withLoggedInUser(clientLoggedIn1Username) - .build(); - clientLoggedIn2 = new MockConnectionBuilder(realtimeNote) - .withAcceptingRealtimeUserStatus() - .withLoggedInUser(clientLoggedIn2Username) - .build(); - clientGuest = new MockConnectionBuilder(realtimeNote) - .withAcceptingRealtimeUserStatus() - .withGuestUser(guestDisplayName) - .build(); - clientNotReady = new MockConnectionBuilder(realtimeNote) - .withAcceptingRealtimeUserStatus() - .withLoggedInUser(clientNotReadyUsername) - .build(); - clientDecline = new MockConnectionBuilder(realtimeNote) - .withDecliningRealtimeUserStatus() - .withLoggedInUser(clientDeclineUsername) - .build(); - clientLoggedIn1SendMessageSpy = spyOnSendMessage(clientLoggedIn1); - clientLoggedIn2SendMessageSpy = spyOnSendMessage(clientLoggedIn2); - clientGuestSendMessageSpy = spyOnSendMessage(clientGuest); - clientNotReadySendMessageSpy = spyOnSendMessage(clientNotReady); - clientDeclineSendMessageSpy = spyOnSendMessage(clientDecline); + clientLoggedIn1SendMessageSpy = jest.spyOn( + messageTransporterLoggedIn1, + 'sendMessage', + ); + clientLoggedIn2SendMessageSpy = jest.spyOn( + messageTransporterLoggedIn2, + 'sendMessage', + ); + clientGuestSendMessageSpy = jest.spyOn( + messageTransporterGuest, + 'sendMessage', + ); + clientNotReadySendMessageSpy = jest.spyOn( + messageTransporterNotReady, + 'sendMessage', + ); + clientDeclineSendMessageSpy = jest.spyOn( + messageTransporterDecline, + 'sendMessage', + ); - clientLoggedIn1.getTransporter().sendReady(); - clientLoggedIn2.getTransporter().sendReady(); - clientGuest.getTransporter().sendReady(); - clientDecline.getTransporter().sendReady(); + messageTransporterLoggedIn1.sendReady(); + messageTransporterLoggedIn2.sendReady(); + messageTransporterGuest.sendReady(); + messageTransporterDecline.sendReady(); }); it('can answer a state request', () => { @@ -87,9 +135,7 @@ describe('realtime user status adapter', () => { expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0); expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0); - clientLoggedIn1 - .getTransporter() - .emit(MessageType.REALTIME_USER_STATE_REQUEST); + messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_STATE_REQUEST); const expectedMessage1: Message = { type: MessageType.REALTIME_USER_STATE_SET, @@ -149,15 +195,13 @@ describe('realtime user status adapter', () => { const newFrom = Math.floor(Math.random() * 100); const newTo = Math.floor(Math.random() * 100); - clientLoggedIn1 - .getTransporter() - .emit(MessageType.REALTIME_USER_SINGLE_UPDATE, { - type: MessageType.REALTIME_USER_SINGLE_UPDATE, - payload: { - from: newFrom, - to: newTo, - }, - }); + messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_SINGLE_UPDATE, { + type: MessageType.REALTIME_USER_SINGLE_UPDATE, + payload: { + from: newFrom, + to: newTo, + }, + }); const expectedMessage2: Message = { type: MessageType.REALTIME_USER_STATE_SET, @@ -302,7 +346,7 @@ describe('realtime user status adapter', () => { expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0); expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0); - clientLoggedIn2.getTransporter().disconnect(); + messageTransporterLoggedIn2.disconnect(); const expectedMessage1: Message = { type: MessageType.REALTIME_USER_STATE_SET, @@ -417,14 +461,12 @@ describe('realtime user status adapter', () => { expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0); expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0); - clientLoggedIn1 - .getTransporter() - .emit(MessageType.REALTIME_USER_SET_ACTIVITY, { - type: MessageType.REALTIME_USER_SET_ACTIVITY, - payload: { - active: false, - }, - }); + messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_SET_ACTIVITY, { + type: MessageType.REALTIME_USER_SET_ACTIVITY, + payload: { + active: false, + }, + }); const expectedInactivityMessage2: Message = { @@ -564,14 +606,12 @@ describe('realtime user status adapter', () => { expectedInactivityMessage5, ); - clientLoggedIn1 - .getTransporter() - .emit(MessageType.REALTIME_USER_SET_ACTIVITY, { - type: MessageType.REALTIME_USER_SET_ACTIVITY, - payload: { - active: false, - }, - }); + messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_SET_ACTIVITY, { + type: MessageType.REALTIME_USER_SET_ACTIVITY, + payload: { + active: false, + }, + }); expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0); expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith( @@ -588,14 +628,19 @@ describe('realtime user status adapter', () => { expectedInactivityMessage5, ); - clientLoggedIn1 - .getTransporter() - .emit(MessageType.REALTIME_USER_SET_ACTIVITY, { - type: MessageType.REALTIME_USER_SET_ACTIVITY, - payload: { - active: true, - }, - }); + messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_SET_ACTIVITY, { + type: MessageType.REALTIME_USER_SET_ACTIVITY, + payload: { + active: true, + }, + }); + + messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_SET_ACTIVITY, { + type: MessageType.REALTIME_USER_SET_ACTIVITY, + payload: { + active: true, + }, + }); const expectedReactivityMessage2: Message = { @@ -735,14 +780,12 @@ describe('realtime user status adapter', () => { expectedReactivityMessage5, ); - clientLoggedIn1 - .getTransporter() - .emit(MessageType.REALTIME_USER_SET_ACTIVITY, { - type: MessageType.REALTIME_USER_SET_ACTIVITY, - payload: { - active: true, - }, - }); + messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_SET_ACTIVITY, { + type: MessageType.REALTIME_USER_SET_ACTIVITY, + payload: { + active: true, + }, + }); expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0); expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith( diff --git a/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts b/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts index e925587a1..ff8a9eccf 100644 --- a/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts +++ b/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts @@ -3,11 +3,15 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { Message, MessageType, RealtimeUser } from '@hedgedoc/commons'; +import { + Message, + MessageTransporter, + MessageType, + RealtimeUser, +} from '@hedgedoc/commons'; import { Listener } from 'eventemitter2'; -import { RealtimeConnection } from './realtime-connection'; -import { RealtimeNote } from './realtime-note'; +export type OtherAdapterCollector = () => RealtimeUserStatusAdapter[]; /** * Saves the current realtime status of a specific client and sends updates of changes to other clients. @@ -16,30 +20,23 @@ export class RealtimeUserStatusAdapter { private readonly realtimeUser: RealtimeUser; constructor( - username: string | null, - displayName: string, - private connection: RealtimeConnection, + private readonly username: string | null, + private readonly displayName: string, + private collectOtherAdapters: OtherAdapterCollector, + private messageTransporter: MessageTransporter, private acceptCursorUpdateProvider: () => boolean, ) { - this.realtimeUser = this.createInitialRealtimeUserState( - username, - displayName, - connection.getRealtimeNote(), - ); - this.bindRealtimeUserStateEvents(connection); + this.realtimeUser = this.createInitialRealtimeUserState(); + this.bindRealtimeUserStateEvents(); } - private createInitialRealtimeUserState( - username: string | null, - displayName: string, - realtimeNote: RealtimeNote, - ): RealtimeUser { + private createInitialRealtimeUserState(): RealtimeUser { return { - username: username, - displayName: displayName, + username: this.username, + displayName: this.displayName, active: true, styleIndex: this.findLeastUsedStyleIndex( - this.createStyleIndexToCountMap(realtimeNote), + this.createStyleIndexToCountMap(), ), cursor: !this.acceptCursorUpdateProvider() ? null @@ -50,51 +47,55 @@ export class RealtimeUserStatusAdapter { }; } - private bindRealtimeUserStateEvents(connection: RealtimeConnection): void { - const realtimeNote = connection.getRealtimeNote(); - const transporterMessagesListener = connection.getTransporter().on( + private bindRealtimeUserStateEvents(): void { + const transporterMessagesListener = this.messageTransporter.on( MessageType.REALTIME_USER_SINGLE_UPDATE, (message: Message) => { this.realtimeUser.cursor = this.acceptCursorUpdateProvider() ? message.payload : null; - this.sendRealtimeUserStatusUpdateEvent(connection); - }, - { objectify: true }, - ) as Listener; - const transporterRequestMessageListener = connection.getTransporter().on( - MessageType.REALTIME_USER_STATE_REQUEST, - () => { - this.sendCompleteStateToClient(connection); + this.collectOtherAdapters() + .filter((adapter) => adapter !== this) + .forEach((adapter) => adapter.sendCompleteStateToClient()); }, { objectify: true }, ) as Listener; - const clientRemoveListener = realtimeNote.on( - 'clientRemoved', - (client: RealtimeConnection) => { - if (client === connection) { - this.sendRealtimeUserStatusUpdateEvent(connection); - } + const transporterRequestMessageListener = this.messageTransporter.on( + MessageType.REALTIME_USER_STATE_REQUEST, + () => { + this.sendCompleteStateToClient(); + }, + { objectify: true }, + ) as Listener; + + const clientRemoveListener = this.messageTransporter.on( + 'disconnected', + () => { + this.collectOtherAdapters() + .filter((adapter) => adapter !== this) + .forEach((adapter) => adapter.sendCompleteStateToClient()); }, { objectify: true, }, ) as Listener; - const realtimeUserSetActivityListener = connection.getTransporter().on( + const realtimeUserSetActivityListener = this.messageTransporter.on( MessageType.REALTIME_USER_SET_ACTIVITY, (message: Message) => { if (this.realtimeUser.active === message.payload.active) { return; } this.realtimeUser.active = message.payload.active; - this.sendRealtimeUserStatusUpdateEvent(connection); + this.collectOtherAdapters() + .filter((adapter) => adapter !== this) + .forEach((adapter) => adapter.sendCompleteStateToClient()); }, { objectify: true }, ) as Listener; - connection.getTransporter().on('disconnected', () => { + this.messageTransporter.on('disconnected', () => { transporterMessagesListener?.off(); transporterRequestMessageListener.off(); clientRemoveListener.off(); @@ -102,45 +103,31 @@ export class RealtimeUserStatusAdapter { }); } - private sendRealtimeUserStatusUpdateEvent( - exceptClient: RealtimeConnection, - ): void { - this.collectAllConnectionsExcept(exceptClient).forEach( - this.sendCompleteStateToClient.bind(this), - ); + private getSendableState(): RealtimeUser | undefined { + return this.messageTransporter.isReady() ? this.realtimeUser : undefined; } - private sendCompleteStateToClient(receivingClient: RealtimeConnection): void { - const realtimeUser = - receivingClient.getRealtimeUserStateAdapter().realtimeUser; - const realtimeUsers = this.collectAllConnectionsExcept(receivingClient) - .map((client) => client.getRealtimeUserStateAdapter().realtimeUser) - .filter((realtimeUser) => realtimeUser !== null); + public sendCompleteStateToClient(): void { + if (!this.messageTransporter.isReady()) { + return; + } + const realtimeUsers = this.collectOtherAdapters() + .filter((adapter) => adapter !== this) + .map((adapter) => adapter.getSendableState()) + .filter((value) => value !== undefined) as RealtimeUser[]; - receivingClient.getTransporter().sendMessage({ + this.messageTransporter.sendMessage({ type: MessageType.REALTIME_USER_STATE_SET, payload: { users: realtimeUsers, ownUser: { - displayName: realtimeUser.displayName, - styleIndex: realtimeUser.styleIndex, + displayName: this.realtimeUser.displayName, + styleIndex: this.realtimeUser.styleIndex, }, }, }); } - private collectAllConnectionsExcept( - exceptClient: RealtimeConnection, - ): RealtimeConnection[] { - return this.connection - .getRealtimeNote() - .getConnections() - .filter( - (client) => - client !== exceptClient && client.getTransporter().isReady(), - ); - } - private findLeastUsedStyleIndex(map: Map): number { let leastUsedStyleIndex = 0; let leastUsedStyleIndexCount = map.get(0) ?? 0; @@ -154,15 +141,9 @@ export class RealtimeUserStatusAdapter { return leastUsedStyleIndex; } - private createStyleIndexToCountMap( - realtimeNote: RealtimeNote, - ): Map { - return realtimeNote - .getConnections() - .map( - (connection) => - connection.getRealtimeUserStateAdapter().realtimeUser?.styleIndex, - ) + private createStyleIndexToCountMap(): Map { + return this.collectOtherAdapters() + .map((adapter) => adapter.realtimeUser.styleIndex) .reduce((map, styleIndex) => { if (styleIndex !== undefined) { const count = (map.get(styleIndex) ?? 0) + 1; diff --git a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts index a24c929e6..3c9c4d97f 100644 --- a/backend/src/realtime/realtime-note/test-utils/mock-connection.ts +++ b/backend/src/realtime/realtime-note/test-utils/mock-connection.ts @@ -82,8 +82,21 @@ export class MockConnectionBuilder { const displayName = this.deriveDisplayName(); const transporter = new MockedBackendMessageTransporter(''); - let realtimeUserStateAdapter: RealtimeUserStatusAdapter = - Mock.of({}); + const realtimeUserStateAdapter: RealtimeUserStatusAdapter = + this.includeRealtimeUserStatus === RealtimeUserState.WITHOUT + ? Mock.of({}) + : new RealtimeUserStatusAdapter( + this.username ?? null, + displayName, + () => + this.realtimeNote + .getConnections() + .map((connection) => connection.getRealtimeUserStateAdapter()), + transporter, + () => + this.includeRealtimeUserStatus === + RealtimeUserState.WITH_READWRITE, + ); const mockUser = this.username === null @@ -107,16 +120,6 @@ export class MockConnectionBuilder { this.realtimeNote.removeClient(connection), ); - if (this.includeRealtimeUserStatus !== RealtimeUserState.WITHOUT) { - realtimeUserStateAdapter = new RealtimeUserStatusAdapter( - this.username ?? null, - displayName, - connection, - () => - this.includeRealtimeUserStatus === RealtimeUserState.WITH_READWRITE, - ); - } - this.realtimeNote.addClient(connection); return connection;