polfan-server-js-client 0.3.3 → 0.4.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.
Files changed (45) hide show
  1. package/.gitmodules +3 -3
  2. package/.idea/shelf/Uncommitted_changes_before_Checkout_at_05_08_2026_17_28_[Changes]/shelved.patch +447 -0
  3. package/.idea/shelf/Uncommitted_changes_before_Checkout_at_05_08_2026_17_28_[Changes]1/shelved.patch +0 -0
  4. package/.idea/shelf/Uncommitted_changes_before_Checkout_at_05_08_2026_17_28__Changes_.xml +4 -0
  5. package/.idea/workspace.xml +193 -6
  6. package/CODE_OF_CONDUCT.md +76 -76
  7. package/README.md +44 -44
  8. package/babel.config.js +5 -5
  9. package/build/index.cjs.js +392 -429
  10. package/build/index.cjs.js.map +1 -1
  11. package/build/index.umd.js +1 -1
  12. package/build/index.umd.js.map +1 -1
  13. package/build/types/index.d.ts +2 -1
  14. package/build/types/state-tracker/RoomMessagesHistory.d.ts +4 -15
  15. package/build/types/state-tracker/TopicHistoryWindow.d.ts +0 -48
  16. package/build/types/types/src/index.d.ts +3 -3
  17. package/build/types/types/src/schemes/SessionData.d.ts +6 -0
  18. package/build/types/types/src/schemes/User.d.ts +15 -0
  19. package/build/types/types/src/schemes/UserData.d.ts +5 -0
  20. package/build/types/types/src/schemes/commands/SetSessionData.d.ts +9 -1
  21. package/build/types/types/src/schemes/commands/SetUserData.d.ts +1 -0
  22. package/jest.config.ts +199 -199
  23. package/package.json +43 -43
  24. package/scripts/getPackageJson.js +24 -24
  25. package/src/FilesClient.ts +42 -42
  26. package/src/index.ts +31 -29
  27. package/src/state-tracker/ChatStateTracker.ts +71 -71
  28. package/src/state-tracker/RoomMessagesHistory.ts +9 -27
  29. package/src/state-tracker/TopicHistoryWindow.ts +1 -133
  30. package/src/state-tracker/UsersManager.ts +51 -51
  31. package/src/state-tracker/functions.ts +28 -28
  32. package/src/types/src/index.ts +313 -311
  33. package/src/types/src/schemes/SessionData.ts +8 -1
  34. package/src/types/src/schemes/User.ts +17 -1
  35. package/src/types/src/schemes/UserData.ts +5 -0
  36. package/src/types/src/schemes/commands/SetSessionData.ts +11 -2
  37. package/src/types/src/schemes/commands/SetUserData.ts +1 -0
  38. package/tests/async-utils.test.ts +29 -29
  39. package/tests/history-window.test.ts +0 -131
  40. package/tests/space-roles.test.ts +42 -42
  41. package/tests/state-reconnect.test.ts +0 -260
  42. package/.idea/shelf/Uncommitted_changes_before_Checkout_at_26_07_2026_21_26_[Changes]/shelved.patch +0 -174
  43. package/.idea/shelf/Uncommitted_changes_before_Checkout_at_26_07_2026_21_26__Changes_.xml +0 -4
  44. package/.idea/shelf/Uncommitted_changes_before_rebase_[Changes]/shelved.patch +0 -18
  45. package/.idea/shelf/Uncommitted_changes_before_rebase__Changes_.xml +0 -4
@@ -1,9 +1,25 @@
1
1
  export type UserTags = 'bot' | 'temp' | string;
2
2
 
3
+ /**
4
+ * Availability of a user, aggregated over all of their sessions.
5
+ *
6
+ * - `Offline` - no session is connected and none can be reached asynchronously.
7
+ * - `Online` - at least one session holds a live connection.
8
+ * - `OnlineAsync` - no live connection, but a push-registered device was active
9
+ * recently, so a message will still reach the user.
10
+ */
11
+ export enum UserStatus {
12
+ Offline = 0,
13
+ Online = 1,
14
+ OnlineAsync = 2,
15
+ }
16
+
3
17
  export interface User {
4
18
  id: string;
5
19
  nick: string;
6
20
  avatar: string;
7
21
  tags: UserTags[];
22
+ status: UserStatus;
23
+ /** @deprecated Use {@link status}. Kept for clients older than the numeric status. */
8
24
  online: boolean;
9
- }
25
+ }
@@ -2,4 +2,9 @@ export type PrivateMessagePolicy = 'None' | 'Mutual' | 'All';
2
2
 
