@hocuspocus/extension-redis 3.2.4 → 3.2.6
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 +55 -58
- package/dist/hocuspocus-redis.cjs.map +1 -1
- package/dist/hocuspocus-redis.esm.js +55 -58
- package/dist/hocuspocus-redis.esm.js.map +1 -1
- package/dist/packages/extension-redis/src/Redis.d.ts +6 -11
- package/dist/packages/extension-webhook/src/index.d.ts +2 -2
- package/dist/packages/server/src/Document.d.ts +2 -0
- package/package.json +3 -4
- package/src/Redis.ts +78 -73
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var crypto = require('node:crypto');
|
|
3
4
|
var server = require('@hocuspocus/server');
|
|
4
5
|
var redlock = require('@sesamecare-oss/redlock');
|
|
5
6
|
var RedisClient = require('ioredis');
|
|
6
|
-
var uuid = require('uuid');
|
|
7
7
|
|
|
8
8
|
class Redis {
|
|
9
9
|
constructor(configuration) {
|
|
@@ -17,17 +17,12 @@ class Redis {
|
|
|
17
17
|
port: 6379,
|
|
18
18
|
host: "127.0.0.1",
|
|
19
19
|
prefix: "hocuspocus",
|
|
20
|
-
identifier: `host-${
|
|
20
|
+
identifier: `host-${crypto.randomUUID()}`,
|
|
21
21
|
lockTimeout: 1000,
|
|
22
22
|
disconnectDelay: 1000,
|
|
23
23
|
};
|
|
24
24
|
this.redisTransactionOrigin = "__hocuspocus__redis__origin__";
|
|
25
25
|
this.locks = new Map();
|
|
26
|
-
/**
|
|
27
|
-
* When we have a high frequency of updates to a document we don't need tons of setTimeouts
|
|
28
|
-
* piling up, so we'll track them to keep it to the most recent per document.
|
|
29
|
-
*/
|
|
30
|
-
this.pendingDisconnects = new Map();
|
|
31
26
|
this.pendingAfterStoreDocumentResolves = new Map();
|
|
32
27
|
/**
|
|
33
28
|
* Handle incoming messages published on subscribed document channels.
|
|
@@ -50,37 +45,6 @@ class Redis {
|
|
|
50
45
|
return this.pub.publish(this.pubKey(document.name), this.encodeMessage(reply));
|
|
51
46
|
});
|
|
52
47
|
};
|
|
53
|
-
/**
|
|
54
|
-
* Make sure to *not* listen for further changes, when there’s
|
|
55
|
-
* no one connected anymore.
|
|
56
|
-
*/
|
|
57
|
-
this.onDisconnect = async ({ documentName }) => {
|
|
58
|
-
const pending = this.pendingDisconnects.get(documentName);
|
|
59
|
-
if (pending) {
|
|
60
|
-
clearTimeout(pending);
|
|
61
|
-
this.pendingDisconnects.delete(documentName);
|
|
62
|
-
}
|
|
63
|
-
const disconnect = () => {
|
|
64
|
-
const document = this.instance.documents.get(documentName);
|
|
65
|
-
this.pendingDisconnects.delete(documentName);
|
|
66
|
-
// Do nothing, when other users are still connected to the document.
|
|
67
|
-
if (document && document.getConnectionsCount() > 0) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
// Time to end the subscription on the document channel.
|
|
71
|
-
this.sub.unsubscribe(this.subKey(documentName), (error) => {
|
|
72
|
-
if (error) {
|
|
73
|
-
console.error(error);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
if (document) {
|
|
77
|
-
this.instance.unloadDocument(document);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
// Delay the disconnect procedure to allow last minute syncs to happen
|
|
81
|
-
const timeout = setTimeout(disconnect, this.configuration.disconnectDelay);
|
|
82
|
-
this.pendingDisconnects.set(documentName, timeout);
|
|
83
|
-
};
|
|
84
48
|
this.configuration = {
|
|
85
49
|
...this.configuration,
|
|
86
50
|
...configuration,
|
|
@@ -105,7 +69,7 @@ class Redis {
|
|
|
105
69
|
}
|
|
106
70
|
this.sub.on("messageBuffer", this.handleIncomingMessage);
|
|
107
71
|
this.redlock = new redlock.Redlock([this.pub], {
|
|
108
|
-
|
|
72
|
+
retryCount: 0,
|
|
109
73
|
});
|
|
110
74
|
const identifierBuffer = Buffer.from(this.configuration.identifier, "utf-8");
|
|
111
75
|
this.messagePrefix = Buffer.concat([
|
|
@@ -179,32 +143,46 @@ class Redis {
|
|
|
179
143
|
// to avoid conflict with other instances storing the same document.
|
|
180
144
|
const resource = this.lockKey(documentName);
|
|
181
145
|
const ttl = this.configuration.lockTimeout;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
146
|
+
try {
|
|
147
|
+
await this.redlock.acquire([resource], ttl);
|
|
148
|
+
const oldLock = this.locks.get(resource);
|
|
149
|
+
if (oldLock) {
|
|
150
|
+
await oldLock.release;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
//based on: https://github.com/sesamecare/redlock/blob/508e00dcd1e4d2bc6373ce455f4fe847e98a9aab/src/index.ts#L347-L349
|
|
155
|
+
if (error ==
|
|
156
|
+
"ExecutionError: The operation was unable to achieve a quorum during its retry window.") {
|
|
157
|
+
// Expected behavior: Could not acquire lock, another instance locked it already.
|
|
158
|
+
// No further `onStoreDocument` hooks will be executed; should throw a silent error with no message.
|
|
159
|
+
throw new Error("", {
|
|
160
|
+
cause: "Could not acquire lock, another instance locked it already.",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
//unexpected error
|
|
164
|
+
console.error("unexpected error:", error);
|
|
165
|
+
throw error;
|
|
186
166
|
}
|
|
187
|
-
this.locks.set(resource, { lock });
|
|
188
167
|
}
|
|
189
168
|
/**
|
|
190
169
|
* Release the Redis lock, so other instances can store documents.
|
|
191
170
|
*/
|
|
192
|
-
async afterStoreDocument({ documentName, socketId }) {
|
|
171
|
+
async afterStoreDocument({ documentName, socketId, }) {
|
|
193
172
|
const lockKey = this.lockKey(documentName);
|
|
194
173
|
const lock = this.locks.get(lockKey);
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
this.locks.delete(lockKey);
|
|
174
|
+
if (lock) {
|
|
175
|
+
try {
|
|
176
|
+
// Always try to unlock and clean up the lock
|
|
177
|
+
lock.release = lock.lock.release();
|
|
178
|
+
await lock.release;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Lock will expire on its own after timeout
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
this.locks.delete(lockKey);
|
|
185
|
+
}
|
|
208
186
|
}
|
|
209
187
|
// if the change was initiated by a directConnection, we need to delay this hook to make sure sync can finish first.
|
|
210
188
|
// for provider connections, this usually happens in the onDisconnect hook
|
|
@@ -246,6 +224,25 @@ class Redis {
|
|
|
246
224
|
return this.publishFirstSyncStep(data.documentName, data.document);
|
|
247
225
|
}
|
|
248
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Delay unloading to allow syncs to finish
|
|
229
|
+
*/
|
|
230
|
+
async beforeUnloadDocument(data) {
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
resolve();
|
|
234
|
+
}, this.configuration.disconnectDelay);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async afterUnloadDocument(data) {
|
|
238
|
+
if (data.instance.documents.has(data.documentName))
|
|
239
|
+
return; // skip unsubscribe if the document is already loaded again (maybe fast reconnect)
|
|
240
|
+
this.sub.unsubscribe(this.subKey(data.documentName), (error) => {
|
|
241
|
+
if (error) {
|
|
242
|
+
console.error(error);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
249
246
|
async beforeBroadcastStateless(data) {
|
|
250
247
|
const message = new server.OutgoingMessage(data.documentName).writeBroadcastStateless(data.payload);
|
|
251
248
|
return this.pub.publish(this.pubKey(data.documentName), this.encodeMessage(message.toUint8Array()));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hocuspocus-redis.cjs","sources":["../src/Redis.ts"],"sourcesContent":[null],"names":["
|
|
1
|
+
{"version":3,"file":"hocuspocus-redis.cjs","sources":["../src/Redis.ts"],"sourcesContent":[null],"names":["IncomingMessage","MessageReceiver","Redlock","OutgoingMessage"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import { IncomingMessage, MessageReceiver, OutgoingMessage } from '@hocuspocus/server';
|
|
2
3
|
import { Redlock } from '@sesamecare-oss/redlock';
|
|
3
4
|
import RedisClient from 'ioredis';
|
|
4
|
-
import { v4 } from 'uuid';
|
|
5
5
|
|
|
6
6
|
class Redis {
|
|
7
7
|
constructor(configuration) {
|
|
@@ -15,17 +15,12 @@ class Redis {
|
|
|
15
15
|
port: 6379,
|
|
16
16
|
host: "127.0.0.1",
|
|
17
17
|
prefix: "hocuspocus",
|
|
18
|
-
identifier: `host-${
|
|
18
|
+
identifier: `host-${crypto.randomUUID()}`,
|
|
19
19
|
lockTimeout: 1000,
|
|
20
20
|
disconnectDelay: 1000,
|
|
21
21
|
};
|
|
22
22
|
this.redisTransactionOrigin = "__hocuspocus__redis__origin__";
|
|
23
23
|
this.locks = new Map();
|
|
24
|
-
/**
|
|
25
|
-
* When we have a high frequency of updates to a document we don't need tons of setTimeouts
|
|
26
|
-
* piling up, so we'll track them to keep it to the most recent per document.
|
|
27
|
-
*/
|
|
28
|
-
this.pendingDisconnects = new Map();
|
|
29
24
|
this.pendingAfterStoreDocumentResolves = new Map();
|
|
30
25
|
/**
|
|
31
26
|
* Handle incoming messages published on subscribed document channels.
|
|
@@ -48,37 +43,6 @@ class Redis {
|
|
|
48
43
|
return this.pub.publish(this.pubKey(document.name), this.encodeMessage(reply));
|
|
49
44
|
});
|
|
50
45
|
};
|
|
51
|
-
/**
|
|
52
|
-
* Make sure to *not* listen for further changes, when there’s
|
|
53
|
-
* no one connected anymore.
|
|
54
|
-
*/
|
|
55
|
-
this.onDisconnect = async ({ documentName }) => {
|
|
56
|
-
const pending = this.pendingDisconnects.get(documentName);
|
|
57
|
-
if (pending) {
|
|
58
|
-
clearTimeout(pending);
|
|
59
|
-
this.pendingDisconnects.delete(documentName);
|
|
60
|
-
}
|
|
61
|
-
const disconnect = () => {
|
|
62
|
-
const document = this.instance.documents.get(documentName);
|
|
63
|
-
this.pendingDisconnects.delete(documentName);
|
|
64
|
-
// Do nothing, when other users are still connected to the document.
|
|
65
|
-
if (document && document.getConnectionsCount() > 0) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
// Time to end the subscription on the document channel.
|
|
69
|
-
this.sub.unsubscribe(this.subKey(documentName), (error) => {
|
|
70
|
-
if (error) {
|
|
71
|
-
console.error(error);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
if (document) {
|
|
75
|
-
this.instance.unloadDocument(document);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
// Delay the disconnect procedure to allow last minute syncs to happen
|
|
79
|
-
const timeout = setTimeout(disconnect, this.configuration.disconnectDelay);
|
|
80
|
-
this.pendingDisconnects.set(documentName, timeout);
|
|
81
|
-
};
|
|
82
46
|
this.configuration = {
|
|
83
47
|
...this.configuration,
|
|
84
48
|
...configuration,
|
|
@@ -103,7 +67,7 @@ class Redis {
|
|
|
103
67
|
}
|
|
104
68
|
this.sub.on("messageBuffer", this.handleIncomingMessage);
|
|
105
69
|
this.redlock = new Redlock([this.pub], {
|
|
106
|
-
|
|
70
|
+
retryCount: 0,
|
|
107
71
|
});
|
|
108
72
|
const identifierBuffer = Buffer.from(this.configuration.identifier, "utf-8");
|
|
109
73
|
this.messagePrefix = Buffer.concat([
|
|
@@ -177,32 +141,46 @@ class Redis {
|
|
|
177
141
|
// to avoid conflict with other instances storing the same document.
|
|
178
142
|
const resource = this.lockKey(documentName);
|
|
179
143
|
const ttl = this.configuration.lockTimeout;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
144
|
+
try {
|
|
145
|
+
await this.redlock.acquire([resource], ttl);
|
|
146
|
+
const oldLock = this.locks.get(resource);
|
|
147
|
+
if (oldLock) {
|
|
148
|
+
await oldLock.release;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
//based on: https://github.com/sesamecare/redlock/blob/508e00dcd1e4d2bc6373ce455f4fe847e98a9aab/src/index.ts#L347-L349
|
|
153
|
+
if (error ==
|
|
154
|
+
"ExecutionError: The operation was unable to achieve a quorum during its retry window.") {
|
|
155
|
+
// Expected behavior: Could not acquire lock, another instance locked it already.
|
|
156
|
+
// No further `onStoreDocument` hooks will be executed; should throw a silent error with no message.
|
|
157
|
+
throw new Error("", {
|
|
158
|
+
cause: "Could not acquire lock, another instance locked it already.",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
//unexpected error
|
|
162
|
+
console.error("unexpected error:", error);
|
|
163
|
+
throw error;
|
|
184
164
|
}
|
|
185
|
-
this.locks.set(resource, { lock });
|
|
186
165
|
}
|
|
187
166
|
/**
|
|
188
167
|
* Release the Redis lock, so other instances can store documents.
|
|
189
168
|
*/
|
|
190
|
-
async afterStoreDocument({ documentName, socketId }) {
|
|
169
|
+
async afterStoreDocument({ documentName, socketId, }) {
|
|
191
170
|
const lockKey = this.lockKey(documentName);
|
|
192
171
|
const lock = this.locks.get(lockKey);
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.locks.delete(lockKey);
|
|
172
|
+
if (lock) {
|
|
173
|
+
try {
|
|
174
|
+
// Always try to unlock and clean up the lock
|
|
175
|
+
lock.release = lock.lock.release();
|
|
176
|
+
await lock.release;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Lock will expire on its own after timeout
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
this.locks.delete(lockKey);
|
|
183
|
+
}
|
|
206
184
|
}
|
|
207
185
|
// if the change was initiated by a directConnection, we need to delay this hook to make sure sync can finish first.
|
|
208
186
|
// for provider connections, this usually happens in the onDisconnect hook
|
|
@@ -244,6 +222,25 @@ class Redis {
|
|
|
244
222
|
return this.publishFirstSyncStep(data.documentName, data.document);
|
|
245
223
|
}
|
|
246
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Delay unloading to allow syncs to finish
|
|
227
|
+
*/
|
|
228
|
+
async beforeUnloadDocument(data) {
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
resolve();
|
|
232
|
+
}, this.configuration.disconnectDelay);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async afterUnloadDocument(data) {
|
|
236
|
+
if (data.instance.documents.has(data.documentName))
|
|
237
|
+
return; // skip unsubscribe if the document is already loaded again (maybe fast reconnect)
|
|
238
|
+
this.sub.unsubscribe(this.subKey(data.documentName), (error) => {
|
|
239
|
+
if (error) {
|
|
240
|
+
console.error(error);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
247
244
|
async beforeBroadcastStateless(data) {
|
|
248
245
|
const message = new OutgoingMessage(data.documentName).writeBroadcastStateless(data.payload);
|
|
249
246
|
return this.pub.publish(this.pubKey(data.documentName), this.encodeMessage(message.toUint8Array()));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hocuspocus-redis.esm.js","sources":["../src/Redis.ts"],"sourcesContent":[null],"names":[
|
|
1
|
+
{"version":3,"file":"hocuspocus-redis.esm.js","sources":["../src/Redis.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Extension, Hocuspocus, afterLoadDocumentPayload, afterStoreDocumentPayload, beforeBroadcastStatelessPayload, onAwarenessUpdatePayload, onChangePayload, onConfigurePayload,
|
|
2
|
-
import {
|
|
1
|
+
import type { Extension, Hocuspocus, afterLoadDocumentPayload, afterStoreDocumentPayload, afterUnloadDocumentPayload, beforeBroadcastStatelessPayload, beforeUnloadDocumentPayload, onAwarenessUpdatePayload, onChangePayload, onConfigurePayload, onStoreDocumentPayload } from "@hocuspocus/server";
|
|
2
|
+
import { type ExecutionResult, type Lock, Redlock } from "@sesamecare-oss/redlock";
|
|
3
3
|
import type { Cluster, ClusterNode, ClusterOptions, RedisOptions } from "ioredis";
|
|
4
4
|
import RedisClient from "ioredis";
|
|
5
5
|
export type RedisInstance = RedisClient | Cluster;
|
|
@@ -67,11 +67,6 @@ export declare class Redis implements Extension {
|
|
|
67
67
|
release?: Promise<ExecutionResult>;
|
|
68
68
|
}>;
|
|
69
69
|
messagePrefix: Buffer;
|
|
70
|
-
/**
|
|
71
|
-
* When we have a high frequency of updates to a document we don't need tons of setTimeouts
|
|
72
|
-
* piling up, so we'll track them to keep it to the most recent per document.
|
|
73
|
-
*/
|
|
74
|
-
private pendingDisconnects;
|
|
75
70
|
private pendingAfterStoreDocumentResolves;
|
|
76
71
|
constructor(configuration: Partial<Configuration>);
|
|
77
72
|
onConfigure({ instance }: onConfigurePayload): Promise<void>;
|
|
@@ -101,7 +96,7 @@ export declare class Redis implements Extension {
|
|
|
101
96
|
/**
|
|
102
97
|
* Release the Redis lock, so other instances can store documents.
|
|
103
98
|
*/
|
|
104
|
-
afterStoreDocument({ documentName, socketId }: afterStoreDocumentPayload): Promise<void>;
|
|
99
|
+
afterStoreDocument({ documentName, socketId, }: afterStoreDocumentPayload): Promise<void>;
|
|
105
100
|
/**
|
|
106
101
|
* Handle awareness update messages received directly by this Hocuspocus instance.
|
|
107
102
|
*/
|
|
@@ -117,10 +112,10 @@ export declare class Redis implements Extension {
|
|
|
117
112
|
*/
|
|
118
113
|
onChange(data: onChangePayload): Promise<any>;
|
|
119
114
|
/**
|
|
120
|
-
*
|
|
121
|
-
* no one connected anymore.
|
|
115
|
+
* Delay unloading to allow syncs to finish
|
|
122
116
|
*/
|
|
123
|
-
|
|
117
|
+
beforeUnloadDocument(data: beforeUnloadDocumentPayload): Promise<void>;
|
|
118
|
+
afterUnloadDocument(data: afterUnloadDocumentPayload): Promise<void>;
|
|
124
119
|
beforeBroadcastStateless(data: beforeBroadcastStatelessPayload): Promise<number>;
|
|
125
120
|
/**
|
|
126
121
|
* Kill the Redlock connection immediately.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Extension, onChangePayload, onConnectPayload,
|
|
2
|
-
import type { Doc } from "yjs";
|
|
1
|
+
import type { Extension, onChangePayload, onConnectPayload, onDisconnectPayload, onLoadDocumentPayload } from "@hocuspocus/server";
|
|
3
2
|
import type { Transformer } from "@hocuspocus/transformer";
|
|
3
|
+
import type { Doc } from "yjs";
|
|
4
4
|
export declare enum Events {
|
|
5
5
|
onChange = "change",
|
|
6
6
|
onConnect = "connect",
|
|
@@ -2,6 +2,7 @@ import type WebSocket from "ws";
|
|
|
2
2
|
import { Awareness } from "y-protocols/awareness";
|
|
3
3
|
import { Doc } from "yjs";
|
|
4
4
|
import type Connection from "./Connection.ts";
|
|
5
|
+
import { Mutex } from "async-mutex";
|
|
5
6
|
export declare class Document extends Doc {
|
|
6
7
|
awareness: Awareness;
|
|
7
8
|
callbacks: {
|
|
@@ -16,6 +17,7 @@ export declare class Document extends Doc {
|
|
|
16
17
|
name: string;
|
|
17
18
|
isLoading: boolean;
|
|
18
19
|
isDestroyed: boolean;
|
|
20
|
+
saveMutex: Mutex;
|
|
19
21
|
/**
|
|
20
22
|
* Constructor.
|
|
21
23
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hocuspocus/extension-redis",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.6",
|
|
4
4
|
"description": "Scale Hocuspocus horizontally with Redis",
|
|
5
5
|
"homepage": "https://hocuspocus.dev",
|
|
6
6
|
"keywords": [
|
|
@@ -31,12 +31,11 @@
|
|
|
31
31
|
"@types/lodash.debounce": "^4.0.6"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@hocuspocus/server": "^3.2.
|
|
34
|
+
"@hocuspocus/server": "^3.2.6",
|
|
35
35
|
"@sesamecare-oss/redlock": "^1.4.0",
|
|
36
36
|
"ioredis": "^5.6.1",
|
|
37
37
|
"kleur": "^4.1.4",
|
|
38
|
-
"lodash.debounce": "^4.0.8"
|
|
39
|
-
"uuid": "^13.0.0"
|
|
38
|
+
"lodash.debounce": "^4.0.8"
|
|
40
39
|
},
|
|
41
40
|
"peerDependencies": {
|
|
42
41
|
"y-protocols": "^1.0.6",
|
package/src/Redis.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import type {
|
|
2
3
|
Document,
|
|
3
4
|
Extension,
|
|
4
5
|
Hocuspocus,
|
|
5
6
|
afterLoadDocumentPayload,
|
|
6
7
|
afterStoreDocumentPayload,
|
|
8
|
+
afterUnloadDocumentPayload,
|
|
7
9
|
beforeBroadcastStatelessPayload,
|
|
10
|
+
beforeUnloadDocumentPayload,
|
|
8
11
|
onAwarenessUpdatePayload,
|
|
9
12
|
onChangePayload,
|
|
10
13
|
onConfigurePayload,
|
|
11
|
-
onDisconnectPayload,
|
|
12
14
|
onStoreDocumentPayload,
|
|
13
15
|
} from "@hocuspocus/server";
|
|
14
16
|
import {
|
|
@@ -16,11 +18,19 @@ import {
|
|
|
16
18
|
MessageReceiver,
|
|
17
19
|
OutgoingMessage,
|
|
18
20
|
} from "@hocuspocus/server";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
+
import {
|
|
22
|
+
type ExecutionResult,
|
|
23
|
+
type Lock,
|
|
24
|
+
Redlock,
|
|
25
|
+
} from "@sesamecare-oss/redlock";
|
|
26
|
+
import type {
|
|
27
|
+
Cluster,
|
|
28
|
+
ClusterNode,
|
|
29
|
+
ClusterOptions,
|
|
30
|
+
RedisOptions,
|
|
31
|
+
} from "ioredis";
|
|
21
32
|
import RedisClient from "ioredis";
|
|
22
|
-
|
|
23
|
-
export type RedisInstance = RedisClient | Cluster
|
|
33
|
+
export type RedisInstance = RedisClient | Cluster;
|
|
24
34
|
export interface Configuration {
|
|
25
35
|
/**
|
|
26
36
|
* Redis port
|
|
@@ -80,7 +90,7 @@ export class Redis implements Extension {
|
|
|
80
90
|
port: 6379,
|
|
81
91
|
host: "127.0.0.1",
|
|
82
92
|
prefix: "hocuspocus",
|
|
83
|
-
identifier: `host-${
|
|
93
|
+
identifier: `host-${crypto.randomUUID()}`,
|
|
84
94
|
lockTimeout: 1000,
|
|
85
95
|
disconnectDelay: 1000,
|
|
86
96
|
};
|
|
@@ -95,16 +105,10 @@ export class Redis implements Extension {
|
|
|
95
105
|
|
|
96
106
|
redlock: Redlock;
|
|
97
107
|
|
|
98
|
-
locks = new Map<string, {lock: Lock; release?: Promise<ExecutionResult>}>();
|
|
108
|
+
locks = new Map<string, { lock: Lock; release?: Promise<ExecutionResult> }>();
|
|
99
109
|
|
|
100
110
|
messagePrefix: Buffer;
|
|
101
111
|
|
|
102
|
-
/**
|
|
103
|
-
* When we have a high frequency of updates to a document we don't need tons of setTimeouts
|
|
104
|
-
* piling up, so we'll track them to keep it to the most recent per document.
|
|
105
|
-
*/
|
|
106
|
-
private pendingDisconnects = new Map<string, NodeJS.Timeout>();
|
|
107
|
-
|
|
108
112
|
private pendingAfterStoreDocumentResolves = new Map<
|
|
109
113
|
string,
|
|
110
114
|
{ timeout: NodeJS.Timeout; resolve: () => void }
|
|
@@ -136,7 +140,7 @@ export class Redis implements Extension {
|
|
|
136
140
|
this.sub.on("messageBuffer", this.handleIncomingMessage);
|
|
137
141
|
|
|
138
142
|
this.redlock = new Redlock([this.pub], {
|
|
139
|
-
|
|
143
|
+
retryCount: 0,
|
|
140
144
|
});
|
|
141
145
|
|
|
142
146
|
const identifierBuffer = Buffer.from(
|
|
@@ -236,37 +240,55 @@ export class Redis implements Extension {
|
|
|
236
240
|
* Before the document is stored, make sure to set a lock in Redis.
|
|
237
241
|
* That’s meant to avoid conflicts with other instances trying to store the document.
|
|
238
242
|
*/
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
243
|
+
async onStoreDocument({ documentName }: onStoreDocumentPayload) {
|
|
244
|
+
// Attempt to acquire a lock and read lastReceivedTimestamp from Redis,
|
|
245
|
+
// to avoid conflict with other instances storing the same document.
|
|
246
|
+
const resource = this.lockKey(documentName);
|
|
247
|
+
const ttl = this.configuration.lockTimeout;
|
|
248
|
+
try {
|
|
249
|
+
await this.redlock.acquire([resource], ttl);
|
|
250
|
+
const oldLock = this.locks.get(resource);
|
|
251
|
+
if (oldLock) {
|
|
252
|
+
await oldLock.release;
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
//based on: https://github.com/sesamecare/redlock/blob/508e00dcd1e4d2bc6373ce455f4fe847e98a9aab/src/index.ts#L347-L349
|
|
256
|
+
if (
|
|
257
|
+
error ==
|
|
258
|
+
"ExecutionError: The operation was unable to achieve a quorum during its retry window."
|
|
259
|
+
) {
|
|
260
|
+
// Expected behavior: Could not acquire lock, another instance locked it already.
|
|
261
|
+
// No further `onStoreDocument` hooks will be executed; should throw a silent error with no message.
|
|
262
|
+
throw new Error("", {
|
|
263
|
+
cause: "Could not acquire lock, another instance locked it already.",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
//unexpected error
|
|
267
|
+
console.error("unexpected error:", error);
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
251
271
|
|
|
252
272
|
/**
|
|
253
273
|
* Release the Redis lock, so other instances can store documents.
|
|
254
274
|
*/
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
async afterStoreDocument({
|
|
276
|
+
documentName,
|
|
277
|
+
socketId,
|
|
278
|
+
}: afterStoreDocumentPayload) {
|
|
279
|
+
const lockKey = this.lockKey(documentName);
|
|
280
|
+
const lock = this.locks.get(lockKey);
|
|
281
|
+
if (lock) {
|
|
282
|
+
try {
|
|
283
|
+
// Always try to unlock and clean up the lock
|
|
284
|
+
lock.release = lock.lock.release();
|
|
285
|
+
await lock.release;
|
|
286
|
+
} catch {
|
|
287
|
+
// Lock will expire on its own after timeout
|
|
288
|
+
} finally {
|
|
289
|
+
this.locks.delete(lockKey);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
270
292
|
// if the change was initiated by a directConnection, we need to delay this hook to make sure sync can finish first.
|
|
271
293
|
// for provider connections, this usually happens in the onDisconnect hook
|
|
272
294
|
if (socketId === "server") {
|
|
@@ -362,42 +384,25 @@ export class Redis implements Extension {
|
|
|
362
384
|
}
|
|
363
385
|
|
|
364
386
|
/**
|
|
365
|
-
*
|
|
366
|
-
* no one connected anymore.
|
|
387
|
+
* Delay unloading to allow syncs to finish
|
|
367
388
|
*/
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const disconnect = () => {
|
|
377
|
-
const document = this.instance.documents.get(documentName);
|
|
378
|
-
|
|
379
|
-
this.pendingDisconnects.delete(documentName);
|
|
380
|
-
|
|
381
|
-
// Do nothing, when other users are still connected to the document.
|
|
382
|
-
if (document && document.getConnectionsCount() > 0) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
389
|
+
async beforeUnloadDocument(data: beforeUnloadDocumentPayload) {
|
|
390
|
+
return new Promise<void>((resolve) => {
|
|
391
|
+
setTimeout(() => {
|
|
392
|
+
resolve();
|
|
393
|
+
}, this.configuration.disconnectDelay);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
385
396
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (error) {
|
|
389
|
-
console.error(error);
|
|
390
|
-
}
|
|
391
|
-
});
|
|
397
|
+
async afterUnloadDocument(data: afterUnloadDocumentPayload) {
|
|
398
|
+
if (data.instance.documents.has(data.documentName)) return; // skip unsubscribe if the document is already loaded again (maybe fast reconnect)
|
|
392
399
|
|
|
393
|
-
|
|
394
|
-
|
|
400
|
+
this.sub.unsubscribe(this.subKey(data.documentName), (error: any) => {
|
|
401
|
+
if (error) {
|
|
402
|
+
console.error(error);
|
|
395
403
|
}
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const timeout = setTimeout(disconnect, this.configuration.disconnectDelay);
|
|
399
|
-
this.pendingDisconnects.set(documentName, timeout);
|
|
400
|
-
};
|
|
404
|
+
});
|
|
405
|
+
}
|
|
401
406
|
|
|
402
407
|
async beforeBroadcastStateless(data: beforeBroadcastStatelessPayload) {
|
|
403
408
|
const message = new OutgoingMessage(
|