polfan-server-js-client 0.2.3 → 0.2.7

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 (93) hide show
  1. package/.idea/copilot.data.migration.agent.xml +6 -0
  2. package/.idea/copilot.data.migration.ask.xml +6 -0
  3. package/.idea/copilot.data.migration.ask2agent.xml +6 -0
  4. package/.idea/copilot.data.migration.edit.xml +6 -0
  5. package/.idea/workspace.xml +401 -177
  6. package/README.md +22 -2
  7. package/babel.config.js +4 -5
  8. package/build/index.cjs.js +4595 -1816
  9. package/build/index.cjs.js.map +1 -1
  10. package/build/index.umd.js +1 -1
  11. package/build/index.umd.js.map +1 -1
  12. package/build/types/AbstractChatClient.d.ts +14 -2
  13. package/build/types/FilesClient.d.ts +7 -6
  14. package/build/types/IndexedObjectCollection.d.ts +4 -3
  15. package/build/types/Permissions.d.ts +4 -0
  16. package/build/types/WebSocketChatClient.d.ts +23 -0
  17. package/build/types/state-tracker/ChatStateTracker.d.ts +5 -0
  18. package/build/types/state-tracker/RelationshipsManager.d.ts +15 -0
  19. package/build/types/state-tracker/RoomMessagesHistory.d.ts +2 -0
  20. package/build/types/state-tracker/SpacesManager.d.ts +1 -0
  21. package/build/types/state-tracker/TopicHistoryWindow.d.ts +23 -6
  22. package/build/types/state-tracker/UsersManager.d.ts +3 -1
  23. package/build/types/types/src/index.d.ts +14 -3
  24. package/build/types/types/src/schemes/Emoticon.d.ts +1 -0
  25. package/build/types/types/src/schemes/Message.d.ts +1 -1
  26. package/build/types/types/src/schemes/Room.d.ts +2 -0
  27. package/build/types/types/src/schemes/RoomHistory.d.ts +5 -0
  28. package/build/types/types/src/schemes/RoomMember.d.ts +3 -0
  29. package/build/types/types/src/schemes/RoomSummary.d.ts +1 -0
  30. package/build/types/types/src/schemes/SpaceSummary.d.ts +1 -0
  31. package/build/types/types/src/schemes/User.d.ts +2 -2
  32. package/build/types/types/src/schemes/UserRelationship.d.ts +6 -0
  33. package/build/types/types/src/schemes/commands/CreateMessage.d.ts +2 -0
  34. package/build/types/types/src/schemes/commands/CreateRelationship.d.ts +5 -0
  35. package/build/types/types/src/schemes/commands/CreateTopic.d.ts +5 -2
  36. package/build/types/types/src/schemes/commands/DeleteRelationship.d.ts +5 -0
  37. package/build/types/types/src/schemes/commands/GetRelationships.d.ts +2 -0
  38. package/build/types/types/src/schemes/commands/Ping.d.ts +2 -0
  39. package/build/types/types/src/schemes/commands/UpdateRoom.d.ts +2 -0
  40. package/build/types/types/src/schemes/commands/UpdateRoomMember.d.ts +7 -0
  41. package/build/types/types/src/schemes/commands/UpdateSpaceMember.d.ts +5 -0
  42. package/build/types/types/src/schemes/events/NewRelationship.d.ts +4 -0
  43. package/build/types/types/src/schemes/events/Pong.d.ts +2 -0
  44. package/build/types/types/src/schemes/events/RelationshipDeleted.d.ts +4 -0
  45. package/build/types/types/src/schemes/events/Relationships.d.ts +4 -0
  46. package/build/types/types/src/schemes/events/RoomSummaryUpdated.d.ts +7 -0
  47. package/build/types/types/src/schemes/events/Session.d.ts +1 -0
  48. package/package.json +15 -28
  49. package/src/AbstractChatClient.ts +30 -4
  50. package/src/FilesClient.ts +26 -13
  51. package/src/IndexedObjectCollection.ts +26 -10
  52. package/src/Permissions.ts +1 -0
  53. package/src/WebSocketChatClient.ts +92 -14
  54. package/src/state-tracker/ChatStateTracker.ts +22 -6
  55. package/src/state-tracker/EmoticonsManager.ts +6 -4
  56. package/src/state-tracker/MessagesManager.ts +3 -4
  57. package/src/state-tracker/RelationshipsManager.ts +68 -0
  58. package/src/state-tracker/RoomMessagesHistory.ts +20 -3
  59. package/src/state-tracker/RoomsManager.ts +38 -8
  60. package/src/state-tracker/SpacesManager.ts +28 -1
  61. package/src/state-tracker/TopicHistoryWindow.ts +92 -32
  62. package/src/state-tracker/UsersManager.ts +16 -6
  63. package/src/types/src/index.ts +30 -5
  64. package/src/types/src/schemes/Emoticon.ts +1 -0
  65. package/src/types/src/schemes/Message.ts +1 -1
  66. package/src/types/src/schemes/Room.ts +2 -0
  67. package/src/types/src/schemes/RoomHistory.ts +6 -0
  68. package/src/types/src/schemes/RoomMember.ts +3 -0
  69. package/src/types/src/schemes/RoomSummary.ts +1 -0
  70. package/src/types/src/schemes/SpaceSummary.ts +1 -0
  71. package/src/types/src/schemes/User.ts +2 -2
  72. package/src/types/src/schemes/UserRelationship.ts +8 -0
  73. package/src/types/src/schemes/commands/CreateMessage.ts +2 -0
  74. package/src/types/src/schemes/commands/CreateRelationship.ts +6 -0
  75. package/src/types/src/schemes/commands/CreateTopic.ts +6 -2
  76. package/src/types/src/schemes/commands/DeleteRelationship.ts +6 -0
  77. package/src/types/src/schemes/commands/GetRelationships.ts +3 -0
  78. package/src/types/src/schemes/commands/Ping.ts +3 -0
  79. package/src/types/src/schemes/commands/UpdateRoom.ts +2 -0
  80. package/src/types/src/schemes/commands/UpdateRoomMember.ts +7 -0
  81. package/src/types/src/schemes/commands/UpdateSpaceMember.ts +5 -0
  82. package/src/types/src/schemes/events/NewRelationship.ts +5 -0
  83. package/src/types/src/schemes/events/Pong.ts +3 -0
  84. package/src/types/src/schemes/events/RelationshipDeleted.ts +5 -0
  85. package/src/types/src/schemes/events/Relationships.ts +5 -0
  86. package/src/types/src/schemes/events/RoomSummaryUpdated.ts +8 -0
  87. package/src/types/src/schemes/events/Session.ts +1 -0
  88. package/tests/history-window.test.ts +6 -1
  89. package/webpack.config.browser.js +2 -24
  90. package/webpack.config.node.js +2 -14
  91. package/.eslintignore +0 -0
  92. package/.eslintrc.json +0 -0
  93. package/src/types/src/schemes/commands/SetCustomNick.ts +0 -5
