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

@ -8,13 +8,10 @@ import type { Note, NoteMetadata } from '../../../api/notes/types'
import * as useSingleStringUrlParameterModule from '../../../hooks/common/use-single-string-url-parameter'
import { mockI18n } from '../../../test-utils/mock-i18n'
import { CreateNonExistingNoteHint } from './create-non-existing-note-hint'
import { waitForOtherPromisesToFinish } from '@hedgedoc/commons'
import { act, render, screen, waitFor } from '@testing-library/react'
import { Mock } from 'ts-mockery'
function waitForOtherPromisesToFinish(): Promise<void> {
return new Promise((resolve) => process.nextTick(resolve))
}
jest.mock('../../../api/notes')
jest.mock('../../../hooks/common/use-single-string-url-parameter')

View file

@ -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)
})
})

View file

@ -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))
}
}

View file

@ -8,11 +8,10 @@ import { getGlobalState } from '../../../../../redux'
import { setRealtimeConnectionState } from '../../../../../redux/realtime/methods'
import { Logger } from '../../../../../utils/logger'
import { isMockMode } from '../../../../../utils/test-modes'
import { FrontendWebsocketAdapter } from './frontend-websocket-adapter'
import { useWebsocketUrl } from './use-websocket-url'
import type { MessageTransporter } from '@hedgedoc/commons'
import { MockedBackendMessageTransporter, WebsocketTransporter } from '@hedgedoc/commons'
import { MessageTransporter, MockedBackendTransportAdapter } from '@hedgedoc/commons'
import type { Listener } from 'eventemitter2'
import WebSocket from 'isomorphic-ws'
import { useCallback, useEffect, useMemo, useRef } from 'react'
const logger = new Logger('websocket connection')
@ -20,28 +19,25 @@ const WEBSOCKET_RECONNECT_INTERVAL = 2000
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
*/
export const useRealtimeConnection = (): MessageTransporter => {
const websocketUrl = useWebsocketUrl()
const messageTransporter = useMemo(() => {
if (isMockMode) {
logger.debug('Creating Loopback connection...')
return new MockedBackendMessageTransporter(getGlobalState().noteDetails.markdownContent.plain)
} else {
logger.debug('Creating Websocket connection...')
return new WebsocketTransporter()
}
}, [])
const messageTransporter = useMemo(() => new MessageTransporter(), [])
const reconnectCount = useRef(0)
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()}`)
const socket = new WebSocket(websocketUrl)
const socket = new WebSocket(websocketUrl.toString())
socket.addEventListener('error', () => {
const timeout = WEBSOCKET_RECONNECT_INTERVAL + reconnectCount.current * 1000 + Math.random() * 1000
setTimeout(() => {
@ -50,7 +46,7 @@ export const useRealtimeConnection = (): MessageTransporter => {
}, Math.max(timeout, WEBSOCKET_RECONNECT_MAX_DURATION))
})
socket.addEventListener('open', () => {
messageTransporter.setWebsocket(socket)
messageTransporter.setAdapter(new FrontendWebsocketAdapter(socket))
})
}
}, [messageTransporter, websocketUrl])