@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.
@@ -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
- * Redis-backed presence store. One hash per document; members expire via
4
- * TTL refresh on join (no heartbeat handled here — drivers refresh on
5
- * connect and we prune stale entries on read).
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
- const key = this.key(docName);
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
- .hset(key, member.userId, JSON.stringify(member))
20
- .expire(key, TTL_SECONDS)
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 entries = await this.redis.hgetall(this.key(docName));
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
- for (const value of Object.values(entries)) {
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;AAEvB;;;;GAIG;AACH,MAAM,OAAO,kBAAkB;IACA,KAAK;IAAlC,YAA6B,KAAY;qBAAZ,KAAK;IAAU,CAAC;IAErC,GAAG,CAAC,OAAe;QACzB,OAAO,mBAAmB,OAAO,EAAE,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAe,EAAE,MAAsB;QAChD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC9B,MAAM,IAAI,CAAC,KAAK;aACb,KAAK,EAAE;aACP,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;aAChD,MAAM,CAAC,GAAG,EAAE,WAAW,CAAC;aACxB,IAAI,EAAE,CAAC;IACZ,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,OAAe,EAAE,MAAc;QACzC,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAe;QACxB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC5D,MAAM,OAAO,GAAqB,EAAE,CAAC;QACrC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3C,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAe,CAAmB,CAAC,CAAC;YAC9D,CAAC;YAAC,MAAM,CAAC;gBACP,uCAAuC;YACzC,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,OAAO,eAAe;IACG,KAAK;IAAlC,YAA6B,KAAoB;qBAApB,KAAK;IAAkB,CAAC;IAErD,KAAK,CAAC,IAAI,CAAC,OAAe,EAAE,MAAsB;QAChD,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,OAAe,EAAE,MAAc;QACzC,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAe;QACxB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC;CACF"}
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.0",
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.5.2",
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.18",
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.4"
141
+ "vitest": "5.0.5"
141
142
  },
142
143
  "engines": {
143
144
  "node": ">=24"