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.
- package/.idea/workspace.xml +45 -53
- package/build/index.cjs.js +414 -211
- package/build/index.cjs.js.map +1 -1
- package/build/index.umd.js +1 -1
- package/build/index.umd.js.map +1 -1
- package/build/types/state-tracker/RoomMessagesHistory.d.ts +15 -4
- package/build/types/state-tracker/TopicHistoryWindow.d.ts +48 -0
- package/package.json +1 -1
- package/src/state-tracker/RoomMessagesHistory.ts +27 -9
- package/src/state-tracker/TopicHistoryWindow.ts +133 -1
- package/tests/history-window.test.ts +131 -0
- package/tests/state-reconnect.test.ts +260 -0
|
@@ -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();
|