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
@@ -45,7 +45,6 @@ export abstract class TraversableRemoteCollection<
45
45
  fetchLimit: number,
46
46
  lastFetchCount: number,
47
47
  oldestId: string | null,
48
- gaps: string[],
49
48
  } = {
50
49
  current: WindowState.LIVE,
51
50
  ongoing: undefined,
@@ -54,7 +53,6 @@ export abstract class TraversableRemoteCollection<
54
53
  fetchLimit: 50,
55
54
  lastFetchCount: 0,
56
55
  oldestId: null,
57
- gaps: [],
58
56
  };
59
57
 
60
58
  /**
@@ -105,21 +103,6 @@ export abstract class TraversableRemoteCollection<
105
103
  return [WindowState.LATEST, WindowState.LIVE].includes(this.state);
106
104
  }
107
105
 
108
- /**
109
- * IDs of the items the window could not stitch to the ones loaded before
110
- * them: there is a gap in front of each of them, i.e. the item right above
111
- * it in the window is not its real predecessor and an unknown number of
112
- * items in between was never fetched.
113
- *
114
- * Such a gap appears when the collection is resynchronised after a
115
- * reconnect (see resyncToLatest) and more items than a single page arrived
116
- * while the connection was down. The markers are kept in the window order
117
- * and disappear together with the items they point at.
118
- */
119
- public get gaps(): readonly string[] {
120
- return [...this.internalState.gaps];
121
- }
122
-
123
106
  public get hasOldest(): boolean {
124
107
  return this.state === WindowState.OLDEST
125
108
  || this.state === WindowState.LATEST && this.length < this.fetchLimit
@@ -145,68 +128,11 @@ export abstract class TraversableRemoteCollection<
145
128
  }
146
129
 
147
130
  this._items.deleteAll(); // Directly call deleteAll to prevent event emit.
148
- this.internalState.gaps = []; // Whole content is replaced, old markers mean nothing.
149
131
  this.addItems(result, 'tail');
150
132
  this.internalState.current = WindowState.LATEST;
151
133
  this.emitChangeWithDiff(true, originalState);
152
134
  }
153
135
 
