import { Extension, Hocuspocus, RedisTransactionOrigin, afterLoadDocumentPayload, afterStoreDocumentPayload, afterUnloadDocumentPayload, beforeBroadcastStatelessPayload, beforeUnloadDocumentPayload, onAwarenessUpdatePayload, onChangePayload, onConfigurePayload, onStoreDocumentPayload } from "@hocuspocus/server"; import { ExecutionResult, Lock, Redlock } from "@sesamecare-oss/redlock"; import RedisClient, { Cluster, ClusterNode, ClusterOptions, RedisOptions } from "ioredis"; //#region packages/extension-redis/src/Redis.d.ts type RedisInstance = RedisClient | Cluster; interface Configuration { /** * Redis port */ port: number; /** * Redis host */ host: string; /** * Redis Cluster */ nodes?: ClusterNode[]; /** * Duplicate from an existed Redis instance */ redis?: RedisInstance; /** * Redis instance creator */ createClient?: () => RedisInstance; /** * Options passed directly to Redis constructor * * https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options */ options?: ClusterOptions | RedisOptions; /** * An unique instance name, required to filter messages in Redis. * If none is provided an unique id is generated. */ identifier: string; /** * Namespace for Redis keys, if none is provided 'hocuspocus' is used */ prefix: string; /** * The maximum time for the Redis lock in ms (in case it can’t be released). */ lockTimeout: number; /** * A delay before onDisconnect is executed. This allows last minute updates' * sync messages to be received by the subscription before it's closed. */ disconnectDelay: number; /** * The maximum time (in ms) `afterLoadDocument` waits for another instance to * send its state when loading a document that is already open elsewhere. * * When a document is loaded on this instance while another instance already * has it open (and possibly modified in memory, not yet persisted), we * publish a SyncStep1 and block the load until the peer replies with its * state (SyncStep2) — or this timeout elapses. This guarantees that callers * (most importantly a `DirectConnection`) observe the latest collaborative * state instead of just what was loaded from storage. * * If no other instance is subscribed to the document, the wait is skipped * entirely, so a cold/lone load is not delayed. * * Set to `0` to disable the wait and restore the legacy fire-and-forget * behavior. */ awaitInitialSyncTimeout: number; } declare class Redis implements Extension { /** * Make sure to give that extension a higher priority, so * the `onStoreDocument` hook is able to intercept the chain, * before documents are stored to the database. */ priority: number; configuration: Configuration; redisTransactionOrigin: RedisTransactionOrigin; pub: RedisInstance; sub: RedisInstance; instance: Hocuspocus; redlock: Redlock; locks: Map; }>; messagePrefix: Buffer; private pendingAfterStoreDocumentResolves; /** * Documents this instance has loaded and subscribed to, keyed by name. * * This mirrors `instance.documents` but is populated at the very start of * `afterLoadDocument` — i.e. *before* Hocuspocus registers the document in * `instance.documents` (which only happens once loading, including this * hook, has fully resolved). That early registration is what lets * `handleIncomingMessage` apply a peer's SyncStep2 reply while we are still * blocking the load on it. */ private documents; /** * Resolvers for in-flight `afterLoadDocument` waits, keyed by document name. * Invoked by `handleIncomingMessage` as soon as a peer's state arrives. */ private pendingInitialSyncResolves; constructor(configuration: Partial); onConfigure({ instance }: onConfigurePayload): Promise; private getKey; private pubKey; private subKey; private lockKey; private encodeMessage; private decodeMessage; /** * Once a document is loaded, subscribe to the channel in Redis. */ afterLoadDocument({ documentName, document }: afterLoadDocumentPayload): Promise; /** * Announce our state to other instances and — when another instance already * has this document open — block until it has sent us its state (or the * configured timeout elapses). See {@link Configuration.awaitInitialSyncTimeout}. */ private syncInitialStateFromPeers; /** * Whether another instance is currently subscribed to a document's channel. * We are subscribed ourselves, so a subscriber count greater than one means * at least one peer has the document open. */ private hasOtherSubscribers; /** * Resolve a pending `afterLoadDocument` wait once a peer's state has been * applied. */ private resolveInitialSync; /** * Peek whether a message carries another instance's document state * (SyncStep2 or Update) without consuming the decoder. A SyncStep1 only * *requests* state, so it must not release an initial-sync wait. */ private messageCarriesPeerState; /** * Publish the first sync step through Redis. */ private publishFirstSyncStep; /** * Let’s ask Redis who is connected already. */ private requestAwarenessFromOtherInstances; /** * Before the document is stored, make sure to set a lock in Redis. * That’s meant to avoid conflicts with other instances trying to store the document. */ onStoreDocument({ documentName }: onStoreDocumentPayload): Promise; /** * Release the Redis lock, so other instances can store documents. */ afterStoreDocument({ documentName, lastTransactionOrigin }: afterStoreDocumentPayload): Promise; /** * Handle awareness update messages received directly by this Hocuspocus instance. */ onAwarenessUpdate({ documentName, awareness, added, updated, removed, document }: onAwarenessUpdatePayload): Promise; /** * Handle incoming messages published on subscribed document channels. * Note that this will also include messages from ourselves as it is not possible * in Redis to filter these. */ private handleIncomingMessage; /** * if the ydoc changed, we'll need to inform other Hocuspocus servers about it. */ onChange(data: onChangePayload): Promise; /** * Delay unloading to allow syncs to finish */ beforeUnloadDocument(data: beforeUnloadDocumentPayload): Promise; afterUnloadDocument(data: afterUnloadDocumentPayload): Promise; beforeBroadcastStateless(data: beforeBroadcastStatelessPayload): Promise; /** * Kill the Redlock connection immediately. */ onDestroy(): Promise; } //#endregion export { Configuration, Redis, RedisInstance };