refactor: remove isomorphic-ws

The package caused some issues while working on other features.
Mostly because bundlers have been unable to determine the correct
websocket constructor.
So I replaced it with a more object-oriented approach.

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-05-31 22:38:45 +02:00
parent 14ba7ea9ce
commit 753c6e593f
23 changed files with 724 additions and 283 deletions

View file

@ -5,7 +5,7 @@
*/
import {
MessageTransporter,
MockedBackendMessageTransporter,
MockedBackendTransportAdapter,
YDocSyncServerAdapter,
} from '@hedgedoc/commons';
import * as HedgeDocCommonsModule from '@hedgedoc/commons';
@ -49,7 +49,8 @@ describe('websocket connection', () => {
displayName: mockedDisplayName,
});
mockedMessageTransporter = new MockedBackendMessageTransporter('');
mockedMessageTransporter = new MessageTransporter();
mockedMessageTransporter.setAdapter(new MockedBackendTransportAdapter(''));
});
afterEach(() => {

View file

@ -5,16 +5,15 @@
*/
import {
Message,
MessageTransporter,
MessageType,
MockedBackendMessageTransporter,
MockedBackendTransportAdapter,
waitForOtherPromisesToFinish,
} from '@hedgedoc/commons';
import { RealtimeUserStatusAdapter } from './realtime-user-status-adapter';
type SendMessageSpy = jest.SpyInstance<
void,
[Required<MockedBackendMessageTransporter['sendMessage']>]
>;
type SendMessageSpy = jest.SpyInstance<void, [content: Message<MessageType>]>;
describe('realtime user status adapter', () => {
let clientLoggedIn1: RealtimeUserStatusAdapter | undefined;
@ -36,24 +35,42 @@ describe('realtime user status adapter', () => {
const guestDisplayName = 'Virtuous Mockingbird';
let messageTransporterLoggedIn1: MockedBackendMessageTransporter;
let messageTransporterLoggedIn2: MockedBackendMessageTransporter;
let messageTransporterGuest: MockedBackendMessageTransporter;
let messageTransporterNotReady: MockedBackendMessageTransporter;
let messageTransporterDecline: MockedBackendMessageTransporter;
let messageTransporterLoggedIn1: MessageTransporter;
let messageTransporterLoggedIn2: MessageTransporter;
let messageTransporterGuest: MessageTransporter;
let messageTransporterNotReady: MessageTransporter;
let messageTransporterDecline: MessageTransporter;
beforeEach(() => {
beforeEach(async () => {
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('');
messageTransporterLoggedIn1 = new MessageTransporter();
messageTransporterLoggedIn2 = new MessageTransporter();
messageTransporterGuest = new MessageTransporter();
messageTransporterNotReady = new MessageTransporter();
messageTransporterDecline = new MessageTransporter();
const mockedTransportAdapterLoggedIn1 = new MockedBackendTransportAdapter(
'',
);
const mockedTransportAdapterLoggedIn2 = new MockedBackendTransportAdapter(
'',
);
const mockedTransportAdapterGuest = new MockedBackendTransportAdapter('');
const mockedTransportAdapterNotReady = new MockedBackendTransportAdapter(
'',
);
const mockedTransportAdapterDecline = new MockedBackendTransportAdapter('');
messageTransporterLoggedIn1.setAdapter(mockedTransportAdapterLoggedIn1);
messageTransporterLoggedIn2.setAdapter(mockedTransportAdapterLoggedIn2);
messageTransporterGuest.setAdapter(mockedTransportAdapterGuest);
messageTransporterNotReady.setAdapter(mockedTransportAdapterNotReady);
messageTransporterDecline.setAdapter(mockedTransportAdapterDecline);
function otherAdapterCollector(): RealtimeUserStatusAdapter[] {
return [
@ -126,14 +143,15 @@ describe('realtime user status adapter', () => {
messageTransporterLoggedIn2.sendReady();
messageTransporterGuest.sendReady();
messageTransporterDecline.sendReady();
await waitForOtherPromisesToFinish();
});
it('can answer a state request', () => {
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_STATE_REQUEST);
@ -176,21 +194,21 @@ describe('realtime user status adapter', () => {
},
};
expect(clientLoggedIn1SendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedMessage1,
);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
});
it('can save an cursor update', () => {
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
const newFrom = Math.floor(Math.random() * 100);
const newTo = Math.floor(Math.random() * 100);
@ -323,28 +341,28 @@ describe('realtime user status adapter', () => {
},
};
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedMessage2,
);
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedMessage3,
);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedMessage5,
);
});
it('will inform other clients about removed client', () => {
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
messageTransporterLoggedIn2.disconnect();
@ -439,27 +457,27 @@ describe('realtime user status adapter', () => {
};
expect(clientLoggedIn1SendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedMessage1,
);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedMessage3,
);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedMessage5,
);
});
it('will inform other clients about inactivity and reactivity', () => {
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_SET_ACTIVITY, {
type: MessageType.REALTIME_USER_SET_ACTIVITY,
@ -591,18 +609,18 @@ describe('realtime user status adapter', () => {
},
};
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedInactivityMessage2,
);
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedInactivityMessage3,
);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedInactivityMessage5,
);
@ -613,18 +631,18 @@ describe('realtime user status adapter', () => {
},
});
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedInactivityMessage2,
);
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedInactivityMessage3,
);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedInactivityMessage5,
);
@ -765,18 +783,18 @@ describe('realtime user status adapter', () => {
},
};
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedReactivityMessage2,
);
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedReactivityMessage3,
);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedReactivityMessage5,
);
@ -787,18 +805,18 @@ describe('realtime user status adapter', () => {
},
});
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedReactivityMessage2,
);
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedReactivityMessage3,
);
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
1,
2,
expectedReactivityMessage5,
);
});