154
- /**
155
- * Refresh the window with the latest page, keeping the already loaded items
156
- * instead of replacing them.
157
- *
158
- * This is the reconnect-friendly variant of resetToLatest: the items missed
159
- * while the connection was down are pulled with a single request and merged
160
- * on top of the loaded ones (items returned in both are deduplicated), so
161
- * the context the application already had does not disappear. The window
162
- * size limit is the only thing that pushes the oldest items out.
163
- *
164
- * An empty or partial page is not a reason to drop anything: it only means
165
- * the collection has little (or nothing) left on the remote side, while the
166
- * items loaded earlier are still valid. When the page is full and does not
167
- * reach the loaded items, an unknown number of items in between was never
168
- * fetched - both parts are still kept, and the seam between them is recorded
169
- * in `gaps` so the application can show where the history is not continuous.
170
- */
171
- public async resyncToLatest(): Promise<void> {
172
- if (this.internalState.ongoing) {
173
- return;
174
- }
175
-
176
- let result;
177
- const originalState = this.state;
178
- this.internalState.ongoing = WindowState.LATEST;
179
-
180
- try {
181
- result = await this.fetchLatestItems();
182
- this.internalState.lastFetchCount = result.length;
183
- } finally {
184
- this.internalState.ongoing = undefined;
185
- }
186
-
187
- const loaded = this.items;
188
- const fetchedIds = new Set(result.map(item => this.getId(item)));
189
- // Items present in the page are taken from it - the server copy is the
190
- // up-to-date one.
191
- const retained = loaded.filter(item => ! fetchedIds.has(this.getId(item)));
192
-
193
- // Nothing can be missing in between when there is nothing to stitch,
194
- // when the page is everything the remote side has (it is shorter than
195
- // the requested limit), or when the page reaches the newest loaded item.
196
- const isContinuous = ! retained.length
197
- || result.length < this.internalState.fetchLimit
198
- || fetchedIds.has(this.getId(loaded[loaded.length - 1]));
199
-
200
- if (! isContinuous) {
201
- this.markGapBefore(this.getId(result[0]));
202
- }
203
-
204
- this._items.deleteAll(); // Directly call deleteAll to prevent event emit.
205
- this.addItems([...retained, ...result], 'tail');
206
- this.internalState.current = WindowState.LATEST;
207
- this.emitChangeWithDiff(true, originalState);
208
- }
209
-
210
136
  public async fetchPrevious(): Promise<void> {
211
137
  if (this.internalState.ongoing || this.hasOldest) {
212
138
  return;
@@ -214,7 +140,6 @@ export abstract class TraversableRemoteCollection<
214
140
 
215
141
  let result;
216
142
  const originalState = this.state;
217
- const firstItem = this.getAt(0);
218
143
  this.internalState.ongoing = WindowState.PAST;
219
144
 
220
145
  try {
@@ -229,6 +154,7 @@ export abstract class TraversableRemoteCollection<
229
154
  }
230
155
 
231
156
  if (! result.length) {
157
+ const firstItem = this.getAt(0);
232
158
  this.internalState.oldestId = firstItem ? this.getId(firstItem) : null;
233
159
 
234
160
  await this.refreshFetchedState();
@@ -242,13 +168,6 @@ export abstract class TraversableRemoteCollection<
242
168
  return;
243
169
  }
244
170
 
245
- if (firstItem) {
246
- // The fetch asked for the items right before the one that was first,
247
- // so whatever came back is its real predecessor: a gap marked in
248
- // front of it (it used to be the top of the window) is closed now.
249
- this.clearGapBefore(this.getId(firstItem));
250
- }
251
-
252
171
  this.addItems(result, 'head');
253
172
  await this.refreshFetchedState();
254
173
  this.emitChangeWithDiff(true, originalState);
@@ -298,7 +217,6 @@ export abstract class TraversableRemoteCollection<
298
217
 
299
218
  if (result) {
300
219
  this._items.deleteAll(); // Directly call deleteAll to prevent event emit.
301
- this.internalState.gaps = []; // Whole content is replaced, old markers mean nothing.
302
220
  this.addItems(result, 'tail');
303
221
  await this.refreshFetchedState();
304
222
  }
@@ -309,16 +227,6 @@ export abstract class TraversableRemoteCollection<
309
227
  this.emitChangeWithDiff(!!result, originalState);
310
228
  }
311
229
 
312
- public delete(...ids: string[]): void {
313
- super.delete(...ids);
314
- this.dropDanglingGaps();
315
- }
316
-
317
- public deleteAll(): void {
318
- super.deleteAll();
319
- this.internalState.gaps = [];
320
- }
321
-
322
230
  protected abstract fetchLatestItems(): Promise<ItemT[]>;
323
231
 
324
232
  protected abstract fetchItemsBefore(): Promise<ItemT[] | null>;
@@ -347,8 +255,6 @@ export abstract class TraversableRemoteCollection<
347
255
  // Directly calls to prevent event emit.
348
256
  this._items.deleteAll();
349
257
  this._items.set(...(result.map(item => [this.getId(item), item] as [string, ItemT])));
350
-
351
- this.dropDanglingGaps();
352
258
  }
353
259
 
354
260
  protected emitChangeWithDiff(itemChanged: boolean, originalState: WindowState): void {
@@ -357,37 +263,6 @@ export abstract class TraversableRemoteCollection<
357
263
  }
358
264
  }
359
265
 
360
- /**
361
- * Record that the history is not continuous in front of the given item.
362
- */
363
- protected markGapBefore(id: string): void {
364
- if (! this.internalState.gaps.includes(id)) {
365
- this.internalState.gaps = [...this.internalState.gaps, id];
366
- }
367
- }
368
-
369
- /**
370
- * Forget the gap in front of the given item - the items before it are known
371
- * to be its real predecessors now.
372
- */
373
- protected clearGapBefore(id: string): void {
374
- if (this.internalState.gaps.includes(id)) {
375
- this.internalState.gaps = this.internalState.gaps.filter(gapId => gapId !== id);
376
- }
377
- }
378
-
379
- /**
380
- * Forget the gap markers pointing at items that are no longer in the window
381
- * (trimmed, deleted or replaced), so `gaps` never refers to nothing.
382
- */
383
- protected dropDanglingGaps(): void {
384
- if (! this.internalState.gaps.length) {
385
- return;
386
- }
387
-
388
- this.internalState.gaps = this.internalState.gaps.filter(id => this.has(id));
389
- }
390
-
391
266
  /**
392
267
  * Return array with messages trimmed using High/Low Watermark strategy.
393
268
  */
