stream-chat 9.52.0 → 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.
- package/dist/cjs/index.browser.js +108 -48
- package/dist/cjs/index.browser.js.map +2 -2
- package/dist/cjs/index.node.js +108 -48
- package/dist/cjs/index.node.js.map +2 -2
- package/dist/esm/index.mjs +108 -48
- package/dist/esm/index.mjs.map +2 -2
- package/dist/types/channel.d.ts +4 -2
- package/dist/types/channel_batch_updater.d.ts +9 -2
- package/dist/types/client.d.ts +17 -0
- package/dist/types/types.d.ts +33 -0
- package/dist/types/utils.d.ts +9 -5
- package/package.json +1 -1
- package/src/channel.ts +26 -18
- package/src/channel_batch_updater.ts +28 -2
- package/src/client.ts +94 -23
- package/src/messageDelivery/MessageDeliveryReporter.ts +5 -4
- package/src/types.ts +37 -0
- package/src/utils.ts +30 -7
package/dist/types/channel.d.ts
CHANGED
|
@@ -491,14 +491,16 @@ export declare class Channel {
|
|
|
491
491
|
*/
|
|
492
492
|
markRead(data?: MarkReadOptions): Promise<EventAPIResponse | null>;
|
|
493
493
|
/**
|
|
494
|
-
* markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is
|
|
494
|
+
* markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is
|
|
495
|
+
* enabled and the user holds the `read-events` capability on this channel
|
|
495
496
|
*
|
|
496
497
|
* @param {MarkReadOptions} data
|
|
497
498
|
* @return {Promise<EventAPIResponse | null>} Description
|
|
498
499
|
*/
|
|
499
500
|
markAsReadRequest(data?: MarkReadOptions): Promise<EventAPIResponse | null>;
|
|
500
501
|
/**
|
|
501
|
-
* markUnread - Mark the channel as unread from messageID, only works if the `read_events` setting is
|
|
502
|
+
* markUnread - Mark the channel as unread from messageID, only works if the `read_events` setting is
|
|
503
|
+
* enabled and the user holds the `read-events` capability on this channel
|
|
502
504
|
*
|
|
503
505
|
* @param {MarkUnreadOptions} data
|
|
504
506
|
* @return {APIResponse} An API response
|
|
@@ -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}
|
|
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
|
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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/dist/types/utils.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ import type { AxiosRequestConfig } from 'axios';
|
|
|
12
12
|
*
|
|
13
13
|
*/
|
|
14
14
|
export declare function logChatPromiseExecution<T>(promise: Promise<T>, name: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* The default no-op client logger.
|
|
17
|
+
*/
|
|
18
|
+
export declare const noopLogger: Logger;
|
|
19
|
+
/**
|
|
20
|
+
* Invokes a single event listener in isolation, so that a faulty listener cannot
|
|
21
|
+
* abort the dispatch loop or the bookkeeping that follows it.
|
|
22
|
+
*/
|
|
23
|
+
export declare const invokeEventListener: <T>(listener: (event: T) => unknown, event: T, logger: Logger) => void;
|
|
15
24
|
export declare const sleep: (m: number) => Promise<void>;
|
|
16
25
|
export declare function isFunction(value: unknown): value is (...args: unknown[]) => unknown;
|
|
17
26
|
export declare const chatCodes: {
|
|
@@ -28,11 +37,6 @@ export declare const channelHasReadEvents: (channel?: Channel) => boolean;
|
|
|
28
37
|
* channelTracksReadLocally - Whether a channel maintains a client local unread count.
|
|
29
38
|
*/
|
|
30
39
|
export declare const channelTracksReadLocally: (channel?: Channel) => boolean;
|
|
31
|
-
/**
|
|
32
|
-
* userHasReadReceipts - Whether the current user allows read receipts, per their privacy settings.
|
|
33
|
-
* Read receipts are treated as enabled unless the user has explicitly disabled them.
|
|
34
|
-
*/
|
|
35
|
-
export declare const userHasReadReceipts: (client: StreamChat) => boolean;
|
|
36
40
|
export declare function addFileToFormData(uri: string | NodeJS.ReadableStream | Buffer | File, name?: string, contentType?: string): FormData;
|
|
37
41
|
export declare function normalizeQuerySort<T extends Record<string, AscDesc | undefined>>(sort: T | T[]): {
|
|
38
42
|
direction: AscDesc;
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { MessageReceiptsTracker } from './messageDelivery';
|
|
|
6
6
|
import {
|
|
7
7
|
channelHasReadEvents,
|
|
8
8
|
generateChannelTempCid,
|
|
9
|
+
invokeEventListener,
|
|
9
10
|
logChatPromiseExecution,
|
|
10
11
|
messageSetPagination,
|
|
11
12
|
normalizeQuerySort,
|
|
@@ -1221,7 +1222,8 @@ export class Channel {
|
|
|
1221
1222
|
}
|
|
1222
1223
|
|
|
1223
1224
|
/**
|
|
1224
|
-
* markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is
|
|
1225
|
+
* markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is
|
|
1226
|
+
* enabled and the user holds the `read-events` capability on this channel
|
|
1225
1227
|
*
|
|
1226
1228
|
* @param {MarkReadOptions} data
|
|
1227
1229
|
* @return {Promise<EventAPIResponse | null>} Description
|
|
@@ -1229,7 +1231,13 @@ export class Channel {
|
|
|
1229
1231
|
async markAsReadRequest(data: MarkReadOptions = {}) {
|
|
1230
1232
|
this._checkInitialized();
|
|
1231
1233
|
|
|
1232
|
-
|
|
1234
|
+
// Server-side auth acts on behalf of a user with no own capabilities, so only the client-side
|
|
1235
|
+
// flow is gated. Channels the user has no `read-events` capability on do not track read state
|
|
1236
|
+
// on the backend at all - see `markReadLocally` for the client-local unread count.
|
|
1237
|
+
if (
|
|
1238
|
+
!this.getClient()._isUsingServerAuth() &&
|
|
1239
|
+
(!this.getConfig()?.read_events || !channelHasReadEvents(this))
|
|
1240
|
+
) {
|
|
1233
1241
|
return null;
|
|
1234
1242
|
}
|
|
1235
1243
|
|
|
@@ -1239,7 +1247,8 @@ export class Channel {
|
|
|
1239
1247
|
}
|
|
1240
1248
|
|
|
1241
1249
|
/**
|
|
1242
|
-
* markUnread - Mark the channel as unread from messageID, only works if the `read_events` setting is
|
|
1250
|
+
* markUnread - Mark the channel as unread from messageID, only works if the `read_events` setting is
|
|
1251
|
+
* enabled and the user holds the `read-events` capability on this channel
|
|
1243
1252
|
*
|
|
1244
1253
|
* @param {MarkUnreadOptions} data
|
|
1245
1254
|
* @return {APIResponse} An API response
|
|
@@ -1247,7 +1256,11 @@ export class Channel {
|
|
|
1247
1256
|
async markUnread(data: MarkUnreadOptions) {
|
|
1248
1257
|
this._checkInitialized();
|
|
1249
1258
|
|
|
1250
|
-
|
|
1259
|
+
// Gated the same way as `markAsReadRequest` - see the comment there.
|
|
1260
|
+
if (
|
|
1261
|
+
!this.getClient()._isUsingServerAuth() &&
|
|
1262
|
+
(!this.getConfig()?.read_events || !channelHasReadEvents(this))
|
|
1263
|
+
) {
|
|
1251
1264
|
return Promise.resolve(null);
|
|
1252
1265
|
}
|
|
1253
1266
|
|
|
@@ -2354,22 +2367,17 @@ export class Channel {
|
|
|
2354
2367
|
}
|
|
2355
2368
|
|
|
2356
2369
|
_callChannelListeners = (event: Event) => {
|
|
2357
|
-
//
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
}
|
|
2364
|
-
if (channel.listeners[event.type]) {
|
|
2365
|
-
listeners.push(...channel.listeners[event.type]);
|
|
2366
|
-
}
|
|
2370
|
+
// snapshot before dispatching: `on` pushes into these arrays in place, so a
|
|
2371
|
+
// listener that subscribes while handling an event must not be invoked for it
|
|
2372
|
+
const listeners = [
|
|
2373
|
+
...(this.listeners.all ?? []),
|
|
2374
|
+
...(this.listeners[event.type] ?? []),
|
|
2375
|
+
];
|
|
2367
2376
|
|
|
2368
|
-
|
|
2377
|
+
const { logger } = this._client;
|
|
2369
2378
|
for (const listener of listeners) {
|
|
2370
|
-
if (typeof listener
|
|
2371
|
-
|
|
2372
|
-
}
|
|
2379
|
+
if (typeof listener === 'string') continue;
|
|
2380
|
+
invokeEventListener(listener, event, logger);
|
|
2373
2381
|
}
|
|
2374
2382
|
};
|
|
2375
2383
|
|
|
@@ -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}
|
|
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
|
@@ -29,10 +29,12 @@ import {
|
|
|
29
29
|
axiosParamsSerializer,
|
|
30
30
|
chatCodes,
|
|
31
31
|
generateChannelTempCid,
|
|
32
|
+
invokeEventListener,
|
|
32
33
|
isFunction,
|
|
33
34
|
isOnline,
|
|
34
35
|
isOwnUserBaseProperty,
|
|
35
36
|
messageSetPagination,
|
|
37
|
+
noopLogger,
|
|
36
38
|
normalizeQuerySort,
|
|
37
39
|
randomId,
|
|
38
40
|
retryInterval,
|
|
@@ -391,6 +393,10 @@ export class StreamChat {
|
|
|
391
393
|
wsConnection: StableWSConnection | null;
|
|
392
394
|
wsFallback?: WSConnectionFallback;
|
|
393
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;
|
|
394
400
|
consecutiveFailures: number;
|
|
395
401
|
insightMetrics: InsightMetrics;
|
|
396
402
|
defaultWSTimeoutWithFallback: number;
|
|
@@ -575,7 +581,7 @@ export class StreamChat {
|
|
|
575
581
|
* channel: object
|
|
576
582
|
* }
|
|
577
583
|
*/
|
|
578
|
-
this.logger = isFunction(inputOptions.logger) ? inputOptions.logger :
|
|
584
|
+
this.logger = isFunction(inputOptions.logger) ? inputOptions.logger : noopLogger;
|
|
579
585
|
this.recoverStateOnReconnect = this.options.recoverStateOnReconnect;
|
|
580
586
|
this.threads = new ThreadManager({ client: this });
|
|
581
587
|
this.polls = new PollManager({ client: this });
|
|
@@ -711,18 +717,25 @@ export class StreamChat {
|
|
|
711
717
|
|
|
712
718
|
const wsPromise = this.openConnection();
|
|
713
719
|
|
|
714
|
-
|
|
720
|
+
const setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
|
|
715
721
|
(result) => result[1], // We only return connection promise;
|
|
716
722
|
);
|
|
723
|
+
this.setUserPromise = setUserPromise;
|
|
717
724
|
|
|
718
725
|
try {
|
|
719
|
-
return await
|
|
726
|
+
return await setUserPromise;
|
|
720
727
|
} catch (err) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
+
}
|
|
726
739
|
}
|
|
727
740
|
throw err;
|
|
728
741
|
}
|
|
@@ -850,9 +863,58 @@ export class StreamChat {
|
|
|
850
863
|
}
|
|
851
864
|
|
|
852
865
|
this.clientID = `${this.userID}--${randomId()}`;
|
|
853
|
-
|
|
866
|
+
const wsPromise = this._bindWsPromise(this.connect());
|
|
854
867
|
this._startCleaning();
|
|
855
|
-
return
|
|
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;
|
|
856
918
|
};
|
|
857
919
|
|
|
858
920
|
/**
|
|
@@ -1057,6 +1119,11 @@ export class StreamChat {
|
|
|
1057
1119
|
|
|
1058
1120
|
this.anonymous = false;
|
|
1059
1121
|
|
|
1122
|
+
this._rejectPendingWsPromise(
|
|
1123
|
+
new Error('Connection was closed because disconnectUser() was called'),
|
|
1124
|
+
);
|
|
1125
|
+
this.wsPromise = null;
|
|
1126
|
+
|
|
1060
1127
|
const closePromise = this.closeConnection(timeout);
|
|
1061
1128
|
|
|
1062
1129
|
for (const channel of Object.values(this.activeChannels)) {
|
|
@@ -1700,20 +1767,15 @@ export class StreamChat {
|
|
|
1700
1767
|
}
|
|
1701
1768
|
|
|
1702
1769
|
_callClientListeners = (event: Event) => {
|
|
1703
|
-
//
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
}
|
|
1710
|
-
if (client.listeners[event.type]) {
|
|
1711
|
-
listeners.push(...client.listeners[event.type]);
|
|
1712
|
-
}
|
|
1770
|
+
// snapshot before dispatching: `on` pushes into these arrays in place, so a
|
|
1771
|
+
// listener that subscribes while handling an event must not be invoked for it
|
|
1772
|
+
const listeners = [
|
|
1773
|
+
...(this.listeners.all ?? []),
|
|
1774
|
+
...(this.listeners[event.type] ?? []),
|
|
1775
|
+
];
|
|
1713
1776
|
|
|
1714
|
-
// call the event and send it to the listeners
|
|
1715
1777
|
for (const listener of listeners) {
|
|
1716
|
-
listener
|
|
1778
|
+
invokeEventListener(listener, event, this.logger);
|
|
1717
1779
|
}
|
|
1718
1780
|
};
|
|
1719
1781
|
|
|
@@ -1754,7 +1816,12 @@ export class StreamChat {
|
|
|
1754
1816
|
} as Event);
|
|
1755
1817
|
}
|
|
1756
1818
|
|
|
1757
|
-
|
|
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
|
+
}
|
|
1758
1825
|
this.setUserPromise = Promise.resolve();
|
|
1759
1826
|
};
|
|
1760
1827
|
|
|
@@ -5309,6 +5376,10 @@ export class StreamChat {
|
|
|
5309
5376
|
/**
|
|
5310
5377
|
* Update Channels Batch
|
|
5311
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
|
+
*
|
|
5312
5383
|
* @param {UpdateChannelsBatchOptions} payload for updating channels in batch
|
|
5313
5384
|
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
5314
5385
|
*/
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
MarkReadOptions,
|
|
11
11
|
} from '../types';
|
|
12
12
|
import { type APIErrorResponse } from '../types';
|
|
13
|
-
import { throttle
|
|
13
|
+
import { throttle } from '../utils';
|
|
14
14
|
import { isAPIError, isErrorRetryable } from '../errors';
|
|
15
15
|
|
|
16
16
|
const MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD = 100 as const;
|
|
@@ -279,8 +279,6 @@ export class MessageDeliveryReporter {
|
|
|
279
279
|
* @param options
|
|
280
280
|
*/
|
|
281
281
|
public markRead = async (collection: Channel | Thread, options?: MarkReadOptions) => {
|
|
282
|
-
if (!userHasReadReceipts(this.client)) return null;
|
|
283
|
-
|
|
284
282
|
let result: EventAPIResponse | null = null;
|
|
285
283
|
if (isChannel(collection)) {
|
|
286
284
|
result = await collection.markAsReadRequest(options);
|
|
@@ -291,7 +289,10 @@ export class MessageDeliveryReporter {
|
|
|
291
289
|
});
|
|
292
290
|
}
|
|
293
291
|
|
|
294
|
-
|
|
292
|
+
// A read implies delivery, so the candidate is only dropped once the request actually went
|
|
293
|
+
// out. `markAsReadRequest` returns null for channels that do not track read state on the
|
|
294
|
+
// backend, and those candidates must survive for the next delivery report.
|
|
295
|
+
if (result) this.removeCandidateFor(collection);
|
|
295
296
|
return result;
|
|
296
297
|
};
|
|
297
298
|
|
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'>>
|