stream-chat 4.2.0 → 4.4.1

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/src/client.ts CHANGED
@@ -72,7 +72,7 @@ import {
72
72
  ListCommandsResponse,
73
73
  LiteralStringForUnion,
74
74
  Logger,
75
- MarkAllReadOptions,
75
+ MarkChannelsReadOptions,
76
76
  Message,
77
77
  MessageFilters,
78
78
  MessageResponse,
@@ -84,6 +84,7 @@ import {
84
84
  PartialUserUpdate,
85
85
  PermissionAPIResponse,
86
86
  PermissionsAPIResponse,
87
+ PushProvider,
87
88
  ReactionResponse,
88
89
  SearchOptions,
89
90
  SearchPayload,
@@ -111,7 +112,13 @@ import {
111
112
  Segment,
112
113
  Campaign,
113
114
  CampaignData,
115
+ OGAttachment,
116
+ TaskStatus,
117
+ DeleteUserOptions,
118
+ DeleteChannelsResponse,
119
+ TaskResponse,
114
120
  } from './types';
121
+ import { InsightTypes, InsightMetrics } from './insights';
115
122
 
116
123
  function isString(x: unknown): x is string {
117
124
  return typeof x === 'string' || x instanceof String;
@@ -191,6 +198,7 @@ export class StreamChat<
191
198
  wsConnection: StableWSConnection<ChannelType, CommandType, UserType> | null;
192
199
  wsPromise: ConnectAPIResponse<ChannelType, CommandType, UserType> | null;
193
200
  consecutiveFailures: number;
201
+ insightMetrics: InsightMetrics;
194
202
 
195
203
  /**
196
204
  * Initialize a client
@@ -285,6 +293,7 @@ export class StreamChat<
285
293
  // generated from secret.
286
294
  this.tokenManager = new TokenManager(this.secret);
287
295
  this.consecutiveFailures = 0;
296
+ this.insightMetrics = new InsightMetrics();
288
297
 
289
298
  /**
290
299
  * logger function should accept 3 parameters:
@@ -729,6 +738,7 @@ export class StreamChat<
729
738
  apnTemplate: '{}', //if app doesn't have apn configured it will error
730
739
  firebaseTemplate: '{}', //if app doesn't have firebase configured it will error
731
740
  firebaseDataTemplate: '{}', //if app doesn't have firebase configured it will error
741
+ huaweiDataTemplate: '{}' //if app doesn't have huawei configured it will error
732
742
  skipDevices: true, // skip config/device checks and sending to real devices
733
743
  }
734
744
  */
@@ -741,6 +751,9 @@ export class StreamChat<
741
751
  ...(data.firebaseDataTemplate
742
752
  ? { firebase_data_template: data.firebaseDataTemplate }
743
753
  : {}),
754
+ ...(data.huaweiDataTemplate
755
+ ? { huawei_data_template: data.huaweiDataTemplate }
756
+ : {}),
744
757
  ...(data.skipDevices ? { skip_devices: true } : {}),
745
758
  });
746
759
  }
@@ -748,13 +761,11 @@ export class StreamChat<
748
761
  /**
749
762
  * testSQSSettings - Tests that the given or configured SQS configuration is valid
750
763
  *
751
- * @param {string} userID User ID. If user has no devices, it will error
752
- * @param {TestPushDataInput} [data] Overrides for push templates/message used
764
+ * @param {TestSQSDataInput} [data] Overrides SQS settings for testing if needed
753
765
  * IE: {
754
- messageID: 'id-of-message',//will error if message does not exist
755
- apnTemplate: '{}', //if app doesn't have apn configured it will error
756
- firebaseTemplate: '{}', //if app doesn't have firebase configured it will error
757
- firebaseDataTemplate: '{}', //if app doesn't have firebase configured it will error
766
+ sqs_key: 'auth_key',
767
+ sqs_secret: 'auth_secret',
768
+ sqs_url: 'url_to_queue',
758
769
  }
759
770
  */
760
771
  async testSQSSettings(data: TestSQSDataInput = {}) {
@@ -1609,6 +1620,7 @@ export class StreamChat<
1609
1620
  }
1610
1621
 
