import type { IncomingMessage, ServerResponse } from "node:http"; import type { Awareness } from "y-protocols/awareness"; import type Connection from "./Connection.ts"; import type Document from "./Document.ts"; import type { Hocuspocus } from "./Hocuspocus.ts"; export interface ConnectionTransactionOrigin { source: "connection"; connection: Connection; } export interface RedisTransactionOrigin { source: "redis"; } export interface LocalTransactionOrigin { source: "local"; skipStoreHooks?: boolean; context?: any; } export type TransactionOrigin = | ConnectionTransactionOrigin | RedisTransactionOrigin | LocalTransactionOrigin; export function isTransactionOrigin( origin: unknown, ): origin is TransactionOrigin { return ( typeof origin === "object" && origin !== null && "source" in origin && ((origin as any).source === "connection" || (origin as any).source === "redis" || (origin as any).source === "local") ); } export function shouldSkipStoreHooks(origin: unknown): boolean { if (!isTransactionOrigin(origin)) return false; switch (origin.source) { case "connection": return false; case "redis": return true; case "local": return origin.skipStoreHooks ?? false; } } /** * Minimal interface for any WebSocket-like object for WebSocket, Bun's ServerWebSocket, ws, Deno, etc. */ export interface WebSocketLike { send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void; close(code?: number, reason?: string): void; readyState: number; } export enum MessageType { Unknown = -1, Sync = 0, Awareness = 1, Auth = 2, QueryAwareness = 3, SyncReply = 4, // same as Sync, but won't trigger another 'SyncStep1' Stateless = 5, BroadcastStateless = 6, CLOSE = 7, SyncStatus = 8, Ping = 9, Pong = 10, } export interface AwarenessUpdate { added: Array; updated: Array; removed: Array; } export interface ConnectionConfiguration { readOnly: boolean; isAuthenticated: boolean; } export interface Extension { priority?: number; extensionName?: string; onConfigure?(data: onConfigurePayload): Promise; onListen?(data: onListenPayload): Promise; onUpgrade?(data: onUpgradePayload): Promise; onConnect?(data: onConnectPayload): Promise; connected?(data: connectedPayload): Promise; onAuthenticate?(data: onAuthenticatePayload): Promise; onTokenSync?(data: onTokenSyncPayload): Promise; onCreateDocument?(data: onCreateDocumentPayload): Promise; onLoadDocument?(data: onLoadDocumentPayload): Promise; afterLoadDocument?(data: afterLoadDocumentPayload): Promise; beforeHandleMessage?(data: beforeHandleMessagePayload): Promise; beforeSync?(data: beforeSyncPayload): Promise; beforeBroadcastStateless?( data: beforeBroadcastStatelessPayload, ): Promise; onStateless?(payload: onStatelessPayload): Promise; onChange?(data: onChangePayload): Promise; onStoreDocument?(data: onStoreDocumentPayload): Promise; afterStoreDocument?(data: afterStoreDocumentPayload): Promise; onAwarenessUpdate?(data: onAwarenessUpdatePayload): Promise; onRequest?(data: onRequestPayload): Promise; onDisconnect?(data: onDisconnectPayload): Promise; beforeUnloadDocument?(data: beforeUnloadDocumentPayload): Promise; afterUnloadDocument?(data: afterUnloadDocumentPayload): Promise; onDestroy?(data: onDestroyPayload): Promise; } export type HookName = | "onConfigure" | "onListen" | "onUpgrade" | "onConnect" | "connected" | "onAuthenticate" | "onTokenSync" | "onCreateDocument" | "onLoadDocument" | "afterLoadDocument" | "beforeHandleMessage" | "beforeBroadcastStateless" | "beforeSync" | "onStateless" | "onChange" | "onStoreDocument" | "afterStoreDocument" | "onAwarenessUpdate" | "onRequest" | "onDisconnect" | "beforeUnloadDocument" | "afterUnloadDocument" | "onDestroy"; export type HookPayloadByName = { onConfigure: onConfigurePayload; onListen: onListenPayload; onUpgrade: onUpgradePayload; onConnect: onConnectPayload; connected: connectedPayload; onAuthenticate: onAuthenticatePayload; onTokenSync: onTokenSyncPayload; onCreateDocument: onCreateDocumentPayload; onLoadDocument: onLoadDocumentPayload; afterLoadDocument: afterLoadDocumentPayload; beforeHandleMessage: beforeHandleMessagePayload; beforeBroadcastStateless: beforeBroadcastStatelessPayload; beforeSync: beforeSyncPayload; onStateless: onStatelessPayload; onChange: onChangePayload; onStoreDocument: onStoreDocumentPayload; afterStoreDocument: afterStoreDocumentPayload; onAwarenessUpdate: onAwarenessUpdatePayload; onRequest: onRequestPayload; onDisconnect: onDisconnectPayload; afterUnloadDocument: afterUnloadDocumentPayload; beforeUnloadDocument: beforeUnloadDocumentPayload; onDestroy: onDestroyPayload; }; export interface Configuration extends Extension { /** * A name for the instance, used for logging. */ name: string | null; /** * A list of hocuspocus extensions. */ extensions: Array; /** * Defines in which interval the server sends a ping, and closes the connection when no pong is sent back. */ timeout: number; /** * Debounces the call of the `onStoreDocument` hook for the given amount of time in ms. * Otherwise every single update would be persisted. */ debounce: number; /** * Makes sure to call `onStoreDocument` at least in the given amount of time (ms). */ maxDebounce: number; /** * By default, the servers show a start screen. If passed false, the server will start quietly. */ quiet: boolean; /** * If set to false, respects the debounce time of `onStoreDocument` before unloading a document. * Otherwise, the document will be unloaded immediately. * * This prevents a client from DOSing the server by repeatedly connecting and disconnecting when * your onStoreDocument is rate-limited. */ unloadImmediately: boolean; /** * options to pass to the ydoc document */ yDocOptions: { gc: boolean; // enable or disable garbage collection (see https://github.com/yjs/yjs/blob/main/INTERNALS.md#deletions) gcFilter: () => boolean; // will be called before garbage collecting ; return false to keep it }; } export interface onStatelessPayload { connection: Connection; documentName: string; document: Document; payload: string; } export interface onAuthenticatePayload { context: Context; documentName: string; instance: Hocuspocus; requestHeaders: Headers; requestParameters: URLSearchParams; request: Request; socketId: string; token: string; connectionConfig: ConnectionConfiguration; providerVersion: string | null; } export interface onTokenSyncPayload { context: Context; document: Document; documentName: string; instance: Hocuspocus; requestHeaders: Headers; requestParameters: URLSearchParams; socketId: string; token: string; connectionConfig: ConnectionConfiguration; connection: Connection; } export interface onCreateDocumentPayload { context: Context; documentName: string; instance: Hocuspocus; requestHeaders: Headers; requestParameters: URLSearchParams; socketId: string; connectionConfig: ConnectionConfiguration; } export interface onConnectPayload { context: Context; documentName: string; instance: Hocuspocus; request: Request; requestHeaders: Headers; requestParameters: URLSearchParams; socketId: string; connectionConfig: ConnectionConfiguration; providerVersion: string | null; } export interface connectedPayload { context: Context; documentName: string; instance: Hocuspocus; request: Request; requestHeaders: Headers; requestParameters: URLSearchParams; socketId: string; connectionConfig: ConnectionConfiguration; connection: Connection; providerVersion: string | null; } export interface onLoadDocumentPayload { context: Context; document: Document; documentName: string; instance: Hocuspocus; requestHeaders: Headers; requestParameters: URLSearchParams; socketId: string; connectionConfig: ConnectionConfiguration; } export interface afterLoadDocumentPayload { context: Context; document: Document; documentName: string; instance: Hocuspocus; requestHeaders: Headers; requestParameters: URLSearchParams; socketId: string; connectionConfig: ConnectionConfiguration; } export interface onChangePayload { clientsCount: number; context: Context; document: Document; documentName: string; instance: Hocuspocus; requestHeaders: Headers; requestParameters: URLSearchParams; update: Uint8Array; socketId: string; transactionOrigin: unknown; connection?: Connection; } export interface beforeHandleMessagePayload { clientsCount: number; context: Context; document: Document; documentName: string; instance: Hocuspocus; requestHeaders: Headers; requestParameters: URLSearchParams; update: Uint8Array; socketId: string; connection: Connection; } export interface beforeSyncPayload { clientsCount: number; context: Context; document: Document; documentName: string; connection: Connection; /** * The y-protocols/sync message type * @example * 0: SyncStep1 * 1: SyncStep2 * 2: YjsUpdate * * @see https://github.com/yjs/y-protocols/blob/master/sync.js#L13-L40 */ type: number; /** * The payload of the y-sync message. */ payload: Uint8Array; } export interface beforeBroadcastStatelessPayload { document: Document; documentName: string; payload: string; } export interface onStoreDocumentPayload { clientsCount: number; document: Document; lastContext: Context; lastTransactionOrigin: unknown; documentName: string; instance: Hocuspocus; } // eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type export interface afterStoreDocumentPayload extends onStoreDocumentPayload {} export interface onAwarenessUpdatePayload { document: Document; documentName: string; instance: Hocuspocus; transactionOrigin: unknown; connection?: Connection; added: number[]; updated: number[]; removed: number[]; awareness: Awareness; states: StatesArray; } export type StatesArray = { clientId: number; [key: string | number]: any }[]; export interface fetchPayload { context: Context; document: Document; documentName: string; instance: Hocuspocus; requestHeaders: Headers; requestParameters: URLSearchParams; socketId: string; connectionConfig: ConnectionConfiguration; } export interface storePayload extends onStoreDocumentPayload { state: Buffer; } export interface onDisconnectPayload { clientsCount: number; context: Context; document: Document; documentName: string; instance: Hocuspocus; requestHeaders: Headers; requestParameters: URLSearchParams; socketId: string; } export interface onRequestPayload { request: IncomingMessage; response: ServerResponse; instance: Hocuspocus; } export interface onUpgradePayload { request: IncomingMessage; socket: any; head: any; instance: Hocuspocus; } export interface onListenPayload { instance: Hocuspocus; configuration: Configuration; port: number; } export interface onDestroyPayload { instance: Hocuspocus; } export interface onConfigurePayload { instance: Hocuspocus; configuration: Configuration; version: string; } export interface afterUnloadDocumentPayload { instance: Hocuspocus; documentName: string; } export interface beforeUnloadDocumentPayload { instance: Hocuspocus; documentName: string; document: Document; } export interface DirectConnection { transact(transaction: (document: Document) => void): Promise; disconnect(): void; }