mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-17 16:44:49 -04: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
|
@ -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')
|
||||
|
||||
|
|
|
@ -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 { 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])
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue