mirror of
https://github.com/docmost/docmost.git
synced 2026-08-28 17:27:06 +08:00
fix: survive hocuspocus v4 message timeout
This commit is contained in:
@@ -12,7 +12,10 @@ let releaseTimer: ReturnType<typeof setTimeout> | null = null;
|
|||||||
|
|
||||||
export function getCollabSocket(): HocuspocusProviderWebsocket {
|
export function getCollabSocket(): HocuspocusProviderWebsocket {
|
||||||
if (!socket) {
|
if (!socket) {
|
||||||
socket = new HocuspocusProviderWebsocket({ url: getCollaborationUrl() });
|
socket = new HocuspocusProviderWebsocket({
|
||||||
|
url: getCollaborationUrl(),
|
||||||
|
autoConnect: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
@@ -24,6 +27,7 @@ export function acquireCollabSocket(): void {
|
|||||||
releaseTimer = null;
|
releaseTimer = null;
|
||||||
}
|
}
|
||||||
const collabSocket = getCollabSocket();
|
const collabSocket = getCollabSocket();
|
||||||
|
collabSocket.shouldConnect = true;
|
||||||
if (collabSocket.status === WebSocketStatus.Disconnected) {
|
if (collabSocket.status === WebSocketStatus.Disconnected) {
|
||||||
collabSocket.connect();
|
collabSocket.connect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export class CollabProxySocket implements WebSocketLike {
|
|||||||
private pub: RedisClient;
|
private pub: RedisClient;
|
||||||
private readonly pack: Pack;
|
private readonly pack: Pack;
|
||||||
readyState = 1;
|
readyState = 1;
|
||||||
|
onClose?: (code?: number, reason?: string) => void;
|
||||||
|
|
||||||
constructor(pub: RedisClient, pack: Pack, replyTo: string, socketId: string) {
|
constructor(pub: RedisClient, pack: Pack, replyTo: string, socketId: string) {
|
||||||
this.replyTo = replyTo;
|
this.replyTo = replyTo;
|
||||||
@@ -30,13 +31,7 @@ export class CollabProxySocket implements WebSocketLike {
|
|||||||
close(code?: number, reason?: string) {
|
close(code?: number, reason?: string) {
|
||||||
if (this.readyState !== 1) return;
|
if (this.readyState !== 1) return;
|
||||||
this.readyState = 3;
|
this.readyState = 3;
|
||||||
const msg: RSAMessageClose = {
|
this.onClose?.(code, reason);
|
||||||
type: 'close',
|
|
||||||
code,
|
|
||||||
reason,
|
|
||||||
socketId: this.socketId,
|
|
||||||
};
|
|
||||||
this.publish(msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send(message: Uint8Array) {
|
send(message: Uint8Array) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
afterUnloadDocumentPayload,
|
afterUnloadDocumentPayload,
|
||||||
WebSocketLike,
|
WebSocketLike,
|
||||||
} from '@hocuspocus/server';
|
} from '@hocuspocus/server';
|
||||||
|
import { ConnectionTimeout, Unauthorized } from '@hocuspocus/common';
|
||||||
import RedisClient from 'ioredis';
|
import RedisClient from 'ioredis';
|
||||||
import { CollabProxySocket } from './collab-proxy-socket';
|
import { CollabProxySocket } from './collab-proxy-socket';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
CustomEvents,
|
CustomEvents,
|
||||||
Pack,
|
Pack,
|
||||||
RSAMessage,
|
RSAMessage,
|
||||||
|
RSAMessageClose,
|
||||||
RSAMessageCloseProxy,
|
RSAMessageCloseProxy,
|
||||||
RSAMessageCustomEventComplete,
|
RSAMessageCustomEventComplete,
|
||||||
RSAMessageCustomEventStart,
|
RSAMessageCustomEventStart,
|
||||||
@@ -120,6 +122,23 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
|
|||||||
replyTo,
|
replyTo,
|
||||||
socketId,
|
socketId,
|
||||||
);
|
);
|
||||||
|
// A proxy connection with no live documents (client left the page, auth
|
||||||
|
// failed, or the origin server crashed) is reaped by hocuspocus' message
|
||||||
|
// timeout. Dispose it silently in that case: relaying the timeout close
|
||||||
|
// to the origin would kill the client's real socket, which may be busy
|
||||||
|
// serving other documents. Genuine protocol closes are still relayed.
|
||||||
|
socket.onClose = (code, reason) => {
|
||||||
|
delete this.proxyConnections[socketId];
|
||||||
|
if (code !== ConnectionTimeout.code) {
|
||||||
|
const msg: RSAMessageClose = {
|
||||||
|
type: 'close',
|
||||||
|
code,
|
||||||
|
reason,
|
||||||
|
socketId,
|
||||||
|
};
|
||||||
|
this.pub.publish(replyTo, this.pack(msg));
|
||||||
|
}
|
||||||
|
};
|
||||||
const clientConnection = this.instance.handleConnection(
|
const clientConnection = this.instance.handleConnection(
|
||||||
socket,
|
socket,
|
||||||
toWebRequest(serializedHTTPRequest),
|
toWebRequest(serializedHTTPRequest),
|
||||||
@@ -318,21 +337,29 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
|
|||||||
serializedHTTPRequest: SerializedHTTPRequest,
|
serializedHTTPRequest: SerializedHTTPRequest,
|
||||||
detachableMsg: ArrayBuffer,
|
detachableMsg: ArrayBuffer,
|
||||||
) {
|
) {
|
||||||
const message = new Uint8Array(detachableMsg.slice());
|
|
||||||
const tmpMsg = new IncomingMessage(detachableMsg);
|
|
||||||
const documentNameAndSessionId = tmpMsg.readVarString();
|
|
||||||
// session-aware providers suffix the documentName with \0sessionId
|
|
||||||
const sepIdx = documentNameAndSessionId.indexOf('\0');
|
|
||||||
const documentName =
|
|
||||||
sepIdx === -1
|
|
||||||
? documentNameAndSessionId
|
|
||||||
: documentNameAndSessionId.slice(0, sepIdx);
|
|
||||||
const isDocLoadedOnInstance = this.instance.documents.has(documentName);
|
|
||||||
const socketId = serializedHTTPRequest.headers['sec-websocket-key'];
|
const socketId = serializedHTTPRequest.headers['sec-websocket-key'];
|
||||||
const entry = this.originConnections[socketId];
|
const entry = this.originConnections[socketId];
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
const { clientConnection } = entry;
|
const { clientConnection } = entry;
|
||||||
|
|
||||||
|
let message: Uint8Array;
|
||||||
|
let documentName: string;
|
||||||
|
try {
|
||||||
|
message = new Uint8Array(detachableMsg.slice());
|
||||||
|
const tmpMsg = new IncomingMessage(detachableMsg);
|
||||||
|
const documentNameAndSessionId = tmpMsg.readVarString();
|
||||||
|
// session-aware providers suffix the documentName with \0sessionId
|
||||||
|
const sepIdx = documentNameAndSessionId.indexOf('\0');
|
||||||
|
documentName =
|
||||||
|
sepIdx === -1
|
||||||
|
? documentNameAndSessionId
|
||||||
|
: documentNameAndSessionId.slice(0, sepIdx);
|
||||||
|
} catch (error) {
|
||||||
|
entry.socket.close(Unauthorized.code, Unauthorized.reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isDocLoadedOnInstance = this.instance.documents.has(documentName);
|
||||||
|
|
||||||
if (isDocLoadedOnInstance) {
|
if (isDocLoadedOnInstance) {
|
||||||
clientConnection.handleMessage(message);
|
clientConnection.handleMessage(message);
|
||||||
return;
|
return;
|
||||||
@@ -340,6 +367,13 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
|
|||||||
|
|
||||||
const proxyTo = await this.getOrClaimLockThrottled(documentName);
|
const proxyTo = await this.getOrClaimLockThrottled(documentName);
|
||||||
if (proxyTo && proxyTo !== this.serverId) {
|
if (proxyTo && proxyTo !== this.serverId) {
|
||||||
|
// Proxied messages bypass handleMessage, so refresh the connection's
|
||||||
|
// liveness fields manually or hocuspocus' message timeout would reap the
|
||||||
|
// real socket every `timeout` ms. connectionEstablishedAt is the
|
||||||
|
// reference while unauthenticated (auth for remote docs is proxied too)
|
||||||
|
// and is private upstream.
|
||||||
|
clientConnection.lastMessageReceivedAt = Date.now();
|
||||||
|
(clientConnection as any).connectionEstablishedAt = Date.now();
|
||||||
// another server owns the doc
|
// another server owns the doc
|
||||||
const proxyMessage: RSAMessageProxy = {
|
const proxyMessage: RSAMessageProxy = {
|
||||||
serializedHTTPRequest: serializedHTTPRequest,
|
serializedHTTPRequest: serializedHTTPRequest,
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ export type RSAMessageClose = {
|
|||||||
socketId: string;
|
socketId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RSAMessagePong = {
|
|
||||||
type: 'pong';
|
|
||||||
socketId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RSAMessageSend = {
|
export type RSAMessageSend = {
|
||||||
type: 'send';
|
type: 'send';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -74,7 +69,6 @@ export type RSAMessage =
|
|||||||
| RSAMessageCloseProxy
|
| RSAMessageCloseProxy
|
||||||
| RSAMessageUnload
|
| RSAMessageUnload
|
||||||
| RSAMessageClose
|
| RSAMessageClose
|
||||||
| RSAMessagePong
|
|
||||||
| RSAMessageSend
|
| RSAMessageSend
|
||||||
| RSAMessageCustomEventStart
|
| RSAMessageCustomEventStart
|
||||||
| RSAMessageCustomEventComplete;
|
| RSAMessageCustomEventComplete;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@casl/ability": "6.8.0",
|
"@casl/ability": "6.8.0",
|
||||||
"@docmost/editor-ext": "workspace:*",
|
"@docmost/editor-ext": "workspace:*",
|
||||||
"@floating-ui/dom": "1.7.3",
|
"@floating-ui/dom": "1.7.3",
|
||||||
|
"@hocuspocus/common": "4.4.0",
|
||||||
"@hocuspocus/provider": "4.4.0",
|
"@hocuspocus/provider": "4.4.0",
|
||||||
"@hocuspocus/provider-react": "4.4.0",
|
"@hocuspocus/provider-react": "4.4.0",
|
||||||
"@hocuspocus/server": "4.4.0",
|
"@hocuspocus/server": "4.4.0",
|
||||||
|
|||||||
Generated
+3
@@ -64,6 +64,9 @@ importers:
|
|||||||
'@floating-ui/dom':
|
'@floating-ui/dom':
|
||||||
specifier: 1.7.3
|
specifier: 1.7.3
|
||||||
version: 1.7.3
|
version: 1.7.3
|
||||||
|
'@hocuspocus/common':
|
||||||
|
specifier: 4.4.0
|
||||||
|
version: 4.4.0
|
||||||
'@hocuspocus/provider':
|
'@hocuspocus/provider':
|
||||||
specifier: 4.4.0
|
specifier: 4.4.0
|
||||||
version: 4.4.0(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)
|
version: 4.4.0(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)
|
||||||
|
|||||||
Reference in New Issue
Block a user