mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -05:00
feat(realtime): add disconnect reason
The frontend now doesn't try to reconnect, when the disconnection happened because of a lack of permissions Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
f6cfe74d8c
commit
723f3f611c
11 changed files with 111 additions and 46 deletions
|
@ -3,9 +3,14 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { ConnectionState, Message, MessageType } from '@hedgedoc/commons';
|
import {
|
||||||
|
ConnectionState,
|
||||||
|
DisconnectReason,
|
||||||
|
Message,
|
||||||
|
MessageType,
|
||||||
|
} from '@hedgedoc/commons';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import WebSocket, { MessageEvent } from 'ws';
|
import WebSocket, { CloseEvent, MessageEvent } from 'ws';
|
||||||
|
|
||||||
import { BackendWebsocketAdapter } from './backend-websocket-adapter';
|
import { BackendWebsocketAdapter } from './backend-websocket-adapter';
|
||||||
|
|
||||||
|
@ -29,17 +34,26 @@ describe('backend websocket adapter', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can bind and unbind the close event', () => {
|
it('can bind and unbind the close event', () => {
|
||||||
const handler = jest.fn();
|
const handler = jest.fn((reason?: DisconnectReason) => console.log(reason));
|
||||||
|
|
||||||
|
let modifiedHandler: (event: CloseEvent) => void = jest.fn();
|
||||||
|
jest
|
||||||
|
.spyOn(mockedSocket, 'addEventListener')
|
||||||
|
.mockImplementation((event, handler_) => {
|
||||||
|
modifiedHandler = handler_;
|
||||||
|
});
|
||||||
|
|
||||||
const unbind = sut.bindOnCloseEvent(handler);
|
const unbind = sut.bindOnCloseEvent(handler);
|
||||||
expect(mockedSocket.addEventListener).toHaveBeenCalledWith(
|
|
||||||
'close',
|
modifiedHandler(
|
||||||
handler,
|
Mock.of<CloseEvent>({ code: DisconnectReason.USER_NOT_PERMITTED }),
|
||||||
);
|
);
|
||||||
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
expect(handler).toHaveBeenCalledWith(DisconnectReason.USER_NOT_PERMITTED);
|
||||||
|
|
||||||
unbind();
|
unbind();
|
||||||
expect(mockedSocket.removeEventListener).toHaveBeenCalledWith(
|
|
||||||
'close',
|
expect(mockedSocket.removeEventListener).toHaveBeenCalled();
|
||||||
handler,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can bind and unbind the connect event', () => {
|
it('can bind and unbind the connect event', () => {
|
||||||
|
|
|
@ -3,9 +3,14 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { ConnectionState, Message, MessageType } from '@hedgedoc/commons';
|
import {
|
||||||
|
ConnectionState,
|
||||||
|
DisconnectReason,
|
||||||
|
Message,
|
||||||
|
MessageType,
|
||||||
|
} from '@hedgedoc/commons';
|
||||||
import type { TransportAdapter } from '@hedgedoc/commons';
|
import type { TransportAdapter } from '@hedgedoc/commons';
|
||||||
import WebSocket, { MessageEvent } from 'ws';
|
import WebSocket, { CloseEvent, MessageEvent } from 'ws';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a transport adapter that communicates using a nodejs socket.
|
* Implements a transport adapter that communicates using a nodejs socket.
|
||||||
|
@ -13,10 +18,13 @@ import WebSocket, { MessageEvent } from 'ws';
|
||||||
export class BackendWebsocketAdapter implements TransportAdapter {
|
export class BackendWebsocketAdapter implements TransportAdapter {
|
||||||
constructor(private socket: WebSocket) {}
|
constructor(private socket: WebSocket) {}
|
||||||
|
|
||||||
bindOnCloseEvent(handler: () => void): () => void {
|
bindOnCloseEvent(handler: (reason?: DisconnectReason) => void): () => void {
|
||||||
this.socket.addEventListener('close', handler);
|
function callback(event: CloseEvent): void {
|
||||||
|
handler(event.code);
|
||||||
|
}
|
||||||
|
this.socket.addEventListener('close', callback);
|
||||||
return () => {
|
return () => {
|
||||||
this.socket.removeEventListener('close', handler);
|
this.socket.removeEventListener('close', callback);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
DisconnectReason,
|
||||||
MessageTransporter,
|
MessageTransporter,
|
||||||
NotePermissions,
|
NotePermissions,
|
||||||
userCanEdit,
|
userCanEdit,
|
||||||
|
@ -66,13 +67,11 @@ export class WebsocketGateway implements OnGatewayConnection {
|
||||||
note,
|
note,
|
||||||
);
|
);
|
||||||
if (notePermission < NotePermission.READ) {
|
if (notePermission < NotePermission.READ) {
|
||||||
//TODO: [mrdrogdrog] inform client about reason of disconnect.
|
|
||||||
// (https://github.com/hedgedoc/hedgedoc/issues/5034)
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Access denied to note '${note.id}' for user '${username}'`,
|
`Access denied to note '${note.id}' for user '${username}'`,
|
||||||
'handleConnection',
|
'handleConnection',
|
||||||
);
|
);
|
||||||
clientSocket.close();
|
clientSocket.close(DisconnectReason.USER_NOT_PERMITTED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
9
commons/src/message-transporters/disconnect_reason.ts
Normal file
9
commons/src/message-transporters/disconnect_reason.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum DisconnectReason {
|
||||||
|
USER_NOT_PERMITTED = 4000
|
||||||
|
}
|
|
@ -9,3 +9,4 @@ export * from './message-transporter.js'
|
||||||
export * from './realtime-user.js'
|
export * from './realtime-user.js'
|
||||||
export * from './transport-adapter.js'
|
export * from './transport-adapter.js'
|
||||||
export * from './mocked-backend-transport-adapter.js'
|
export * from './mocked-backend-transport-adapter.js'
|
||||||
|
export * from './disconnect_reason.js'
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { Message, MessagePayloads, MessageType } from './message.js'
|
import {
|
||||||
|
ConnectionStateEvent,
|
||||||
|
Message,
|
||||||
|
MessagePayloads,
|
||||||
|
MessageType
|
||||||
|
} from './message.js'
|
||||||
import { TransportAdapter } from './transport-adapter.js'
|
import { TransportAdapter } from './transport-adapter.js'
|
||||||
import { EventEmitter2, Listener } from 'eventemitter2'
|
import { EventEmitter2, Listener } from 'eventemitter2'
|
||||||
|
import { DisconnectReason } from './disconnect_reason.js'
|
||||||
|
|
||||||
export type MessageEvents = MessageType | 'connected' | 'disconnected' | 'ready'
|
export type AllEvents = MessageType | ConnectionStateEvent
|
||||||
|
|
||||||
type MessageEventPayloadMap = {
|
type MessageEventPayloadMap = {
|
||||||
[E in MessageEvents]: E extends keyof MessagePayloads
|
[E in AllEvents]: E extends keyof MessagePayloads
|
||||||
? (message: Message<E>) => void
|
? (message: Message<E>) => void
|
||||||
|
: E extends ConnectionStateEvent.DISCONNECTED
|
||||||
|
? (reason?: DisconnectReason) => void
|
||||||
: () => void
|
: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,14 +165,14 @@ export class MessageTransporter extends EventEmitter2<MessageEventPayloadMap> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindWebsocketEvents(websocket: TransportAdapter) {
|
private bindWebsocketEvents(transportAdapter: TransportAdapter) {
|
||||||
this.destroyOnErrorEventHandler = websocket.bindOnErrorEvent(
|
this.destroyOnErrorEventHandler = transportAdapter.bindOnErrorEvent(
|
||||||
this.onDisconnecting.bind(this)
|
this.onDisconnecting.bind(this)
|
||||||
)
|
)
|
||||||
this.destroyOnCloseEventHandler = websocket.bindOnCloseEvent(
|
this.destroyOnCloseEventHandler = transportAdapter.bindOnCloseEvent(
|
||||||
this.onDisconnecting.bind(this)
|
this.onDisconnecting.bind(this)
|
||||||
)
|
)
|
||||||
this.destroyOnMessageEventHandler = websocket.bindOnMessageEvent(
|
this.destroyOnMessageEventHandler = transportAdapter.bindOnMessageEvent(
|
||||||
this.receiveMessage.bind(this)
|
this.receiveMessage.bind(this)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -172,10 +180,10 @@ export class MessageTransporter extends EventEmitter2<MessageEventPayloadMap> {
|
||||||
protected onConnected(): void {
|
protected onConnected(): void {
|
||||||
this.destroyOnConnectedEventHandler?.()
|
this.destroyOnConnectedEventHandler?.()
|
||||||
this.destroyOnConnectedEventHandler = undefined
|
this.destroyOnConnectedEventHandler = undefined
|
||||||
this.emit('connected')
|
this.emit(ConnectionStateEvent.CONNECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onDisconnecting(): void {
|
protected onDisconnecting(reason?: DisconnectReason): void {
|
||||||
if (this.transportAdapter === undefined) {
|
if (this.transportAdapter === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -184,7 +192,7 @@ export class MessageTransporter extends EventEmitter2<MessageEventPayloadMap> {
|
||||||
this.thisSideReady = false
|
this.thisSideReady = false
|
||||||
this.otherSideReady = false
|
this.otherSideReady = false
|
||||||
this.transportAdapter = undefined
|
this.transportAdapter = undefined
|
||||||
this.emit('disconnected')
|
this.emit(ConnectionStateEvent.DISCONNECTED, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -22,6 +22,12 @@ export enum MessageType {
|
||||||
READY_ANSWER = 'READY_ANSWER'
|
READY_ANSWER = 'READY_ANSWER'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ConnectionStateEvent {
|
||||||
|
READY = 'ready',
|
||||||
|
CONNECTED = 'connected',
|
||||||
|
DISCONNECTED = 'disconnected'
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessagePayloads {
|
export interface MessagePayloads {
|
||||||
[MessageType.NOTE_CONTENT_STATE_REQUEST]: number[]
|
[MessageType.NOTE_CONTENT_STATE_REQUEST]: number[]
|
||||||
[MessageType.NOTE_CONTENT_UPDATE]: number[]
|
[MessageType.NOTE_CONTENT_UPDATE]: number[]
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { ConnectionState } from './message-transporter.js'
|
import { ConnectionState } from './message-transporter.js'
|
||||||
import { Message, MessageType } from './message.js'
|
import { Message, MessageType } from './message.js'
|
||||||
|
import { DisconnectReason } from './disconnect_reason.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines methods that must be implemented to send and receive messages using an {@link AdapterMessageTransporter}.
|
* Defines methods that must be implemented to send and receive messages using an {@link AdapterMessageTransporter}.
|
||||||
|
@ -18,7 +19,7 @@ export interface TransportAdapter {
|
||||||
|
|
||||||
bindOnErrorEvent(handler: () => void): () => void
|
bindOnErrorEvent(handler: () => void): () => void
|
||||||
|
|
||||||
bindOnCloseEvent(handler: () => void): () => void
|
bindOnCloseEvent(handler: (reason?: DisconnectReason) => void): () => void
|
||||||
|
|
||||||
disconnect(): void
|
disconnect(): void
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { FrontendWebsocketAdapter } from './frontend-websocket-adapter'
|
import { FrontendWebsocketAdapter } from './frontend-websocket-adapter'
|
||||||
import type { Message } from '@hedgedoc/commons'
|
import type { Message } from '@hedgedoc/commons'
|
||||||
import { ConnectionState, MessageType } from '@hedgedoc/commons'
|
import { ConnectionState, DisconnectReason, MessageType } from '@hedgedoc/commons'
|
||||||
import { Mock } from 'ts-mockery'
|
import { Mock } from 'ts-mockery'
|
||||||
|
|
||||||
describe('frontend websocket', () => {
|
describe('frontend websocket', () => {
|
||||||
|
@ -34,11 +34,22 @@ describe('frontend websocket', () => {
|
||||||
|
|
||||||
it('can bind and unbind the close event', () => {
|
it('can bind and unbind the close event', () => {
|
||||||
mockSocket()
|
mockSocket()
|
||||||
const handler = jest.fn()
|
const handler = jest.fn((reason?: DisconnectReason) => console.log(reason))
|
||||||
|
|
||||||
|
let modifiedHandler: (event: CloseEvent) => void = jest.fn()
|
||||||
|
jest.spyOn(mockedSocket, 'addEventListener').mockImplementation((event, handler_) => {
|
||||||
|
modifiedHandler = handler_
|
||||||
|
})
|
||||||
|
|
||||||
const unbind = adapter.bindOnCloseEvent(handler)
|
const unbind = adapter.bindOnCloseEvent(handler)
|
||||||
expect(addEventListenerSpy).toHaveBeenCalledWith('close', handler)
|
|
||||||
|
modifiedHandler(Mock.of<CloseEvent>({ code: DisconnectReason.USER_NOT_PERMITTED }))
|
||||||
|
expect(handler).toHaveBeenCalledTimes(1)
|
||||||
|
expect(handler).toHaveBeenCalledWith(DisconnectReason.USER_NOT_PERMITTED)
|
||||||
|
|
||||||
unbind()
|
unbind()
|
||||||
expect(removeEventListenerSpy).toHaveBeenCalledWith('close', handler)
|
|
||||||
|
expect(removeEventListenerSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can bind and unbind the connect event', () => {
|
it('can bind and unbind the connect event', () => {
|
||||||
|
|
|
@ -3,9 +3,8 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { TransportAdapter } from '@hedgedoc/commons'
|
import type { DisconnectReason, Message, MessageType, TransportAdapter } from '@hedgedoc/commons'
|
||||||
import { ConnectionState } from '@hedgedoc/commons'
|
import { ConnectionState } from '@hedgedoc/commons'
|
||||||
import type { Message, MessageType } from '@hedgedoc/commons'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a transport adapter that communicates using a browser websocket.
|
* Implements a transport adapter that communicates using a browser websocket.
|
||||||
|
@ -13,10 +12,11 @@ import type { Message, MessageType } from '@hedgedoc/commons'
|
||||||
export class FrontendWebsocketAdapter implements TransportAdapter {
|
export class FrontendWebsocketAdapter implements TransportAdapter {
|
||||||
constructor(private socket: WebSocket) {}
|
constructor(private socket: WebSocket) {}
|
||||||
|
|
||||||
bindOnCloseEvent(handler: () => void): () => void {
|
bindOnCloseEvent(handler: (reason?: DisconnectReason) => void): () => void {
|
||||||
this.socket.addEventListener('close', handler)
|
const callback = (event: CloseEvent): void => handler(event.code)
|
||||||
|
this.socket.addEventListener('close', callback)
|
||||||
return () => {
|
return () => {
|
||||||
this.socket.removeEventListener('close', handler)
|
this.socket.removeEventListener('close', callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -10,7 +10,7 @@ import { Logger } from '../../../../../utils/logger'
|
||||||
import { isMockMode } from '../../../../../utils/test-modes'
|
import { isMockMode } from '../../../../../utils/test-modes'
|
||||||
import { FrontendWebsocketAdapter } from './frontend-websocket-adapter'
|
import { FrontendWebsocketAdapter } from './frontend-websocket-adapter'
|
||||||
import { useWebsocketUrl } from './use-websocket-url'
|
import { useWebsocketUrl } from './use-websocket-url'
|
||||||
import { MessageTransporter, MockedBackendTransportAdapter } from '@hedgedoc/commons'
|
import { DisconnectReason, MessageTransporter, MockedBackendTransportAdapter } from '@hedgedoc/commons'
|
||||||
import type { Listener } from 'eventemitter2'
|
import type { Listener } from 'eventemitter2'
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
||||||
const messageTransporter = useMemo(() => new MessageTransporter(), [])
|
const messageTransporter = useMemo(() => new MessageTransporter(), [])
|
||||||
|
|
||||||
const reconnectCount = useRef(0)
|
const reconnectCount = useRef(0)
|
||||||
|
const disconnectReason = useRef<DisconnectReason | undefined>(undefined)
|
||||||
const establishWebsocketConnection = useCallback(() => {
|
const establishWebsocketConnection = useCallback(() => {
|
||||||
if (isMockMode) {
|
if (isMockMode) {
|
||||||
logger.debug('Creating Loopback connection...')
|
logger.debug('Creating Loopback connection...')
|
||||||
|
@ -57,7 +58,7 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
||||||
const isConnected = useApplicationState((state) => state.realtimeStatus.isConnected)
|
const isConnected = useApplicationState((state) => state.realtimeStatus.isConnected)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnected || reconnectCount.current > 0) {
|
if (isConnected || reconnectCount.current > 0 || disconnectReason.current === DisconnectReason.USER_NOT_PERMITTED) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
establishWebsocketConnection()
|
establishWebsocketConnection()
|
||||||
|
@ -86,9 +87,16 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const connectedListener = messageTransporter.doAsSoonAsReady(() => setRealtimeConnectionState(true))
|
const connectedListener = messageTransporter.doAsSoonAsReady(() => setRealtimeConnectionState(true))
|
||||||
const disconnectedListener = messageTransporter.on('disconnected', () => setRealtimeConnectionState(false), {
|
const disconnectedListener = messageTransporter.on(
|
||||||
objectify: true
|
'disconnected',
|
||||||
}) as Listener
|
(reason?: DisconnectReason) => {
|
||||||
|
disconnectReason.current = reason
|
||||||
|
setRealtimeConnectionState(false)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
objectify: true
|
||||||
|
}
|
||||||
|
) as Listener
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
connectedListener.off()
|
connectedListener.off()
|
||||||
|
|
Loading…
Reference in a new issue