matrix-nodejs-homeserver 0.3.1 → 0.3.3

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,23 +2,27 @@ 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
+ import { deviceListChangesQuery } from "../core/keys.js";
7
8
  import { body } from "../http/input.js";
8
9
  import { procedure } from "../http/procedure.js";
10
+ import { readBatch } from "../storage/batch.js";
9
11
  import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
12
+ import { Semaphore } from "../core/semaphore.js";
10
13
  import { fullProfile } from "../core/profile.js";
11
14
  //#region src/api/sync.ts
12
15
  function parseSyncToken(raw) {
13
16
  if (!raw) return null;
14
- const m = /^s(\d+)(?:_(\d+))?$/.exec(raw);
17
+ const m = /^s(\d+)(?:_(\d+))?(_a)?$/.exec(raw);
15
18
  if (!m) throw errors.invalidParam("Invalid since token");
16
19
  return {
17
20
  pos: Number(m[1]),
18
- toDevice: Number(m[2] ?? m[1])
21
+ toDevice: Number(m[2] ?? m[1]),
22
+ ack: !!m[3]
19
23
  };
20
24
  }
21
- const formatToken = (t) => `s${t.pos}_${t.toDevice}`;
25
+ const formatToken = (t) => `s${t.pos}_${t.toDevice}${t.ack ? "_a" : ""}`;
22
26
  async function loadFilter(hs, userId, raw) {
23
27
  if (!raw) return {};
24
28
  if (raw.trim().startsWith("{")) {
@@ -64,7 +68,8 @@ async function snapshotAt(hs, roomId, pos) {
64
68
  field: "stream",
65
69
  direction: "desc"
66
70
  },
67
- limit: 1
71
+ limit: 1,
72
+ fields: ["state_snapshot"]
68
73
  }))[0]?.state_snapshot ?? 0;
69
74
  }
70
75
  /** Build the timeline + state section of a room. */
@@ -84,6 +89,8 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
84
89
  operator: "gt",
85
90
  value: since
86
91
  });
92
+ const sinceIdsRead = since !== null ? snapshotAt(hs, roomId, since).then((id) => hs.rooms.snapshotIds(id)) : Promise.resolve({});
93
+ const scanned = [];
87
94
  const events = [];
88
95
  let limited = false;
89
96
  let cursor = upTo;
@@ -102,6 +109,7 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
102
109
  limit: batchSize
103
110
  });
104
111
  if (rows.length === 0) break;
112
+ scanned.push(...rows);
105
113
  const stored = rows.map((r) => hs.rooms.rowToStored(r));
106
114
  const visible = await hs.rooms.filterVisible(p.session.userId, roomId, stored);
107
115
  const ok = new Map(visible.map((v) => [v.pdu.event_id, v]));
@@ -126,7 +134,8 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
126
134
  }
127
135
  if (limit === 0 && events.length === 0) limited = (await hs.db.findMany("events", {
128
136
  where,
129
- limit: 1
137
+ limit: 1,
138
+ fields: ["stream"]
130
139
  })).length > 0;
131
140
  if (since === null) events.sort((a, b) => (a.pdu.depth ?? 0) - (b.pdu.depth ?? 0) || a.stream - b.stream);
132
141
  else events.reverse();
@@ -138,24 +147,50 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
138
147
  events.splice(0, lastGap);
139
148
  limited = true;
140
149
  }
141
- if (!limited && events.length) limited = (await hs.db.findMany("events", {
142
- where: [...where.filter((c) => c.operator !== "lte"), {
143
- field: "stream",
144
- operator: "lt",
145
- value: events[0].stream
146
- }],
147
- limit: 1
148
- })).length > 0;
149
150
  const startPos = events.length ? Math.min(...events.map((e) => e.stream)) - 1 : upTo;
