@hocuspocus/extension-redis 2.2.3 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -117,7 +117,7 @@ class Redis {
117
117
  return `${this.getKey(documentName)}:lock`;
118
118
  }
119
119
  /**
120
- * Once a document is laoded, subscribe to the channel in Redis.
120
+ * Once a document is loaded, subscribe to the channel in Redis.
121
121
  */
122
122
  async afterLoadDocument({ documentName, document }) {
123
123
  return new Promise((resolve, reject) => {
@@ -1 +1 @@
1
- {"version":3,"file":"hocuspocus-redis.cjs","sources":["../src/Redis.ts"],"sourcesContent":["import RedisClient, { ClusterNode, ClusterOptions, RedisOptions } from 'ioredis'\nimport Redlock from 'redlock'\nimport { v4 as uuid } from 'uuid'\nimport {\n IncomingMessage,\n OutgoingMessage,\n Document,\n Extension,\n afterLoadDocumentPayload,\n afterStoreDocumentPayload,\n onDisconnectPayload,\n onStoreDocumentPayload,\n onAwarenessUpdatePayload,\n onChangePayload,\n MessageReceiver,\n Debugger,\n onConfigurePayload,\n beforeBroadcastStatelessPayload, Hocuspocus,\n} from '@hocuspocus/server'\n\nexport type RedisInstance = RedisClient.Cluster | RedisClient.Redis\n\nexport interface Configuration {\n /**\n * Redis port\n */\n port: number,\n /**\n * Redis host\n */\n host: string,\n /**\n * Redis Cluster\n */\n nodes?: ClusterNode[],\n /**\n * Duplicate from an existed Redis instance\n */\n redis?: RedisInstance,\n /**\n * Redis instance creator\n */\n createClient?: () => RedisInstance,\n /**\n * Options passed directly to Redis constructor\n *\n * https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options\n */\n options?: ClusterOptions | RedisOptions,\n /**\n * An unique instance name, required to filter messages in Redis.\n * If none is provided an unique id is generated.\n */\n identifier: string,\n /**\n * Namespace for Redis keys, if none is provided 'hocuspocus' is used\n */\n prefix: string,\n /**\n * The maximum time for the Redis lock in ms (in case it can’t be released).\n */\n lockTimeout: number,\n /**\n * A delay before onDisconnect is executed. This allows last minute updates'\n * sync messages to be received by the subscription before it's closed.\n */\n disconnectDelay: number,\n}\n\nexport class Redis implements Extension {\n /**\n * Make sure to give that extension a higher priority, so\n * the `onStoreDocument` hook is able to intercept the chain,\n * before documents are stored to the database.\n */\n priority = 1000\n\n configuration: Configuration = {\n port: 6379,\n host: '127.0.0.1',\n prefix: 'hocuspocus',\n identifier: `host-${uuid()}`,\n lockTimeout: 1000,\n disconnectDelay: 1000,\n }\n\n pub: RedisInstance\n\n sub: RedisInstance\n\n instance!: Hocuspocus\n\n redlock: Redlock\n\n locks = new Map<string, Redlock.Lock>()\n\n logger: Debugger\n\n public constructor(configuration: Partial<Configuration>) {\n this.configuration = {\n ...this.configuration,\n ...configuration,\n }\n\n // We’ll replace that in the onConfigure hook with the global instance.\n this.logger = new Debugger()\n\n // Create Redis instance\n const {\n port,\n host,\n options,\n nodes,\n redis,\n createClient,\n } = this.configuration\n\n if (typeof createClient === 'function') {\n this.pub = createClient()\n this.sub = createClient()\n } else if (redis) {\n this.pub = redis.duplicate()\n this.sub = redis.duplicate()\n } else if (nodes && nodes.length > 0) {\n this.pub = new RedisClient.Cluster(nodes, options)\n this.sub = new RedisClient.Cluster(nodes, options)\n } else {\n this.pub = new RedisClient(port, host, options)\n this.sub = new RedisClient(port, host, options)\n }\n this.sub.on('pmessageBuffer', this.handleIncomingMessage)\n\n this.redlock = new Redlock([this.pub])\n }\n\n async onConfigure({ instance }: onConfigurePayload) {\n this.logger = instance.debugger\n this.instance = instance\n }\n\n private getKey(documentName: string) {\n return `${this.configuration.prefix}:${documentName}`\n }\n\n private pubKey(documentName: string) {\n return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`\n }\n\n private subKey(documentName: string) {\n return `${this.getKey(documentName)}:*`\n }\n\n private lockKey(documentName: string) {\n return `${this.getKey(documentName)}:lock`\n }\n\n /**\n * Once a document is laoded, subscribe to the channel in Redis.\n */\n public async afterLoadDocument({ documentName, document }: afterLoadDocumentPayload) {\n return new Promise((resolve, reject) => {\n // On document creation the node will connect to pub and sub channels\n // for the document.\n this.sub.psubscribe(this.subKey(documentName), async error => {\n if (error) {\n reject(error)\n return\n }\n\n this.publishFirstSyncStep(documentName, document)\n this.requestAwarenessFromOtherInstances(documentName)\n\n resolve(undefined)\n })\n })\n }\n\n /**\n * Publish the first sync step through Redis.\n */\n private async publishFirstSyncStep(documentName: string, document: Document) {\n const syncMessage = new OutgoingMessage(documentName)\n .createSyncMessage()\n .writeFirstSyncStepFor(document)\n\n return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(syncMessage.toUint8Array()))\n }\n\n /**\n * Let’s ask Redis who is connected already.\n */\n private async requestAwarenessFromOtherInstances(documentName: string) {\n const awarenessMessage = new OutgoingMessage(documentName)\n .writeQueryAwareness()\n\n return this.pub.publishBuffer(\n this.pubKey(documentName),\n Buffer.from(awarenessMessage.toUint8Array()),\n )\n }\n\n /**\n * Before the document is stored, make sure to set a lock in Redis.\n * That’s meant to avoid conflicts with other instances trying to store the document.\n */\n async onStoreDocument({ documentName }: onStoreDocumentPayload) {\n // Attempt to acquire a lock and read lastReceivedTimestamp from Redis,\n // to avoid conflict with other instances storing the same document.\n return new Promise((resolve, reject) => {\n this.redlock.lock(this.lockKey(documentName), this.configuration.lockTimeout, async (error, lock) => {\n if (error || !lock) {\n // Expected behavior: Could not acquire lock, another instance locked it already.\n // No further `onStoreDocument` hooks will be executed.\n reject()\n return\n }\n\n this.locks.set(this.lockKey(documentName), lock)\n\n resolve(undefined)\n })\n })\n }\n\n /**\n * Release the Redis lock, so other instances can store documents.\n */\n async afterStoreDocument({ documentName }: afterStoreDocumentPayload) {\n this.locks.get(this.lockKey(documentName))?.unlock()\n .catch(() => {\n // Not able to unlock Redis. The lock will expire after ${lockTimeout} ms.\n // console.error(`Not able to unlock Redis. The lock will expire after ${this.configuration.lockTimeout}ms.`)\n })\n .finally(() => {\n this.locks.delete(this.lockKey(documentName))\n })\n }\n\n /**\n * Handle awareness update messages received directly by this Hocuspocus instance.\n */\n async onAwarenessUpdate({\n documentName, awareness, added, updated, removed,\n }: onAwarenessUpdatePayload) {\n const changedClients = added.concat(updated, removed)\n const message = new OutgoingMessage(documentName)\n .createAwarenessUpdateMessage(awareness, changedClients)\n\n return this.pub.publishBuffer(\n this.pubKey(documentName),\n Buffer.from(message.toUint8Array()),\n )\n }\n\n /**\n * Handle incoming messages published on all subscribed document channels.\n * Note that this will also include messages from ourselves as it is not possible\n * in Redis to filter these.\n */\n private handleIncomingMessage = async (channel: Buffer, pattern: Buffer, data: Buffer) => {\n const message = new IncomingMessage(data)\n // we don't need the documentName from the message, we are just taking it from the redis channelName.\n // we have to immediately write it back to the encoder though, to make sure the structure of the message is correct\n message.writeVarString(message.readVarString())\n\n const channelName = pattern.toString()\n const [_, documentName, identifier] = channelName.split(':')\n const document = this.instance.documents.get(documentName)\n\n if (identifier === this.configuration.identifier) {\n return\n }\n\n if (!document) {\n return\n }\n\n new MessageReceiver(\n message,\n this.logger,\n ).apply(document, undefined, reply => {\n return this.pub.publishBuffer(\n this.pubKey(document.name),\n Buffer.from(reply),\n )\n })\n }\n\n /**\n * if the ydoc changed, we'll need to inform other Hocuspocus servers about it.\n */\n public async onChange(data: onChangePayload): Promise<any> {\n return this.publishFirstSyncStep(data.documentName, data.document)\n }\n\n /**\n * Make sure to *not* listen for further changes, when there’s\n * noone connected anymore.\n */\n public onDisconnect = async ({ documentName }: onDisconnectPayload) => {\n const disconnect = () => {\n const document = this.instance.documents.get(documentName)\n\n // Do nothing, when other users are still connected to the document.\n if (document && document.getConnectionsCount() > 0) {\n return\n }\n\n // Time to end the subscription on the document channel.\n this.sub.punsubscribe(this.subKey(documentName), (error: any) => {\n if (error) {\n console.error(error)\n }\n })\n }\n // Delay the disconnect procedure to allow last minute syncs to happen\n setTimeout(disconnect, this.configuration.disconnectDelay)\n }\n\n async beforeBroadcastStateless(data: beforeBroadcastStatelessPayload) {\n const message = new OutgoingMessage(data.documentName)\n .writeBroadcastStateless(data.payload)\n\n return this.pub.publishBuffer(\n this.pubKey(data.documentName),\n Buffer.from(message.toUint8Array()),\n )\n }\n\n /**\n * Kill the Redlock connection immediately.\n */\n async onDestroy() {\n await this.redlock.quit()\n this.pub.disconnect(false)\n this.sub.disconnect(false)\n }\n}\n"],"names":["uuid","IncomingMessage","MessageReceiver","Debugger","RedisClient","Redlock","OutgoingMessage"],"mappings":"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"}
1
+ {"version":3,"file":"hocuspocus-redis.cjs","sources":["../src/Redis.ts"],"sourcesContent":["import RedisClient, { ClusterNode, ClusterOptions, RedisOptions } from 'ioredis'\nimport Redlock from 'redlock'\nimport { v4 as uuid } from 'uuid'\nimport {\n IncomingMessage,\n OutgoingMessage,\n Document,\n Extension,\n afterLoadDocumentPayload,\n afterStoreDocumentPayload,\n onDisconnectPayload,\n onStoreDocumentPayload,\n onAwarenessUpdatePayload,\n onChangePayload,\n MessageReceiver,\n Debugger,\n onConfigurePayload,\n beforeBroadcastStatelessPayload, Hocuspocus,\n} from '@hocuspocus/server'\n\nexport type RedisInstance = RedisClient.Cluster | RedisClient.Redis\n\nexport interface Configuration {\n /**\n * Redis port\n */\n port: number,\n /**\n * Redis host\n */\n host: string,\n /**\n * Redis Cluster\n */\n nodes?: ClusterNode[],\n /**\n * Duplicate from an existed Redis instance\n */\n redis?: RedisInstance,\n /**\n * Redis instance creator\n */\n createClient?: () => RedisInstance,\n /**\n * Options passed directly to Redis constructor\n *\n * https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options\n */\n options?: ClusterOptions | RedisOptions,\n /**\n * An unique instance name, required to filter messages in Redis.\n * If none is provided an unique id is generated.\n */\n identifier: string,\n /**\n * Namespace for Redis keys, if none is provided 'hocuspocus' is used\n */\n prefix: string,\n /**\n * The maximum time for the Redis lock in ms (in case it can’t be released).\n */\n lockTimeout: number,\n /**\n * A delay before onDisconnect is executed. This allows last minute updates'\n * sync messages to be received by the subscription before it's closed.\n */\n disconnectDelay: number,\n}\n\nexport class Redis implements Extension {\n /**\n * Make sure to give that extension a higher priority, so\n * the `onStoreDocument` hook is able to intercept the chain,\n * before documents are stored to the database.\n */\n priority = 1000\n\n configuration: Configuration = {\n port: 6379,\n host: '127.0.0.1',\n prefix: 'hocuspocus',\n identifier: `host-${uuid()}`,\n lockTimeout: 1000,\n disconnectDelay: 1000,\n }\n\n pub: RedisInstance\n\n sub: RedisInstance\n\n instance!: Hocuspocus\n\n redlock: Redlock\n\n locks = new Map<string, Redlock.Lock>()\n\n logger: Debugger\n\n public constructor(configuration: Partial<Configuration>) {\n this.configuration = {\n ...this.configuration,\n ...configuration,\n }\n\n // We’ll replace that in the onConfigure hook with the global instance.\n this.logger = new Debugger()\n\n // Create Redis instance\n const {\n port,\n host,\n options,\n nodes,\n redis,\n createClient,\n } = this.configuration\n\n if (typeof createClient === 'function') {\n this.pub = createClient()\n this.sub = createClient()\n } else if (redis) {\n this.pub = redis.duplicate()\n this.sub = redis.duplicate()\n } else if (nodes && nodes.length > 0) {\n this.pub = new RedisClient.Cluster(nodes, options)\n this.sub = new RedisClient.Cluster(nodes, options)\n } else {\n this.pub = new RedisClient(port, host, options)\n this.sub = new RedisClient(port, host, options)\n }\n this.sub.on('pmessageBuffer', this.handleIncomingMessage)\n\n this.redlock = new Redlock([this.pub])\n }\n\n async onConfigure({ instance }: onConfigurePayload) {\n this.logger = instance.debugger\n this.instance = instance\n }\n\n private getKey(documentName: string) {\n return `${this.configuration.prefix}:${documentName}`\n }\n\n private pubKey(documentName: string) {\n return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`\n }\n\n private subKey(documentName: string) {\n return `${this.getKey(documentName)}:*`\n }\n\n private lockKey(documentName: string) {\n return `${this.getKey(documentName)}:lock`\n }\n\n /**\n * Once a document is loaded, subscribe to the channel in Redis.\n */\n public async afterLoadDocument({ documentName, document }: afterLoadDocumentPayload) {\n return new Promise((resolve, reject) => {\n // On document creation the node will connect to pub and sub channels\n // for the document.\n this.sub.psubscribe(this.subKey(documentName), async error => {\n if (error) {\n reject(error)\n return\n }\n\n this.publishFirstSyncStep(documentName, document)\n this.requestAwarenessFromOtherInstances(documentName)\n\n resolve(undefined)\n })\n })\n }\n\n /**\n * Publish the first sync step through Redis.\n */\n private async publishFirstSyncStep(documentName: string, document: Document) {\n const syncMessage = new OutgoingMessage(documentName)\n .createSyncMessage()\n .writeFirstSyncStepFor(document)\n\n return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(syncMessage.toUint8Array()))\n }\n\n /**\n * Let’s ask Redis who is connected already.\n */\n private async requestAwarenessFromOtherInstances(documentName: string) {\n const awarenessMessage = new OutgoingMessage(documentName)\n .writeQueryAwareness()\n\n return this.pub.publishBuffer(\n this.pubKey(documentName),\n Buffer.from(awarenessMessage.toUint8Array()),\n )\n }\n\n /**\n * Before the document is stored, make sure to set a lock in Redis.\n * That’s meant to avoid conflicts with other instances trying to store the document.\n */\n async onStoreDocument({ documentName }: onStoreDocumentPayload) {\n // Attempt to acquire a lock and read lastReceivedTimestamp from Redis,\n // to avoid conflict with other instances storing the same document.\n return new Promise((resolve, reject) => {\n this.redlock.lock(this.lockKey(documentName), this.configuration.lockTimeout, async (error, lock) => {\n if (error || !lock) {\n // Expected behavior: Could not acquire lock, another instance locked it already.\n // No further `onStoreDocument` hooks will be executed.\n reject()\n return\n }\n\n this.locks.set(this.lockKey(documentName), lock)\n\n resolve(undefined)\n })\n })\n }\n\n /**\n * Release the Redis lock, so other instances can store documents.\n */\n async afterStoreDocument({ documentName }: afterStoreDocumentPayload) {\n this.locks.get(this.lockKey(documentName))?.unlock()\n .catch(() => {\n // Not able to unlock Redis. The lock will expire after ${lockTimeout} ms.\n // console.error(`Not able to unlock Redis. The lock will expire after ${this.configuration.lockTimeout}ms.`)\n })\n .finally(() => {\n this.locks.delete(this.lockKey(documentName))\n })\n }\n\n /**\n * Handle awareness update messages received directly by this Hocuspocus instance.\n */\n async onAwarenessUpdate({\n documentName, awareness, added, updated, removed,\n }: onAwarenessUpdatePayload) {\n const changedClients = added.concat(updated, removed)\n const message = new OutgoingMessage(documentName)\n .createAwarenessUpdateMessage(awareness, changedClients)\n\n return this.pub.publishBuffer(\n this.pubKey(documentName),\n Buffer.from(message.toUint8Array()),\n )\n }\n\n /**\n * Handle incoming messages published on all subscribed document channels.\n * Note that this will also include messages from ourselves as it is not possible\n * in Redis to filter these.\n */\n private handleIncomingMessage = async (channel: Buffer, pattern: Buffer, data: Buffer) => {\n const message = new IncomingMessage(data)\n // we don't need the documentName from the message, we are just taking it from the redis channelName.\n // we have to immediately write it back to the encoder though, to make sure the structure of the message is correct\n message.writeVarString(message.readVarString())\n\n const channelName = pattern.toString()\n const [_, documentName, identifier] = channelName.split(':')\n const document = this.instance.documents.get(documentName)\n\n if (identifier === this.configuration.identifier) {\n return\n }\n\n if (!document) {\n return\n }\n\n new MessageReceiver(\n message,\n this.logger,\n ).apply(document, undefined, reply => {\n return this.pub.publishBuffer(\n this.pubKey(document.name),\n Buffer.from(reply),\n )\n })\n }\n\n /**\n * if the ydoc changed, we'll need to inform other Hocuspocus servers about it.\n */\n public async onChange(data: onChangePayload): Promise<any> {\n return this.publishFirstSyncStep(data.documentName, data.document)\n }\n\n /**\n * Make sure to *not* listen for further changes, when there’s\n * noone connected anymore.\n */\n public onDisconnect = async ({ documentName }: onDisconnectPayload) => {\n const disconnect = () => {\n const document = this.instance.documents.get(documentName)\n\n // Do nothing, when other users are still connected to the document.\n if (document && document.getConnectionsCount() > 0) {\n return\n }\n\n // Time to end the subscription on the document channel.\n this.sub.punsubscribe(this.subKey(documentName), (error: any) => {\n if (error) {\n console.error(error)\n }\n })\n }\n // Delay the disconnect procedure to allow last minute syncs to happen\n setTimeout(disconnect, this.configuration.disconnectDelay)\n }\n\n async beforeBroadcastStateless(data: beforeBroadcastStatelessPayload) {\n const message = new OutgoingMessage(data.documentName)\n .writeBroadcastStateless(data.payload)\n\n return this.pub.publishBuffer(\n this.pubKey(data.documentName),\n Buffer.from(message.toUint8Array()),\n )\n }\n\n /**\n * Kill the Redlock connection immediately.\n */\n async onDestroy() {\n await this.redlock.quit()\n this.pub.disconnect(false)\n this.sub.disconnect(false)\n }\n}\n"],"names":["uuid","IncomingMessage","MessageReceiver","Debugger","RedisClient","Redlock","OutgoingMessage"],"mappings":"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"}
@@ -108,7 +108,7 @@ class Redis {
108
108
  return `${this.getKey(documentName)}:lock`;
109
109
  }
110
110
  /**
111
- * Once a document is laoded, subscribe to the channel in Redis.
111
+ * Once a document is loaded, subscribe to the channel in Redis.
112
112
  */
113
113
  async afterLoadDocument({ documentName, document }) {
114
114
  return new Promise((resolve, reject) => {
@@ -1 +1 @@
1
- {"version":3,"file":"hocuspocus-redis.esm.js","sources":["../src/Redis.ts"],"sourcesContent":["import RedisClient, { ClusterNode, ClusterOptions, RedisOptions } from 'ioredis'\nimport Redlock from 'redlock'\nimport { v4 as uuid } from 'uuid'\nimport {\n IncomingMessage,\n OutgoingMessage,\n Document,\n Extension,\n afterLoadDocumentPayload,\n afterStoreDocumentPayload,\n onDisconnectPayload,\n onStoreDocumentPayload,\n onAwarenessUpdatePayload,\n onChangePayload,\n MessageReceiver,\n Debugger,\n onConfigurePayload,\n beforeBroadcastStatelessPayload, Hocuspocus,\n} from '@hocuspocus/server'\n\nexport type RedisInstance = RedisClient.Cluster | RedisClient.Redis\n\nexport interface Configuration {\n /**\n * Redis port\n */\n port: number,\n /**\n * Redis host\n */\n host: string,\n /**\n * Redis Cluster\n */\n nodes?: ClusterNode[],\n /**\n * Duplicate from an existed Redis instance\n */\n redis?: RedisInstance,\n /**\n * Redis instance creator\n */\n createClient?: () => RedisInstance,\n /**\n * Options passed directly to Redis constructor\n *\n * https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options\n */\n options?: ClusterOptions | RedisOptions,\n /**\n * An unique instance name, required to filter messages in Redis.\n * If none is provided an unique id is generated.\n */\n identifier: string,\n /**\n * Namespace for Redis keys, if none is provided 'hocuspocus' is used\n */\n prefix: string,\n /**\n * The maximum time for the Redis lock in ms (in case it can’t be released).\n */\n lockTimeout: number,\n /**\n * A delay before onDisconnect is executed. This allows last minute updates'\n * sync messages to be received by the subscription before it's closed.\n */\n disconnectDelay: number,\n}\n\nexport class Redis implements Extension {\n /**\n * Make sure to give that extension a higher priority, so\n * the `onStoreDocument` hook is able to intercept the chain,\n * before documents are stored to the database.\n */\n priority = 1000\n\n configuration: Configuration = {\n port: 6379,\n host: '127.0.0.1',\n prefix: 'hocuspocus',\n identifier: `host-${uuid()}`,\n lockTimeout: 1000,\n disconnectDelay: 1000,\n }\n\n pub: RedisInstance\n\n sub: RedisInstance\n\n instance!: Hocuspocus\n\n redlock: Redlock\n\n locks = new Map<string, Redlock.Lock>()\n\n logger: Debugger\n\n public constructor(configuration: Partial<Configuration>) {\n this.configuration = {\n ...this.configuration,\n ...configuration,\n }\n\n // We’ll replace that in the onConfigure hook with the global instance.\n this.logger = new Debugger()\n\n // Create Redis instance\n const {\n port,\n host,\n options,\n nodes,\n redis,\n createClient,\n } = this.configuration\n\n if (typeof createClient === 'function') {\n this.pub = createClient()\n this.sub = createClient()\n } else if (redis) {\n this.pub = redis.duplicate()\n this.sub = redis.duplicate()\n } else if (nodes && nodes.length > 0) {\n this.pub = new RedisClient.Cluster(nodes, options)\n this.sub = new RedisClient.Cluster(nodes, options)\n } else {\n this.pub = new RedisClient(port, host, options)\n this.sub = new RedisClient(port, host, options)\n }\n this.sub.on('pmessageBuffer', this.handleIncomingMessage)\n\n this.redlock = new Redlock([this.pub])\n }\n\n async onConfigure({ instance }: onConfigurePayload) {\n this.logger = instance.debugger\n this.instance = instance\n }\n\n private getKey(documentName: string) {\n return `${this.configuration.prefix}:${documentName}`\n }\n\n private pubKey(documentName: string) {\n return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`\n }\n\n private subKey(documentName: string) {\n return `${this.getKey(documentName)}:*`\n }\n\n private lockKey(documentName: string) {\n return `${this.getKey(documentName)}:lock`\n }\n\n /**\n * Once a document is laoded, subscribe to the channel in Redis.\n */\n public async afterLoadDocument({ documentName, document }: afterLoadDocumentPayload) {\n return new Promise((resolve, reject) => {\n // On document creation the node will connect to pub and sub channels\n // for the document.\n this.sub.psubscribe(this.subKey(documentName), async error => {\n if (error) {\n reject(error)\n return\n }\n\n this.publishFirstSyncStep(documentName, document)\n this.requestAwarenessFromOtherInstances(documentName)\n\n resolve(undefined)\n })\n })\n }\n\n /**\n * Publish the first sync step through Redis.\n */\n private async publishFirstSyncStep(documentName: string, document: Document) {\n const syncMessage = new OutgoingMessage(documentName)\n .createSyncMessage()\n .writeFirstSyncStepFor(document)\n\n return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(syncMessage.toUint8Array()))\n }\n\n /**\n * Let’s ask Redis who is connected already.\n */\n private async requestAwarenessFromOtherInstances(documentName: string) {\n const awarenessMessage = new OutgoingMessage(documentName)\n .writeQueryAwareness()\n\n return this.pub.publishBuffer(\n this.pubKey(documentName),\n Buffer.from(awarenessMessage.toUint8Array()),\n )\n }\n\n /**\n * Before the document is stored, make sure to set a lock in Redis.\n * That’s meant to avoid conflicts with other instances trying to store the document.\n */\n async onStoreDocument({ documentName }: onStoreDocumentPayload) {\n // Attempt to acquire a lock and read lastReceivedTimestamp from Redis,\n // to avoid conflict with other instances storing the same document.\n return new Promise((resolve, reject) => {\n this.redlock.lock(this.lockKey(documentName), this.configuration.lockTimeout, async (error, lock) => {\n if (error || !lock) {\n // Expected behavior: Could not acquire lock, another instance locked it already.\n // No further `onStoreDocument` hooks will be executed.\n reject()\n return\n }\n\n this.locks.set(this.lockKey(documentName), lock)\n\n resolve(undefined)\n })\n })\n }\n\n /**\n * Release the Redis lock, so other instances can store documents.\n */\n async afterStoreDocument({ documentName }: afterStoreDocumentPayload) {\n this.locks.get(this.lockKey(documentName))?.unlock()\n .catch(() => {\n // Not able to unlock Redis. The lock will expire after ${lockTimeout} ms.\n // console.error(`Not able to unlock Redis. The lock will expire after ${this.configuration.lockTimeout}ms.`)\n })\n .finally(() => {\n this.locks.delete(this.lockKey(documentName))\n })\n }\n\n /**\n * Handle awareness update messages received directly by this Hocuspocus instance.\n */\n async onAwarenessUpdate({\n documentName, awareness, added, updated, removed,\n }: onAwarenessUpdatePayload) {\n const changedClients = added.concat(updated, removed)\n const message = new OutgoingMessage(documentName)\n .createAwarenessUpdateMessage(awareness, changedClients)\n\n return this.pub.publishBuffer(\n this.pubKey(documentName),\n Buffer.from(message.toUint8Array()),\n )\n }\n\n /**\n * Handle incoming messages published on all subscribed document channels.\n * Note that this will also include messages from ourselves as it is not possible\n * in Redis to filter these.\n */\n private handleIncomingMessage = async (channel: Buffer, pattern: Buffer, data: Buffer) => {\n const message = new IncomingMessage(data)\n // we don't need the documentName from the message, we are just taking it from the redis channelName.\n // we have to immediately write it back to the encoder though, to make sure the structure of the message is correct\n message.writeVarString(message.readVarString())\n\n const channelName = pattern.toString()\n const [_, documentName, identifier] = channelName.split(':')\n const document = this.instance.documents.get(documentName)\n\n if (identifier === this.configuration.identifier) {\n return\n }\n\n if (!document) {\n return\n }\n\n new MessageReceiver(\n message,\n this.logger,\n ).apply(document, undefined, reply => {\n return this.pub.publishBuffer(\n this.pubKey(document.name),\n Buffer.from(reply),\n )\n })\n }\n\n /**\n * if the ydoc changed, we'll need to inform other Hocuspocus servers about it.\n */\n public async onChange(data: onChangePayload): Promise<any> {\n return this.publishFirstSyncStep(data.documentName, data.document)\n }\n\n /**\n * Make sure to *not* listen for further changes, when there’s\n * noone connected anymore.\n */\n public onDisconnect = async ({ documentName }: onDisconnectPayload) => {\n const disconnect = () => {\n const document = this.instance.documents.get(documentName)\n\n // Do nothing, when other users are still connected to the document.\n if (document && document.getConnectionsCount() > 0) {\n return\n }\n\n // Time to end the subscription on the document channel.\n this.sub.punsubscribe(this.subKey(documentName), (error: any) => {\n if (error) {\n console.error(error)\n }\n })\n }\n // Delay the disconnect procedure to allow last minute syncs to happen\n setTimeout(disconnect, this.configuration.disconnectDelay)\n }\n\n async beforeBroadcastStateless(data: beforeBroadcastStatelessPayload) {\n const message = new OutgoingMessage(data.documentName)\n .writeBroadcastStateless(data.payload)\n\n return this.pub.publishBuffer(\n this.pubKey(data.documentName),\n Buffer.from(message.toUint8Array()),\n )\n }\n\n /**\n * Kill the Redlock connection immediately.\n */\n async onDestroy() {\n await this.redlock.quit()\n this.pub.disconnect(false)\n this.sub.disconnect(false)\n }\n}\n"],"names":["uuid"],"mappings":"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"}
1
+ {"version":3,"file":"hocuspocus-redis.esm.js","sources":["../src/Redis.ts"],"sourcesContent":["import RedisClient, { ClusterNode, ClusterOptions, RedisOptions } from 'ioredis'\nimport Redlock from 'redlock'\nimport { v4 as uuid } from 'uuid'\nimport {\n IncomingMessage,\n OutgoingMessage,\n Document,\n Extension,\n afterLoadDocumentPayload,\n afterStoreDocumentPayload,\n onDisconnectPayload,\n onStoreDocumentPayload,\n onAwarenessUpdatePayload,\n onChangePayload,\n MessageReceiver,\n Debugger,\n onConfigurePayload,\n beforeBroadcastStatelessPayload, Hocuspocus,\n} from '@hocuspocus/server'\n\nexport type RedisInstance = RedisClient.Cluster | RedisClient.Redis\n\nexport interface Configuration {\n /**\n * Redis port\n */\n port: number,\n /**\n * Redis host\n */\n host: string,\n /**\n * Redis Cluster\n */\n nodes?: ClusterNode[],\n /**\n * Duplicate from an existed Redis instance\n */\n redis?: RedisInstance,\n /**\n * Redis instance creator\n */\n createClient?: () => RedisInstance,\n /**\n * Options passed directly to Redis constructor\n *\n * https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options\n */\n options?: ClusterOptions | RedisOptions,\n /**\n * An unique instance name, required to filter messages in Redis.\n * If none is provided an unique id is generated.\n */\n identifier: string,\n /**\n * Namespace for Redis keys, if none is provided 'hocuspocus' is used\n */\n prefix: string,\n /**\n * The maximum time for the Redis lock in ms (in case it can’t be released).\n */\n lockTimeout: number,\n /**\n * A delay before onDisconnect is executed. This allows last minute updates'\n * sync messages to be received by the subscription before it's closed.\n */\n disconnectDelay: number,\n}\n\nexport class Redis implements Extension {\n /**\n * Make sure to give that extension a higher priority, so\n * the `onStoreDocument` hook is able to intercept the chain,\n * before documents are stored to the database.\n */\n priority = 1000\n\n configuration: Configuration = {\n port: 6379,\n host: '127.0.0.1',\n prefix: 'hocuspocus',\n identifier: `host-${uuid()}`,\n lockTimeout: 1000,\n disconnectDelay: 1000,\n }\n\n pub: RedisInstance\n\n sub: RedisInstance\n\n instance!: Hocuspocus\n\n redlock: Redlock\n\n locks = new Map<string, Redlock.Lock>()\n\n logger: Debugger\n\n public constructor(configuration: Partial<Configuration>) {\n this.configuration = {\n ...this.configuration,\n ...configuration,\n }\n\n // We’ll replace that in the onConfigure hook with the global instance.\n this.logger = new Debugger()\n\n // Create Redis instance\n const {\n port,\n host,\n options,\n nodes,\n redis,\n createClient,\n } = this.configuration\n\n if (typeof createClient === 'function') {\n this.pub = createClient()\n this.sub = createClient()\n } else if (redis) {\n this.pub = redis.duplicate()\n this.sub = redis.duplicate()\n } else if (nodes && nodes.length > 0) {\n this.pub = new RedisClient.Cluster(nodes, options)\n this.sub = new RedisClient.Cluster(nodes, options)\n } else {\n this.pub = new RedisClient(port, host, options)\n this.sub = new RedisClient(port, host, options)\n }\n this.sub.on('pmessageBuffer', this.handleIncomingMessage)\n\n this.redlock = new Redlock([this.pub])\n }\n\n async onConfigure({ instance }: onConfigurePayload) {\n this.logger = instance.debugger\n this.instance = instance\n }\n\n private getKey(documentName: string) {\n return `${this.configuration.prefix}:${documentName}`\n }\n\n private pubKey(documentName: string) {\n return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`\n }\n\n private subKey(documentName: string) {\n return `${this.getKey(documentName)}:*`\n }\n\n private lockKey(documentName: string) {\n return `${this.getKey(documentName)}:lock`\n }\n\n /**\n * Once a document is loaded, subscribe to the channel in Redis.\n */\n public async afterLoadDocument({ documentName, document }: afterLoadDocumentPayload) {\n return new Promise((resolve, reject) => {\n // On document creation the node will connect to pub and sub channels\n // for the document.\n this.sub.psubscribe(this.subKey(documentName), async error => {\n if (error) {\n reject(error)\n return\n }\n\n this.publishFirstSyncStep(documentName, document)\n this.requestAwarenessFromOtherInstances(documentName)\n\n resolve(undefined)\n })\n })\n }\n\n /**\n * Publish the first sync step through Redis.\n */\n private async publishFirstSyncStep(documentName: string, document: Document) {\n const syncMessage = new OutgoingMessage(documentName)\n .createSyncMessage()\n .writeFirstSyncStepFor(document)\n\n return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(syncMessage.toUint8Array()))\n }\n\n /**\n * Let’s ask Redis who is connected already.\n */\n private async requestAwarenessFromOtherInstances(documentName: string) {\n const awarenessMessage = new OutgoingMessage(documentName)\n .writeQueryAwareness()\n\n return this.pub.publishBuffer(\n this.pubKey(documentName),\n Buffer.from(awarenessMessage.toUint8Array()),\n )\n }\n\n /**\n * Before the document is stored, make sure to set a lock in Redis.\n * That’s meant to avoid conflicts with other instances trying to store the document.\n */\n async onStoreDocument({ documentName }: onStoreDocumentPayload) {\n // Attempt to acquire a lock and read lastReceivedTimestamp from Redis,\n // to avoid conflict with other instances storing the same document.\n return new Promise((resolve, reject) => {\n this.redlock.lock(this.lockKey(documentName), this.configuration.lockTimeout, async (error, lock) => {\n if (error || !lock) {\n // Expected behavior: Could not acquire lock, another instance locked it already.\n // No further `onStoreDocument` hooks will be executed.\n reject()\n return\n }\n\n this.locks.set(this.lockKey(documentName), lock)\n\n resolve(undefined)\n })\n })\n }\n\n /**\n * Release the Redis lock, so other instances can store documents.\n */\n async afterStoreDocument({ documentName }: afterStoreDocumentPayload) {\n this.locks.get(this.lockKey(documentName))?.unlock()\n .catch(() => {\n // Not able to unlock Redis. The lock will expire after ${lockTimeout} ms.\n // console.error(`Not able to unlock Redis. The lock will expire after ${this.configuration.lockTimeout}ms.`)\n })\n .finally(() => {\n this.locks.delete(this.lockKey(documentName))\n })\n }\n\n /**\n * Handle awareness update messages received directly by this Hocuspocus instance.\n */\n async onAwarenessUpdate({\n documentName, awareness, added, updated, removed,\n }: onAwarenessUpdatePayload) {\n const changedClients = added.concat(updated, removed)\n const message = new OutgoingMessage(documentName)\n .createAwarenessUpdateMessage(awareness, changedClients)\n\n return this.pub.publishBuffer(\n this.pubKey(documentName),\n Buffer.from(message.toUint8Array()),\n )\n }\n\n /**\n * Handle incoming messages published on all subscribed document channels.\n * Note that this will also include messages from ourselves as it is not possible\n * in Redis to filter these.\n */\n private handleIncomingMessage = async (channel: Buffer, pattern: Buffer, data: Buffer) => {\n const message = new IncomingMessage(data)\n // we don't need the documentName from the message, we are just taking it from the redis channelName.\n // we have to immediately write it back to the encoder though, to make sure the structure of the message is correct\n message.writeVarString(message.readVarString())\n\n const channelName = pattern.toString()\n const [_, documentName, identifier] = channelName.split(':')\n const document = this.instance.documents.get(documentName)\n\n if (identifier === this.configuration.identifier) {\n return\n }\n\n if (!document) {\n return\n }\n\n new MessageReceiver(\n message,\n this.logger,\n ).apply(document, undefined, reply => {\n return this.pub.publishBuffer(\n this.pubKey(document.name),\n Buffer.from(reply),\n )\n })\n }\n\n /**\n * if the ydoc changed, we'll need to inform other Hocuspocus servers about it.\n */\n public async onChange(data: onChangePayload): Promise<any> {\n return this.publishFirstSyncStep(data.documentName, data.document)\n }\n\n /**\n * Make sure to *not* listen for further changes, when there’s\n * noone connected anymore.\n */\n public onDisconnect = async ({ documentName }: onDisconnectPayload) => {\n const disconnect = () => {\n const document = this.instance.documents.get(documentName)\n\n // Do nothing, when other users are still connected to the document.\n if (document && document.getConnectionsCount() > 0) {\n return\n }\n\n // Time to end the subscription on the document channel.\n this.sub.punsubscribe(this.subKey(documentName), (error: any) => {\n if (error) {\n console.error(error)\n }\n })\n }\n // Delay the disconnect procedure to allow last minute syncs to happen\n setTimeout(disconnect, this.configuration.disconnectDelay)\n }\n\n async beforeBroadcastStateless(data: beforeBroadcastStatelessPayload) {\n const message = new OutgoingMessage(data.documentName)\n .writeBroadcastStateless(data.payload)\n\n return this.pub.publishBuffer(\n this.pubKey(data.documentName),\n Buffer.from(message.toUint8Array()),\n )\n }\n\n /**\n * Kill the Redlock connection immediately.\n */\n async onDestroy() {\n await this.redlock.quit()\n this.pub.disconnect(false)\n this.sub.disconnect(false)\n }\n}\n"],"names":["uuid"],"mappings":"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"}
@@ -69,7 +69,7 @@ export declare class Redis implements Extension {
69
69
  private subKey;
70
70
  private lockKey;
71
71
  /**
72
- * Once a document is laoded, subscribe to the channel in Redis.
72
+ * Once a document is loaded, subscribe to the channel in Redis.
73
73
  */
74
74
  afterLoadDocument({ documentName, document }: afterLoadDocumentPayload): Promise<unknown>;
75
75
  /**
@@ -1,15 +1,15 @@
1
- import * as Y from 'yjs';
2
- import { Awareness } from 'y-protocols/awareness';
3
1
  import * as mutex from 'lib0/mutex';
4
2
  import type { CloseEvent, Event, MessageEvent } from 'ws';
3
+ import { Awareness } from 'y-protocols/awareness';
4
+ import * as Y from 'yjs';
5
5
  import EventEmitter from './EventEmitter.js';
6
- import { ConstructableOutgoingMessage, onAuthenticationFailedParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatelessParameters, onStatusParameters, onSyncedParameters, WebSocketStatus, onAwarenessChangeParameters, onAwarenessUpdateParameters } from './types.js';
7
6
  import { CompleteHocuspocusProviderWebsocketConfiguration, HocuspocusProviderWebsocket } from './HocuspocusProviderWebsocket.js';
7
+ import { ConstructableOutgoingMessage, WebSocketStatus, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatelessParameters, onStatusParameters, onSyncedParameters } from './types.js';
8
8
  export type HocuspocusProviderConfiguration = Required<Pick<CompleteHocuspocusProviderConfiguration, 'name'>> & Partial<CompleteHocuspocusProviderConfiguration> & (Required<Pick<CompleteHocuspocusProviderWebsocketConfiguration, 'url'>> | Required<Pick<CompleteHocuspocusProviderConfiguration, 'websocketProvider'>>);
9
9
  export interface CompleteHocuspocusProviderConfiguration {
10
10
  /**
11
- * The identifier/name of your document
12
- */
11
+ * The identifier/name of your document
12
+ */
13
13
  name: string;
14
14
  /**
15
15
  * The actual Y.js document
@@ -59,6 +59,14 @@ export interface CompleteHocuspocusProviderConfiguration {
59
59
  * Don’t output any warnings.
60
60
  */
61
61
  quiet: boolean;
62
+ /**
63
+ * Pass `false` to start the connection manually.
64
+ */
65
+ connect: boolean;
66
+ /**
67
+ * Pass `false` to close the connection manually.
68
+ */
69
+ preserveConnection: boolean;
62
70
  }
63
71
  export declare class HocuspocusProvider extends EventEmitter {
64
72
  configuration: CompleteHocuspocusProviderConfiguration;
@@ -73,7 +81,7 @@ export declare class HocuspocusProvider extends EventEmitter {
73
81
  isConnected: boolean;
74
82
  constructor(configuration: HocuspocusProviderConfiguration);
75
83
  boundBroadcastChannelSubscriber: (data: ArrayBuffer) => void;
76
- boundBeforeUnload: () => void;
84
+ boundPageUnload: () => void;
77
85
  boundOnOpen: (event: Event) => Promise<void>;
78
86
  boundOnMessage: (event: MessageEvent) => void;
79
87
  boundOnClose: (event: CloseEvent) => void;
@@ -88,13 +96,20 @@ export declare class HocuspocusProvider extends EventEmitter {
88
96
  get document(): Y.Doc;
89
97
  get awareness(): Awareness;
90
98
  get hasUnsyncedChanges(): boolean;
91
- updateUnsyncedChanges(unsyncedChanges?: number): void;
99
+ incrementUnsyncedChanges(): void;
100
+ decrementUnsyncedChanges(): void;
92
101
  forceSync(): void;
93
- beforeUnload(): void;
102
+ pageUnload(): void;
94
103
  registerEventListeners(): void;
95
104
  sendStateless(payload: string): void;
96
105
  documentUpdateHandler(update: Uint8Array, origin: any): void;
97
106
  awarenessUpdateHandler({ added, updated, removed }: any, origin: any): void;
107
+ /**
108
+ * Indicates whether a first handshake with the server has been established
109
+ *
110
+ * Note: this does not mean all updates from the client have been persisted to the backend. For this,
111
+ * use `hasUnsyncedChanges`.
112
+ */
98
113
  get synced(): boolean;
99
114
  set synced(state: boolean);
100
115
  receiveStateless(payload: string): void;
@@ -2,8 +2,8 @@ import * as mutex from 'lib0/mutex';
2
2
  import type { MessageEvent } from 'ws';
3
3
  import { Event } from 'ws';
4
4
  import EventEmitter from './EventEmitter.js';
5
- import { onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatusParameters, WebSocketStatus, onAwarenessChangeParameters, onAwarenessUpdateParameters } from './types.js';
6
5
  import { HocuspocusProvider } from './HocuspocusProvider.js';
6
+ import { WebSocketStatus, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatusParameters } from './types.js';
7
7
  export type HocuspocusProviderWebsocketConfiguration = Required<Pick<CompleteHocuspocusProviderWebsocketConfiguration, 'url'>> & Partial<CompleteHocuspocusProviderWebsocketConfiguration>;
8
8
  export interface CompleteHocuspocusProviderWebsocketConfiguration {
9
9
  /**
@@ -76,6 +76,7 @@ export interface CompleteHocuspocusProviderWebsocketConfiguration {
76
76
  quiet: boolean;
77
77
  }
78
78
  export declare class HocuspocusProviderWebsocket extends EventEmitter {
79
+ private messageQueue;
79
80
  configuration: CompleteHocuspocusProviderWebsocketConfiguration;
80
81
  subscribedToBroadcastChannel: boolean;
81
82
  webSocket: WebSocket | null;
@@ -104,6 +105,7 @@ export declare class HocuspocusProviderWebsocket extends EventEmitter {
104
105
  resolveConnectionAttempt(): void;
105
106
  stopConnectionAttempt(): void;
106
107
  rejectConnectionAttempt(): void;
108
+ closeTries: number;
107
109
  checkConnection(): void;
108
110
  registerEventListeners(): void;
109
111
  get serverUrl(): string;
@@ -5,8 +5,9 @@ export declare class MessageReceiver {
5
5
  broadcasted: boolean;
6
6
  constructor(message: IncomingMessage);
7
7
  setBroadcasted(value: boolean): this;
8
- apply(provider: HocuspocusProvider, emitSynced?: boolean): void;
8
+ apply(provider: HocuspocusProvider, emitSynced: boolean): void;
9
9
  private applySyncMessage;
10
+ applySyncStatusMessage(provider: HocuspocusProvider, applied: boolean): void;
10
11
  private applyAwarenessMessage;
11
12
  private applyAuthMessage;
12
13
  private applyQueryAwarenessMessage;
@@ -23,6 +23,6 @@ export declare class TiptapCollabProvider extends HocuspocusProvider {
23
23
  watchVersions(callback: Parameters<AbstractType<YArrayEvent<AuditHistoryVersion>>['observe']>[0]): void;
24
24
  unwatchVersions(callback: Parameters<AbstractType<YArrayEvent<AuditHistoryVersion>>['unobserve']>[0]): void;
25
25
  isAutoVersioning(): boolean;
26
- enableAutoVersioning(): number;
27
- disableAutoVersioning(): number;
26
+ enableAutoVersioning(): 1;
27
+ disableAutoVersioning(): 0;
28
28
  }
@@ -1,7 +1,8 @@
1
1
  import { Encoder } from 'lib0/encoding';
2
- import type { CloseEvent, Event, MessageEvent } from 'ws';
2
+ import type { Event, MessageEvent } from 'ws';
3
3
  import { Awareness } from 'y-protocols/awareness';
4
4
  import * as Y from 'yjs';
5
+ import { CloseEvent } from '@hocuspocus/common';
5
6
  import { IncomingMessage } from './IncomingMessage.js';
6
7
  import { OutgoingMessage } from './OutgoingMessage.js';
7
8
  import { AuthenticationMessage } from './OutgoingMessages/AuthenticationMessage.js';
@@ -5,7 +5,7 @@ import WebSocket, { AddressInfo, WebSocketServer } from 'ws';
5
5
  import { Debugger } from './Debugger.js';
6
6
  import { DirectConnection } from './DirectConnection.js';
7
7
  import Document from './Document.js';
8
- import { Configuration, ConnectionConfiguration, HookName, HookPayload, onListenPayload, onStoreDocumentPayload } from './types.js';
8
+ import { Configuration, ConnectionConfiguration, HookName, HookPayloadByName, onListenPayload, onStoreDocumentPayload } from './types.js';
9
9
  export declare const defaultConfiguration: {
10
10
  name: null;
11
11
  port: number;
@@ -91,7 +91,8 @@ export declare class Hocuspocus {
91
91
  * Run the given hook on all configured extensions.
92
92
  * Runs the given callback after each hook.
93
93
  */
94
- hooks(name: HookName, payload: HookPayload, callback?: Function | null): Promise<any>;
94
+ hooks<T extends HookName>(name: T, payload: HookPayloadByName[T], callback?: Function | null): Promise<any>;
95
+ unloadDocument(document: Document): void;
95
96
  enableDebugging(): void;
96
97
  enableMessageLogging(): void;
97
98
  disableLogging(): void;
@@ -1,7 +1,7 @@
1
1
  import Connection from './Connection.js';
2
- import { IncomingMessage } from './IncomingMessage.js';
3
2
  import { Debugger } from './Debugger.js';
4
3
  import Document from './Document.js';
4
+ import { IncomingMessage } from './IncomingMessage.js';
5
5
  export declare class MessageReceiver {
6
6
  message: IncomingMessage;
7
7
  logger: Debugger;
@@ -16,5 +16,6 @@ export declare class OutgoingMessage {
16
16
  writeUpdate(update: Uint8Array): OutgoingMessage;
17
17
  writeStateless(payload: string): OutgoingMessage;
18
18
  writeBroadcastStateless(payload: string): OutgoingMessage;
19
+ writeSyncStatus(updateSaved: boolean): OutgoingMessage;
19
20
  toUint8Array(): Uint8Array;
20
21
  }
@@ -4,9 +4,9 @@
4
4
  import { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'http';
5
5
  import { URLSearchParams } from 'url';
6
6
  import { Awareness } from 'y-protocols/awareness';
7
+ import Connection from './Connection.js';
7
8
  import Document from './Document.js';
8
9
  import { Hocuspocus } from './Hocuspocus.js';
9
- import Connection from './Connection.js';
10
10
  export declare enum MessageType {
11
11
  Unknown = -1,
12
12
  Sync = 0,
@@ -16,7 +16,8 @@ export declare enum MessageType {
16
16
  SyncReply = 4,
17
17
  Stateless = 5,
18
18
  BroadcastStateless = 6,
19
- CLOSE = 7
19
+ CLOSE = 7,
20
+ SyncStatus = 8
20
21
  }
21
22
  export interface AwarenessUpdate {
22
23
  added: Array<any>;
@@ -47,10 +48,31 @@ export interface Extension {
47
48
  onAwarenessUpdate?(data: onAwarenessUpdatePayload): Promise<any>;
48
49
  onRequest?(data: onRequestPayload): Promise<any>;
49
50
  onDisconnect?(data: onDisconnectPayload): Promise<any>;
51
+ afterUnloadDocument?(data: onLoadDocumentPayload): Promise<any>;
50
52
  onDestroy?(data: onDestroyPayload): Promise<any>;
51
53
  }
52
- export type HookName = 'onConfigure' | 'onListen' | 'onUpgrade' | 'onConnect' | 'connected' | 'onAuthenticate' | 'onLoadDocument' | 'afterLoadDocument' | 'beforeHandleMessage' | 'beforeBroadcastStateless' | 'onStateless' | 'onChange' | 'onStoreDocument' | 'afterStoreDocument' | 'onAwarenessUpdate' | 'onRequest' | 'onDisconnect' | 'onDestroy';
53
- export type HookPayload = onConfigurePayload | onListenPayload | onUpgradePayload | onConnectPayload | connectedPayload | onAuthenticatePayload | onLoadDocumentPayload | onStatelessPayload | beforeHandleMessagePayload | beforeBroadcastStatelessPayload | onChangePayload | onStoreDocumentPayload | afterStoreDocumentPayload | onAwarenessUpdatePayload | onRequestPayload | onDisconnectPayload | onDestroyPayload;
54
+ export type HookName = 'onConfigure' | 'onListen' | 'onUpgrade' | 'onConnect' | 'connected' | 'onAuthenticate' | 'onLoadDocument' | 'afterLoadDocument' | 'beforeHandleMessage' | 'beforeBroadcastStateless' | 'onStateless' | 'onChange' | 'onStoreDocument' | 'afterStoreDocument' | 'onAwarenessUpdate' | 'onRequest' | 'onDisconnect' | 'afterUnloadDocument' | 'onDestroy';
55
+ export type HookPayloadByName = {
56
+ onConfigure: onConfigurePayload;
57
+ onListen: onListenPayload;
58
+ onUpgrade: onUpgradePayload;
59
+ onConnect: onConnectPayload;
60
+ connected: connectedPayload;
61
+ onAuthenticate: onAuthenticatePayload;
62
+ onLoadDocument: onLoadDocumentPayload;
63
+ afterLoadDocument: onLoadDocumentPayload;
64
+ beforeHandleMessage: beforeHandleMessagePayload;
65
+ beforeBroadcastStateless: beforeBroadcastStatelessPayload;
66
+ onStateless: onStatelessPayload;
67
+ onChange: onChangePayload;
68
+ onStoreDocument: onStoreDocumentPayload;
69
+ afterStoreDocument: afterStoreDocumentPayload;
70
+ onAwarenessUpdate: onAwarenessUpdatePayload;
71
+ onRequest: onRequestPayload;
72
+ onDisconnect: onDisconnectPayload;
73
+ afterUnloadDocument: afterUnloadDocumentPayload;
74
+ onDestroy: onDestroyPayload;
75
+ };
54
76
  export interface Configuration extends Extension {
55
77
  /**
56
78
  * A name for the instance, used for logging.
@@ -198,14 +220,12 @@ export interface onStoreDocumentPayload {
198
220
  export interface afterStoreDocumentPayload extends onStoreDocumentPayload {
199
221
  }
200
222
  export interface onAwarenessUpdatePayload {
201
- clientsCount: number;
202
223
  context: any;
203
224
  document: Document;
204
225
  documentName: string;
205
226
  instance: Hocuspocus;
206
227
  requestHeaders: IncomingHttpHeaders;
207
228
  requestParameters: URLSearchParams;
208
- update: Uint8Array;
209
229
  socketId: string;
210
230
  added: number[];
211
231
  updated: number[];
@@ -264,6 +284,10 @@ export interface onConfigurePayload {
264
284
  configuration: Configuration;
265
285
  version: string;
266
286
  }
287
+ export interface afterUnloadDocumentPayload {
288
+ instance: Hocuspocus;
289
+ documentName: string;
290
+ }
267
291
  export interface DirectConnection {
268
292
  transact(transaction: (document: Document) => void): Promise<void>;
269
293
  disconnect(): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hocuspocus/extension-redis",
3
- "version": "2.2.3",
3
+ "version": "2.3.1",
4
4
  "description": "Scale Hocuspocus horizontally with Redis",
5
5
  "homepage": "https://hocuspocus.dev",
6
6
  "keywords": [
@@ -33,7 +33,7 @@
33
33
  "@types/redlock": "^4.0.3"
34
34
  },
35
35
  "dependencies": {
36
- "@hocuspocus/server": "^2.2.3",
36
+ "@hocuspocus/server": "^2.3.1",
37
37
  "ioredis": "^4.28.2",
38
38
  "kleur": "^4.1.4",
39
39
  "lodash.debounce": "^4.0.8",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "y-protocols": "^1.0.5",
45
- "yjs": "^13.5.23"
45
+ "yjs": "^13.6.4"
46
46
  },
47
47
  "gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d"
48
48
  }
package/src/Redis.ts CHANGED
@@ -155,7 +155,7 @@ export class Redis implements Extension {
155
155
  }
156
156
 
157
157
  /**
158
- * Once a document is laoded, subscribe to the channel in Redis.
158
+ * Once a document is loaded, subscribe to the channel in Redis.
159
159
  */
160
160
  public async afterLoadDocument({ documentName, document }: afterLoadDocumentPayload) {
161
161
  return new Promise((resolve, reject) => {