@hocuspocus/extension-redis 2.5.0-rc.0 → 2.6.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.
- package/dist/hocuspocus-redis.cjs +34 -21
- package/dist/hocuspocus-redis.cjs.map +1 -1
- package/dist/hocuspocus-redis.esm.js +34 -21
- package/dist/hocuspocus-redis.esm.js.map +1 -1
- package/dist/packages/extension-redis/src/Redis.d.ts +6 -1
- package/dist/packages/provider/src/HocuspocusProviderWebsocket.d.ts +10 -2
- package/dist/packages/provider/src/TiptapCollabProvider.d.ts +19 -8
- package/dist/packages/provider/src/types.d.ts +45 -0
- package/dist/packages/server/src/Hocuspocus.d.ts +1 -9
- package/dist/packages/server/src/MessageReceiver.d.ts +2 -1
- package/dist/packages/server/src/index.d.ts +1 -0
- package/dist/packages/server/src/types.d.ts +4 -12
- package/dist/packages/server/src/util/debounce.d.ts +1 -0
- package/package.json +2 -2
- package/src/Redis.ts +43 -21
- package/dist/tests/extension-redis/closeConnections.d.ts +0 -1
- package/dist/tests/extension-redis/getConnectionCount.d.ts +0 -1
- package/dist/tests/extension-redis/getDocumentsCount.d.ts +0 -1
|
@@ -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
|
|
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,
|
|
38
|
-
const
|
|
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),
|
|
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.
|
|
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('
|
|
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
|
|
114
|
+
return this.getKey(documentName);
|
|
112
115
|
}
|
|
113
116
|
subKey(documentName) {
|
|
114
|
-
return
|
|
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.
|
|
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),
|
|
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),
|
|
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),
|
|
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
|
-
|
|
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),
|
|
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
|
|
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,
|
|
29
|
-
const
|
|
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),
|
|
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.
|
|
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('
|
|
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
|
|
105
|
+
return this.getKey(documentName);
|
|
103
106
|
}
|
|
104
107
|
subKey(documentName) {
|
|
105
|
-
return
|
|
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.
|
|
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),
|
|
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),
|
|
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),
|
|
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
|
-
|
|
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),
|
|
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
|
|
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
|
*/
|
|
@@ -4,6 +4,9 @@ import { Event } from 'ws';
|
|
|
4
4
|
import EventEmitter from './EventEmitter.js';
|
|
5
5
|
import { HocuspocusProvider } from './HocuspocusProvider.js';
|
|
6
6
|
import { WebSocketStatus, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatusParameters } from './types.js';
|
|
7
|
+
export type HocusPocusWebSocket = WebSocket & {
|
|
8
|
+
identifier: string;
|
|
9
|
+
};
|
|
7
10
|
export type HocuspocusProviderWebsocketConfiguration = Required<Pick<CompleteHocuspocusProviderWebsocketConfiguration, 'url'>> & Partial<CompleteHocuspocusProviderWebsocketConfiguration>;
|
|
8
11
|
export interface CompleteHocuspocusProviderWebsocketConfiguration {
|
|
9
12
|
/**
|
|
@@ -79,10 +82,14 @@ export declare class HocuspocusProviderWebsocket extends EventEmitter {
|
|
|
79
82
|
private messageQueue;
|
|
80
83
|
configuration: CompleteHocuspocusProviderWebsocketConfiguration;
|
|
81
84
|
subscribedToBroadcastChannel: boolean;
|
|
82
|
-
webSocket:
|
|
85
|
+
webSocket: HocusPocusWebSocket | null;
|
|
86
|
+
webSocketHandlers: {
|
|
87
|
+
[key: string]: any;
|
|
88
|
+
};
|
|
83
89
|
shouldConnect: boolean;
|
|
84
90
|
status: WebSocketStatus;
|
|
85
91
|
lastMessageReceived: number;
|
|
92
|
+
identifier: number;
|
|
86
93
|
mux: mutex.mutex;
|
|
87
94
|
intervals: any;
|
|
88
95
|
connectionAttempt: {
|
|
@@ -100,6 +107,8 @@ export declare class HocuspocusProviderWebsocket extends EventEmitter {
|
|
|
100
107
|
boundConnect: () => Promise<unknown>;
|
|
101
108
|
cancelWebsocketRetry?: () => void;
|
|
102
109
|
connect(): Promise<unknown>;
|
|
110
|
+
attachWebSocketListeners(ws: HocusPocusWebSocket, reject: Function): void;
|
|
111
|
+
cleanupWebSocket(): void;
|
|
103
112
|
createWebSocketConnection(): Promise<unknown>;
|
|
104
113
|
onMessage(event: MessageEvent): void;
|
|
105
114
|
resolveConnectionAttempt(): void;
|
|
@@ -107,7 +116,6 @@ export declare class HocuspocusProviderWebsocket extends EventEmitter {
|
|
|
107
116
|
rejectConnectionAttempt(): void;
|
|
108
117
|
closeTries: number;
|
|
109
118
|
checkConnection(): void;
|
|
110
|
-
registerEventListeners(): void;
|
|
111
119
|
get serverUrl(): string;
|
|
112
120
|
get url(): string;
|
|
113
121
|
disconnect(): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AbstractType, YArrayEvent } from 'yjs';
|
|
2
2
|
import { HocuspocusProvider, HocuspocusProviderConfiguration } from './HocuspocusProvider.js';
|
|
3
3
|
import { TiptapCollabProviderWebsocket } from './TiptapCollabProviderWebsocket.js';
|
|
4
|
+
import type { THistoryVersion } from './types';
|
|
4
5
|
export type TiptapCollabProviderConfiguration = Required<Pick<HocuspocusProviderConfiguration, 'name'>> & Partial<HocuspocusProviderConfiguration> & (Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'websocketProvider'>> | Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'appId'>>);
|
|
5
6
|
export interface AdditionalTiptapCollabProviderConfiguration {
|
|
6
7
|
/**
|
|
@@ -9,19 +10,29 @@ export interface AdditionalTiptapCollabProviderConfiguration {
|
|
|
9
10
|
appId?: string;
|
|
10
11
|
websocketProvider?: TiptapCollabProviderWebsocket;
|
|
11
12
|
}
|
|
12
|
-
export type AuditHistoryVersion = {
|
|
13
|
-
name?: string;
|
|
14
|
-
version: number;
|
|
15
|
-
date: number;
|
|
16
|
-
};
|
|
17
13
|
export declare class TiptapCollabProvider extends HocuspocusProvider {
|
|
18
14
|
tiptapCollabConfigurationPrefix: string;
|
|
19
15
|
constructor(configuration: TiptapCollabProviderConfiguration);
|
|
16
|
+
/**
|
|
17
|
+
* note: this will only work if your server loaded @hocuspocus-pro/extension-history, or if you are on a Tiptap business plan.
|
|
18
|
+
*/
|
|
20
19
|
createVersion(name?: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* note: this will only work if your server loaded @hocuspocus-pro/extension-history, or if you are on a Tiptap business plan.
|
|
22
|
+
*/
|
|
21
23
|
revertToVersion(targetVersion: number): void;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* note: this will only work if your server loaded @hocuspocus-pro/extension-history, or if you are on a Tiptap business plan.
|
|
26
|
+
*
|
|
27
|
+
* The server will reply with a stateless message (THistoryVersionPreviewEvent)
|
|
28
|
+
*/
|
|
29
|
+
previewVersion(targetVersion: number): void;
|
|
30
|
+
/**
|
|
31
|
+
* note: this will only work if your server loaded @hocuspocus-pro/extension-history, or if you are on a Tiptap business plan.
|
|
32
|
+
*/
|
|
33
|
+
getVersions(): THistoryVersion[];
|
|
34
|
+
watchVersions(callback: Parameters<AbstractType<YArrayEvent<THistoryVersion>>['observe']>[0]): void;
|
|
35
|
+
unwatchVersions(callback: Parameters<AbstractType<YArrayEvent<THistoryVersion>>['unobserve']>[0]): void;
|
|
25
36
|
isAutoVersioning(): boolean;
|
|
26
37
|
enableAutoVersioning(): 1;
|
|
27
38
|
disableAutoVersioning(): 0;
|
|
@@ -84,3 +84,48 @@ export type StatesArray = {
|
|
|
84
84
|
clientId: number;
|
|
85
85
|
[key: string | number]: any;
|
|
86
86
|
}[];
|
|
87
|
+
export type THistoryVersion = {
|
|
88
|
+
name?: string;
|
|
89
|
+
version: number;
|
|
90
|
+
date: number;
|
|
91
|
+
};
|
|
92
|
+
export type THistoryConfiguration = {
|
|
93
|
+
autoVersioning: boolean;
|
|
94
|
+
currentVersion: number;
|
|
95
|
+
stateCaptured: number;
|
|
96
|
+
};
|
|
97
|
+
export type THistoryAction = THistoryDocumentRevertAction | THistoryVersionCreateAction | THistoryVersionPreviewAction;
|
|
98
|
+
export type THistoryDocumentRevertAction = {
|
|
99
|
+
action: 'document.revert';
|
|
100
|
+
/**
|
|
101
|
+
* if changes havent been persisted to a version yet, we'll create one with the specified name,
|
|
102
|
+
* expect when `false` is passed.
|
|
103
|
+
*/
|
|
104
|
+
currentVersionName?: string | false;
|
|
105
|
+
/**
|
|
106
|
+
* Name of the version that is created after the revert. Pass `false` to avoid generating a new version.
|
|
107
|
+
*/
|
|
108
|
+
newVersionName?: string | false;
|
|
109
|
+
};
|
|
110
|
+
export type THistoryVersionCreateAction = {
|
|
111
|
+
action: 'version.create';
|
|
112
|
+
name?: string;
|
|
113
|
+
};
|
|
114
|
+
export type THistoryVersionPreviewAction = {
|
|
115
|
+
action: 'version.preview';
|
|
116
|
+
version: number;
|
|
117
|
+
};
|
|
118
|
+
export type THistoryEvent = THistoryVersionPreviewEvent | THistoryVersionCreatedEvent | THistoryDocumentRevertedEvent;
|
|
119
|
+
export type THistoryVersionCreatedEvent = {
|
|
120
|
+
event: 'version.created';
|
|
121
|
+
version: number;
|
|
122
|
+
};
|
|
123
|
+
export type THistoryVersionPreviewEvent = {
|
|
124
|
+
event: 'version.preview';
|
|
125
|
+
version: number;
|
|
126
|
+
ydoc: string;
|
|
127
|
+
};
|
|
128
|
+
export type THistoryDocumentRevertedEvent = {
|
|
129
|
+
event: 'document.reverted';
|
|
130
|
+
version: number;
|
|
131
|
+
};
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -39,7 +39,7 @@ export interface Extension {
|
|
|
39
39
|
connected?(data: connectedPayload): Promise<any>;
|
|
40
40
|
onAuthenticate?(data: onAuthenticatePayload): Promise<any>;
|
|
41
41
|
onLoadDocument?(data: onLoadDocumentPayload): Promise<any>;
|
|
42
|
-
afterLoadDocument?(data:
|
|
42
|
+
afterLoadDocument?(data: afterLoadDocumentPayload): Promise<any>;
|
|
43
43
|
beforeHandleMessage?(data: beforeHandleMessagePayload): Promise<any>;
|
|
44
44
|
beforeBroadcastStateless?(data: beforeBroadcastStatelessPayload): Promise<any>;
|
|
45
45
|
onStateless?(payload: onStatelessPayload): Promise<any>;
|
|
@@ -49,7 +49,7 @@ export interface Extension {
|
|
|
49
49
|
onAwarenessUpdate?(data: onAwarenessUpdatePayload): Promise<any>;
|
|
50
50
|
onRequest?(data: onRequestPayload): Promise<any>;
|
|
51
51
|
onDisconnect?(data: onDisconnectPayload): Promise<any>;
|
|
52
|
-
afterUnloadDocument?(data:
|
|
52
|
+
afterUnloadDocument?(data: afterUnloadDocumentPayload): Promise<any>;
|
|
53
53
|
onDestroy?(data: onDestroyPayload): Promise<any>;
|
|
54
54
|
}
|
|
55
55
|
export type HookName = 'onConfigure' | 'onListen' | 'onUpgrade' | 'onConnect' | 'connected' | 'onAuthenticate' | 'onLoadDocument' | 'afterLoadDocument' | 'beforeHandleMessage' | 'beforeBroadcastStateless' | 'onStateless' | 'onChange' | 'onStoreDocument' | 'afterStoreDocument' | 'onAwarenessUpdate' | 'onRequest' | 'onDisconnect' | 'afterUnloadDocument' | 'onDestroy';
|
|
@@ -61,7 +61,7 @@ export type HookPayloadByName = {
|
|
|
61
61
|
connected: connectedPayload;
|
|
62
62
|
onAuthenticate: onAuthenticatePayload;
|
|
63
63
|
onLoadDocument: onLoadDocumentPayload;
|
|
64
|
-
afterLoadDocument:
|
|
64
|
+
afterLoadDocument: afterLoadDocumentPayload;
|
|
65
65
|
beforeHandleMessage: beforeHandleMessagePayload;
|
|
66
66
|
beforeBroadcastStateless: beforeBroadcastStatelessPayload;
|
|
67
67
|
onStateless: onStatelessPayload;
|
|
@@ -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;
|
|
@@ -149,6 +140,7 @@ export interface onAuthenticatePayload {
|
|
|
149
140
|
connection: ConnectionConfiguration;
|
|
150
141
|
}
|
|
151
142
|
export interface onConnectPayload {
|
|
143
|
+
context: any;
|
|
152
144
|
documentName: string;
|
|
153
145
|
instance: Hocuspocus;
|
|
154
146
|
request: IncomingMessage;
|
|
@@ -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.
|
|
3
|
+
"version": "2.6.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.
|
|
36
|
+
"@hocuspocus/server": "^2.6.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('
|
|
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
|
|
153
|
+
return this.getKey(documentName)
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
private subKey(documentName: string) {
|
|
150
|
-
return
|
|
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.
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
269
|
+
this.encodeMessage(message.toUint8Array()),
|
|
252
270
|
)
|
|
253
271
|
}
|
|
254
272
|
|
|
255
273
|
/**
|
|
256
|
-
* Handle incoming messages published on
|
|
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,
|
|
261
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 {};
|