matrix-nodejs-homeserver 0.3.2 → 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/dist/api/sync.js CHANGED
@@ -2,26 +2,29 @@ import { M, UNSTABLE } from "../core/m.js";
2
2
  import { eventIdOf, splitStateKey, stateKey, toClientEvent } from "../core/events.js";
3
3
  import { errors } from "../core/errors.js";
4
4
  import { getState } from "../core/authrules.js";
5
- import { loadRules } from "../core/pushrules.js";
5
+ import { readOnlyRules } from "../core/pushrules.js";
6
6
  import { inviteDecision } from "../core/invitefilter.js";
7
7
  import { deviceListChangesQuery } from "../core/keys.js";
8
8
  import { body } from "../http/input.js";
9
9
  import { procedure } from "../http/procedure.js";
10
- import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
11
10
  import { readBatch } from "../storage/batch.js";
11
+ import { trackedReads } from "../core/changes.js";
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
15
17
  function parseSyncToken(raw) {
16
18
  if (!raw) return null;
17
- const m = /^s(\d+)(?:_(\d+))?$/.exec(raw);
19
+ const m = /^s(\d+)(?:_(\d+))?(_a)?$/.exec(raw);
18
20
  if (!m) throw errors.invalidParam("Invalid since token");
19
21
  return {
20
22
  pos: Number(m[1]),
21
- toDevice: Number(m[2] ?? m[1])
23
+ toDevice: Number(m[2] ?? m[1]),
24
+ ack: !!m[3]
22
25
  };
23
26
  }
24
- const formatToken = (t) => `s${t.pos}_${t.toDevice}`;
27
+ const formatToken = (t) => `s${t.pos}_${t.toDevice}${t.ack ? "_a" : ""}`;
25
28
  async function loadFilter(hs, userId, raw) {
26
29
  if (!raw) return {};
27
30
  if (raw.trim().startsWith("{")) {
@@ -88,6 +91,8 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
88
91
  operator: "gt",
89
92
  value: since
90
93
  });
94
+ const sinceIdsRead = since !== null ? snapshotAt(hs, roomId, since).then((id) => hs.rooms.snapshotIds(id)) : Promise.resolve({});
95
+ const scanned = [];
91
96
  const events = [];
92
97
  let limited = false;
93
98
  let cursor = upTo;
@@ -106,6 +111,7 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
106
111
  limit: batchSize
107
112
  });
108
113
  if (rows.length === 0) break;
114
+ scanned.push(...rows);
109
115
  const stored = rows.map((r) => hs.rooms.rowToStored(r));
110
116
  const visible = await hs.rooms.filterVisible(p.session.userId, roomId, stored);
111
117
  const ok = new Map(visible.map((v) => [v.pdu.event_id, v]));
@@ -143,25 +149,50 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
143
149
  events.splice(0, lastGap);
144
150
  limited = true;
145
151
  }
146
- if (!limited && events.length) limited = (await hs.db.findMany("events", {
147
- where: [...where.filter((c) => c.operator !== "lte"), {
148
- field: "stream",
149
- operator: "lt",
150
- value: events[0].stream
151
- }],
152
- limit: 1,
153
- fields: ["stream"]
154
- })).length > 0;
155
152
  const startPos = events.length ? Math.min(...events.map((e) => e.stream)) - 1 : upTo;
156
- const startState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, startPos));
153
+ const newestAtOrBefore = async (pos) => {
154
+ const row = scanned.find((r) => r.stream <= pos);
155
+ if (row) return row;
156
+ return (await hs.db.findMany("events", {
157
+ where: [{
158
+ field: "room_id",
159
+ value: roomId
160
+ }, {
161
+ field: "stream",
162
+ operator: "lte",
163
+ value: pos
164
+ }],
165
+ sortBy: {
166
+ field: "stream",
167
+ direction: "desc"
168
+ },
169
+ limit: 1,
170
+ fields: ["stream", "state_snapshot"]
171
+ }))[0] ?? null;
172
+ };
173
+ const [atStart, atEnd] = await Promise.all([newestAtOrBefore(startPos), startPos === upTo ? null : newestAtOrBefore(upTo)]);
174
+ const startSnapshot = atStart?.state_snapshot ?? 0;
175
+ const endSnapshot = startPos === upTo ? startSnapshot : atEnd?.state_snapshot ?? 0;
176
+ if (!limited && events.length) {
177
+ if (events[0].stream === startPos + 1) limited = atStart !== null && (since === null || atStart.stream > since);
178
+ else limited = (await hs.db.findMany("events", {
179
+ where: [...where.filter((c) => c.operator !== "lte"), {
180
+ field: "stream",
181
+ operator: "lt",
182
+ value: events[0].stream
183
+ }],
184
+ limit: 1,
185
+ fields: ["stream"]
186
+ })).length > 0;
187
+ }
188
+ const [startState, endState] = await Promise.all([hs.rooms.stateAtSnapshot(startSnapshot), hs.rooms.stateAtSnapshot(endSnapshot)]);
157
189
  const timelineIds = new Set(events.map((e) => e.pdu.event_id));
