polfan-server-js-client 0.2.91 → 0.2.93

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.
@@ -67,9 +67,11 @@ export declare abstract class TraversableRemoteCollection<ItemT, EventMapT exten
67
67
  resetToLatest(): Promise<void>;
68
68
  fetchPrevious(): Promise<void>;
69
69
  fetchNext(): Promise<void>;
70
+ jumpTo(id: string): Promise<void>;
70
71
  protected abstract fetchLatestItems(): Promise<ItemT[]>;
71
72
  protected abstract fetchItemsBefore(): Promise<ItemT[] | null>;
72
73
  protected abstract fetchItemsAfter(): Promise<ItemT[] | null>;
74
+ protected abstract fetchItemsAround(id: string): Promise<ItemT[] | null>;
73
75
  protected abstract isLatestItemLoaded(): Promise<boolean>;
74
76
  protected refreshFetchedState(): Promise<void>;
75
77
  protected addItems(newItems: ItemT[], to: 'head' | 'tail'): void;
@@ -100,12 +102,14 @@ export declare class TopicHistoryWindow extends TraversableRemoteCollection<Mess
100
102
  resetToLatest(): Promise<void>;
101
103
  fetchNext(): Promise<void>;
102
104
  fetchPrevious(): Promise<void>;
105
+ jumpTo(id: string): Promise<void>;
103
106
  /**
104
107
  * For internal use.
105
108
  * @internal
106
109
  */
107
110
  _updateMessageReference(refTopic: Topic): void;
108
111
  protected fetchItemsAfter(): Promise<Message[] | null>;
112
+ protected fetchItemsAround(id: string): Promise<Message[] | null>;
109
113
  protected fetchItemsBefore(): Promise<Message[] | null>;
110
114
  protected fetchLatestItems(): Promise<Message[]>;
111
115
  private getTopic;
@@ -3,5 +3,6 @@ export interface GetMessages {
3
3
  location: ChatLocation;
4
4
  before?: string;
5
5
  after?: string;
6
+ around?: string;
6
7
  limit?: number;
7
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polfan-server-js-client",
3
- "version": "0.2.91",
3
+ "version": "0.2.93",
4
4
  "description": "JavaScript client library for handling communication with Polfan chat server.",
5
5
  "author": "Jarosław Żak",
6
6
  "license": "MIT",
@@ -153,18 +153,17 @@ export class RoomsManager {
153
153
  for (const room of this.list.findBy('spaceId', ev.spaceId).items) {
154
154
  const roomMembers = this.members.get(room.id);
155
155
 
156
- if (! roomMembers || ! roomMembers.has(ev.userId)) {
156
+ if (!roomMembers || !roomMembers.has(ev.userId)) {
157
157
  // Skip update if member list for this room is not loaded
158
158
  // or user is not in room
159
159
  continue;
160
160
  }
161
161
 
162
+ // Update space member in roomMember, but first fill the user object (it's null in event)
162
163
  const roomMember = roomMembers.get(ev.userId);
163
- const user = roomMember.spaceMember.user;
164
-
165
- // Update space member but first fill user object (it's null in event object)
166
- roomMember.spaceMember = {...ev.member, user};
167
- roomMembers.set(roomMember);
164
+ const spaceMember = ev.member;
165
+ spaceMember.user = roomMember.spaceMember.user;
166
+ roomMembers.set({ ...roomMember, spaceMember });
168
167
  }
169
168
  }
170
169
 
@@ -243,8 +242,9 @@ export class RoomsManager {
243
242
  }
244
243
 
245
244
  if (room.defaultTopic.id === ev.topic.id) {
246
- room.defaultTopic = ev.topic;
247
- this.list.set(room);
245
+ const newRoom = { ...room };
246
+ newRoom.defaultTopic = ev.topic;
247
+ this.list.set(newRoom);
248
248
  }
249
249
  }
250
250
 
@@ -202,12 +202,40 @@ export abstract class TraversableRemoteCollection<
202
202
  }
203
203
  }
204
204
 