1611
1622
  // The StableWSConnection handles all the reconnection logic.
1623
+
1612
1624
  this.wsConnection = new StableWSConnection<ChannelType, CommandType, UserType>({
1613
1625
  wsBaseURL: client.wsBaseURL,
1614
1626
  clientID: client.clientID,
@@ -1623,6 +1635,8 @@ export class StreamChat<
1623
1635
  eventCallback: this.dispatchEvent as (event: ConnectionChangeEvent) => void,
1624
1636
  logger: this.logger,
1625
1637
  device: this.options.device,
1638
+ postInsights: this.options.enableInsights ? this.postInsights : undefined,
1639
+ insightMetrics: this.insightMetrics,
1626
1640
  });
1627
1641
 
1628
1642
  let warmUpPromise;
@@ -1890,7 +1904,7 @@ export class StreamChat<
1890
1904
  *
1891
1905
  * @param {BaseDeviceFields} device the device object
1892
1906
  * @param {string} device.id device id
1893
- * @param {string} device.push_provider the push provider (apn or firebase)
1907
+ * @param {string} device.push_provider the push provider
1894
1908
  *
1895
1909
  */
1896
1910
  setLocalDevice(device: BaseDeviceFields) {
@@ -1905,11 +1919,11 @@ export class StreamChat<
1905
1919
  * addDevice - Adds a push device for a user.
1906
1920
  *
1907
1921
  * @param {string} id the device id
1908
- * @param {'apn' | 'firebase'} push_provider the push provider (apn or firebase)
1922
+ * @param {PushProvider} push_provider the push provider
1909
1923
  * @param {string} [userID] the user id (defaults to current user)
1910
1924
  *
1911
1925
  */
1912
- async addDevice(id: string, push_provider: 'apn' | 'firebase', userID?: string) {
1926
+ async addDevice(id: string, push_provider: PushProvider, userID?: string) {
1913
1927
  return await this.post<APIResponse>(this.baseURL + '/devices', {
1914
1928
  id,
1915
1929
  push_provider,
@@ -2505,12 +2519,24 @@ export class StreamChat<
2505
2519
  }
2506
2520
 
2507
2521
  /**
2522
+ * @deprecated use markChannelsRead instead
2523
+ *
2508
2524
  * markAllRead - marks all channels for this user as read
2509
2525
  * @param {MarkAllReadOptions<UserType>} [data]
2510
2526
  *
2511
2527
  * @return {Promise<APIResponse>}
2512
2528
  */
2513
- async markAllRead(data: MarkAllReadOptions<UserType> = {}) {
2529
+ markAllRead = this.markChannelsRead;
2530
+
2531
+ /**
2532
+ * markChannelsRead - marks channels read -
2533
+ * it accepts a map of cid:messageid pairs, if messageid is empty, the whole channel will be marked as read
2534
+ *
2535
+ * @param {MarkChannelsReadOptions <UserType>} [data]
2536
+ *
2537
+ * @return {Promise<APIResponse>}
2538
+ */
2539
+ async markChannelsRead(data: MarkChannelsReadOptions<UserType> = {}) {
2514
2540
  await this.post<APIResponse>(this.baseURL + '/channels/read', {
2515
2541
  ...data,
2516
2542
  });
@@ -2692,6 +2718,7 @@ export class StreamChat<
2692
2718
  *
2693
2719
  * @param {Omit<MessageResponse<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType>, 'mentioned_users'> & { mentioned_users?: string[] }} message object, id needs to be specified
2694
2720
  * @param {string | { id: string }} [userId]
2721
+ * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message
2695
2722
  *
2696
2723
  * @return {APIResponse & { message: MessageResponse<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType> }} Response that includes the message
2697
2724
  */
@@ -2705,6 +2732,7 @@ export class StreamChat<
2705
2732
  UserType
2706
2733
  >,
2707
2734
  userId?: string | { id: string },
2735
+ options?: { skip_enrich_url?: boolean },
2708
2736
  ) {
2709
2737
  if (!message.id) {
2710
2738
  throw Error('Please specify the message id when calling updateMessage');
@@ -2777,6 +2805,7 @@ export class StreamChat<
2777
2805
  >
2778
2806
  >(this.baseURL + `/messages/${message.id}`, {
2779
2807
  message: clonedMessage,
2808
+ ...options,
2780
2809
  });
2781
2810
  }
2782
2811
 
@@ -2789,12 +2818,15 @@ export class StreamChat<
2789
2818
  * example: {id: "user1", set:{text: "hi"}, unset:["color"]}
2790
2819
  * @param {string | { id: string }} [userId]
2791
2820
  *
2821
+ * @param {boolean} [options.skip_enrich_url] Do not try to enrich the URLs within message
2822
+ *
2792
2823
  * @return {APIResponse & { message: MessageResponse<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType> }} Response that includes the updated message
2793
2824
  */
2794
2825
  async partialUpdateMessage(
2795
2826
  id: string,
2796
2827
  partialMessageObject: PartialMessageUpdate<MessageType>,
2797
2828
  userId?: string | { id: string },
2829
+ options?: { skip_enrich_url?: boolean },
2798
2830
  ) {
2799
2831
  if (!id) {
2800
2832
  throw Error('Please specify the message id when calling partialUpdateMessage');
@@ -2814,6 +2846,7 @@ export class StreamChat<
2814
2846
  >
2815
2847
  >(this.baseURL + `/messages/${id}`, {
2816
2848
  ...partialMessageObject,
2849
+ ...options,
2817
2850
  user,
2818
2851
  });
2819
2852
  }
@@ -3279,4 +3312,95 @@ export class StreamChat<
3279
3312
  );
3280
3313
  return campaign;
3281
3314
  }
3315
+
3316
+ /**
3317
+ * enrichURL - Get OpenGraph data of the given link
3318
+ *
3319
+ * @param {string} url link
3320
+ * @return {OGAttachment} OG Attachment
3321
+ */
3322
+ async enrichURL(url: string) {
3323
+ return this.get<APIResponse & OGAttachment>(this.baseURL + `/og`, { url });
3324
+ }
3325
+
3326
+ /**
3327
+ * getTask - Gets status of a long running task
3328
+ *
3329
+ * @param {string} id Task ID
3330
+ *
3331
+ * @return {TaskStatus} The task status
3332
+ */
3333
+ async getTask(id: string) {
3334
+ return this.get<APIResponse & TaskStatus>(`${this.baseURL}/tasks/${id}`);
3335
+ }
3336
+
3337
+ /**
3338
+ * deleteChannels - Deletes a list of channel
3339
+ *
3340
+ * @param {string[]} cids Channel CIDs
3341
+ * @param {boolean} [options.hard_delete] Defines if the channel is hard deleted or not
3342
+ *
3343
+ * @return {DeleteChannelsResponse} Result of the soft deletion, if server-side, it holds the task ID as well
3344
+ */
3345
+ async deleteChannels(cids: string[], options: { hard_delete?: boolean } = {}) {
3346
+ return await this.post<APIResponse & DeleteChannelsResponse>(
3347
+ this.baseURL + `/channels/delete`,
3348
+ { cids, ...options },
3349
+ );
3350
+ }
3351
+
3352
+ postInsights = async (insightType: InsightTypes, insights: Record<string, unknown>) => {
3353
+ const maxAttempts = 3;
3354
+ for (let i = 0; i < maxAttempts; i++) {
3355
+ try {
3356
+ await this.axiosInstance.post(
3357
+ `https://insights.stream-io-api.com/insights/${insightType}`,
3358
+ insights,
3359
+ );
3360
+ } catch (e) {
3361
+ this.logger('warn', `failed to send insights event ${insightType}`, {
3362
+ tags: ['insights', 'connection'],
3363
+ error: e,
3364
+ insights,
3365
+ });
3366
+ await sleep((i + 1) * 3000);
3367
+ continue;
3368
+ }
3369
+ break;
3370
+ }
3371
+ };
3372
+
3373
+ /**
3374
+ * deleteUsers - Batch Delete Users
3375
+ *
3376
+ * @param {string[]} user_ids which users to delete
3377
+ * @param {DeleteUserOptions} options Configuration how to delete users
3378
+ *
3379
+ * @return {APIResponse} A task ID
3380
+ */
3381
+ async deleteUsers(user_ids: string[], options: DeleteUserOptions) {
3382
+ if (options?.user !== 'soft' && options?.user !== 'hard') {
3383
+ throw new Error('Invalid delete user options. user must be one of [soft hard]');
3384
+ }
3385
+ if (
3386
+ options.messages !== undefined &&
3387
+ options.messages !== 'soft' &&
3388
+ options.messages !== 'hard'
3389
+ ) {
3390
+ throw new Error('Invalid delete user options. messages must be one of [soft hard]');
3391
+ }
3392
+ if (
3393
+ options.conversations !== undefined &&
3394
+ options.conversations !== 'soft' &&
3395
+ options.conversations !== 'hard'
3396
+ ) {
3397
+ throw new Error(
3398
+ 'Invalid delete user options. conversations must be one of [soft hard]',
3399
+ );
3400
+ }
3401
+ return await this.post<APIResponse & TaskResponse>(this.baseURL + `/users/delete`, {
3402
+ user_ids,
3403
+ ...options,
3404
+ });
3405
+ }
3282
3406
  }
package/src/connection.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import WebSocket from 'isomorphic-ws';
2
- import { chatCodes, sleep, retryInterval } from './utils';
2
+ import { chatCodes, sleep, retryInterval, randomId } from './utils';
3
3
  import { TokenManager } from './token_manager';
4
+ import {
5
+ buildWsFatalInsight,
6
+ buildWsSuccessAfterFailureInsight,
7
+ InsightMetrics,
8
+ InsightTypes,
9
+ } from './insights';
4
10
  import {
5
11
  BaseDeviceFields,
6
12
  ConnectAPIResponse,
@@ -30,6 +36,7 @@ type Constructor<
30
36
  authType: 'anonymous' | 'jwt';
31
37
  clientID: string;
32
38
  eventCallback: (event: ConnectionChangeEvent) => void;
39
+ insightMetrics: InsightMetrics;
33
40
  logger: Logger | (() => void);
34
41
  messageCallback: (messageEvent: WebSocket.MessageEvent) => void;
35
42
  recoverCallback: (
@@ -41,6 +48,7 @@ type Constructor<
41
48
  userID: string;
42
49
  wsBaseURL: string;
43
50
  device?: BaseDeviceFields;
51
+ postInsights?: (eventType: InsightTypes, event: Record<string, unknown>) => void;
44
52
  };
45
53
 
46
54
  /**
@@ -78,7 +86,6 @@ export class StableWSConnection<
78
86
  userID: Constructor<ChannelType, CommandType, UserType>['userID'];
79
87
  wsBaseURL: Constructor<ChannelType, CommandType, UserType>['wsBaseURL'];
80
88
  device: Constructor<ChannelType, CommandType, UserType>['device'];
81
-
82
89
  connectionID?: string;
83
90
  connectionOpen?: ConnectAPIResponse<ChannelType, CommandType, UserType>;
84
91
  consecutiveFailures: number;
@@ -97,11 +104,14 @@ export class StableWSConnection<
97
104
  StatusCode?: string | number;
98
105
  },
99
106
  ) => void;
107
+ requestID: string | undefined;
108
+ connectionStartTimestamp: number | undefined;
100
109
  resolvePromise?: (value: WebSocket.MessageEvent) => void;
101
110
  totalFailures: number;
102
111
  ws?: WebSocket;
103
112
  wsID: number;
104
-
113
+ postInsights?: Constructor<ChannelType, CommandType, UserType>['postInsights'];
114
+ insightMetrics: InsightMetrics;
105
115
  constructor({
106
116
  apiKey,
107
117
  authType,
@@ -116,6 +126,8 @@ export class StableWSConnection<
116
126
  userID,
117
127
  wsBaseURL,
118
128
  device,
129
+ postInsights,
130
+ insightMetrics,
119
131
  }: Constructor<ChannelType, CommandType, UserType>) {
120
132
  this.wsBaseURL = wsBaseURL;
121
133
  this.clientID = clientID;
@@ -149,6 +161,8 @@ export class StableWSConnection<
149
161
  this.pingInterval = 25 * 1000;
150
162
  this.connectionCheckTimeout = this.pingInterval + 10 * 1000;
151
163
  this._listenForConnectionChanges();
164
+ this.postInsights = postInsights;
165
+ this.insightMetrics = insightMetrics;
152
166
  }
153
167
 
154
168
  /**
@@ -244,13 +258,19 @@ export class StableWSConnection<
244
258
  ]);
245
259
  }
246
260
 
247
- _buildUrl = () => {
261
+ /**
262
+ * Builds and returns the url for websocket.
263
+ * @param reqID Unique identifier generated on client side, to help tracking apis on backend.
264
+ * @returns url string
265
+ */
266
+ _buildUrl = (reqID?: string) => {
248
267
  const params = {
249
268
  user_id: this.user.id,
250
269
  user_details: this.user,
251
270
  user_token: this.tokenManager.getToken(),
252
271
  server_determines_connection_id: true,
253
272
  device: this.device,
273
+ request_id: reqID,
254
274
  };
255
275
  const qs = encodeURIComponent(JSON.stringify(params));
256
276
  const token = this.tokenManager.getToken();
@@ -352,11 +372,12 @@ export class StableWSConnection<
352
372
  async _connect() {
353
373
  if (this.isConnecting) return; // simply ignore _connect if it's currently trying to connect
354
374
  this.isConnecting = true;
355
-
375
+ this.requestID = randomId();
376
+ this.insightMetrics.connectionStartTimestamp = new Date().getTime();
356
377
  try {
357
378
  await this.tokenManager.tokenReady();
358
379
  this._setupConnectionPromise();
359
- const wsURL = this._buildUrl();
380
+ const wsURL = this._buildUrl(this.requestID);
360
381
  this.ws = new WebSocket(wsURL);
361
382
  this.ws.onopen = this.onopen.bind(this, this.wsID);
362
383
  this.ws.onclose = this.onclose.bind(this, this.wsID);
@@ -367,6 +388,13 @@ export class StableWSConnection<
367
388
 
368
389
  if (response) {
369
390
  this.connectionID = response.connection_id;
391
+ if (this.insightMetrics.wsConsecutiveFailures > 0) {
392
+ this.postInsights?.(
393
+ 'ws_success_after_failure',
394
+ buildWsSuccessAfterFailureInsight(this),
395
+ );
396
+ this.insightMetrics.wsConsecutiveFailures = 0;
397
+ }
370
398
  return response;
371
399
  }
372
400
  } catch (err) {
@@ -559,6 +587,12 @@ export class StableWSConnection<
559
587
  };
560
588
 
561
589
  onclose = (wsID: number, event: WebSocket.CloseEvent) => {
590
+ if (event.code !== chatCodes.WS_CLOSED_SUCCESS) {
591
+ this.insightMetrics.wsConsecutiveFailures++;
592
+ this.insightMetrics.wsTotalFailures++;
593
+ this.postInsights?.('ws_fatal', buildWsFatalInsight(this, event));
594
+ }
595
+
562
596
  this.logger('info', 'connection:onclose() - onclose callback - ' + event.code, {
563
597
  tags: ['connection'],
564
598
  event,
@@ -701,25 +735,20 @@ export class StableWSConnection<
701
735
 
702
736
  /**
703
737
  * _listenForConnectionChanges - Adds an event listener for the browser going online or offline
704
- *
705
738
  */
706
739
  _listenForConnectionChanges = () => {
707
- if (
708
- typeof window !== 'undefined' &&
709
- window != null &&
710
- window.addEventListener != null
711
- ) {
740
+ // (typeof window !== 'undefined') check is for environments where window is not defined, such as nextjs environment,
741
+ // and thus (window === undefined) will result in ReferenceError.
742
+ if (typeof window !== 'undefined' && window?.addEventListener) {
712
743
  window.addEventListener('offline', this.onlineStatusChanged);
713
744
  window.addEventListener('online', this.onlineStatusChanged);
714
745
  }
715
746
  };
716
747
 
717
748
  _removeConnectionListeners = () => {
718
- if (
719
- typeof window !== 'undefined' &&
720
- window != null &&
721
- window.addEventListener != null
722
- ) {
749
+ // (typeof window !== 'undefined') check is for environments where window is not defined, such as nextjs environment,
750
+ // and thus (window === undefined) will result in ReferenceError.
751
+ if (typeof window !== 'undefined' && window?.removeEventListener) {
723
752
  window.removeEventListener('offline', this.onlineStatusChanged);
724
753
  window.removeEventListener('online', this.onlineStatusChanged);
725
754
  }
@@ -796,7 +825,6 @@ export class StableWSConnection<
796
825
  {
797
826
  type: 'health.check',
798
827
  client_id: this.clientID,
799
- user_id: this.userID,
800
828
  },
801
829
  ];
802
830
  // try to send on the connection
package/src/index.ts CHANGED
@@ -8,5 +8,6 @@ export * from './events';
8
8
  export * from './permissions';
9
9
  export * from './signing';
10
10
  export * from './token_manager';
11
+ export * from './insights';
11
12
  export * from './types';
12
13
  export { isOwnUser, chatCodes, logChatPromiseExecution } from './utils';
@@ -0,0 +1,68 @@
1
+ import { StableWSConnection } from './connection';
2
+ import WebSocket from 'isomorphic-ws';
3
+ import { LiteralStringForUnion, UnknownType } from './types';
4
+
5
+ export type InsightTypes = 'ws_fatal' | 'ws_success_after_failure';
6
+ export class InsightMetrics {
7
+ connectionStartTimestamp: number | null;
8
+ wsConsecutiveFailures: number;
9
+ wsTotalFailures: number;
10
+
11
+ constructor() {
12
+ this.connectionStartTimestamp = null;
13
+ this.wsTotalFailures = 0;
14
+ this.wsConsecutiveFailures = 0;
15
+ }
16
+ }
17
+
18
+ export function buildWsFatalInsight<
19
+ ChannelType extends UnknownType = UnknownType,
20
+ CommandType extends string = LiteralStringForUnion,
21
+ UserType extends UnknownType = UnknownType
22
+ >(
23
+ connection: StableWSConnection<ChannelType, CommandType, UserType>,
24
+ event: WebSocket.CloseEvent,
25
+ ) {
26
+ return {
27
+ err: {
28
+ wasClean: event.wasClean,
29
+ code: event.code,
30
+ reason: event.reason,
31
+ },
32
+ ...buildWsBaseInsight(connection),
33
+ };
34
+ }
35
+
36
+ function buildWsBaseInsight<
37
+ ChannelType extends UnknownType = UnknownType,
38
+ CommandType extends string = LiteralStringForUnion,
39
+ UserType extends UnknownType = UnknownType
40
+ >(connection: StableWSConnection<ChannelType, CommandType, UserType>) {
41
+ return {
42
+ ready_state: connection.ws?.readyState,
43
+ url: connection._buildUrl(connection.requestID),
44
+ api_key: connection.apiKey,
45
+ start_ts: connection.insightMetrics.connectionStartTimestamp,
46
+ end_ts: new Date().getTime(),
47
+ auth_type: connection.authType,
48
+ token: connection.tokenManager.token,
49
+ user_id: connection.userID,
50
+ user_details: connection.user,
51
+ device: connection.device,
52
+ client_id: connection.connectionID,
53
+ ws_details: connection.ws,
54
+ ws_consecutive_failures: connection.insightMetrics.wsConsecutiveFailures,
55
+ ws_total_failures: connection.insightMetrics.wsTotalFailures,
56
+ request_id: connection.requestID,
57
+ online: typeof navigator !== 'undefined' ? navigator?.onLine : null,
58
+ user_agent: typeof navigator !== 'undefined' ? navigator?.userAgent : null,
59
+ };
60
+ }
61
+
62
+ export function buildWsSuccessAfterFailureInsight<
63
+ ChannelType extends UnknownType = UnknownType,
64
+ CommandType extends string = LiteralStringForUnion,
65
+ UserType extends UnknownType = UnknownType
66
+ >(connection: StableWSConnection<ChannelType, CommandType, UserType>) {
67
+ return buildWsBaseInsight(connection);
68
+ }