158
- const endState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, upTo));
159
190
  for (const [key, ev] of endState) {
160
191
  if (timelineIds.has(ev.event_id) || startState.get(key)?.event_id === ev.event_id) continue;
161
192
  if (events.some((e) => e.pdu.state_key !== void 0 && stateKey(e.pdu.type, e.pdu.state_key) === key)) continue;
162
193
  startState.set(key, ev);
163
194
  }
164
- const sinceIds = since !== null ? await hs.rooms.snapshotIds(await snapshotAt(hs, roomId, since)) : {};
195
+ const sinceIds = await sinceIdsRead;
165
196
  const sf = p.filter.room?.state;
166
197
  const lazy = !!sf?.lazy_load_members;
167
198
  const senders = new Set(events.map((e) => e.pdu.sender));
@@ -181,15 +212,12 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
181
212
  if (member && matchesFilter(sf, member)) state.push(member);
182
213
  }
183
214
  const stateAfter = [];
184
- if (p.useStateAfter) {
185
- const endState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, upTo));
186
- for (const [key, ev] of endState) {
187
- if (sinceIds[key] === ev.event_id) continue;
188
- const [type, sk] = splitStateKey(key);
189
- if (!matchesFilter(sf, ev)) continue;
190
- if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
191
- stateAfter.push(ev);
192
- }
215
+ if (p.useStateAfter) for (const [key, ev] of endState) {
216
+ if (sinceIds[key] === ev.event_id) continue;
217
+ const [type, sk] = splitStateKey(key);
218
+ if (!matchesFilter(sf, ev)) continue;
219
+ if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
220
+ stateAfter.push(ev);
193
221
  }
194
222
  return {
195
223
  stateAfter,
@@ -304,6 +332,8 @@ async function changesSince(hs, p, memberships, since, accountRows) {
304
332
  operator: "lte",
305
333
  value: p.to
306
334
  }];
335
+ const logs = hs.changes?.logs;
336
+ const mayHaveChanged = (log, rooms) => logs ? logs[log].filterChanged(rooms, since) : rooms;
307
337
  const chunks = (list) => Array.from({ length: Math.ceil(list.length / 500) }, (_, i) => list.slice(i * 500, i * 500 + 500));
308
338
  const inRooms = (chunk) => ({
309
339
  field: "room_id",
@@ -311,31 +341,34 @@ async function changesSince(hs, p, memberships, since, accountRows) {
311
341
  value: chunk
312
342
  });
313
343
  const queries = [];
314
- for (const chunk of chunks(joined)) {
315
- queries.push({
316
- model: "events",
317
- opts: {
318
- where: [inRooms(chunk), ...range],
319
- fields: ["room_id"]
320
- }
321
- });
322
- queries.push({
323
- model: "receipts",
324
- opts: { where: [inRooms(chunk), ...range] }
325
- });
326
- queries.push({
327
- model: "partial_state_rooms",
328
- opts: {
329
- where: [inRooms(chunk), {
330
- field: "completed_stream",
331
- operator: "gt",
332
- value: since
333
- }],
334
- fields: ["room_id"]
335
- }
336
- });
337
- }
338
- for (const chunk of chunks(anyMembership)) queries.push({
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", {
339
372
  model: "memberships",
340
373
  opts: { where: [inRooms(chunk), {
341
374
  field: "stream",
@@ -343,20 +376,18 @@ async function changesSince(hs, p, memberships, since, accountRows) {
343
376
  value: since
344
377
  }] }
345
378
  });
346
- const results = await readBatch(hs.db, queries);
379
+ const results = queries.length ? await readBatch(hs.db, queries) : [];
347
380
  const touched = /* @__PURE__ */ new Set();
348
381
  const receipts = /* @__PURE__ */ new Map();
349
- const joinedChunks = Math.ceil(joined.length / 500);
350
- for (let c = 0; c < joinedChunks; c++) {
351
- const [events, receiptRows, completed] = results.slice(c * 3, c * 3 + 3);
352
- for (const r of events) touched.add(r.room_id);
353
- for (const r of receiptRows) {
354
- touched.add(r.room_id);
355
- 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]);
356
389
  }
357
- for (const r of completed) touched.add(r.room_id);
358
- }
359
- const memberChanges = results.slice(joinedChunks * 3).flat();
390
+ });
360
391
  const accountData = /* @__PURE__ */ new Map();