@@ -82,14 +82,30 @@ import {
82
82
  GetEmoticons,
83
83
  Emoticons,
84
84
  EmoticonDeleted,
85
- NewEmoticon, Bans, GetBans, Ban, Unban, Kick, ClientData, GetClientData, SetClientData,
85
+ NewEmoticon,
86
+ Bans,
87
+ GetBans,
88
+ Ban,
89
+ Unban,
90
+ Kick,
91
+ ClientData,
92
+ GetClientData,
93
+ SetClientData,
86
94
  GetRoomSummary,
87
95
  GetSpaceSummary,
88
96
  RoomSummaryEvent,
89
97
  SpaceSummaryEvent,
90
- SetCustomNick,
98
+ UpdateSpaceMember,
99
+ Relationships,
100
+ RelationshipDeleted,
101
+ NewRelationship,
102
+ DeleteRelationship,
103
+ CreateRelationship,
104
+ RoomSummaryUpdated, Pong, Ping,
91
105
  } from "./types/src/index";
92
106
  import {EventTarget} from "./EventTarget";
107
+ import {GetRelationships} from "./types/src/schemes/commands/GetRelationships";
108
+ import {UpdateRoomMember} from "./types/src/schemes/commands/UpdateRoomMember";
93
109
 
94
110
  type ArrayOfPromiseResolvers = [(value: any) => void, (reason?: any) => void];
95
111
 
@@ -139,7 +155,7 @@ export abstract class AbstractChatClient extends EventTarget {
139
155
  if (!this.awaitingResponse.has(envelope.ref)) {
140
156
  return;
141
157
  }
142
- this.awaitingResponse.get(envelope.ref)[0](error);
158
+ this.awaitingResponse.get(envelope.ref)[1](error);
143
159
  this.awaitingResponse.delete(envelope.ref);
144
160
  }
145
161
  }
