@hocuspocus/extension-redis 1.0.1 → 1.1.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 +25 -5
- package/dist/hocuspocus-redis.cjs.map +1 -1
- package/dist/hocuspocus-redis.esm.js +25 -5
- package/dist/hocuspocus-redis.esm.js.map +1 -1
- package/dist/packages/extension-redis/src/Redis.d.ts +19 -5
- package/dist/packages/provider/src/HocuspocusProvider.d.ts +4 -1
- package/dist/packages/provider/src/OutgoingMessages/StatelessMessage.d.ts +7 -0
- package/dist/packages/provider/src/types.d.ts +6 -1
- package/dist/packages/server/src/Connection.d.ts +9 -0
- package/dist/packages/server/src/Document.d.ts +9 -0
- package/dist/packages/server/src/IncomingMessage.d.ts +1 -0
- package/dist/packages/server/src/OutgoingMessage.d.ts +2 -0
- package/dist/packages/server/src/types.d.ts +19 -3
- package/dist/packages/transformer/src/Prosemirror.d.ts +1 -1
- package/dist/packages/transformer/src/Tiptap.d.ts +1 -1
- package/dist/tests/extension-redis/onStateless.d.ts +1 -0
- package/dist/tests/provider/onStateless.d.ts +1 -0
- package/dist/tests/server/beforeBroadcastStateless.d.ts +1 -0
- package/dist/tests/server/onStateless.d.ts +1 -0
- package/package.json +2 -2
- package/src/Redis.ts +54 -11
|
@@ -72,13 +72,28 @@ class Redis {
|
|
|
72
72
|
...this.configuration,
|
|
73
73
|
...configuration,
|
|
74
74
|
};
|
|
75
|
-
const { port, host, options } = this.configuration;
|
|
76
|
-
this.pub = new RedisClient__default["default"](port, host, options);
|
|
77
|
-
this.sub = new RedisClient__default["default"](port, host, options);
|
|
78
|
-
this.sub.on('pmessageBuffer', this.handleIncomingMessage);
|
|
79
|
-
this.redlock = new Redlock__default["default"]([this.pub]);
|
|
80
75
|
// We’ll replace that in the onConfigure hook with the global instance.
|
|
81
76
|
this.logger = new server.Debugger();
|
|
77
|
+
// Create Redis instance
|
|
78
|
+
const { port, host, options, nodes, redis, createClient, } = this.configuration;
|
|
79
|
+
if (typeof createClient === 'function') {
|
|
80
|
+
this.pub = createClient();
|
|
81
|
+
this.sub = createClient();
|
|
82
|
+
}
|
|
83
|
+
else if (redis) {
|
|
84
|
+
this.pub = redis.duplicate();
|
|
85
|
+
this.sub = redis.duplicate();
|
|
86
|
+
}
|
|
87
|
+
else if (nodes && nodes.length > 0) {
|
|
88
|
+
this.pub = new RedisClient__default["default"].Cluster(nodes, options);
|
|
89
|
+
this.sub = new RedisClient__default["default"].Cluster(nodes, options);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.pub = new RedisClient__default["default"](port, host, options);
|
|
93
|
+
this.sub = new RedisClient__default["default"](port, host, options);
|
|
94
|
+
}
|
|
95
|
+
this.sub.on('pmessageBuffer', this.handleIncomingMessage);
|
|
96
|
+
this.redlock = new Redlock__default["default"]([this.pub]);
|
|
82
97
|
}
|
|
83
98
|
async onConfigure({ instance }) {
|
|
84
99
|
this.logger = instance.debugger;
|
|
@@ -185,6 +200,11 @@ class Redis {
|
|
|
185
200
|
async onChange(data) {
|
|
186
201
|
return this.publishFirstSyncStep(data.documentName, data.document);
|
|
187
202
|
}
|
|
203
|
+
async beforeBroadcastStateless(data) {
|
|
204
|
+
const message = new server.OutgoingMessage()
|
|
205
|
+
.writeBroadcastStateless(data.payload);
|
|
206
|
+
return this.pub.publishBuffer(this.pubKey(data.documentName), Buffer.from(message.toUint8Array()));
|
|
207
|
+
}
|
|
188
208
|
/**
|
|
189
209
|
* Kill the Redlock connection immediately.
|
|
190
210
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hocuspocus-redis.cjs","sources":["../src/Redis.ts"],"sourcesContent":["import RedisClient 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 onListenPayload,\n} from '@hocuspocus/server'\nimport kleur from 'kleur'\n\nexport interface Configuration {\n /**\n * Redis port\n */\n port: number,\n /**\n * Redis host\n */\n host: string,\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?: RedisClient.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\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 }\n\n pub: RedisClient.Redis\n\n sub: RedisClient.Redis\n\n documents: Map<string, Document> = new Map()\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 const { port, host, options } = this.configuration\n\n this.pub = new RedisClient(port, host, options)\n\n this.sub = new RedisClient(port, host, options)\n this.sub.on('pmessageBuffer', this.handleIncomingMessage)\n\n this.redlock = new Redlock([this.pub])\n\n // We’ll replace that in the onConfigure hook with the global instance.\n this.logger = new Debugger()\n }\n\n async onConfigure({ instance }: onConfigurePayload) {\n this.logger = instance.debugger\n }\n\n async onListen({ configuration }: onListenPayload) {\n if (configuration.quiet) {\n return\n }\n\n console.warn(` ${kleur.yellow('[BREAKING CHANGE] Wait, the Redis extension got an overhaul. The new Redis extension doesn’t persist data, it only syncs data between instances. Use @hocuspocus/extension-database to store your documents. It works well with the Redis extension.')}`)\n console.log()\n }\n\n private getKey(documentName: string) {\n return `${this.configuration.prefix}:${documentName}`\n }\n\n private pubKey(documentName: string) {\n return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`\n }\n\n private subKey(documentName: string) {\n return `${this.getKey(documentName)}:*`\n }\n\n private lockKey(documentName: string) {\n return `${this.getKey(documentName)}:lock`\n }\n\n /**\n * Once a document is laoded, subscribe to the channel in Redis.\n */\n public async afterLoadDocument({ documentName, document }: afterLoadDocumentPayload) {\n this.documents.set(documentName, document)\n\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()\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()\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()\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 channelName = pattern.toString()\n const [_, documentName, identifier] = channelName.split(':')\n const document = this.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 new IncomingMessage(data),\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, clientsCount }: onDisconnectPayload) => {\n // Do nothing, when other users are still connected to the document.\n if (clientsCount > 0) {\n return\n }\n\n // It was indeed the last connected user.\n this.documents.delete(documentName)\n\n // Time to end the subscription on the document channel.\n this.sub.punsubscribe(this.subKey(documentName), error => {\n if (error) {\n console.error(error)\n }\n })\n }\n\n /**\n * Kill the Redlock connection immediately.\n */\n async onDestroy() {\n this.redlock.quit()\n }\n}\n"],"names":["uuid","MessageReceiver","IncomingMessage","RedisClient","Redlock","Debugger","kleur","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 onListenPayload,\n beforeBroadcastStatelessPayload,\n} from '@hocuspocus/server'\nimport kleur from 'kleur'\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\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 }\n\n pub: RedisInstance\n\n sub: RedisInstance\n\n documents: Map<string, Document> = new Map()\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 }\n\n async onListen({ configuration }: onListenPayload) {\n if (configuration.quiet) {\n return\n }\n\n console.warn(` ${kleur.yellow('[BREAKING CHANGE] Wait, the Redis extension got an overhaul. The new Redis extension doesn’t persist data, it only syncs data between instances. Use @hocuspocus/extension-database to store your documents. It works well with the Redis extension.')}`)\n console.log()\n }\n\n private getKey(documentName: string) {\n return `${this.configuration.prefix}:${documentName}`\n }\n\n private pubKey(documentName: string) {\n return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`\n }\n\n private subKey(documentName: string) {\n return `${this.getKey(documentName)}:*`\n }\n\n private lockKey(documentName: string) {\n return `${this.getKey(documentName)}:lock`\n }\n\n /**\n * Once a document is laoded, subscribe to the channel in Redis.\n */\n public async afterLoadDocument({ documentName, document }: afterLoadDocumentPayload) {\n this.documents.set(documentName, document)\n\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()\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()\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()\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 channelName = pattern.toString()\n const [_, documentName, identifier] = channelName.split(':')\n const document = this.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 new IncomingMessage(data),\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, clientsCount }: onDisconnectPayload) => {\n // Do nothing, when other users are still connected to the document.\n if (clientsCount > 0) {\n return\n }\n\n // It was indeed the last connected user.\n this.documents.delete(documentName)\n\n // Time to end the subscription on the document channel.\n this.sub.punsubscribe(this.subKey(documentName), error => {\n if (error) {\n console.error(error)\n }\n })\n }\n\n async beforeBroadcastStateless(data: beforeBroadcastStatelessPayload) {\n const message = new OutgoingMessage()\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 this.redlock.quit()\n }\n}\n"],"names":["uuid","MessageReceiver","IncomingMessage","Debugger","RedisClient","Redlock","kleur","OutgoingMessage"],"mappings":"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"}
|
|
@@ -62,13 +62,28 @@ class Redis {
|
|
|
62
62
|
...this.configuration,
|
|
63
63
|
...configuration,
|
|
64
64
|
};
|
|
65
|
-
const { port, host, options } = this.configuration;
|
|
66
|
-
this.pub = new RedisClient(port, host, options);
|
|
67
|
-
this.sub = new RedisClient(port, host, options);
|
|
68
|
-
this.sub.on('pmessageBuffer', this.handleIncomingMessage);
|
|
69
|
-
this.redlock = new Redlock([this.pub]);
|
|
70
65
|
// We’ll replace that in the onConfigure hook with the global instance.
|
|
71
66
|
this.logger = new Debugger();
|
|
67
|
+
// Create Redis instance
|
|
68
|
+
const { port, host, options, nodes, redis, createClient, } = this.configuration;
|
|
69
|
+
if (typeof createClient === 'function') {
|
|
70
|
+
this.pub = createClient();
|
|
71
|
+
this.sub = createClient();
|
|
72
|
+
}
|
|
73
|
+
else if (redis) {
|
|
74
|
+
this.pub = redis.duplicate();
|
|
75
|
+
this.sub = redis.duplicate();
|
|
76
|
+
}
|
|
77
|
+
else if (nodes && nodes.length > 0) {
|
|
78
|
+
this.pub = new RedisClient.Cluster(nodes, options);
|
|
79
|
+
this.sub = new RedisClient.Cluster(nodes, options);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.pub = new RedisClient(port, host, options);
|
|
83
|
+
this.sub = new RedisClient(port, host, options);
|
|
84
|
+
}
|
|
85
|
+
this.sub.on('pmessageBuffer', this.handleIncomingMessage);
|
|
86
|
+
this.redlock = new Redlock([this.pub]);
|
|
72
87
|
}
|
|
73
88
|
async onConfigure({ instance }) {
|
|
74
89
|
this.logger = instance.debugger;
|
|
@@ -175,6 +190,11 @@ class Redis {
|
|
|
175
190
|
async onChange(data) {
|
|
176
191
|
return this.publishFirstSyncStep(data.documentName, data.document);
|
|
177
192
|
}
|
|
193
|
+
async beforeBroadcastStateless(data) {
|
|
194
|
+
const message = new OutgoingMessage()
|
|
195
|
+
.writeBroadcastStateless(data.payload);
|
|
196
|
+
return this.pub.publishBuffer(this.pubKey(data.documentName), Buffer.from(message.toUint8Array()));
|
|
197
|
+
}
|
|
178
198
|
/**
|
|
179
199
|
* Kill the Redlock connection immediately.
|
|
180
200
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hocuspocus-redis.esm.js","sources":["../src/Redis.ts"],"sourcesContent":["import RedisClient 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 onListenPayload,\n} from '@hocuspocus/server'\nimport kleur from 'kleur'\n\nexport interface Configuration {\n /**\n * Redis port\n */\n port: number,\n /**\n * Redis host\n */\n host: string,\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?: RedisClient.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\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 }\n\n pub: RedisClient.Redis\n\n sub: RedisClient.Redis\n\n documents: Map<string, Document> = new Map()\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 const { port, host, options } = this.configuration\n\n this.pub = new RedisClient(port, host, options)\n\n this.sub = new RedisClient(port, host, options)\n this.sub.on('pmessageBuffer', this.handleIncomingMessage)\n\n this.redlock = new Redlock([this.pub])\n\n // We’ll replace that in the onConfigure hook with the global instance.\n this.logger = new Debugger()\n }\n\n async onConfigure({ instance }: onConfigurePayload) {\n this.logger = instance.debugger\n }\n\n async onListen({ configuration }: onListenPayload) {\n if (configuration.quiet) {\n return\n }\n\n console.warn(` ${kleur.yellow('[BREAKING CHANGE] Wait, the Redis extension got an overhaul. The new Redis extension doesn’t persist data, it only syncs data between instances. Use @hocuspocus/extension-database to store your documents. It works well with the Redis extension.')}`)\n console.log()\n }\n\n private getKey(documentName: string) {\n return `${this.configuration.prefix}:${documentName}`\n }\n\n private pubKey(documentName: string) {\n return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`\n }\n\n private subKey(documentName: string) {\n return `${this.getKey(documentName)}:*`\n }\n\n private lockKey(documentName: string) {\n return `${this.getKey(documentName)}:lock`\n }\n\n /**\n * Once a document is laoded, subscribe to the channel in Redis.\n */\n public async afterLoadDocument({ documentName, document }: afterLoadDocumentPayload) {\n this.documents.set(documentName, document)\n\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()\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()\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()\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 channelName = pattern.toString()\n const [_, documentName, identifier] = channelName.split(':')\n const document = this.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 new IncomingMessage(data),\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, clientsCount }: onDisconnectPayload) => {\n // Do nothing, when other users are still connected to the document.\n if (clientsCount > 0) {\n return\n }\n\n // It was indeed the last connected user.\n this.documents.delete(documentName)\n\n // Time to end the subscription on the document channel.\n this.sub.punsubscribe(this.subKey(documentName), error => {\n if (error) {\n console.error(error)\n }\n })\n }\n\n /**\n * Kill the Redlock connection immediately.\n */\n async onDestroy() {\n this.redlock.quit()\n }\n}\n"],"names":["uuid"],"mappings":";;;;;;MAmDa,KAAK,CAAA;AA4BhB,IAAA,WAAA,CAAmB,aAAqC,EAAA;AA3BxD;;;;AAIG;QACH,IAAQ,CAAA,QAAA,GAAG,IAAI,CAAA;AAEf,QAAA,IAAA,CAAA,aAAa,GAAkB;AAC7B,YAAA,IAAI,EAAE,IAAI;AACV,YAAA,IAAI,EAAE,WAAW;AACjB,YAAA,MAAM,EAAE,YAAY;AACpB,YAAA,UAAU,EAAE,CAAA,KAAA,EAAQA,EAAI,EAAE,CAAE,CAAA;AAC5B,YAAA,WAAW,EAAE,IAAI;SAClB,CAAA;AAMD,QAAA,IAAA,CAAA,SAAS,GAA0B,IAAI,GAAG,EAAE,CAAA;AAI5C,QAAA,IAAA,CAAA,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAA;AAwJvC;;;;AAIE;QACM,IAAqB,CAAA,qBAAA,GAAG,OAAO,OAAe,EAAE,OAAe,EAAE,IAAY,KAAI;AACvF,YAAA,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAA;AACtC,YAAA,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,UAAU,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;AAEjD,YAAA,IAAI,UAAU,KAAK,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE;gBAChD,OAAM;AACP,aAAA;YAED,IAAI,CAAC,QAAQ,EAAE;gBACb,OAAM;AACP,aAAA;YAED,IAAI,eAAe,CACjB,IAAI,eAAe,CAAC,IAAI,CAAC,EACzB,IAAI,CAAC,MAAM,CACZ,CAAC,KAAK,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,IAAG;gBACnC,OAAO,IAAI,CAAC,GAAG,CAAC,aAAa,CAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAC1B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CACnB,CAAA;AACH,aAAC,CAAC,CAAA;AACJ,SAAC,CAAA;AASD;;;AAGG;QACI,IAAY,CAAA,YAAA,GAAG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAuB,KAAI;;YAElF,IAAI,YAAY,GAAG,CAAC,EAAE;gBACpB,OAAM;AACP,aAAA;;AAGD,YAAA,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;;AAGnC,YAAA,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,KAAK,IAAG;AACvD,gBAAA,IAAI,KAAK,EAAE;AACT,oBAAA,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AACrB,iBAAA;AACH,aAAC,CAAC,CAAA;AACJ,SAAC,CAAA;QA1MC,IAAI,CAAC,aAAa,GAAG;YACnB,GAAG,IAAI,CAAC,aAAa;AACrB,YAAA,GAAG,aAAa;SACjB,CAAA;QAED,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,aAAa,CAAA;AAElD,QAAA,IAAI,CAAC,GAAG,GAAG,IAAI,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;AAE/C,QAAA,IAAI,CAAC,GAAG,GAAG,IAAI,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;QAC/C,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,gBAAgB,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAA;AAEzD,QAAA,IAAI,CAAC,OAAO,GAAG,IAAI,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;;AAGtC,QAAA,IAAI,CAAC,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAA;KAC7B;AAED,IAAA,MAAM,WAAW,CAAC,EAAE,QAAQ,EAAsB,EAAA;AAChD,QAAA,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAA;KAChC;AAED,IAAA,MAAM,QAAQ,CAAC,EAAE,aAAa,EAAmB,EAAA;QAC/C,IAAI,aAAa,CAAC,KAAK,EAAE;YACvB,OAAM;AACP,SAAA;AAED,QAAA,OAAO,CAAC,IAAI,CAAC,CAAA,EAAA,EAAK,KAAK,CAAC,MAAM,CAAC,sPAAsP,CAAC,CAAE,CAAA,CAAC,CAAA;QACzR,OAAO,CAAC,GAAG,EAAE,CAAA;KACd;AAEO,IAAA,MAAM,CAAC,YAAoB,EAAA;QACjC,OAAO,CAAA,EAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAA,CAAA,EAAI,YAAY,CAAA,CAAE,CAAA;KACtD;AAEO,IAAA,MAAM,CAAC,YAAoB,EAAA;QACjC,OAAO,CAAA,EAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA,CAAE,CAAA;KACzF;AAEO,IAAA,MAAM,CAAC,YAAoB,EAAA;QACjC,OAAO,CAAA,EAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAA;KACxC;AAEO,IAAA,OAAO,CAAC,YAAoB,EAAA;QAClC,OAAO,CAAA,EAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAA;KAC3C;AAED;;AAEG;AACI,IAAA,MAAM,iBAAiB,CAAC,EAAE,YAAY,EAAE,QAAQ,EAA4B,EAAA;QACjF,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAA;QAE1C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,KAAI;;;AAGrC,YAAA,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,OAAM,KAAK,KAAG;AAC3D,gBAAA,IAAI,KAAK,EAAE;oBACT,MAAM,CAAC,KAAK,CAAC,CAAA;oBACb,OAAM;AACP,iBAAA;AAED,gBAAA,IAAI,CAAC,oBAAoB,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAA;AACjD,gBAAA,IAAI,CAAC,kCAAkC,CAAC,YAAY,CAAC,CAAA;gBAErD,OAAO,CAAC,SAAS,CAAC,CAAA;AACpB,aAAC,CAAC,CAAA;AACJ,SAAC,CAAC,CAAA;KACH;AAED;;AAEG;AACK,IAAA,MAAM,oBAAoB,CAAC,YAAoB,EAAE,QAAkB,EAAA;AACzE,QAAA,MAAM,WAAW,GAAG,IAAI,eAAe,EAAE;AACtC,aAAA,iBAAiB,EAAE;aACnB,qBAAqB,CAAC,QAAQ,CAAC,CAAA;QAElC,OAAO,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,YAAY,EAAE,CAAC,CAAC,CAAA;KAClG;AAED;;AAEG;IACK,MAAM,kCAAkC,CAAC,YAAoB,EAAA;AACnE,QAAA,MAAM,gBAAgB,GAAG,IAAI,eAAe,EAAE;AAC3C,aAAA,mBAAmB,EAAE,CAAA;QAExB,OAAO,IAAI,CAAC,GAAG,CAAC,aAAa,CAC3B,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EACzB,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,YAAY,EAAE,CAAC,CAC7C,CAAA;KACF;AAED;;;AAGG;AACH,IAAA,MAAM,eAAe,CAAC,EAAE,YAAY,EAA0B,EAAA;;;QAG5D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,KAAI;YACrC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,OAAO,KAAK,EAAE,IAAI,KAAI;AAClG,gBAAA,IAAI,KAAK,IAAI,CAAC,IAAI,EAAE;;;AAGlB,oBAAA,MAAM,EAAE,CAAA;oBACR,OAAM;AACP,iBAAA;AAED,gBAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,CAAA;gBAEhD,OAAO,CAAC,SAAS,CAAC,CAAA;AACpB,aAAC,CAAC,CAAA;AACJ,SAAC,CAAC,CAAA;KACH;AAED;;AAEG;AACH,IAAA,MAAM,kBAAkB,CAAC,EAAE,YAAY,EAA6B,EAAA;;AAClE,QAAA,CAAA,EAAA,GAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,MAAM,GAC/C,KAAK,CAAC,MAAK;;;AAGZ,SAAC,CACA,CAAA,OAAO,CAAC,MAAK;AACZ,YAAA,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAA;AAC/C,SAAC,CAAC,CAAA;KACL;AAED;;AAEG;AACH,IAAA,MAAM,iBAAiB,CAAC,EACtB,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GACvB,EAAA;QACzB,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;AACrD,QAAA,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE;AAClC,aAAA,4BAA4B,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;QAE1D,OAAO,IAAI,CAAC,GAAG,CAAC,aAAa,CAC3B,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EACzB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CACpC,CAAA;KACF;AA+BD;;AAEG;IACI,MAAM,QAAQ,CAAC,IAAqB,EAAA;AACzC,QAAA,OAAO,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;KACnE;AAuBD;;AAEG;AACH,IAAA,MAAM,SAAS,GAAA;AACb,QAAA,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAA;KACpB;AACF;;;;"}
|
|
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 onListenPayload,\n beforeBroadcastStatelessPayload,\n} from '@hocuspocus/server'\nimport kleur from 'kleur'\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\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 }\n\n pub: RedisInstance\n\n sub: RedisInstance\n\n documents: Map<string, Document> = new Map()\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 }\n\n async onListen({ configuration }: onListenPayload) {\n if (configuration.quiet) {\n return\n }\n\n console.warn(` ${kleur.yellow('[BREAKING CHANGE] Wait, the Redis extension got an overhaul. The new Redis extension doesn’t persist data, it only syncs data between instances. Use @hocuspocus/extension-database to store your documents. It works well with the Redis extension.')}`)\n console.log()\n }\n\n private getKey(documentName: string) {\n return `${this.configuration.prefix}:${documentName}`\n }\n\n private pubKey(documentName: string) {\n return `${this.getKey(documentName)}:${this.configuration.identifier.replace(/:/g, '')}`\n }\n\n private subKey(documentName: string) {\n return `${this.getKey(documentName)}:*`\n }\n\n private lockKey(documentName: string) {\n return `${this.getKey(documentName)}:lock`\n }\n\n /**\n * Once a document is laoded, subscribe to the channel in Redis.\n */\n public async afterLoadDocument({ documentName, document }: afterLoadDocumentPayload) {\n this.documents.set(documentName, document)\n\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()\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()\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()\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 channelName = pattern.toString()\n const [_, documentName, identifier] = channelName.split(':')\n const document = this.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 new IncomingMessage(data),\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, clientsCount }: onDisconnectPayload) => {\n // Do nothing, when other users are still connected to the document.\n if (clientsCount > 0) {\n return\n }\n\n // It was indeed the last connected user.\n this.documents.delete(documentName)\n\n // Time to end the subscription on the document channel.\n this.sub.punsubscribe(this.subKey(documentName), error => {\n if (error) {\n console.error(error)\n }\n })\n }\n\n async beforeBroadcastStateless(data: beforeBroadcastStatelessPayload) {\n const message = new OutgoingMessage()\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 this.redlock.quit()\n }\n}\n"],"names":["uuid"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import RedisClient from 'ioredis';
|
|
1
|
+
import RedisClient, { ClusterNode, ClusterOptions, RedisOptions } from 'ioredis';
|
|
2
2
|
import Redlock from 'redlock';
|
|
3
|
-
import { Document, Extension, afterLoadDocumentPayload, afterStoreDocumentPayload, onDisconnectPayload, onStoreDocumentPayload, onAwarenessUpdatePayload, onChangePayload, Debugger, onConfigurePayload, onListenPayload } from '@hocuspocus/server';
|
|
3
|
+
import { Document, Extension, afterLoadDocumentPayload, afterStoreDocumentPayload, onDisconnectPayload, onStoreDocumentPayload, onAwarenessUpdatePayload, onChangePayload, Debugger, onConfigurePayload, onListenPayload, beforeBroadcastStatelessPayload } from '@hocuspocus/server';
|
|
4
|
+
export declare type RedisInstance = RedisClient.Cluster | RedisClient.Redis;
|
|
4
5
|
export interface Configuration {
|
|
5
6
|
/**
|
|
6
7
|
* Redis port
|
|
@@ -10,12 +11,24 @@ export interface Configuration {
|
|
|
10
11
|
* Redis host
|
|
11
12
|
*/
|
|
12
13
|
host: string;
|
|
14
|
+
/**
|
|
15
|
+
* Redis Cluster
|
|
16
|
+
*/
|
|
17
|
+
nodes?: ClusterNode[];
|
|
18
|
+
/**
|
|
19
|
+
* Duplicate from an existed Redis instance
|
|
20
|
+
*/
|
|
21
|
+
redis?: RedisInstance;
|
|
22
|
+
/**
|
|
23
|
+
* Redis instance creator
|
|
24
|
+
*/
|
|
25
|
+
createClient?: () => RedisInstance;
|
|
13
26
|
/**
|
|
14
27
|
* Options passed directly to Redis constructor
|
|
15
28
|
*
|
|
16
29
|
* https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options
|
|
17
30
|
*/
|
|
18
|
-
options?:
|
|
31
|
+
options?: ClusterOptions | RedisOptions;
|
|
19
32
|
/**
|
|
20
33
|
* An unique instance name, required to filter messages in Redis.
|
|
21
34
|
* If none is provided an unique id is generated.
|
|
@@ -38,8 +51,8 @@ export declare class Redis implements Extension {
|
|
|
38
51
|
*/
|
|
39
52
|
priority: number;
|
|
40
53
|
configuration: Configuration;
|
|
41
|
-
pub:
|
|
42
|
-
sub:
|
|
54
|
+
pub: RedisInstance;
|
|
55
|
+
sub: RedisInstance;
|
|
43
56
|
documents: Map<string, Document>;
|
|
44
57
|
redlock: Redlock;
|
|
45
58
|
locks: Map<string, Redlock.Lock>;
|
|
@@ -91,6 +104,7 @@ export declare class Redis implements Extension {
|
|
|
91
104
|
* noone connected anymore.
|
|
92
105
|
*/
|
|
93
106
|
onDisconnect: ({ documentName, clientsCount }: onDisconnectPayload) => Promise<void>;
|
|
107
|
+
beforeBroadcastStateless(data: beforeBroadcastStatelessPayload): Promise<number>;
|
|
94
108
|
/**
|
|
95
109
|
* Kill the Redlock connection immediately.
|
|
96
110
|
*/
|
|
@@ -3,7 +3,7 @@ import { Awareness } from 'y-protocols/awareness';
|
|
|
3
3
|
import * as mutex from 'lib0/mutex';
|
|
4
4
|
import type { Event, CloseEvent, MessageEvent } from 'ws';
|
|
5
5
|
import EventEmitter from './EventEmitter';
|
|
6
|
-
import { ConstructableOutgoingMessage, onAuthenticationFailedParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatusParameters, onSyncedParameters, WebSocketStatus } from './types';
|
|
6
|
+
import { ConstructableOutgoingMessage, onAuthenticationFailedParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatelessParameters, onStatusParameters, onSyncedParameters, WebSocketStatus } from './types';
|
|
7
7
|
import { onAwarenessChangeParameters, onAwarenessUpdateParameters } from '.';
|
|
8
8
|
export declare type HocuspocusProviderConfiguration = Required<Pick<CompleteHocuspocusProviderConfiguration, 'url' | 'name'>> & Partial<CompleteHocuspocusProviderConfiguration>;
|
|
9
9
|
export interface CompleteHocuspocusProviderConfiguration {
|
|
@@ -98,6 +98,7 @@ export interface CompleteHocuspocusProviderConfiguration {
|
|
|
98
98
|
onDestroy: () => void;
|
|
99
99
|
onAwarenessUpdate: (data: onAwarenessUpdateParameters) => void;
|
|
100
100
|
onAwarenessChange: (data: onAwarenessChangeParameters) => void;
|
|
101
|
+
onStateless: (data: onStatelessParameters) => void;
|
|
101
102
|
/**
|
|
102
103
|
* Don’t output any warnings.
|
|
103
104
|
*/
|
|
@@ -136,6 +137,7 @@ export declare class HocuspocusProvider extends EventEmitter {
|
|
|
136
137
|
boundBeforeUnload: () => void;
|
|
137
138
|
beforeUnload(): void;
|
|
138
139
|
registerEventListeners(): void;
|
|
140
|
+
sendStateless(payload: string): void;
|
|
139
141
|
documentUpdateHandler(update: Uint8Array, origin: any): void;
|
|
140
142
|
awarenessUpdateHandler({ added, updated, removed }: any, origin: any): void;
|
|
141
143
|
permissionDeniedHandler(reason: string): void;
|
|
@@ -144,6 +146,7 @@ export declare class HocuspocusProvider extends EventEmitter {
|
|
|
144
146
|
get url(): string;
|
|
145
147
|
get synced(): boolean;
|
|
146
148
|
set synced(state: boolean);
|
|
149
|
+
receiveStateless(payload: string): void;
|
|
147
150
|
get isAuthenticationRequired(): boolean;
|
|
148
151
|
disconnect(): void;
|
|
149
152
|
onOpen(event: Event): Promise<void>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MessageType, OutgoingMessageArguments } from '../types';
|
|
2
|
+
import { OutgoingMessage } from '../OutgoingMessage';
|
|
3
|
+
export declare class StatelessMessage extends OutgoingMessage {
|
|
4
|
+
type: MessageType;
|
|
5
|
+
description: string;
|
|
6
|
+
get(args: Partial<OutgoingMessageArguments>): import("lib0/encoding").Encoder;
|
|
7
|
+
}
|
|
@@ -14,7 +14,8 @@ export declare enum MessageType {
|
|
|
14
14
|
Sync = 0,
|
|
15
15
|
Awareness = 1,
|
|
16
16
|
Auth = 2,
|
|
17
|
-
QueryAwareness = 3
|
|
17
|
+
QueryAwareness = 3,
|
|
18
|
+
Stateless = 5
|
|
18
19
|
}
|
|
19
20
|
export declare enum WebSocketStatus {
|
|
20
21
|
Connecting = "connecting",
|
|
@@ -34,6 +35,7 @@ export interface OutgoingMessageArguments {
|
|
|
34
35
|
[key: string]: any;
|
|
35
36
|
}>;
|
|
36
37
|
update: any;
|
|
38
|
+
payload: string;
|
|
37
39
|
encoder: Encoder;
|
|
38
40
|
}
|
|
39
41
|
export interface Constructable<T> {
|
|
@@ -71,6 +73,9 @@ export declare type onAwarenessUpdateParameters = {
|
|
|
71
73
|
export declare type onAwarenessChangeParameters = {
|
|
72
74
|
states: StatesArray;
|
|
73
75
|
};
|
|
76
|
+
export declare type onStatelessParameters = {
|
|
77
|
+
payload: string;
|
|
78
|
+
};
|
|
74
79
|
export declare type StatesArray = {
|
|
75
80
|
clientId: number;
|
|
76
81
|
[key: string | number]: any;
|
|
@@ -5,6 +5,7 @@ import { IncomingMessage as HTTPIncomingMessage } from 'http';
|
|
|
5
5
|
import { CloseEvent } from '@hocuspocus/common';
|
|
6
6
|
import Document from './Document';
|
|
7
7
|
import { Debugger } from './Debugger';
|
|
8
|
+
import { onStatelessPayload } from './types';
|
|
8
9
|
export declare class Connection {
|
|
9
10
|
webSocket: WebSocket;
|
|
10
11
|
context: any;
|
|
@@ -26,6 +27,10 @@ export declare class Connection {
|
|
|
26
27
|
* Set a callback that will be triggered when the connection is closed
|
|
27
28
|
*/
|
|
28
29
|
onClose(callback: (document: Document) => void): Connection;
|
|
30
|
+
/**
|
|
31
|
+
* Set a callback that will be triggered when an stateless message is received
|
|
32
|
+
*/
|
|
33
|
+
onStatelessCallback(callback: (payload: onStatelessPayload) => Promise<void>): Connection;
|
|
29
34
|
/**
|
|
30
35
|
* Set a callback that will be triggered before an message is handled
|
|
31
36
|
*/
|
|
@@ -34,6 +39,10 @@ export declare class Connection {
|
|
|
34
39
|
* Send the given message
|
|
35
40
|
*/
|
|
36
41
|
send(message: any): void;
|
|
42
|
+
/**
|
|
43
|
+
* Send a stateless message with payload
|
|
44
|
+
*/
|
|
45
|
+
sendStateless(payload: string): void;
|
|
37
46
|
/**
|
|
38
47
|
* Graceful wrapper around the WebSocket close method.
|
|
39
48
|
*/
|
|
@@ -8,6 +8,7 @@ export declare class Document extends Doc {
|
|
|
8
8
|
awareness: Awareness;
|
|
9
9
|
callbacks: {
|
|
10
10
|
onUpdate: (document: Document, connection: Connection, update: Uint8Array) => void;
|
|
11
|
+
beforeBroadcastStateless: (document: Document, stateless: string) => void;
|
|
11
12
|
};
|
|
12
13
|
connections: Map<WebSocket, {
|
|
13
14
|
clients: Set<any>;
|
|
@@ -33,6 +34,10 @@ export declare class Document extends Doc {
|
|
|
33
34
|
* Set a callback that will be triggered when the document is updated
|
|
34
35
|
*/
|
|
35
36
|
onUpdate(callback: (document: Document, connection: Connection, update: Uint8Array) => void): Document;
|
|
37
|
+
/**
|
|
38
|
+
* Set a callback that will be triggered before a stateless message is broadcasted
|
|
39
|
+
*/
|
|
40
|
+
beforeBroadcastStateless(callback: (document: Document, stateless: string) => void): Document;
|
|
36
41
|
/**
|
|
37
42
|
* Register a connection and a set of clients on this document keyed by the
|
|
38
43
|
* underlying websocket connection
|
|
@@ -75,5 +80,9 @@ export declare class Document extends Doc {
|
|
|
75
80
|
* Handle an updated document and sync changes to clients
|
|
76
81
|
*/
|
|
77
82
|
private handleUpdate;
|
|
83
|
+
/**
|
|
84
|
+
* Broadcast stateless message to all connections
|
|
85
|
+
*/
|
|
86
|
+
broadcastStateless(payload: string): void;
|
|
78
87
|
}
|
|
79
88
|
export default Document;
|
|
@@ -14,5 +14,7 @@ export declare class OutgoingMessage {
|
|
|
14
14
|
writePermissionDenied(reason: string): OutgoingMessage;
|
|
15
15
|
writeFirstSyncStepFor(document: Document): OutgoingMessage;
|
|
16
16
|
writeUpdate(update: Uint8Array): OutgoingMessage;
|
|
17
|
+
writeStateless(payload: string): OutgoingMessage;
|
|
18
|
+
writeBroadcastStateless(payload: string): OutgoingMessage;
|
|
17
19
|
toUint8Array(): Uint8Array;
|
|
18
20
|
}
|
|
@@ -4,13 +4,16 @@ import { URLSearchParams } from 'url';
|
|
|
4
4
|
import { Awareness } from 'y-protocols/awareness';
|
|
5
5
|
import Document from './Document';
|
|
6
6
|
import { Hocuspocus } from './Hocuspocus';
|
|
7
|
+
import Connection from './Connection';
|
|
7
8
|
export declare enum MessageType {
|
|
8
9
|
Unknown = -1,
|
|
9
10
|
Sync = 0,
|
|
10
11
|
Awareness = 1,
|
|
11
12
|
Auth = 2,
|
|
12
13
|
QueryAwareness = 3,
|
|
13
|
-
SyncReply = 4
|
|
14
|
+
SyncReply = 4,
|
|
15
|
+
Stateless = 5,
|
|
16
|
+
BroadcastStateless = 6
|
|
14
17
|
}
|
|
15
18
|
export interface AwarenessUpdate {
|
|
16
19
|
added: Array<any>;
|
|
@@ -33,6 +36,8 @@ export interface Extension {
|
|
|
33
36
|
onLoadDocument?(data: onLoadDocumentPayload): Promise<any>;
|
|
34
37
|
afterLoadDocument?(data: onLoadDocumentPayload): Promise<any>;
|
|
35
38
|
beforeHandleMessage?(data: beforeHandleMessagePayload): Promise<any>;
|
|
39
|
+
beforeBroadcastStateless?(data: beforeBroadcastStatelessPayload): Promise<any>;
|
|
40
|
+
onStateless?(payload: onStatelessPayload): Promise<any>;
|
|
36
41
|
onChange?(data: onChangePayload): Promise<any>;
|
|
37
42
|
onStoreDocument?(data: onStoreDocumentPayload): Promise<any>;
|
|
38
43
|
afterStoreDocument?(data: afterStoreDocumentPayload): Promise<any>;
|
|
@@ -41,8 +46,8 @@ export interface Extension {
|
|
|
41
46
|
onDisconnect?(data: onDisconnectPayload): Promise<any>;
|
|
42
47
|
onDestroy?(data: onDestroyPayload): Promise<any>;
|
|
43
48
|
}
|
|
44
|
-
export declare type HookName = 'onConfigure' | 'onListen' | 'onUpgrade' | 'onConnect' | 'connected' | 'onAuthenticate' | 'onLoadDocument' | 'afterLoadDocument' | 'beforeHandleMessage' | 'onChange' | 'onStoreDocument' | 'afterStoreDocument' | 'onAwarenessUpdate' | 'onRequest' | 'onDisconnect' | 'onDestroy';
|
|
45
|
-
export declare type HookPayload = onConfigurePayload | onListenPayload | onUpgradePayload | onConnectPayload | connectedPayload | onAuthenticatePayload | onLoadDocumentPayload | onChangePayload | onStoreDocumentPayload | afterStoreDocumentPayload | onAwarenessUpdatePayload | onRequestPayload | onDisconnectPayload | onDestroyPayload;
|
|
49
|
+
export declare type HookName = 'onConfigure' | 'onListen' | 'onUpgrade' | 'onConnect' | 'connected' | 'onAuthenticate' | 'onLoadDocument' | 'afterLoadDocument' | 'beforeHandleMessage' | 'beforeBroadcastStateless' | 'onStateless' | 'onChange' | 'onStoreDocument' | 'afterStoreDocument' | 'onAwarenessUpdate' | 'onRequest' | 'onDisconnect' | 'onDestroy';
|
|
50
|
+
export declare type HookPayload = onConfigurePayload | onListenPayload | onUpgradePayload | onConnectPayload | connectedPayload | onAuthenticatePayload | onLoadDocumentPayload | onStatelessPayload | beforeHandleMessagePayload | beforeBroadcastStatelessPayload | onChangePayload | onStoreDocumentPayload | afterStoreDocumentPayload | onAwarenessUpdatePayload | onRequestPayload | onDisconnectPayload | onDestroyPayload;
|
|
46
51
|
export interface Configuration extends Extension {
|
|
47
52
|
/**
|
|
48
53
|
* A name for the instance, used for logging.
|
|
@@ -94,6 +99,12 @@ export interface getDocumentNamePayload {
|
|
|
94
99
|
request: IncomingMessage;
|
|
95
100
|
requestParameters: URLSearchParams;
|
|
96
101
|
}
|
|
102
|
+
export interface onStatelessPayload {
|
|
103
|
+
connection: Connection;
|
|
104
|
+
documentName: string;
|
|
105
|
+
document: Document;
|
|
106
|
+
payload: string;
|
|
107
|
+
}
|
|
97
108
|
export interface onAuthenticatePayload {
|
|
98
109
|
documentName: string;
|
|
99
110
|
instance: Hocuspocus;
|
|
@@ -163,6 +174,11 @@ export interface beforeHandleMessagePayload {
|
|
|
163
174
|
update: Uint8Array;
|
|
164
175
|
socketId: string;
|
|
165
176
|
}
|
|
177
|
+
export interface beforeBroadcastStatelessPayload {
|
|
178
|
+
document: Document;
|
|
179
|
+
documentName: string;
|
|
180
|
+
payload: string;
|
|
181
|
+
}
|
|
166
182
|
export interface onStoreDocumentPayload {
|
|
167
183
|
clientsCount: number;
|
|
168
184
|
context: any;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Doc } from 'yjs';
|
|
2
|
-
import { Extensions } from '@tiptap/core
|
|
2
|
+
import { Extensions } from '@tiptap/core';
|
|
3
3
|
import { Transformer } from './types';
|
|
4
4
|
export declare class Tiptap implements Transformer {
|
|
5
5
|
defaultExtensions: Extensions;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hocuspocus/extension-redis",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Scale Hocuspocus horizontally with Redis",
|
|
5
5
|
"homepage": "https://hocuspocus.dev",
|
|
6
6
|
"keywords": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@types/redlock": "^4.0.3"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@hocuspocus/server": "^1.0
|
|
35
|
+
"@hocuspocus/server": "^1.1.0",
|
|
36
36
|
"ioredis": "^4.28.2",
|
|
37
37
|
"kleur": "^4.1.4",
|
|
38
38
|
"lodash.debounce": "^4.0.8",
|
package/src/Redis.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import RedisClient from 'ioredis'
|
|
1
|
+
import RedisClient, { ClusterNode, ClusterOptions, RedisOptions } from 'ioredis'
|
|
2
2
|
import Redlock from 'redlock'
|
|
3
3
|
import { v4 as uuid } from 'uuid'
|
|
4
4
|
import {
|
|
@@ -16,9 +16,12 @@ import {
|
|
|
16
16
|
Debugger,
|
|
17
17
|
onConfigurePayload,
|
|
18
18
|
onListenPayload,
|
|
19
|
+
beforeBroadcastStatelessPayload,
|
|
19
20
|
} from '@hocuspocus/server'
|
|
20
21
|
import kleur from 'kleur'
|
|
21
22
|
|
|
23
|
+
export type RedisInstance = RedisClient.Cluster | RedisClient.Redis
|
|
24
|
+
|
|
22
25
|
export interface Configuration {
|
|
23
26
|
/**
|
|
24
27
|
* Redis port
|
|
@@ -28,12 +31,24 @@ export interface Configuration {
|
|
|
28
31
|
* Redis host
|
|
29
32
|
*/
|
|
30
33
|
host: string,
|
|
34
|
+
/**
|
|
35
|
+
* Redis Cluster
|
|
36
|
+
*/
|
|
37
|
+
nodes?: ClusterNode[],
|
|
38
|
+
/**
|
|
39
|
+
* Duplicate from an existed Redis instance
|
|
40
|
+
*/
|
|
41
|
+
redis?: RedisInstance,
|
|
42
|
+
/**
|
|
43
|
+
* Redis instance creator
|
|
44
|
+
*/
|
|
45
|
+
createClient?: () => RedisInstance,
|
|
31
46
|
/**
|
|
32
47
|
* Options passed directly to Redis constructor
|
|
33
48
|
*
|
|
34
49
|
* https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options
|
|
35
50
|
*/
|
|
36
|
-
options?:
|
|
51
|
+
options?: ClusterOptions | RedisOptions,
|
|
37
52
|
/**
|
|
38
53
|
* An unique instance name, required to filter messages in Redis.
|
|
39
54
|
* If none is provided an unique id is generated.
|
|
@@ -65,9 +80,9 @@ export class Redis implements Extension {
|
|
|
65
80
|
lockTimeout: 1000,
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
pub:
|
|
83
|
+
pub: RedisInstance
|
|
69
84
|
|
|
70
|
-
sub:
|
|
85
|
+
sub: RedisInstance
|
|
71
86
|
|
|
72
87
|
documents: Map<string, Document> = new Map()
|
|
73
88
|
|
|
@@ -83,17 +98,35 @@ export class Redis implements Extension {
|
|
|
83
98
|
...configuration,
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.pub = new RedisClient(port, host, options)
|
|
101
|
+
// We’ll replace that in the onConfigure hook with the global instance.
|
|
102
|
+
this.logger = new Debugger()
|
|
89
103
|
|
|
90
|
-
|
|
104
|
+
// Create Redis instance
|
|
105
|
+
const {
|
|
106
|
+
port,
|
|
107
|
+
host,
|
|
108
|
+
options,
|
|
109
|
+
nodes,
|
|
110
|
+
redis,
|
|
111
|
+
createClient,
|
|
112
|
+
} = this.configuration
|
|
113
|
+
|
|
114
|
+
if (typeof createClient === 'function') {
|
|
115
|
+
this.pub = createClient()
|
|
116
|
+
this.sub = createClient()
|
|
117
|
+
} else if (redis) {
|
|
118
|
+
this.pub = redis.duplicate()
|
|
119
|
+
this.sub = redis.duplicate()
|
|
120
|
+
} else if (nodes && nodes.length > 0) {
|
|
121
|
+
this.pub = new RedisClient.Cluster(nodes, options)
|
|
122
|
+
this.sub = new RedisClient.Cluster(nodes, options)
|
|
123
|
+
} else {
|
|
124
|
+
this.pub = new RedisClient(port, host, options)
|
|
125
|
+
this.sub = new RedisClient(port, host, options)
|
|
126
|
+
}
|
|
91
127
|
this.sub.on('pmessageBuffer', this.handleIncomingMessage)
|
|
92
128
|
|
|
93
129
|
this.redlock = new Redlock([this.pub])
|
|
94
|
-
|
|
95
|
-
// We’ll replace that in the onConfigure hook with the global instance.
|
|
96
|
-
this.logger = new Debugger()
|
|
97
130
|
}
|
|
98
131
|
|
|
99
132
|
async onConfigure({ instance }: onConfigurePayload) {
|
|
@@ -282,6 +315,16 @@ export class Redis implements Extension {
|
|
|
282
315
|
})
|
|
283
316
|
}
|
|
284
317
|
|
|
318
|
+
async beforeBroadcastStateless(data: beforeBroadcastStatelessPayload) {
|
|
319
|
+
const message = new OutgoingMessage()
|
|
320
|
+
.writeBroadcastStateless(data.payload)
|
|
321
|
+
|
|
322
|
+
return this.pub.publishBuffer(
|
|
323
|
+
this.pubKey(data.documentName),
|
|
324
|
+
Buffer.from(message.toUint8Array()),
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
285
328
|
/**
|
|
286
329
|
* Kill the Redlock connection immediately.
|
|
287
330
|
*/
|