@hocuspocus/extension-redis 2.5.0-rc.0 → 2.5.0

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.
@@ -28,28 +28,29 @@ class Redis {
28
28
  lockTimeout: 1000,
29
29
  disconnectDelay: 1000,
30
30
  };
31
+ this.redisTransactionOrigin = '__hocuspocus__redis__origin__';
31
32
  this.locks = new Map();
32
33
  /**
33
- * Handle incoming messages published on all subscribed document channels.
34
+ * Handle incoming messages published on subscribed document channels.
34
35
  * Note that this will also include messages from ourselves as it is not possible
35
36
  * in Redis to filter these.
36
37
  */
37
- this.handleIncomingMessage = async (channel, pattern, data) => {
38
- const message = new server.IncomingMessage(data);
39
- // we don't need the documentName from the message, we are just taking it from the redis channelName.
40
- // we have to immediately write it back to the encoder though, to make sure the structure of the message is correct
41
- message.writeVarString(message.readVarString());
42
- const channelName = pattern.toString();
43
- const [_, documentName, identifier] = channelName.split(':');
44
- const document = this.instance.documents.get(documentName);
38
+ this.handleIncomingMessage = async (channel, data) => {
39
+ const [identifier, messageBuffer] = this.decodeMessage(data);
45
40
  if (identifier === this.configuration.identifier) {
46
41
  return;
47
42
  }
43
+ const message = new server.IncomingMessage(messageBuffer);
44
+ const documentName = message.readVarString();
45
+ message.writeVarString(documentName);
46
+ const document = this.instance.documents.get(documentName);
48
47
  if (!document) {
48
+ // What does this mean? Why are we subscribed to this document?
49
+ this.logger.log(`Received message for unknown document ${documentName}`);
49
50
  return;
50
51
  }
51
- new server.MessageReceiver(message, this.logger).apply(document, undefined, reply => {
52
- return this.pub.publishBuffer(this.pubKey(document.name), Buffer.from(reply));
52
+ new server.MessageReceiver(message, this.logger, this.redisTransactionOrigin).apply(document, undefined, reply => {
53
+ return this.pub.publishBuffer(this.pubKey(document.name), this.encodeMessage(reply));
53
54
  });
54
55
  };
55
56
  /**
@@ -64,7 +65,7 @@ class Redis {
64
65
  return;
65
66
  }
66
67
  // Time to end the subscription on the document channel.
67
- this.sub.punsubscribe(this.subKey(documentName), (error) => {
68
+ this.sub.unsubscribe(this.subKey(documentName), (error) => {
68
69
  if (error) {
69
70
  console.error(error);
70
71
  }
@@ -97,8 +98,10 @@ class Redis {
97
98
  this.pub = new RedisClient__default["default"](port, host, options);
98
99
  this.sub = new RedisClient__default["default"](port, host, options);
99
100
  }
100
- this.sub.on('pmessageBuffer', this.handleIncomingMessage);
101
+ this.sub.on('messageBuffer', this.handleIncomingMessage);
101
102
  this.redlock = new Redlock__default["default"]([this.pub]);
103
+ const identifierBuffer = Buffer.from(this.configuration.identifier, 'utf-8');
104
+ this.messagePrefix = Buffer.concat([Buffer.from([identifierBuffer.length]), identifierBuffer]);
102
105
  }
103
106
  async onConfigure({ instance }) {
104
107
  this.logger = instance.debugger;
@@ -108,14 +111,22 @@ class Redis {
108
111
  return `${this.configuration.prefix}:${documentName}`;
109
112
  }
110
113
  pubKey(documentName) {
111
- return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`;
114
+ return this.getKey(documentName);
112
115
  }
113
116
  subKey(documentName) {
114
- return `${this.getKey(documentName)}:*`;
117
+ return this.getKey(documentName);
115
118
  }
116
119
  lockKey(documentName) {
117
120
  return `${this.getKey(documentName)}:lock`;
118
121
  }
122
+ encodeMessage(message) {
123
+ return Buffer.concat([this.messagePrefix, Buffer.from(message)]);
124
+ }
125
+ decodeMessage(buffer) {
126
+ const identifierLength = buffer[0];
127
+ const identifier = buffer.toString('utf-8', 1, identifierLength + 1);
128
+ return [identifier, buffer.slice(identifierLength + 1)];
129
+ }
119
130
  /**
120
131
  * Once a document is loaded, subscribe to the channel in Redis.
121
132
  */
@@ -123,7 +134,7 @@ class Redis {
123
134
  return new Promise((resolve, reject) => {
124
135
  // On document creation the node will connect to pub and sub channels
125
136
  // for the document.
126
- this.sub.psubscribe(this.subKey(documentName), async (error) => {
137
+ this.sub.subscribe(this.subKey(documentName), async (error) => {
127
138
  if (error) {
128
139
  reject(error);
129
140
  return;
@@ -141,7 +152,7 @@ class Redis {
141
152
  const syncMessage = new server.OutgoingMessage(documentName)
142
153
  .createSyncMessage()
143
154
  .writeFirstSyncStepFor(document);
144
- return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(syncMessage.toUint8Array()));
155
+ return this.pub.publishBuffer(this.pubKey(documentName), this.encodeMessage(syncMessage.toUint8Array()));
145
156
  }
146
157
  /**
147
158
  * Let’s ask Redis who is connected already.
@@ -149,7 +160,7 @@ class Redis {
149
160
  async requestAwarenessFromOtherInstances(documentName) {
150
161
  const awarenessMessage = new server.OutgoingMessage(documentName)
151
162
  .writeQueryAwareness();
152
- return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(awarenessMessage.toUint8Array()));
163
+ return this.pub.publishBuffer(this.pubKey(documentName), this.encodeMessage(awarenessMessage.toUint8Array()));
153
164
  }
154
165
  /**
155
166
  * Before the document is stored, make sure to set a lock in Redis.
@@ -190,18 +201,20 @@ class Redis {
190
201
  const changedClients = added.concat(updated, removed);
191
202
  const message = new server.OutgoingMessage(documentName)
192
203
  .createAwarenessUpdateMessage(awareness, changedClients);
193
- return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(message.toUint8Array()));
204
+ return this.pub.publishBuffer(this.pubKey(documentName), this.encodeMessage(message.toUint8Array()));
194
205
  }
195
206
  /**
196
207
  * if the ydoc changed, we'll need to inform other Hocuspocus servers about it.
197
208
  */
198
209
  async onChange(data) {
199
- return this.publishFirstSyncStep(data.documentName, data.document);
210
+ if (data.transactionOrigin !== this.redisTransactionOrigin) {
211
+ return this.publishFirstSyncStep(data.documentName, data.document);
212
+ }
200
213
  }
201
214
  async beforeBroadcastStateless(data) {
202
215
  const message = new server.OutgoingMessage(data.documentName)
203
216
  .writeBroadcastStateless(data.payload);
204
- return this.pub.publishBuffer(this.pubKey(data.documentName), Buffer.from(message.toUint8Array()));
217
+ return this.pub.publishBuffer(this.pubKey(data.documentName), this.encodeMessage(message.toUint8Array()));
205
218
  }
206
219
  /**
207
220
  * Kill the Redlock connection immediately.
@@ -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 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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"}
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 redisTransactionOrigin = '__hocuspocus__redis__origin__'\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 messagePrefix: Buffer\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('messageBuffer', this.handleIncomingMessage)\n\n this.redlock = new Redlock([this.pub])\n\n const identifierBuffer = Buffer.from(this.configuration.identifier, 'utf-8')\n this.messagePrefix = Buffer.concat([Buffer.from([identifierBuffer.length]), identifierBuffer])\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)\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 private encodeMessage(message: Uint8Array) {\n return Buffer.concat([this.messagePrefix, Buffer.from(message)])\n }\n\n private decodeMessage(buffer: Buffer) {\n const identifierLength = buffer[0]\n const identifier = buffer.toString('utf-8', 1, identifierLength + 1)\n\n return [identifier, buffer.slice(identifierLength + 1)]\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.subscribe(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), this.encodeMessage(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 this.encodeMessage(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 this.encodeMessage(message.toUint8Array()),\n )\n }\n\n /**\n * Handle incoming messages published on 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, data: Buffer) => {\n const [identifier, messageBuffer] = this.decodeMessage(data)\n\n if (identifier === this.configuration.identifier) {\n return\n }\n\n const message = new IncomingMessage(messageBuffer)\n const documentName = message.readVarString()\n message.writeVarString(documentName)\n\n const document = this.instance.documents.get(documentName)\n\n if (!document) {\n // What does this mean? Why are we subscribed to this document?\n this.logger.log(`Received message for unknown document ${documentName}`)\n return\n }\n\n new MessageReceiver(\n message,\n this.logger,\n this.redisTransactionOrigin,\n ).apply(document, undefined, reply => {\n return this.pub.publishBuffer(\n this.pubKey(document.name),\n this.encodeMessage(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 if (data.transactionOrigin !== this.redisTransactionOrigin) {\n return this.publishFirstSyncStep(data.documentName, data.document)\n }\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.unsubscribe(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 this.encodeMessage(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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"}
@@ -19,28 +19,29 @@ class Redis {
19
19
  lockTimeout: 1000,
20
20
  disconnectDelay: 1000,
21
21
  };
22
+ this.redisTransactionOrigin = '__hocuspocus__redis__origin__';
22
23
  this.locks = new Map();
23
24
  /**
24
- * Handle incoming messages published on all subscribed document channels.
25
+ * Handle incoming messages published on subscribed document channels.
25
26
  * Note that this will also include messages from ourselves as it is not possible
26
27
  * in Redis to filter these.
27
28
  */
28
- this.handleIncomingMessage = async (channel, pattern, data) => {
29
- const message = new IncomingMessage(data);
30
- // we don't need the documentName from the message, we are just taking it from the redis channelName.
31
- // we have to immediately write it back to the encoder though, to make sure the structure of the message is correct
32
- message.writeVarString(message.readVarString());
33
- const channelName = pattern.toString();
34
- const [_, documentName, identifier] = channelName.split(':');
35
- const document = this.instance.documents.get(documentName);
29
+ this.handleIncomingMessage = async (channel, data) => {
30
+ const [identifier, messageBuffer] = this.decodeMessage(data);
36
31
  if (identifier === this.configuration.identifier) {
37
32
  return;
38
33
  }
34
+ const message = new IncomingMessage(messageBuffer);
35
+ const documentName = message.readVarString();
36
+ message.writeVarString(documentName);
37
+ const document = this.instance.documents.get(documentName);
39
38
  if (!document) {
39
+ // What does this mean? Why are we subscribed to this document?
40
+ this.logger.log(`Received message for unknown document ${documentName}`);
40
41
  return;
41
42
  }
42
- new MessageReceiver(message, this.logger).apply(document, undefined, reply => {
43
- return this.pub.publishBuffer(this.pubKey(document.name), Buffer.from(reply));
43
+ new MessageReceiver(message, this.logger, this.redisTransactionOrigin).apply(document, undefined, reply => {
44
+ return this.pub.publishBuffer(this.pubKey(document.name), this.encodeMessage(reply));
44
45
  });
45
46
  };
46
47
  /**
@@ -55,7 +56,7 @@ class Redis {
55
56
  return;
56
57
  }
57
58
  // Time to end the subscription on the document channel.
58
- this.sub.punsubscribe(this.subKey(documentName), (error) => {
59
+ this.sub.unsubscribe(this.subKey(documentName), (error) => {
59
60
  if (error) {
60
61
  console.error(error);
61
62
  }
@@ -88,8 +89,10 @@ class Redis {
88
89
  this.pub = new RedisClient(port, host, options);
89
90
  this.sub = new RedisClient(port, host, options);
90
91
  }
91
- this.sub.on('pmessageBuffer', this.handleIncomingMessage);
92
+ this.sub.on('messageBuffer', this.handleIncomingMessage);
92
93
  this.redlock = new Redlock([this.pub]);
94
+ const identifierBuffer = Buffer.from(this.configuration.identifier, 'utf-8');
95
+ this.messagePrefix = Buffer.concat([Buffer.from([identifierBuffer.length]), identifierBuffer]);
93
96
  }
94
97
  async onConfigure({ instance }) {
95
98
  this.logger = instance.debugger;
@@ -99,14 +102,22 @@ class Redis {
99
102
  return `${this.configuration.prefix}:${documentName}`;
100
103
  }
101
104
  pubKey(documentName) {
102
- return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`;
105
+ return this.getKey(documentName);
103
106
  }
104
107
  subKey(documentName) {
105
- return `${this.getKey(documentName)}:*`;
108
+ return this.getKey(documentName);
106
109
  }
107
110
  lockKey(documentName) {
108
111
  return `${this.getKey(documentName)}:lock`;
109
112
  }
113
+ encodeMessage(message) {
114
+ return Buffer.concat([this.messagePrefix, Buffer.from(message)]);
115
+ }
116
+ decodeMessage(buffer) {
117
+ const identifierLength = buffer[0];
118
+ const identifier = buffer.toString('utf-8', 1, identifierLength + 1);
119
+ return [identifier, buffer.slice(identifierLength + 1)];
120
+ }
110
121
  /**
111
122
  * Once a document is loaded, subscribe to the channel in Redis.
112
123
  */
@@ -114,7 +125,7 @@ class Redis {
114
125
  return new Promise((resolve, reject) => {
115
126
  // On document creation the node will connect to pub and sub channels
116
127
  // for the document.
117
- this.sub.psubscribe(this.subKey(documentName), async (error) => {
128
+ this.sub.subscribe(this.subKey(documentName), async (error) => {
118
129
  if (error) {
119
130
  reject(error);
120
131
  return;
@@ -132,7 +143,7 @@ class Redis {
132
143
  const syncMessage = new OutgoingMessage(documentName)
133
144
  .createSyncMessage()
134
145
  .writeFirstSyncStepFor(document);
135
- return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(syncMessage.toUint8Array()));
146
+ return this.pub.publishBuffer(this.pubKey(documentName), this.encodeMessage(syncMessage.toUint8Array()));
136
147
  }
137
148
  /**
138
149
  * Let’s ask Redis who is connected already.
@@ -140,7 +151,7 @@ class Redis {
140
151
  async requestAwarenessFromOtherInstances(documentName) {
141
152
  const awarenessMessage = new OutgoingMessage(documentName)
142
153
  .writeQueryAwareness();
143
- return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(awarenessMessage.toUint8Array()));
154
+ return this.pub.publishBuffer(this.pubKey(documentName), this.encodeMessage(awarenessMessage.toUint8Array()));
144
155
  }
145
156
  /**
146
157
  * Before the document is stored, make sure to set a lock in Redis.
@@ -181,18 +192,20 @@ class Redis {
181
192
  const changedClients = added.concat(updated, removed);
182
193
  const message = new OutgoingMessage(documentName)
183
194
  .createAwarenessUpdateMessage(awareness, changedClients);
184
- return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(message.toUint8Array()));
195
+ return this.pub.publishBuffer(this.pubKey(documentName), this.encodeMessage(message.toUint8Array()));
185
196
  }
186
197
  /**
187
198
  * if the ydoc changed, we'll need to inform other Hocuspocus servers about it.
188
199
  */
189
200
  async onChange(data) {
190
- return this.publishFirstSyncStep(data.documentName, data.document);
201
+ if (data.transactionOrigin !== this.redisTransactionOrigin) {
202
+ return this.publishFirstSyncStep(data.documentName, data.document);
203
+ }
191
204
  }
192
205
  async beforeBroadcastStateless(data) {
193
206
  const message = new OutgoingMessage(data.documentName)
194
207
  .writeBroadcastStateless(data.payload);
195
- return this.pub.publishBuffer(this.pubKey(data.documentName), Buffer.from(message.toUint8Array()));
208
+ return this.pub.publishBuffer(this.pubKey(data.documentName), this.encodeMessage(message.toUint8Array()));
196
209
  }
197
210
  /**
198
211
  * Kill the Redlock connection immediately.
@@ -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 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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"}
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 redisTransactionOrigin = '__hocuspocus__redis__origin__'\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 messagePrefix: Buffer\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('messageBuffer', this.handleIncomingMessage)\n\n this.redlock = new Redlock([this.pub])\n\n const identifierBuffer = Buffer.from(this.configuration.identifier, 'utf-8')\n this.messagePrefix = Buffer.concat([Buffer.from([identifierBuffer.length]), identifierBuffer])\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)\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 private encodeMessage(message: Uint8Array) {\n return Buffer.concat([this.messagePrefix, Buffer.from(message)])\n }\n\n private decodeMessage(buffer: Buffer) {\n const identifierLength = buffer[0]\n const identifier = buffer.toString('utf-8', 1, identifierLength + 1)\n\n return [identifier, buffer.slice(identifierLength + 1)]\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.subscribe(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), this.encodeMessage(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 this.encodeMessage(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 this.encodeMessage(message.toUint8Array()),\n )\n }\n\n /**\n * Handle incoming messages published on 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, data: Buffer) => {\n const [identifier, messageBuffer] = this.decodeMessage(data)\n\n if (identifier === this.configuration.identifier) {\n return\n }\n\n const message = new IncomingMessage(messageBuffer)\n const documentName = message.readVarString()\n message.writeVarString(documentName)\n\n const document = this.instance.documents.get(documentName)\n\n if (!document) {\n // What does this mean? Why are we subscribed to this document?\n this.logger.log(`Received message for unknown document ${documentName}`)\n return\n }\n\n new MessageReceiver(\n message,\n this.logger,\n this.redisTransactionOrigin,\n ).apply(document, undefined, reply => {\n return this.pub.publishBuffer(\n this.pubKey(document.name),\n this.encodeMessage(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 if (data.transactionOrigin !== this.redisTransactionOrigin) {\n return this.publishFirstSyncStep(data.documentName, data.document)\n }\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.unsubscribe(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 this.encodeMessage(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,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import RedisClient, { ClusterNode, ClusterOptions, RedisOptions } from 'ioredis';
2
3
  import Redlock from 'redlock';
3
4
  import { Extension, afterLoadDocumentPayload, afterStoreDocumentPayload, onDisconnectPayload, onStoreDocumentPayload, onAwarenessUpdatePayload, onChangePayload, Debugger, onConfigurePayload, beforeBroadcastStatelessPayload, Hocuspocus } from '@hocuspocus/server';
@@ -56,18 +57,22 @@ export declare class Redis implements Extension {
56
57
  */
57
58
  priority: number;
58
59
  configuration: Configuration;
60
+ redisTransactionOrigin: string;
59
61
  pub: RedisInstance;
60
62
  sub: RedisInstance;
61
63
  instance: Hocuspocus;
62
64
  redlock: Redlock;
63
65
  locks: Map<string, Redlock.Lock>;
64
66
  logger: Debugger;
67
+ messagePrefix: Buffer;
65
68
  constructor(configuration: Partial<Configuration>);
66
69
  onConfigure({ instance }: onConfigurePayload): Promise<void>;
67
70
  private getKey;
68
71
  private pubKey;
69
72
  private subKey;
70
73
  private lockKey;
74
+ private encodeMessage;
75
+ private decodeMessage;
71
76
  /**
72
77
  * Once a document is loaded, subscribe to the channel in Redis.
73
78
  */
@@ -94,7 +99,7 @@ export declare class Redis implements Extension {
94
99
  */
95
100
  onAwarenessUpdate({ documentName, awareness, added, updated, removed, }: onAwarenessUpdatePayload): Promise<number>;
96
101
  /**
97
- * Handle incoming messages published on all subscribed document channels.
102
+ * Handle incoming messages published on subscribed document channels.
98
103
  * Note that this will also include messages from ourselves as it is not possible
99
104
  * in Redis to filter these.
100
105
  */
@@ -1,5 +1,4 @@
1
1
  /// <reference types="node" />
2
- /// <reference types="node" />
3
2
  import { IncomingMessage } from 'http';
4
3
  import WebSocket, { AddressInfo } from 'ws';
5
4
  import { Server as HocuspocusServer } from './Server';
@@ -29,6 +28,7 @@ export declare class Hocuspocus {
29
28
  documents: Map<string, Document>;
30
29
  server?: HocuspocusServer;
31
30
  debugger: Debugger;
31
+ debounce: (id: string, func: Function, debounce: number, maxDebounce: number) => void;
32
32
  constructor(configuration?: Partial<Configuration>);
33
33
  /**
34
34
  * Configure the server
@@ -78,14 +78,6 @@ export declare class Hocuspocus {
78
78
  * the update is incoming from the provider, but can be anything if the updates is originated from an extension.
79
79
  */
80
80
  private handleDocumentUpdate;
81
- timers: Map<string, {
82
- timeout: NodeJS.Timeout;
83
- start: number;
84
- }>;
85
- /**
86
- * debounce the given function, using the given identifier
87
- */
88
- debounce(id: string, func: Function, immediately?: boolean): void;
89
81
  /**
90
82
  * Create a new document by the given request
91
83
  */
@@ -5,7 +5,8 @@ import { IncomingMessage } from './IncomingMessage.js';
5
5
  export declare class MessageReceiver {
6
6
  message: IncomingMessage;
7
7
  logger: Debugger;
8
- constructor(message: IncomingMessage, logger: Debugger);
8
+ defaultTransactionOrigin?: string;
9
+ constructor(message: IncomingMessage, logger: Debugger, defaultTransactionOrigin?: string);
9
10
  apply(document: Document, connection?: Connection, reply?: (message: Uint8Array) => void): void;
10
11
  readSyncMessage(message: IncomingMessage, document: Document, connection?: Connection, reply?: (message: Uint8Array) => void, requestFirstSync?: boolean): 0 | 2 | 1;
11
12
  applyQueryAwarenessMessage(document: Document, reply?: (message: Uint8Array) => void): void;
@@ -6,3 +6,4 @@ export * from './IncomingMessage.js';
6
6
  export * from './MessageReceiver.js';
7
7
  export * from './OutgoingMessage.js';
8
8
  export * from './types.js';
9
+ export * from './util/debounce.js';
@@ -123,15 +123,6 @@ export interface Configuration extends Extension {
123
123
  gc: boolean;
124
124
  gcFilter: () => boolean;
125
125
  };
126
- /**
127
- * Function which returns the (customized) document name based on the request
128
- */
129
- getDocumentName?(data: getDocumentNamePayload): string | Promise<string>;
130
- }
131
- export interface getDocumentNamePayload {
132
- documentName: string;
133
- request: IncomingMessage;
134
- requestParameters: URLSearchParams;
135
126
  }
136
127
  export interface onStatelessPayload {
137
128
  connection: Connection;
@@ -0,0 +1 @@
1
+ export declare const useDebounce: () => (id: string, func: Function, debounce: number, maxDebounce: number) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hocuspocus/extension-redis",
3
- "version": "2.5.0-rc.0",
3
+ "version": "2.5.0",
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.5.0-rc.0",
36
+ "@hocuspocus/server": "^2.5.0",
37
37
  "ioredis": "^4.28.2",
38
38
  "kleur": "^4.1.4",
39
39
  "lodash.debounce": "^4.0.8",
package/src/Redis.ts CHANGED
@@ -84,6 +84,8 @@ export class Redis implements Extension {
84
84
  disconnectDelay: 1000,
85
85
  }
86
86
 
87
+ redisTransactionOrigin = '__hocuspocus__redis__origin__'
88
+
87
89
  pub: RedisInstance
88
90
 
89
91
  sub: RedisInstance
@@ -96,6 +98,8 @@ export class Redis implements Extension {
96
98
 
97
99
  logger: Debugger
98
100
 
101
+ messagePrefix: Buffer
102
+
99
103
  public constructor(configuration: Partial<Configuration>) {
100
104
  this.configuration = {
101
105
  ...this.configuration,
@@ -128,9 +132,12 @@ export class Redis implements Extension {
128
132
  this.pub = new RedisClient(port, host, options)
129
133
  this.sub = new RedisClient(port, host, options)
130
134
  }
131
- this.sub.on('pmessageBuffer', this.handleIncomingMessage)
135
+ this.sub.on('messageBuffer', this.handleIncomingMessage)
132
136
 
133
137
  this.redlock = new Redlock([this.pub])
138
+
139
+ const identifierBuffer = Buffer.from(this.configuration.identifier, 'utf-8')
140
+ this.messagePrefix = Buffer.concat([Buffer.from([identifierBuffer.length]), identifierBuffer])
134
141
  }
135
142
 
136
143
  async onConfigure({ instance }: onConfigurePayload) {
@@ -143,17 +150,28 @@ export class Redis implements Extension {
143
150
  }
144
151
 
145
152
  private pubKey(documentName: string) {
146
- return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`
153
+ return this.getKey(documentName)
147
154
  }
148
155
 
149
156
  private subKey(documentName: string) {
150
- return `${this.getKey(documentName)}:*`
157
+ return this.getKey(documentName)
151
158
  }
152
159
 
153
160
  private lockKey(documentName: string) {
154
161
  return `${this.getKey(documentName)}:lock`
155
162
  }
156
163
 
164
+ private encodeMessage(message: Uint8Array) {
165
+ return Buffer.concat([this.messagePrefix, Buffer.from(message)])
166
+ }
167
+
168
+ private decodeMessage(buffer: Buffer) {
169
+ const identifierLength = buffer[0]
170
+ const identifier = buffer.toString('utf-8', 1, identifierLength + 1)
171
+
172
+ return [identifier, buffer.slice(identifierLength + 1)]
173
+ }
174
+
157
175
  /**
158
176
  * Once a document is loaded, subscribe to the channel in Redis.
159
177
  */
@@ -161,7 +179,7 @@ export class Redis implements Extension {
161
179
  return new Promise((resolve, reject) => {
162
180
  // On document creation the node will connect to pub and sub channels
163
181
  // for the document.
164
- this.sub.psubscribe(this.subKey(documentName), async error => {
182
+ this.sub.subscribe(this.subKey(documentName), async error => {
165
183
  if (error) {
166
184
  reject(error)
167
185
  return
@@ -183,7 +201,7 @@ export class Redis implements Extension {
183
201
  .createSyncMessage()
184
202
  .writeFirstSyncStepFor(document)
185
203
 
186
- return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(syncMessage.toUint8Array()))
204
+ return this.pub.publishBuffer(this.pubKey(documentName), this.encodeMessage(syncMessage.toUint8Array()))
187
205
  }
188
206
 
189
207
  /**
@@ -195,7 +213,7 @@ export class Redis implements Extension {
195
213
 
196
214
  return this.pub.publishBuffer(
197
215
  this.pubKey(documentName),
198
- Buffer.from(awarenessMessage.toUint8Array()),
216
+ this.encodeMessage(awarenessMessage.toUint8Array()),
199
217
  )
200
218
  }
201
219
 
@@ -248,40 +266,42 @@ export class Redis implements Extension {
248
266
 
249
267
  return this.pub.publishBuffer(
250
268
  this.pubKey(documentName),
251
- Buffer.from(message.toUint8Array()),
269
+ this.encodeMessage(message.toUint8Array()),
252
270
  )
253
271
  }
254
272
 
255
273
  /**
256
- * Handle incoming messages published on all subscribed document channels.
274
+ * Handle incoming messages published on subscribed document channels.
257
275
  * Note that this will also include messages from ourselves as it is not possible
258
276
  * in Redis to filter these.
259
277
  */
260
- private handleIncomingMessage = async (channel: Buffer, pattern: Buffer, data: Buffer) => {
261
- const message = new IncomingMessage(data)
262
- // we don't need the documentName from the message, we are just taking it from the redis channelName.
263
- // we have to immediately write it back to the encoder though, to make sure the structure of the message is correct
264
- message.writeVarString(message.readVarString())
265
-
266
- const channelName = pattern.toString()
267
- const [_, documentName, identifier] = channelName.split(':')
268
- const document = this.instance.documents.get(documentName)
278
+ private handleIncomingMessage = async (channel: Buffer, data: Buffer) => {
279
+ const [identifier, messageBuffer] = this.decodeMessage(data)
269
280
 
270
281
  if (identifier === this.configuration.identifier) {
271
282
  return
272
283
  }
273
284
 
285
+ const message = new IncomingMessage(messageBuffer)
286
+ const documentName = message.readVarString()
287
+ message.writeVarString(documentName)
288
+
289
+ const document = this.instance.documents.get(documentName)
290
+
274
291
  if (!document) {
292
+ // What does this mean? Why are we subscribed to this document?
293
+ this.logger.log(`Received message for unknown document ${documentName}`)
275
294
  return
276
295
  }
277
296
 
278
297
  new MessageReceiver(
279
298
  message,
280
299
  this.logger,
300
+ this.redisTransactionOrigin,
281
301
  ).apply(document, undefined, reply => {
282
302
  return this.pub.publishBuffer(
283
303
  this.pubKey(document.name),
284
- Buffer.from(reply),
304
+ this.encodeMessage(reply),
285
305
  )
286
306
  })
287
307
  }
@@ -290,7 +310,9 @@ export class Redis implements Extension {
290
310
  * if the ydoc changed, we'll need to inform other Hocuspocus servers about it.
291
311
  */
292
312
  public async onChange(data: onChangePayload): Promise<any> {
293
- return this.publishFirstSyncStep(data.documentName, data.document)
313
+ if (data.transactionOrigin !== this.redisTransactionOrigin) {
314
+ return this.publishFirstSyncStep(data.documentName, data.document)
315
+ }
294
316
  }
295
317
 
296
318
  /**
@@ -307,7 +329,7 @@ export class Redis implements Extension {
307
329
  }
308
330
 
309
331
  // Time to end the subscription on the document channel.
310
- this.sub.punsubscribe(this.subKey(documentName), (error: any) => {
332
+ this.sub.unsubscribe(this.subKey(documentName), (error: any) => {
311
333
  if (error) {
312
334
  console.error(error)
313
335
  }
@@ -323,7 +345,7 @@ export class Redis implements Extension {
323
345
 
324
346
  return this.pub.publishBuffer(
325
347
  this.pubKey(data.documentName),
326
- Buffer.from(message.toUint8Array()),
348
+ this.encodeMessage(message.toUint8Array()),
327
349
  )
328
350
  }
329
351
 
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};