View file

@ -4,7 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
MockedBackendMessageTransporter,
MessageTransporter,
MockedBackendTransportAdapter,
YDocSyncServerAdapter,
} from '@hedgedoc/commons';
import { Mock } from 'ts-mockery';
@ -81,7 +82,8 @@ export class MockConnectionBuilder {
public build(): RealtimeConnection {
const displayName = this.deriveDisplayName();
const transporter = new MockedBackendMessageTransporter('');
const transporter = new MessageTransporter();
transporter.setAdapter(new MockedBackendTransportAdapter(''));
const realtimeUserStateAdapter: RealtimeUserStatusAdapter =
this.includeRealtimeUserStatus === RealtimeUserState.WITHOUT
? Mock.of<RealtimeUserStatusAdapter>({})

View file

@ -0,0 +1,129 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConnectionState, Message, MessageType } from '@hedgedoc/commons';
import { Mock } from 'ts-mockery';
import WebSocket, { MessageEvent } from 'ws';
import { BackendWebsocketAdapter } from './backend-websocket-adapter';
describe('backend websocket adapter', () => {
let sut: BackendWebsocketAdapter;
let mockedSocket: WebSocket;
function mockSocket(readyState: 0 | 1 | 2 | 3 = 0) {
mockedSocket = Mock.of<WebSocket>({
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
close: jest.fn(),
send: jest.fn(),
readyState: readyState,
});
sut = new BackendWebsocketAdapter(mockedSocket);
}
beforeEach(() => {
mockSocket(0);
});
it('can bind and unbind the close event', () => {
const handler = jest.fn();
const unbind = sut.bindOnCloseEvent(handler);
expect(mockedSocket.addEventListener).toHaveBeenCalledWith(
'close',
handler,
);
unbind();
expect(mockedSocket.removeEventListener).toHaveBeenCalledWith(
'close',
handler,
);
});
it('can bind and unbind the connect event', () => {
const handler = jest.fn();
const unbind = sut.bindOnConnectedEvent(handler);
expect(mockedSocket.addEventListener).toHaveBeenCalledWith('open', handler);
unbind();
expect(mockedSocket.removeEventListener).toHaveBeenCalledWith(
'open',
handler,
);
});
it('can bind and unbind the error event', () => {
const handler = jest.fn();
const unbind = sut.bindOnErrorEvent(handler);
expect(mockedSocket.addEventListener).toHaveBeenCalledWith(
'error',
handler,
);
unbind();
expect(mockedSocket.removeEventListener).toHaveBeenCalledWith(
'error',
handler,
);
});
it('can bind, unbind and translate the message event', () => {
const handler = jest.fn();
let modifiedHandler: (event: MessageEvent) => void = jest.fn();
jest
.spyOn(mockedSocket, 'addEventListener')
.mockImplementation((event, handler_) => {
modifiedHandler = handler_;
});
const unbind = sut.bindOnMessageEvent(handler);
modifiedHandler(Mock.of<MessageEvent>({ data: new ArrayBuffer(0) }));
expect(handler).toHaveBeenCalledTimes(0);
modifiedHandler(Mock.of<MessageEvent>({ data: '{ "type": "READY" }' }));
expect(handler).toHaveBeenCalledWith({ type: 'READY' });
expect(mockedSocket.addEventListener).toHaveBeenCalledWith(
'message',
modifiedHandler,
);
unbind();
expect(mockedSocket.removeEventListener).toHaveBeenCalledWith(
'message',
modifiedHandler,
);
});
it('can disconnect the socket', () => {
sut.disconnect();
expect(mockedSocket.close).toHaveBeenCalled();
});
it('can send messages', () => {
const value: Message<MessageType> = { type: MessageType.READY };
sut.send(value);
expect(mockedSocket.send).toHaveBeenCalledWith('{"type":"READY"}');
});
it('can read the connection state when open', () => {
mockSocket(WebSocket.OPEN);
expect(sut.getConnectionState()).toBe(ConnectionState.CONNECTED);
});
it('can read the connection state when connecting', () => {
mockSocket(WebSocket.CONNECTING);
expect(sut.getConnectionState()).toBe(ConnectionState.CONNECTING);
});
it('can read the connection state when closing', () => {
mockSocket(WebSocket.CLOSING);
expect(sut.getConnectionState()).toBe(ConnectionState.DISCONNECTED);
});
it('can read the connection state when closed', () => {
mockSocket(WebSocket.CLOSED);
expect(sut.getConnectionState()).toBe(ConnectionState.DISCONNECTED);
});
});

View file

@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConnectionState, Message, MessageType } from '@hedgedoc/commons';
import type { TransportAdapter } from '@hedgedoc/commons';
import WebSocket, { MessageEvent } from 'ws';
/**
* Implements a transport adapter that communicates using a nodejs socket.
*/
export class BackendWebsocketAdapter implements TransportAdapter {
constructor(private socket: WebSocket) {}
bindOnCloseEvent(handler: () => void): () => void {
this.socket.addEventListener('close', handler);
return () => {
this.socket.removeEventListener('close', handler);
};
}
bindOnConnectedEvent(handler: () => void): () => void {
this.socket.addEventListener('open', handler);
return () => {
this.socket.removeEventListener('open', handler);
};
}
bindOnErrorEvent(handler: () => void): () => void {
this.socket.addEventListener('error', handler);
return () => {
this.socket.removeEventListener('error', handler);
};
}
bindOnMessageEvent(
handler: (value: Message<MessageType>) => void,
): () => void {
function adjustedHandler(message: MessageEvent): void {
if (typeof message.data !== 'string') {
return;
}
handler(JSON.parse(message.data) as Message<MessageType>);
}
this.socket.addEventListener('message', adjustedHandler);
return () => {
this.socket.removeEventListener('message', adjustedHandler);
};
}
disconnect(): void {
this.socket.close();
}
getConnectionState(): ConnectionState {
if (this.socket.readyState === WebSocket.OPEN) {
return ConnectionState.CONNECTED;
} else if (this.socket.readyState === WebSocket.CONNECTING) {
return ConnectionState.CONNECTING;
} else {
return ConnectionState.DISCONNECTED;
}
}
send(value: Message<MessageType>): void {
this.socket.send(JSON.stringify(value));
}
}

