@adonis-agora/collaboration 0.19.0 → 0.19.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/CHANGELOG.md +18 -0
- package/dist/src/drivers/automerge/automerge_driver.d.ts +51 -2
- package/dist/src/drivers/automerge/automerge_driver.d.ts.map +1 -1
- package/dist/src/drivers/automerge/automerge_driver.js +250 -87
- package/dist/src/drivers/automerge/automerge_driver.js.map +1 -1
- package/dist/src/drivers/lazy_deps.d.ts +10 -0
- package/dist/src/drivers/lazy_deps.d.ts.map +1 -0
- package/dist/src/drivers/lazy_deps.js +28 -0
- package/dist/src/drivers/lazy_deps.js.map +1 -0
- package/dist/src/drivers/partykit/partykit_driver.d.ts.map +1 -1
- package/dist/src/drivers/partykit/partykit_driver.js +2 -1
- package/dist/src/drivers/partykit/partykit_driver.js.map +1 -1
- package/dist/src/drivers/yjs/yjs_driver.d.ts +13 -1
- package/dist/src/drivers/yjs/yjs_driver.d.ts.map +1 -1
- package/dist/src/drivers/yjs/yjs_driver.js +63 -15
- package/dist/src/drivers/yjs/yjs_driver.js.map +1 -1
- package/dist/src/services/presence_service.d.ts +18 -3
- package/dist/src/services/presence_service.d.ts.map +1 -1
- package/dist/src/services/presence_service.js +104 -11
- package/dist/src/services/presence_service.js.map +1 -1
- package/package.json +5 -4
|
@@ -1,32 +1,92 @@
|
|
|
1
1
|
const TTL_SECONDS = 60;
|
|
2
|
+
/** How often a live instance re-stamps the members it holds. */
|
|
3
|
+
const REFRESH_INTERVAL_MS = (TTL_SECONDS * 1000) / 3;
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* Drops the members whose stamp is older than the cutoff — read again inside
|
|
6
|
+
* the script, so a member re-stamped since the list read stays.
|
|
7
|
+
* KEYS: members hash, stamps zset. ARGV: cutoff, then the user ids.
|
|
8
|
+
*/
|
|
9
|
+
const PRUNE_SCRIPT = `
|
|
10
|
+
local removed = 0
|
|
11
|
+
for i = 2, #ARGV do
|
|
12
|
+
local seen = redis.call('ZSCORE', KEYS[2], ARGV[i])
|
|
13
|
+
if seen and tonumber(seen) < tonumber(ARGV[1]) then
|
|
14
|
+
redis.call('HDEL', KEYS[1], ARGV[i])
|
|
15
|
+
redis.call('ZREM', KEYS[2], ARGV[i])
|
|
16
|
+
removed = removed + 1
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
return removed
|
|
20
|
+
`;
|
|
21
|
+
/**
|
|
22
|
+
* Redis-backed presence store. One hash of members per document, plus a
|
|
23
|
+
* sorted set stamping when each member was last confirmed. A member whose
|
|
24
|
+
* stamp is older than the TTL is not listed (and is pruned on the read), so
|
|
25
|
+
* the users of an instance that died without running its disconnects drop
|
|
26
|
+
* out even while others keep the document busy. Every live instance
|
|
27
|
+
* re-stamps the members it holds on an interval, which keeps them listed
|
|
28
|
+
* for as long as they stay connected. A member with no stamp is governed by
|
|
29
|
+
* the TTL of the whole key alone.
|
|
6
30
|
*/
|
|
7
31
|
export class RedisPresenceStore {
|
|
8
32
|
redis;
|
|
33
|
+
/** Members joined through this instance, by document and user. */
|
|
34
|
+
local = new Map();
|
|
35
|
+
refreshTimer;
|
|
9
36
|
constructor(redis) {
|
|
10
37
|
this.redis = redis;
|
|
11
38
|
}
|
|
12
39
|
key(docName) {
|
|
13
40
|
return `collab:presence:${docName}`;
|
|
14
41
|
}
|
|
42
|
+
seenKey(docName) {
|
|
43
|
+
return `collab:presence:${docName}:seen`;
|
|
44
|
+
}
|
|
15
45
|
async join(docName, member) {
|
|
16
|
-
|
|
46
|
+
let members = this.local.get(docName);
|
|
47
|
+
if (!members) {
|
|
48
|
+
members = new Map();
|
|
49
|
+
this.local.set(docName, members);
|
|
50
|
+
}
|
|
51
|
+
members.set(member.userId, member);
|
|
52
|
+
this.startRefreshing();
|
|
53
|
+
await this.stamp(docName, [member]);
|
|
54
|
+
}
|
|
55
|
+
async leave(docName, userId) {
|
|
56
|
+
const members = this.local.get(docName);
|
|
57
|
+
members?.delete(userId);
|
|
58
|
+
if (members?.size === 0)
|
|
59
|
+
this.local.delete(docName);
|
|
60
|
+
if (this.local.size === 0)
|
|
61
|
+
this.stopRefreshing();
|
|
17
62
|
await this.redis
|
|
18
63
|
.multi()
|
|
19
|
-
.
|
|
20
|
-
.
|
|
64
|
+
.hdel(this.key(docName), userId)
|
|
65
|
+
.zrem(this.seenKey(docName), userId)
|
|
21
66
|
.exec();
|
|
22
67
|
}
|
|
23
|
-
async leave(docName, userId) {
|
|
24
|
-
await this.redis.hdel(this.key(docName), userId);
|
|
25
|
-
}
|
|
26
68
|
async list(docName) {
|
|
27
|
-
const
|
|
69
|
+
const key = this.key(docName);
|
|
70
|
+
const seenKey = this.seenKey(docName);
|
|
71
|
+
const results = await this.redis
|
|
72
|
+
.multi()
|
|
73
|
+
.hgetall(key)
|
|
74
|
+
.zrange(seenKey, '0', '-1', 'WITHSCORES')
|
|
75
|
+
.exec();
|
|
76
|
+
const entries = (results?.[0]?.[1] ?? {});
|
|
77
|
+
const stamps = (results?.[1]?.[1] ?? []);
|
|
78
|
+
const seen = new Map();
|
|
79
|
+
for (let i = 0; i < stamps.length; i += 2)
|
|
80
|
+
seen.set(stamps[i], Number(stamps[i + 1]));
|
|
81
|
+
const cutoff = Date.now() - TTL_SECONDS * 1000;
|
|
28
82
|
const members = [];
|
|
29
|
-
|
|
83
|
+
const expired = [];
|
|
84
|
+
for (const [userId, value] of Object.entries(entries)) {
|
|
85
|
+
const stampedAt = seen.get(userId);
|
|
86
|
+
if (stampedAt !== undefined && stampedAt < cutoff) {
|
|
87
|
+
expired.push(userId);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
30
90
|
try {
|
|
31
91
|
members.push(JSON.parse(value));
|
|
32
92
|
}
|
|
@@ -34,11 +94,44 @@ export class RedisPresenceStore {
|
|
|
34
94
|
// Entrada corrompida/parcial — ignora.
|
|
35
95
|
}
|
|
36
96
|
}
|
|
97
|
+
if (expired.length > 0) {
|
|
98
|
+
await this.redis.eval(PRUNE_SCRIPT, 2, key, seenKey, String(cutoff), ...expired);
|
|
99
|
+
}
|
|
37
100
|
return members;
|
|
38
101
|
}
|
|
39
102
|
async close() {
|
|
103
|
+
this.stopRefreshing();
|
|
40
104
|
await this.redis.quit();
|
|
41
105
|
}
|
|
106
|
+
/** Writes `members` with a fresh stamp and extends both keys' TTL. */
|
|
107
|
+
async stamp(docName, members) {
|
|
108
|
+
const key = this.key(docName);
|
|
109
|
+
const seenKey = this.seenKey(docName);
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const pipeline = this.redis.multi();
|
|
112
|
+
for (const member of members) {
|
|
113
|
+
pipeline.hset(key, member.userId, JSON.stringify(member));
|
|
114
|
+
pipeline.zadd(seenKey, now, member.userId);
|
|
115
|
+
}
|
|
116
|
+
await pipeline.expire(key, TTL_SECONDS).expire(seenKey, TTL_SECONDS).exec();
|
|
117
|
+
}
|
|
118
|
+
startRefreshing() {
|
|
119
|
+
if (this.refreshTimer)
|
|
120
|
+
return;
|
|
121
|
+
this.refreshTimer = setInterval(() => void this.refresh(), REFRESH_INTERVAL_MS);
|
|
122
|
+
this.refreshTimer.unref?.();
|
|
123
|
+
}
|
|
124
|
+
stopRefreshing() {
|
|
125
|
+
if (this.refreshTimer)
|
|
126
|
+
clearInterval(this.refreshTimer);
|
|
127
|
+
this.refreshTimer = undefined;
|
|
128
|
+
}
|
|
129
|
+
/** Re-stamps every member this instance holds, so they outlive the TTL while connected. */
|
|
130
|
+
async refresh() {
|
|
131
|
+
await Promise.all([...this.local].map(([docName, members]) => this.stamp(docName, [...members.values()]).catch(() => {
|
|
132
|
+
// A failed refresh is retried on the next tick; expiry only needs one to land.
|
|
133
|
+
})));
|
|
134
|
+
}
|
|
42
135
|
}
|
|
43
136
|
/**
|
|
44
137
|
* Server-side presence (cross-instance). Join/leave is fed by the WS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presence_service.js","sourceRoot":"","sources":["../../../src/services/presence_service.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"presence_service.js","sourceRoot":"","sources":["../../../src/services/presence_service.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/collaboration",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "Collaborative editing (CRDT) for AdonisJS 7 — Yjs (Hocuspocus) and Automerge drivers, Redis-backed presence, unified version control. Part of the Agora ecosystem.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -122,12 +122,12 @@
|
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
124
|
"devDependencies": {
|
|
125
|
-
"@adonisjs/assembler": "8.
|
|
125
|
+
"@adonisjs/assembler": "8.6.0",
|
|
126
126
|
"@adonisjs/core": "7.6.0",
|
|
127
127
|
"@adonisjs/lucid": "22.4.2",
|
|
128
128
|
"@hocuspocus/provider": "4.7.1",
|
|
129
129
|
"@mdx-js/mdx": "3.1.1",
|
|
130
|
-
"@swc/core": "1.16.
|
|
130
|
+
"@swc/core": "1.16.20",
|
|
131
131
|
"@testcontainers/postgresql": "12.2.1",
|
|
132
132
|
"@types/luxon": "3.7.6",
|
|
133
133
|
"@types/node": "26.6.5",
|
|
@@ -135,9 +135,10 @@
|
|
|
135
135
|
"@types/ws": "8.18.2",
|
|
136
136
|
"luxon": "3.7.2",
|
|
137
137
|
"pg": "8.23.1",
|
|
138
|
+
"testcontainers": "12.2.1",
|
|
138
139
|
"typescript": "7.0.2",
|
|
139
140
|
"unplugin-swc": "2.0.0",
|
|
140
|
-
"vitest": "5.0.
|
|
141
|
+
"vitest": "5.0.5"
|
|
141
142
|
},
|
|
142
143
|
"engines": {
|
|
143
144
|
"node": ">=24"
|