@pairlobby/server-core 0.23.0 → 0.25.0

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/service.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_ROOM_POLICY } from '@pairlobby/protocol';
2
- import type { AdapterCapabilities, Attachment, AutoClosePolicy, FileRecord, MessageRequest, RequestPage, ExportResponse, ListFilesResponse, ParticipantKind, PermissionChange, RenameSelfRequest, NameSource, ParticipantRole, ReadEventsResponse, RelayLimits, RoomEvent, RoomInvitationChange, RoomPolicy, RoomSnapshot, ResolveRequestRequest, SendEventRequest } from '@pairlobby/protocol';
2
+ import type { AdapterCapabilities, Attachment, AutoClosePolicy, FileRecord, MessageRequest, RequestPage, ExportResponse, ListFilesResponse, ParticipantKind, PermissionChange, RenameSelfRequest, NameSource, ParticipantRole, ReadEventsResponse, ReceiptMode, ReceiptResponse, ReceiptUpdate, RelayLimits, RoomRecord, RoomEvent, RoomInvitationChange, RoomPolicy, RoomSnapshot, ResolveRequestRequest, SendEventRequest } from '@pairlobby/protocol';
3
3
  import { TurnCoordinator } from './turns.js';
4
4
  import type { RoomStore } from './store.js';
5
5
  type InviteDirectory = {
@@ -39,6 +39,8 @@ export interface Identity {
39
39
  kind: ParticipantKind;
40
40
  sessionId?: string | undefined;
41
41
  capabilities?: AdapterCapabilities | undefined;
42
+ /** The relay member joining, verified by the relay from a personal token. Never taken from a client request. */
43
+ member?: string | undefined;
42
44
  }
43
45
  export interface CreateRoomInput extends Identity {
44
46
  name: string;
@@ -51,6 +53,8 @@ export interface CreateRoomInput extends Identity {
51
53
  * relay with one Durable Object per room. Never taken from a client request.
52
54
  */
53
55
  roomId?: string;
56
+ /** The relay member creating the room, verified by the relay from a personal token. Never taken from a client request. */
57
+ owner?: string | undefined;
54
58
  }
55
59
  export interface CreatedRoom {
56
60
  roomId: string;
@@ -87,6 +91,18 @@ export type Clock = () => number;
87
91
  export type RelayProfile = {
88
92
  files: boolean;
89
93
  limits: RelayLimits;
94
+ receiptMode: ReceiptMode;
95
+ };
96
+ export type RoomServiceOptions = {
97
+ /** How plain receipts are kept (`RECEIPT_MODES`); `both` until every client in use reads watermarks. */
98
+ receiptMode?: ReceiptMode;
99
+ };
100
+ /** The room with this participant's watermark moved up to `throughSeq`, never past the last event; the same room when it would not move. */
101
+ export declare function withReceipt(room: RoomRecord, participantId: string, throughSeq: number, at: number): RoomRecord;
102
+ /** The watermarks that moved after `after`, for a read that asked for them. */
103
+ export declare function receiptsSince(room: RoomRecord, after: number): {
104
+ receipts: ReceiptUpdate[];
105
+ receiptRevision: number;
90
106
  };
91
107
  /** The limits a new room gets under `policy`, as a relay reports them; file limits only where files are kept. */
92
108
  export declare function relayLimits(policy: RoomPolicy, files: boolean): RelayLimits;
@@ -96,11 +112,12 @@ export declare class RoomService {
96
112
  private readonly store;
97
113
  private readonly now;
98
114
  readonly turns: TurnCoordinator;
115
+ readonly receiptMode: ReceiptMode;
99
116
  /**
100
117
  * `roomPolicy` is what a new room gets when the caller names no policy, such as a
101
118
  * self-hosted relay's configured limits. Existing rooms keep the policy they were made with.
102
119
  */
103
- constructor(store: RoomStore, now?: Clock, directory?: InviteDirectory | undefined, roomPolicy?: RoomPolicy);
120
+ constructor(store: RoomStore, now?: Clock, directory?: InviteDirectory | undefined, roomPolicy?: RoomPolicy, options?: RoomServiceOptions);
104
121
  private ctx;
105
122
  private view;
106
123
  /** What the room looks like, plus what this relay can do that depends on its store rather than on the room. */
@@ -126,9 +143,24 @@ export declare class RoomService {
126
143
  * crash between the two steps leaves a resumable reservation rather than an
127
144
  * invite a different stranger can claim.
128
145
  */
146
+ /** The room an invite's digest belongs to, for routing before the room checks the code; null when no invite has it. */
147
+ inviteRoom(digest: string): Promise<string | null>;
129
148
  redeemInvite(input: RedeemInput): Promise<RedeemResult>;
130
149
  send(roomId: string, credential: string, request: SendEventRequest): Promise<SentEventResult>;
131
150
  acknowledgeMessage(roomId: string, credential: string, eventId: string): Promise<MessageRequest | null>;
151
+ /**
152
+ * `POST /v1/rooms/:id/receipts`: this participant has received every event up to `throughSeq`.
153
+ * Moves their watermark and marks their requests up to there received, with no event.
154
+ */
155
+ receipt(roomId: string, credential: string, throughSeq: number): Promise<ReceiptResponse>;
156
+ /** A plain receipt with no event: the request it answers when it is addressed to this participant, else their watermark. */
157
+ private plainReceipt;
158
+ /** Writes the moved watermark and the requests it marks received, in one write without an event. */
159
+ private moveWatermark;
160
+ /** This participant's requests whose message is at or before `throughSeq` and not yet received, marked received at `now`. */
161
+ private receivedThrough;
162
+ /** The transcript seq of a request's message; 0 when that message is no longer retained, which puts it behind any watermark. */
163
+ private messageSeqOf;
132
164
  private publicRequest;
133
165
  requests(roomId: string, credential: string, after?: number, limit?: number, recipientId?: string, supportsTurns?: boolean): Promise<RequestPage>;
134
166
  request(roomId: string, credential: string, eventId: string): Promise<MessageRequest>;
@@ -174,7 +206,7 @@ export declare class RoomService {
174
206
  */
175
207
  closeDueRooms(limit?: number): Promise<AutoCloseSweep>;
176
208
  close(roomId: string, credential: string): Promise<RoomEvent>;
177
- read(roomId: string, credential: string, after: number, limit: number): Promise<ReadEventsResponse>;
209
+ read(roomId: string, credential: string, after: number, limit: number, receiptsAfter?: number): Promise<ReadEventsResponse>;
178
210
  /** Older group events kept the all flag only in their canonical idempotency request. */
179
211
  private restoreAudience;
180
212
  snapshot(roomId: string, credential: string): Promise<RoomSnapshot>;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
package/dist/service.js CHANGED
@@ -1,10 +1,30 @@
1
1
  //! Room operations shared by every transport. The Cloudflare Durable Object and
2
2
  //! the Node server both run this; only the store and the socket layer differ.
3
- import { ABSOLUTE_MAX_FILE_BYTES, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_RETAINED_FILE_BYTES, DEFAULT_ROOM_POLICY, ProtocolError, UNATTACHED_FILE_LIFETIME_MS, formatFileSize, hashCredential, newId, newInviteCode, normalizeInviteCode, safeFileName } from '@pairlobby/protocol';
3
+ import { ABSOLUTE_MAX_FILE_BYTES, DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_RETAINED_FILE_BYTES, DEFAULT_ROOM_POLICY, PROTOCOL_VERSION, ProtocolError, UNATTACHED_FILE_LIFETIME_MS, formatFileSize, hashCredential, newId, newInviteCode, normalizeInviteCode, safeFileName } from '@pairlobby/protocol';
4
4
  import { assertRoomWritable, appendEvent, assertRoomJoinable, assertCanWrite, assertCanAcknowledge, assertActiveMember, assertController, assertPermitted, authenticate, closeRoom, createRoom, joinAsGuest, joinOnLocalNetwork, recordInvitation, joinRoom, leaveRoom, rejoinRoom, renameRoom, renameSelf, requestControl, revokeParticipant, sendEvent, setExpiry, setAutoClose, setInviteRole, setJoinPolicy, setLocalJoin, setLocked, setMuted, setParticipantPermissions, setParticipantRole, setRoomPermissions, emptyMutation, toSnapshot } from '@pairlobby/room-core';
5
5
  import { stableStringify } from './stable-json.js';
6
6
  import { TurnCoordinator, assertTurn } from './turns.js';
7
7
  import { AutoCloseStore } from './auto-close-store.js';
8
+ /** Requests a watermark marks received in one write; the rest wait for the next one. */
9
+ const WATERMARK_REQUEST_LIMIT = 2000;
10
+ /** A receipt that only says the message arrived: no Read stage, no action. */
11
+ function isPlainReceipt(request) {
12
+ return request.type === 'message.received' && !request.payload.stage && !request.payload.action;
13
+ }
14
+ /** The room with this participant's watermark moved up to `throughSeq`, never past the last event; the same room when it would not move. */
15
+ export function withReceipt(room, participantId, throughSeq, at) {
16
+ const through = Math.min(throughSeq, room.nextSeq - 1);
17
+ if (through <= (room.receipts?.[participantId]?.throughSeq ?? 0)) {
18
+ return room;
19
+ }
20
+ const revision = (room.receiptRevision ?? 0) + 1;
21
+ return { ...room, receipts: { ...room.receipts, [participantId]: { throughSeq: through, revision, at } }, receiptRevision: revision };
22
+ }
23
+ /** The watermarks that moved after `after`, for a read that asked for them. */
24
+ export function receiptsSince(room, after) {
25
+ const receipts = Object.entries(room.receipts ?? {}).filter(([, mark]) => mark.revision > after).map(([participantId, mark]) => ({ participantId, ...mark }));
26
+ return { receipts: receipts.sort((a, b) => a.revision - b.revision), receiptRevision: room.receiptRevision ?? 0 };
27
+ }
8
28
  /** The limits a new room gets under `policy`, as a relay reports them; file limits only where files are kept. */
9
29
  export function relayLimits(policy, files) {
10
30
  return {
@@ -21,14 +41,16 @@ export class RoomService {
21
41
  store;
22
42
  now;
23
43
  turns;
44
+ receiptMode;
24
45
  /**
25
46
  * `roomPolicy` is what a new room gets when the caller names no policy, such as a
26
47
  * self-hosted relay's configured limits. Existing rooms keep the policy they were made with.
27
48
  */
28
- constructor(store, now = () => Date.now(), directory, roomPolicy = DEFAULT_ROOM_POLICY) {
49
+ constructor(store, now = () => Date.now(), directory, roomPolicy = DEFAULT_ROOM_POLICY, options = {}) {
29
50
  this.directory = directory;
30
51
  this.roomPolicy = roomPolicy;
31
52
  this.now = now;
53
+ this.receiptMode = options.receiptMode ?? 'both';
32
54
  this.store = new AutoCloseStore(store, () => this.now());
33
55
  this.turns = new TurnCoordinator(this.store, now);
34
56
  }
@@ -44,12 +66,12 @@ export class RoomService {
44
66
  }
45
67
  /** What the room looks like, plus what this relay can do that depends on its store rather than on the room. */
46
68
  snapshotOf(view) {
47
- return { ...toSnapshot(view), filesSupported: this.store.files !== undefined };
69
+ return { ...toSnapshot(view), filesSupported: this.store.files !== undefined, receiptMode: this.receiptMode };
48
70
  }
49
71
  /** Whether rooms here keep files, and the limits a new room gets: what `GET /v1/server` reports. */
50
72
  relayProfile() {
51
73
  const files = this.store.files !== undefined;
52
- return { files, limits: relayLimits(this.roomPolicy, files) };
74
+ return { files, limits: relayLimits(this.roomPolicy, files), receiptMode: this.receiptMode };
53
75
  }
54
76
  fileStore() {
55
77
  if (!this.store.files) {
@@ -147,6 +169,8 @@ export class RoomService {
147
169
  nameSource: input.nameSource,
148
170
  sessionId: input.sessionId ?? null,
149
171
  capabilities: input.capabilities ?? null,
172
+ ...(input.member ? { member: input.member } : {}),
173
+ ...(input.owner ? { owner: input.owner } : {}),
150
174
  ...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
151
175
  policy: input.policy ?? this.roomPolicy
152
176
  }, this.ctx());
@@ -205,6 +229,10 @@ export class RoomService {
205
229
  * crash between the two steps leaves a resumable reservation rather than an
206
230
  * invite a different stranger can claim.
207
231
  */
232
+ /** The room an invite's digest belongs to, for routing before the room checks the code; null when no invite has it. */
233
+ async inviteRoom(digest) {
234
+ return (await this.store.inviteByDigest(digest))?.roomId ?? null;
235
+ }
208
236
  async redeemInvite(input) {
209
237
  const normalized = normalizeInviteCode(input.code);
210
238
  if (!normalized) {
@@ -274,7 +302,8 @@ export class RoomService {
274
302
  kind: input.kind,
275
303
  nameSource: input.nameSource,
276
304
  sessionId: input.sessionId ?? null,
277
- capabilities: input.capabilities ?? null
305
+ capabilities: input.capabilities ?? null,
306
+ ...(input.member ? { member: input.member } : {})
278
307
  }, this.ctx());
279
308
  if (previousOccupant?.muted) {
280
309
  joined.participant.muted = true;
@@ -304,6 +333,13 @@ export class RoomService {
304
333
  const target = await this.turns.resolve(roomId, request.payload.eventId, participant.participantId);
305
334
  const eventId = target?.conversationId ?? request.payload.eventId;
306
335
  request = { ...request, payload: { ...request.payload, eventId }, ...(!request.payload.action ? { idempotencyKey: `${request.payload.stage === 'read' ? 'read' : 'receipt'}-${eventId}-${participant.participantId}` } : {}) };
336
+ if (this.receiptMode === 'watermark' && isPlainReceipt(request)) {
337
+ // An older client's plain receipt: kept as the watermark, answered as an event that already existed.
338
+ assertRoomWritable(view, now);
339
+ await this.plainReceipt(view, participant, eventId, now);
340
+ const event = { protocolVersion: PROTOCOL_VERSION, roomId, seq: Math.max(1, view.room.nextSeq - 1), eventId: newId('event'), senderId: participant.participantId, idempotencyKey: request.idempotencyKey, recipientId: null, replyTo: null, at: now, type: 'message.received', payload: request.payload };
341
+ return { event, deduplicated: true };
342
+ }
307
343
  }
308
344
  const requestDigest = stableStringify({ type: request.type, payload: request.payload, recipientId: request.recipientId ?? null, replyTo: request.replyTo ?? null,
309
345
  ...(request.recipientIds ? { recipientIds: request.recipientIds } : {}), ...(request.allRecipients ? { allRecipients: true } : {}), ...(request.quoteOf ? { quoteOf: request.quoteOf } : {}) });
@@ -369,6 +405,8 @@ export class RoomService {
369
405
  }
370
406
  const updates = [];
371
407
  let replyTarget = null;
408
+ // A plain receipt for a message not addressed to this participant also moves their watermark, in `both`.
409
+ let watermarkSeq = null;
372
410
  if (request.type === 'message.received') {
373
411
  const participant = assertCanAcknowledge(actor);
374
412
  const target = await this.turns.resolve(roomId, request.payload.eventId, participant.participantId);
@@ -379,6 +417,9 @@ export class RoomService {
379
417
  if ((target?.from ?? original?.senderId) === participant.participantId) {
380
418
  throw new ProtocolError('unauthorized', 'a sender cannot acknowledge their own message');
381
419
  }
420
+ if (this.receiptMode === 'both' && isPlainReceipt(request) && target?.to !== participant.participantId) {
421
+ watermarkSeq = original?.seq ?? (await this.messageSeqOf(roomId, target));
422
+ }
382
423
  if (target?.to === participant.participantId) {
383
424
  updates.push({ ...target, receivedAt: target.receivedAt ?? now, ...(request.payload.stage === 'read' ? { readAt: target.readAt ?? now } : {}) });
384
425
  }
@@ -488,6 +529,9 @@ export class RoomService {
488
529
  }
489
530
  const outgoing = replyTarget?.conversationId ? { ...request, replyTo: replyTarget.conversationId } : request;
490
531
  const mutation = sendEvent(view, await hashCredential(credential), outgoing, { now, newEventId: () => newId('event') });
532
+ if (watermarkSeq !== null && participantId) {
533
+ mutation.room = withReceipt(mutation.room, participantId, watermarkSeq, now);
534
+ }
491
535
  if (attached.length) {
492
536
  mutation.attachFiles = attached;
493
537
  }
@@ -524,7 +568,7 @@ export class RoomService {
524
568
  let seq = Math.max(view.room.nextRequestSeq ?? 0, mutation.appendEvent.seq);
525
569
  for (const recipient of recipients) {
526
570
  updates.push({
527
- roomId, eventId: group ? newId('event') : mutation.appendEvent.eventId, seq: seq++,
571
+ roomId, eventId: group ? newId('event') : mutation.appendEvent.eventId, seq: seq++, messageSeq: mutation.appendEvent.seq,
528
572
  from: mutation.appendEvent.senderId, to: recipient, text: quoteContext + request.payload.text, at: mutation.appendEvent.at,
529
573
  requiresReply: !request.replyTo, receivedAt: null, responseEventId: null, respondedAt: null, progressAt: null,
530
574
  ...(group ? { conversationId: mutation.appendEvent.eventId, turnRequired: true } : {}),
@@ -542,12 +586,20 @@ export class RoomService {
542
586
  return { event: mutation.appendEvent, deduplicated: false };
543
587
  }
544
588
  async acknowledgeMessage(roomId, credential, eventId) {
545
- const actor = authenticate(await this.view(roomId), await hashCredential(credential), this.now());
589
+ const view = await this.view(roomId);
590
+ const now = this.now();
591
+ const actor = authenticate(view, await hashCredential(credential), now);
546
592
  const participant = assertCanAcknowledge(actor);
547
593
  const target = await this.turns.resolve(roomId, eventId, participant.participantId);
548
594
  if (target?.to === participant.participantId && target.receivedAt !== null) {
549
595
  return this.publicRequest(target);
550
596
  }
597
+ if (this.receiptMode === 'watermark') {
598
+ assertRoomWritable(view, now);
599
+ await this.plainReceipt(view, participant, eventId, now);
600
+ const received = await this.turns.resolve(roomId, eventId, participant.participantId);
601
+ return received ? this.publicRequest(received) : null;
602
+ }
551
603
  const root = target?.conversationId ?? eventId;
552
604
  const key = `receipt-${root}-${participant.participantId}`;
553
605
  const saved = await this.store.idempotencyRecord(roomId, key);
@@ -561,6 +613,81 @@ export class RoomService {
561
613
  const updated = await this.turns.resolve(roomId, eventId, participant.participantId);
562
614
  return updated ? this.publicRequest(updated) : null;
563
615
  }
616
+ /**
617
+ * `POST /v1/rooms/:id/receipts`: this participant has received every event up to `throughSeq`.
618
+ * Moves their watermark and marks their requests up to there received, with no event.
619
+ */
620
+ async receipt(roomId, credential, throughSeq) {
621
+ if (this.receiptMode === 'events') {
622
+ throw new ProtocolError('unsupported_capability', 'this relay keeps receipts as events; acknowledge each message instead');
623
+ }
624
+ const view = await this.view(roomId);
625
+ const now = this.now();
626
+ const participant = assertCanAcknowledge(authenticate(view, await hashCredential(credential), now));
627
+ assertRoomWritable(view, now);
628
+ const room = await this.moveWatermark(view, participant.participantId, throughSeq, now);
629
+ return { throughSeq: room.receipts?.[participant.participantId]?.throughSeq ?? 0, receiptRevision: room.receiptRevision ?? 0 };
630
+ }
631
+ /** A plain receipt with no event: the request it answers when it is addressed to this participant, else their watermark. */
632
+ async plainReceipt(view, participant, eventId, now) {
633
+ const roomId = view.room.roomId;
634
+ const target = await this.turns.resolve(roomId, eventId, participant.participantId);
635
+ const original = target ? null : await this.store.eventById(roomId, eventId);
636
+ if (!target && original?.type !== 'message') {
637
+ throw new ProtocolError('invalid_request', 'no such retained message in this room');
638
+ }
639
+ if ((target?.from ?? original?.senderId) === participant.participantId) {
640
+ throw new ProtocolError('unauthorized', 'a sender cannot acknowledge their own message');
641
+ }
642
+ if (target?.to === participant.participantId) {
643
+ // A receiver may confirm one request out of order, so it says nothing about earlier messages.
644
+ if (target.receivedAt === null) {
645
+ await this.store.updateTurns(view.room, [{ ...target, receivedAt: now }], view.room.turnRevision ?? 0);
646
+ }
647
+ return;
648
+ }
649
+ // Everything else is confirmed by a reader going through the room in order.
650
+ await this.moveWatermark(view, participant.participantId, original?.seq ?? (await this.messageSeqOf(roomId, target)), now);
651
+ }
652
+ /** Writes the moved watermark and the requests it marks received, in one write without an event. */
653
+ async moveWatermark(view, participantId, throughSeq, now) {
654
+ const room = withReceipt(view.room, participantId, throughSeq, now);
655
+ if (room === view.room) {
656
+ return room;
657
+ }
658
+ const requests = await this.receivedThrough(room.roomId, participantId, room.receipts[participantId].throughSeq, now);
659
+ await this.store.updateTurns(room, requests, view.room.turnRevision ?? 0);
660
+ return room;
661
+ }
662
+ /** This participant's requests whose message is at or before `throughSeq` and not yet received, marked received at `now`. */
663
+ async receivedThrough(roomId, participantId, throughSeq, now) {
664
+ const page = (after) => this.store.unreceivedRequests ? this.store.unreceivedRequests(roomId, participantId, after, 200) : this.store.messageRequests(roomId, after, 200, participantId);
665
+ const marked = [];
666
+ let after = 0;
667
+ while (marked.length < WATERMARK_REQUEST_LIMIT) {
668
+ const { requests, hasMore } = await page(after);
669
+ for (const request of requests) {
670
+ if (request.receivedAt !== null) {
671
+ continue;
672
+ }
673
+ // Requests are numbered in message order, so the first one past the watermark ends the search.
674
+ if ((await this.messageSeqOf(roomId, request)) > throughSeq) {
675
+ return marked;
676
+ }
677
+ marked.push({ ...request, receivedAt: now });
678
+ }
679
+ const last = requests.at(-1)?.seq;
680
+ if (!hasMore || last === undefined || last <= after) {
681
+ return marked;
682
+ }
683
+ after = last;
684
+ }
685
+ return marked;
686
+ }
687
+ /** The transcript seq of a request's message; 0 when that message is no longer retained, which puts it behind any watermark. */
688
+ async messageSeqOf(roomId, request) {
689
+ return request.messageSeq ?? (await this.store.eventById(roomId, request.conversationId ?? request.eventId))?.seq ?? 0;
690
+ }
564
691
  publicRequest(request) {
565
692
  const result = { ...request };
566
693
  delete result.turnToken;
@@ -828,7 +955,7 @@ export class RoomService {
828
955
  async close(roomId, credential) {
829
956
  return this.applyOne(closeRoom(await this.view(roomId), await hashCredential(credential), this.ctx()));
830
957
  }
831
- async read(roomId, credential, after, limit) {
958
+ async read(roomId, credential, after, limit, receiptsAfter) {
832
959
  const view = await this.view(roomId);
833
960
  authenticate(view, await hashCredential(credential), this.now());
834
961
  if (after > 0 && after < view.earliestSeq - 1) {
@@ -836,7 +963,7 @@ export class RoomService {
836
963
  }
837
964
  const page = await this.store.readEvents(roomId, after, limit);
838
965
  const events = await Promise.all(page.events.map((event) => this.restoreAudience(event)));
839
- return { events, earliestSeq: view.earliestSeq, latestSeq: view.room.nextSeq - 1, hasMore: page.hasMore };
966
+ return { events, earliestSeq: view.earliestSeq, latestSeq: view.room.nextSeq - 1, hasMore: page.hasMore, ...(receiptsAfter !== undefined && this.receiptMode !== 'events' ? receiptsSince(view.room, receiptsAfter) : {}) };
840
967
  }
841
968
  /** Older group events kept the all flag only in their canonical idempotency request. */
842
969
  async restoreAudience(event) {