150
- const startState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, startPos));
151
+ const newestAtOrBefore = async (pos) => {
152
+ const row = scanned.find((r) => r.stream <= pos);
153
+ if (row) return row;
154
+ return (await hs.db.findMany("events", {
155
+ where: [{
156
+ field: "room_id",
157
+ value: roomId
158
+ }, {
159
+ field: "stream",
160
+ operator: "lte",
161
+ value: pos
162
+ }],
163
+ sortBy: {
164
+ field: "stream",
165
+ direction: "desc"
166
+ },
167
+ limit: 1,
168
+ fields: ["stream", "state_snapshot"]
169
+ }))[0] ?? null;
170
+ };
171
+ const [atStart, atEnd] = await Promise.all([newestAtOrBefore(startPos), startPos === upTo ? null : newestAtOrBefore(upTo)]);
172
+ const startSnapshot = atStart?.state_snapshot ?? 0;
173
+ const endSnapshot = startPos === upTo ? startSnapshot : atEnd?.state_snapshot ?? 0;
174
+ if (!limited && events.length) {
175
+ if (events[0].stream === startPos + 1) limited = atStart !== null && (since === null || atStart.stream > since);
176
+ else limited = (await hs.db.findMany("events", {
177
+ where: [...where.filter((c) => c.operator !== "lte"), {
178
+ field: "stream",
179
+ operator: "lt",
180
+ value: events[0].stream
181
+ }],
182
+ limit: 1,
183
+ fields: ["stream"]
184
+ })).length > 0;
185
+ }
186
+ const [startState, endState] = await Promise.all([hs.rooms.stateAtSnapshot(startSnapshot), hs.rooms.stateAtSnapshot(endSnapshot)]);
151
187
  const timelineIds = new Set(events.map((e) => e.pdu.event_id));
152
- const endState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, upTo));
153
188
  for (const [key, ev] of endState) {
154
189
  if (timelineIds.has(ev.event_id) || startState.get(key)?.event_id === ev.event_id) continue;
155
190
  if (events.some((e) => e.pdu.state_key !== void 0 && stateKey(e.pdu.type, e.pdu.state_key) === key)) continue;
156
191
  startState.set(key, ev);
157
192
  }
158
- const sinceIds = since !== null ? await hs.rooms.snapshotIds(await snapshotAt(hs, roomId, since)) : {};
193
+ const sinceIds = await sinceIdsRead;
159
194
  const sf = p.filter.room?.state;
160
195
  const lazy = !!sf?.lazy_load_members;
161
196
  const senders = new Set(events.map((e) => e.pdu.sender));
@@ -175,15 +210,12 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
175
210
  if (member && matchesFilter(sf, member)) state.push(member);
176
211
  }
177
212
  const stateAfter = [];
178
- if (p.useStateAfter) {
179
- const endState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, upTo));
180
- for (const [key, ev] of endState) {
181
- if (sinceIds[key] === ev.event_id) continue;
182
- const [type, sk] = splitStateKey(key);
183
- if (!matchesFilter(sf, ev)) continue;
184
- if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
185
- stateAfter.push(ev);
186
- }
213
+ if (p.useStateAfter) for (const [key, ev] of endState) {
214
+ if (sinceIds[key] === ev.event_id) continue;
215
+ const [type, sk] = splitStateKey(key);
216
+ if (!matchesFilter(sf, ev)) continue;
217
+ if (lazy && type === M.ROOM.MEMBER && !senders.has(sk)) continue;
218
+ stateAfter.push(ev);
187
219
  }
188
220
  return {
189
221
  stateAfter,
@@ -196,7 +228,7 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
196
228
  timelineEvents: events
197
229
  };
198
230
  }
199
- async function roomEphemeral(hs, p, roomId) {
231
+ async function roomEphemeral(hs, p, roomId, prefetched) {
200
232
  const since = p.since?.pos ?? null;
201
233
  const out = [];
202
234
  const typing = hs.typing.typingSince(roomId, since);
@@ -213,7 +245,7 @@ async function roomEphemeral(hs, p, roomId) {
213
245
  operator: "gt",
214
246
  value: since
215
247
  });
216
- const receipts = (await hs.db.findMany("receipts", { where })).filter((r) => r.stream <= p.to);
248
+ const receipts = (prefetched ?? await hs.db.findMany("receipts", { where })).filter((r) => r.stream <= p.to);
217
249
  const content = {};
218
250
  for (const r of receipts) {
219
251
  if (r.receipt_type === M.READ.PRIVATE && r.user_id !== p.session.userId) continue;
@@ -231,7 +263,7 @@ async function roomEphemeral(hs, p, roomId) {
231
263
  });
232
264
  return out.filter((e) => matchesFilter(p.filter.room?.ephemeral, e));
233
265
  }
