mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 09:16:30 -05:00
feat(realtime): use CBOR encoding in production mode
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
8a66031ff3
commit
7f8add6cd4
10 changed files with 225 additions and 19 deletions
|
@ -3,7 +3,11 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { WebsocketTransporter } from '@hedgedoc/commons';
|
||||
import {
|
||||
CborMessageEncoder,
|
||||
JsonMessageEncoder,
|
||||
WebsocketTransporter,
|
||||
} from '@hedgedoc/commons';
|
||||
import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
|
||||
import { IncomingMessage } from 'http';
|
||||
import WebSocket from 'ws';
|
||||
|
@ -76,7 +80,12 @@ export class WebsocketGateway implements OnGatewayConnection {
|
|||
const realtimeNote =
|
||||
await this.realtimeNoteService.getOrCreateRealtimeNote(note);
|
||||
|
||||
const websocketTransporter = new WebsocketTransporter();
|
||||
const messageEncoder =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? new JsonMessageEncoder()
|
||||
: new CborMessageEncoder();
|
||||
|
||||
const websocketTransporter = new WebsocketTransporter(messageEncoder);
|
||||
const connection = new RealtimeConnection(
|
||||
websocketTransporter,
|
||||
user,
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"url": "https://github.com/hedgedoc/hedgedoc.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"cbor-x": "1.5.1",
|
||||
"eventemitter2": "6.4.9",
|
||||
"isomorphic-ws": "5.0.0",
|
||||
"reveal.js": "4.4.0",
|
||||
|
|
|
@ -10,6 +10,10 @@ export * from './message-transporters/message-transporter.js'
|
|||
export * from './message-transporters/realtime-user.js'
|
||||
export * from './message-transporters/websocket-transporter.js'
|
||||
|
||||
export * from './message-encoders/message-encoder.js'
|
||||
export * from './message-encoders/cbor-message-encoder.js'
|
||||
export * from './message-encoders/json-message-encoder.js'
|
||||
|
||||
export { parseUrl } from './utils/parse-url.js'
|
||||
export {
|
||||
MissingTrailingSlashError,
|
||||
|
|
56
commons/src/message-encoders/cbor-message-encoder.ts
Normal file
56
commons/src/message-encoders/cbor-message-encoder.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Message, MessageType } from '../message-transporters/message.js'
|
||||
import { MessageEncoder } from './message-encoder.js'
|
||||
import { Encoder, Decoder } from 'cbor-x'
|
||||
|
||||
interface ReceivedMessage {
|
||||
type: number
|
||||
payload: unknown
|
||||
}
|
||||
|
||||
const keyMap = {
|
||||
type: 0,
|
||||
payload: 1,
|
||||
users: 2,
|
||||
ownUser: 3,
|
||||
displayName: 4,
|
||||
styleIndex: 5,
|
||||
active: 6,
|
||||
username: 7,
|
||||
cursor: 8,
|
||||
from: 9,
|
||||
to: 10
|
||||
}
|
||||
|
||||
const messageTypes = Object.values(MessageType)
|
||||
|
||||
export class CborMessageEncoder implements MessageEncoder {
|
||||
private readonly encoder: Encoder = new Encoder({
|
||||
keyMap
|
||||
})
|
||||
private readonly decoder: Decoder = new Decoder({
|
||||
keyMap
|
||||
})
|
||||
|
||||
encode(message: Message<MessageType>): Uint8Array {
|
||||
const type = messageTypes.indexOf(message.type)
|
||||
return this.encoder.encode({
|
||||
...message,
|
||||
type
|
||||
})
|
||||
}
|
||||
|
||||
decode(message: ArrayBuffer): Message<MessageType> {
|
||||
const uint8Array = new Uint8Array(message)
|
||||
const decoded = this.decoder.decode(uint8Array) as ReceivedMessage
|
||||
const type = messageTypes[decoded.type]
|
||||
return {
|
||||
...decoded,
|
||||
type
|
||||
} as Message<MessageType>
|
||||
}
|
||||
}
|
17
commons/src/message-encoders/json-message-encoder.ts
Normal file
17
commons/src/message-encoders/json-message-encoder.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Message, MessageType } from '../message-transporters/message.js'
|
||||
import { MessageEncoder } from './message-encoder.js'
|
||||
|
||||
export class JsonMessageEncoder implements MessageEncoder {
|
||||
public encode(message: Message<MessageType>): string {
|
||||
return JSON.stringify(message)
|
||||
}
|
||||
|
||||
public decode(message: string): Message<MessageType> {
|
||||
return JSON.parse(message) as Message<MessageType>
|
||||
}
|
||||
}
|
13
commons/src/message-encoders/message-encoder.ts
Normal file
13
commons/src/message-encoders/message-encoder.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Message, MessageType } from '../message-transporters/message.js'
|
||||
import WebSocket from 'isomorphic-ws'
|
||||
|
||||
export abstract class MessageEncoder {
|
||||
public abstract encode(message: Message<MessageType>): WebSocket.Data
|
||||
|
||||
public abstract decode(message: WebSocket.Data): Message<MessageType>
|
||||
}
|
|
@ -6,19 +6,22 @@
|
|||
import { RealtimeUser, RemoteCursor } from './realtime-user.js'
|
||||
|
||||
export enum MessageType {
|
||||
NOTE_CONTENT_STATE_REQUEST = 'NOTE_CONTENT_STATE_REQUEST',
|
||||
// This enum is sorted by frequency of usage for efficient binary encoding
|
||||
REALTIME_USER_STATE_SET = 'REALTIME_USER_STATE_SET',
|
||||
REALTIME_USER_SET_ACTIVITY = 'REALTIME_USER_SET_ACTIVITY',
|
||||
REALTIME_USER_SINGLE_UPDATE = 'REALTIME_USER_SINGLE_UPDATE',
|
||||
NOTE_CONTENT_UPDATE = 'NOTE_CONTENT_UPDATE',
|
||||
|
||||
NOTE_CONTENT_STATE_REQUEST = 'NOTE_CONTENT_STATE_REQUEST',
|
||||
REALTIME_USER_STATE_REQUEST = 'REALTIME_USER_STATE_REQUEST',
|
||||
|
||||
PING = 'PING',
|
||||
PONG = 'PONG',
|
||||
READY = 'READY',
|
||||
|
||||
METADATA_UPDATED = 'METADATA_UPDATED',
|
||||
DOCUMENT_DELETED = 'DOCUMENT_DELETED',
|
||||
SERVER_VERSION_UPDATED = 'SERVER_VERSION_UPDATED',
|
||||
REALTIME_USER_STATE_SET = 'REALTIME_USER_STATE_SET',
|
||||
REALTIME_USER_SINGLE_UPDATE = 'REALTIME_USER_SINGLE_UPDATE',
|
||||
REALTIME_USER_STATE_REQUEST = 'REALTIME_USER_STATE_REQUEST',
|
||||
REALTIME_USER_SET_ACTIVITY = 'REALTIME_USER_SET_ACTIVITY',
|
||||
|
||||
READY = 'READY'
|
||||
SERVER_VERSION_UPDATED = 'SERVER_VERSION_UPDATED'
|
||||
}
|
||||
|
||||
export interface MessagePayloads {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { MessageEncoder } from '../message-encoders/message-encoder.js'
|
||||
import { ConnectionState, MessageTransporter } from './message-transporter.js'
|
||||
import { Message, MessageType } from './message.js'
|
||||
import WebSocket, { MessageEvent } from 'isomorphic-ws'
|
||||
|
@ -13,7 +14,7 @@ export class WebsocketTransporter extends MessageTransporter {
|
|||
private messageCallback: undefined | ((event: MessageEvent) => void)
|
||||
private closeCallback: undefined | (() => void)
|
||||
|
||||
constructor() {
|
||||
constructor(private readonly encoder: MessageEncoder) {
|
||||
super()
|
||||
}
|
||||
|
||||
|
@ -57,10 +58,7 @@ export class WebsocketTransporter extends MessageTransporter {
|
|||
}
|
||||
|
||||
private processMessageEvent(event: MessageEvent): void {
|
||||
if (typeof event.data !== 'string') {
|
||||
return
|
||||
}
|
||||
const message = JSON.parse(event.data) as Message<MessageType>
|
||||
const message = this.encoder.decode(event.data)
|
||||
this.receiveMessage(message)
|
||||
}
|
||||
|
||||
|
@ -92,7 +90,8 @@ export class WebsocketTransporter extends MessageTransporter {
|
|||
}
|
||||
|
||||
try {
|
||||
this.websocket.send(JSON.stringify(content))
|
||||
const encoded = this.encoder.encode(content)
|
||||
this.websocket.send(encoded)
|
||||
} catch (error: unknown) {
|
||||
this.disconnect()
|
||||
throw error
|
||||
|
|
|
@ -7,10 +7,15 @@ import { useApplicationState } from '../../../../../hooks/common/use-application
|
|||
import { getGlobalState } from '../../../../../redux'
|
||||
import { setRealtimeConnectionState } from '../../../../../redux/realtime/methods'
|
||||
import { Logger } from '../../../../../utils/logger'
|
||||
import { isMockMode } from '../../../../../utils/test-modes'
|
||||
import { isMockMode, isDevMode } from '../../../../../utils/test-modes'
|
||||
import { useWebsocketUrl } from './use-websocket-url'
|
||||
import type { MessageTransporter } from '@hedgedoc/commons'
|
||||
import { MockedBackendMessageTransporter, WebsocketTransporter } from '@hedgedoc/commons'
|
||||
import {
|
||||
CborMessageEncoder,
|
||||
JsonMessageEncoder,
|
||||
MockedBackendMessageTransporter,
|
||||
WebsocketTransporter
|
||||
} from '@hedgedoc/commons'
|
||||
import type { Listener } from 'eventemitter2'
|
||||
import WebSocket from 'isomorphic-ws'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
|
@ -31,7 +36,8 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
|||
return new MockedBackendMessageTransporter(getGlobalState().noteDetails.markdownContent.plain)
|
||||
} else {
|
||||
logger.debug('Creating Websocket connection...')
|
||||
return new WebsocketTransporter()
|
||||
const encoder = isDevMode ? new JsonMessageEncoder() : new CborMessageEncoder()
|
||||
return new WebsocketTransporter(encoder)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -39,6 +45,7 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
|||
if (messageTransporter instanceof WebsocketTransporter && websocketUrl) {
|
||||
logger.debug(`Connecting to ${websocketUrl.toString()}`)
|
||||
const socket = new WebSocket(websocketUrl)
|
||||
socket.binaryType = 'arraybuffer'
|
||||
socket.addEventListener('error', () => {
|
||||
setTimeout(() => {
|
||||
establishWebsocketConnection()
|
||||
|
|
97
yarn.lock
97
yarn.lock
|
@ -1702,6 +1702,48 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-darwin-arm64@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-darwin-arm64@npm:2.1.1"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-darwin-x64@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-darwin-x64@npm:2.1.1"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-linux-arm64@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-linux-arm64@npm:2.1.1"
|
||||
conditions: os=linux & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-linux-arm@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-linux-arm@npm:2.1.1"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-linux-x64@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-linux-x64@npm:2.1.1"
|
||||
conditions: os=linux & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-win32-x64@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-win32-x64@npm:2.1.1"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/autocomplete@npm:6.4.2":
|
||||
version: 6.4.2
|
||||
resolution: "@codemirror/autocomplete@npm:6.4.2"
|
||||
|
@ -2320,6 +2362,7 @@ __metadata:
|
|||
"@types/ws": 8.5.4
|
||||
"@typescript-eslint/eslint-plugin": 5.57.0
|
||||
"@typescript-eslint/parser": 5.57.0
|
||||
cbor-x: 1.5.1
|
||||
eslint: 8.37.0
|
||||
eslint-config-prettier: 8.8.0
|
||||
eslint-plugin-jest: 27.2.1
|
||||
|
@ -6753,6 +6796,49 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cbor-extract@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "cbor-extract@npm:2.1.1"
|
||||
dependencies:
|
||||
"@cbor-extract/cbor-extract-darwin-arm64": 2.1.1
|
||||
"@cbor-extract/cbor-extract-darwin-x64": 2.1.1
|
||||
"@cbor-extract/cbor-extract-linux-arm": 2.1.1
|
||||
"@cbor-extract/cbor-extract-linux-arm64": 2.1.1
|
||||
"@cbor-extract/cbor-extract-linux-x64": 2.1.1
|
||||
"@cbor-extract/cbor-extract-win32-x64": 2.1.1
|
||||
node-gyp: latest
|
||||
node-gyp-build-optional-packages: 5.0.3
|
||||
dependenciesMeta:
|
||||
"@cbor-extract/cbor-extract-darwin-arm64":
|
||||
optional: true
|
||||
"@cbor-extract/cbor-extract-darwin-x64":
|
||||
optional: true
|
||||
"@cbor-extract/cbor-extract-linux-arm":
|
||||
optional: true
|
||||
"@cbor-extract/cbor-extract-linux-arm64":
|
||||
optional: true
|
||||
"@cbor-extract/cbor-extract-linux-x64":
|
||||
optional: true
|
||||
"@cbor-extract/cbor-extract-win32-x64":
|
||||
optional: true
|
||||
bin:
|
||||
download-cbor-prebuilds: bin/download-prebuilds.js
|
||||
checksum: 283d9cdb3c716b171b5ad8666673f4ac373f975b51d9a38233d280c6f9381d66c6af4c011a561d993c4be6e427e34681bc3c5af194b9da0c9ab3401d424b7988
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cbor-x@npm:1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "cbor-x@npm:1.5.1"
|
||||
dependencies:
|
||||
cbor-extract: ^2.1.1
|
||||
dependenciesMeta:
|
||||
cbor-extract:
|
||||
optional: true
|
||||
checksum: e4ff6012194e93739a36027a2d8abbe4e98f62c003c77896bed1c22bf29782fe371480c7db37a0fc288a1a56cb0b711e48072ed6f42d51bdf96395a1ed01bcea
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "chalk@npm:4.1.2"
|
||||
|
@ -13816,6 +13902,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp-build-optional-packages@npm:5.0.3":
|
||||
version: 5.0.3
|
||||
resolution: "node-gyp-build-optional-packages@npm:5.0.3"
|
||||
bin:
|
||||
node-gyp-build-optional-packages: bin.js
|
||||
node-gyp-build-optional-packages-optional: optional.js
|
||||
node-gyp-build-optional-packages-test: build-test.js
|
||||
checksum: be3f0235925c8361e5bc1a03848f5e24815b0df8aa90bd13f1eac91cd86264bbb8b7689ca6cd083b02c8099c7b54f9fb83066c7bb77c2389dc4eceab921f084f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp@npm:8.x":
|
||||
version: 8.4.1
|
||||
resolution: "node-gyp@npm:8.4.1"
|
||||
|
|
Loading…
Reference in a new issue