3
3
  export interface UserData {
4
4
  privateMessagePolicy: PrivateMessagePolicy;
5
+ /**
6
+ * Whether others may see that the user is reachable on a push-registered
7
+ * device while disconnected. When false the user simply reads as offline.
8
+ */
9
+ showAsyncPresence: boolean;
5
10
  }
@@ -1,10 +1,19 @@
1
+ import {SessionPlatform} from "../SessionData";
2
+
1
3
  export interface SessionPush {
2
4
  token?: string;
3
- platform?: 'ios' | 'android' | 'web';
4
5
  active?: boolean;
6
+ /**
7
+ * @deprecated The platform describes the session, not its push registration.
8
+ * Use {@link SetSessionData.platform} (or the `platform` connection query
9
+ * parameter). Still accepted by the server, but the top-level field wins.
10
+ */
11
+ platform?: SessionPlatform;
5
12
  }
6
13
 
7
14
  export interface SetSessionData {
8
15
  clientFocused?: boolean;
9
16
  push?: SessionPush;
10
- }
17
+ /** Client platform of this session; null leaves the stored value unchanged. */
18
+ platform?: SessionPlatform;
19
+ }
@@ -2,4 +2,5 @@ import {PrivateMessagePolicy} from "../UserData";
2
2
 
3
3
  export interface SetUserData {
4
4
  privateMessagePolicy?: PrivateMessagePolicy;
5
+ showAsyncPresence?: boolean;
5
6
  }
