mirror of
https://github.com/outline/outline.git
synced 2026-01-02 00:59:53 -06:00
102 lines
2.9 KiB
TypeScript
102 lines
2.9 KiB
TypeScript
import type {
|
|
Extension,
|
|
connectedPayload,
|
|
onConnectPayload,
|
|
onDisconnectPayload,
|
|
} from "@hocuspocus/server";
|
|
import pluralize from "pluralize";
|
|
import { TooManyConnections } from "@shared/collaboration/CloseEvents";
|
|
import env from "@server/env";
|
|
import Logger from "@server/logging/Logger";
|
|
import { trace } from "@server/logging/tracing";
|
|
import type { withContext } from "./types";
|
|
|
|
@trace()
|
|
export class ConnectionLimitExtension implements Extension {
|
|
/**
|
|
* Map of documentId -> connection count
|
|
*/
|
|
public connectionsByDocument: Map<string, Set<string>> = new Map();
|
|
|
|
/**
|
|
* On disconnect hook
|
|
*
|
|
* @param data The disconnect payload
|
|
* @returns Promise
|
|
*/
|
|
onDisconnect({ documentName, socketId }: withContext<onDisconnectPayload>) {
|
|
const connections = this.connectionsByDocument.get(documentName);
|
|
if (connections) {
|
|
connections.delete(socketId);
|
|
|
|
if (connections.size === 0) {
|
|
this.connectionsByDocument.delete(documentName);
|
|
} else {
|
|
this.connectionsByDocument.set(documentName, connections);
|
|
}
|
|
}
|
|
|
|
const connectionCount = connections?.size ?? 0;
|
|
Logger.debug(
|
|
"multiplayer",
|
|
`${connectionCount} ${pluralize(
|
|
"connection",
|
|
connectionCount
|
|
)} to "${documentName}"`
|
|
);
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* onConnect hook is called when a new connection has been established.
|
|
* This is where we can check if the document has reached the maximum number of
|
|
* connections and reject the connection if it has.
|
|
*
|
|
* @param data The onConnect payload
|
|
* @returns Promise, resolving will allow the connection, rejecting will drop.
|
|
*/
|
|
onConnect({ documentName }: withContext<onConnectPayload>) {
|
|
const connections =
|
|
this.connectionsByDocument.get(documentName) || new Set();
|
|
|
|
if (connections?.size >= env.COLLABORATION_MAX_CLIENTS_PER_DOCUMENT) {
|
|
Logger.info(
|
|
"multiplayer",
|
|
`Rejected connection to "${documentName}" because it has reached the maximum number of connections`
|
|
);
|
|
|
|
// Rejecting the promise will cause the connection to be dropped.
|
|
return Promise.reject(TooManyConnections);
|
|
}
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* Connected hook is called after a new connection has been established.
|
|
* We can safely update the connection count for the document.
|
|
*
|
|
* @param data The onConnect payload
|
|
* @returns Promise
|
|
*/
|
|
connected({ documentName, socketId }: withContext<connectedPayload>) {
|
|
const connections =
|
|
this.connectionsByDocument.get(documentName) || new Set();
|
|
|
|
connections.add(socketId);
|
|
this.connectionsByDocument.set(documentName, connections);
|
|
const connectionCount = connections.size ?? 0;
|
|
|
|
Logger.debug(
|
|
"multiplayer",
|
|
`${connectionCount} ${pluralize(
|
|
"connection",
|
|
connectionCount
|
|
)} to "${documentName}"`
|
|
);
|
|
|
|
return Promise.resolve();
|
|
}
|
|
}
|