205
+ public async jumpTo(id: string): Promise<void> {
206
+ if (this.internalState.ongoing || this._items.has(id)) {
207
+ return;
208
+ }
209
+
210
+ let result: ItemT[] | null;
211
+ const originalState = this.state;
212
+ this.internalState.ongoing = WindowState.PAST;
213
+
214
+ try {
215
+ result = await this.fetchItemsAround(id);
216
+ this.internalState.lastFetchCount = result ? result.length : 0;
217
+ } finally {
218
+ this.internalState.ongoing = undefined;
219
+ }
220
+
221
+ if (!result) {
222
+ return;
223
+ }
224
+
225
+ this._items.deleteAll(); // Directly call deleteAll to prevent event emit.
226
+ this.addItems(result, 'tail');
227
+ this.internalState.current = WindowState.PAST;
228
+ this.emitChangeWithDiff(true, originalState);
229
+ }
230
+
205
231
  protected abstract fetchLatestItems(): Promise<ItemT[]>;
206
232
 
207
233
  protected abstract fetchItemsBefore(): Promise<ItemT[] | null>;
208
234
 
209
235
  protected abstract fetchItemsAfter(): Promise<ItemT[] | null>;
210
236
 
237
+ protected abstract fetchItemsAround(id: string): Promise<ItemT[] | null>;
238
+
211
239
  protected abstract isLatestItemLoaded(): Promise<boolean>;
212
240
 
213
241
  protected async refreshFetchedState(): Promise<void> {
@@ -334,6 +362,13 @@ export class TopicHistoryWindow extends TraversableRemoteCollection<
334
362
  return super.fetchPrevious();
335
363
  }
336
364
 