@@ -165,6 +181,10 @@ export type EventsMap = {
165
181
  Emoticons: Emoticons,
166
182
  Bans: Bans,
167
183
  ClientData: ClientData,
184
+ NewRelationship: NewRelationship,
185
+ RelationshipDeleted: RelationshipDeleted,
186
+ Relationships: Relationships,
187
+ Pong: Pong,
168
188
  // Space events
169
189
  DiscoverableSpaces: DiscoverableSpaces,
170
190
  SpaceJoined: SpaceJoined,
@@ -191,6 +211,7 @@ export type EventsMap = {
191
211
  RoomDeleted: RoomDeleted,
192
212
  RoomUpdated: RoomUpdated,
193
213
  RoomSummaryEvent: RoomSummaryEvent,
214
+ RoomSummaryUpdated: RoomSummaryUpdated,
194
215
  // Topic events
195
216
  NewTopic: NewTopic,
196
217
  TopicDeleted: TopicDeleted,
@@ -227,6 +248,10 @@ export type CommandsMap = {
227
248
  Kick: [Kick, EventsMap['Ok']],
228
249
  GetClientData: [GetClientData, EventsMap['ClientData']],
229
250
  SetClientData: [SetClientData, EventsMap['Ok']],
251
+ DeleteRelationship: [DeleteRelationship, EventsMap['RelationshipDeleted']],
252
+ CreateRelationship: [CreateRelationship, EventsMap['NewRelationship']],
253
+ GetRelationships: [GetRelationships, EventsMap['Relationships']],
254
+ Ping: [Ping, EventsMap['Pong']],
230
255
  // Space commands
231
256
  GetDiscoverableSpaces: [GetDiscoverableSpaces, EventsMap['DiscoverableSpaces']],
232
257
  JoinSpace: [JoinSpace, EventsMap['SpaceJoined']],
@@ -242,7 +267,7 @@ export type CommandsMap = {
242
267
  AssignRole: [AssignRole, EventsMap['SpaceMemberUpdated'] | EventsMap['RoomMemberUpdated']],
243
268
  DeassignRole: [DeassignRole, EventsMap['SpaceMemberUpdated'] | EventsMap['RoomMemberUpdated']],
244
269
  GetSpaceSummary: [GetSpaceSummary, EventsMap['SpaceSummaryEvent']],
245
- SetCustomNick: [SetCustomNick, EventsMap['SpaceMemberUpdated']],
270
+ UpdateSpaceMember: [UpdateSpaceMember, EventsMap['SpaceMemberUpdated']],
246
271
  // Room commands
247
272
  JoinRoom: [JoinRoom, EventsMap['RoomJoined']],
248
273
  LeaveRoom: [LeaveRoom, EventsMap['RoomLeft']],
@@ -251,6 +276,7 @@ export type CommandsMap = {
251
276
  UpdateRoom: [UpdateRoom, EventsMap['RoomUpdated']],
252
277
  GetRoomMembers: [GetRoomMembers, EventsMap['RoomMembers']],
253
278
  GetRoomSummary: [GetRoomSummary, EventsMap['RoomSummaryEvent']],
279
+ UpdateRoomMember: [UpdateRoomMember, EventsMap['RoomMemberUpdated']],
254
280
  // Topic commands
255
281
  CreateTopic: [CreateTopic, EventsMap['NewTopic']],
256
282
  DeleteTopic: [DeleteTopic, EventsMap['TopicDeleted']],
@@ -3,28 +3,41 @@ import {AbstractRestClient, RestClientResponse} from "./AbstractRestClient";
3
3
  export interface File {
4
4
  id: string;
5
5
  url: string;
6
- original_url: string;
7
- original_name: string;
8
- mime_type: string;
6
+ name: string;
7
+ mime: string;
9
8
  size: number;
10
- image_dimensions: [number, number] | null;
9
+ width?: number;
10
+ height?: number;
11
11
  }
12
12
 
13
13
  export class FilesClient extends AbstractRestClient {
14
- protected defaultUrl: string = 'https://polfan.pl/webservice/api/files';
14
+ protected defaultUrl: string = 'https://files.devana.pl';
15
15
 
16
- public async uploadFile(file: Parameters<typeof FormData.prototype.append>[1]): Promise<RestClientResponse<File>> {
17
- const formData = new FormData();
18
- formData.append('file', file);
16
+ public async uploadFile(file: globalThis.File | Blob): Promise<RestClientResponse<File>> {
17
+ const name = encodeURIComponent((file as globalThis.File).name ?? '');
18
+ let headers = {
19
+ ...this.getAuthHeaders(),
20
+ Accept: 'application/json',
21
+ 'Content-Disposition': `attachment; filename="${name}"`,
22
+ 'Content-Length': file.size
23
+ };
19
24
 
20
- let headers = {...this.getAuthHeaders(), Accept: 'application/json'};
21
-
22
- const response = await fetch(this.defaultUrl, {method: 'POST', body: formData, headers});
25
+ const response = await fetch(this.getUrl('files'), {
26
+ method: 'POST',
27
+ body: file,
28
+ headers
29
+ });
23
30
 
24
31
  return this.convertFetchResponse<File>(response);
25
32
  }
26
33
 
27
- public async getFileMetadata(id: string): Promise<RestClientResponse<File>> {
28
- return this.send('GET', '/' + id);
34
+ public async getFileMeta(id: string): Promise<RestClientResponse<File>> {
35
+ return this.send('GET', 'files/' + id);
36
+ }
37
+
38
+ public async getFileMetaBulk(ids: string[]): Promise<RestClientResponse<File[]>> {
39
+ const searchParams = new URLSearchParams();
40
+ ids.forEach(id => searchParams.append('id[]', id));
41
+ return this.send('GET', 'files?' + searchParams);
29
42
  }
30
43
  }
@@ -2,16 +2,11 @@ import {EventTarget, ObservableInterface} from "./EventTarget";
2
2
 
3
3
  export class IndexedCollection<KeyT, ValueT> {
4
4
  protected _items: Map<KeyT, ValueT> = new Map();
5
- protected _mutationCounter: number = 0;
6
5
 
7
6
  public constructor(items: [key: KeyT, value: ValueT][] = []) {
8
7
  this.set(...items);
9
8
  }
10
9
 
11
- public get mutationCounter(): number {
12
- return this._mutationCounter;
13
- }
14
-
15
10
  public get items(): Map<KeyT, ValueT> {
16
11
  return this._items;
17
12
  }
@@ -21,7 +16,6 @@ export class IndexedCollection<KeyT, ValueT> {
21
16
  }
22
17
 
23
18
  public set(...items: [KeyT, ValueT][]): void {
24
- this._mutationCounter++;
25
19
  for (const item of items) {
26
20
  this._items.set(item[0], item[1]);
27
21
  }
@@ -58,6 +52,12 @@ export class IndexedCollection<KeyT, ValueT> {
58
52
  }
59
53
  return result;
60
54
  }
55
+
56
+ public createMirror(): IndexedCollection<KeyT, ValueT> {
57
+ const copy = new IndexedCollection<KeyT, ValueT>();
58
+ copy._items = this._items;
59
+ return copy;
60
+ }
61
61
  }
62
62
 
63
63
  export class IndexedObjectCollection<T> {
@@ -79,10 +79,6 @@ export class IndexedObjectCollection<T> {
79
79
  return this._items.length;
80
80
  }
81
81
 
82
- public get mutationCounter(): number {
83
- return this._items.mutationCounter;
84
- }
85
-
86
82
  public set(...items: T[]): void {
87
83
  this._items.set(...(items.map(item => [this.getId(item), item] as [string, T])));
88
84
  }
@@ -120,6 +116,12 @@ export class IndexedObjectCollection<T> {
120
116
  return result;
121
117
  }
122
118
 
119
+ public createMirror(): IndexedObjectCollection<T> {
120
+ const copy = new IndexedObjectCollection<T>(this.id);
121
+ copy._items = this._items;
122
+ return copy;
123
+ }
124
+
123
125
  protected getId(item: T): any {
124
126
  return typeof this.id === 'function' ? this.id(item) : item[this.id];
125
127
  }
@@ -161,6 +163,13 @@ export class ObservableIndexedCollection<KeyT, ValueT> extends IndexedCollection
161
163
  }
162
164
  }
163
165
 
166
+ public createMirror(): ObservableIndexedCollection<KeyT, ValueT> {
167
+ const copy = new ObservableIndexedCollection<KeyT, ValueT>();
168
+ copy.eventTarget = this.eventTarget;
169
+ copy._items = this._items;
170
+ return copy;
171
+ }
172
+
164
173
  public on(eventName: 'change', handler: (ev?: ObservableCollectionEvent<KeyT>) => void): this {
165
174
  this.eventTarget.on(eventName, handler);
166
175
  return this;
@@ -211,6 +220,13 @@ export class ObservableIndexedObjectCollection<T> extends IndexedObjectCollectio
211
220
  }
212
221
  }
213
222
 
223
+ public createMirror(): IndexedObjectCollection<T> {
224
+ const copy = new ObservableIndexedObjectCollection<T>(this.id);
225
+ copy.eventTarget = this.eventTarget;
226
+ copy._items = this._items;
227
+ return copy;
228
+ }
229
+
214
230
  public on(eventName: 'change', handler: (ev?: ObservableCollectionEvent<string>) => void): this {
215
231
  this.eventTarget.on(eventName, handler);
216
232
  return this;
@@ -30,6 +30,7 @@ export class Permissions {
30
30
  ManageBan: {value: 1 << 15, maxLayer: Layer.Room},
31
31
  Kick: {value: 1 << 16, maxLayer: Layer.Room},
32
32
  ChangeOwnNick: {value: 1 << 17, maxLayer: Layer.Space},
33
+ ChangeOwnColor: {value: 1 << 18, maxLayer: Layer.Room},
33
34
  };
34
35
 
35
36
  public static getNames(): (keyof typeof this.list)[] {
@@ -1,5 +1,5 @@
1
1
  import {ObservableInterface} from "./EventTarget";
2
- import {AbstractChatClient, CommandResult, CommandsMap} from "./AbstractChatClient";
2
+ import {AbstractChatClient, CommandResult, CommandsMap, EventsMap} from "./AbstractChatClient";
3
3
  import {ChatStateTracker} from "./state-tracker/ChatStateTracker";
4
4
  import {Envelope} from "./types/src";
5
5
 
@@ -10,6 +10,20 @@ export interface WebSocketClientOptions {
10
10
  awaitQueueSendDelayMs?: number;
11
11
  stateTracking?: boolean;
12
12
  queryParams?: Record<string, string>;
13
+ /**
14
+ * Ping/pong configuration, enabled by default.
15
+ */
16
+ ping?: {
17
+ enabled?: boolean;
18
+ /**
19
+ * Time without activity after which a ping will be sent. Default is 10 seconds.
20
+ */
21
+ noActivityTimeoutMs?: number;
22
+ /**
23
+ * Time to wait for a pong response before considering the connection dead. Default is 2 seconds.
24
+ */
25
+ pongBackTimeoutMs?: number;
26
+ },
13
27
  }
14
28
 
15
29
  enum WebSocketChatClientEvent {
@@ -28,15 +42,27 @@ export class WebSocketChatClient extends AbstractChatClient implements Observabl
28
42
  protected connectingTimeoutId: any;
29
43
  protected authenticated: boolean;
30
44
  protected authenticatedResolvers: [() => void, (error: Error) => void];
45
+ protected pingIntervalId?: NodeJS.Timeout;
46
+ protected lastReceivedMessageAt?: number;
47
+ protected pingInFlight: boolean;
31
48
 
32
49
  public constructor(private readonly options: WebSocketClientOptions) {
33
50
  super();
34
51
  if (this.options.stateTracking ?? true) {
35
52
  this.state = new ChatStateTracker(this);
36
53
  }
54
+
55
+ options.ping ??= {};
56
+ options.ping.enabled ??= true;
57
+ options.ping.noActivityTimeoutMs ??= 15000;
58
+ options.ping.pongBackTimeoutMs ??= 5000;
37
59
  }
38
60
 
39
61
  public async connect(): Promise<void> {
62
+ if (this.isOpenWsState() || this.isConnectingWsState()) {
63
+ return;
64
+ }
65
+
40
66
  const params = new URLSearchParams(this.options.queryParams ?? {});
41
67
  params.set('token', this.options.token);
42
68
 
@@ -48,43 +74,47 @@ export class WebSocketChatClient extends AbstractChatClient implements Observabl
48
74
  this.options.connectingTimeoutMs ?? 10000
49
75
  );
50
76
  this.authenticated = false;
51
-
52
77
  return new Promise((...args) => this.authenticatedResolvers = args);
53
78
  }
54
79
 
55
80
  public disconnect(): void {
56
81
  this.sendQueue = [];
57
- this.ws?.close();
82
+ this.ws?.close(1000); // Normal closure
58
83
  this.ws = null;
59
84
  }
60
85
 
61
86
  public async send<CommandType extends keyof CommandsMap>(commandType: CommandType, commandData: CommandsMap[CommandType][0]):
62
87
  Promise<CommandResult<CommandsMap[CommandType][1]>> {
63
- if (!this.ws || [this.ws.CLOSED, this.ws.CLOSING].includes(this.ws.readyState)) {
64
- throw new Error('Cannot send; close or closing connection state');
65
- }
66
-
67
88
  const envelope = this.createEnvelope<CommandsMap[CommandType][0]>(commandType, commandData);
68
89
  const promise = this.createPromiseFromCommandEnvelope<CommandType>(envelope);
69
90
 
70
- if (this.ws.readyState === this.ws.CONNECTING || !this.authenticated) {
91
+ if (this.isConnectingWsState() || !this.authenticated && this.isOpenWsState()) {
71
92
  this.sendQueue.push(envelope);
72
93
  return promise;
73
94
  }
74
95
 
75
- if (this.ws.readyState !== this.ws.OPEN) {
76
- throw new Error(`Invalid websocket state=${this.ws.readyState}`);
77
- }
78
-
79
96
  this.sendEnvelope(envelope);
80
97
  return promise;
81
98
  }
82
99
 
100
+ public get isReady(): boolean {
101
+ return this.isOpenWsState() && this.authenticated;
102
+ }
103
+
83
104
  private sendEnvelope(envelope: Envelope): void {
84
- this.ws.send(JSON.stringify(envelope));
105
+ if (this.isReady) {
106
+ this.ws.send(JSON.stringify(envelope));
107
+ return;
108
+ }
109
+
110
+ this.handleEnvelopeSendError(
111
+ envelope,
112
+ new Error(`Cannot send - client is not ready (state=${this.ws?.readyState ?? '[no connection]'}; authenticated=${this.authenticated})`)
113
+ );
85
114
  }
86
115
 
87
116
  private onMessage(event: MessageEvent): void {
117
+ this.lastReceivedMessageAt = Date.now();
88
118
  const envelope: Envelope = JSON.parse(event.data);
89
119
  this.handleIncomingEnvelope(envelope);
90
120
  this.emit(envelope.type, envelope.data);
@@ -95,6 +125,7 @@ export class WebSocketChatClient extends AbstractChatClient implements Observabl
95
125
  const isAuthenticated = envelope.type !== 'Bye';
96
126
  this.authenticated = isAuthenticated;
97
127
  if (isAuthenticated) {
128
+ this.startConnectionMonitor();
98
129
  this.authenticatedResolvers[0]();
99
130
  this.emit(this.Event.connect);
100
131
  this.sendFromQueue();
@@ -105,10 +136,11 @@ export class WebSocketChatClient extends AbstractChatClient implements Observabl
105
136
  }
106
137
 
107
138
  private onClose(event: CloseEvent): void {
139
+ this.stopConnectionMonitor();
108
140
  clearTimeout(this.connectingTimeoutId);
109
141
  const reconnect = event.code !== 1000; // Connection was closed because of error
110
142
  if (reconnect) {
111
- this.connect();
143
+ void this.connect();
112
144
  }
113
145
  this.emit(this.Event.disconnect, reconnect);
114
146
  }
@@ -129,4 +161,50 @@ export class WebSocketChatClient extends AbstractChatClient implements Observabl
129
161
  this.disconnect();
130
162
  this.emit(this.Event.error, new Error('Connection timeout'));
131
163
  }
164
+
165
+ private isConnectingWsState(): boolean {
166
+ return this.ws && this.ws.readyState === this.ws.CONNECTING;
167
+ }
168
+
169
+ private isOpenWsState(): boolean {
170
+ return this.ws && this.ws.readyState === this.ws.OPEN;
171
+ }
172
+
173
+ private startConnectionMonitor(): void {
174
+ if (!this.options.ping!.enabled) {
175
+ return;
176
+ }
177
+
178
+ this.lastReceivedMessageAt = Date.now();
179
+
180
+ this.pingIntervalId = setInterval(async () => {
181
+ if (!this.isReady || this.pingInFlight) {
182
+ return;
183
+ }
184
+
185
+ if ((Date.now() - this.lastReceivedMessageAt) < this.options.ping!.noActivityTimeoutMs) {
186
+ return;
187
+ }
188
+
189
+ const timeout = setTimeout(() => {
190
+ this.pingInFlight = false;
191
+ this.ws.close(3000); // Service Restart (reconnect)
192
+ }, this.options.ping.pongBackTimeoutMs);
193
+
194
+ this.pingInFlight = true;
195
+
196
+ this.send('Ping', {}).then(() => {
197
+ this.pingInFlight = false;
198
+ clearTimeout(timeout);
199
+ });
200
+ }, 1000);
201
+ }
202
+
203
+ private stopConnectionMonitor(): void {
204
+ if (this.pingIntervalId) {
205
+ clearInterval(this.pingIntervalId);
206
+ this.pingIntervalId = undefined;
207
+ }
208
+ this.pingInFlight = false;
209
+ }
132
210
  }
@@ -6,38 +6,54 @@ import {PermissionsManager} from "./PermissionsManager";
6
6
  import {DeferredTask} from "./AsyncUtils";
7
7
  import {EmoticonsManager} from "./EmoticonsManager";
8
8
  import {UsersManager} from "./UsersManager";
9
+ import {RelationshipsManager} from "./RelationshipsManager";
9
10
 
10
11
  export class ChatStateTracker {
12
+ public readonly client: WebSocketChatClient;
13
+
11
14
  /**
12
15
  * State of your permissions.
13
16
  */
14
- public readonly permissions = new PermissionsManager(this);
17
+ public readonly permissions: PermissionsManager;
15
18
 
16
19
  /**
17
20
  * State of the rooms you are in.
18
21
  */
19
- public readonly rooms: RoomsManager = new RoomsManager(this);
22
+ public readonly rooms: RoomsManager;
20
23
 
21
24
  /**
22
25
  * State of the spaces you are in.
23
26
  */
24
- public readonly spaces = new SpacesManager(this);
27
+ public readonly spaces: SpacesManager;
25
28
 
26
29
  /**
27
30
  * State of the emoticons (global and space-related).
28
31
  */
29
- public readonly emoticons = new EmoticonsManager(this);
32
+ public readonly emoticons: EmoticonsManager;
30
33
 
31
34
  /**
32
35
  * Users related state.
33
36
  */
34
- public readonly users = new UsersManager(this);
37
+ public readonly users: UsersManager;
38
+
39
+ /**
40
+ * State of relationships with other users.
41
+ */
42
+ public readonly relationships: RelationshipsManager;
35
43
 
36
44
  private _me: User = null;
37
45
  private readonly deferredSession = new DeferredTask();
38
46
 
39
- public constructor(public readonly client: WebSocketChatClient) {
47
+ public constructor(client: WebSocketChatClient) {
48
+ this.client = client;
40
49
  this.client.on('Session', ev => this.handleSession(ev));
50
+
51
+ this.permissions = new PermissionsManager(this);
52
+ this.rooms = new RoomsManager(this);
53
+ this.spaces = new SpacesManager(this);
54
+ this.emoticons = new EmoticonsManager(this);
55
+ this.users = new UsersManager(this);
56
+ this.relationships = new RelationshipsManager(this);
41
57
  }
42
58
 
43
59
  public get me(): User | null {
@@ -21,18 +21,20 @@ export class EmoticonsManager {
21
21
  }
22
22
 
23
23
  public async get(spaceId?: string): Promise<ObservableIndexedObjectCollection<Emoticon>> {
24
- if (this.emoticonsPromises.notExist(spaceId)) {
24
+ const key = spaceId ?? GLOBAL_KEY;
25
+
26
+ if (this.emoticonsPromises.notExist(key)) {
25
27
  this.emoticonsPromises.registerByFunction(async () => {
26
28
  const result = await this.tracker.client.send('GetEmoticons', {spaceId});
27
29
  if (result.error) {
28
30
  throw result.error;
29
31
  }
30
32
  this.handleEmoticons(result.data);
31
- }, spaceId ?? GLOBAL_KEY);
33
+ }, key);
32
34
  }
33
35
 
34
- await this.emoticonsPromises.get(spaceId);
35
- return this.list.get(spaceId);
36
+ await this.emoticonsPromises.get(key);
37
+ return this.list.get(key);
36
38
  }
37
39
 
38
40
  private handleEmoticons(event: Emoticons): void {
@@ -1,6 +1,5 @@
1
1
  import {ChatStateTracker} from "./ChatStateTracker";
2
2
  import {
3
- ChatLocation,
4
3
  NewMessage,
5
4
  FollowedTopic,
6
5
  TopicFollowed,
@@ -8,7 +7,7 @@ import {
8
7
  RoomDeleted,
9
8
  RoomLeft,
10
9
  TopicDeleted,
11
- FollowedTopicUpdated, RoomJoined, NewTopic, Session, Room,
10
+ FollowedTopicUpdated, RoomJoined, NewTopic, Session, Room, MessageType,
12
11
  } from "../types/src";
13
12
  import {
14
13
  IndexedCollection,
@@ -210,8 +209,8 @@ export class MessagesManager {
210
209
  const roomFollowedTopics = this.followedTopics.get(ev.message.location.roomId);
211
210
  const followedTopic = roomFollowedTopics?.get(ev.message.location.topicId);
212
211
 
213
- if (! roomFollowedTopics || ! followedTopic) {
214
- // Skip if we don't follow this room or targeted topic
212
+ if (!roomFollowedTopics || !followedTopic || ev.message.type === 'Ephemeral') {
213
+ // Skip if we don't follow this room or targeted topic or the message is ephemeral
215
214
  return;
216
215
  }
217
216
 
@@ -0,0 +1,68 @@
1
+ import {ObservableIndexedObjectCollection} from "../IndexedObjectCollection";
2
+ import {
3
+ NewRelationship,
4
+ RelationshipDeleted,
5
+ Relationships,
6
+ UserRelationship,
7
+ UserRelationshipType
8
+ } from "../types/src";
9
+ import {PromiseRegistry} from "./AsyncUtils";
10
+ import {ChatStateTracker} from "./ChatStateTracker";
11
+
12
+ const getId = (refUserId: string, type: UserRelationshipType): string => `${refUserId}-${type}`;
13
+ const getIdFromRelationship = (relationship: UserRelationship): string => getId(relationship.refUser.id, relationship.type);
14
+
15
+ export class RelationshipsManager {
16
+ private relationships: ObservableIndexedObjectCollection<UserRelationship> = new ObservableIndexedObjectCollection<UserRelationship>(getIdFromRelationship);
17
+ private promises = new PromiseRegistry();
18
+
19
+ public constructor(private tracker: ChatStateTracker) {
20
+ this.tracker.client.on('Relationships', ev => this.handleRelationships(ev));
21
+ this.tracker.client.on('NewRelationship', ev => this.handleNewRelationship(ev));
22
+ this.tracker.client.on('RelationshipDeleted', ev => this.handleRelationshipDeleted(ev));
23
+ this.tracker.client.on('Session', () => this.handleSession());
24
+ }
25
+
26
+ public async get(): Promise<ObservableIndexedObjectCollection<UserRelationship>> {
27
+ if (this.promises.notExist('all')) {
28
+ this.promises.registerByFunction(async () => {
29
+ const result = await this.tracker.client.send('GetRelationships', {});
30
+ if (result.error) {
31
+ throw result.error;
32
+ }
33
+ }, 'all');
34
+ }
35
+
36
+ await this.promises.get('all');
37
+ return this.relationships;
38
+ }
39
+
40
+ public async exists(refUserId: string, type: UserRelationshipType): Promise<boolean> {
41
+ await this.get();
42
+ return this.relationships.has(getId(refUserId, type));
43
+ }
44
+
45
+ private handleRelationships(ev: Relationships): void {
46
+ this.relationships.deleteAll();
47
+ ev.relationships.forEach(relationship => {
48
+ this.relationships.set(relationship);
49
+ });
50
+ }
51
+
52
+ private handleNewRelationship(ev: NewRelationship): void {
53
+ if (this.promises.has('all')) {
54
+ this.relationships.set(ev.relationship);
55
+ }
56
+ }
57
+
58
+ private handleRelationshipDeleted(ev: RelationshipDeleted): void {
59
+ if (this.promises.has('all')) {
60
+ this.relationships.delete(getIdFromRelationship(ev.relationship));
61
+ }
62
+ }
63
+
64
+ private handleSession(): void {
65
+ this.promises.forgetAll();
66
+ this.relationships.deleteAll();
67
+ }
68
+ }
@@ -5,6 +5,7 @@ import {TopicHistoryWindow} from "./TopicHistoryWindow";
5
5
 
6
6
  export class RoomMessagesHistory {
7
7
  private historyWindows = new IndexedCollection<string, TopicHistoryWindow>();
8
+ private traverseLock: boolean = false;
8
9
 
9
10
  public constructor(
10
11
  private room: Room,
@@ -14,6 +15,8 @@ export class RoomMessagesHistory {
14
15
  this.tracker.client.on('NewTopic', ev => this.handleNewTopic(ev));
15
16
  this.tracker.client.on('TopicDeleted', ev => this.handleTopicDeleted(ev));
16
17
 
18
+ this.updateTraverseLock(this.room);
19
+
17
20
  if (this.room.defaultTopic) {
18
21
  this.createHistoryWindowForTopic(this.room.defaultTopic);
19
22
  }
@@ -25,7 +28,7 @@ export class RoomMessagesHistory {
25
28
  public async getMessagesWindow(topicId: string): Promise<TopicHistoryWindow | undefined> {
26
29
  let historyWindow = this.historyWindows.get(topicId);
27
30
 
28
- if (! historyWindow) {
31
+ if (!historyWindow) {
29
32
  const topic = (await this.tracker.rooms.getTopics(this.room.id, [topicId])).get(topicId);
30
33
 
31
34
  if (topic) {
@@ -36,13 +39,19 @@ export class RoomMessagesHistory {
36
39
  return this.historyWindows.get(topicId);
37
40
  }
38
41
 
39
- private handleRoomUpdated(ev: RoomUpdated): void {
42
+ private async handleRoomUpdated(ev: RoomUpdated): Promise<void> {
40
43
  if (this.room.id === ev.room.id) {
41
44
  this.room = ev.room;
42
45
 
46
+ this.updateTraverseLock(ev.room);
47
+
43
48
  if (ev.room.defaultTopic) {
44
49
  this.createHistoryWindowForTopic(ev.room.defaultTopic);
45
50
  }
51
+
52
+ for (const [, window] of Array.from(this.historyWindows.items)) {
53
+ await window.setTraverseLock(this.traverseLock);
54
+ }
46
55
  }
47
56
  }
48
57
 
@@ -63,7 +72,11 @@ export class RoomMessagesHistory {
63
72
  return;
64
73
  }
65
74
 
66
- this.historyWindows.set([topic.id, new TopicHistoryWindow(this.room.id, topic.id, this.tracker)]);
75
+ const historyWindow = new TopicHistoryWindow(this.room.id, topic.id, this.tracker);
76
+
77
+ void historyWindow.setTraverseLock(this.traverseLock);
78
+
79
+ this.historyWindows.set([topic.id, historyWindow]);
67
80
 
68
81
  // If new topic refers to some message from this room, update other structures
69
82
  if (topic.refMessage) {
@@ -71,4 +84,8 @@ export class RoomMessagesHistory {
71
84
  refHistoryWindow?._updateMessageReference(topic);
72
85
  }
73
86
  }
87
+
88
+ private updateTraverseLock(room: Room): void {
89
+ this.traverseLock = room.history.mode === 'Ephemeral';
90
+ }
74
91
  }