stream-chat 9.52.1 → 9.53.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.
@@ -1,5 +1,5 @@
1
1
  import type { StreamChat } from './client';
2
- import type { APIResponse, BatchChannelDataUpdate, NewMemberPayload, UpdateChannelsBatchFilters, UpdateChannelsBatchResponse } from './types';
2
+ import type { APIResponse, BatchChannelDataUpdate, ChannelBatchDataUpdateOptions, NewMemberPayload, UpdateChannelsBatchFilters, UpdateChannelsBatchResponse } from './types';
3
3
  /**
4
4
  * ChannelBatchUpdater - A class that provides convenience methods for batch channel operations
5
5
  */
@@ -85,9 +85,16 @@ export declare class ChannelBatchUpdater {
85
85
  /**
86
86
  * updateData - Update data on channels matching the filter
87
87
  *
88
+ * `data.custom` replaces the channel's whole custom object. To patch
89
+ * individual custom keys, pass an options object with `custom_set` or
90
+ * `custom_unset`. The client sends those fields at the request root rather
91
+ * than inside `data`. The options object can also include `data` to update
92
+ * channel fields in the same request.
93
+ *
88
94
  * @param {UpdateChannelsBatchFilters} filter Filter to select channels
89
- * @param {BatchChannelDataUpdate} data Data to update
95
+ * @param {BatchChannelDataUpdate | ChannelBatchDataUpdateOptions} update Data or update options
90
96
  * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
91
97
  */
92
98
  updateData(filter: UpdateChannelsBatchFilters, data: BatchChannelDataUpdate): Promise<APIResponse & UpdateChannelsBatchResponse>;
99
+ updateData(filter: UpdateChannelsBatchFilters, options: ChannelBatchDataUpdateOptions): Promise<APIResponse & UpdateChannelsBatchResponse>;
93
100
  }