234
- async function accountData(hs, p, roomId) {
266
+ async function accountData(hs, p, roomId, prefetched) {
235
267
  const where = [{
236
268
  field: "user_id",
237
269
  value: p.session.userId
@@ -244,7 +276,7 @@ async function accountData(hs, p, roomId) {
244
276
  operator: "gt",
245
277
  value: p.since.pos
246
278
  });
247
- const rows = await hs.db.findMany("account_data", { where });
279
+ const rows = prefetched ?? await hs.db.findMany("account_data", { where });
248
280
  const filter = roomId ? p.filter.room?.account_data : p.filter.account_data;
249
281
  return rows.filter((r) => r.stream <= p.to && (p.since || Object.keys(r.content ?? {}).length > 0)).map((r) => ({
250
282
  type: r.type,
@@ -271,50 +303,223 @@ async function roomSummary(hs, roomId, userId) {
271
303
  if (!getState(state, M.ROOM.NAME)?.content?.name && !getState(state, M.ROOM.CANONICAL_ALIAS)?.content?.alias) summary[M.HEROES] = heroes.sort();
272
304
  return summary;
273
305
  }
306
+ /**
307
+ * What changed for the user since the token, for all of their rooms at once: the rooms with new
308
+ * events or newly complete state, and the new receipts and account data by room.
309
+ *
310
+ * https://github.com/k18k/matrix-nodejs-homeserver/issues/33: an incremental sync queried every
311
+ * joined room (timeline, receipts, account data) and only then found that most had nothing new,
312
+ * so a sync cost as many queries as the user had rooms, each time a long-poll woke up. These few
313
+ * queries find the rooms that changed, and only those are built.
314
+ */
315
+ /** One-time key counts by algorithm, as /sync reports them (signed_curve25519 always present). */
316
+ function countAlgorithms(rows) {
317
+ const counts = { signed_curve25519: 0 };
318
+ for (const r of rows) counts[r.algorithm] = (counts[r.algorithm] ?? 0) + 1;
319
+ return counts;
320
+ }
321
+ async function changesSince(hs, p, memberships, since, accountRows) {
322
+ const joined = memberships.filter((m) => m.membership === "join").map((m) => m.room_id);
323
+ const anyMembership = [...new Set(memberships.map((m) => m.room_id))];
324
+ const range = [{
325
+ field: "stream",
326
+ operator: "gt",
327
+ value: since
328
+ }, {
329
+ field: "stream",
330
+ operator: "lte",
331
+ value: p.to
332
+ }];
333
+ const chunks = (list) => Array.from({ length: Math.ceil(list.length / 500) }, (_, i) => list.slice(i * 500, i * 500 + 500));
334
+ const inRooms = (chunk) => ({
335
+ field: "room_id",
336
+ operator: "in",
337
+ value: chunk
338
+ });
339
+ const queries = [];
340
+ for (const chunk of chunks(joined)) {
341
+ queries.push({
342
+ model: "events",
343
+ opts: {
344
+ where: [inRooms(chunk), ...range],
345
+ fields: ["room_id"]
346
+ }
347
+ });
348
+ queries.push({
349
+ model: "receipts",
350
+ opts: { where: [inRooms(chunk), ...range] }
351
+ });
352
+ queries.push({
353
+ model: "partial_state_rooms",
354
+ opts: {
355
+ where: [inRooms(chunk), {
356
+ field: "completed_stream",
357
+ operator: "gt",
358
+ value: since
359
+ }],
360
+ fields: ["room_id"]
361
+ }
362
+ });
363
+ }
364
+ for (const chunk of chunks(anyMembership)) queries.push({
365
+ model: "memberships",
366
+ opts: { where: [inRooms(chunk), {
367
+ field: "stream",
368
+ operator: "gt",
369
+ value: since
370
+ }] }
371
+ });
372
+ const results = await readBatch(hs.db, queries);
373
+ const touched = /* @__PURE__ */ new Set();
374
+ const receipts = /* @__PURE__ */ new Map();
375
+ const joinedChunks = Math.ceil(joined.length / 500);
376
+ for (let c = 0; c < joinedChunks; c++) {
377
+ const [events, receiptRows, completed] = results.slice(c * 3, c * 3 + 3);
378
+ for (const r of events) touched.add(r.room_id);
379
+ for (const r of receiptRows) {
380
+ touched.add(r.room_id);
381
+ receipts.set(r.room_id, [...receipts.get(r.room_id) ?? [], r]);
382
+ }
383
+ for (const r of completed) touched.add(r.room_id);
384
+ }
385
+ const memberChanges = results.slice(joinedChunks * 3).flat();
386
+ const accountData = /* @__PURE__ */ new Map();
387
+ for (const r of accountRows) {
388
+ if (r.room_id) touched.add(r.room_id);
389
+ accountData.set(r.room_id, [...accountData.get(r.room_id) ?? [], r]);
390
+ }
391
+ return {
392
+ touched,
393
+ receipts,
394
+ accountData,
395
+ memberChanges
396
+ };
397
+ }
398
+ /** Presence or device-list changes after a token read without a user filter, at most (#44). */
399
+ const RECENT_CHANGES = 500;
400
+ /** How many of a sync's joined rooms are built at once (#38). */
401
+ const JOINED_ROOMS_AT_ONCE = 8;
402
+ /** `fn` over `items` with at most `limit` running at once; results in input order. */
403
+ async function mapConcurrently(items, limit, fn) {
404
+ const out = [];
405
+ let next = 0;
406
+ const worker = async () => {
407
+ while (next < items.length) {
408
+ const i = next++;
409
+ out[i] = await fn(items[i]);
410
+ }
411
+ };
412
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
413
+ return out;
414
+ }
274
415
  async function buildSync(hs, p) {
275
416
  const userId = p.session.userId;
276
417
  const since = p.since?.pos ?? null;
277
- const memberships = await hs.db.findMany("memberships", { where: [{
418
+ const incremental = since !== null && !p.fullState;
419
+ const deviceWhere = [{
278
420
  field: "user_id",
279
421
  value: userId
280
- }] });
422
+ }, {
423
+ field: "device_id",
424
+ value: p.session.deviceId
425
+ }];
426
+ const acknowledged = p.since?.ack ? hs.db.delete("to_device", [...deviceWhere, {
427
+ field: "stream",
428
+ operator: "lte",
429
+ value: p.since.toDevice
430
+ }]) : Promise.resolve(0);
431
+ const [memberships, ignoredRows, toDeviceRows, oneTimeKeyRows, fallbackRows, accountRows = []] = await readBatch(hs.db, [
432
+ {
433
+ model: "memberships",
434
+ opts: { where: [{
435
+ field: "user_id",
436
+ value: userId
437
+ }] }
438
+ },
439
+ {
440
+ model: "account_data",
441
+ opts: { where: [
442
+ {
443
+ field: "user_id",
444
+ value: userId
445
+ },
446
+ {
447
+ field: "room_id",
448
+ value: ""
449
+ },
450
+ {
451
+ field: "type",
452
+ value: M.IGNORED_USER_LIST
453
+ }
454
+ ] }
455
+ },
456
+ {
457
+ model: "to_device",
458
+ opts: {
459
+ where: [...deviceWhere, {
460
+ field: "stream",
461
+ operator: "gt",
462
+ value: p.since?.toDevice ?? 0
463
+ }],
464
+ sortBy: {
465
+ field: "stream",
466
+ direction: "asc"
467
+ },
468
+ limit: 100
469
+ }
470
+ },
471
+ {
472
+ model: "one_time_keys",
473
+ opts: {
474
+ where: [...deviceWhere, {
475
+ field: "fallback",
476
+ value: false
477
+ }],
478
+ fields: ["algorithm"]
479
+ }
480
+ },
481
+ {
482
+ model: "fallback_keys",
483
+ opts: {
484
+ where: [...deviceWhere, {
485
+ field: "used",
486
+ value: false
487
+ }],
488
+ fields: ["algorithm"]
489
+ }
490
+ },
491
+ ...incremental ? [{
492
+ model: "account_data",
493
+ opts: { where: [{
494
+ field: "user_id",
495
+ value: userId
496
+ }, {
497
+ field: "stream",
498
+ operator: "gt",
499
+ value: since
500
+ }] }
501
+ }] : []
502
+ ]);
503
+ p = {
504
+ ...p,
505
+ ignored: new Set(Object.keys(ignoredRows[0]?.content?.ignored_users ?? {}))
506
+ };
507
+ const changes = incremental ? await changesSince(hs, p, memberships, since, accountRows) : null;
508
+ let memberChangesRead = changes ? Promise.resolve(changes.memberChanges) : null;
509
+ const memberChanges = () => memberChangesRead ??= hs.rooms.membershipChangesSince(userId, since ?? 0, memberships);
281
510
  const join = {};
282
511
  const invite = {};
283
512
  const leave = {};
284
513
  const knock = {};
285
514
  let empty = true;
515
+ const joinedBuilds = [];
286
516
  for (const m of memberships) {
287
517
  if (m.stream > p.to || !roomAllowed(p.filter, m.room_id)) continue;
288
518
  const roomId = m.room_id;
289
519
  const changed = since === null || m.stream > since;
290
520
  if (m.membership === "join") {
291
- if (!p.filter.room?.state?.lazy_load_members && await hs.partialState.isPartial(roomId)) continue;
292
- const completedAt = await hs.partialState.completedAt(roomId);
293
- const initial = since === null || p.fullState || m.stream > since || completedAt !== null && completedAt > since && completedAt <= p.to;
294
- const { timeline, state, stateAfter, timelineEvents } = await roomTimeline(hs, p, roomId, initial ? null : since, p.to);
295
- const ephemeral = await roomEphemeral(hs, p, roomId);
296
- const roomAccountData = await accountData(hs, p, roomId);
297
- if (!initial && timelineEvents.length === 0 && state.length === 0 && stateAfter.length === 0 && ephemeral.length === 0 && roomAccountData.length === 0) continue;
298
- join[roomId] = {
299
- timeline,
300
- ...p.useStateAfter ? { [p.stateAfterKey ?? "state_after"]: { events: stateAfter.map((e) => toClientEvent(e, { includeRoomId: false })) } } : { state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) } },
301
- ephemeral: { events: ephemeral },
302
- account_data: { events: roomAccountData },
303
- summary: await roomSummary(hs, roomId, userId)
304
- };
305
- const counts = await hs.push.unreadCounts(userId, roomId);
306
- if (p.filter.room?.timeline?.unread_thread_notifications) {
307
- join[roomId].unread_notifications = counts.room;
308
- if (Object.keys(counts.threads).length) join[roomId].unread_thread_notifications = counts.threads;
309
- } else {
310
- const total = { ...counts.room };
311
- for (const t of Object.values(counts.threads)) {
312
- total.notification_count += t.notification_count;
313
- total.highlight_count += t.highlight_count;
314
- }
315
- join[roomId].unread_notifications = total;
316
- }
317
- empty = false;
521
+ if (changes && !changed && !changes.touched.has(roomId) && hs.typing.typingSince(roomId, since) === null) continue;
522
+ joinedBuilds.push(m);
318
523
  } else if (m.membership === "invite") {
319
524
  if (!changed && !p.fullState) continue;
320
525
  const pdu = await hs.rooms.getPdu(m.event_id);
@@ -341,68 +546,135 @@ async function buildSync(hs, p) {
341
546
  leave[roomId] = {
342
547
  timeline,
343
548
  state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) },
344
- account_data: { events: await accountData(hs, p, roomId) }
549
+ account_data: { events: await accountData(hs, p, roomId, changes ? changes.accountData.get(roomId) ?? [] : void 0) }
345
550
  };
346
551
  empty = false;
347
552
  }
348
553
  }
349
- const globalAccountData = await accountData(hs, p, "");
554
+ const joinedRoom = async (m) => {
555
+ const roomId = m.room_id;
556
+ if (!p.filter.room?.state?.lazy_load_members && await hs.partialState.isPartial(roomId)) return null;
557
+ const completedAt = await hs.partialState.completedAt(roomId);
558
+ const initial = since === null || p.fullState || m.stream > since || completedAt !== null && completedAt > since && completedAt <= p.to;
559
+ const [{ timeline, state, stateAfter, timelineEvents }, ephemeral, roomAccountData, summary, counts] = await Promise.all([
560
+ roomTimeline(hs, p, roomId, initial ? null : since, p.to),
561
+ roomEphemeral(hs, p, roomId, changes ? changes.receipts.get(roomId) ?? [] : void 0),
562
+ accountData(hs, p, roomId, changes ? changes.accountData.get(roomId) ?? [] : void 0),
563
+ roomSummary(hs, roomId, userId),
564
+ hs.push.unreadCounts(userId, roomId)
565
+ ]);
566
+ if (!initial && timelineEvents.length === 0 && state.length === 0 && stateAfter.length === 0 && ephemeral.length === 0 && roomAccountData.length === 0) return null;
567
+ const room = {
568
+ timeline,
569
+ ...p.useStateAfter ? { [p.stateAfterKey ?? "state_after"]: { events: stateAfter.map((e) => toClientEvent(e, { includeRoomId: false })) } } : { state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) } },
570
+ ephemeral: { events: ephemeral },
571
+ account_data: { events: roomAccountData },
572
+ summary
573
+ };
574
+ if (p.filter.room?.timeline?.unread_thread_notifications) {
575
+ room.unread_notifications = counts.room;
576
+ if (Object.keys(counts.threads).length) room.unread_thread_notifications = counts.threads;
577
+ } else {
578
+ const total = { ...counts.room };
579
+ for (const t of Object.values(counts.threads)) {
580
+ total.notification_count += t.notification_count;
581
+ total.highlight_count += t.highlight_count;
582
+ }
583
+ room.unread_notifications = total;
584
+ }
585
+ return room;
586
+ };
587
+ const built = await mapConcurrently(joinedBuilds, JOINED_ROOMS_AT_ONCE, joinedRoom);
588
+ joinedBuilds.forEach((m, i) => {
589
+ if (!built[i]) return;
590
+ join[m.room_id] = built[i];
591
+ empty = false;
592
+ });
593
+ const globalAccountData = await accountData(hs, p, "", changes ? changes.accountData.get("") ?? [] : void 0);
350
594
  if (since === null && !globalAccountData.some((e) => e.type === M.PUSH_RULES)) globalAccountData.push({
351
595
  type: M.PUSH_RULES,
352
- content: { global: await loadRules(hs, userId) }
596
+ content: { global: await readOnlyRules(hs, userId) }
353
597
  });
354
598
  if (globalAccountData.length) empty = false;
355
- const deviceWhere = [{
356
- field: "user_id",
357
- value: userId
358
- }, {
359
- field: "device_id",
360
- value: p.session.deviceId
361
- }];
362
- if (p.since) await hs.db.delete("to_device", [...deviceWhere, {
363
- field: "stream",
364
- operator: "lte",
365
- value: p.since.toDevice
366
- }]);
367
- const toDevice = (await hs.db.findMany("to_device", {
368
- where: [...deviceWhere, {
369
- field: "stream",
370
- operator: "gt",
371
- value: p.since?.toDevice ?? 0
372
- }],
373
- sortBy: {
374
- field: "stream",
375
- direction: "asc"
376
- },
377
- limit: 100
378
- })).filter((r) => r.stream <= p.to);
599
+ await acknowledged;
600
+ const toDevice = toDeviceRows.filter((r) => r.stream <= p.to);
379
601
  const nextToDevice = toDevice.length ? toDevice[toDevice.length - 1].stream : p.since?.toDevice ?? 0;
380
602
  if (toDevice.length) empty = false;
381
603
  const presenceEvents = [];
382
- if (!p.filter.presence?.types || p.filter.presence.types.length) {
383
- const sharing = await hs.rooms.usersSharingRooms(userId);
384
- const newlyShared = /* @__PURE__ */ new Set();
385
- if (since !== null) {
386
- const changes = await hs.db.findMany("memberships", { where: [{
604
+ const wantPresence = !p.filter.presence?.types || p.filter.presence.types.length > 0;
605
+ const sharing = await hs.rooms.usersSharingRooms(userId);
606
+ const joinedRooms = memberships.filter((m) => m.membership === "join").map((m) => m.room_id);
607
+ const newlyShared = /* @__PURE__ */ new Set();
608
+ if (since !== null && wantPresence) {
609
+ const myRooms = new Set(joinedRooms);
610
+ for (const c of await memberChanges()) {
611
+ if (c.membership !== "join" || !myRooms.has(c.room_id)) continue;
612
+ if (c.user_id === userId) for (const u of await hs.rooms.joinedMembers(c.room_id)) newlyShared.add(u);
613
+ else newlyShared.add(c.user_id);
614
+ }
615
+ }
616
+ const sharingList = [...sharing];
617
+ const recent = (model) => ({
618
+ model,
619
+ opts: {
620
+ where: [{
387
621
  field: "stream",
388
622
  operator: "gt",
389
623
  value: since
390
- }] });
391
- const myRooms = new Set(memberships.filter((m) => m.membership === "join").map((m) => m.room_id));
392
- for (const c of changes) {
393
- if (c.membership !== "join" || !myRooms.has(c.room_id)) continue;
394
- if (c.user_id === userId) for (const u of await hs.rooms.joinedMembers(c.room_id)) newlyShared.add(u);
395
- else newlyShared.add(c.user_id);
396
- }
624
+ }],
625
+ sortBy: {
626
+ field: "stream",
627
+ direction: "asc"
628
+ },
629
+ limit: 501
397
630
  }
398
- const where = [{
631
+ });
632
+ const presenceQuery = since === null ? {
633
+ model: "presence",
634
+ opts: { where: [{
635
+ field: "user_id",
636
+ operator: "in",
637
+ value: wantPresence ? sharingList : []
638
+ }] }
639
+ } : wantPresence ? recent("presence") : {
640
+ model: "presence",
641
+ opts: { where: [{
399
642
  field: "user_id",
400
643
  operator: "in",
401
- value: [...sharing]
402
- }];
403
- for (const row of await hs.db.findMany("presence", { where })) {
644
+ value: []
645
+ }] }
646
+ };
647
+ let [presenceRows = [], newlySharedPresence = [], deviceRows] = await readBatch(hs.db, [
648
+ presenceQuery,
649
+ {
650
+ model: "presence",
651
+ opts: { where: [{
652
+ field: "user_id",
653
+ operator: "in",
654
+ value: [...newlyShared]
655
+ }] }
656
+ },
657
+ ...since !== null ? [recent("device_list_changes")] : []
658
+ ]);
659
+ if (since !== null) {
660
+ if (presenceRows.length > RECENT_CHANGES) presenceRows = await hs.db.findMany("presence", { where: [{
661
+ field: "user_id",
662
+ operator: "in",
663
+ value: sharingList
664
+ }, {
665
+ field: "stream",
666
+ operator: "gt",
667
+ value: since
668
+ }] });
669
+ else presenceRows = presenceRows.filter((r) => sharing.has(r.user_id));
670
+ if (deviceRows.length > RECENT_CHANGES) deviceRows = await hs.db.findMany("device_list_changes", deviceListChangesQuery(sharing, since));
671
+ }
672
+ if (wantPresence) {
673
+ const seen = /* @__PURE__ */ new Set();
674
+ for (const row of [...presenceRows, ...newlySharedPresence]) {
675
+ if (seen.has(row.user_id)) continue;
676
+ seen.add(row.user_id);
404
677
  if (row.stream > p.to) continue;
405
- if (since !== null && row.stream <= since && !newlyShared.has(row.user_id)) continue;
406
678
  const content = {
407
679
  presence: row.presence,
408
680
  last_active_ago: Date.now() - row.last_active_ts
@@ -418,12 +690,13 @@ async function buildSync(hs, p) {
418
690
  }
419
691
  }
420
692
  if (presenceEvents.length) empty = false;
421
- const profileUpdates = await profileUpdatesFor(hs, p, memberships);
693
+ const profileUpdates = await profileUpdatesFor(hs, p, memberships, memberChanges);
422
694
  if (profileUpdates) empty = false;
423
695
  const body = {
424
696
  next_batch: formatToken({
425
697
  pos: p.to,
426
- toDevice: nextToDevice
698
+ toDevice: nextToDevice,
699
+ ack: toDevice.length > 0
427
700
  }),
428
701
  rooms: Object.fromEntries(Object.entries({
429
702
  join,
@@ -438,12 +711,16 @@ async function buildSync(hs, p) {
438
711
  type: r.type,
439
712
  content: r.content
440
713
  })) },
441
- device_one_time_keys_count: await hs.keys.oneTimeKeyCounts(userId, p.session.deviceId),
442
- device_unused_fallback_key_types: await hs.keys.unusedFallbackKeyTypes(userId, p.session.deviceId)
714
+ device_one_time_keys_count: countAlgorithms(oneTimeKeyRows),
715
+ device_unused_fallback_key_types: [...new Set(fallbackRows.map((r) => r.algorithm))]
443
716
  };
444
717
  if (profileUpdates) body[UNSTABLE.ORG.MATRIX.MSC4429.USERS] = profileUpdates;
445
718
  if (since !== null) {
446
- const lists = await hs.keys.changesSince(userId, since, p.to);
719
+ const lists = await hs.keys.changesSince(userId, since, p.to, {
720
+ memberChanges: await memberChanges(),
721
+ deviceRows,
722
+ joinedRooms
723
+ });
447
724
  body.device_lists = lists;
448
725
  if (lists.changed.length || lists.left.length) empty = false;
449
726
  }
@@ -453,6 +730,12 @@ async function buildSync(hs, p) {
453
730
  nextToDevice
454
731
  };
455
732
  }
733
+ const slots = /* @__PURE__ */ new WeakMap();
734
+ function syncSlots(hs) {
735
+ let s = slots.get(hs);
736
+ if (!s) slots.set(hs, s = new Semaphore(hs.config.maxConcurrentSyncs));
737
+ return s;
738
+ }
456
739
  async function sync(ctx) {
457
740
  const hs = ctx.hs;
458
741
  const session = await ctx.auth();
@@ -465,33 +748,21 @@ async function sync(ctx) {
465
748
  const deadline = Date.now() + timeout;
466
749
  for (;;) {
467
750
  const to = await hs.streams.position();
468
- const ignoredRow = await hs.db.findOne("account_data", [
469
- {
470
- field: "user_id",
471
- value: session.userId
472
- },
473
- {
474
- field: "room_id",
475
- value: ""
476
- },
477
- {
478
- field: "type",
479
- value: M.IGNORED_USER_LIST
480
- }
481
- ]);
482
- const ignored = new Set(Object.keys(ignoredRow?.content?.ignored_users ?? {}));
751
+ const ignored = /* @__PURE__ */ new Set();
483
752
  const stable = ctx.query.get("use_state_after") === "true";
484
753
  const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
485
- const { body, empty } = await buildSync(hs, {
754
+ const useStateAfter = stable || unstable;
755
+ const stateAfterKey = stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER;
756
+ const { body, empty } = await syncSlots(hs).run(() => buildSync(hs, {
486
757
  session,
487
758
  ignored,
488
759
  since,
489
760
  to,
490
761
  filter,
491
762
  fullState,
492
- useStateAfter: stable || unstable,
493
- stateAfterKey: stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER
494
- });
763
+ useStateAfter,
764
+ stateAfterKey
765
+ }));
495
766
  const remaining = deadline - Date.now();
496
767
  if (!since || !empty || remaining <= 0 || fullState) return body;
497
768
  if (await hs.streams.position() > to) continue;
@@ -499,6 +770,7 @@ async function sync(ctx) {
499
770
  if (ctx.request.signal?.aborted) return body;
500
771
  }
501
772
  }
773
+ const LAST_ACTIVE_GRANULARITY_MS = 6e4;
502
774
  async function touchPresence(hs, userId, presence) {
503
775
  const state = presence === "unavailable" ? "unavailable" : "online";
504
776
  const row = await hs.db.findOne("presence", [{
@@ -506,7 +778,7 @@ async function touchPresence(hs, userId, presence) {
506
778
  value: userId
507
779
  }]);
508
780
  if (row && row.presence === state) {
509
- await hs.db.update("presence", [{
781
+ if (Date.now() - row.last_active_ts >= LAST_ACTIVE_GRANULARITY_MS) await hs.db.update("presence", [{
510
782
  field: "user_id",
511
783
  value: userId
512
784
  }], { last_active_ts: Date.now() });
@@ -564,7 +836,7 @@ function registerSyncRoutes(r) {
564
836
  * MSC4429: profile changes of users sharing a room, limited to the fields the filter asks for.
565
837
  * The initial sync carries current values; users we no longer share any room with get `null`.
566
838
  */
567
- async function profileUpdatesFor(hs, p, memberships) {
839
+ async function profileUpdatesFor(hs, p, memberships, memberChanges) {
568
840
  const ids = (p.filter.profile_fields ?? p.filter[UNSTABLE.ORG.MATRIX.MSC4429.PROFILE_FIELDS])?.ids;
569
841
  if (!Array.isArray(ids) || !ids.length) return null;
570
842
  const wanted = new Set(ids.filter((x) => typeof x === "string"));
@@ -577,11 +849,7 @@ async function profileUpdatesFor(hs, p, memberships) {
577
849
  const full = new Set(since === null ? sharing : []);
578
850
  if (since !== null) {
579
851
  const myRooms = new Set(memberships.filter((m) => m.membership === "join").map((m) => m.room_id));
580
- const changes = await hs.db.findMany("memberships", { where: [{
581
- field: "stream",
582
- operator: "gt",
583
- value: since
584
- }] });
852
+ const changes = await memberChanges();
585
853
  for (const c of changes) {
586
854
  if (c.stream > p.to || c.user_id === p.session.userId) continue;
587
855
  if (c.membership === "join" && myRooms.has(c.room_id)) {