365
+ public async jumpTo(id: string): Promise<void> {
366
+ if (this.internalState.traverseLock) {
367
+ return;
368
+ }
369
+ return super.jumpTo(id);
370
+ }
371
+
337
372
  /**
338
373
  * For internal use.
339
374
  * @internal
@@ -368,6 +403,20 @@ export class TopicHistoryWindow extends TraversableRemoteCollection<
368
403
  return result.data.messages;
369
404
  }
370
405
 
406
+ protected async fetchItemsAround(id: string): Promise<Message[] | null> {
407
+ const result = await this.tracker.client.send('GetMessages', {
408
+ location: {roomId: this.roomId, topicId: this.topicId},
409
+ around: id,
410
+ limit: this.internalState.fetchLimit,
411
+ });
412
+
413
+ if (result.error) {
414
+ throw new Error(`Cannot fetch messages: ${result.error.message}`);
415
+ }
416
+
417
+ return result.data.messages;
418
+ }
419
+
371
420
  protected async fetchItemsBefore(): Promise<Message[] | null> {
372
421
  const beforeId = this.getAt(0)?.id;
373
422
 
@@ -446,4 +495,4 @@ export class TopicHistoryWindow extends TraversableRemoteCollection<
446
495
  this.eventTarget.emit('reftopicsdeleted', refTopicIds);
447
496
  }
448
497
  }
449
- }
498
+ }
@@ -4,5 +4,6 @@ export interface GetMessages {
4
4
  location: ChatLocation;
5
5
  before?: string;
6
6
  after?: string;
7
+ around?: string;
7
8
  limit?: number;
8
9
  }
@@ -18,12 +18,32 @@ const messages: SimpleMessage[] = [
18
18
  ];
19
19
 
20
20
  class TestableHistoryWindow extends TraversableRemoteCollection<SimpleMessage> {
21
+ declare protected internalState: TraversableRemoteCollection<SimpleMessage>['internalState'] & {
22
+ traverseLock: boolean,
23
+ };
24
+
21
25
  public createMirror(): TraversableRemoteCollection<SimpleMessage> {
22
26
  throw new Error('Method not implemented.');
23
27
  }
24
28
 
25
29
  public constructor() {
26
30
  super('id');
31
+ this.internalState.traverseLock = false;
32
+ }
33
+
34
+ public async setTraverseLock(lock: boolean): Promise<void> {
35
+ this.internalState.traverseLock = lock;
36
+
37
+ if (lock && (this.state !== WindowState.LIVE && this.state !== WindowState.LATEST)) {
38
+ await super.resetToLatest();
39
+ }
40
+ }
41
+
42
+ public async jumpTo(id: string): Promise<void> {
43
+ if (this.internalState.traverseLock) {
44
+ return;
45
+ }
46
+ return super.jumpTo(id);
27
47
  }
28
48
 
29
49
  public simulateNewMessageReceived(): void {
@@ -49,6 +69,18 @@ class TestableHistoryWindow extends TraversableRemoteCollection<SimpleMessage> {
49
69
  return messages.slice(after + 1, after + 4);
50
70
  }
51
71
 
72
+ protected async fetchItemsAround(id: string): Promise<SimpleMessage[] | null> {
73
+ const numericId = parseInt(id, 10);
74
+ const item = messages.find(m => m.id === numericId);
75
+ if (!item) {
76
+ return null;
77
+ }
78
+ const index = messages.indexOf(item);
79
+ const start = Math.max(0, index - Math.floor(this.fetchLimit / 2));
80
+ const end = Math.min(messages.length, index + Math.ceil(this.fetchLimit / 2));
81
+ return messages.slice(start, end);
82
+ }
83
+
52
84
  protected async fetchItemsBefore(): Promise<SimpleMessage[]> {
53
85
  const before = this.getAt(0)?.id;
54
86
  if (before === undefined) {
@@ -177,6 +209,57 @@ test('history window - reset to latest', async () => {
177
209
  [7,8,9].forEach(id => expect(window.items.map(item => item.id)).toContain(id));
178
210
  });
179
211
 
212
+ test('history window - jump to message', async () => {
213
+ const window = new TestableHistoryWindow();
214
+ window.limit = 5;
215
+ window.fetchLimit = 3;
216
+
217
+ await window.resetToLatest(); // [7,8,9]
218
+ await window.jumpTo('2'); // [1,2,3]
219
+
220
+ expect(window.state).toEqual(WindowState.PAST);
221
+ expect(window.items).toHaveLength(3);
222
+ [1,2,3].forEach(id => expect(window.items.map(item => item.id)).toContain(id));
223
+ });
224
+
225
+ test('history window - jump to message already in window', async () => {
226
+ const window = new TestableHistoryWindow();
227
+ window.limit = 5;
228
+ window.fetchLimit = 3;
229
+
230
+ await window.resetToLatest(); // [7,8,9]
231
+ const itemsBeforeJump = window.items;
232
+ await window.jumpTo('8');
233
+
234
+ expect(window.items).toEqual(itemsBeforeJump);
235
+ });
236
+
237
+ test('history window - jump to non-existent message', async () => {
238
+ const window = new TestableHistoryWindow();
239
+ window.limit = 5;
240
+ window.fetchLimit = 3;
241
+
242
+ await window.resetToLatest(); // [7,8,9]
243
+ const itemsBeforeJump = window.items;
244
+ await window.jumpTo('99'); // non-existent
245
+
246
+ expect(window.items).toEqual(itemsBeforeJump);
247
+ });
248
+
249
+ test('history window - jump to message with traverseLock', async () => {
250
+ const window = new TestableHistoryWindow();
251
+ window.limit = 5;
252
+ window.fetchLimit = 3;
253
+
254
+ await window.resetToLatest(); // [7,8,9]
255
+ await window.setTraverseLock(true);
256
+ await window.jumpTo('2');
257
+
258
+ expect(window.state).toEqual(WindowState.LATEST);
259
+ expect(window.items).toHaveLength(3);
260
+ [7,8,9].forEach(id => expect(window.items.map(item => item.id)).toContain(id));
261
+ });
262
+
180
263
  test('history window - trim messages window to limit', async () => {
181
264
  const window = new TestableHistoryWindow();
182
265
  window.limit = 5;
@@ -284,4 +367,4 @@ test('history window - high/low watermark limit (retainRatio)', async () => {
284
367
  expect(window.items).toHaveLength(3);
285
368
  [7, 8, 9].forEach(id => expect(window.items.map(item => item.id)).toContain(id));
286
369
  expect(window.state).toEqual(WindowState.LATEST);
287
- });
370
+ });