@@ -472,13 +347,6 @@ export class TopicHistoryWindow extends TraversableRemoteCollection<
472
347
  return super.resetToLatest(force);
473
348
  }
474
349
 
475
- public async resyncToLatest(): Promise<void> {
476
- if (this.internalState.traverseLock) {
477
- return;
478
- }
479
- return super.resyncToLatest();
480
- }
481
-
482
350
  public async fetchNext(): Promise<void> {
483
351
  if (this.internalState.traverseLock) {
484
352
  return;
@@ -1,52 +1,52 @@
1
- import {ChatStateTracker} from "./ChatStateTracker";
2
- import {ObservableIndexedObjectCollection} from "../IndexedObjectCollection";
3
- import {RoomMember, Session, SpaceMember, User} from "../types/src";
4
- import {extractUserFromMember} from "./functions";
5
- import {EventTarget} from "../EventTarget";
6
-
7
- export class UsersManager {
8
- public readonly onlineStatus = new EventTarget<{ change: User }>();
9
-
10
- private readonly users: ObservableIndexedObjectCollection<User> = new ObservableIndexedObjectCollection('id');
11
-
12
- public constructor(private tracker: ChatStateTracker) {
13
- // RoomMemberUpdated & SpaceMemberUpdated events are not contains user object
14
- tracker.client.on('UserUpdated', event => this.handleUsers([event.user]));
15
- tracker.client.on('RoomMembersJoined', event => this.handleMembers(event.members));
16
- tracker.client.on('SpaceMemberJoined', event => this.handleMembers([event.member]));
17
- tracker.client.on('SpaceMembers', event => this.handleMembers(event.members));
18
- tracker.client.on('RoomMembers', event => this.handleMembers(event.members));
19
- tracker.client.on('Messages', event => this.handleUsers(event.messages.map(message => message.author.user)));
20
- tracker.client.on('NewMessage', event => this.handleUsers([event.message.author.user]));
21
- tracker.client.on('Session', event => this.handleSession(event));
22
- }
23
-
24
- /**
25
- * Get all available (cached) user objects at once.
26
- */
27
- public async getAvailable(): Promise<ObservableIndexedObjectCollection<User>> {
28
- return this.users;
29
- }
30
-
31
- private handleMembers(members: (RoomMember | SpaceMember)[]): void {
32
- this.handleUsers(members.map(extractUserFromMember));
33
- }
34
-
35
- private handleSession(session: Session): void {
36
- // Keep the "seen users" cache across reconnects so bound user lists do
37
- // not blank out; just ensure our own user is present/updated. Stale
38
- // entries are refreshed as member/message collections refetch.
39
- this.handleUsers([session.user]);
40
- }
41
-
42
- private handleUsers(users: User[]): void {
43
- users.forEach(newUser => {
44
- const oldUser = this.users.get(newUser.id);
45
- if (oldUser && oldUser.online !== newUser.online) {
46
- this.onlineStatus.emit('change', newUser);
47
- }
48
- });
49
-
50
- this.users.set(...users);
51
- }
1
+ import {ChatStateTracker} from "./ChatStateTracker";
2
+ import {ObservableIndexedObjectCollection} from "../IndexedObjectCollection";
3
+ import {RoomMember, Session, SpaceMember, User} from "../types/src";
4
+ import {extractUserFromMember} from "./functions";
5
+ import {EventTarget} from "../EventTarget";
6
+
7
+ export class UsersManager {
8
+ public readonly onlineStatus = new EventTarget<{ change: User }>();
9
+
10
+ private readonly users: ObservableIndexedObjectCollection<User> = new ObservableIndexedObjectCollection('id');
11
+
12
+ public constructor(private tracker: ChatStateTracker) {
13
+ // RoomMemberUpdated & SpaceMemberUpdated events are not contains user object
14
+ tracker.client.on('UserUpdated', event => this.handleUsers([event.user]));
15
+ tracker.client.on('RoomMembersJoined', event => this.handleMembers(event.members));
16
+ tracker.client.on('SpaceMemberJoined', event => this.handleMembers([event.member]));
17
+ tracker.client.on('SpaceMembers', event => this.handleMembers(event.members));
18
+ tracker.client.on('RoomMembers', event => this.handleMembers(event.members));
19
+ tracker.client.on('Messages', event => this.handleUsers(event.messages.map(message => message.author.user)));
20
+ tracker.client.on('NewMessage', event => this.handleUsers([event.message.author.user]));
21
+ tracker.client.on('Session', event => this.handleSession(event));
22
+ }
23
+
24
+ /**
25
+ * Get all available (cached) user objects at once.
26
+ */
27
+ public async getAvailable(): Promise<ObservableIndexedObjectCollection<User>> {
28
+ return this.users;
29
+ }
30
+
31
+ private handleMembers(members: (RoomMember | SpaceMember)[]): void {
32
+ this.handleUsers(members.map(extractUserFromMember));
33
+ }
34
+
35
+ private handleSession(session: Session): void {
36
+ // Keep the "seen users" cache across reconnects so bound user lists do
37
+ // not blank out; just ensure our own user is present/updated. Stale
38
+ // entries are refreshed as member/message collections refetch.
39
+ this.handleUsers([session.user]);
40
+ }
41
+
42
+ private handleUsers(users: User[]): void {
43
+ users.forEach(newUser => {
44
+ const oldUser = this.users.get(newUser.id);
45
+ if (oldUser && oldUser.status !== newUser.status) {
46
+ this.onlineStatus.emit('change', newUser);
47
+ }
48
+ });
49
+
50
+ this.users.set(...users);
51
+ }
52
52
  }
@@ -1,29 +1,29 @@
1
- import {Role, RoomMember, SpaceMember, User} from "../types/src";
2
-
3
- export function reorderRolesOnPriorityUpdate(allRoles: Role[], oldRole: Role, updatedRole: Role): Role[] {
4
- // If the priority has changed, adjust the rest of roles
5
- const increased = (updatedRole.priority - oldRole.priority) > 0;
6
- const decreased = ! increased;
7
- const changedRoles: Role[] = [];
8
-
9
- allRoles.forEach(role => {
10
- if (role.id === updatedRole.id) {
11
- // Skip the updated role
12
- return;
13
- }
14
- if (increased && oldRole.priority <= role.priority) {
15
- role.priority--;
16
- changedRoles.push(role);
17
- }
18
- if (decreased && updatedRole.priority <= role.priority) {
19
- role.priority++;
20
- changedRoles.push(role);
21
- }
22
- });
23
-
24
- return changedRoles;
25
- }
26
-
27
- export function extractUserFromMember(member: RoomMember | SpaceMember): User | null {
28
- return member.user ?? (member as RoomMember).spaceMember?.user;
1
+ import {Role, RoomMember, SpaceMember, User} from "../types/src";
2
+
3
+ export function reorderRolesOnPriorityUpdate(allRoles: Role[], oldRole: Role, updatedRole: Role): Role[] {
4
+ // If the priority has changed, adjust the rest of roles
5
+ const increased = (updatedRole.priority - oldRole.priority) > 0;
6
+ const decreased = ! increased;
7
+ const changedRoles: Role[] = [];
8
+
9
+ allRoles.forEach(role => {
10
+ if (role.id === updatedRole.id) {
11
+ // Skip the updated role
12
+ return;
13
+ }
14
+ if (increased && oldRole.priority <= role.priority) {
15
+ role.priority--;
16
+ changedRoles.push(role);
17
+ }
18
+ if (decreased && updatedRole.priority <= role.priority) {
19
+ role.priority++;
20
+ changedRoles.push(role);
21
+ }
22
+ });
23
+
24
+ return changedRoles;
25
+ }
26
+
27
+ export function extractUserFromMember(member: RoomMember | SpaceMember): User | null {
28
+ return member.user ?? (member as RoomMember).spaceMember?.user;
29
29
  }