@hocuspocus/extension-redis 3.4.6-rc.0 → 3.4.6-rc.2
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/package.json +2 -2
- package/dist/index.js +0 -206
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hocuspocus/extension-redis",
|
|
3
|
-
"version": "3.4.6-rc.
|
|
3
|
+
"version": "3.4.6-rc.2",
|
|
4
4
|
"description": "Scale Hocuspocus horizontally with Redis",
|
|
5
5
|
"homepage": "https://hocuspocus.dev",
|
|
6
6
|
"keywords": [
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@types/lodash.debounce": "^4.0.6"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@hocuspocus/server": "^3.4.6-rc.
|
|
34
|
+
"@hocuspocus/server": "^3.4.6-rc.2",
|
|
35
35
|
"@sesamecare-oss/redlock": "^1.4.0",
|
|
36
36
|
"ioredis": "^5.6.1",
|
|
37
37
|
"kleur": "^4.1.4",
|
package/dist/index.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import { IncomingMessage, MessageReceiver, OutgoingMessage } from "@hocuspocus/server";
|
|
3
|
-
import { Redlock } from "@sesamecare-oss/redlock";
|
|
4
|
-
import RedisClient from "ioredis";
|
|
5
|
-
|
|
6
|
-
//#region packages/extension-redis/src/Redis.ts
|
|
7
|
-
var Redis = class {
|
|
8
|
-
constructor(configuration) {
|
|
9
|
-
this.priority = 1e3;
|
|
10
|
-
this.configuration = {
|
|
11
|
-
port: 6379,
|
|
12
|
-
host: "127.0.0.1",
|
|
13
|
-
prefix: "hocuspocus",
|
|
14
|
-
identifier: `host-${crypto.randomUUID()}`,
|
|
15
|
-
lockTimeout: 1e3,
|
|
16
|
-
disconnectDelay: 1e3
|
|
17
|
-
};
|
|
18
|
-
this.redisTransactionOrigin = "__hocuspocus__redis__origin__";
|
|
19
|
-
this.locks = /* @__PURE__ */ new Map();
|
|
20
|
-
this.pendingAfterStoreDocumentResolves = /* @__PURE__ */ new Map();
|
|
21
|
-
this.handleIncomingMessage = async (channel, data) => {
|
|
22
|
-
const [identifier, messageBuffer] = this.decodeMessage(data);
|
|
23
|
-
if (identifier === this.configuration.identifier) return;
|
|
24
|
-
const message = new IncomingMessage(messageBuffer);
|
|
25
|
-
const documentName = message.readVarString();
|
|
26
|
-
message.writeVarString(documentName);
|
|
27
|
-
const document = this.instance.documents.get(documentName);
|
|
28
|
-
if (!document) return;
|
|
29
|
-
new MessageReceiver(message, this.redisTransactionOrigin).apply(document, void 0, (reply) => {
|
|
30
|
-
return this.pub.publish(this.pubKey(document.name), this.encodeMessage(reply));
|
|
31
|
-
});
|
|
32
|
-
};
|
|
33
|
-
this.configuration = {
|
|
34
|
-
...this.configuration,
|
|
35
|
-
...configuration
|
|
36
|
-
};
|
|
37
|
-
const { port, host, options, nodes, redis, createClient } = this.configuration;
|
|
38
|
-
if (typeof createClient === "function") {
|
|
39
|
-
this.pub = createClient();
|
|
40
|
-
this.sub = createClient();
|
|
41
|
-
} else if (redis) {
|
|
42
|
-
this.pub = redis.duplicate();
|
|
43
|
-
this.sub = redis.duplicate();
|
|
44
|
-
} else if (nodes && nodes.length > 0) {
|
|
45
|
-
this.pub = new RedisClient.Cluster(nodes, options);
|
|
46
|
-
this.sub = new RedisClient.Cluster(nodes, options);
|
|
47
|
-
} else {
|
|
48
|
-
this.pub = new RedisClient(port, host, options ?? {});
|
|
49
|
-
this.sub = new RedisClient(port, host, options ?? {});
|
|
50
|
-
}
|
|
51
|
-
this.sub.on("messageBuffer", this.handleIncomingMessage);
|
|
52
|
-
this.redlock = new Redlock([this.pub], { retryCount: 0 });
|
|
53
|
-
const identifierBuffer = Buffer.from(this.configuration.identifier, "utf-8");
|
|
54
|
-
this.messagePrefix = Buffer.concat([Buffer.from([identifierBuffer.length]), identifierBuffer]);
|
|
55
|
-
}
|
|
56
|
-
async onConfigure({ instance }) {
|
|
57
|
-
this.instance = instance;
|
|
58
|
-
}
|
|
59
|
-
getKey(documentName) {
|
|
60
|
-
return `${this.configuration.prefix}:${documentName}`;
|
|
61
|
-
}
|
|
62
|
-
pubKey(documentName) {
|
|
63
|
-
return this.getKey(documentName);
|
|
64
|
-
}
|
|
65
|
-
subKey(documentName) {
|
|
66
|
-
return this.getKey(documentName);
|
|
67
|
-
}
|
|
68
|
-
lockKey(documentName) {
|
|
69
|
-
return `${this.getKey(documentName)}:lock`;
|
|
70
|
-
}
|
|
71
|
-
encodeMessage(message) {
|
|
72
|
-
return Buffer.concat([this.messagePrefix, Buffer.from(message)]);
|
|
73
|
-
}
|
|
74
|
-
decodeMessage(buffer) {
|
|
75
|
-
const identifierLength = buffer[0];
|
|
76
|
-
return [buffer.toString("utf-8", 1, identifierLength + 1), buffer.slice(identifierLength + 1)];
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Once a document is loaded, subscribe to the channel in Redis.
|
|
80
|
-
*/
|
|
81
|
-
async afterLoadDocument({ documentName, document }) {
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
this.sub.subscribe(this.subKey(documentName), async (error) => {
|
|
84
|
-
if (error) {
|
|
85
|
-
reject(error);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
this.publishFirstSyncStep(documentName, document);
|
|
89
|
-
this.requestAwarenessFromOtherInstances(documentName);
|
|
90
|
-
resolve(void 0);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Publish the first sync step through Redis.
|
|
96
|
-
*/
|
|
97
|
-
async publishFirstSyncStep(documentName, document) {
|
|
98
|
-
const syncMessage = new OutgoingMessage(documentName).createSyncMessage().writeFirstSyncStepFor(document);
|
|
99
|
-
return this.pub.publish(this.pubKey(documentName), this.encodeMessage(syncMessage.toUint8Array()));
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Let’s ask Redis who is connected already.
|
|
103
|
-
*/
|
|
104
|
-
async requestAwarenessFromOtherInstances(documentName) {
|
|
105
|
-
const awarenessMessage = new OutgoingMessage(documentName).writeQueryAwareness();
|
|
106
|
-
return this.pub.publish(this.pubKey(documentName), this.encodeMessage(awarenessMessage.toUint8Array()));
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Before the document is stored, make sure to set a lock in Redis.
|
|
110
|
-
* That’s meant to avoid conflicts with other instances trying to store the document.
|
|
111
|
-
*/
|
|
112
|
-
async onStoreDocument({ documentName }) {
|
|
113
|
-
const resource = this.lockKey(documentName);
|
|
114
|
-
const ttl = this.configuration.lockTimeout;
|
|
115
|
-
try {
|
|
116
|
-
const lock = await this.redlock.acquire([resource], ttl);
|
|
117
|
-
const oldLock = this.locks.get(resource);
|
|
118
|
-
if (oldLock?.release) await oldLock.release;
|
|
119
|
-
this.locks.set(resource, { lock });
|
|
120
|
-
} catch (error) {
|
|
121
|
-
if (error == "ExecutionError: The operation was unable to achieve a quorum during its retry window.") throw new Error("", { cause: "Could not acquire lock, another instance locked it already." });
|
|
122
|
-
console.error("unexpected error:", error);
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Release the Redis lock, so other instances can store documents.
|
|
128
|
-
*/
|
|
129
|
-
async afterStoreDocument({ documentName, socketId }) {
|
|
130
|
-
const lockKey = this.lockKey(documentName);
|
|
131
|
-
const lock = this.locks.get(lockKey);
|
|
132
|
-
if (lock) try {
|
|
133
|
-
lock.release = lock.lock.release();
|
|
134
|
-
await lock.release;
|
|
135
|
-
} catch {} finally {
|
|
136
|
-
this.locks.delete(lockKey);
|
|
137
|
-
}
|
|
138
|
-
if (socketId === "server") {
|
|
139
|
-
const pending = this.pendingAfterStoreDocumentResolves.get(documentName);
|
|
140
|
-
if (pending) {
|
|
141
|
-
clearTimeout(pending.timeout);
|
|
142
|
-
pending.resolve();
|
|
143
|
-
this.pendingAfterStoreDocumentResolves.delete(documentName);
|
|
144
|
-
}
|
|
145
|
-
let resolveFunction = () => {};
|
|
146
|
-
const delayedPromise = new Promise((resolve) => {
|
|
147
|
-
resolveFunction = resolve;
|
|
148
|
-
});
|
|
149
|
-
const timeout = setTimeout(() => {
|
|
150
|
-
this.pendingAfterStoreDocumentResolves.delete(documentName);
|
|
151
|
-
resolveFunction();
|
|
152
|
-
}, this.configuration.disconnectDelay);
|
|
153
|
-
this.pendingAfterStoreDocumentResolves.set(documentName, {
|
|
154
|
-
timeout,
|
|
155
|
-
resolve: resolveFunction
|
|
156
|
-
});
|
|
157
|
-
await delayedPromise;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Handle awareness update messages received directly by this Hocuspocus instance.
|
|
162
|
-
*/
|
|
163
|
-
async onAwarenessUpdate({ documentName, awareness, added, updated, removed, document }) {
|
|
164
|
-
if ((document?.connections.size || 0) === 0) return;
|
|
165
|
-
const changedClients = added.concat(updated, removed);
|
|
166
|
-
const message = new OutgoingMessage(documentName).createAwarenessUpdateMessage(awareness, changedClients);
|
|
167
|
-
return this.pub.publish(this.pubKey(documentName), this.encodeMessage(message.toUint8Array()));
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* if the ydoc changed, we'll need to inform other Hocuspocus servers about it.
|
|
171
|
-
*/
|
|
172
|
-
async onChange(data) {
|
|
173
|
-
if (data.transactionOrigin !== this.redisTransactionOrigin) return this.publishFirstSyncStep(data.documentName, data.document);
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Delay unloading to allow syncs to finish
|
|
177
|
-
*/
|
|
178
|
-
async beforeUnloadDocument(data) {
|
|
179
|
-
return new Promise((resolve) => {
|
|
180
|
-
setTimeout(() => {
|
|
181
|
-
resolve();
|
|
182
|
-
}, this.configuration.disconnectDelay);
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
async afterUnloadDocument(data) {
|
|
186
|
-
if (data.instance.documents.has(data.documentName)) return;
|
|
187
|
-
this.sub.unsubscribe(this.subKey(data.documentName), (error) => {
|
|
188
|
-
if (error) console.error(error);
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
async beforeBroadcastStateless(data) {
|
|
192
|
-
const message = new OutgoingMessage(data.documentName).writeBroadcastStateless(data.payload);
|
|
193
|
-
return this.pub.publish(this.pubKey(data.documentName), this.encodeMessage(message.toUint8Array()));
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Kill the Redlock connection immediately.
|
|
197
|
-
*/
|
|
198
|
-
async onDestroy() {
|
|
199
|
-
await this.redlock.quit();
|
|
200
|
-
this.pub.disconnect(false);
|
|
201
|
-
this.sub.disconnect(false);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
//#endregion
|
|
206
|
-
export { Redis };
|