mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 03:06:31 -05:00
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:
parent
14ba7ea9ce
commit
753c6e593f
23 changed files with 724 additions and 283 deletions
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
MessageTransporter,
|
MessageTransporter,
|
||||||
MockedBackendMessageTransporter,
|
MockedBackendTransportAdapter,
|
||||||
YDocSyncServerAdapter,
|
YDocSyncServerAdapter,
|
||||||
} from '@hedgedoc/commons';
|
} from '@hedgedoc/commons';
|
||||||
import * as HedgeDocCommonsModule from '@hedgedoc/commons';
|
import * as HedgeDocCommonsModule from '@hedgedoc/commons';
|
||||||
|
@ -49,7 +49,8 @@ describe('websocket connection', () => {
|
||||||
displayName: mockedDisplayName,
|
displayName: mockedDisplayName,
|
||||||
});
|
});
|
||||||
|
|
||||||
mockedMessageTransporter = new MockedBackendMessageTransporter('');
|
mockedMessageTransporter = new MessageTransporter();
|
||||||
|
mockedMessageTransporter.setAdapter(new MockedBackendTransportAdapter(''));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
|
@ -5,16 +5,15 @@
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
Message,
|
Message,
|
||||||
|
MessageTransporter,
|
||||||
MessageType,
|
MessageType,
|
||||||
MockedBackendMessageTransporter,
|
MockedBackendTransportAdapter,
|
||||||
|
waitForOtherPromisesToFinish,
|
||||||
} from '@hedgedoc/commons';
|
} from '@hedgedoc/commons';
|
||||||
|
|
||||||
import { RealtimeUserStatusAdapter } from './realtime-user-status-adapter';
|
import { RealtimeUserStatusAdapter } from './realtime-user-status-adapter';
|
||||||
|
|
||||||
type SendMessageSpy = jest.SpyInstance<
|
type SendMessageSpy = jest.SpyInstance<void, [content: Message<MessageType>]>;
|
||||||
void,
|
|
||||||
[Required<MockedBackendMessageTransporter['sendMessage']>]
|
|
||||||
>;
|
|
||||||
|
|
||||||
describe('realtime user status adapter', () => {
|
describe('realtime user status adapter', () => {
|
||||||
let clientLoggedIn1: RealtimeUserStatusAdapter | undefined;
|
let clientLoggedIn1: RealtimeUserStatusAdapter | undefined;
|
||||||
|
@ -36,24 +35,42 @@ describe('realtime user status adapter', () => {
|
||||||
|
|
||||||
const guestDisplayName = 'Virtuous Mockingbird';
|
const guestDisplayName = 'Virtuous Mockingbird';
|
||||||
|
|
||||||
let messageTransporterLoggedIn1: MockedBackendMessageTransporter;
|
let messageTransporterLoggedIn1: MessageTransporter;
|
||||||
let messageTransporterLoggedIn2: MockedBackendMessageTransporter;
|
let messageTransporterLoggedIn2: MessageTransporter;
|
||||||
let messageTransporterGuest: MockedBackendMessageTransporter;
|
let messageTransporterGuest: MessageTransporter;
|
||||||
let messageTransporterNotReady: MockedBackendMessageTransporter;
|
let messageTransporterNotReady: MessageTransporter;
|
||||||
let messageTransporterDecline: MockedBackendMessageTransporter;
|
let messageTransporterDecline: MessageTransporter;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
clientLoggedIn1 = undefined;
|
clientLoggedIn1 = undefined;
|
||||||
clientLoggedIn2 = undefined;
|
clientLoggedIn2 = undefined;
|
||||||
clientGuest = undefined;
|
clientGuest = undefined;
|
||||||
clientNotReady = undefined;
|
clientNotReady = undefined;
|
||||||
clientDecline = undefined;
|
clientDecline = undefined;
|
||||||
|
|
||||||
messageTransporterLoggedIn1 = new MockedBackendMessageTransporter('');
|
messageTransporterLoggedIn1 = new MessageTransporter();
|
||||||
messageTransporterLoggedIn2 = new MockedBackendMessageTransporter('');
|
messageTransporterLoggedIn2 = new MessageTransporter();
|
||||||
messageTransporterGuest = new MockedBackendMessageTransporter('');
|
messageTransporterGuest = new MessageTransporter();
|
||||||
messageTransporterNotReady = new MockedBackendMessageTransporter('');
|
messageTransporterNotReady = new MessageTransporter();
|
||||||
messageTransporterDecline = new MockedBackendMessageTransporter('');
|
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[] {
|
function otherAdapterCollector(): RealtimeUserStatusAdapter[] {
|
||||||
return [
|
return [
|
||||||
|
@ -126,14 +143,15 @@ describe('realtime user status adapter', () => {
|
||||||
messageTransporterLoggedIn2.sendReady();
|
messageTransporterLoggedIn2.sendReady();
|
||||||
messageTransporterGuest.sendReady();
|
messageTransporterGuest.sendReady();
|
||||||
messageTransporterDecline.sendReady();
|
messageTransporterDecline.sendReady();
|
||||||
|
await waitForOtherPromisesToFinish();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can answer a state request', () => {
|
it('can answer a state request', () => {
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_STATE_REQUEST);
|
messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_STATE_REQUEST);
|
||||||
|
|
||||||
|
@ -176,21 +194,21 @@ describe('realtime user status adapter', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientLoggedIn1SendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedMessage1,
|
expectedMessage1,
|
||||||
);
|
);
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can save an cursor update', () => {
|
it('can save an cursor update', () => {
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
const newFrom = Math.floor(Math.random() * 100);
|
const newFrom = Math.floor(Math.random() * 100);
|
||||||
const newTo = 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(
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedMessage2,
|
expectedMessage2,
|
||||||
);
|
);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedMessage3,
|
expectedMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedMessage5,
|
expectedMessage5,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will inform other clients about removed client', () => {
|
it('will inform other clients about removed client', () => {
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
messageTransporterLoggedIn2.disconnect();
|
messageTransporterLoggedIn2.disconnect();
|
||||||
|
|
||||||
|
@ -439,27 +457,27 @@ describe('realtime user status adapter', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientLoggedIn1SendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedMessage1,
|
expectedMessage1,
|
||||||
);
|
);
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedMessage3,
|
expectedMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedMessage5,
|
expectedMessage5,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will inform other clients about inactivity and reactivity', () => {
|
it('will inform other clients about inactivity and reactivity', () => {
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_SET_ACTIVITY, {
|
messageTransporterLoggedIn1.emit(MessageType.REALTIME_USER_SET_ACTIVITY, {
|
||||||
type: 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(
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedInactivityMessage2,
|
expectedInactivityMessage2,
|
||||||
);
|
);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedInactivityMessage3,
|
expectedInactivityMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedInactivityMessage5,
|
expectedInactivityMessage5,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -613,18 +631,18 @@ describe('realtime user status adapter', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedInactivityMessage2,
|
expectedInactivityMessage2,
|
||||||
);
|
);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedInactivityMessage3,
|
expectedInactivityMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedInactivityMessage5,
|
expectedInactivityMessage5,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -765,18 +783,18 @@ describe('realtime user status adapter', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedReactivityMessage2,
|
expectedReactivityMessage2,
|
||||||
);
|
);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedReactivityMessage3,
|
expectedReactivityMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedReactivityMessage5,
|
expectedReactivityMessage5,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -787,18 +805,18 @@ describe('realtime user status adapter', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientLoggedIn2SendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedReactivityMessage2,
|
expectedReactivityMessage2,
|
||||||
);
|
);
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientGuestSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedReactivityMessage3,
|
expectedReactivityMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
expectedReactivityMessage5,
|
expectedReactivityMessage5,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
MockedBackendMessageTransporter,
|
MessageTransporter,
|
||||||
|
MockedBackendTransportAdapter,
|
||||||
YDocSyncServerAdapter,
|
YDocSyncServerAdapter,
|
||||||
} from '@hedgedoc/commons';
|
} from '@hedgedoc/commons';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
|
@ -81,7 +82,8 @@ export class MockConnectionBuilder {
|
||||||
public build(): RealtimeConnection {
|
public build(): RealtimeConnection {
|
||||||
const displayName = this.deriveDisplayName();
|
const displayName = this.deriveDisplayName();
|
||||||
|
|
||||||
const transporter = new MockedBackendMessageTransporter('');
|
const transporter = new MessageTransporter();
|
||||||
|
transporter.setAdapter(new MockedBackendTransportAdapter(''));
|
||||||
const realtimeUserStateAdapter: RealtimeUserStatusAdapter =
|
const realtimeUserStateAdapter: RealtimeUserStatusAdapter =
|
||||||
this.includeRealtimeUserStatus === RealtimeUserState.WITHOUT
|
this.includeRealtimeUserStatus === RealtimeUserState.WITHOUT
|
||||||
? Mock.of<RealtimeUserStatusAdapter>({})
|
? Mock.of<RealtimeUserStatusAdapter>({})
|
||||||
|
|
129
backend/src/realtime/websocket/backend-websocket-adapter.spec.ts
Normal file
129
backend/src/realtime/websocket/backend-websocket-adapter.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
71
backend/src/realtime/websocket/backend-websocket-adapter.ts
Normal file
71
backend/src/realtime/websocket/backend-websocket-adapter.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,9 +4,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
MessageTransporter,
|
||||||
NotePermissions,
|
NotePermissions,
|
||||||
userCanEdit,
|
userCanEdit,
|
||||||
WebsocketTransporter,
|
|
||||||
} from '@hedgedoc/commons';
|
} from '@hedgedoc/commons';
|
||||||
import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
|
import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
|
||||||
import { IncomingMessage } from 'http';
|
import { IncomingMessage } from 'http';
|
||||||
|
@ -21,6 +21,7 @@ import { User } from '../../users/user.entity';
|
||||||
import { UsersService } from '../../users/users.service';
|
import { UsersService } from '../../users/users.service';
|
||||||
import { RealtimeConnection } from '../realtime-note/realtime-connection';
|
import { RealtimeConnection } from '../realtime-note/realtime-connection';
|
||||||
import { RealtimeNoteService } from '../realtime-note/realtime-note.service';
|
import { RealtimeNoteService } from '../realtime-note/realtime-note.service';
|
||||||
|
import { BackendWebsocketAdapter } from './backend-websocket-adapter';
|
||||||
import { extractNoteIdFromRequestUrl } from './utils/extract-note-id-from-request-url';
|
import { extractNoteIdFromRequestUrl } from './utils/extract-note-id-from-request-url';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,7 +86,7 @@ export class WebsocketGateway implements OnGatewayConnection {
|
||||||
const realtimeNote =
|
const realtimeNote =
|
||||||
await this.realtimeNoteService.getOrCreateRealtimeNote(note);
|
await this.realtimeNoteService.getOrCreateRealtimeNote(note);
|
||||||
|
|
||||||
const websocketTransporter = new WebsocketTransporter();
|
const websocketTransporter = new MessageTransporter();
|
||||||
const permissions = await this.noteService.toNotePermissionsDto(note);
|
const permissions = await this.noteService.toNotePermissionsDto(note);
|
||||||
const acceptEdits: boolean = userCanEdit(
|
const acceptEdits: boolean = userCanEdit(
|
||||||
permissions as NotePermissions,
|
permissions as NotePermissions,
|
||||||
|
@ -97,7 +98,9 @@ export class WebsocketGateway implements OnGatewayConnection {
|
||||||
realtimeNote,
|
realtimeNote,
|
||||||
acceptEdits,
|
acceptEdits,
|
||||||
);
|
);
|
||||||
websocketTransporter.setWebsocket(clientSocket);
|
websocketTransporter.setAdapter(
|
||||||
|
new BackendWebsocketAdapter(clientSocket),
|
||||||
|
);
|
||||||
|
|
||||||
realtimeNote.addClient(connection);
|
realtimeNote.addClient(connection);
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domhandler": "5.0.3",
|
"domhandler": "5.0.3",
|
||||||
"eventemitter2": "6.4.9",
|
"eventemitter2": "6.4.9",
|
||||||
"isomorphic-ws": "5.0.0",
|
|
||||||
"joi": "17.9.2",
|
"joi": "17.9.2",
|
||||||
"reveal.js": "4.5.0",
|
"reveal.js": "4.5.0",
|
||||||
"ws": "8.13.0",
|
"ws": "8.13.0",
|
||||||
|
|
|
@ -12,3 +12,4 @@ export * from './parse-url/index.js'
|
||||||
export * from './permissions/index.js'
|
export * from './permissions/index.js'
|
||||||
export * from './title-extraction/index.js'
|
export * from './title-extraction/index.js'
|
||||||
export * from './y-doc-sync/index.js'
|
export * from './y-doc-sync/index.js'
|
||||||
|
export * from './utils/index.js'
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './mocked-backend-message-transporter.js'
|
|
||||||
export * from './message.js'
|
export * from './message.js'
|
||||||
export * from './message-transporter.js'
|
export * from './message-transporter.js'
|
||||||
export * from './realtime-user.js'
|
export * from './realtime-user.js'
|
||||||
export * from './websocket-transporter.js'
|
export * from './transport-adapter.js'
|
||||||
|
export * from './mocked-backend-transport-adapter.js'
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { Message, MessagePayloads, MessageType } from './message.js'
|
import { Message, MessagePayloads, MessageType } from './message.js'
|
||||||
|
import { TransportAdapter } from './transport-adapter.js'
|
||||||
import { EventEmitter2, Listener } from 'eventemitter2'
|
import { EventEmitter2, Listener } from 'eventemitter2'
|
||||||
|
|
||||||
export type MessageEvents = MessageType | 'connected' | 'disconnected'
|
export type MessageEvents = MessageType | 'connected' | 'disconnected'
|
||||||
|
@ -15,18 +16,60 @@ type MessageEventPayloadMap = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConnectionState {
|
export enum ConnectionState {
|
||||||
DISCONNECT,
|
DISCONNECTED = 'DISCONNECTED',
|
||||||
CONNECTING,
|
CONNECTING = 'CONNECTING',
|
||||||
CONNECTED
|
CONNECTED = 'CONNECTED'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for event based message communication.
|
* Coordinates the sending, receiving and handling of messages for realtime communication.
|
||||||
*/
|
*/
|
||||||
export abstract class MessageTransporter extends EventEmitter2<MessageEventPayloadMap> {
|
export class MessageTransporter extends EventEmitter2<MessageEventPayloadMap> {
|
||||||
|
private transportAdapter: TransportAdapter | undefined
|
||||||
private readyMessageReceived = false
|
private readyMessageReceived = false
|
||||||
|
private destroyOnMessageEventHandler: undefined | (() => void)
|
||||||
|
private destroyOnErrorEventHandler: undefined | (() => void)
|
||||||
|
private destroyOnCloseEventHandler: undefined | (() => void)
|
||||||
|
private destroyOnConnectedEventHandler: undefined | (() => void)
|
||||||
|
|
||||||
public abstract sendMessage<M extends MessageType>(content: Message<M>): void
|
public sendMessage<M extends MessageType>(content: Message<M>): void {
|
||||||
|
if (!this.isConnected()) {
|
||||||
|
this.onDisconnecting()
|
||||||
|
console.debug(
|
||||||
|
"Can't send message over closed connection. Triggering onDisconencted event. Message that couldn't be sent was",
|
||||||
|
content
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.transportAdapter === undefined) {
|
||||||
|
throw new Error('no transport adapter set')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.transportAdapter.send(content)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.disconnect()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAdapter(websocket: TransportAdapter) {
|
||||||
|
if (websocket.getConnectionState() !== ConnectionState.CONNECTED) {
|
||||||
|
throw new Error('Websocket must be connected')
|
||||||
|
}
|
||||||
|
this.unbindEventsFromPreviousWebsocket()
|
||||||
|
this.transportAdapter = websocket
|
||||||
|
this.bindWebsocketEvents(websocket)
|
||||||
|
|
||||||
|
if (this.isConnected()) {
|
||||||
|
this.onConnected()
|
||||||
|
} else {
|
||||||
|
this.destroyOnConnectedEventHandler = websocket.bindOnConnectedEvent(
|
||||||
|
this.onConnected.bind(this)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected receiveMessage<L extends MessageType>(message: Message<L>): void {
|
protected receiveMessage<L extends MessageType>(message: Message<L>): void {
|
||||||
if (message.type === MessageType.READY) {
|
if (message.type === MessageType.READY) {
|
||||||
|
@ -35,21 +78,53 @@ export abstract class MessageTransporter extends EventEmitter2<MessageEventPaylo
|
||||||
this.emit(message.type, message)
|
this.emit(message.type, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendReady(): void {
|
public disconnect(): void {
|
||||||
this.sendMessage({
|
this.transportAdapter?.disconnect()
|
||||||
type: MessageType.READY
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract disconnect(): void
|
public getConnectionState(): ConnectionState {
|
||||||
|
return (
|
||||||
|
this.transportAdapter?.getConnectionState() ??
|
||||||
|
ConnectionState.DISCONNECTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public abstract getConnectionState(): ConnectionState
|
private unbindEventsFromPreviousWebsocket() {
|
||||||
|
if (this.transportAdapter) {
|
||||||
|
this.destroyOnMessageEventHandler?.()
|
||||||
|
this.destroyOnCloseEventHandler?.()
|
||||||
|
this.destroyOnErrorEventHandler?.()
|
||||||
|
|
||||||
|
this.destroyOnMessageEventHandler = undefined
|
||||||
|
this.destroyOnCloseEventHandler = undefined
|
||||||
|
this.destroyOnErrorEventHandler = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindWebsocketEvents(websocket: TransportAdapter) {
|
||||||
|
this.destroyOnErrorEventHandler = websocket.bindOnErrorEvent(
|
||||||
|
this.onDisconnecting.bind(this)
|
||||||
|
)
|
||||||
|
this.destroyOnCloseEventHandler = websocket.bindOnCloseEvent(
|
||||||
|
this.onDisconnecting.bind(this)
|
||||||
|
)
|
||||||
|
this.destroyOnMessageEventHandler = websocket.bindOnMessageEvent(
|
||||||
|
this.receiveMessage.bind(this)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
protected onConnected(): void {
|
protected onConnected(): void {
|
||||||
|
this.destroyOnConnectedEventHandler?.()
|
||||||
|
this.destroyOnConnectedEventHandler = undefined
|
||||||
this.emit('connected')
|
this.emit('connected')
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onDisconnecting(): void {
|
protected onDisconnecting(): void {
|
||||||
|
if (this.transportAdapter === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.unbindEventsFromPreviousWebsocket()
|
||||||
|
this.transportAdapter = undefined
|
||||||
this.readyMessageReceived = false
|
this.readyMessageReceived = false
|
||||||
this.emit('disconnected')
|
this.emit('disconnected')
|
||||||
}
|
}
|
||||||
|
@ -99,4 +174,10 @@ export abstract class MessageTransporter extends EventEmitter2<MessageEventPaylo
|
||||||
objectify: true
|
objectify: true
|
||||||
}) as Listener
|
}) as Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sendReady(): void {
|
||||||
|
this.sendMessage({
|
||||||
|
type: MessageType.READY
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { RealtimeDoc } from '../y-doc-sync/realtime-doc.js'
|
|
||||||
import { ConnectionState, MessageTransporter } from './message-transporter.js'
|
|
||||||
import { Message, MessageType } from './message.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A mocked connection that doesn't send or receive any data and is instantly ready.
|
|
||||||
* The only exception is the note content state request that is answered with the given initial content.
|
|
||||||
*/
|
|
||||||
export class MockedBackendMessageTransporter extends MessageTransporter {
|
|
||||||
private readonly doc: RealtimeDoc
|
|
||||||
|
|
||||||
private connected = true
|
|
||||||
|
|
||||||
constructor(initialContent: string) {
|
|
||||||
super()
|
|
||||||
this.doc = new RealtimeDoc(initialContent)
|
|
||||||
|
|
||||||
this.onConnected()
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect(): void {
|
|
||||||
if (!this.connected) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.connected = false
|
|
||||||
this.onDisconnecting()
|
|
||||||
}
|
|
||||||
|
|
||||||
sendReady() {
|
|
||||||
this.receiveMessage({
|
|
||||||
type: MessageType.READY
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessage<M extends MessageType>(content: Message<M>) {
|
|
||||||
if (content.type === MessageType.NOTE_CONTENT_STATE_REQUEST) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.receiveMessage({
|
|
||||||
type: MessageType.NOTE_CONTENT_UPDATE,
|
|
||||||
payload: this.doc.encodeStateAsUpdate(content.payload)
|
|
||||||
})
|
|
||||||
}, 10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getConnectionState(): ConnectionState {
|
|
||||||
return this.connected
|
|
||||||
? ConnectionState.CONNECTED
|
|
||||||
: ConnectionState.DISCONNECT
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { RealtimeDoc } from '../y-doc-sync/index.js'
|
||||||
|
import { ConnectionState } from './message-transporter.js'
|
||||||
|
import { Message, MessageType } from './message.js'
|
||||||
|
import { TransportAdapter } from './transport-adapter.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a transport adapter that simulates a connection with a real HedgeDoc realtime backend.
|
||||||
|
*/
|
||||||
|
export class MockedBackendTransportAdapter implements TransportAdapter {
|
||||||
|
private readonly doc: RealtimeDoc
|
||||||
|
|
||||||
|
private connected = true
|
||||||
|
|
||||||
|
private closeHandler: undefined | (() => void)
|
||||||
|
|
||||||
|
private messageHandler: undefined | ((value: Message<MessageType>) => void)
|
||||||
|
|
||||||
|
constructor(initialContent: string) {
|
||||||
|
this.doc = new RealtimeDoc(initialContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindOnCloseEvent(handler: () => void): () => void {
|
||||||
|
this.closeHandler = handler
|
||||||
|
return () => {
|
||||||
|
this.connected = false
|
||||||
|
this.closeHandler = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bindOnConnectedEvent(handler: () => void): () => void {
|
||||||
|
handler()
|
||||||
|
return () => {
|
||||||
|
//empty on purpose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bindOnErrorEvent(): () => void {
|
||||||
|
return () => {
|
||||||
|
//empty on purpose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bindOnMessageEvent(
|
||||||
|
handler: (value: Message<MessageType>) => void
|
||||||
|
): () => void {
|
||||||
|
this.messageHandler = handler
|
||||||
|
return () => {
|
||||||
|
this.messageHandler = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): void {
|
||||||
|
if (!this.connected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.connected = false
|
||||||
|
this.closeHandler?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
getConnectionState(): ConnectionState {
|
||||||
|
return this.connected
|
||||||
|
? ConnectionState.CONNECTED
|
||||||
|
: ConnectionState.DISCONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
send(value: Message<MessageType>): void {
|
||||||
|
if (value.type === MessageType.NOTE_CONTENT_STATE_REQUEST) {
|
||||||
|
new Promise(() => {
|
||||||
|
this.messageHandler?.({
|
||||||
|
type: MessageType.NOTE_CONTENT_UPDATE,
|
||||||
|
payload: this.doc.encodeStateAsUpdate(value.payload)
|
||||||
|
})
|
||||||
|
}).catch((error: Error) => console.error(error))
|
||||||
|
} else if (value.type === MessageType.READY) {
|
||||||
|
new Promise(() => {
|
||||||
|
this.messageHandler?.({
|
||||||
|
type: MessageType.READY
|
||||||
|
})
|
||||||
|
}).catch((error: Error) => console.error(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
commons/src/message-transporters/transport-adapter.ts
Normal file
26
commons/src/message-transporters/transport-adapter.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { ConnectionState } from './message-transporter.js'
|
||||||
|
import { Message, MessageType } from './message.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines methods that must be implemented to send and receive messages using an {@link AdapterMessageTransporter}.
|
||||||
|
*/
|
||||||
|
export interface TransportAdapter {
|
||||||
|
getConnectionState(): ConnectionState
|
||||||
|
|
||||||
|
bindOnMessageEvent(handler: (value: Message<MessageType>) => void): () => void
|
||||||
|
|
||||||
|
bindOnConnectedEvent(handler: () => void): () => void
|
||||||
|
|
||||||
|
bindOnErrorEvent(handler: () => void): () => void
|
||||||
|
|
||||||
|
bindOnCloseEvent(handler: () => void): () => void
|
||||||
|
|
||||||
|
disconnect(): void
|
||||||
|
|
||||||
|
send(value: Message<MessageType>): void
|
||||||
|
}
|
|
@ -1,111 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { ConnectionState, MessageTransporter } from './message-transporter.js'
|
|
||||||
import { Message, MessageType } from './message.js'
|
|
||||||
import WebSocket, { MessageEvent } from 'isomorphic-ws'
|
|
||||||
|
|
||||||
export class WebsocketTransporter extends MessageTransporter {
|
|
||||||
private websocket: WebSocket | undefined
|
|
||||||
|
|
||||||
private messageCallback: undefined | ((event: MessageEvent) => void)
|
|
||||||
private closeCallback: undefined | (() => void)
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
public setWebsocket(websocket: WebSocket) {
|
|
||||||
if (
|
|
||||||
websocket.readyState === WebSocket.CLOSED ||
|
|
||||||
websocket.readyState === WebSocket.CLOSING
|
|
||||||
) {
|
|
||||||
throw new Error('Websocket must be open')
|
|
||||||
}
|
|
||||||
this.undbindEventsFromPreviousWebsocket()
|
|
||||||
this.websocket = websocket
|
|
||||||
this.bindWebsocketEvents(websocket)
|
|
||||||
|
|
||||||
if (this.isConnected()) {
|
|
||||||
this.onConnected()
|
|
||||||
} else {
|
|
||||||
this.websocket.addEventListener('open', this.onConnected.bind(this))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private undbindEventsFromPreviousWebsocket() {
|
|
||||||
if (this.websocket) {
|
|
||||||
if (this.messageCallback) {
|
|
||||||
this.websocket.removeEventListener('message', this.messageCallback)
|
|
||||||
}
|
|
||||||
if (this.closeCallback) {
|
|
||||||
this.websocket.removeEventListener('error', this.closeCallback)
|
|
||||||
this.websocket.removeEventListener('close', this.closeCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bindWebsocketEvents(websocket: WebSocket) {
|
|
||||||
this.messageCallback = this.processMessageEvent.bind(this)
|
|
||||||
this.closeCallback = this.onDisconnecting.bind(this)
|
|
||||||
|
|
||||||
websocket.addEventListener('message', this.messageCallback)
|
|
||||||
websocket.addEventListener('error', this.closeCallback)
|
|
||||||
websocket.addEventListener('close', this.closeCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private processMessageEvent(event: MessageEvent): void {
|
|
||||||
if (typeof event.data !== 'string') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const message = JSON.parse(event.data) as Message<MessageType>
|
|
||||||
this.receiveMessage(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnect(): void {
|
|
||||||
this.websocket?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onDisconnecting() {
|
|
||||||
if (this.websocket === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.undbindEventsFromPreviousWebsocket()
|
|
||||||
this.websocket = undefined
|
|
||||||
super.onDisconnecting()
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendMessage(content: Message<MessageType>): void {
|
|
||||||
if (!this.isConnected()) {
|
|
||||||
this.onDisconnecting()
|
|
||||||
console.debug(
|
|
||||||
"Can't send message over closed connection. Triggering onDisconencted event. Message that couldn't be sent was",
|
|
||||||
content
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.websocket === undefined) {
|
|
||||||
throw new Error('websocket transporter has no websocket connection')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.websocket.send(JSON.stringify(content))
|
|
||||||
} catch (error: unknown) {
|
|
||||||
this.disconnect()
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getConnectionState(): ConnectionState {
|
|
||||||
if (this.websocket?.readyState === WebSocket.OPEN) {
|
|
||||||
return ConnectionState.CONNECTED
|
|
||||||
} else if (this.websocket?.readyState === WebSocket.CONNECTING) {
|
|
||||||
return ConnectionState.CONNECTING
|
|
||||||
} else {
|
|
||||||
return ConnectionState.DISCONNECT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
7
commons/src/utils/index.ts
Normal file
7
commons/src/utils/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './wait-for-other-promises-to-finish.js'
|
17
commons/src/utils/wait-for-other-promises-to-finish.ts
Normal file
17
commons/src/utils/wait-for-other-promises-to-finish.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits until all other pending promises are processed.
|
||||||
|
*
|
||||||
|
* NodeJS has a queue for async code that waits for being processed. This method adds a promise to the very end of this queue.
|
||||||
|
* If the promise is resolved then this means that all other promises before it have been processed as well.
|
||||||
|
*
|
||||||
|
* @return A promise which resolves when all other promises have been processed
|
||||||
|
*/
|
||||||
|
export function waitForOtherPromisesToFinish(): Promise<void> {
|
||||||
|
return new Promise((resolve) => process.nextTick(resolve))
|
||||||
|
}
|
|
@ -47,6 +47,6 @@ export class InMemoryConnectionMessageTransporter extends MessageTransporter {
|
||||||
getConnectionState(): ConnectionState {
|
getConnectionState(): ConnectionState {
|
||||||
return this.otherSide !== undefined
|
return this.otherSide !== undefined
|
||||||
? ConnectionState.CONNECTED
|
? ConnectionState.CONNECTED
|
||||||
: ConnectionState.DISCONNECT
|
: ConnectionState.DISCONNECTED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,6 @@
|
||||||
"i18next": "22.5.0",
|
"i18next": "22.5.0",
|
||||||
"i18next-browser-languagedetector": "7.0.2",
|
"i18next-browser-languagedetector": "7.0.2",
|
||||||
"i18next-resources-to-backend": "1.1.4",
|
"i18next-resources-to-backend": "1.1.4",
|
||||||
"isomorphic-ws": "5.0.0",
|
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"katex": "0.16.7",
|
"katex": "0.16.7",
|
||||||
"luxon": "3.3.0",
|
"luxon": "3.3.0",
|
||||||
|
|
|
@ -8,13 +8,10 @@ import type { Note, NoteMetadata } from '../../../api/notes/types'
|
||||||
import * as useSingleStringUrlParameterModule from '../../../hooks/common/use-single-string-url-parameter'
|
import * as useSingleStringUrlParameterModule from '../../../hooks/common/use-single-string-url-parameter'
|
||||||
import { mockI18n } from '../../../test-utils/mock-i18n'
|
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||||
import { CreateNonExistingNoteHint } from './create-non-existing-note-hint'
|
import { CreateNonExistingNoteHint } from './create-non-existing-note-hint'
|
||||||
|
import { waitForOtherPromisesToFinish } from '@hedgedoc/commons'
|
||||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||||
import { Mock } from 'ts-mockery'
|
import { Mock } from 'ts-mockery'
|
||||||
|
|
||||||
function waitForOtherPromisesToFinish(): Promise<void> {
|
|
||||||
return new Promise((resolve) => process.nextTick(resolve))
|
|
||||||
}
|
|
||||||
|
|
||||||
jest.mock('../../../api/notes')
|
jest.mock('../../../api/notes')
|
||||||
jest.mock('../../../hooks/common/use-single-string-url-parameter')
|
jest.mock('../../../hooks/common/use-single-string-url-parameter')
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { FrontendWebsocketAdapter } from './frontend-websocket-adapter'
|
||||||
|
import type { Message } from '@hedgedoc/commons'
|
||||||
|
import { ConnectionState, MessageType } from '@hedgedoc/commons'
|
||||||
|
import { Mock } from 'ts-mockery'
|
||||||
|
|
||||||
|
describe('frontend websocket', () => {
|
||||||
|
let addEventListenerSpy: jest.Mock
|
||||||
|
let removeEventListenerSpy: jest.Mock
|
||||||
|
let closeSpy: jest.Mock
|
||||||
|
let sendSpy: jest.Mock
|
||||||
|
let adapter: FrontendWebsocketAdapter
|
||||||
|
let mockedSocket: WebSocket
|
||||||
|
|
||||||
|
function mockSocket(readyState: 0 | 1 | 2 | 3 = WebSocket.OPEN) {
|
||||||
|
addEventListenerSpy = jest.fn()
|
||||||
|
removeEventListenerSpy = jest.fn()
|
||||||
|
closeSpy = jest.fn()
|
||||||
|
sendSpy = jest.fn()
|
||||||
|
|
||||||
|
mockedSocket = Mock.of<WebSocket>({
|
||||||
|
addEventListener: addEventListenerSpy,
|
||||||
|
removeEventListener: removeEventListenerSpy,
|
||||||
|
close: closeSpy,
|
||||||
|
send: sendSpy,
|
||||||
|
readyState: readyState
|
||||||
|
})
|
||||||
|
adapter = new FrontendWebsocketAdapter(mockedSocket)
|
||||||
|
}
|
||||||
|
|
||||||
|
it('can bind and unbind the close event', () => {
|
||||||
|
mockSocket()
|
||||||
|
const handler = jest.fn()
|
||||||
|
const unbind = adapter.bindOnCloseEvent(handler)
|
||||||
|
expect(addEventListenerSpy).toHaveBeenCalledWith('close', handler)
|
||||||
|
unbind()
|
||||||
|
expect(removeEventListenerSpy).toHaveBeenCalledWith('close', handler)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can bind and unbind the connect event', () => {
|
||||||
|
mockSocket()
|
||||||
|
const handler = jest.fn()
|
||||||
|
const unbind = adapter.bindOnConnectedEvent(handler)
|
||||||
|
expect(addEventListenerSpy).toHaveBeenCalledWith('open', handler)
|
||||||
|
unbind()
|
||||||
|
expect(removeEventListenerSpy).toHaveBeenCalledWith('open', handler)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can bind and unbind the error event', () => {
|
||||||
|
mockSocket()
|
||||||
|
const handler = jest.fn()
|
||||||
|
const unbind = adapter.bindOnErrorEvent(handler)
|
||||||
|
expect(addEventListenerSpy).toHaveBeenCalledWith('error', handler)
|
||||||
|
unbind()
|
||||||
|
expect(removeEventListenerSpy).toHaveBeenCalledWith('error', handler)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can bind, unbind and translate the message event', () => {
|
||||||
|
mockSocket()
|
||||||
|
const handler = jest.fn()
|
||||||
|
|
||||||
|
let modifiedHandler: EventListenerOrEventListenerObject = jest.fn()
|
||||||
|
jest.spyOn(mockedSocket, 'addEventListener').mockImplementation((event, handler_) => {
|
||||||
|
modifiedHandler = handler_
|
||||||
|
})
|
||||||
|
|
||||||
|
const unbind = adapter.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(addEventListenerSpy).toHaveBeenCalledWith('message', modifiedHandler)
|
||||||
|
unbind()
|
||||||
|
expect(removeEventListenerSpy).toHaveBeenCalledWith('message', modifiedHandler)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can disconnect the socket', () => {
|
||||||
|
mockSocket()
|
||||||
|
adapter.disconnect()
|
||||||
|
expect(closeSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can send messages', () => {
|
||||||
|
mockSocket()
|
||||||
|
const value: Message<MessageType> = { type: MessageType.READY }
|
||||||
|
adapter.send(value)
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith('{"type":"READY"}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can read the connection state when open', () => {
|
||||||
|
mockSocket(WebSocket.OPEN)
|
||||||
|
expect(adapter.getConnectionState()).toBe(ConnectionState.CONNECTED)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can read the connection state when connecting', () => {
|
||||||
|
mockSocket(WebSocket.CONNECTING)
|
||||||
|
expect(adapter.getConnectionState()).toBe(ConnectionState.CONNECTING)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can read the connection state when closing', () => {
|
||||||
|
mockSocket(WebSocket.CLOSING)
|
||||||
|
expect(adapter.getConnectionState()).toBe(ConnectionState.DISCONNECTED)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can read the connection state when closed', () => {
|
||||||
|
mockSocket(WebSocket.CLOSED)
|
||||||
|
expect(adapter.getConnectionState()).toBe(ConnectionState.DISCONNECTED)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { ConnectionState } from '@hedgedoc/commons'
|
||||||
|
import type { TransportAdapter } from '@hedgedoc/commons'
|
||||||
|
import type { Message, MessageType } from '@hedgedoc/commons/dist'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a transport adapter that communicates using a browser websocket.
|
||||||
|
*/
|
||||||
|
export class FrontendWebsocketAdapter 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 processStringAsMessage(message: MessageEvent): void {
|
||||||
|
if (typeof message.data !== 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(JSON.parse(message.data) as Message<MessageType>)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.addEventListener('message', processStringAsMessage)
|
||||||
|
return () => {
|
||||||
|
this.socket.removeEventListener('message', processStringAsMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,11 +8,10 @@ import { getGlobalState } from '../../../../../redux'
|
||||||
import { setRealtimeConnectionState } from '../../../../../redux/realtime/methods'
|
import { setRealtimeConnectionState } from '../../../../../redux/realtime/methods'
|
||||||
import { Logger } from '../../../../../utils/logger'
|
import { Logger } from '../../../../../utils/logger'
|
||||||
import { isMockMode } from '../../../../../utils/test-modes'
|
import { isMockMode } from '../../../../../utils/test-modes'
|
||||||
|
import { FrontendWebsocketAdapter } from './frontend-websocket-adapter'
|
||||||
import { useWebsocketUrl } from './use-websocket-url'
|
import { useWebsocketUrl } from './use-websocket-url'
|
||||||
import type { MessageTransporter } from '@hedgedoc/commons'
|
import { MessageTransporter, MockedBackendTransportAdapter } from '@hedgedoc/commons'
|
||||||
import { MockedBackendMessageTransporter, WebsocketTransporter } from '@hedgedoc/commons'
|
|
||||||
import type { Listener } from 'eventemitter2'
|
import type { Listener } from 'eventemitter2'
|
||||||
import WebSocket from 'isomorphic-ws'
|
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
|
|
||||||
const logger = new Logger('websocket connection')
|
const logger = new Logger('websocket connection')
|
||||||
|
@ -20,28 +19,25 @@ const WEBSOCKET_RECONNECT_INTERVAL = 2000
|
||||||
const WEBSOCKET_RECONNECT_MAX_DURATION = 5000
|
const WEBSOCKET_RECONNECT_MAX_DURATION = 5000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link WebsocketTransporter websocket message transporter} that handles the realtime communication with the backend.
|
* Creates a {@link MessageTransporter message transporter} that handles the realtime communication with the backend.
|
||||||
*
|
*
|
||||||
* @return the created connection handler
|
* @return the created connection handler
|
||||||
*/
|
*/
|
||||||
export const useRealtimeConnection = (): MessageTransporter => {
|
export const useRealtimeConnection = (): MessageTransporter => {
|
||||||
const websocketUrl = useWebsocketUrl()
|
const websocketUrl = useWebsocketUrl()
|
||||||
const messageTransporter = useMemo(() => {
|
const messageTransporter = useMemo(() => new MessageTransporter(), [])
|
||||||
if (isMockMode) {
|
|
||||||
logger.debug('Creating Loopback connection...')
|
|
||||||
return new MockedBackendMessageTransporter(getGlobalState().noteDetails.markdownContent.plain)
|
|
||||||
} else {
|
|
||||||
logger.debug('Creating Websocket connection...')
|
|
||||||
return new WebsocketTransporter()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const reconnectCount = useRef(0)
|
const reconnectCount = useRef(0)
|
||||||
|
|
||||||
const establishWebsocketConnection = useCallback(() => {
|
const establishWebsocketConnection = useCallback(() => {
|
||||||
if (messageTransporter instanceof WebsocketTransporter && websocketUrl) {
|
if (isMockMode) {
|
||||||
|
logger.debug('Creating Loopback connection...')
|
||||||
|
messageTransporter.setAdapter(
|
||||||
|
new MockedBackendTransportAdapter(getGlobalState().noteDetails.markdownContent.plain)
|
||||||
|
)
|
||||||
|
} else if (websocketUrl) {
|
||||||
logger.debug(`Connecting to ${websocketUrl.toString()}`)
|
logger.debug(`Connecting to ${websocketUrl.toString()}`)
|
||||||
const socket = new WebSocket(websocketUrl)
|
|
||||||
|
const socket = new WebSocket(websocketUrl.toString())
|
||||||
socket.addEventListener('error', () => {
|
socket.addEventListener('error', () => {
|
||||||
const timeout = WEBSOCKET_RECONNECT_INTERVAL + reconnectCount.current * 1000 + Math.random() * 1000
|
const timeout = WEBSOCKET_RECONNECT_INTERVAL + reconnectCount.current * 1000 + Math.random() * 1000
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -50,7 +46,7 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
||||||
}, Math.max(timeout, WEBSOCKET_RECONNECT_MAX_DURATION))
|
}, Math.max(timeout, WEBSOCKET_RECONNECT_MAX_DURATION))
|
||||||
})
|
})
|
||||||
socket.addEventListener('open', () => {
|
socket.addEventListener('open', () => {
|
||||||
messageTransporter.setWebsocket(socket)
|
messageTransporter.setAdapter(new FrontendWebsocketAdapter(socket))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [messageTransporter, websocketUrl])
|
}, [messageTransporter, websocketUrl])
|
||||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -2384,7 +2384,6 @@ __metadata:
|
||||||
eslint-plugin-jest: 27.2.1
|
eslint-plugin-jest: 27.2.1
|
||||||
eslint-plugin-prettier: 4.2.1
|
eslint-plugin-prettier: 4.2.1
|
||||||
eventemitter2: 6.4.9
|
eventemitter2: 6.4.9
|
||||||
isomorphic-ws: 5.0.0
|
|
||||||
jest: 29.5.0
|
jest: 29.5.0
|
||||||
joi: 17.9.2
|
joi: 17.9.2
|
||||||
microbundle: 0.15.1
|
microbundle: 0.15.1
|
||||||
|
@ -2487,7 +2486,6 @@ __metadata:
|
||||||
i18next: 22.5.0
|
i18next: 22.5.0
|
||||||
i18next-browser-languagedetector: 7.0.2
|
i18next-browser-languagedetector: 7.0.2
|
||||||
i18next-resources-to-backend: 1.1.4
|
i18next-resources-to-backend: 1.1.4
|
||||||
isomorphic-ws: 5.0.0
|
|
||||||
jest: 29.5.0
|
jest: 29.5.0
|
||||||
jest-environment-jsdom: 29.5.0
|
jest-environment-jsdom: 29.5.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
|
@ -11625,15 +11623,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"isomorphic-ws@npm:5.0.0":
|
|
||||||
version: 5.0.0
|
|
||||||
resolution: "isomorphic-ws@npm:5.0.0"
|
|
||||||
peerDependencies:
|
|
||||||
ws: "*"
|
|
||||||
checksum: e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"isomorphic.js@npm:^0.2.4":
|
"isomorphic.js@npm:^0.2.4":
|
||||||
version: 0.2.5
|
version: 0.2.5
|
||||||
resolution: "isomorphic.js@npm:0.2.5"
|
resolution: "isomorphic.js@npm:0.2.5"
|
||||||
|
|
Loading…
Reference in a new issue