361
392
  for (const r of accountRows) {
362
393
  if (r.room_id) touched.add(r.room_id);
@@ -369,6 +400,23 @@ async function changesSince(hs, p, memberships, since, accountRows) {
369
400
  memberChanges
370
401
  };
371
402
  }
403
+ /** Presence or device-list changes after a token read without a user filter, at most (#44). */
404
+ const RECENT_CHANGES = 500;
405
+ /** How many of a sync's joined rooms are built at once (#38). */
406
+ const JOINED_ROOMS_AT_ONCE = 8;
407
+ /** `fn` over `items` with at most `limit` running at once; results in input order. */
408
+ async function mapConcurrently(items, limit, fn) {
409
+ const out = [];
410
+ let next = 0;
411
+ const worker = async () => {
412
+ while (next < items.length) {
413
+ const i = next++;
414
+ out[i] = await fn(items[i]);
415
+ }
416
+ };
417
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
418
+ return out;
419
+ }
372
420
  async function buildSync(hs, p) {
373
421
  const userId = p.session.userId;
374
422
  const since = p.since?.pos ?? null;
@@ -380,81 +428,120 @@ async function buildSync(hs, p) {
380
428
  field: "device_id",
381
429
  value: p.session.deviceId
382
430
  }];
383
- const acknowledged = p.since ? hs.db.delete("to_device", [...deviceWhere, {
431
+ const acknowledged = p.since?.ack ? hs.db.delete("to_device", [...deviceWhere, {
384
432
  field: "stream",
385
433
  operator: "lte",
386
434
  value: p.since.toDevice
387
435
  }]) : Promise.resolve(0);
388
- const [memberships, ignoredRows, toDeviceRows, oneTimeKeyRows, fallbackRows, accountRows = []] = await readBatch(hs.db, [
436
+ const device = `${userId}|${p.session.deviceId}`;
437
+ const [memberships, ignoredRows, toDeviceRows, oneTimeKeyRows, fallbackRows, accountRows = []] = await trackedReads(hs.db, hs.changes, [
389
438
  {
390
- model: "memberships",
391
- opts: { where: [{
392
- field: "user_id",
393
- value: userId
394
- }] }
395
- },
396
- {
397
- model: "account_data",
398
- opts: { where: [
399
- {
439
+ query: {
440
+ model: "memberships",
441
+ opts: { where: [{
400
442
  field: "user_id",
401
443
  value: userId
402
- },
403
- {
404
- field: "room_id",
405
- value: ""
406
- },
407
- {
408
- field: "type",
409
- value: M.IGNORED_USER_LIST
410
- }
411
- ] }
444
+ }] }
445
+ },
446
+ cached: {
447
+ name: "userMemberships",
448
+ key: userId
449
+ }
412
450
  },
413
451
  {
414
- model: "to_device",
415
- opts: {
416
- where: [...deviceWhere, {
417
- field: "stream",
418
- operator: "gt",
419
- value: p.since?.toDevice ?? 0
420
- }],
421
- sortBy: {
422
- field: "stream",
423
- direction: "asc"
424
- },
425
- limit: 100
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
426
472
  }
427
473
  },
428
474
  {
429
- model: "one_time_keys",
430
- opts: {
431
- where: [...deviceWhere, {
432
- field: "fallback",
433
- value: false
434
- }],
435
- fields: ["algorithm"]
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
494
+ }
495
+ },
496
+ {
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
436
510
  }
437
511
  },
438
512
  {
439
- model: "fallback_keys",
440
- opts: {
441
- where: [...deviceWhere, {
442
- field: "used",
443
- value: false
444
- }],
445
- fields: ["algorithm"]
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
446
526
  }
447
527
  },
448
528
  ...incremental ? [{
449
- model: "account_data",
450
- opts: { where: [{
451
- field: "user_id",
452
- value: userId
453
- }, {
454
- field: "stream",
455
- operator: "gt",
456
- value: since
457
- }] }
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
+ }
458
545
  }] : []
459
546
  ]);
460
547
  p = {
@@ -469,39 +556,14 @@ async function buildSync(hs, p) {
469
556
  const leave = {};
470
557
  const knock = {};
471
558
  let empty = true;
559
+ const joinedBuilds = [];
472
560
  for (const m of memberships) {
473
561
  if (m.stream > p.to || !roomAllowed(p.filter, m.room_id)) continue;
474
562
  const roomId = m.room_id;
475
563
  const changed = since === null || m.stream > since;
476
564
  if (m.membership === "join") {
477
565
  if (changes && !changed && !changes.touched.has(roomId) && hs.typing.typingSince(roomId, since) === null) continue;
478
- if (!p.filter.room?.state?.lazy_load_members && await hs.partialState.isPartial(roomId)) continue;
479
- const completedAt = await hs.partialState.completedAt(roomId);
480
- const initial = since === null || p.fullState || m.stream > since || completedAt !== null && completedAt > since && completedAt <= p.to;
481
- const { timeline, state, stateAfter, timelineEvents } = await roomTimeline(hs, p, roomId, initial ? null : since, p.to);
482
- const ephemeral = await roomEphemeral(hs, p, roomId, changes ? changes.receipts.get(roomId) ?? [] : void 0);
483
- const roomAccountData = await accountData(hs, p, roomId, changes ? changes.accountData.get(roomId) ?? [] : void 0);
484
- if (!initial && timelineEvents.length === 0 && state.length === 0 && stateAfter.length === 0 && ephemeral.length === 0 && roomAccountData.length === 0) continue;
485
- join[roomId] = {
486
- timeline,
487
- ...p.useStateAfter ? { [p.stateAfterKey ?? "state_after"]: { events: stateAfter.map((e) => toClientEvent(e, { includeRoomId: false })) } } : { state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) } },
488
- ephemeral: { events: ephemeral },
489
- account_data: { events: roomAccountData },
490
- summary: await roomSummary(hs, roomId, userId)
491
- };
492
- const counts = await hs.push.unreadCounts(userId, roomId);
493
- if (p.filter.room?.timeline?.unread_thread_notifications) {
494
- join[roomId].unread_notifications = counts.room;
495
- if (Object.keys(counts.threads).length) join[roomId].unread_thread_notifications = counts.threads;
496
- } else {
497
- const total = { ...counts.room };
498
- for (const t of Object.values(counts.threads)) {
499
- total.notification_count += t.notification_count;
500
- total.highlight_count += t.highlight_count;
501
- }
502
- join[roomId].unread_notifications = total;
503
- }
504
- empty = false;
566
+ joinedBuilds.push(m);
505
567
  } else if (m.membership === "invite") {
506
568
  if (!changed && !p.fullState) continue;
507
569
  const pdu = await hs.rooms.getPdu(m.event_id);
@@ -533,10 +595,49 @@ async function buildSync(hs, p) {
533
595
  empty = false;
534
596
  }
535
597
  }
598
+ const joinedRoom = async (m) => {
599
+ const roomId = m.room_id;
600
+ if (!p.filter.room?.state?.lazy_load_members && await hs.partialState.isPartial(roomId)) return null;
601
+ const completedAt = await hs.partialState.completedAt(roomId);
602
+ const initial = since === null || p.fullState || m.stream > since || completedAt !== null && completedAt > since && completedAt <= p.to;
603
+ const [{ timeline, state, stateAfter, timelineEvents }, ephemeral, roomAccountData, summary, counts] = await Promise.all([
604
+ roomTimeline(hs, p, roomId, initial ? null : since, p.to),
605
+ roomEphemeral(hs, p, roomId, changes ? changes.receipts.get(roomId) ?? [] : void 0),
606
+ accountData(hs, p, roomId, changes ? changes.accountData.get(roomId) ?? [] : void 0),
607
+ roomSummary(hs, roomId, userId),
608
+ hs.push.unreadCounts(userId, roomId)
609
+ ]);
610
+ if (!initial && timelineEvents.length === 0 && state.length === 0 && stateAfter.length === 0 && ephemeral.length === 0 && roomAccountData.length === 0) return null;
611
+ const room = {
612
+ timeline,
613
+ ...p.useStateAfter ? { [p.stateAfterKey ?? "state_after"]: { events: stateAfter.map((e) => toClientEvent(e, { includeRoomId: false })) } } : { state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) } },
614
+ ephemeral: { events: ephemeral },
615
+ account_data: { events: roomAccountData },
616
+ summary
617
+ };
618
+ if (p.filter.room?.timeline?.unread_thread_notifications) {
619
+ room.unread_notifications = counts.room;
620
+ if (Object.keys(counts.threads).length) room.unread_thread_notifications = counts.threads;
621
+ } else {
622
+ const total = { ...counts.room };
623
+ for (const t of Object.values(counts.threads)) {
624
+ total.notification_count += t.notification_count;
625
+ total.highlight_count += t.highlight_count;
626
+ }
627
+ room.unread_notifications = total;
628
+ }
629
+ return room;
630
+ };
631
+ const built = await mapConcurrently(joinedBuilds, JOINED_ROOMS_AT_ONCE, joinedRoom);
632
+ joinedBuilds.forEach((m, i) => {
633
+ if (!built[i]) return;
634
+ join[m.room_id] = built[i];
635
+ empty = false;
636
+ });
536
637
  const globalAccountData = await accountData(hs, p, "", changes ? changes.accountData.get("") ?? [] : void 0);
537
638
  if (since === null && !globalAccountData.some((e) => e.type === M.PUSH_RULES)) globalAccountData.push({
538
639
  type: M.PUSH_RULES,
539
- content: { global: await loadRules(hs, userId) }
640
+ content: { global: await readOnlyRules(hs, userId) }
540
641
  });
541
642
  if (globalAccountData.length) empty = false;
542
643
  await acknowledged;
@@ -557,32 +658,75 @@ async function buildSync(hs, p) {
557
658
  }
558
659
  }
559
660
  const sharingList = [...sharing];
560
- const [presenceRows = [], newlySharedPresence = [], deviceRows] = await readBatch(hs.db, [
561
- {
562
- model: "presence",
563
- opts: { where: [{
564
- field: "user_id",
565
- operator: "in",
566
- value: wantPresence ? sharingList : []
567
- }, ...since !== null ? [{
661
+ const recent = (model) => ({
662
+ model,
663
+ opts: {
664
+ where: [{
568
665
  field: "stream",
569
666
  operator: "gt",
570
667
  value: since
571
- }] : []] }
668
+ }],
669
+ sortBy: {
670
+ field: "stream",
671
+ direction: "asc"
672
+ },
673
+ limit: 501
674
+ }
675
+ });
676
+ const presenceQuery = since === null ? {
677
+ model: "presence",
678
+ opts: { where: [{
679
+ field: "user_id",
680
+ operator: "in",
681
+ value: wantPresence ? sharingList : []
682
+ }] }
683
+ } : wantPresence ? recent("presence") : {
684
+ model: "presence",
685
+ opts: { where: [{
686
+ field: "user_id",
687
+ operator: "in",
688
+ value: []
689
+ }] }
690
+ };
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
+ }
572
699
  },
573
- {
700
+ { query: {
574
701
  model: "presence",
575
702
  opts: { where: [{
576
703
  field: "user_id",
577
704
  operator: "in",
578
705
  value: [...newlyShared]
579
706
  }] }
580
- },
707
+ } },
581
708
  ...since !== null ? [{
582
- model: "device_list_changes",
583
- opts: deviceListChangesQuery(sharing, since)
709
+ query: recent("device_list_changes"),
710
+ unlessChanged: {
711
+ log: "deviceLists",
712
+ key: "*",
713
+ after: since
714
+ }
584
715
  }] : []
585
716
  ]);
