matrix-nodejs-homeserver 0.3.3 → 0.3.4
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/README.md +46 -35
- package/dist/api/longpoll.js +61 -0
- package/dist/api/slidingsync.js +28 -30
- package/dist/api/sync.js +180 -119
- package/dist/core/admin.js +1 -0
- package/dist/core/changes.d.ts +79 -0
- package/dist/core/changes.js +332 -0
- package/dist/core/cluster.d.ts +19 -3
- package/dist/core/cluster.js +44 -4
- package/dist/core/keys.js +4 -2
- package/dist/core/notifier.d.ts +49 -3
- package/dist/core/notifier.js +123 -26
- package/dist/core/push.d.ts +12 -11
- package/dist/core/push.js +6 -4
- package/dist/core/rooms.js +7 -1
- package/dist/core/semaphore.js +1 -1
- package/dist/homeserver.d.ts +7 -1
- package/dist/homeserver.js +16 -7
- package/dist/storage/sql.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/k18k/matrix-nodejs-homeserver/actions/workflows/loadtest.yml)
|
|
5
5
|
[](https://github.com/k18k/matrix-nodejs-homeserver/actions/workflows/loadtest.yml)
|
|
6
6
|
[](https://github.com/k18k/matrix-nodejs-homeserver/actions/workflows/loadtest.yml)
|
|
7
|
-
[](https://github.com/k18k/matrix-nodejs-homeserver/actions/workflows/loadtest.yml)
|
|
8
8
|
|
|
9
9
|
A [Matrix](https://matrix.org) homeserver as an importable Node.js library. It isn't
|
|
10
10
|
tied to any HTTP framework, database, auth system or deployment shape. You can use
|
|
@@ -516,42 +516,53 @@ BASE_URL=http://localhost:8008 ONLINE_USERS=1000 MESSAGES_PER_SECOND=50 ROOMS=25
|
|
|
516
516
|
|
|
517
517
|
<!-- loadtest-results:start -->
|
|
518
518
|
The badges at the top come from the [Load test workflow](https://github.com/k18k/matrix-nodejs-homeserver/actions/workflows/loadtest.yml) on main: the same
|
|
519
|
-
scenario on one GitHub runner (4 CPUs), with k6 on the same machine
|
|
519
|
+
scenario on one GitHub runner (4 CPUs), with k6 on the same machine, and clusters of two nodes
|
|
520
|
+
(five nodes would compete with Postgres and k6 for four CPUs). Each run's job summary has
|
|
520
521
|
the full table.
|
|
521
522
|
|
|
522
|
-
Latest results
|
|
523
|
-
30 s
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
523
|
+
Latest results, on one developer machine (WSL2, Docker Desktop): 1000 users online in 25 rooms
|
|
524
|
+
(40 per room), 30 s ramp-up, then 30 s of messages. Times are p50 / p95; "sent" is the share of
|
|
525
|
+
the messages the scenario asks for that the server accepted (a server that falls behind takes
|
|
526
|
+
fewer, and then looks faster than it is).
|
|
527
|
+
|
|
528
|
+
At 10 messages/s (every user receiving one message every 2.5 s):
|
|
529
|
+
|
|
530
|
+
| Setup | send | delivery | sent | failed |
|
|
531
|
+
|---|---|---|---|---|
|
|
532
|
+
| SQLite, 1 process | 5 ms / 15 ms | 14 ms / 0.18 s | 100% | 0% |
|
|
533
|
+
| Postgres, no cluster | 9 ms / 30 ms | 29 ms / 0.60 s | 100% | 0% |
|
|
534
|
+
|
|
535
|
+
At 50 messages/s (every user receiving two messages a second, 59,000 deliveries in 30 s):
|
|
536
|
+
|
|
537
|
+
| Setup | req/s | send | /sync | delivery | sent | failed |
|
|
538
|
+
|---|---|---|---|---|---|---|
|
|
539
|
+
| SQLite, 1 process | 722 | 11 ms / 25 ms | 0.77 s / 1.3 s | 1.2 s / 1.6 s | 100% | 0% |
|
|
540
|
+
| Postgres, no cluster | 687 | 35 ms / 49 ms | 0.79 s / 1.2 s | 1.2 s / 1.6 s | 100% | 0% |
|
|
541
|
+
| Postgres, cluster, 1 node | 766 | 72 ms / 98 ms | 1.1 s / 1.5 s | 1.7 s / 2.2 s | 100% | 0% |
|
|
542
|
+
| Postgres, cluster, 5 nodes | 910 | 146 ms / 233 ms | 0.42 s / 1.6 s | 0.37 s / 1.5 s | 100% | 0% |
|
|
543
|
+
|
|
544
|
+
At 50 messages/s, delivery time is how long a process takes to rebuild the `/sync` of every
|
|
545
|
+
user in turn, since every user has something new in nearly every rebuild.
|
|
546
|
+
|
|
547
|
+
What the load tests found and fixed:
|
|
548
|
+
- presence written on every `/sync` (#26), one commit per push notification (#27), a missing
|
|
549
|
+
index (#28), Postgres cluster locks that could deadlock the pool (#29), stream writes
|
|
550
|
+
serialized per instance (#30), `/sync` reading every change on the server (#31) and every
|
|
551
|
+
joined room (#33), lock handover by polling (#32), unbatched `/sync` reads (#34), syncs
|
|
552
|
+
crowding out sends (#35);
|
|
553
|
+
- in 0.3.3 (#38–#47): rooms built one after another (#38), a statement per push notification
|
|
554
|
+
(#39), Postgres re-planning every statement (#40), push rules copied per member (#41),
|
|
555
|
+
needless to-device deletes (#42), concurrent reads as separate statements (#43), presence and
|
|
556
|
+
device lists looked up per user (#44), costly batched reads (#45), an fsync per SQLite commit
|
|
557
|
+
(#46), a blocked event loop (#47);
|
|
558
|
+
- since 0.3.3: waiting `/sync` requests rebuilt whenever anything on the server changed, and
|
|
559
|
+
about two thirds of all rebuilds found nothing new (#50); a single instance now keeps track of
|
|
560
|
+
where data last changed, so `/sync` skips reads that would find nothing, like Synapse's stream
|
|
561
|
+
change caches (#51); queued syncs held the event loop, and yielding per statement made sends
|
|
562
|
+
slow (#52).
|
|
563
|
+
|
|
564
|
+
The load test itself was fixed too: its senders registered during the measurement, so a slow
|
|
565
|
+
server was sent fewer messages, and nginx could start after k6 (a red badge with "100% failed").
|
|
555
566
|
<!-- loadtest-results:end -->
|
|
556
567
|
|
|
557
568
|
## License
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/api/longpoll.ts
|
|
2
|
+
/**
|
|
3
|
+
* The long-poll loop of `/sync` and sliding sync: build a response up to the current stream
|
|
4
|
+
* position, and while it has nothing new, wait for a change for `userId` and build again.
|
|
5
|
+
*
|
|
6
|
+
* https://github.com/k18k/matrix-nodejs-homeserver/issues/50: after a build with nothing new, the
|
|
7
|
+
* loop used to build again whenever the server-wide position had moved, which under load is
|
|
8
|
+
* always, so waiting requests rebuilt back to back. Now it registers for the user's wake-ups
|
|
9
|
+
* before reading the position, so none is missed during a build, and otherwise sleeps. A wake-up
|
|
10
|
+
* says which position its change has; if the next build cannot see it yet (another write in
|
|
11
|
+
* flight holds the position back), the loop waits for the position to get there.
|
|
12
|
+
*
|
|
13
|
+
* `build(to)` returns whether the response is final (`done`); returns the last built value, the
|
|
14
|
+
* position it was built at, and whether the request was aborted while waiting.
|
|
15
|
+
*/
|
|
16
|
+
async function longPoll(hs, userId, timeoutMs, signal, build) {
|
|
17
|
+
const deadline = Date.now() + timeoutMs;
|
|
18
|
+
let pending = 0;
|
|
19
|
+
for (;;) {
|
|
20
|
+
const listener = hs.notifier.listen(userId);
|
|
21
|
+
pending = Math.max(pending, listener.recent);
|
|
22
|
+
let to;
|
|
23
|
+
let built;
|
|
24
|
+
try {
|
|
25
|
+
to = await hs.streams.position();
|
|
26
|
+
if (pending > to) to = await hs.streams.position(true);
|
|
27
|
+
built = await build(to);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
listener.cancel();
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
if (pending <= to) pending = 0;
|
|
33
|
+
const remaining = deadline - Date.now();
|
|
34
|
+
if (built.done || remaining <= 0 || signal?.aborted) {
|
|
35
|
+
listener.cancel();
|
|
36
|
+
return {
|
|
37
|
+
value: built.value,
|
|
38
|
+
to,
|
|
39
|
+
aborted: !!signal?.aborted
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (pending > to) {
|
|
43
|
+
listener.cancel();
|
|
44
|
+
if (await hs.streams.waitFor(pending, remaining, signal)) continue;
|
|
45
|
+
return {
|
|
46
|
+
value: built.value,
|
|
47
|
+
to,
|
|
48
|
+
aborted: !!signal?.aborted
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const woke = await listener.wait(remaining, signal);
|
|
52
|
+
if (woke === null) return {
|
|
53
|
+
value: built.value,
|
|
54
|
+
to,
|
|
55
|
+
aborted: !!signal?.aborted
|
|
56
|
+
};
|
|
57
|
+
pending = Math.max(pending, woke);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { longPoll };
|
package/dist/api/slidingsync.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getState } from "../core/authrules.js";
|
|
|
6
6
|
import { record, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
|
|
7
7
|
import { body, lenient } from "../http/input.js";
|
|
8
8
|
import { procedure } from "../http/procedure.js";
|
|
9
|
+
import { longPoll } from "./longpoll.js";
|
|
9
10
|
import { roomEphemeral, roomTimeline, toStripped } from "./sync.js";
|
|
10
11
|
//#region src/api/slidingsync.ts
|
|
11
12
|
const slidingSyncBody = body({
|
|
@@ -104,37 +105,34 @@ async function slidingSync(ctx) {
|
|
|
104
105
|
rooms: {}
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (since === null || !empty || remaining <= 0) {
|
|
113
|
-
await hs.db.upsert("sliding_sync_connections", {
|
|
114
|
-
user_id: s.userId,
|
|
115
|
-
device_id: s.deviceId,
|
|
116
|
-
conn_id: connId,
|
|
117
|
-
pos: to,
|
|
118
|
-
state: {
|
|
119
|
-
...next,
|
|
120
|
-
prevPos: since
|
|
121
|
-
},
|
|
122
|
-
updated_ts: Date.now()
|
|
123
|
-
});
|
|
124
|
-
return {
|
|
125
|
-
pos: String(to),
|
|
126
|
-
...response
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
if (await hs.streams.position() > to) continue;
|
|
130
|
-
await hs.notifier.wait(s.userId, remaining, ctx.request.signal);
|
|
131
|
-
if (ctx.request.signal?.aborted) return {
|
|
132
|
-
pos: String(since),
|
|
133
|
-
lists: {},
|
|
134
|
-
rooms: {},
|
|
135
|
-
extensions: {}
|
|
108
|
+
const { value, to, aborted } = await longPoll(hs, s.userId, timeout, ctx.request.signal, async (to) => {
|
|
109
|
+
const built = await buildResponse(hs, s, body, conn, since, to);
|
|
110
|
+
return {
|
|
111
|
+
value: built,
|
|
112
|
+
done: since === null || !built.empty
|
|
136
113
|
};
|
|
137
|
-
}
|
|
114
|
+
});
|
|
115
|
+
if (aborted && value.empty) return {
|
|
116
|
+
pos: String(since),
|
|
117
|
+
lists: {},
|
|
118
|
+
rooms: {},
|
|
119
|
+
extensions: {}
|
|
120
|
+
};
|
|
121
|
+
await hs.db.upsert("sliding_sync_connections", {
|
|
122
|
+
user_id: s.userId,
|
|
123
|
+
device_id: s.deviceId,
|
|
124
|
+
conn_id: connId,
|
|
125
|
+
pos: to,
|
|
126
|
+
state: {
|
|
127
|
+
...value.next,
|
|
128
|
+
prevPos: since
|
|
129
|
+
},
|
|
130
|
+
updated_ts: Date.now()
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
pos: String(to),
|
|
134
|
+
...value.response
|
|
135
|
+
};
|
|
138
136
|
}
|
|
139
137
|
async function buildResponse(hs, s, body, conn, since, to) {
|
|
140
138
|
const userId = s.userId;
|
package/dist/api/sync.js
CHANGED
|
@@ -8,7 +8,9 @@ import { deviceListChangesQuery } from "../core/keys.js";
|
|
|
8
8
|
import { body } from "../http/input.js";
|
|
9
9
|
import { procedure } from "../http/procedure.js";
|
|
10
10
|
import { readBatch } from "../storage/batch.js";
|
|
11
|
+
import { trackedReads } from "../core/changes.js";
|
|
11
12
|
import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
|
|
13
|
+
import { longPoll } from "./longpoll.js";
|
|
12
14
|
import { Semaphore } from "../core/semaphore.js";
|
|
13
15
|
import { fullProfile } from "../core/profile.js";
|
|
14
16
|
//#region src/api/sync.ts
|
|
@@ -330,6 +332,8 @@ async function changesSince(hs, p, memberships, since, accountRows) {
|
|
|
330
332
|
operator: "lte",
|
|
331
333
|
value: p.to
|
|
332
334
|
}];
|
|
335
|
+
const logs = hs.changes?.logs;
|
|
336
|
+
const mayHaveChanged = (log, rooms) => logs ? logs[log].filterChanged(rooms, since) : rooms;
|
|
333
337
|
const chunks = (list) => Array.from({ length: Math.ceil(list.length / 500) }, (_, i) => list.slice(i * 500, i * 500 + 500));
|
|
334
338
|
const inRooms = (chunk) => ({
|
|
335
339
|
field: "room_id",
|
|
@@ -337,31 +341,34 @@ async function changesSince(hs, p, memberships, since, accountRows) {
|
|
|
337
341
|
value: chunk
|
|
338
342
|
});
|
|
339
343
|
const queries = [];
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
344
|
+
const kinds = [];
|
|
345
|
+
const add = (kind, query) => {
|
|
346
|
+
kinds.push(kind);
|
|
347
|
+
queries.push(query);
|
|
348
|
+
};
|
|
349
|
+
for (const chunk of chunks(mayHaveChanged("roomEvents", joined))) add("events", {
|
|
350
|
+
model: "events",
|
|
351
|
+
opts: {
|
|
352
|
+
where: [inRooms(chunk), ...range],
|
|
353
|
+
fields: ["room_id"]
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
for (const chunk of chunks(mayHaveChanged("roomReceipts", joined))) add("receipts", {
|
|
357
|
+
model: "receipts",
|
|
358
|
+
opts: { where: [inRooms(chunk), ...range] }
|
|
359
|
+
});
|
|
360
|
+
for (const chunk of chunks(mayHaveChanged("roomPartialState", joined))) add("completed", {
|
|
361
|
+
model: "partial_state_rooms",
|
|
362
|
+
opts: {
|
|
363
|
+
where: [inRooms(chunk), {
|
|
364
|
+
field: "completed_stream",
|
|
365
|
+
operator: "gt",
|
|
366
|
+
value: since
|
|
367
|
+
}],
|
|
368
|
+
fields: ["room_id"]
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
for (const chunk of chunks(mayHaveChanged("roomMembers", anyMembership))) add("members", {
|
|
365
372
|
model: "memberships",
|
|
366
373
|
opts: { where: [inRooms(chunk), {
|
|
367
374
|
field: "stream",
|
|
@@ -369,20 +376,18 @@ async function changesSince(hs, p, memberships, since, accountRows) {
|
|
|
369
376
|
value: since
|
|
370
377
|
}] }
|
|
371
378
|
});
|
|
372
|
-
const results = await readBatch(hs.db, queries);
|
|
379
|
+
const results = queries.length ? await readBatch(hs.db, queries) : [];
|
|
373
380
|
const touched = /* @__PURE__ */ new Set();
|
|
374
381
|
const receipts = /* @__PURE__ */ new Map();
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
for (const r of
|
|
379
|
-
|
|
380
|
-
touched.add(r.room_id);
|
|
381
|
-
receipts.set(r.room_id, [...receipts.get(r.room_id) ?? [], r]);
|
|
382
|
+
const memberChanges = [];
|
|
383
|
+
results.forEach((rows, i) => {
|
|
384
|
+
const kind = kinds[i];
|
|
385
|
+
for (const r of rows) {
|
|
386
|
+
if (kind === "members") memberChanges.push(r);
|
|
387
|
+
else touched.add(r.room_id);
|
|
388
|
+
if (kind === "receipts") receipts.set(r.room_id, [...receipts.get(r.room_id) ?? [], r]);
|
|
382
389
|
}
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
const memberChanges = results.slice(joinedChunks * 3).flat();
|
|
390
|
+
});
|
|
386
391
|
const accountData = /* @__PURE__ */ new Map();
|
|
387
392
|
for (const r of accountRows) {
|
|
388
393
|
if (r.room_id) touched.add(r.room_id);
|
|
@@ -428,76 +433,115 @@ async function buildSync(hs, p) {
|
|
|
428
433
|
operator: "lte",
|
|
429
434
|
value: p.since.toDevice
|
|
430
435
|
}]) : Promise.resolve(0);
|
|
431
|
-
const
|
|
436
|
+
const device = `${userId}|${p.session.deviceId}`;
|
|
437
|
+
const [memberships, ignoredRows, toDeviceRows, oneTimeKeyRows, fallbackRows, accountRows = []] = await trackedReads(hs.db, hs.changes, [
|
|
432
438
|
{
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
value: userId
|
|
437
|
-
}] }
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
model: "account_data",
|
|
441
|
-
opts: { where: [
|
|
442
|
-
{
|
|
439
|
+
query: {
|
|
440
|
+
model: "memberships",
|
|
441
|
+
opts: { where: [{
|
|
443
442
|
field: "user_id",
|
|
444
443
|
value: userId
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
field: "type",
|
|
452
|
-
value: M.IGNORED_USER_LIST
|
|
453
|
-
}
|
|
454
|
-
] }
|
|
444
|
+
}] }
|
|
445
|
+
},
|
|
446
|
+
cached: {
|
|
447
|
+
name: "userMemberships",
|
|
448
|
+
key: userId
|
|
449
|
+
}
|
|
455
450
|
},
|
|
456
451
|
{
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
where: [
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
452
|
+
query: {
|
|
453
|
+
model: "account_data",
|
|
454
|
+
opts: { where: [
|
|
455
|
+
{
|
|
456
|
+
field: "user_id",
|
|
457
|
+
value: userId
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
field: "room_id",
|
|
461
|
+
value: ""
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
field: "type",
|
|
465
|
+
value: M.IGNORED_USER_LIST
|
|
466
|
+
}
|
|
467
|
+
] }
|
|
468
|
+
},
|
|
469
|
+
cached: {
|
|
470
|
+
name: "ignoredUsers",
|
|
471
|
+
key: userId
|
|
469
472
|
}
|
|
470
473
|
},
|
|
471
474
|
{
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
475
|
+
query: {
|
|
476
|
+
model: "to_device",
|
|
477
|
+
opts: {
|
|
478
|
+
where: [...deviceWhere, {
|
|
479
|
+
field: "stream",
|
|
480
|
+
operator: "gt",
|
|
481
|
+
value: p.since?.toDevice ?? 0
|
|
482
|
+
}],
|
|
483
|
+
sortBy: {
|
|
484
|
+
field: "stream",
|
|
485
|
+
direction: "asc"
|
|
486
|
+
},
|
|
487
|
+
limit: 100
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
unlessChanged: {
|
|
491
|
+
log: "toDevice",
|
|
492
|
+
key: device,
|
|
493
|
+
after: p.since?.toDevice ?? 0
|
|
479
494
|
}
|
|
480
495
|
},
|
|
481
496
|
{
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
497
|
+
query: {
|
|
498
|
+
model: "one_time_keys",
|
|
499
|
+
opts: {
|
|
500
|
+
where: [...deviceWhere, {
|
|
501
|
+
field: "fallback",
|
|
502
|
+
value: false
|
|
503
|
+
}],
|
|
504
|
+
fields: ["algorithm"]
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
cached: {
|
|
508
|
+
name: "oneTimeKeys",
|
|
509
|
+
key: device
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
query: {
|
|
514
|
+
model: "fallback_keys",
|
|
515
|
+
opts: {
|
|
516
|
+
where: [...deviceWhere, {
|
|
517
|
+
field: "used",
|
|
518
|
+
value: false
|
|
519
|
+
}],
|
|
520
|
+
fields: ["algorithm"]
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
cached: {
|
|
524
|
+
name: "fallbackKeys",
|
|
525
|
+
key: device
|
|
489
526
|
}
|
|
490
527
|
},
|
|
491
528
|
...incremental ? [{
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
529
|
+
query: {
|
|
530
|
+
model: "account_data",
|
|
531
|
+
opts: { where: [{
|
|
532
|
+
field: "user_id",
|
|
533
|
+
value: userId
|
|
534
|
+
}, {
|
|
535
|
+
field: "stream",
|
|
536
|
+
operator: "gt",
|
|
537
|
+
value: since
|
|
538
|
+
}] }
|
|
539
|
+
},
|
|
540
|
+
unlessChanged: {
|
|
541
|
+
log: "accountData",
|
|
542
|
+
key: userId,
|
|
543
|
+
after: since
|
|
544
|
+
}
|
|
501
545
|
}] : []
|
|
502
546
|
]);
|
|
503
547
|
p = {
|
|
@@ -644,17 +688,31 @@ async function buildSync(hs, p) {
|
|
|
644
688
|
value: []
|
|
645
689
|
}] }
|
|
646
690
|
};
|
|
647
|
-
let [presenceRows = [], newlySharedPresence = [], deviceRows] = await
|
|
648
|
-
presenceQuery
|
|
649
|
-
|
|
691
|
+
let [presenceRows = [], newlySharedPresence = [], deviceRows] = await trackedReads(hs.db, hs.changes, [
|
|
692
|
+
since === null ? { query: presenceQuery } : {
|
|
693
|
+
query: presenceQuery,
|
|
694
|
+
unlessChanged: {
|
|
695
|
+
log: "presence",
|
|
696
|
+
key: "*",
|
|
697
|
+
after: since
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
{ query: {
|
|
650
701
|
model: "presence",
|
|
651
702
|
opts: { where: [{
|
|
652
703
|
field: "user_id",
|
|
653
704
|
operator: "in",
|
|
654
705
|
value: [...newlyShared]
|
|
655
706
|
}] }
|
|
656
|
-
},
|
|
657
|
-
...since !== null ? [
|
|
707
|
+
} },
|
|
708
|
+
...since !== null ? [{
|
|
709
|
+
query: recent("device_list_changes"),
|
|
710
|
+
unlessChanged: {
|
|
711
|
+
log: "deviceLists",
|
|
712
|
+
key: "*",
|
|
713
|
+
after: since
|
|
714
|
+
}
|
|
715
|
+
}] : []
|
|
658
716
|
]);
|
|
659
717
|
if (since !== null) {
|
|
660
718
|
if (presenceRows.length > RECENT_CHANGES) presenceRows = await hs.db.findMany("presence", { where: [{
|
|
@@ -745,14 +803,12 @@ async function sync(ctx) {
|
|
|
745
803
|
const timeout = Math.min(Math.max(Number.parseInt(ctx.query.get("timeout") ?? "0", 10) || 0, 0), hs.config.maxSyncTimeoutMs);
|
|
746
804
|
const setPresence = ctx.query.get("set_presence") ?? "online";
|
|
747
805
|
if (setPresence !== "offline") await touchPresence(hs, session.userId, setPresence);
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const useStateAfter = stable || unstable;
|
|
755
|
-
const stateAfterKey = stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER;
|
|
806
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
807
|
+
const stable = ctx.query.get("use_state_after") === "true";
|
|
808
|
+
const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
|
|
809
|
+
const useStateAfter = stable || unstable;
|
|
810
|
+
const stateAfterKey = stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER;
|
|
811
|
+
const { value } = await longPoll(hs, session.userId, timeout, ctx.request.signal, async (to) => {
|
|
756
812
|
const { body, empty } = await syncSlots(hs).run(() => buildSync(hs, {
|
|
757
813
|
session,
|
|
758
814
|
ignored,
|
|
@@ -763,20 +819,21 @@ async function sync(ctx) {
|
|
|
763
819
|
useStateAfter,
|
|
764
820
|
stateAfterKey
|
|
765
821
|
}));
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
822
|
+
return {
|
|
823
|
+
value: body,
|
|
824
|
+
done: !since || !empty || fullState
|
|
825
|
+
};
|
|
826
|
+
});
|
|
827
|
+
return value;
|
|
772
828
|
}
|
|
773
829
|
const LAST_ACTIVE_GRANULARITY_MS = 6e4;
|
|
774
830
|
async function touchPresence(hs, userId, presence) {
|
|
775
831
|
const state = presence === "unavailable" ? "unavailable" : "online";
|
|
776
|
-
const
|
|
832
|
+
const read = () => hs.db.findOne("presence", [{
|
|
777
833
|
field: "user_id",
|
|
778
834
|
value: userId
|
|
779
835
|
}]);
|
|
836
|
+
const row = hs.changes ? await hs.changes.values.presenceRow.get(userId, read) : await read();
|
|
780
837
|
if (row && row.presence === state) {
|
|
781
838
|
if (Date.now() - row.last_active_ts >= LAST_ACTIVE_GRANULARITY_MS) await hs.db.update("presence", [{
|
|
782
839
|
field: "user_id",
|
|
@@ -784,13 +841,17 @@ async function touchPresence(hs, userId, presence) {
|
|
|
784
841
|
}], { last_active_ts: Date.now() });
|
|
785
842
|
return;
|
|
786
843
|
}
|
|
787
|
-
await hs.streams.write((stream) =>
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
844
|
+
const stream = await hs.streams.write(async (stream) => {
|
|
845
|
+
await hs.db.upsert("presence", {
|
|
846
|
+
user_id: userId,
|
|
847
|
+
presence: state,
|
|
848
|
+
status_msg: row?.status_msg ?? null,
|
|
849
|
+
last_active_ts: Date.now(),
|
|
850
|
+
stream
|
|
851
|
+
});
|
|
852
|
+
return stream;
|
|
853
|
+
});
|
|
854
|
+
hs.notifier.notify([userId, ...await hs.rooms.usersSharingRooms(userId)], stream);
|
|
794
855
|
await hs.federation.onPresence(userId, state, row?.status_msg ?? null).catch(() => {});
|
|
795
856
|
}
|
|
796
857
|
function registerSyncRoutes(r) {
|