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.
- package/.gitmodules +3 -3
- package/.idea/shelf/Uncommitted_changes_before_Checkout_at_05_08_2026_17_28_[Changes]/shelved.patch +447 -0
- package/.idea/shelf/Uncommitted_changes_before_Checkout_at_05_08_2026_17_28_[Changes]1/shelved.patch +0 -0
- package/.idea/shelf/Uncommitted_changes_before_Checkout_at_05_08_2026_17_28__Changes_.xml +4 -0
- package/.idea/workspace.xml +193 -6
- package/CODE_OF_CONDUCT.md +76 -76
- package/README.md +44 -44
- package/babel.config.js +5 -5
- package/build/index.cjs.js +392 -429
- 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/index.d.ts +2 -1
- package/build/types/state-tracker/RoomMessagesHistory.d.ts +4 -15
- package/build/types/state-tracker/TopicHistoryWindow.d.ts +0 -48
- package/build/types/types/src/index.d.ts +3 -3
- package/build/types/types/src/schemes/SessionData.d.ts +6 -0
- package/build/types/types/src/schemes/User.d.ts +15 -0
- package/build/types/types/src/schemes/UserData.d.ts +5 -0
- package/build/types/types/src/schemes/commands/SetSessionData.d.ts +9 -1
- package/build/types/types/src/schemes/commands/SetUserData.d.ts +1 -0
- package/jest.config.ts +199 -199
- package/package.json +43 -43
- package/scripts/getPackageJson.js +24 -24
- package/src/FilesClient.ts +42 -42
- package/src/index.ts +31 -29
- package/src/state-tracker/ChatStateTracker.ts +71 -71
- package/src/state-tracker/RoomMessagesHistory.ts +9 -27
- package/src/state-tracker/TopicHistoryWindow.ts +1 -133
- package/src/state-tracker/UsersManager.ts +51 -51
- package/src/state-tracker/functions.ts +28 -28
- package/src/types/src/index.ts +313 -311
- package/src/types/src/schemes/SessionData.ts +8 -1
- package/src/types/src/schemes/User.ts +17 -1
- package/src/types/src/schemes/UserData.ts +5 -0
- package/src/types/src/schemes/commands/SetSessionData.ts +11 -2
- package/src/types/src/schemes/commands/SetUserData.ts +1 -0
- package/tests/async-utils.test.ts +29 -29
- package/tests/history-window.test.ts +0 -131
- package/tests/space-roles.test.ts +42 -42
- package/tests/state-reconnect.test.ts +0 -260
- package/.idea/shelf/Uncommitted_changes_before_Checkout_at_26_07_2026_21_26_[Changes]/shelved.patch +0 -174
- package/.idea/shelf/Uncommitted_changes_before_Checkout_at_26_07_2026_21_26__Changes_.xml +0 -4
- package/.idea/shelf/Uncommitted_changes_before_rebase_[Changes]/shelved.patch +0 -18
- 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
|
+
}
|
|
@@ -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();
|