717
+ if (since !== null) {
718
+ if (presenceRows.length > RECENT_CHANGES) presenceRows = await hs.db.findMany("presence", { where: [{
719
+ field: "user_id",
720
+ operator: "in",
721
+ value: sharingList
722
+ }, {
723
+ field: "stream",
724
+ operator: "gt",
725
+ value: since
726
+ }] });
727
+ else presenceRows = presenceRows.filter((r) => sharing.has(r.user_id));
728
+ if (deviceRows.length > RECENT_CHANGES) deviceRows = await hs.db.findMany("device_list_changes", deviceListChangesQuery(sharing, since));
729
+ }
586
730
  if (wantPresence) {
587
731
  const seen = /* @__PURE__ */ new Set();
588
732
  for (const row of [...presenceRows, ...newlySharedPresence]) {
@@ -609,7 +753,8 @@ async function buildSync(hs, p) {
609
753
  const body = {
610
754
  next_batch: formatToken({
611
755
  pos: p.to,
612
- toDevice: nextToDevice
756
+ toDevice: nextToDevice,
757
+ ack: toDevice.length > 0
613
758
  }),
614
759
  rooms: Object.fromEntries(Object.entries({
615
760
  join,
@@ -658,14 +803,12 @@ async function sync(ctx) {
658
803
  const timeout = Math.min(Math.max(Number.parseInt(ctx.query.get("timeout") ?? "0", 10) || 0, 0), hs.config.maxSyncTimeoutMs);
659
804
  const setPresence = ctx.query.get("set_presence") ?? "online";
660
805
  if (setPresence !== "offline") await touchPresence(hs, session.userId, setPresence);
661
- const deadline = Date.now() + timeout;
662
- for (;;) {
663
- const to = await hs.streams.position();
664
- const ignored = /* @__PURE__ */ new Set();
665
- const stable = ctx.query.get("use_state_after") === "true";
666
- const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
667
- const useStateAfter = stable || unstable;
668
- 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) => {
669
812
  const { body, empty } = await syncSlots(hs).run(() => buildSync(hs, {
670
813
  session,
671
814
  ignored,
@@ -676,20 +819,21 @@ async function sync(ctx) {
676
819
  useStateAfter,
677
820
  stateAfterKey
678
821
  }));
679
- const remaining = deadline - Date.now();
680
- if (!since || !empty || remaining <= 0 || fullState) return body;
681
- if (await hs.streams.position() > to) continue;
682
- await hs.notifier.wait(session.userId, remaining, ctx.request.signal);
683
- if (ctx.request.signal?.aborted) return body;
684
- }
822
+ return {
823
+ value: body,
824
+ done: !since || !empty || fullState
825
+ };
826
+ });
827
+ return value;
685
828
  }
686
829
  const LAST_ACTIVE_GRANULARITY_MS = 6e4;
687
830
  async function touchPresence(hs, userId, presence) {
688
831
  const state = presence === "unavailable" ? "unavailable" : "online";
689
- const row = await hs.db.findOne("presence", [{
832
+ const read = () => hs.db.findOne("presence", [{
690
833
  field: "user_id",
691
834
  value: userId
692
835
  }]);
836
+ const row = hs.changes ? await hs.changes.values.presenceRow.get(userId, read) : await read();
693
837
  if (row && row.presence === state) {
694
838
  if (Date.now() - row.last_active_ts >= LAST_ACTIVE_GRANULARITY_MS) await hs.db.update("presence", [{
695
839
  field: "user_id",
@@ -697,13 +841,17 @@ async function touchPresence(hs, userId, presence) {
697
841
  }], { last_active_ts: Date.now() });
698
842
  return;
699
843
  }
700
- await hs.streams.write((stream) => hs.db.upsert("presence", {
701
- user_id: userId,
702
- presence: state,
703
- status_msg: row?.status_msg ?? null,
704
- last_active_ts: Date.now(),
705
- stream
706
- }));
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);
707
855
  await hs.federation.onPresence(userId, state, row?.status_msg ?? null).catch(() => {});
708
856
  }
709
857
  function registerSyncRoutes(r) {