View file

@ -4,9 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
MessageTransporter,
NotePermissions,
userCanEdit,
WebsocketTransporter,
} from '@hedgedoc/commons';
import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
import { IncomingMessage } from 'http';
@ -21,6 +21,7 @@ import { User } from '../../users/user.entity';
import { UsersService } from '../../users/users.service';
import { RealtimeConnection } from '../realtime-note/realtime-connection';
import { RealtimeNoteService } from '../realtime-note/realtime-note.service';
import { BackendWebsocketAdapter } from './backend-websocket-adapter';
import { extractNoteIdFromRequestUrl } from './utils/extract-note-id-from-request-url';
/**
@ -85,7 +86,7 @@ export class WebsocketGateway implements OnGatewayConnection {
const realtimeNote =
await this.realtimeNoteService.getOrCreateRealtimeNote(note);
const websocketTransporter = new WebsocketTransporter();
const websocketTransporter = new MessageTransporter();
const permissions = await this.noteService.toNotePermissionsDto(note);
const acceptEdits: boolean = userCanEdit(
permissions as NotePermissions,
@ -97,7 +98,9 @@ export class WebsocketGateway implements OnGatewayConnection {
realtimeNote,
acceptEdits,
);
websocketTransporter.setWebsocket(clientSocket);
websocketTransporter.setAdapter(
new BackendWebsocketAdapter(clientSocket),
);
realtimeNote.addClient(connection);