polfan-server-js-client 0.3.2 → 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.
@@ -210,6 +210,266 @@ 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
+
213
473
  describe('reconnect - SpacesManager', () => {
214
474
  test('reconciles roles in place and drops only removed spaces', async () => {
215
475
  const { client, tracker } = createTracker();