mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-24 20:14:35 -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
|
@ -12,3 +12,4 @@ export * from './parse-url/index.js'
|
|||
export * from './permissions/index.js'
|
||||
export * from './title-extraction/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
|
||||
*/
|
||||
|
||||
export * from './mocked-backend-message-transporter.js'
|
||||
export * from './message.js'
|
||||
export * from './message-transporter.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
|
||||
*/
|
||||
import { Message, MessagePayloads, MessageType } from './message.js'
|
||||
import { TransportAdapter } from './transport-adapter.js'
|
||||
import { EventEmitter2, Listener } from 'eventemitter2'
|
||||
|
||||
export type MessageEvents = MessageType | 'connected' | 'disconnected'
|
||||
|
@ -15,18 +16,60 @@ type MessageEventPayloadMap = {
|
|||
}
|
||||
|
||||
export enum ConnectionState {
|
||||
DISCONNECT,
|
||||
CONNECTING,
|
||||
CONNECTED
|
||||
DISCONNECTED = 'DISCONNECTED',
|
||||
CONNECTING = 'CONNECTING',
|
||||
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 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 {
|
||||
if (message.type === MessageType.READY) {
|
||||
|
@ -35,21 +78,53 @@ export abstract class MessageTransporter extends EventEmitter2<MessageEventPaylo
|
|||
this.emit(message.type, message)
|
||||
}
|
||||
|
||||
public sendReady(): void {
|
||||
this.sendMessage({
|
||||
type: MessageType.READY
|
||||
})
|
||||
public disconnect(): void {
|
||||
this.transportAdapter?.disconnect()
|
||||
}
|
||||
|
||||
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 {
|
||||
this.destroyOnConnectedEventHandler?.()
|
||||
this.destroyOnConnectedEventHandler = undefined
|
||||
this.emit('connected')
|
||||
}
|
||||
|
||||
protected onDisconnecting(): void {
|
||||
if (this.transportAdapter === undefined) {
|
||||
return
|
||||
}
|
||||
this.unbindEventsFromPreviousWebsocket()
|
||||
this.transportAdapter = undefined
|
||||
this.readyMessageReceived = false
|
||||
this.emit('disconnected')
|
||||
}
|
||||
|
@ -99,4 +174,10 @@ export abstract class MessageTransporter extends EventEmitter2<MessageEventPaylo
|
|||
objectify: true
|
||||
}) 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 {
|
||||
return this.otherSide !== undefined
|
||||
? ConnectionState.CONNECTED
|
||||
: ConnectionState.DISCONNECT
|
||||
: ConnectionState.DISCONNECTED
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue