stream-chat 9.50.2 → 9.51.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 +252 -94
- package/dist/cjs/index.browser.js.map +3 -3
- package/dist/cjs/index.node.js +252 -94
- package/dist/cjs/index.node.js.map +3 -3
- package/dist/esm/index.mjs +252 -94
- package/dist/esm/index.mjs.map +3 -3
- package/dist/types/channel.d.ts +3 -2
- package/dist/types/client.d.ts +13 -9
- package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +15 -9
- package/dist/types/offline-support/offline_support_api.d.ts +7 -1
- package/dist/types/offline-support/offline_sync_manager.d.ts +19 -1
- package/dist/types/search/BaseSearchSource.d.ts +42 -24
- package/dist/types/search/ChannelMemberSearchSource.d.ts +9 -4
- package/dist/types/search/ChannelSearchSource.d.ts +9 -3
- package/dist/types/search/MessageSearchSource.d.ts +14 -3
- package/dist/types/search/UserSearchSource.d.ts +9 -3
- package/dist/types/search/types.d.ts +14 -1
- package/dist/types/types.d.ts +20 -1
- package/dist/types/utils.d.ts +1 -1
- package/package.json +7 -7
- package/src/channel.ts +4 -0
- package/src/client.ts +54 -12
- package/src/messageComposer/middleware/textComposer/mentions.ts +75 -23
- package/src/offline-support/offline_support_api.ts +17 -2
- package/src/offline-support/offline_sync_manager.ts +73 -14
- package/src/search/BaseSearchSource.ts +123 -39
- package/src/search/ChannelMemberSearchSource.ts +22 -11
- package/src/search/ChannelSearchSource.ts +16 -5
- package/src/search/MessageSearchSource.ts +36 -4
- package/src/search/UserSearchSource.ts +16 -5
- package/src/search/types.ts +14 -1
- package/src/types.ts +21 -1
- package/src/utils.ts +6 -3
package/src/channel.ts
CHANGED
|
@@ -62,6 +62,7 @@ import type {
|
|
|
62
62
|
QueryMembersOptions,
|
|
63
63
|
Reaction,
|
|
64
64
|
ReactionAPIResponse,
|
|
65
|
+
RequestOptions,
|
|
65
66
|
SearchAPIResponse,
|
|
66
67
|
SearchMessageSortBase,
|
|
67
68
|
SearchOptions,
|
|
@@ -377,6 +378,7 @@ export class Channel {
|
|
|
377
378
|
* @param {MemberSort} [sort] Sort options, for instance [{created_at: -1}].
|
|
378
379
|
* When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{name: -1}, {created_at: 1}]
|
|
379
380
|
* @param {{ limit?: number; offset?: number }} [options] Option object, {limit: 10, offset:10}
|
|
381
|
+
* @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
|
|
380
382
|
*
|
|
381
383
|
* @return {Promise<ChannelMemberAPIResponse>} Query Members response
|
|
382
384
|
*/
|
|
@@ -384,6 +386,7 @@ export class Channel {
|
|
|
384
386
|
filterConditions: MemberFilters,
|
|
385
387
|
sort: MemberSort = [],
|
|
386
388
|
options: QueryMembersOptions = {},
|
|
389
|
+
requestOptions: RequestOptions = {},
|
|
387
390
|
) {
|
|
388
391
|
let id: string | undefined;
|
|
389
392
|
const type = this.type;
|
|
@@ -406,6 +409,7 @@ export class Channel {
|
|
|
406
409
|
...options,
|
|
407
410
|
},
|
|
408
411
|
},
|
|
412
|
+
requestOptions,
|
|
409
413
|
);
|
|
410
414
|
}
|
|
411
415
|
|
package/src/client.ts
CHANGED
|
@@ -219,6 +219,7 @@ import type {
|
|
|
219
219
|
ReminderAPIResponse,
|
|
220
220
|
RemoveUserGroupMembersOptions,
|
|
221
221
|
RemoveUserGroupMembersResponse,
|
|
222
|
+
RequestOptions,
|
|
222
223
|
ReviewFlagReportOptions,
|
|
223
224
|
ReviewFlagReportResponse,
|
|
224
225
|
SdkIdentifier,
|
|
@@ -1312,6 +1313,9 @@ export class StreamChat {
|
|
|
1312
1313
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1313
1314
|
} catch (e: any /**TODO: generalize error types */) {
|
|
1314
1315
|
e.client_request_id = requestConfig.headers?.['x-client-request-id'];
|
|
1316
|
+
// An aborted request is a deliberate cancellation, not a failure - logging it
|
|
1317
|
+
// and counting it towards the token-refresh backoff would be misleading.
|
|
1318
|
+
if (axios.isCancel(e)) throw e;
|
|
1315
1319
|
this._logApiError(type, url, e);
|
|
1316
1320
|
this.consecutiveFailures += 1;
|
|
1317
1321
|
if (e.response) {
|
|
@@ -1333,16 +1337,22 @@ export class StreamChat {
|
|
|
1333
1337
|
}
|
|
1334
1338
|
};
|
|
1335
1339
|
|
|
1336
|
-
get<T>(
|
|
1337
|
-
|
|
1340
|
+
get<T>(
|
|
1341
|
+
url: string,
|
|
1342
|
+
params?: AxiosRequestConfig['params'],
|
|
1343
|
+
// `params` is omitted deliberately: _enrichAxiosOptions spreads `config` after the
|
|
1344
|
+
// enriched params, so a `config.params` would drop api_key/user_id/connection_id.
|
|
1345
|
+
config?: Omit<AxiosRequestConfig, 'params'>,
|
|
1346
|
+
) {
|
|
1347
|
+
return this.doAxiosRequest<T>('get', url, null, { params, config });
|
|
1338
1348
|
}
|
|
1339
1349
|
|
|
1340
1350
|
put<T>(url: string, data?: unknown) {
|
|
1341
1351
|
return this.doAxiosRequest<T>('put', url, data);
|
|
1342
1352
|
}
|
|
1343
1353
|
|
|
1344
|
-
post<T>(url: string, data?: unknown) {
|
|
1345
|
-
return this.doAxiosRequest<T>('post', url, data);
|
|
1354
|
+
post<T>(url: string, data?: unknown, config?: Omit<AxiosRequestConfig, 'params'>) {
|
|
1355
|
+
return this.doAxiosRequest<T>('post', url, data, { config });
|
|
1346
1356
|
}
|
|
1347
1357
|
|
|
1348
1358
|
patch<T>(url: string, data?: unknown) {
|
|
@@ -1837,6 +1847,7 @@ export class StreamChat {
|
|
|
1837
1847
|
* @param {UserSort} sort Sort options, for instance [{last_active: -1}].
|
|
1838
1848
|
* When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_active: -1}, {created_at: 1}]
|
|
1839
1849
|
* @param {UserOptions} options Option object, {presence: true}
|
|
1850
|
+
* @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
|
|
1840
1851
|
*
|
|
1841
1852
|
* @return {Promise<{ users: Array<UserResponse> }>} User Query Response
|
|
1842
1853
|
*/
|
|
@@ -1844,6 +1855,7 @@ export class StreamChat {
|
|
|
1844
1855
|
filterConditions: UserFilters,
|
|
1845
1856
|
sort: UserSort = [],
|
|
1846
1857
|
options: UserOptions = {},
|
|
1858
|
+
requestOptions: RequestOptions = {},
|
|
1847
1859
|
) {
|
|
1848
1860
|
const defaultOptions = {
|
|
1849
1861
|
presence: false,
|
|
@@ -1867,6 +1879,7 @@ export class StreamChat {
|
|
|
1867
1879
|
...options,
|
|
1868
1880
|
},
|
|
1869
1881
|
},
|
|
1882
|
+
requestOptions,
|
|
1870
1883
|
);
|
|
1871
1884
|
|
|
1872
1885
|
this.state.updateUsers(data.users);
|
|
@@ -1918,13 +1931,17 @@ export class StreamChat {
|
|
|
1918
1931
|
* searchUserGroups - Search user groups by prefix for autocomplete
|
|
1919
1932
|
*
|
|
1920
1933
|
* @param {SearchUserGroupsOptions} options The search options
|
|
1921
|
-
*
|
|
1934
|
+
* @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
|
|
1922
1935
|
* @return {Promise<SearchUserGroupsResponse>} User Group Search Response
|
|
1923
1936
|
*/
|
|
1924
|
-
async searchUserGroups(
|
|
1937
|
+
async searchUserGroups(
|
|
1938
|
+
options: SearchUserGroupsOptions,
|
|
1939
|
+
requestOptions: RequestOptions = {},
|
|
1940
|
+
) {
|
|
1925
1941
|
return await this.get<SearchUserGroupsResponse>(
|
|
1926
1942
|
this.baseURL + '/usergroups/search',
|
|
1927
1943
|
options,
|
|
1944
|
+
requestOptions,
|
|
1928
1945
|
);
|
|
1929
1946
|
}
|
|
1930
1947
|
|
|
@@ -2061,6 +2078,7 @@ export class StreamChat {
|
|
|
2061
2078
|
* @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}.
|
|
2062
2079
|
* When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}]
|
|
2063
2080
|
* @param {ChannelOptions} [options] Options object. Can include predefined_filter, filter_values, and sort_values for using predefined filters.
|
|
2081
|
+
* @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
|
|
2064
2082
|
*
|
|
2065
2083
|
* @return {Promise<QueryChannelsAPIResponse>} full search channels response
|
|
2066
2084
|
*/
|
|
@@ -2068,6 +2086,7 @@ export class StreamChat {
|
|
|
2068
2086
|
filterConditions: ChannelFilters,
|
|
2069
2087
|
sort: ChannelSort = [],
|
|
2070
2088
|
options: ChannelOptions = {},
|
|
2089
|
+
requestOptions: RequestOptions = {},
|
|
2071
2090
|
): Promise<QueryChannelsAPIResponse> {
|
|
2072
2091
|
const defaultOptions: ChannelOptions = {
|
|
2073
2092
|
state: true,
|
|
@@ -2101,7 +2120,11 @@ export class StreamChat {
|
|
|
2101
2120
|
...restOptions,
|
|
2102
2121
|
};
|
|
2103
2122
|
|
|
2104
|
-
return await this.post<QueryChannelsAPIResponse>(
|
|
2123
|
+
return await this.post<QueryChannelsAPIResponse>(
|
|
2124
|
+
this.baseURL + '/channels',
|
|
2125
|
+
payload,
|
|
2126
|
+
requestOptions,
|
|
2127
|
+
);
|
|
2105
2128
|
}
|
|
2106
2129
|
|
|
2107
2130
|
/**
|
|
@@ -2147,6 +2170,7 @@ export class StreamChat {
|
|
|
2147
2170
|
* - stateOptions.skipInitialization - Skips the initialization of the state for the channels matching the ids in the list.
|
|
2148
2171
|
* - stateOptions.skipHydration - Skips returning the channels as instances of the Channel class and rather returns the raw query response.
|
|
2149
2172
|
* - stateOptions.withResponse - Returns the full query response with hydrated channels. This is a compatibility bridge for internal callers that need response-level metadata while the default return value remains `Channel[]`.
|
|
2173
|
+
* - stateOptions.signal - Aborts the request. See AbortController.
|
|
2150
2174
|
*
|
|
2151
2175
|
* @return {Promise<Array<Channel>>} search channels response
|
|
2152
2176
|
*/
|
|
@@ -2172,6 +2196,8 @@ export class StreamChat {
|
|
|
2172
2196
|
filterConditions,
|
|
2173
2197
|
sort,
|
|
2174
2198
|
options,
|
|
2199
|
+
// stateOptions is never serialized, so it carries the abort signal
|
|
2200
|
+
{ signal: stateOptions.signal },
|
|
2175
2201
|
);
|
|
2176
2202
|
const channels = queryChannelsResponse.channels;
|
|
2177
2203
|
|
|
@@ -2317,6 +2343,7 @@ export class StreamChat {
|
|
|
2317
2343
|
* @param {ChannelFilters} filterConditions MongoDB style filter conditions
|
|
2318
2344
|
* @param {MessageFilters | string} query search query or object MongoDB style filters
|
|
2319
2345
|
* @param {SearchOptions} [options] Option object, {user_id: 'tommaso'}
|
|
2346
|
+
* @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
|
|
2320
2347
|
*
|
|
2321
2348
|
* @return {Promise<SearchAPIResponse>} search messages response
|
|
2322
2349
|
*/
|
|
@@ -2324,6 +2351,7 @@ export class StreamChat {
|
|
|
2324
2351
|
filterConditions: ChannelFilters,
|
|
2325
2352
|
query: string | MessageFilters,
|
|
2326
2353
|
options: SearchOptions = {},
|
|
2354
|
+
requestOptions: RequestOptions = {},
|
|
2327
2355
|
) {
|
|
2328
2356
|
if (options.offset && options.next) {
|
|
2329
2357
|
throw Error(`Cannot specify offset with next`);
|
|
@@ -2346,7 +2374,11 @@ export class StreamChat {
|
|
|
2346
2374
|
// Make sure we wait for the connect promise if there is a pending one
|
|
2347
2375
|
await this.wsPromise;
|
|
2348
2376
|
|
|
2349
|
-
return await this.get<SearchAPIResponse>(
|
|
2377
|
+
return await this.get<SearchAPIResponse>(
|
|
2378
|
+
this.baseURL + '/search',
|
|
2379
|
+
{ payload },
|
|
2380
|
+
requestOptions,
|
|
2381
|
+
);
|
|
2350
2382
|
}
|
|
2351
2383
|
|
|
2352
2384
|
/**
|
|
@@ -3806,6 +3838,12 @@ export class StreamChat {
|
|
|
3806
3838
|
...axiosRequestConfigRest
|
|
3807
3839
|
} = this.options.axiosRequestConfig || {};
|
|
3808
3840
|
|
|
3841
|
+
// Most specific wins, and it is spread last so that a config carrying an explicit
|
|
3842
|
+
// `signal: undefined` cannot clobber a client-wide one or an armed
|
|
3843
|
+
// nextRequestAbortController.
|
|
3844
|
+
const resolvedSignal =
|
|
3845
|
+
options.config?.signal ?? axiosRequestConfigRest.signal ?? signal;
|
|
3846
|
+
|
|
3809
3847
|
return {
|
|
3810
3848
|
params: {
|
|
3811
3849
|
user_id: this.userID,
|
|
@@ -3821,9 +3859,9 @@ export class StreamChat {
|
|
|
3821
3859
|
...options.headers,
|
|
3822
3860
|
...(axiosRequestConfigHeaders || {}),
|
|
3823
3861
|
},
|
|
3824
|
-
...(signal ? { signal } : {}),
|
|
3825
3862
|
...options.config,
|
|
3826
|
-
...
|
|
3863
|
+
...axiosRequestConfigRest,
|
|
3864
|
+
...(resolvedSignal ? { signal: resolvedSignal } : {}),
|
|
3827
3865
|
};
|
|
3828
3866
|
}
|
|
3829
3867
|
|
|
@@ -3993,8 +4031,12 @@ export class StreamChat {
|
|
|
3993
4031
|
*
|
|
3994
4032
|
* @returns {Promise<APIResponse>}
|
|
3995
4033
|
*/
|
|
3996
|
-
searchRoles(options: SearchRolesOptions) {
|
|
3997
|
-
return this.get<SearchRolesAPIResponse>(
|
|
4034
|
+
searchRoles(options: SearchRolesOptions, requestOptions: RequestOptions = {}) {
|
|
4035
|
+
return this.get<SearchRolesAPIResponse>(
|
|
4036
|
+
`${this.baseURL}/roles/search`,
|
|
4037
|
+
options,
|
|
4038
|
+
requestOptions,
|
|
4039
|
+
);
|
|
3998
4040
|
}
|
|
3999
4041
|
|
|
4000
4042
|
/** deleteRole - deletes a custom role
|
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
userSuggestionToMentionEntity,
|
|
10
10
|
userSuggestionToUserResponse,
|
|
11
11
|
} from './mentionUtils';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
BaseSearchSource,
|
|
14
|
+
type SearchQueryOptions,
|
|
15
|
+
type SearchSourceOptions,
|
|
16
|
+
} from '../../../search';
|
|
13
17
|
import { mergeWith } from '../../../utils/mergeWith';
|
|
14
18
|
import type {
|
|
15
19
|
ChannelMentionSuggestion,
|
|
@@ -92,6 +96,13 @@ export const calculateLevenshtein = (query: string, name: string) => {
|
|
|
92
96
|
};
|
|
93
97
|
|
|
94
98
|
export type MentionsSearchSourceOptions = SearchSourceOptions & {
|
|
99
|
+
/** Static base filters for the app-wide user query (mentionAllAppUsers). */
|
|
100
|
+
userFilters?: UserFilters;
|
|
101
|
+
userSort?: UserSort;
|
|
102
|
+
/** Static base filters for the channel member query. */
|
|
103
|
+
memberFilters?: MemberFilters;
|
|
104
|
+
memberSort?: MemberSort;
|
|
105
|
+
searchOptions?: Omit<UserOptions, 'limit' | 'offset'>;
|
|
95
106
|
mentionAllAppUsers?: boolean;
|
|
96
107
|
suggestionFactoryMappers?: MentionSuggestionFactoryMapperOverrides;
|
|
97
108
|
textComposerText?: string;
|
|
@@ -299,20 +310,34 @@ export class MentionsSearchSource extends BaseSearchSource<MentionSuggestion> {
|
|
|
299
310
|
userSort: UserSort | undefined;
|
|
300
311
|
memberSort: MemberSort | undefined; // todo: document there are filters and sort options for users and members
|
|
301
312
|
searchOptions: Omit<UserOptions, 'limit' | 'offset'> | undefined;
|
|
302
|
-
config:
|
|
313
|
+
config: Pick<
|
|
314
|
+
MentionsSearchSourceOptions,
|
|
315
|
+
'mentionAllAppUsers' | 'suggestionFactoryMappers' | 'textComposerText' | 'trigger'
|
|
316
|
+
>;
|
|
303
317
|
|
|
304
318
|
constructor(channel: Channel, options?: MentionsSearchSourceOptions) {
|
|
305
319
|
const {
|
|
306
320
|
mentionAllAppUsers,
|
|
321
|
+
memberFilters,
|
|
322
|
+
memberSort,
|
|
323
|
+
searchOptions,
|
|
307
324
|
suggestionFactoryMappers,
|
|
308
325
|
textComposerText,
|
|
309
326
|
transliterate,
|
|
310
327
|
trigger,
|
|
328
|
+
userFilters,
|
|
329
|
+
userSort,
|
|
311
330
|
...restOptions
|
|
312
331
|
} = options || {};
|
|
313
|
-
|
|
332
|
+
// suggestions are shown for a bare trigger, so the empty query must be allowed
|
|
333
|
+
super({ ...restOptions, allowEmptySearchString: true });
|
|
314
334
|
this.client = channel.getClient();
|
|
315
335
|
this.channel = channel;
|
|
336
|
+
this.userFilters = userFilters;
|
|
337
|
+
this.userSort = userSort;
|
|
338
|
+
this.memberFilters = memberFilters;
|
|
339
|
+
this.memberSort = memberSort;
|
|
340
|
+
this.searchOptions = searchOptions;
|
|
316
341
|
this.config = {
|
|
317
342
|
mentionAllAppUsers,
|
|
318
343
|
suggestionFactoryMappers,
|
|
@@ -425,11 +450,6 @@ export class MentionsSearchSource extends BaseSearchSource<MentionSuggestion> {
|
|
|
425
450
|
};
|
|
426
451
|
}
|
|
427
452
|
|
|
428
|
-
canExecuteQuery = (newSearchString?: string) => {
|
|
429
|
-
const hasNewSearchQuery = typeof newSearchString !== 'undefined';
|
|
430
|
-
return this.isActive && !this.isLoading && (hasNewSearchQuery || this.hasNext);
|
|
431
|
-
};
|
|
432
|
-
|
|
433
453
|
protected updatePaginationStateFromQuery() {
|
|
434
454
|
const userPaginationState = this.latestUserPaginationState ?? { itemCount: 0 };
|
|
435
455
|
|
|
@@ -472,10 +492,13 @@ export class MentionsSearchSource extends BaseSearchSource<MentionSuggestion> {
|
|
|
472
492
|
: []),
|
|
473
493
|
].filter(({ name }) => this.matchesPrefixSearchQuery(name, searchQuery));
|
|
474
494
|
|
|
475
|
-
getRoleMentionSuggestions = async (
|
|
495
|
+
getRoleMentionSuggestions = async (
|
|
496
|
+
query: string,
|
|
497
|
+
queryOptions: SearchQueryOptions = {},
|
|
498
|
+
): Promise<RoleMentionSuggestion[]> => {
|
|
476
499
|
if (!this.isMentionTypeAllowed('role')) return [];
|
|
477
500
|
if (!query) return [];
|
|
478
|
-
const { roles } = await this.client.searchRoles({ query });
|
|
501
|
+
const { roles } = await this.client.searchRoles({ query }, queryOptions);
|
|
479
502
|
return [...(roles?.map((role) => role.name) ?? [])]
|
|
480
503
|
.sort((left, right) => left.localeCompare(right))
|
|
481
504
|
.map((role) => this.toRoleMentionSuggestion(role, query));
|
|
@@ -558,23 +581,40 @@ export class MentionsSearchSource extends BaseSearchSource<MentionSuggestion> {
|
|
|
558
581
|
};
|
|
559
582
|
};
|
|
560
583
|
|
|
561
|
-
queryUsers = async (
|
|
584
|
+
queryUsers = async (
|
|
585
|
+
searchQuery: string,
|
|
586
|
+
offset = 0,
|
|
587
|
+
queryOptions: SearchQueryOptions = {},
|
|
588
|
+
) => {
|
|
562
589
|
const { filters, sort, options } = this.prepareQueryUsersParams(searchQuery, offset);
|
|
563
|
-
const { users } = await this.client.queryUsers(filters, sort, options);
|
|
590
|
+
const { users } = await this.client.queryUsers(filters, sort, options, queryOptions);
|
|
564
591
|
return users;
|
|
565
592
|
};
|
|
566
593
|
|
|
567
|
-
queryMembers = async (
|
|
594
|
+
queryMembers = async (
|
|
595
|
+
searchQuery: string,
|
|
596
|
+
offset = 0,
|
|
597
|
+
queryOptions: SearchQueryOptions = {},
|
|
598
|
+
) => {
|
|
568
599
|
const { filters, sort, options } = this.prepareQueryMembersParams(
|
|
569
600
|
searchQuery,
|
|
570
601
|
offset,
|
|
571
602
|
);
|
|
572
|
-
const response = await this.channel.queryMembers(
|
|
603
|
+
const response = await this.channel.queryMembers(
|
|
604
|
+
filters,
|
|
605
|
+
sort,
|
|
606
|
+
options,
|
|
607
|
+
queryOptions,
|
|
608
|
+
);
|
|
573
609
|
|
|
574
610
|
return response.members.map((member) => member.user) as UserResponse[];
|
|
575
611
|
};
|
|
576
612
|
|
|
577
|
-
getUserSuggestionsPage = async (
|
|
613
|
+
getUserSuggestionsPage = async (
|
|
614
|
+
searchQuery: string,
|
|
615
|
+
userOffset = 0,
|
|
616
|
+
queryOptions: SearchQueryOptions = {},
|
|
617
|
+
) => {
|
|
578
618
|
if (!this.isMentionTypeAllowed('user')) {
|
|
579
619
|
return {
|
|
580
620
|
items: [],
|
|
@@ -587,7 +627,7 @@ export class MentionsSearchSource extends BaseSearchSource<MentionSuggestion> {
|
|
|
587
627
|
this.allMembersLoadedWithInitialChannelQuery || !searchQuery;
|
|
588
628
|
|
|
589
629
|
if (this.config.mentionAllAppUsers) {
|
|
590
|
-
users = await this.queryUsers(searchQuery, userOffset);
|
|
630
|
+
users = await this.queryUsers(searchQuery, userOffset, queryOptions);
|
|
591
631
|
} else if (shouldSearchLocally) {
|
|
592
632
|
const localUsers = this.searchMembersLocally(searchQuery);
|
|
593
633
|
const items = localUsers
|
|
@@ -601,7 +641,7 @@ export class MentionsSearchSource extends BaseSearchSource<MentionSuggestion> {
|
|
|
601
641
|
: undefined,
|
|
602
642
|
};
|
|
603
643
|
} else {
|
|
604
|
-
users = await this.queryMembers(searchQuery, userOffset);
|
|
644
|
+
users = await this.queryMembers(searchQuery, userOffset, queryOptions);
|
|
605
645
|
}
|
|
606
646
|
|
|
607
647
|
const items = users.map((user) => this.toUserSuggestion(user, searchQuery));
|
|
@@ -623,7 +663,11 @@ export class MentionsSearchSource extends BaseSearchSource<MentionSuggestion> {
|
|
|
623
663
|
} satisfies UserGroupSearchCursor);
|
|
624
664
|
};
|
|
625
665
|
|
|
626
|
-
getUserGroupSuggestionsPage = async (
|
|
666
|
+
getUserGroupSuggestionsPage = async (
|
|
667
|
+
searchQuery: string,
|
|
668
|
+
cursor?: string,
|
|
669
|
+
queryOptions: SearchQueryOptions = {},
|
|
670
|
+
) => {
|
|
627
671
|
if (!this.isMentionTypeAllowed('user_group')) {
|
|
628
672
|
return {
|
|
629
673
|
items: [],
|
|
@@ -647,7 +691,7 @@ export class MentionsSearchSource extends BaseSearchSource<MentionSuggestion> {
|
|
|
647
691
|
...(userGroupCursor?.id_gt ? { id_gt: userGroupCursor.id_gt } : {}),
|
|
648
692
|
...(userGroupCursor?.name_gt ? { name_gt: userGroupCursor.name_gt } : {}),
|
|
649
693
|
};
|
|
650
|
-
const { user_groups } = await this.client.searchUserGroups(options);
|
|
694
|
+
const { user_groups } = await this.client.searchUserGroups(options, queryOptions);
|
|
651
695
|
|
|
652
696
|
return {
|
|
653
697
|
items: user_groups.map((userGroup) =>
|
|
@@ -657,20 +701,28 @@ export class MentionsSearchSource extends BaseSearchSource<MentionSuggestion> {
|
|
|
657
701
|
};
|
|
658
702
|
};
|
|
659
703
|
|
|
660
|
-
async query(searchQuery: string) {
|
|
704
|
+
async query(searchQuery: string, queryOptions: SearchQueryOptions = {}) {
|
|
661
705
|
const userOffset = this.offset ?? 0;
|
|
662
706
|
const isFirstPage = userOffset === 0 && typeof this.userGroupCursor === 'undefined';
|
|
663
707
|
const previousUserPaginationState = this.latestUserPaginationState;
|
|
664
708
|
const previousUserGroupCursor = this.userGroupCursor;
|
|
665
709
|
const [userResultsState, userGroupResultsState, roleSuggestionsState] =
|
|
666
710
|
await Promise.allSettled([
|
|
667
|
-
this.getUserSuggestionsPage(searchQuery, userOffset),
|
|
668
|
-
this.getUserGroupSuggestionsPage(
|
|
711
|
+
this.getUserSuggestionsPage(searchQuery, userOffset, queryOptions),
|
|
712
|
+
this.getUserGroupSuggestionsPage(
|
|
713
|
+
searchQuery,
|
|
714
|
+
previousUserGroupCursor,
|
|
715
|
+
queryOptions,
|
|
716
|
+
),
|
|
669
717
|
isFirstPage
|
|
670
|
-
? this.getRoleMentionSuggestions(searchQuery)
|
|
718
|
+
? this.getRoleMentionSuggestions(searchQuery, queryOptions)
|
|
671
719
|
: Promise.resolve([] as RoleMentionSuggestion[]),
|
|
672
720
|
]);
|
|
673
721
|
|
|
722
|
+
// On abort the requests above reject and the fallback branches below would write
|
|
723
|
+
// empty results into the pagination cursors, corrupting them for the newer query.
|
|
724
|
+
if (queryOptions.signal?.aborted) return { items: [] };
|
|
725
|
+
|
|
674
726
|
const userResults =
|
|
675
727
|
userResultsState.status === 'fulfilled'
|
|
676
728
|
? userResultsState.value
|
|
@@ -40,9 +40,24 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
40
40
|
public syncManager: OfflineDBSyncManager;
|
|
41
41
|
public state: StateStore<OfflineDBState>;
|
|
42
42
|
|
|
43
|
-
constructor({
|
|
43
|
+
constructor({
|
|
44
|
+
client,
|
|
45
|
+
syncMaxEventCount,
|
|
46
|
+
}: {
|
|
47
|
+
client: StreamChat;
|
|
48
|
+
/**
|
|
49
|
+
* Optional positive cap forwarded to the {@link OfflineDBSyncManager}, which
|
|
50
|
+
* owns sync. `undefined` or any non-positive value means "no limit". See
|
|
51
|
+
* {@link OfflineDBSyncManager.syncMaxEventCount}.
|
|
52
|
+
*/
|
|
53
|
+
syncMaxEventCount?: number;
|
|
54
|
+
}) {
|
|
44
55
|
this.client = client;
|
|
45
|
-
this.syncManager = new OfflineDBSyncManager({
|
|
56
|
+
this.syncManager = new OfflineDBSyncManager({
|
|
57
|
+
client,
|
|
58
|
+
offlineDb: this,
|
|
59
|
+
syncMaxEventCount,
|
|
60
|
+
});
|
|
46
61
|
this.state = new StateStore<OfflineDBState>({
|
|
47
62
|
initialized: false,
|
|
48
63
|
userId: this.client.userID,
|
|
@@ -20,16 +20,29 @@ export class OfflineDBSyncManager {
|
|
|
20
20
|
new Map();
|
|
21
21
|
private client: StreamChat;
|
|
22
22
|
private offlineDb: AbstractOfflineDB;
|
|
23
|
+
/**
|
|
24
|
+
* Optional positive cap on the number of events a single `/sync` response may
|
|
25
|
+
* contain before this manager skips event replay into local storage and relies
|
|
26
|
+
* on the reconnect-time channel refresh (queryChannels + `channel.watch()`) to
|
|
27
|
+
* bring visible surfaces up to date.
|
|
28
|
+
*
|
|
29
|
+
* `undefined` or any non-positive value means "no limit" and event replay always
|
|
30
|
+
* runs with whatever maximum the server decides to return.
|
|
31
|
+
*/
|
|
32
|
+
public readonly syncMaxEventCount?: number;
|
|
23
33
|
|
|
24
34
|
constructor({
|
|
25
35
|
client,
|
|
26
36
|
offlineDb,
|
|
37
|
+
syncMaxEventCount,
|
|
27
38
|
}: {
|
|
28
39
|
client: StreamChat;
|
|
29
40
|
offlineDb: AbstractOfflineDB;
|
|
41
|
+
syncMaxEventCount?: number;
|
|
30
42
|
}) {
|
|
31
43
|
this.client = client;
|
|
32
44
|
this.offlineDb = offlineDb;
|
|
45
|
+
this.syncMaxEventCount = syncMaxEventCount;
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
/**
|
|
@@ -111,14 +124,27 @@ export class OfflineDBSyncManager {
|
|
|
111
124
|
*/
|
|
112
125
|
private invokeSyncStatusListeners = async (status: boolean) => {
|
|
113
126
|
this.syncStatus = status;
|
|
114
|
-
this.syncStatusListeners.forEach((l) =>
|
|
127
|
+
this.syncStatusListeners.forEach((l) => {
|
|
128
|
+
try {
|
|
129
|
+
l(status);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.log('Error in a sync status listener.', error);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
115
134
|
|
|
116
135
|
if (status) {
|
|
117
|
-
const promises = Array.from(this.scheduledSyncStatusCallbacks.values()).map(
|
|
118
|
-
cb
|
|
136
|
+
const promises = Array.from(this.scheduledSyncStatusCallbacks.values()).map(
|
|
137
|
+
async (cb) => {
|
|
138
|
+
try {
|
|
139
|
+
await cb();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.log('Error executing a scheduled sync status callback.', error);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
119
144
|
);
|
|
145
|
+
// Every callback is isolated above, so Promise.all never rejects and clear()
|
|
146
|
+
// always runs (no double-execution on the next sync).
|
|
120
147
|
await Promise.all(promises);
|
|
121
|
-
|
|
122
148
|
this.scheduledSyncStatusCallbacks.clear();
|
|
123
149
|
}
|
|
124
150
|
};
|
|
@@ -160,14 +186,29 @@ export class OfflineDBSyncManager {
|
|
|
160
186
|
await this.offlineDb.resetDB();
|
|
161
187
|
} else {
|
|
162
188
|
const result = await this.client.sync(cids, lastSyncedAtDate.toISOString());
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
189
|
+
|
|
190
|
+
// Opt-in positive cap owned by this manager; undefined/non-positive = no limit.
|
|
191
|
+
const { syncMaxEventCount } = this;
|
|
192
|
+
const exceedsLimit =
|
|
193
|
+
typeof syncMaxEventCount === 'number' &&
|
|
194
|
+
syncMaxEventCount > 0 &&
|
|
195
|
+
result.events.length > syncMaxEventCount;
|
|
196
|
+
if (exceedsLimit) {
|
|
197
|
+
this.client.logger(
|
|
198
|
+
'warn',
|
|
199
|
+
`Skipping sync event replay: received ${result.events.length} events, which exceeds the configured limit of ${syncMaxEventCount}. Visible channels are refreshed on reconnect instead.`,
|
|
200
|
+
{ tags: ['sync', 'offline'] },
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
const queryPromises = result.events.map((event) =>
|
|
204
|
+
this.offlineDb.handleEvent({ event, execute: false }),
|
|
205
|
+
);
|
|
206
|
+
const queriesArray = await Promise.all(queryPromises);
|
|
207
|
+
const queries = queriesArray.flat() as ExecuteBatchDBQueriesType;
|
|
208
|
+
|
|
209
|
+
if (queries.length) {
|
|
210
|
+
await this.offlineDb.executeSqlBatch(queries);
|
|
211
|
+
}
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
}
|
|
@@ -198,9 +239,27 @@ export class OfflineDBSyncManager {
|
|
|
198
239
|
|
|
199
240
|
/**
|
|
200
241
|
* Executes any tasks that were queued while offline and then performs a sync.
|
|
242
|
+
*
|
|
243
|
+
* Each step is isolated so a failure in one does not prevent the other, and
|
|
244
|
+
* neither can escape to the callers (init + the connection.changed handler).
|
|
245
|
+
* This guarantees the subsequent invokeSyncStatusListeners(true) always runs,
|
|
246
|
+
* so syncStatus recovers to true and gated channel queries are unblocked.
|
|
247
|
+
* Failed syncs degrade to "possibly stale data until the next query" rather
|
|
248
|
+
* than freezing all future queries. See issue #1816.
|
|
201
249
|
*/
|
|
202
250
|
private syncAndExecutePendingTasks = async () => {
|
|
203
|
-
|
|
204
|
-
|
|
251
|
+
try {
|
|
252
|
+
await this.offlineDb.executePendingTasks();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.log('Error executing pending tasks during sync.', error);
|
|
255
|
+
}
|
|
256
|
+
// Note: sync() has its own try/catch, but its catch block calls resetDB(),
|
|
257
|
+
// which can itself throw on a corrupted DB and re-reject. This outer guard
|
|
258
|
+
// absorbs that case.
|
|
259
|
+
try {
|
|
260
|
+
await this.sync();
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.log('Error while syncing the DB.', error);
|
|
263
|
+
}
|
|
205
264
|
};
|
|
206
265
|
}
|