matrix-nodejs-homeserver 0.3.1 → 0.3.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/README.md +61 -5
- package/dist/adapters/cluster.d.ts +1 -26
- package/dist/adapters/cluster.js +50 -103
- package/dist/api/account.js +22 -3
- package/dist/api/admin.js +9 -4
- package/dist/api/media.js +1 -1
- package/dist/api/preview.js +33 -70
- package/dist/api/sync.js +268 -87
- package/dist/core/cluster.d.ts +12 -5
- package/dist/core/cluster.js +46 -29
- package/dist/core/html.js +13 -0
- package/dist/core/identity.js +1 -1
- package/dist/core/json.js +10 -1
- package/dist/core/keys.d.ts +10 -1
- package/dist/core/keys.js +60 -41
- package/dist/core/outbound.d.ts +16 -0
- package/dist/core/outbound.js +172 -0
- package/dist/core/push.js +29 -17
- package/dist/core/ratelimit.d.ts +1 -1
- package/dist/core/rooms.d.ts +10 -1
- package/dist/core/rooms.js +25 -0
- package/dist/core/semaphore.js +23 -0
- package/dist/core/sso.d.ts +15 -1
- package/dist/core/sso.js +38 -2
- package/dist/core/threepid.js +1 -9
- package/dist/federation/client.js +7 -0
- package/dist/federation/index.d.ts +1 -1
- package/dist/federation/index.js +13 -6
- package/dist/homeserver.d.ts +21 -3
- package/dist/homeserver.js +11 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/storage/batch.js +11 -0
- package/dist/storage/memory.js +1 -0
- package/dist/storage/schema.js +1 -1
- package/dist/storage/sql.js +20 -1
- package/dist/storage/types.d.ts +16 -0
- package/package.json +6 -2
package/dist/api/sync.js
CHANGED
|
@@ -4,9 +4,12 @@ import { errors } from "../core/errors.js";
|
|
|
4
4
|
import { getState } from "../core/authrules.js";
|
|
5
5
|
import { loadRules } 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";
|
|
9
10
|
import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
|
|
11
|
+
import { readBatch } from "../storage/batch.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) {
|
|
@@ -64,7 +67,8 @@ async function snapshotAt(hs, roomId, pos) {
|
|
|
64
67
|
field: "stream",
|
|
65
68
|
direction: "desc"
|
|
66
69
|
},
|
|
67
|
-
limit: 1
|
|
70
|
+
limit: 1,
|
|
71
|
+
fields: ["state_snapshot"]
|
|
68
72
|
}))[0]?.state_snapshot ?? 0;
|
|
69
73
|
}
|
|
70
74
|
/** Build the timeline + state section of a room. */
|
|
@@ -126,7 +130,8 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
126
130
|
}
|
|
127
131
|
if (limit === 0 && events.length === 0) limited = (await hs.db.findMany("events", {
|
|
128
132
|
where,
|
|
129
|
-
limit: 1
|
|
133
|
+
limit: 1,
|
|
134
|
+
fields: ["stream"]
|
|
130
135
|
})).length > 0;
|
|
131
136
|
if (since === null) events.sort((a, b) => (a.pdu.depth ?? 0) - (b.pdu.depth ?? 0) || a.stream - b.stream);
|
|
132
137
|
else events.reverse();
|
|
@@ -144,7 +149,8 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
144
149
|
operator: "lt",
|
|
145
150
|
value: events[0].stream
|
|
146
151
|
}],
|
|
147
|
-
limit: 1
|
|
152
|
+
limit: 1,
|
|
153
|
+
fields: ["stream"]
|
|
148
154
|
})).length > 0;
|
|
149
155
|
const startPos = events.length ? Math.min(...events.map((e) => e.stream)) - 1 : upTo;
|
|
150
156
|
const startState = await hs.rooms.stateAtSnapshot(await snapshotAt(hs, roomId, startPos));
|
|
@@ -196,7 +202,7 @@ async function roomTimeline(hs, p, roomId, since, upTo) {
|
|
|
196
202
|
timelineEvents: events
|
|
197
203
|
};
|
|
198
204
|
}
|
|
199
|
-
async function roomEphemeral(hs, p, roomId) {
|
|
205
|
+
async function roomEphemeral(hs, p, roomId, prefetched) {
|
|
200
206
|
const since = p.since?.pos ?? null;
|
|
201
207
|
const out = [];
|
|
202
208
|
const typing = hs.typing.typingSince(roomId, since);
|
|
@@ -213,7 +219,7 @@ async function roomEphemeral(hs, p, roomId) {
|
|
|
213
219
|
operator: "gt",
|
|
214
220
|
value: since
|
|
215
221
|
});
|
|
216
|
-
const receipts = (await hs.db.findMany("receipts", { where })).filter((r) => r.stream <= p.to);
|
|
222
|
+
const receipts = (prefetched ?? await hs.db.findMany("receipts", { where })).filter((r) => r.stream <= p.to);
|
|
217
223
|
const content = {};
|
|
218
224
|
for (const r of receipts) {
|
|
219
225
|
if (r.receipt_type === M.READ.PRIVATE && r.user_id !== p.session.userId) continue;
|
|
@@ -231,7 +237,7 @@ async function roomEphemeral(hs, p, roomId) {
|
|
|
231
237
|
});
|
|
232
238
|
return out.filter((e) => matchesFilter(p.filter.room?.ephemeral, e));
|
|
233
239
|
}
|
|
234
|
-
async function accountData(hs, p, roomId) {
|
|
240
|
+
async function accountData(hs, p, roomId, prefetched) {
|
|
235
241
|
const where = [{
|
|
236
242
|
field: "user_id",
|
|
237
243
|
value: p.session.userId
|
|
@@ -244,7 +250,7 @@ async function accountData(hs, p, roomId) {
|
|
|
244
250
|
operator: "gt",
|
|
245
251
|
value: p.since.pos
|
|
246
252
|
});
|
|
247
|
-
const rows = await hs.db.findMany("account_data", { where });
|
|
253
|
+
const rows = prefetched ?? await hs.db.findMany("account_data", { where });
|
|
248
254
|
const filter = roomId ? p.filter.room?.account_data : p.filter.account_data;
|
|
249
255
|
return rows.filter((r) => r.stream <= p.to && (p.since || Object.keys(r.content ?? {}).length > 0)).map((r) => ({
|
|
250
256
|
type: r.type,
|
|
@@ -271,13 +277,193 @@ async function roomSummary(hs, roomId, userId) {
|
|
|
271
277
|
if (!getState(state, M.ROOM.NAME)?.content?.name && !getState(state, M.ROOM.CANONICAL_ALIAS)?.content?.alias) summary[M.HEROES] = heroes.sort();
|
|
272
278
|
return summary;
|
|
273
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* What changed for the user since the token, for all of their rooms at once: the rooms with new
|
|
282
|
+
* events or newly complete state, and the new receipts and account data by room.
|
|
283
|
+
*
|
|
284
|
+
* https://github.com/k18k/matrix-nodejs-homeserver/issues/33: an incremental sync queried every
|
|
285
|
+
* joined room (timeline, receipts, account data) and only then found that most had nothing new,
|
|
286
|
+
* so a sync cost as many queries as the user had rooms, each time a long-poll woke up. These few
|
|
287
|
+
* queries find the rooms that changed, and only those are built.
|
|
288
|
+
*/
|
|
289
|
+
/** One-time key counts by algorithm, as /sync reports them (signed_curve25519 always present). */
|
|
290
|
+
function countAlgorithms(rows) {
|
|
291
|
+
const counts = { signed_curve25519: 0 };
|
|
292
|
+
for (const r of rows) counts[r.algorithm] = (counts[r.algorithm] ?? 0) + 1;
|
|
293
|
+
return counts;
|
|
294
|
+
}
|
|
295
|
+
async function changesSince(hs, p, memberships, since, accountRows) {
|
|
296
|
+
const joined = memberships.filter((m) => m.membership === "join").map((m) => m.room_id);
|
|
297
|
+
const anyMembership = [...new Set(memberships.map((m) => m.room_id))];
|
|
298
|
+
const range = [{
|
|
299
|
+
field: "stream",
|
|
300
|
+
operator: "gt",
|
|
301
|
+
value: since
|
|
302
|
+
}, {
|
|
303
|
+
field: "stream",
|
|
304
|
+
operator: "lte",
|
|
305
|
+
value: p.to
|
|
306
|
+
}];
|
|
307
|
+
const chunks = (list) => Array.from({ length: Math.ceil(list.length / 500) }, (_, i) => list.slice(i * 500, i * 500 + 500));
|
|
308
|
+
const inRooms = (chunk) => ({
|
|
309
|
+
field: "room_id",
|
|
310
|
+
operator: "in",
|
|
311
|
+
value: chunk
|
|
312
|
+
});
|
|
313
|
+
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({
|
|
339
|
+
model: "memberships",
|
|
340
|
+
opts: { where: [inRooms(chunk), {
|
|
341
|
+
field: "stream",
|
|
342
|
+
operator: "gt",
|
|
343
|
+
value: since
|
|
344
|
+
}] }
|
|
345
|
+
});
|
|
346
|
+
const results = await readBatch(hs.db, queries);
|
|
347
|
+
const touched = /* @__PURE__ */ new Set();
|
|
348
|
+
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]);
|
|
356
|
+
}
|
|
357
|
+
for (const r of completed) touched.add(r.room_id);
|
|
358
|
+
}
|
|
359
|
+
const memberChanges = results.slice(joinedChunks * 3).flat();
|
|
360
|
+
const accountData = /* @__PURE__ */ new Map();
|
|
361
|
+
for (const r of accountRows) {
|
|
362
|
+
if (r.room_id) touched.add(r.room_id);
|
|
363
|
+
accountData.set(r.room_id, [...accountData.get(r.room_id) ?? [], r]);
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
touched,
|
|
367
|
+
receipts,
|
|
368
|
+
accountData,
|
|
369
|
+
memberChanges
|
|
370
|
+
};
|
|
371
|
+
}
|
|
274
372
|
async function buildSync(hs, p) {
|
|
275
373
|
const userId = p.session.userId;
|
|
276
374
|
const since = p.since?.pos ?? null;
|
|
277
|
-
const
|
|
375
|
+
const incremental = since !== null && !p.fullState;
|
|
376
|
+
const deviceWhere = [{
|
|
278
377
|
field: "user_id",
|
|
279
378
|
value: userId
|
|
280
|
-
}
|
|
379
|
+
}, {
|
|
380
|
+
field: "device_id",
|
|
381
|
+
value: p.session.deviceId
|
|
382
|
+
}];
|
|
383
|
+
const acknowledged = p.since ? hs.db.delete("to_device", [...deviceWhere, {
|
|
384
|
+
field: "stream",
|
|
385
|
+
operator: "lte",
|
|
386
|
+
value: p.since.toDevice
|
|
387
|
+
}]) : Promise.resolve(0);
|
|
388
|
+
const [memberships, ignoredRows, toDeviceRows, oneTimeKeyRows, fallbackRows, accountRows = []] = await readBatch(hs.db, [
|
|
389
|
+
{
|
|
390
|
+
model: "memberships",
|
|
391
|
+
opts: { where: [{
|
|
392
|
+
field: "user_id",
|
|
393
|
+
value: userId
|
|
394
|
+
}] }
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
model: "account_data",
|
|
398
|
+
opts: { where: [
|
|
399
|
+
{
|
|
400
|
+
field: "user_id",
|
|
401
|
+
value: userId
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
field: "room_id",
|
|
405
|
+
value: ""
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
field: "type",
|
|
409
|
+
value: M.IGNORED_USER_LIST
|
|
410
|
+
}
|
|
411
|
+
] }
|
|
412
|
+
},
|
|
413
|
+
{
|
|
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
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
model: "one_time_keys",
|
|
430
|
+
opts: {
|
|
431
|
+
where: [...deviceWhere, {
|
|
432
|
+
field: "fallback",
|
|
433
|
+
value: false
|
|
434
|
+
}],
|
|
435
|
+
fields: ["algorithm"]
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
model: "fallback_keys",
|
|
440
|
+
opts: {
|
|
441
|
+
where: [...deviceWhere, {
|
|
442
|
+
field: "used",
|
|
443
|
+
value: false
|
|
444
|
+
}],
|
|
445
|
+
fields: ["algorithm"]
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
...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
|
+
}] }
|
|
458
|
+
}] : []
|
|
459
|
+
]);
|
|
460
|
+
p = {
|
|
461
|
+
...p,
|
|
462
|
+
ignored: new Set(Object.keys(ignoredRows[0]?.content?.ignored_users ?? {}))
|
|
463
|
+
};
|
|
464
|
+
const changes = incremental ? await changesSince(hs, p, memberships, since, accountRows) : null;
|
|
465
|
+
let memberChangesRead = changes ? Promise.resolve(changes.memberChanges) : null;
|
|
466
|
+
const memberChanges = () => memberChangesRead ??= hs.rooms.membershipChangesSince(userId, since ?? 0, memberships);
|
|
281
467
|
const join = {};
|
|
282
468
|
const invite = {};
|
|
283
469
|
const leave = {};
|
|
@@ -288,12 +474,13 @@ async function buildSync(hs, p) {
|
|
|
288
474
|
const roomId = m.room_id;
|
|
289
475
|
const changed = since === null || m.stream > since;
|
|
290
476
|
if (m.membership === "join") {
|
|
477
|
+
if (changes && !changed && !changes.touched.has(roomId) && hs.typing.typingSince(roomId, since) === null) continue;
|
|
291
478
|
if (!p.filter.room?.state?.lazy_load_members && await hs.partialState.isPartial(roomId)) continue;
|
|
292
479
|
const completedAt = await hs.partialState.completedAt(roomId);
|
|
293
480
|
const initial = since === null || p.fullState || m.stream > since || completedAt !== null && completedAt > since && completedAt <= p.to;
|
|
294
481
|
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);
|
|
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);
|
|
297
484
|
if (!initial && timelineEvents.length === 0 && state.length === 0 && stateAfter.length === 0 && ephemeral.length === 0 && roomAccountData.length === 0) continue;
|
|
298
485
|
join[roomId] = {
|
|
299
486
|
timeline,
|
|
@@ -341,68 +528,67 @@ async function buildSync(hs, p) {
|
|
|
341
528
|
leave[roomId] = {
|
|
342
529
|
timeline,
|
|
343
530
|
state: { events: state.map((e) => toClientEvent(e, { includeRoomId: false })) },
|
|
344
|
-
account_data: { events: await accountData(hs, p, roomId) }
|
|
531
|
+
account_data: { events: await accountData(hs, p, roomId, changes ? changes.accountData.get(roomId) ?? [] : void 0) }
|
|
345
532
|
};
|
|
346
533
|
empty = false;
|
|
347
534
|
}
|
|
348
535
|
}
|
|
349
|
-
const globalAccountData = await accountData(hs, p, "");
|
|
536
|
+
const globalAccountData = await accountData(hs, p, "", changes ? changes.accountData.get("") ?? [] : void 0);
|
|
350
537
|
if (since === null && !globalAccountData.some((e) => e.type === M.PUSH_RULES)) globalAccountData.push({
|
|
351
538
|
type: M.PUSH_RULES,
|
|
352
539
|
content: { global: await loadRules(hs, userId) }
|
|
353
540
|
});
|
|
354
541
|
if (globalAccountData.length) empty = false;
|
|
355
|
-
|
|
356
|
-
|
|
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);
|
|
542
|
+
await acknowledged;
|
|
543
|
+
const toDevice = toDeviceRows.filter((r) => r.stream <= p.to);
|
|
379
544
|
const nextToDevice = toDevice.length ? toDevice[toDevice.length - 1].stream : p.since?.toDevice ?? 0;
|
|
380
545
|
if (toDevice.length) empty = false;
|
|
381
546
|
const presenceEvents = [];
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
547
|
+
const wantPresence = !p.filter.presence?.types || p.filter.presence.types.length > 0;
|
|
548
|
+
const sharing = await hs.rooms.usersSharingRooms(userId);
|
|
549
|
+
const joinedRooms = memberships.filter((m) => m.membership === "join").map((m) => m.room_id);
|
|
550
|
+
const newlyShared = /* @__PURE__ */ new Set();
|
|
551
|
+
if (since !== null && wantPresence) {
|
|
552
|
+
const myRooms = new Set(joinedRooms);
|
|
553
|
+
for (const c of await memberChanges()) {
|
|
554
|
+
if (c.membership !== "join" || !myRooms.has(c.room_id)) continue;
|
|
555
|
+
if (c.user_id === userId) for (const u of await hs.rooms.joinedMembers(c.room_id)) newlyShared.add(u);
|
|
556
|
+
else newlyShared.add(c.user_id);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
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 ? [{
|
|
387
568
|
field: "stream",
|
|
388
569
|
operator: "gt",
|
|
389
570
|
value: since
|
|
390
|
-
}] }
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
571
|
+
}] : []] }
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
model: "presence",
|
|
575
|
+
opts: { where: [{
|
|
576
|
+
field: "user_id",
|
|
577
|
+
operator: "in",
|
|
578
|
+
value: [...newlyShared]
|
|
579
|
+
}] }
|
|
580
|
+
},
|
|
581
|
+
...since !== null ? [{
|
|
582
|
+
model: "device_list_changes",
|
|
583
|
+
opts: deviceListChangesQuery(sharing, since)
|
|
584
|
+
}] : []
|
|
585
|
+
]);
|
|
586
|
+
if (wantPresence) {
|
|
587
|
+
const seen = /* @__PURE__ */ new Set();
|
|
588
|
+
for (const row of [...presenceRows, ...newlySharedPresence]) {
|
|
589
|
+
if (seen.has(row.user_id)) continue;
|
|
590
|
+
seen.add(row.user_id);
|
|
404
591
|
if (row.stream > p.to) continue;
|
|
405
|
-
if (since !== null && row.stream <= since && !newlyShared.has(row.user_id)) continue;
|
|
406
592
|
const content = {
|
|
407
593
|
presence: row.presence,
|
|
408
594
|
last_active_ago: Date.now() - row.last_active_ts
|
|
@@ -418,7 +604,7 @@ async function buildSync(hs, p) {
|
|
|
418
604
|
}
|
|
419
605
|
}
|
|
420
606
|
if (presenceEvents.length) empty = false;
|
|
421
|
-
const profileUpdates = await profileUpdatesFor(hs, p, memberships);
|
|
607
|
+
const profileUpdates = await profileUpdatesFor(hs, p, memberships, memberChanges);
|
|
422
608
|
if (profileUpdates) empty = false;
|
|
423
609
|
const body = {
|
|
424
610
|
next_batch: formatToken({
|
|
@@ -438,12 +624,16 @@ async function buildSync(hs, p) {
|
|
|
438
624
|
type: r.type,
|
|
439
625
|
content: r.content
|
|
440
626
|
})) },
|
|
441
|
-
device_one_time_keys_count:
|
|
442
|
-
device_unused_fallback_key_types:
|
|
627
|
+
device_one_time_keys_count: countAlgorithms(oneTimeKeyRows),
|
|
628
|
+
device_unused_fallback_key_types: [...new Set(fallbackRows.map((r) => r.algorithm))]
|
|
443
629
|
};
|
|
444
630
|
if (profileUpdates) body[UNSTABLE.ORG.MATRIX.MSC4429.USERS] = profileUpdates;
|
|
445
631
|
if (since !== null) {
|
|
446
|
-
const lists = await hs.keys.changesSince(userId, since, p.to
|
|
632
|
+
const lists = await hs.keys.changesSince(userId, since, p.to, {
|
|
633
|
+
memberChanges: await memberChanges(),
|
|
634
|
+
deviceRows,
|
|
635
|
+
joinedRooms
|
|
636
|
+
});
|
|
447
637
|
body.device_lists = lists;
|
|
448
638
|
if (lists.changed.length || lists.left.length) empty = false;
|
|
449
639
|
}
|
|
@@ -453,6 +643,12 @@ async function buildSync(hs, p) {
|
|
|
453
643
|
nextToDevice
|
|
454
644
|
};
|
|
455
645
|
}
|
|
646
|
+
const slots = /* @__PURE__ */ new WeakMap();
|
|
647
|
+
function syncSlots(hs) {
|
|
648
|
+
let s = slots.get(hs);
|
|
649
|
+
if (!s) slots.set(hs, s = new Semaphore(hs.config.maxConcurrentSyncs));
|
|
650
|
+
return s;
|
|
651
|
+
}
|
|
456
652
|
async function sync(ctx) {
|
|
457
653
|
const hs = ctx.hs;
|
|
458
654
|
const session = await ctx.auth();
|
|
@@ -465,33 +661,21 @@ async function sync(ctx) {
|
|
|
465
661
|
const deadline = Date.now() + timeout;
|
|
466
662
|
for (;;) {
|
|
467
663
|
const to = await hs.streams.position();
|
|
468
|
-
const
|
|
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 ?? {}));
|
|
664
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
483
665
|
const stable = ctx.query.get("use_state_after") === "true";
|
|
484
666
|
const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
|
|
485
|
-
const
|
|
667
|
+
const useStateAfter = stable || unstable;
|
|
668
|
+
const stateAfterKey = stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER;
|
|
669
|
+
const { body, empty } = await syncSlots(hs).run(() => buildSync(hs, {
|
|
486
670
|
session,
|
|
487
671
|
ignored,
|
|
488
672
|
since,
|
|
489
673
|
to,
|
|
490
674
|
filter,
|
|
491
675
|
fullState,
|
|
492
|
-
useStateAfter
|
|
493
|
-
stateAfterKey
|
|
494
|
-
});
|
|
676
|
+
useStateAfter,
|
|
677
|
+
stateAfterKey
|
|
678
|
+
}));
|
|
495
679
|
const remaining = deadline - Date.now();
|
|
496
680
|
if (!since || !empty || remaining <= 0 || fullState) return body;
|
|
497
681
|
if (await hs.streams.position() > to) continue;
|
|
@@ -499,6 +683,7 @@ async function sync(ctx) {
|
|
|
499
683
|
if (ctx.request.signal?.aborted) return body;
|
|
500
684
|
}
|
|
501
685
|
}
|
|
686
|
+
const LAST_ACTIVE_GRANULARITY_MS = 6e4;
|
|
502
687
|
async function touchPresence(hs, userId, presence) {
|
|
503
688
|
const state = presence === "unavailable" ? "unavailable" : "online";
|
|
504
689
|
const row = await hs.db.findOne("presence", [{
|
|
@@ -506,7 +691,7 @@ async function touchPresence(hs, userId, presence) {
|
|
|
506
691
|
value: userId
|
|
507
692
|
}]);
|
|
508
693
|
if (row && row.presence === state) {
|
|
509
|
-
await hs.db.update("presence", [{
|
|
694
|
+
if (Date.now() - row.last_active_ts >= LAST_ACTIVE_GRANULARITY_MS) await hs.db.update("presence", [{
|
|
510
695
|
field: "user_id",
|
|
511
696
|
value: userId
|
|
512
697
|
}], { last_active_ts: Date.now() });
|
|
@@ -564,7 +749,7 @@ function registerSyncRoutes(r) {
|
|
|
564
749
|
* MSC4429: profile changes of users sharing a room, limited to the fields the filter asks for.
|
|
565
750
|
* The initial sync carries current values; users we no longer share any room with get `null`.
|
|
566
751
|
*/
|
|
567
|
-
async function profileUpdatesFor(hs, p, memberships) {
|
|
752
|
+
async function profileUpdatesFor(hs, p, memberships, memberChanges) {
|
|
568
753
|
const ids = (p.filter.profile_fields ?? p.filter[UNSTABLE.ORG.MATRIX.MSC4429.PROFILE_FIELDS])?.ids;
|
|
569
754
|
if (!Array.isArray(ids) || !ids.length) return null;
|
|
570
755
|
const wanted = new Set(ids.filter((x) => typeof x === "string"));
|
|
@@ -577,11 +762,7 @@ async function profileUpdatesFor(hs, p, memberships) {
|
|
|
577
762
|
const full = new Set(since === null ? sharing : []);
|
|
578
763
|
if (since !== null) {
|
|
579
764
|
const myRooms = new Set(memberships.filter((m) => m.membership === "join").map((m) => m.room_id));
|
|
580
|
-
const changes = await
|
|
581
|
-
field: "stream",
|
|
582
|
-
operator: "gt",
|
|
583
|
-
value: since
|
|
584
|
-
}] });
|
|
765
|
+
const changes = await memberChanges();
|
|
585
766
|
for (const c of changes) {
|
|
586
767
|
if (c.stream > p.to || c.user_id === p.session.userId) continue;
|
|
587
768
|
if (c.membership === "join" && myRooms.has(c.room_id)) {
|
package/dist/core/cluster.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
//#region src/core/cluster.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* Coordination between homeserver instances sharing one database. The default
|
|
4
|
-
* (`localCluster()`) keeps everything in process; use `postgresCluster()`
|
|
5
|
-
*
|
|
4
|
+
* (`localCluster()`) keeps everything in process; use `postgresCluster()` to run several
|
|
5
|
+
* instances behind a load balancer.
|
|
6
6
|
*/
|
|
7
7
|
export interface ClusterAdapter {
|
|
8
8
|
/** Allocate the next value of a cluster-wide, strictly increasing sequence. */
|
|
@@ -44,13 +44,20 @@ export declare class StreamManager {
|
|
|
44
44
|
private cluster;
|
|
45
45
|
private instanceId;
|
|
46
46
|
private inflight;
|
|
47
|
-
|
|
48
|
-
private
|
|
47
|
+
/** Allocations waiting for a publish that covers them. */
|
|
48
|
+
private reserving;
|
|
49
|
+
/** Allocations whose publish is done, asking the sequence for their position now. */
|
|
50
|
+
private allocating;
|
|
51
|
+
/** The position of the last published busy state: below every position still being allocated. */
|
|
52
|
+
private lastPos;
|
|
49
53
|
private heartbeat;
|
|
50
54
|
private positionCache;
|
|
55
|
+
private publishing;
|
|
56
|
+
private nextPublish;
|
|
51
57
|
constructor(cluster: ClusterAdapter, instanceId: string);
|
|
52
58
|
private key;
|
|
53
|
-
|
|
59
|
+
/** Publish this writer's state as it is when the publish starts; concurrent callers share it. */
|
|
60
|
+
private flush;
|
|
54
61
|
/** Allocate a position. The caller must call `done()` once its writes are committed. */
|
|
55
62
|
allocate(): Promise<StreamLease>;
|
|
56
63
|
/** Allocate a position, run `fn` with it, then complete it. */
|
package/dist/core/cluster.js
CHANGED
|
@@ -91,38 +91,64 @@ var StreamManager = class {
|
|
|
91
91
|
cluster;
|
|
92
92
|
instanceId;
|
|
93
93
|
inflight = /* @__PURE__ */ new Set();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
/** Allocations waiting for a publish that covers them. */
|
|
95
|
+
reserving = 0;
|
|
96
|
+
/** Allocations whose publish is done, asking the sequence for their position now. */
|
|
97
|
+
allocating = 0;
|
|
98
|
+
/** The position of the last published busy state: below every position still being allocated. */
|
|
99
|
+
lastPos = 0;
|
|
99
100
|
heartbeat;
|
|
100
101
|
positionCache = null;
|
|
102
|
+
publishing = Promise.resolve();
|
|
103
|
+
nextPublish = null;
|
|
101
104
|
constructor(cluster, instanceId) {
|
|
102
105
|
this.cluster = cluster;
|
|
103
106
|
this.instanceId = instanceId;
|
|
104
|
-
this.heartbeat = setInterval(() => void this.
|
|
107
|
+
this.heartbeat = setInterval(() => void this.flush().catch(() => {}), WRITER_TTL_MS / 3);
|
|
105
108
|
this.heartbeat.unref?.();
|
|
106
109
|
}
|
|
107
110
|
key() {
|
|
108
111
|
return `writer:${this.instanceId}`;
|
|
109
112
|
}
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
/** Publish this writer's state as it is when the publish starts; concurrent callers share it. */
|
|
114
|
+
flush() {
|
|
115
|
+
if (this.nextPublish) return this.nextPublish;
|
|
116
|
+
const run = async () => {
|
|
117
|
+
this.nextPublish = null;
|
|
118
|
+
const covered = this.reserving;
|
|
119
|
+
const earlier = this.allocating;
|
|
120
|
+
this.reserving = 0;
|
|
121
|
+
this.allocating += covered;
|
|
122
|
+
const bounds = [];
|
|
123
|
+
if (this.inflight.size) bounds.push(Math.min(...this.inflight) - 1);
|
|
124
|
+
if (earlier) bounds.push(this.lastPos);
|
|
125
|
+
if (covered) bounds.push(await this.cluster.currentId("stream"));
|
|
126
|
+
const state = bounds.length ? {
|
|
127
|
+
busy: true,
|
|
128
|
+
pos: Math.min(...bounds)
|
|
129
|
+
} : {
|
|
130
|
+
busy: false,
|
|
131
|
+
pos: 0
|
|
132
|
+
};
|
|
133
|
+
if (state.busy) this.lastPos = state.pos;
|
|
134
|
+
await this.cluster.set(this.key(), state, WRITER_TTL_MS);
|
|
135
|
+
};
|
|
136
|
+
const p = this.publishing.then(run, run);
|
|
137
|
+
this.nextPublish = p;
|
|
138
|
+
this.publishing = p.catch(() => {});
|
|
139
|
+
return p;
|
|
112
140
|
}
|
|
113
141
|
/** Allocate a position. The caller must call `done()` once its writes are committed. */
|
|
114
142
|
async allocate() {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
const id = await this.cluster.nextId("stream");
|
|
125
|
-
this.inflight.add(id);
|
|
143
|
+
this.reserving++;
|
|
144
|
+
let id;
|
|
145
|
+
try {
|
|
146
|
+
await this.flush();
|
|
147
|
+
id = await this.cluster.nextId("stream");
|
|
148
|
+
this.inflight.add(id);
|
|
149
|
+
} finally {
|
|
150
|
+
this.allocating--;
|
|
151
|
+
}
|
|
126
152
|
let finished = false;
|
|
127
153
|
const timer = setTimeout(() => void lease.done(), 6e4);
|
|
128
154
|
timer.unref?.();
|
|
@@ -133,16 +159,7 @@ var StreamManager = class {
|
|
|
133
159
|
finished = true;
|
|
134
160
|
clearTimeout(timer);
|
|
135
161
|
this.inflight.delete(id);
|
|
136
|
-
await
|
|
137
|
-
this.published = this.inflight.size ? {
|
|
138
|
-
busy: true,
|
|
139
|
-
pos: Math.min(...this.inflight) - 1
|
|
140
|
-
} : {
|
|
141
|
-
busy: false,
|
|
142
|
-
pos: await this.cluster.currentId("stream")
|
|
143
|
-
};
|
|
144
|
-
await this.publishState();
|
|
145
|
-
}));
|
|
162
|
+
await this.flush();
|
|
146
163
|
this.positionCache = null;
|
|
147
164
|
}
|
|
148
165
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/core/html.ts
|
|
2
|
+
/** Escape text for use in HTML content and quoted attributes. */
|
|
3
|
+
function escapeHtml(s) {
|
|
4
|
+
return s.replace(/[&<>"']/g, (c) => ({
|
|
5
|
+
"&": "&",
|
|
6
|
+
"<": "<",
|
|
7
|
+
">": ">",
|
|
8
|
+
"\"": """,
|
|
9
|
+
"'": "'"
|
|
10
|
+
})[c]);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { escapeHtml };
|
package/dist/core/identity.js
CHANGED
|
@@ -18,7 +18,7 @@ var IdentityServers = class {
|
|
|
18
18
|
if (opts.signed) headers.authorization = this.hs.federation.client.authHeader(idServer, method, path, opts.body);
|
|
19
19
|
let res;
|
|
20
20
|
try {
|
|
21
|
-
res = await this.hs.
|
|
21
|
+
res = await this.hs.publicFetch(`${this.base(idServer)}${path}`, {
|
|
22
22
|
method,
|
|
23
23
|
headers,
|
|
24
24
|
body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body),
|