@hocuspocus/extension-redis 1.0.2 → 2.0.0-alpha.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 +33 -9
- package/dist/hocuspocus-redis.cjs.map +1 -1
- package/dist/hocuspocus-redis.esm.js +34 -10
- package/dist/hocuspocus-redis.esm.js.map +1 -1
- package/dist/packages/extension-monitor/src/Dashboard.d.ts +1 -1
- package/dist/packages/extension-monitor/src/index.d.ts +1 -1
- package/dist/packages/extension-redis/src/Redis.d.ts +19 -5
- package/dist/packages/provider/src/HocuspocusCloudProvider.d.ts +2 -1
- package/dist/packages/provider/src/HocuspocusProvider.d.ts +14 -70
- package/dist/packages/provider/src/HocuspocusProviderWebsocket.d.ts +115 -0
- package/dist/packages/provider/src/IncomingMessage.d.ts +2 -0
- package/dist/packages/provider/src/OutgoingMessages/StatelessMessage.d.ts +7 -0
- package/dist/packages/provider/src/index.d.ts +1 -0
- package/dist/packages/provider/src/types.d.ts +7 -1
- package/dist/packages/server/src/Connection.d.ts +10 -11
- package/dist/packages/server/src/Document.d.ts +9 -0
- package/dist/packages/server/src/Hocuspocus.d.ts +3 -8
- package/dist/packages/server/src/IncomingMessage.d.ts +2 -0
- package/dist/packages/server/src/MessageReceiver.d.ts +1 -2
- package/dist/packages/server/src/OutgoingMessage.d.ts +3 -1
- package/dist/packages/server/src/types.d.ts +19 -3
- package/dist/packages/transformer/src/Prosemirror.d.ts +1 -1
- package/dist/tests/providerwebsocket/configuration.d.ts +1 -0
- package/dist/tests/server/beforeBroadcastStateless.d.ts +1 -0
- package/dist/tests/server/onStateless.d.ts +1 -0
- package/dist/tests/utils/index.d.ts +1 -0
- package/dist/tests/utils/newHocuspocusProvider.d.ts +2 -2
- package/dist/tests/utils/newHocuspocusProviderWebsocket.d.ts +3 -0
- package/package.json +2 -2
- package/src/Redis.ts +63 -15
- /package/dist/tests/{provider/configuration.d.ts → extension-redis/onStateless.d.ts} +0 -0
- /package/dist/tests/{server/getDocumentName.d.ts → provider/onStateless.d.ts} +0 -0
|
@@ -37,6 +37,10 @@ class Redis {
|
|
|
37
37
|
* in Redis to filter these.
|
|
38
38
|
*/
|
|
39
39
|
this.handleIncomingMessage = async (channel, pattern, data) => {
|
|
40
|
+
const message = new server.IncomingMessage(data);
|
|
41
|
+
// we don't need the documentName from the message, we are just taking it from the redis channelName.
|
|
42
|
+
// we have to immediately write it back to the encoder though, to make sure the structure of the message is correct
|
|
43
|
+
message.writeVarString(message.readVarString());
|
|
40
44
|
const channelName = pattern.toString();
|
|
41
45
|
const [_, documentName, identifier] = channelName.split(':');
|
|
42
46
|
const document = this.documents.get(documentName);
|
|
@@ -46,7 +50,7 @@ class Redis {
|
|
|
46
50
|
if (!document) {
|
|
47
51
|
return;
|
|
48
52
|
}
|
|
49
|
-
new server.MessageReceiver(
|
|
53
|
+
new server.MessageReceiver(message, this.logger).apply(document, undefined, reply => {
|
|
50
54
|
return this.pub.publishBuffer(this.pubKey(document.name), Buffer.from(reply));
|
|
51
55
|
});
|
|
52
56
|
};
|
|
@@ -72,13 +76,28 @@ class Redis {
|
|
|
72
76
|
...this.configuration,
|
|
73
77
|
...configuration,
|
|
74
78
|
};
|
|
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
79
|
// We’ll replace that in the onConfigure hook with the global instance.
|
|
81
80
|
this.logger = new server.Debugger();
|
|
81
|
+
// Create Redis instance
|
|
82
|
+
const { port, host, options, nodes, redis, createClient, } = this.configuration;
|
|
83
|
+
if (typeof createClient === 'function') {
|
|
84
|
+
this.pub = createClient();
|
|
85
|
+
this.sub = createClient();
|
|
86
|
+
}
|
|
87
|
+
else if (redis) {
|
|
88
|
+
this.pub = redis.duplicate();
|
|
89
|
+
this.sub = redis.duplicate();
|
|
90
|
+
}
|
|
91
|
+
else if (nodes && nodes.length > 0) {
|
|
92
|
+
this.pub = new RedisClient__default["default"].Cluster(nodes, options);
|
|
93
|
+
this.sub = new RedisClient__default["default"].Cluster(nodes, options);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.pub = new RedisClient__default["default"](port, host, options);
|
|
97
|
+
this.sub = new RedisClient__default["default"](port, host, options);
|
|
98
|
+
}
|
|
99
|
+
this.sub.on('pmessageBuffer', this.handleIncomingMessage);
|
|
100
|
+
this.redlock = new Redlock__default["default"]([this.pub]);
|
|
82
101
|
}
|
|
83
102
|
async onConfigure({ instance }) {
|
|
84
103
|
this.logger = instance.debugger;
|
|
@@ -125,7 +144,7 @@ class Redis {
|
|
|
125
144
|
* Publish the first sync step through Redis.
|
|
126
145
|
*/
|
|
127
146
|
async publishFirstSyncStep(documentName, document) {
|
|
128
|
-
const syncMessage = new server.OutgoingMessage()
|
|
147
|
+
const syncMessage = new server.OutgoingMessage(documentName)
|
|
129
148
|
.createSyncMessage()
|
|
130
149
|
.writeFirstSyncStepFor(document);
|
|
131
150
|
return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(syncMessage.toUint8Array()));
|
|
@@ -134,7 +153,7 @@ class Redis {
|
|
|
134
153
|
* Let’s ask Redis who is connected already.
|
|
135
154
|
*/
|
|
136
155
|
async requestAwarenessFromOtherInstances(documentName) {
|
|
137
|
-
const awarenessMessage = new server.OutgoingMessage()
|
|
156
|
+
const awarenessMessage = new server.OutgoingMessage(documentName)
|
|
138
157
|
.writeQueryAwareness();
|
|
139
158
|
return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(awarenessMessage.toUint8Array()));
|
|
140
159
|
}
|
|
@@ -175,7 +194,7 @@ class Redis {
|
|
|
175
194
|
*/
|
|
176
195
|
async onAwarenessUpdate({ documentName, awareness, added, updated, removed, }) {
|
|
177
196
|
const changedClients = added.concat(updated, removed);
|
|
178
|
-
const message = new server.OutgoingMessage()
|
|
197
|
+
const message = new server.OutgoingMessage(documentName)
|
|
179
198
|
.createAwarenessUpdateMessage(awareness, changedClients);
|
|
180
199
|
return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(message.toUint8Array()));
|
|
181
200
|
}
|
|
@@ -185,6 +204,11 @@ class Redis {
|
|
|
185
204
|
async onChange(data) {
|
|
186
205
|
return this.publishFirstSyncStep(data.documentName, data.document);
|
|
187
206
|
}
|
|
207
|
+
async beforeBroadcastStateless(data) {
|
|
208
|
+
const message = new server.OutgoingMessage(data.documentName)
|
|
209
|
+
.writeBroadcastStateless(data.payload);
|
|
210
|
+
return this.pub.publishBuffer(this.pubKey(data.documentName), Buffer.from(message.toUint8Array()));
|
|
211
|
+
}
|
|
188
212
|
/**
|
|
189
213
|
* Kill the Redlock connection immediately.
|
|
190
214
|
*/
|
|
@@ -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(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.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, 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(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 this.redlock.quit()\n }\n}\n"],"names":["uuid","IncomingMessage","MessageReceiver","Debugger","RedisClient","Redlock","kleur","OutgoingMessage"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import RedisClient from 'ioredis';
|
|
2
2
|
import Redlock from 'redlock';
|
|
3
3
|
import { v4 } from 'uuid';
|
|
4
|
-
import {
|
|
4
|
+
import { IncomingMessage, MessageReceiver, Debugger, OutgoingMessage } from '@hocuspocus/server';
|
|
5
5
|
import kleur from 'kleur';
|
|
6
6
|
|
|
7
7
|
class Redis {
|
|
@@ -27,6 +27,10 @@ class Redis {
|
|
|
27
27
|
* in Redis to filter these.
|
|
28
28
|
*/
|
|
29
29
|
this.handleIncomingMessage = async (channel, pattern, data) => {
|
|
30
|
+
const message = new IncomingMessage(data);
|
|
31
|
+
// we don't need the documentName from the message, we are just taking it from the redis channelName.
|
|
32
|
+
// we have to immediately write it back to the encoder though, to make sure the structure of the message is correct
|
|
33
|
+
message.writeVarString(message.readVarString());
|
|
30
34
|
const channelName = pattern.toString();
|
|
31
35
|
const [_, documentName, identifier] = channelName.split(':');
|
|
32
36
|
const document = this.documents.get(documentName);
|
|
@@ -36,7 +40,7 @@ class Redis {
|
|
|
36
40
|
if (!document) {
|
|
37
41
|
return;
|
|
38
42
|
}
|
|
39
|
-
new MessageReceiver(
|
|
43
|
+
new MessageReceiver(message, this.logger).apply(document, undefined, reply => {
|
|
40
44
|
return this.pub.publishBuffer(this.pubKey(document.name), Buffer.from(reply));
|
|
41
45
|
});
|
|
42
46
|
};
|
|
@@ -62,13 +66,28 @@ class Redis {
|
|
|
62
66
|
...this.configuration,
|
|
63
67
|
...configuration,
|
|
64
68
|
};
|
|
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
69
|
// We’ll replace that in the onConfigure hook with the global instance.
|
|
71
70
|
this.logger = new Debugger();
|
|
71
|
+
// Create Redis instance
|
|
72
|
+
const { port, host, options, nodes, redis, createClient, } = this.configuration;
|
|
73
|
+
if (typeof createClient === 'function') {
|
|
74
|
+
this.pub = createClient();
|
|
75
|
+
this.sub = createClient();
|
|
76
|
+
}
|
|
77
|
+
else if (redis) {
|
|
78
|
+
this.pub = redis.duplicate();
|
|
79
|
+
this.sub = redis.duplicate();
|
|
80
|
+
}
|
|
81
|
+
else if (nodes && nodes.length > 0) {
|
|
82
|
+
this.pub = new RedisClient.Cluster(nodes, options);
|
|
83
|
+
this.sub = new RedisClient.Cluster(nodes, options);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
this.pub = new RedisClient(port, host, options);
|
|
87
|
+
this.sub = new RedisClient(port, host, options);
|
|
88
|
+
}
|
|
89
|
+
this.sub.on('pmessageBuffer', this.handleIncomingMessage);
|
|
90
|
+
this.redlock = new Redlock([this.pub]);
|
|
72
91
|
}
|
|
73
92
|
async onConfigure({ instance }) {
|
|
74
93
|
this.logger = instance.debugger;
|
|
@@ -115,7 +134,7 @@ class Redis {
|
|
|
115
134
|
* Publish the first sync step through Redis.
|
|
116
135
|
*/
|
|
117
136
|
async publishFirstSyncStep(documentName, document) {
|
|
118
|
-
const syncMessage = new OutgoingMessage()
|
|
137
|
+
const syncMessage = new OutgoingMessage(documentName)
|
|
119
138
|
.createSyncMessage()
|
|
120
139
|
.writeFirstSyncStepFor(document);
|
|
121
140
|
return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(syncMessage.toUint8Array()));
|
|
@@ -124,7 +143,7 @@ class Redis {
|
|
|
124
143
|
* Let’s ask Redis who is connected already.
|
|
125
144
|
*/
|
|
126
145
|
async requestAwarenessFromOtherInstances(documentName) {
|
|
127
|
-
const awarenessMessage = new OutgoingMessage()
|
|
146
|
+
const awarenessMessage = new OutgoingMessage(documentName)
|
|
128
147
|
.writeQueryAwareness();
|
|
129
148
|
return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(awarenessMessage.toUint8Array()));
|
|
130
149
|
}
|
|
@@ -165,7 +184,7 @@ class Redis {
|
|
|
165
184
|
*/
|
|
166
185
|
async onAwarenessUpdate({ documentName, awareness, added, updated, removed, }) {
|
|
167
186
|
const changedClients = added.concat(updated, removed);
|
|
168
|
-
const message = new OutgoingMessage()
|
|
187
|
+
const message = new OutgoingMessage(documentName)
|
|
169
188
|
.createAwarenessUpdateMessage(awareness, changedClients);
|
|
170
189
|
return this.pub.publishBuffer(this.pubKey(documentName), Buffer.from(message.toUint8Array()));
|
|
171
190
|
}
|
|
@@ -175,6 +194,11 @@ class Redis {
|
|
|
175
194
|
async onChange(data) {
|
|
176
195
|
return this.publishFirstSyncStep(data.documentName, data.document);
|
|
177
196
|
}
|
|
197
|
+
async beforeBroadcastStateless(data) {
|
|
198
|
+
const message = new OutgoingMessage(data.documentName)
|
|
199
|
+
.writeBroadcastStateless(data.payload);
|
|
200
|
+
return this.pub.publishBuffer(this.pubKey(data.documentName), Buffer.from(message.toUint8Array()));
|
|
201
|
+
}
|
|
178
202
|
/**
|
|
179
203
|
* Kill the Redlock connection immediately.
|
|
180
204
|
*/
|
|
@@ -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":"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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 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(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.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, 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(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 this.redlock.quit()\n }\n}\n"],"names":["uuid"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
3
|
-
import WebSocket, { WebSocketServer } from 'ws';
|
|
4
3
|
import { Socket } from 'net';
|
|
4
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
5
5
|
import { Storage } from './Storage';
|
|
6
6
|
export interface Configuration {
|
|
7
7
|
password: string | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import { Extension, onChangePayload, onConfigurePayload, onLoadDocumentPayload, onDisconnectPayload, onRequestPayload, onUpgradePayload, connectedPayload } from '@hocuspocus/server';
|
|
3
2
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
3
|
+
import { Extension, onChangePayload, onConfigurePayload, onLoadDocumentPayload, onDisconnectPayload, onRequestPayload, onUpgradePayload, connectedPayload } from '@hocuspocus/server';
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
5
|
import { Storage } from './Storage';
|
|
6
6
|
import { Dashboard } from './Dashboard';
|
|
@@ -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
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HocuspocusProvider, HocuspocusProviderConfiguration } from './HocuspocusProvider';
|
|
2
|
-
|
|
2
|
+
import { HocuspocusProviderWebsocketConfiguration } from './HocuspocusProviderWebsocket';
|
|
3
|
+
export declare type HocuspocusCloudProviderConfiguration = Required<Pick<HocuspocusProviderConfiguration, 'name'>> & Partial<HocuspocusProviderConfiguration> & Partial<Pick<HocuspocusProviderWebsocketConfiguration, 'url'>> & AdditionalHocuspocusCloudProviderConfiguration;
|
|
3
4
|
export interface AdditionalHocuspocusCloudProviderConfiguration {
|
|
4
5
|
/**
|
|
5
6
|
* A Hocuspocus Cloud key, get one here: https://hocuspocus.cloud/
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import * as Y from 'yjs';
|
|
2
2
|
import { Awareness } from 'y-protocols/awareness';
|
|
3
3
|
import * as mutex from 'lib0/mutex';
|
|
4
|
-
import type {
|
|
4
|
+
import type { CloseEvent, Event, 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
|
+
import { HocuspocusProviderWebsocket } from './HocuspocusProviderWebsocket';
|
|
7
8
|
import { onAwarenessChangeParameters, onAwarenessUpdateParameters } from '.';
|
|
8
|
-
export declare type HocuspocusProviderConfiguration = Required<Pick<CompleteHocuspocusProviderConfiguration, '
|
|
9
|
+
export declare type HocuspocusProviderConfiguration = Required<Pick<CompleteHocuspocusProviderConfiguration, 'name' | 'websocketProvider'>> & Partial<CompleteHocuspocusProviderConfiguration>;
|
|
9
10
|
export interface CompleteHocuspocusProviderConfiguration {
|
|
10
|
-
/**
|
|
11
|
-
* URL of your @hocuspocus/server instance
|
|
12
|
-
*/
|
|
13
|
-
url: string;
|
|
14
11
|
/**
|
|
15
12
|
* The identifier/name of your document
|
|
16
13
|
*/
|
|
@@ -19,10 +16,6 @@ export interface CompleteHocuspocusProviderConfiguration {
|
|
|
19
16
|
* The actual Y.js document
|
|
20
17
|
*/
|
|
21
18
|
document: Y.Doc;
|
|
22
|
-
/**
|
|
23
|
-
* Pass `false` to start the connection manually.
|
|
24
|
-
*/
|
|
25
|
-
connect: boolean;
|
|
26
19
|
/**
|
|
27
20
|
* Pass false to disable broadcasting between browser tabs.
|
|
28
21
|
*/
|
|
@@ -42,49 +35,13 @@ export interface CompleteHocuspocusProviderConfiguration {
|
|
|
42
35
|
[key: string]: any;
|
|
43
36
|
};
|
|
44
37
|
/**
|
|
45
|
-
*
|
|
38
|
+
* Hocuspocus websocket provider
|
|
46
39
|
*/
|
|
47
|
-
|
|
40
|
+
websocketProvider: HocuspocusProviderWebsocket;
|
|
48
41
|
/**
|
|
49
42
|
* Force syncing the document in the defined interval.
|
|
50
43
|
*/
|
|
51
44
|
forceSyncInterval: false | number;
|
|
52
|
-
/**
|
|
53
|
-
* Disconnect when no message is received for the defined amount of milliseconds.
|
|
54
|
-
*/
|
|
55
|
-
messageReconnectTimeout: number;
|
|
56
|
-
/**
|
|
57
|
-
* The delay between each attempt in milliseconds. You can provide a factor to have the delay grow exponentially.
|
|
58
|
-
*/
|
|
59
|
-
delay: number;
|
|
60
|
-
/**
|
|
61
|
-
* The intialDelay is the amount of time to wait before making the first attempt. This option should typically be 0 since you typically want the first attempt to happen immediately.
|
|
62
|
-
*/
|
|
63
|
-
initialDelay: number;
|
|
64
|
-
/**
|
|
65
|
-
* The factor option is used to grow the delay exponentially.
|
|
66
|
-
*/
|
|
67
|
-
factor: number;
|
|
68
|
-
/**
|
|
69
|
-
* The maximum number of attempts or 0 if there is no limit on number of attempts.
|
|
70
|
-
*/
|
|
71
|
-
maxAttempts: number;
|
|
72
|
-
/**
|
|
73
|
-
* minDelay is used to set a lower bound of delay when jitter is enabled. This property has no effect if jitter is disabled.
|
|
74
|
-
*/
|
|
75
|
-
minDelay: number;
|
|
76
|
-
/**
|
|
77
|
-
* The maxDelay option is used to set an upper bound for the delay when factor is enabled. A value of 0 can be provided if there should be no upper bound when calculating delay.
|
|
78
|
-
*/
|
|
79
|
-
maxDelay: number;
|
|
80
|
-
/**
|
|
81
|
-
* If jitter is true then the calculated delay will be a random integer value between minDelay and the calculated delay for the current iteration.
|
|
82
|
-
*/
|
|
83
|
-
jitter: boolean;
|
|
84
|
-
/**
|
|
85
|
-
* A timeout in milliseconds. If timeout is non-zero then a timer is set using setTimeout. If the timeout is triggered then future attempts will be aborted.
|
|
86
|
-
*/
|
|
87
|
-
timeout: number;
|
|
88
45
|
onAuthenticated: () => void;
|
|
89
46
|
onAuthenticationFailed: (data: onAuthenticationFailedParameters) => void;
|
|
90
47
|
onOpen: (data: onOpenParameters) => void;
|
|
@@ -98,6 +55,7 @@ export interface CompleteHocuspocusProviderConfiguration {
|
|
|
98
55
|
onDestroy: () => void;
|
|
99
56
|
onAwarenessUpdate: (data: onAwarenessUpdateParameters) => void;
|
|
100
57
|
onAwarenessChange: (data: onAwarenessChangeParameters) => void;
|
|
58
|
+
onStateless: (data: onStatelessParameters) => void;
|
|
101
59
|
/**
|
|
102
60
|
* Don’t output any warnings.
|
|
103
61
|
*/
|
|
@@ -106,53 +64,39 @@ export interface CompleteHocuspocusProviderConfiguration {
|
|
|
106
64
|
export declare class HocuspocusProvider extends EventEmitter {
|
|
107
65
|
configuration: CompleteHocuspocusProviderConfiguration;
|
|
108
66
|
subscribedToBroadcastChannel: boolean;
|
|
109
|
-
webSocket: WebSocket | null;
|
|
110
|
-
shouldConnect: boolean;
|
|
111
|
-
status: WebSocketStatus;
|
|
112
67
|
isSynced: boolean;
|
|
113
68
|
unsyncedChanges: number;
|
|
69
|
+
status: WebSocketStatus;
|
|
114
70
|
isAuthenticated: boolean;
|
|
115
|
-
lastMessageReceived: number;
|
|
116
71
|
mux: mutex.mutex;
|
|
117
72
|
intervals: any;
|
|
118
|
-
connectionAttempt: {
|
|
119
|
-
resolve: (value?: any) => void;
|
|
120
|
-
reject: (reason?: any) => void;
|
|
121
|
-
} | null;
|
|
122
73
|
constructor(configuration: HocuspocusProviderConfiguration);
|
|
74
|
+
onStatus({ status }: onStatusParameters): void;
|
|
123
75
|
setConfiguration(configuration?: Partial<HocuspocusProviderConfiguration>): void;
|
|
124
|
-
boundConnect: () => Promise<unknown>;
|
|
125
|
-
cancelWebsocketRetry?: () => void;
|
|
126
|
-
connect(): Promise<unknown>;
|
|
127
|
-
createWebSocketConnection(): Promise<unknown>;
|
|
128
|
-
resolveConnectionAttempt(): void;
|
|
129
|
-
stopConnectionAttempt(): void;
|
|
130
|
-
rejectConnectionAttempt(): void;
|
|
131
76
|
get document(): Y.Doc;
|
|
132
77
|
get awareness(): Awareness;
|
|
133
78
|
get hasUnsyncedChanges(): boolean;
|
|
134
|
-
checkConnection(): void;
|
|
135
79
|
forceSync(): void;
|
|
136
80
|
boundBeforeUnload: () => void;
|
|
137
81
|
beforeUnload(): void;
|
|
138
82
|
registerEventListeners(): void;
|
|
83
|
+
sendStateless(payload: string): void;
|
|
139
84
|
documentUpdateHandler(update: Uint8Array, origin: any): void;
|
|
140
85
|
awarenessUpdateHandler({ added, updated, removed }: any, origin: any): void;
|
|
141
|
-
permissionDeniedHandler(reason: string): void;
|
|
142
|
-
authenticatedHandler(): void;
|
|
143
|
-
get serverUrl(): string;
|
|
144
|
-
get url(): string;
|
|
145
86
|
get synced(): boolean;
|
|
146
87
|
set synced(state: boolean);
|
|
88
|
+
receiveStateless(payload: string): void;
|
|
147
89
|
get isAuthenticationRequired(): boolean;
|
|
148
90
|
disconnect(): void;
|
|
149
91
|
onOpen(event: Event): Promise<void>;
|
|
150
92
|
getToken(): Promise<string | null>;
|
|
151
93
|
startSync(): void;
|
|
152
|
-
send(
|
|
94
|
+
send(message: ConstructableOutgoingMessage, args: any, broadcast?: boolean): void;
|
|
153
95
|
onMessage(event: MessageEvent): void;
|
|
154
96
|
onClose(event: CloseEvent): void;
|
|
155
97
|
destroy(): void;
|
|
98
|
+
permissionDeniedHandler(reason: string): void;
|
|
99
|
+
authenticatedHandler(): void;
|
|
156
100
|
get broadcastChannel(): string;
|
|
157
101
|
boundBroadcastChannelSubscriber: (data: ArrayBuffer) => void;
|
|
158
102
|
broadcastChannelSubscriber(data: ArrayBuffer): void;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as mutex from 'lib0/mutex';
|
|
2
|
+
import type { MessageEvent } from 'ws';
|
|
3
|
+
import { Event } from 'ws';
|
|
4
|
+
import EventEmitter from './EventEmitter';
|
|
5
|
+
import { onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatusParameters, WebSocketStatus } from './types';
|
|
6
|
+
import { HocuspocusProvider, onAwarenessChangeParameters, onAwarenessUpdateParameters } from '.';
|
|
7
|
+
export declare type HocuspocusProviderWebsocketConfiguration = Required<Pick<CompleteHocuspocusProviderWebsocketConfiguration, 'url'>> & Partial<CompleteHocuspocusProviderWebsocketConfiguration>;
|
|
8
|
+
export interface CompleteHocuspocusProviderWebsocketConfiguration {
|
|
9
|
+
/**
|
|
10
|
+
* URL of your @hocuspocus/server instance
|
|
11
|
+
*/
|
|
12
|
+
url: string;
|
|
13
|
+
/**
|
|
14
|
+
* Pass `false` to start the connection manually.
|
|
15
|
+
*/
|
|
16
|
+
connect: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* URL parameters that should be added.
|
|
19
|
+
*/
|
|
20
|
+
parameters: {
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* An optional WebSocket polyfill, for example for Node.js
|
|
25
|
+
*/
|
|
26
|
+
WebSocketPolyfill: any;
|
|
27
|
+
/**
|
|
28
|
+
* Disconnect when no message is received for the defined amount of milliseconds.
|
|
29
|
+
*/
|
|
30
|
+
messageReconnectTimeout: number;
|
|
31
|
+
/**
|
|
32
|
+
* The delay between each attempt in milliseconds. You can provide a factor to have the delay grow exponentially.
|
|
33
|
+
*/
|
|
34
|
+
delay: number;
|
|
35
|
+
/**
|
|
36
|
+
* The intialDelay is the amount of time to wait before making the first attempt. This option should typically be 0 since you typically want the first attempt to happen immediately.
|
|
37
|
+
*/
|
|
38
|
+
initialDelay: number;
|
|
39
|
+
/**
|
|
40
|
+
* The factor option is used to grow the delay exponentially.
|
|
41
|
+
*/
|
|
42
|
+
factor: number;
|
|
43
|
+
/**
|
|
44
|
+
* The maximum number of attempts or 0 if there is no limit on number of attempts.
|
|
45
|
+
*/
|
|
46
|
+
maxAttempts: number;
|
|
47
|
+
/**
|
|
48
|
+
* minDelay is used to set a lower bound of delay when jitter is enabled. This property has no effect if jitter is disabled.
|
|
49
|
+
*/
|
|
50
|
+
minDelay: number;
|
|
51
|
+
/**
|
|
52
|
+
* The maxDelay option is used to set an upper bound for the delay when factor is enabled. A value of 0 can be provided if there should be no upper bound when calculating delay.
|
|
53
|
+
*/
|
|
54
|
+
maxDelay: number;
|
|
55
|
+
/**
|
|
56
|
+
* If jitter is true then the calculated delay will be a random integer value between minDelay and the calculated delay for the current iteration.
|
|
57
|
+
*/
|
|
58
|
+
jitter: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* A timeout in milliseconds. If timeout is non-zero then a timer is set using setTimeout. If the timeout is triggered then future attempts will be aborted.
|
|
61
|
+
*/
|
|
62
|
+
timeout: number;
|
|
63
|
+
onOpen: (data: onOpenParameters) => void;
|
|
64
|
+
onConnect: () => void;
|
|
65
|
+
onMessage: (data: onMessageParameters) => void;
|
|
66
|
+
onOutgoingMessage: (data: onOutgoingMessageParameters) => void;
|
|
67
|
+
onStatus: (data: onStatusParameters) => void;
|
|
68
|
+
onDisconnect: (data: onDisconnectParameters) => void;
|
|
69
|
+
onClose: (data: onCloseParameters) => void;
|
|
70
|
+
onDestroy: () => void;
|
|
71
|
+
onAwarenessUpdate: (data: onAwarenessUpdateParameters) => void;
|
|
72
|
+
onAwarenessChange: (data: onAwarenessChangeParameters) => void;
|
|
73
|
+
/**
|
|
74
|
+
* Don’t output any warnings.
|
|
75
|
+
*/
|
|
76
|
+
quiet: boolean;
|
|
77
|
+
}
|
|
78
|
+
export declare class HocuspocusProviderWebsocket extends EventEmitter {
|
|
79
|
+
configuration: CompleteHocuspocusProviderWebsocketConfiguration;
|
|
80
|
+
subscribedToBroadcastChannel: boolean;
|
|
81
|
+
webSocket: WebSocket | null;
|
|
82
|
+
shouldConnect: boolean;
|
|
83
|
+
status: WebSocketStatus;
|
|
84
|
+
lastMessageReceived: number;
|
|
85
|
+
mux: mutex.mutex;
|
|
86
|
+
intervals: any;
|
|
87
|
+
connectionAttempt: {
|
|
88
|
+
resolve: (value?: any) => void;
|
|
89
|
+
reject: (reason?: any) => void;
|
|
90
|
+
} | null;
|
|
91
|
+
constructor(configuration: HocuspocusProviderWebsocketConfiguration);
|
|
92
|
+
receivedOnOpenPayload?: Event | undefined;
|
|
93
|
+
receivedOnStatusPayload?: onStatusParameters | undefined;
|
|
94
|
+
onOpen(event: Event): Promise<void>;
|
|
95
|
+
onStatus(data: onStatusParameters): Promise<void>;
|
|
96
|
+
attach(provider: HocuspocusProvider): void;
|
|
97
|
+
detach(provider: HocuspocusProvider): void;
|
|
98
|
+
setConfiguration(configuration?: Partial<HocuspocusProviderWebsocketConfiguration>): void;
|
|
99
|
+
boundConnect: () => Promise<unknown>;
|
|
100
|
+
cancelWebsocketRetry?: () => void;
|
|
101
|
+
connect(): Promise<unknown>;
|
|
102
|
+
createWebSocketConnection(): Promise<unknown>;
|
|
103
|
+
onMessage(event: MessageEvent): void;
|
|
104
|
+
resolveConnectionAttempt(): void;
|
|
105
|
+
stopConnectionAttempt(): void;
|
|
106
|
+
rejectConnectionAttempt(): void;
|
|
107
|
+
checkConnection(): void;
|
|
108
|
+
registerEventListeners(): void;
|
|
109
|
+
get serverUrl(): string;
|
|
110
|
+
get url(): string;
|
|
111
|
+
disconnect(): void;
|
|
112
|
+
send(message: any): void;
|
|
113
|
+
onClose({ event }: onCloseParameters): void;
|
|
114
|
+
destroy(): void;
|
|
115
|
+
}
|
|
@@ -7,8 +7,10 @@ export declare class IncomingMessage {
|
|
|
7
7
|
decoder: Decoder;
|
|
8
8
|
constructor(data: any);
|
|
9
9
|
readVarUint(): MessageType;
|
|
10
|
+
readVarString(): string;
|
|
10
11
|
readVarUint8Array(): Uint8Array;
|
|
11
12
|
writeVarUint(type: MessageType): void;
|
|
13
|
+
writeVarString(string: string): void;
|
|
12
14
|
writeVarUint8Array(data: Uint8Array): void;
|
|
13
15
|
length(): number;
|
|
14
16
|
}
|
|
@@ -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",
|
|
@@ -26,6 +27,7 @@ export interface OutgoingMessageInterface {
|
|
|
26
27
|
type?: MessageType;
|
|
27
28
|
}
|
|
28
29
|
export interface OutgoingMessageArguments {
|
|
30
|
+
documentName: string;
|
|
29
31
|
token: string;
|
|
30
32
|
document: Y.Doc;
|
|
31
33
|
awareness: Awareness;
|
|
@@ -34,6 +36,7 @@ export interface OutgoingMessageArguments {
|
|
|
34
36
|
[key: string]: any;
|
|
35
37
|
}>;
|
|
36
38
|
update: any;
|
|
39
|
+
payload: string;
|
|
37
40
|
encoder: Encoder;
|
|
38
41
|
}
|
|
39
42
|
export interface Constructable<T> {
|
|
@@ -71,6 +74,9 @@ export declare type onAwarenessUpdateParameters = {
|
|
|
71
74
|
export declare type onAwarenessChangeParameters = {
|
|
72
75
|
states: StatesArray;
|
|
73
76
|
};
|
|
77
|
+
export declare type onStatelessParameters = {
|
|
78
|
+
payload: string;
|
|
79
|
+
};
|
|
74
80
|
export declare type StatesArray = {
|
|
75
81
|
clientId: number;
|
|
76
82
|
[key: string | number]: any;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
+
import { IncomingMessage as HTTPIncomingMessage } from 'http';
|
|
2
3
|
import AsyncLock from 'async-lock';
|
|
3
4
|
import WebSocket from 'ws';
|
|
4
|
-
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
|
*/
|
|
@@ -53,15 +62,5 @@ export declare class Connection {
|
|
|
53
62
|
* @private
|
|
54
63
|
*/
|
|
55
64
|
private handleMessage;
|
|
56
|
-
/**
|
|
57
|
-
* Get the underlying connection instance
|
|
58
|
-
* @deprecated
|
|
59
|
-
*/
|
|
60
|
-
get instance(): WebSocket;
|
|
61
|
-
/**
|
|
62
|
-
* Get the underlying connection instance
|
|
63
|
-
* @deprecated
|
|
64
|
-
*/
|
|
65
|
-
get connection(): WebSocket;
|
|
66
65
|
}
|
|
67
66
|
export default Connection;
|
|
@@ -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;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import WebSocket, { AddressInfo, WebSocketServer } from 'ws';
|
|
3
2
|
import { IncomingMessage, Server as HTTPServer } from 'http';
|
|
4
|
-
import {
|
|
3
|
+
import WebSocket, { AddressInfo, WebSocketServer } from 'ws';
|
|
4
|
+
import { Configuration, HookName, HookPayload, onListenPayload } from './types';
|
|
5
5
|
import Document from './Document';
|
|
6
6
|
import { Debugger } from './Debugger';
|
|
7
|
-
import { onListenPayload } from '.';
|
|
8
7
|
export declare const defaultConfiguration: {
|
|
9
8
|
name: null;
|
|
10
9
|
port: number;
|
|
@@ -68,7 +67,7 @@ export declare class Hocuspocus {
|
|
|
68
67
|
* … and if nothings fails it’ll fully establish the connection and
|
|
69
68
|
* load the Document then.
|
|
70
69
|
*/
|
|
71
|
-
handleConnection(incoming: WebSocket, request: IncomingMessage,
|
|
70
|
+
handleConnection(incoming: WebSocket, request: IncomingMessage, context?: any): void;
|
|
72
71
|
/**
|
|
73
72
|
* Handle update of the given document
|
|
74
73
|
*/
|
|
@@ -98,10 +97,6 @@ export declare class Hocuspocus {
|
|
|
98
97
|
* Get parameters by the given request
|
|
99
98
|
*/
|
|
100
99
|
private static getParameters;
|
|
101
|
-
/**
|
|
102
|
-
* Get document name by the given request
|
|
103
|
-
*/
|
|
104
|
-
private getDocumentNameFromRequest;
|
|
105
100
|
enableDebugging(): void;
|
|
106
101
|
enableMessageLogging(): void;
|
|
107
102
|
disableLogging(): void;
|
|
@@ -13,7 +13,9 @@ export declare class IncomingMessage {
|
|
|
13
13
|
constructor(input: any);
|
|
14
14
|
readVarUint8Array(): Uint8Array;
|
|
15
15
|
readVarUint(): number;
|
|
16
|
+
readVarString(): string;
|
|
16
17
|
toUint8Array(): Uint8Array;
|
|
17
18
|
writeVarUint(type: MessageType): void;
|
|
19
|
+
writeVarString(string: string): void;
|
|
18
20
|
get length(): number;
|
|
19
21
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Awareness } from 'y-protocols/awareness';
|
|
2
1
|
import Connection from './Connection';
|
|
3
2
|
import { IncomingMessage } from './IncomingMessage';
|
|
4
3
|
import { Debugger } from './Debugger';
|
|
@@ -9,5 +8,5 @@ export declare class MessageReceiver {
|
|
|
9
8
|
constructor(message: IncomingMessage, logger: Debugger);
|
|
10
9
|
apply(document: Document, connection?: Connection, reply?: (message: Uint8Array) => void): void;
|
|
11
10
|
readSyncMessage(message: IncomingMessage, document: Document, connection?: Connection, reply?: (message: Uint8Array) => void, requestFirstSync?: boolean): 0 | 2 | 1;
|
|
12
|
-
applyQueryAwarenessMessage(
|
|
11
|
+
applyQueryAwarenessMessage(document: Document, reply?: (message: Uint8Array) => void): void;
|
|
13
12
|
}
|
|
@@ -5,7 +5,7 @@ export declare class OutgoingMessage {
|
|
|
5
5
|
encoder: Encoder;
|
|
6
6
|
type?: number;
|
|
7
7
|
category?: string;
|
|
8
|
-
constructor();
|
|
8
|
+
constructor(documentName: string);
|
|
9
9
|
createSyncMessage(): OutgoingMessage;
|
|
10
10
|
createSyncReplyMessage(): OutgoingMessage;
|
|
11
11
|
createAwarenessUpdateMessage(awareness: Awareness, changedClients?: Array<any>): OutgoingMessage;
|
|
@@ -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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -2,6 +2,7 @@ export * from './createDirectory';
|
|
|
2
2
|
export * from './flushRedis';
|
|
3
3
|
export * from './newHocuspocus';
|
|
4
4
|
export * from './newHocuspocusProvider';
|
|
5
|
+
export * from './newHocuspocusProviderWebsocket';
|
|
5
6
|
export * from './randomInteger';
|
|
6
7
|
export * from './redisConnectionSettings';
|
|
7
8
|
export * from './removeDirectory';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { HocuspocusProvider, HocuspocusProviderConfiguration } from '@hocuspocus/provider';
|
|
1
|
+
import { HocuspocusProvider, HocuspocusProviderConfiguration, HocuspocusProviderWebsocketConfiguration } from '@hocuspocus/provider';
|
|
2
2
|
import { Hocuspocus } from '@hocuspocus/server';
|
|
3
|
-
export declare const newHocuspocusProvider: (server: Hocuspocus, options?: Partial<
|
|
3
|
+
export declare const newHocuspocusProvider: (server: Hocuspocus, options?: Partial<HocuspocusProviderConfiguration>, websocketOptions?: Partial<HocuspocusProviderWebsocketConfiguration>) => HocuspocusProvider;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { HocuspocusProviderWebsocket, HocuspocusProviderWebsocketConfiguration } from '@hocuspocus/provider';
|
|
2
|
+
import { Hocuspocus } from '@hocuspocus/server';
|
|
3
|
+
export declare const newHocuspocusProviderWebsocket: (server: Hocuspocus, options?: Partial<Omit<HocuspocusProviderWebsocketConfiguration, 'url'>>) => HocuspocusProviderWebsocket;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hocuspocus/extension-redis",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-alpha.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": "^
|
|
35
|
+
"@hocuspocus/server": "^2.0.0-alpha.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) {
|
|
@@ -152,7 +185,7 @@ export class Redis implements Extension {
|
|
|
152
185
|
* Publish the first sync step through Redis.
|
|
153
186
|
*/
|
|
154
187
|
private async publishFirstSyncStep(documentName: string, document: Document) {
|
|
155
|
-
const syncMessage = new OutgoingMessage()
|
|
188
|
+
const syncMessage = new OutgoingMessage(documentName)
|
|
156
189
|
.createSyncMessage()
|
|
157
190
|
.writeFirstSyncStepFor(document)
|
|
158
191
|
|
|
@@ -163,7 +196,7 @@ export class Redis implements Extension {
|
|
|
163
196
|
* Let’s ask Redis who is connected already.
|
|
164
197
|
*/
|
|
165
198
|
private async requestAwarenessFromOtherInstances(documentName: string) {
|
|
166
|
-
const awarenessMessage = new OutgoingMessage()
|
|
199
|
+
const awarenessMessage = new OutgoingMessage(documentName)
|
|
167
200
|
.writeQueryAwareness()
|
|
168
201
|
|
|
169
202
|
return this.pub.publishBuffer(
|
|
@@ -216,7 +249,7 @@ export class Redis implements Extension {
|
|
|
216
249
|
documentName, awareness, added, updated, removed,
|
|
217
250
|
}: onAwarenessUpdatePayload) {
|
|
218
251
|
const changedClients = added.concat(updated, removed)
|
|
219
|
-
const message = new OutgoingMessage()
|
|
252
|
+
const message = new OutgoingMessage(documentName)
|
|
220
253
|
.createAwarenessUpdateMessage(awareness, changedClients)
|
|
221
254
|
|
|
222
255
|
return this.pub.publishBuffer(
|
|
@@ -231,6 +264,11 @@ export class Redis implements Extension {
|
|
|
231
264
|
* in Redis to filter these.
|
|
232
265
|
*/
|
|
233
266
|
private handleIncomingMessage = async (channel: Buffer, pattern: Buffer, data: Buffer) => {
|
|
267
|
+
const message = new IncomingMessage(data)
|
|
268
|
+
// we don't need the documentName from the message, we are just taking it from the redis channelName.
|
|
269
|
+
// we have to immediately write it back to the encoder though, to make sure the structure of the message is correct
|
|
270
|
+
message.writeVarString(message.readVarString())
|
|
271
|
+
|
|
234
272
|
const channelName = pattern.toString()
|
|
235
273
|
const [_, documentName, identifier] = channelName.split(':')
|
|
236
274
|
const document = this.documents.get(documentName)
|
|
@@ -244,7 +282,7 @@ export class Redis implements Extension {
|
|
|
244
282
|
}
|
|
245
283
|
|
|
246
284
|
new MessageReceiver(
|
|
247
|
-
|
|
285
|
+
message,
|
|
248
286
|
this.logger,
|
|
249
287
|
).apply(document, undefined, reply => {
|
|
250
288
|
return this.pub.publishBuffer(
|
|
@@ -282,6 +320,16 @@ export class Redis implements Extension {
|
|
|
282
320
|
})
|
|
283
321
|
}
|
|
284
322
|
|
|
323
|
+
async beforeBroadcastStateless(data: beforeBroadcastStatelessPayload) {
|
|
324
|
+
const message = new OutgoingMessage(data.documentName)
|
|
325
|
+
.writeBroadcastStateless(data.payload)
|
|
326
|
+
|
|
327
|
+
return this.pub.publishBuffer(
|
|
328
|
+
this.pubKey(data.documentName),
|
|
329
|
+
Buffer.from(message.toUint8Array()),
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
285
333
|
/**
|
|
286
334
|
* Kill the Redlock connection immediately.
|
|
287
335
|
*/
|
|
File without changes
|
|
File without changes
|