@@ -1,30 +1,30 @@
1
- import {PromiseRegistry} from "../src/state-tracker/AsyncUtils";
2
-
3
- test('promise registry - limit the calling of async process', async () => {
4
- const pr = new PromiseRegistry();
5
- let generationCount = 0;
6
-
7
- async function generateResult() {
8
- return ++generationCount;
9
- }
10
-
11
- async function getResult() {
12
- const resultKey = 'test';
13
-
14
- if (pr.notExist('test')) {
15
- pr.registerByFunction(generateResult, resultKey);
16
- }
17
-
18
- await pr.get(resultKey);
19
- return generationCount;
20
- }
21
-
22
- const call1 = await getResult();
23
- expect(call1).toBe(1);
24
- const call2 = await getResult();
25
- expect(call2).toBe(1);
26
- const call3 = await getResult();
27
- expect(call3).toBe(1);
28
-
29
- expect(generationCount).toBe(1);
1
+ import {PromiseRegistry} from "../src/state-tracker/AsyncUtils";
2
+
3
+ test('promise registry - limit the calling of async process', async () => {
4
+ const pr = new PromiseRegistry();
5
+ let generationCount = 0;
6
+
7
+ async function generateResult() {
8
+ return ++generationCount;
9
+ }
10
+
11
+ async function getResult() {
12
+ const resultKey = 'test';
13
+
14
+ if (pr.notExist('test')) {
15
+ pr.registerByFunction(generateResult, resultKey);
16
+ }
17
+
18
+ await pr.get(resultKey);
19
+ return generationCount;
20
+ }
21
+
22
+ const call1 = await getResult();
23
+ expect(call1).toBe(1);
24
+ const call2 = await getResult();
25
+ expect(call2).toBe(1);
26
+ const call3 = await getResult();
27
+ expect(call3).toBe(1);
28
+
29
+ expect(generationCount).toBe(1);
30
30
  });
@@ -209,137 +209,6 @@ test('history window - reset to latest', async () => {
209
209
  [7,8,9].forEach(id => expect(window.items.map(item => item.id)).toContain(id));
210
210
  });
211
211
 
212
- test('history window - resync to latest merges the new page into loaded items', async () => {
213
- const window = new TestableHistoryWindow();
214
- window.limit = 10;
215
- window.fetchLimit = 3;
216
-
217
- await window.resetToLatest(); // [7,8,9]
218
- await window.fetchPrevious(); // [4,5,6,7,8,9]
219
-
220
- await window.resyncToLatest(); // fetched [7,8,9] overlaps -> nothing is lost
221
-
222
- expect(window.state).toEqual(WindowState.LATEST);
223
- expect(window.items.map(item => item.id)).toEqual([4, 5, 6, 7, 8, 9]);
224
- });
225
-
226
- test('history window - resync to latest trims the merged items to the limit', async () => {
227
- const window = new TestableHistoryWindow();
228
- window.limit = 4;
229
- window.fetchLimit = 3;
230
-
231
- await window.resetToLatest(); // [7,8,9]
232
- await window.fetchPrevious(); // [4,5,6,7] (trimmed to the limit)
233
-
234
- // [4,5,6,7] merged with the page [7,8,9] -> [4,5,6,7,8,9], trimmed again.
235
- await window.resyncToLatest();
236
-
237
- expect(window.state).toEqual(WindowState.LATEST);
238
- expect(window.items.map(item => item.id)).toEqual([6, 7, 8, 9]);
239
- });
240
-
241
- test('history window - resync to latest marks the gap instead of dropping items', async () => {
242
- const window = new TestableHistoryWindow();
243
- window.limit = 10;
244
- window.fetchLimit = 3;
245
-
246
- await window.jumpTo('1'); // [0,1,2]
247
-
248
- expect(window.gaps).toEqual([]);
249
-
250
- // A full page ([7,8,9]) that does not reach the newest loaded item (2):
251
- // items 3..6 were never fetched, so the seam in front of 7 is marked.
252
- await window.resyncToLatest();
253
-
254
- expect(window.state).toEqual(WindowState.LATEST);
255
- expect(window.items.map(item => item.id)).toEqual([0, 1, 2, 7, 8, 9]);
256
- expect(window.gaps).toEqual([7]);
257
- });
258
-
259
- test('history window - gap marker disappears with the item it points at', async () => {
260
- const window = new TestableHistoryWindow();
261
- window.limit = 10;
262
- window.fetchLimit = 3;
263
-
264
- await window.jumpTo('1'); // [0,1,2]
265
- await window.resyncToLatest(); // [0,1,2] | [7,8,9]
266
-
267
- expect(window.gaps).toEqual([7]);
268
-
269
- window.delete(7);
270
-
271
- expect(window.gaps).toEqual([]);
272
- });
273
-
274
- test('history window - gap marker is closed by fetching what is before it', async () => {
275
- const window = new TestableHistoryWindow();
276
- window.limit = 10;
277
- window.fetchLimit = 3;
278
-
279
- await window.jumpTo('1'); // [0,1,2]
280
- await window.resyncToLatest(); // [0,1,2] | [7,8,9]
281
- window.delete(0, 1, 2); // the items above the gap are gone, 7 is the top one
282
-
283
- expect(window.gaps).toEqual([7]);
284
-
285
- // Whatever comes back was fetched as the direct predecessor of 7, so the
286
- // history is continuous again.
287
- await window.fetchPrevious(); // [4,5,6,7,8,9]
288
-
289
- expect(window.items.map(item => item.id)).toEqual([4, 5, 6, 7, 8, 9]);
290
- expect(window.gaps).toEqual([]);
291
- });
292
-
293
- test('history window - gap markers are cleared when the whole window is replaced', async () => {
294
- const window = new TestableHistoryWindow();
295
- window.limit = 10;
296
- window.fetchLimit = 3;
297
-
298
- await window.jumpTo('1'); // [0,1,2]
299
- await window.resyncToLatest(); // [0,1,2] | [7,8,9]
300
-
301
- expect(window.gaps).toEqual([7]);
302
-
303
- await window.resetToLatest(true); // [7,8,9] - a continuous window again
304
-
305
- expect(window.items.map(item => item.id)).toEqual([7, 8, 9]);
306
- expect(window.gaps).toEqual([]);
307
- });
308
-
309
- test('history window - gap marker is forgotten when trimmed out of the window', async () => {
310
- const window = new TestableHistoryWindow();
311
- window.limit = 10;
312
- window.fetchLimit = 3;
313
-
314
- await window.jumpTo('1'); // [0,1,2]
315
- await window.resyncToLatest(); // [0,1,2] | [7,8,9]
316
-
317
- expect(window.gaps).toEqual([7]);
318
-
319
- // The window shrinks and the trimming pushes item 7 (and everything above
320
- // it) out, so the marker has nothing left to point at.
321
- window.limit = 2;
322
- await window.resyncToLatest();
323
-
324
- expect(window.items.map(item => item.id)).toEqual([8, 9]);
325
- expect(window.gaps).toEqual([]);
326
- });
327
-
328
- test('history window - resync to latest keeps loaded items when the page is not full', async () => {
329
- const window = new TestableHistoryWindow();
330
- window.limit = 10;
331
- window.fetchLimit = 5; // The fetch returns 3 items only - all there is.
332
-
333
- await window.jumpTo('1'); // [0,1,2,3]
334
-
335
- // Nothing is missing in between: the page is everything the remote side has,
336
- // so the loaded items are kept even though the page does not reach them.
337
- await window.resyncToLatest();
338
-
339
- expect(window.state).toEqual(WindowState.LATEST);
340
- expect(window.items.map(item => item.id)).toEqual([0, 1, 2, 3, 7, 8, 9]);
341
- });
342
-
343
212
  test('history window - jump to message', async () => {
344
213
  const window = new TestableHistoryWindow();
345
214
  window.limit = 5;
@@ -1,43 +1,43 @@
1
- import {IndexedObjectCollection} from "../src";
2
- import {Role} from "pserv-ts-types";
3
- import {reorderRolesOnPriorityUpdate} from "../src/state-tracker/functions";
4
-
5
- const createCollection = () => new IndexedObjectCollection<Role>(
6
- role => role.id,
7
- [
8
- {id: 'id1', name: 'everyone', priority: 0, color: '#ff0000'},
9
- {id: 'id2', name: 'roleA', priority: 1, color: '#ff0000'},
10
- {id: 'id3', name: 'roleB', priority: 2, color: '#ff0000'},
11
- {id: 'id4', name: 'roleC', priority: 3, color: '#ff0000'},
12
- {id: 'id5', name: 'roleD', priority: 4, color: '#ff0000'},
13
- ],
14
- );
15
-
16
- test('roles reorder - increase', () => {
17
- const collection = createCollection();
18
- const oldRole = collection.get('id2');
19
- const newRole: Role = {...oldRole, priority: 4};
20
- const result = reorderRolesOnPriorityUpdate(collection.items, oldRole, newRole);
21
- collection.set(newRole, ...result);
22
-
23
- //expect(result.length).toBe(3);
24
- expect(collection.get('id1').priority).toBe(0);
25
- expect(collection.get('id2').priority).toBe(4);
26
- expect(collection.get('id3').priority).toBe(1);
27
- expect(collection.get('id4').priority).toBe(2);
28
- expect(collection.get('id5').priority).toBe(3);
29
- });
30
-
31
- test('roles reorder - decrease', () => {
32
- const collection = createCollection();
33
- const oldRole = collection.get('id5');
34
- const newRole: Role = {...oldRole, priority: 0};
35
- const result = reorderRolesOnPriorityUpdate(collection.items, oldRole, newRole);
36
- collection.set(newRole, ...result);
37
-
38
- expect(collection.get('id1').priority).toBe(1);
39
- expect(collection.get('id2').priority).toBe(2);
40
- expect(collection.get('id3').priority).toBe(3);
41
- expect(collection.get('id4').priority).toBe(4);
42
- expect(collection.get('id5').priority).toBe(0);
1
+ import {IndexedObjectCollection} from "../src";
2
+ import {Role} from "pserv-ts-types";
3
+ import {reorderRolesOnPriorityUpdate} from "../src/state-tracker/functions";
4
+
5
+ const createCollection = () => new IndexedObjectCollection<Role>(
6
+ role => role.id,
7
+ [
8
+ {id: 'id1', name: 'everyone', priority: 0, color: '#ff0000'},
9
+ {id: 'id2', name: 'roleA', priority: 1, color: '#ff0000'},
10
+ {id: 'id3', name: 'roleB', priority: 2, color: '#ff0000'},
11
+ {id: 'id4', name: 'roleC', priority: 3, color: '#ff0000'},
12
+ {id: 'id5', name: 'roleD', priority: 4, color: '#ff0000'},
13
+ ],
14
+ );
15
+
16
+ test('roles reorder - increase', () => {
17
+ const collection = createCollection();
18
+ const oldRole = collection.get('id2');
19
+ const newRole: Role = {...oldRole, priority: 4};
20
+ const result = reorderRolesOnPriorityUpdate(collection.items, oldRole, newRole);
21
+ collection.set(newRole, ...result);
22
+
23
+ //expect(result.length).toBe(3);
24
+ expect(collection.get('id1').priority).toBe(0);
25
+ expect(collection.get('id2').priority).toBe(4);
26
+ expect(collection.get('id3').priority).toBe(1);
27
+ expect(collection.get('id4').priority).toBe(2);
28
+ expect(collection.get('id5').priority).toBe(3);
29
+ });
30
+
31
+ test('roles reorder - decrease', () => {
32
+ const collection = createCollection();
33
+ const oldRole = collection.get('id5');
34
+ const newRole: Role = {...oldRole, priority: 0};
35
+ const result = reorderRolesOnPriorityUpdate(collection.items, oldRole, newRole);
36
+ collection.set(newRole, ...result);
37
+
38
+ expect(collection.get('id1').priority).toBe(1);
39
+ expect(collection.get('id2').priority).toBe(2);
40
+ expect(collection.get('id3').priority).toBe(3);
41
+ expect(collection.get('id4').priority).toBe(4);
42
+ expect(collection.get('id5').priority).toBe(0);
43
43
  });
@@ -210,266 +210,6 @@ describe('reconnect - MessagesManager', () => {
210
210
  });
211
211
  });
212
212
 
213
- describe('reconnect - time limited (MaxAge) room history', () => {
214
- const HOUR = 60 * 60 * 1000;
215
-
216
- const message = (id: string, ageInHours: number): any => ({
217
- id,
218
- type: 'Text',
219
- content: '',
220
- createdAt: new Date(Date.now() - ageInHours * HOUR).toISOString(),
221
- location: { roomId: 'M', topicId: 'topic-M' },
222
- author: { user: { id: 'other' } },
223
- topicRef: null,
224
- attachments: null,
225
- });
226
-
227
- /**
228
- * Serves GetMessages from a mutable server-side message list, the way the
229
- * real server does: the latest page is the newest slice of it.
230
- */
231
- const serveMessages = (client: FakeClient, store: any[]): void => {
232
- client.respondTo('GetMessages', (data: any) => {
233
- const limit = data.limit ?? 50;
234
-
235
- if (data.before) {
236
- const index = store.findIndex(m => m.id === data.before);
237
- return { messages: store.slice(Math.max(0, index - limit), index) };
238
- }
239
-
240
- return { messages: store.slice(-limit) };
241
- });
242
- };
243
-
244
- const maxAgeRoom = (store: any[], maxAge: number = 24 * 60 * 60): any => createRoom('M', {
245
- history: { mode: 'MaxAge', maxAge },
246
- defaultTopic: {
247
- id: 'topic-M',
248
- messageCount: store.length,
249
- lastMessage: store[store.length - 1] ?? null,
250
- },
251
- });
252
-
253
- /**
254
- * Room M with a window pulled to LATEST and holding `m1..m5` (m1..m2 loaded
255
- * by traversing back), i.e. more history than a single page.
256
- */
257
- const openWindowWithHistory = async (store: any[]) => {
258
- const { client, tracker } = createTracker();
259
- serveMessages(client, store);
260
-
261
- emitSession(client, [maxAgeRoom(store)]);
262
-
263
- const history = await tracker.rooms.messages.getRoomHistory('M');
264
- const window = await history.getMessagesWindow('topic-M');
265
- window.fetchLimit = 3;
266
-
267
- await window.resetToLatest(); // [m3, m4, m5]
268
- await window.fetchPrevious(); // [m1, m2, m3, m4, m5]
269
-
270
- expect(window.state).toBe(WindowState.LATEST);
271
- expect(window.items.map((m: any) => m.id)).toEqual(['m1', 'm2', 'm3', 'm4', 'm5']);
272
-
273
- client.sent.length = 0;
274
-
275
- return { client, tracker, history, window };
276
- };
277
-
278
- test('loads the missed messages and keeps the history that fits in the window', async () => {
279
- const store = [
280
- message('m1', 5), message('m2', 4), message('m3', 3),
281
- message('m4', 2), message('m5', 1),
282
- ];
283
- const { client, tracker, history, window } = await openWindowWithHistory(store);
284
-
285
- // Two messages arrived while the connection was down.
286
- store.push(message('m6', 0), message('m7', 0));
287
-
288
- emitSession(client, [maxAgeRoom(store)]);
289
- await flush();
290
-
291
- expect(await tracker.rooms.messages.getRoomHistory('M')).toBe(history);
292
- expect(await history.getMessagesWindow('topic-M')).toBe(window);
293
- // One request only - the new messages come with the latest page.
294
- expect(client.countSent('GetMessages')).toBe(1);
295
- expect(window.state).toBe(WindowState.LATEST);
296
- expect(window.items.map((m: any) => m.id))
297
- .toEqual(['m1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7']);
298
- });
299
-
300
- test('keeps the loaded messages older than maxAge - retention is server side only', async () => {
301
- const store = [
302
- message('m1', 30), message('m2', 26), message('m3', 3),
303
- message('m4', 2), message('m5', 1),
304
- ];
305
- const { client, window } = await openWindowWithHistory(store);
306
-
307
- // m1 and m2 are older than the room maxAge (24h), so the server stops
308
- // serving them to keep them away from users who join later. The user who
309
- // was there when they were written keeps them in their window.
310
- store.splice(0, 2);
311
- store.push(message('m6', 0));
312
-
313
- emitSession(client, [maxAgeRoom(store)]);
314
- await flush();
315
-
316
- expect(window.items.map((m: any) => m.id))
317
- .toEqual(['m1', 'm2', 'm3', 'm4', 'm5', 'm6']);
318
- });
319
-
320
- test('drops the oldest loaded messages only when the window size limit is hit', async () => {
321
- const store = [
322
- message('m1', 5), message('m2', 4), message('m3', 3),
323
- message('m4', 2), message('m5', 1),
324
- ];
325
- const { client, window } = await openWindowWithHistory(store);
326
-
327
- window.limit = 4;
328
- store.push(message('m6', 0));
329
-
330
- emitSession(client, [maxAgeRoom(store)]);
331
- await flush();
332
-
333
- expect(window.items.map((m: any) => m.id)).toEqual(['m3', 'm4', 'm5', 'm6']);
334
- });
335
-
336
- test('keeps the loaded history when a short latest page does not reach it', async () => {
337
- const store = [
338
- message('m1', 5), message('m2', 4), message('m3', 3),
339
- message('m4', 2), message('m5', 1),
340
- ];
341
- const { client, window } = await openWindowWithHistory(store);
342
-
343
- // The server dropped everything the client had loaded and holds a single
344
- // message written during the downtime. The loaded messages are still
345
- // inside the room time window, so they must not disappear from the
346
- // window just because they are not in the (partial) page.
347
- store.length = 0;
348
- store.push(message('m6', 0));
349
-
350
- emitSession(client, [maxAgeRoom(store)]);
351
- await flush();
352
-
353
- expect(window.state).toBe(WindowState.LATEST);
354
- expect(window.items.map((m: any) => m.id))
355
- .toEqual(['m1', 'm2', 'm3', 'm4', 'm5', 'm6']);
356
- });
357
-
358
- test('keeps the loaded history when nothing was written during the downtime', async () => {
359
- const store = [
360
- message('m1', 5), message('m2', 4), message('m3', 3),
361
- message('m4', 2), message('m5', 1),
362
- ];
363
- const { client, window } = await openWindowWithHistory(store);
364
-
365
- // Empty latest page - the room is quiet and the server no longer keeps
366
- // the messages the client has. Emptying the window here is exactly what
367
- // must not happen.
368
- store.length = 0;
369
-
370
- emitSession(client, [maxAgeRoom(store)]);
371
- await flush();
372
-
373
- expect(window.state).toBe(WindowState.LATEST);
374
- expect(window.items.map((m: any) => m.id)).toEqual(['m1', 'm2', 'm3', 'm4', 'm5']);
375
- });
376
-
377
- test('deduplicates the messages returned in both the page and the local history', async () => {
378
- const store = [
379
- message('m1', 5), message('m2', 4), message('m3', 3),
380
- message('m4', 2), message('m5', 1),
381
- ];
382
- const { client, window } = await openWindowWithHistory(store);
383
-
384
- // Same messages come back in the page (nothing new was written).
385
- emitSession(client, [maxAgeRoom(store)]);
386
- await flush();
387
-
388
- expect(window.items.map((m: any) => m.id)).toEqual(['m1', 'm2', 'm3', 'm4', 'm5']);
389
- });
390
-
391
- test('marks the gap when the missed messages do not fit in a single page', async () => {
392
- const store = [
393
- message('m1', 5), message('m2', 4), message('m3', 3),
394
- message('m4', 2), message('m5', 1),
395
- ];
396
- const { client, window } = await openWindowWithHistory(store);
397
-
398
- // More messages than a single page arrived, so the loaded ones cannot be
399
- // stitched to the fetched page: m6 was never fetched.
400
- store.push(message('m6', 0), message('m7', 0), message('m8', 0), message('m9', 0));
401
-
402
- emitSession(client, [maxAgeRoom(store)]);
403
- await flush();
404
-
405
- expect(window.state).toBe(WindowState.LATEST);
406
- expect(window.items.map((m: any) => m.id))
407
- .toEqual(['m1', 'm2', 'm3', 'm4', 'm5', 'm7', 'm8', 'm9']);
408
- expect(window.gaps).toEqual(['m7']);
409
- });
410
-
411
- test('does not mark a gap when the loaded history is continuous', async () => {
412
- const store = [
413
- message('m1', 5), message('m2', 4), message('m3', 3),
414
- message('m4', 2), message('m5', 1),
415
- ];
416
- const { client, window } = await openWindowWithHistory(store);
417
-
418
- store.push(message('m6', 0));
419
-
420
- emitSession(client, [maxAgeRoom(store)]);
421
- await flush();
422
-
423
- expect(window.gaps).toEqual([]);
424
- });
425
-
426
- test('a full history room still resets to the latest page', async () => {
427
- const store = [
428
- message('m1', 5), message('m2', 4), message('m3', 3),
429
- message('m4', 2), message('m5', 1),
430
- ];
431
- const { client, tracker } = createTracker();
432
- serveMessages(client, store);
433
-
434
- const fullRoom = () => createRoom('M', {
435
- defaultTopic: { id: 'topic-M', messageCount: store.length, lastMessage: store[store.length - 1] },
436
- });
437
-
438
- emitSession(client, [fullRoom()]);
439
-
440
- const history = await tracker.rooms.messages.getRoomHistory('M');
441
- const window = await history.getMessagesWindow('topic-M');
442
- window.fetchLimit = 3;
443
-
444
- await window.resetToLatest();
445
- await window.fetchPrevious();
446
- client.sent.length = 0;
447
-
448
- emitSession(client, [fullRoom()]);
449
- await flush();
450
-
451
- // Persisted history can always be traversed back, so the window is just
452
- // reset - unchanged behaviour.
453
- expect(window.items.map((m: any) => m.id)).toEqual(['m3', 'm4', 'm5']);
454
- });
455
-
456
- test('ephemeral history is never resynced, even when asked directly', async () => {
457
- const { client, tracker } = createTracker();
458
- client.respondTo('GetMessages', () => ({ messages: [{ id: 'x1' }] }));
459
-
460
- emitSession(client, [createRoom('E', { history: { mode: 'Ephemeral' } })]);
461
-
462
- const history = await tracker.rooms.messages.getRoomHistory('E');
463
- const window = await history.getMessagesWindow('topic-E');
464
- client.sent.length = 0;
465
-
466
- await window.resyncToLatest();
467
-
468
- expect(client.countSent('GetMessages')).toBe(0);
469
- expect(window.items).toHaveLength(0);
470
- });
471
- });
472
-
473
213
  describe('reconnect - SpacesManager', () => {
474
214
  test('reconciles roles in place and drops only removed spaces', async () => {
475
215
  const { client, tracker } = createTracker();