@@ -105,6 +105,10 @@ export declare class StreamChat {
105
105
  wsConnection: StableWSConnection | null;
106
106
  wsFallback?: WSConnectionFallback;
107
107
  wsPromise: ConnectAPIResponse | null;
108
+ private _wsPromiseSettled;
109
+ private _wsConnectId;
110
+ private _resolveWsPromise?;
111
+ private _rejectWsPromise?;
108
112
  consecutiveFailures: number;
109
113
  insightMetrics: InsightMetrics;
110
114
  defaultWSTimeoutWithFallback: number;
@@ -224,6 +228,15 @@ export declare class StreamChat {
224
228
  * Creates a new WebSocket connection with the current user. Returns empty promise, if there is an active connection
225
229
  */
226
230
  openConnection: () => ConnectAPIResponse | undefined;
231
+ /**
232
+ * Rejects the shared `wsPromise`, invalidates any in-flight connect attempt
233
+ */
234
+ private _rejectPendingWsPromise;
235
+ /**
236
+ * Keep a single pending `wsPromise` across close/reopen so callers that already
237
+ * `await this.wsPromise` are not stranded when `openConnection` starts a new connect.
238
+ */
239
+ private _bindWsPromise;
227
240
  /**
228
241
  * @deprecated Please use client.openConnction instead.
229
242
  * @private
@@ -2183,6 +2196,10 @@ export declare class StreamChat {
2183
2196
  /**
2184
2197
  * Update Channels Batch
2185
2198
  *
2199
+ * For the `updateData` operation, `data.custom` replaces the channel's whole
2200
+ * custom object, while the root-level `custom_set` / `custom_unset` patch
2201
+ * individual keys and leave the rest untouched.
2202
+ *
2186
2203
  * @param {UpdateChannelsBatchOptions} payload for updating channels in batch
2187
2204
  * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
2188
2205
  */
@@ -48,6 +48,7 @@ export type TranslateResponse = {
48
48
  export type AppSettingsAPIResponse = APIResponse & {
49
49
  app?: {
50
50
  id?: string | number;
51
+ activity_metrics_config?: Record<string, number>;
51
52
  allow_multi_user_devices?: boolean;
52
53
  feed_audit_logs_enabled?: boolean;
53
54
  moderation_onboarding_complete?: boolean | null;
@@ -93,6 +94,7 @@ export type AppSettingsAPIResponse = APIResponse & {
93
94
  before_message_send_hook_attempt_timeout_ms?: number;
94
95
  campaign_enabled?: boolean;
95
96
  cdn_expiration_seconds?: number;
97
+ chat_primary_use_case?: string;
96
98
  custom_action_handler_url?: string;
97
99
  datadog_info?: {
98
100
  api_key: string;
@@ -117,13 +119,18 @@ export type AppSettingsAPIResponse = APIResponse & {
117
119
  image_upload_config?: FileUploadConfig;
118
120
  allowed_flag_reasons?: string[];
119
121
  max_aggregated_activities_length?: number;
122
+ member_custom_on_mentioned_users_enabled?: boolean;
123
+ member_custom_on_messages_enabled?: boolean;
124
+ member_custom_on_typing_events_enabled?: boolean;
120
125
  moderation_bulk_submit_action_enabled?: boolean;
121
126
  moderation_dashboard_preferences?: Record<string, unknown> | null;
122
127
  moderation_audio_call_moderation_enabled?: boolean;
123
128
  moderation_enabled?: boolean;
124
129
  moderation_flood_rules_enabled?: boolean;
130
+ moderation_keyframe_video_enabled?: boolean;
125
131
  moderation_llm_configurability_enabled?: boolean;
126
132
  moderation_multitenant_blocklist_enabled?: boolean;
133
+ moderation_s3_image_access_role_arn?: string;
127
134
  moderation_video_call_moderation_enabled?: boolean;
128
135
  moderation_webhook_url?: string;
129
136
  multi_tenant_enabled?: boolean;
@@ -160,6 +167,7 @@ export type AppSettingsAPIResponse = APIResponse & {
160
167
  use_hook_v2?: boolean;
161
168
  user_response_time_enabled?: boolean;
162
169
  user_search_disallowed_roles?: string[] | null;
170
+ video_primary_use_case?: string;
163
171
  video_provider?: string;
164
172
  webhook_events?: Array<string>;
165
173
  webhook_url?: string;
@@ -3713,7 +3721,32 @@ export type UpdateChannelsBatchOptions = {
3713
3721
  filter: UpdateChannelsBatchFilters;
3714
3722
  members?: string[] | Array<NewMemberPayload>;
3715
3723
  data?: BatchChannelDataUpdate;
3724
+ /**
3725
+ * `updateData` only. Merges these keys into each matched channel's existing
3726
+ * custom object, leaving every other custom key untouched — unlike
3727
+ * `data.custom`, which replaces the whole object. Keys are dot-paths, so
3728
+ * `a.b` sets key `b` inside object `a` (the parent object must already
3729
+ * exist). Cannot be combined with `data.custom`.
3730
+ *
3731
+ * Lives at the request root, not inside `data`.
3732
+ */
3733
+ custom_set?: Record<string, unknown>;
3734
+ /**
3735
+ * `updateData` only. Deletes these keys from each matched channel's existing
3736
+ * custom object, leaving every other custom key untouched. Keys are
3737
+ * dot-paths; deleting a key that does not exist is a no-op. Cannot be
3738
+ * combined with `data.custom`.
3739
+ *
3740
+ * Lives at the request root, not inside `data`.
3741
+ */
3742
+ custom_unset?: string[];
3716
3743
  };
3744
+ /**
3745
+ * Options for {@link ChannelBatchUpdater.updateData}. `custom_set` and
3746
+ * `custom_unset` are sent at the request root, while `data` contains the
3747
+ * channel fields to update.
3748
+ */
3749
+ export type ChannelBatchDataUpdateOptions = Pick<UpdateChannelsBatchOptions, 'data' | 'custom_set' | 'custom_unset'>;
3717
3750
  export type UpdateChannelsBatchFilters = QueryFilters<{
3718
3751
  cids?: RequireOnlyOne<Pick<QueryFilter<string>, '$in' | '$eq'>> | PrimitiveFilter<string[]>;
3719
3752
  types?: RequireOnlyOne<Pick<QueryFilter<string>, '$in' | '$eq'>> | PrimitiveFilter<string[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.52.1",
3
+ "version": "9.53.0",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
@@ -2,11 +2,17 @@ import type { StreamChat } from './client';
2
2
  import type {
3
3
  APIResponse,
4
4
  BatchChannelDataUpdate,
5
+ ChannelBatchDataUpdateOptions,
5
6
  NewMemberPayload,
6
7
  UpdateChannelsBatchFilters,
7
8
  UpdateChannelsBatchResponse,
8
9
  } from './types';
9
10
 
11
+ const isChannelBatchDataUpdateOptions = (
12
+ update: BatchChannelDataUpdate | ChannelBatchDataUpdateOptions,
13
+ ): update is ChannelBatchDataUpdateOptions =>
14
+ 'data' in update || 'custom_set' in update || 'custom_unset' in update;
15
+
10
16
  /**
11
17
  * ChannelBatchUpdater - A class that provides convenience methods for batch channel operations
12
18
  */
@@ -194,18 +200,38 @@ export class ChannelBatchUpdater {
194
200
  /**
195
201
  * updateData - Update data on channels matching the filter
196
202
  *
203
+ * `data.custom` replaces the channel's whole custom object. To patch
204
+ * individual custom keys, pass an options object with `custom_set` or
205
+ * `custom_unset`. The client sends those fields at the request root rather
206
+ * than inside `data`. The options object can also include `data` to update
207
+ * channel fields in the same request.
208
+ *
197
209
  * @param {UpdateChannelsBatchFilters} filter Filter to select channels
198
- * @param {BatchChannelDataUpdate} data Data to update
210
+ * @param {BatchChannelDataUpdate | ChannelBatchDataUpdateOptions} update Data or update options
199
211
  * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
200
212
  */
201
213
  async updateData(
202
214
  filter: UpdateChannelsBatchFilters,
203
215
  data: BatchChannelDataUpdate,
216
+ ): Promise<APIResponse & UpdateChannelsBatchResponse>;
217
+ async updateData(
218
+ filter: UpdateChannelsBatchFilters,
219
+ options: ChannelBatchDataUpdateOptions,
220
+ ): Promise<APIResponse & UpdateChannelsBatchResponse>;
221
+ async updateData(
222
+ filter: UpdateChannelsBatchFilters,
223
+ update: BatchChannelDataUpdate | ChannelBatchDataUpdateOptions,
204
224
  ): Promise<APIResponse & UpdateChannelsBatchResponse> {
225
+ const { data, custom_set, custom_unset } = isChannelBatchDataUpdateOptions(update)
226
+ ? update
227
+ : { data: update };
228
+
205
229
  return await this.client.updateChannelsBatch({
206
230
  operation: 'updateData',
207
231
  filter,
208
- data,
232
+ ...(data && { data }),
233
+ ...(custom_set && { custom_set }),
234
+ ...(custom_unset && { custom_unset }),
209
235
  });
210
236
  }
211
237
  }
package/src/client.ts CHANGED
@@ -393,6 +393,10 @@ export class StreamChat {
393
393
  wsConnection: StableWSConnection | null;
394
394
  wsFallback?: WSConnectionFallback;
395
395
  wsPromise: ConnectAPIResponse | null;
396
+ private _wsPromiseSettled = true;
397
+ private _wsConnectId = 0;
398
+ private _resolveWsPromise?: (value: Awaited<ConnectAPIResponse>) => void;
399
+ private _rejectWsPromise?: (reason?: unknown) => void;
396
400
  consecutiveFailures: number;
397
401
  insightMetrics: InsightMetrics;
398
402
  defaultWSTimeoutWithFallback: number;
@@ -713,18 +717,25 @@ export class StreamChat {
713
717
 
714
718
  const wsPromise = this.openConnection();
715
719
 
716
- this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
720
+ const setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
717
721
  (result) => result[1], // We only return connection promise;
718
722
  );
723
+ this.setUserPromise = setUserPromise;
719
724
 
720
725
  try {
721
- return await this.setUserPromise;
726
+ return await setUserPromise;
722
727
  } catch (err) {
723
- if (this.persistUserOnConnectionFailure) {
724
- // cleanup client to allow the user to retry connectUser again
725
- this.closeConnection();
726
- } else {
727
- this.disconnectUser();
728
+ // disconnectUser() rejects an in-flight handshake so this catch can run *after* something else has taken over.
729
+ // Cleaning up then would tear down whatever replaced us. setUserPromise catches a
730
+ // newer connectUser(); userID additionally catches connectAnonymousUser(), which
731
+ // never sets setUserPromise, and a bare disconnectUser() that already cleaned up.
732
+ if (this.setUserPromise === setUserPromise && this.userID === user.id) {
733
+ if (this.persistUserOnConnectionFailure) {
734
+ // cleanup client to allow the user to retry connectUser again
735
+ this.closeConnection();
736
+ } else {
737
+ this.disconnectUser();
738
+ }
728
739
  }
729
740
  throw err;
730
741
  }
@@ -852,9 +863,58 @@ export class StreamChat {
852
863
  }
853
864
 
854
865
  this.clientID = `${this.userID}--${randomId()}`;
855
- this.wsPromise = this.connect();
866
+ const wsPromise = this._bindWsPromise(this.connect());
856
867
  this._startCleaning();
857
- return this.wsPromise;
868
+ return wsPromise;
869
+ };
870
+
871
+ /**
872
+ * Rejects the shared `wsPromise`, invalidates any in-flight connect attempt
873
+ */
874
+ private _rejectPendingWsPromise = (reason: Error) => {
875
+ if (!this._wsPromiseSettled) {
876
+ // invalidate in-flight attempts so a late settle cannot revive this promise
877
+ this._wsConnectId += 1;
878
+ this._wsPromiseSettled = true;
879
+ // Avoid unhandled promise rejection errors
880
+ this.wsPromise?.catch(() => {
881
+ // noop - real awaiters still observe the rejection
882
+ });
883
+ this._rejectWsPromise?.(reason);
884
+ }
885
+ };
886
+
887
+ /**
888
+ * Keep a single pending `wsPromise` across close/reopen so callers that already
889
+ * `await this.wsPromise` are not stranded when `openConnection` starts a new connect.
890
+ */
891
+ private _bindWsPromise = (connectPromise: ConnectAPIResponse): ConnectAPIResponse => {
892
+ const connectId = ++this._wsConnectId;
893
+
894
+ let pending = this.wsPromise;
895
+ if (this._wsPromiseSettled || !pending) {
896
+ this._wsPromiseSettled = false;
897
+ pending = new Promise((resolve, reject) => {
898
+ this._resolveWsPromise = resolve;
899
+ this._rejectWsPromise = reject;
900
+ });
901
+ this.wsPromise = pending;
902
+ }
903
+
904
+ connectPromise.then(
905
+ (value) => {
906
+ if (connectId !== this._wsConnectId) return;
907
+ this._wsPromiseSettled = true;
908
+ this._resolveWsPromise?.(value);
909
+ },
910
+ (error) => {
911
+ if (connectId !== this._wsConnectId) return;
912
+ this._wsPromiseSettled = true;
913
+ this._rejectWsPromise?.(error);
914
+ },
915
+ );
916
+
917
+ return pending;
858
918
  };
859
919
 
860
920
  /**
@@ -1059,6 +1119,11 @@ export class StreamChat {
1059
1119
 
1060
1120
  this.anonymous = false;
1061
1121
 
1122
+ this._rejectPendingWsPromise(
1123
+ new Error('Connection was closed because disconnectUser() was called'),
1124
+ );
1125
+ this.wsPromise = null;
1126
+
1062
1127
  const closePromise = this.closeConnection(timeout);
1063
1128
 
1064
1129
  for (const channel of Object.values(this.activeChannels)) {
@@ -1751,7 +1816,12 @@ export class StreamChat {
1751
1816
  } as Event);
1752
1817
  }
1753
1818
 
1754
- this.wsPromise = Promise.resolve();
1819
+ // If state recovery happens afer a failed connect (for example on persistUserOnConnectionFailure: true) we need to flip the wsPromise from rejected to resolved
1820
+ // Otherwise all API calls that wait for the promise will fail
1821
+ // If promise is not yet settled - we leave it for the connect sequence to resolve the promise
1822
+ if (this._wsPromiseSettled) {
1823
+ this.wsPromise = Promise.resolve();
1824
+ }
1755
1825
  this.setUserPromise = Promise.resolve();
1756
1826
  };
1757
1827
 
@@ -5306,6 +5376,10 @@ export class StreamChat {
5306
5376
  /**
5307
5377
  * Update Channels Batch
5308
5378
  *
5379
+ * For the `updateData` operation, `data.custom` replaces the channel's whole
5380
+ * custom object, while the root-level `custom_set` / `custom_unset` patch
5381
+ * individual keys and leave the rest untouched.
5382
+ *
5309
5383
  * @param {UpdateChannelsBatchOptions} payload for updating channels in batch
5310
5384
  * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
5311
5385
  */
package/src/types.ts CHANGED
@@ -83,6 +83,7 @@ export type TranslateResponse = {
83
83
  export type AppSettingsAPIResponse = APIResponse & {
84
84
  app?: {
85
85
  id?: string | number;
86
+ activity_metrics_config?: Record<string, number>;
86
87
  allow_multi_user_devices?: boolean;
87
88
  feed_audit_logs_enabled?: boolean;
88
89
  moderation_onboarding_complete?: boolean | null;
@@ -139,6 +140,7 @@ export type AppSettingsAPIResponse = APIResponse & {
139
140
  before_message_send_hook_attempt_timeout_ms?: number;
140
141
  campaign_enabled?: boolean;
141
142
  cdn_expiration_seconds?: number;
143
+ chat_primary_use_case?: string;
142
144
  custom_action_handler_url?: string;
143
145
  datadog_info?: {
144
146
  api_key: string;
@@ -163,13 +165,18 @@ export type AppSettingsAPIResponse = APIResponse & {
163
165
  image_upload_config?: FileUploadConfig;
164
166
  allowed_flag_reasons?: string[];
165
167
  max_aggregated_activities_length?: number;
168
+ member_custom_on_mentioned_users_enabled?: boolean;
169
+ member_custom_on_messages_enabled?: boolean;
170
+ member_custom_on_typing_events_enabled?: boolean;
166
171
  moderation_bulk_submit_action_enabled?: boolean;
167
172
  moderation_dashboard_preferences?: Record<string, unknown> | null;
168
173
  moderation_audio_call_moderation_enabled?: boolean;
169
174
  moderation_enabled?: boolean;
170
175
  moderation_flood_rules_enabled?: boolean;
176
+ moderation_keyframe_video_enabled?: boolean;
171
177
  moderation_llm_configurability_enabled?: boolean;
172
178
  moderation_multitenant_blocklist_enabled?: boolean;
179
+ moderation_s3_image_access_role_arn?: string;
173
180
  moderation_video_call_moderation_enabled?: boolean;
174
181
  moderation_webhook_url?: string;
175
182
  multi_tenant_enabled?: boolean;
@@ -206,6 +213,7 @@ export type AppSettingsAPIResponse = APIResponse & {
206
213
  use_hook_v2?: boolean;
207
214
  user_response_time_enabled?: boolean;
208
215
  user_search_disallowed_roles?: string[] | null;
216
+ video_primary_use_case?: string;
209
217
  video_provider?: string;
210
218
  webhook_events?: Array<string>;
211
219
  webhook_url?: string;
@@ -4990,8 +4998,37 @@ export type UpdateChannelsBatchOptions = {
4990
4998
  filter: UpdateChannelsBatchFilters;
4991
4999
  members?: string[] | Array<NewMemberPayload>;
4992
5000
  data?: BatchChannelDataUpdate;
5001
+ /**
5002
+ * `updateData` only. Merges these keys into each matched channel's existing
5003
+ * custom object, leaving every other custom key untouched — unlike
5004
+ * `data.custom`, which replaces the whole object. Keys are dot-paths, so
5005
+ * `a.b` sets key `b` inside object `a` (the parent object must already
5006
+ * exist). Cannot be combined with `data.custom`.
5007
+ *
5008
+ * Lives at the request root, not inside `data`.
5009
+ */
5010
+ custom_set?: Record<string, unknown>;
5011
+ /**
5012
+ * `updateData` only. Deletes these keys from each matched channel's existing
5013
+ * custom object, leaving every other custom key untouched. Keys are
5014
+ * dot-paths; deleting a key that does not exist is a no-op. Cannot be
5015
+ * combined with `data.custom`.
5016
+ *
5017
+ * Lives at the request root, not inside `data`.
5018
+ */
5019
+ custom_unset?: string[];
4993
5020
  };
4994
5021
 
5022
+ /**
5023
+ * Options for {@link ChannelBatchUpdater.updateData}. `custom_set` and
5024
+ * `custom_unset` are sent at the request root, while `data` contains the
5025
+ * channel fields to update.
5026
+ */
5027
+ export type ChannelBatchDataUpdateOptions = Pick<
5028
+ UpdateChannelsBatchOptions,
5029
+ 'data' | 'custom_set' | 'custom_unset'
5030
+ >;
5031
+
4995
5032
  export type UpdateChannelsBatchFilters = QueryFilters<{
4996
5033
  cids?:
4997
5034
  | RequireOnlyOne<Pick<QueryFilter<string>, '$in' | '$eq'>>