feat(search): adapt debounce to query length and cancel superseded requests - #1823
Merged
Merged
Conversation
…quests Short queries (1-2 chars) have low selectivity, so the search and QueryChannels endpoints are slow or time out on them. Every search source debounced at a fixed 300ms, and nothing was ever cancelled: an already-dispatched request ran to completion and wrote its result into state, while a new search string typed during an in-flight request was silently dropped by canExecuteQuery. Debounce now depends on query length and is resolved per keystroke, so a query moves between buckets as it grows or shrinks. Defaults: 500ms at or below shortQueryMaxLength (2), 300ms above it. An explicit debounceMs still applies to both buckets so existing configurations keep their interval. A new query aborts the in-flight request before dispatching, and the aborted query's response is discarded rather than clobbering newer state. cancelScheduledQuery() and resetState() abort too. The signal reaches axios via a new optional ApiRequestOptions on search, queryUsers, queryChannels, searchRoles, searchUserGroups and channel.queryMembers, plus an optional config argument on client.get/post. canExecuteQuery is relaxed so a new search query preempts an in-flight one; pagination still waits. Downstream SDKs that relied on the drop-while-loading behaviour will see the change. All additions are optional parameters or widened optional fields, so this is a minor release.
oliverlaz
requested review from
MartinCupela,
arnautov-anton,
isekovanic,
santhoshvai,
szuperaz and
vishalnarkhede
as code owners
August 13, 2026 13:25
… request config Addresses review feedback. The name implied the options were tied to the Chat API, when they are neither API-specific nor even request-specific: a custom search source may resolve from local data and ignore the signal entirely. AbortOptions describes what the bag actually carries and matches the existing UploadRequestOptions precedent for not prefixing transport concerns with "Api". Parameters renamed to abortOptions. client.get/post took a full AxiosRequestConfig, which let a caller pass config.params. _enrichAxiosOptions spreads config after the enriched params object, so that replaced it wholesale and dropped api_key, user_id and connection_id, breaking authentication. Both now take Omit<AxiosRequestConfig, 'params'>.
…uestOptions Follow-up to review feedback on the naming. One shared type could not serve both roles honestly. A search source's query() may resolve from local data and never issue a request, so naming its options after a request was wrong. That bag is now SearchQueryOptions, declared in the search domain, and its doc comment says outright that ignoring the signal is valid. The client and channel methods do perform HTTP requests, so RequestOptions fits there. It cannot share the SearchQueryOptions name anyway: client.search() already takes a SearchOptions, and two near-identical names on one signature would be worse than the problem being fixed. Parameters are queryOptions and requestOptions. Sources forward the former straight into the latter where they do issue requests.
Member
Author
|
Naming and the
|
szuperaz
approved these changes
Aug 13, 2026
szuperaz
left a comment
Contributor
There was a problem hiding this comment.
Didn't review the code as I've seen there are comments already, but tested with RN and was wokring fine for me.
Rename the execute-result generic to R. Drop the debounce field initializers. The constructor always calls setDebounceOptions, and resolveDebounceOptions already applies the defaults, so every instance was writing those three fields twice. The resolution itself has to stay in setDebounceOptions rather than move into the constructor, since it is part of the SearchSource and SearchSourceSync interfaces and must re-resolve at runtime. Give the initialState getter an explicit SearchSourceState<T> return type. It was inferring items as undefined rather than T[] | undefined, so it only satisfied ISearchSource by assignability and a missing field would have surfaced somewhere downstream instead of here. Inline the debounce default values into resolveDebounceOptions.
… search source filters, sort and searchOptions could only be set by assigning to the instance after construction, which is why the documented constructor form did not compile. Each source now takes them in its options argument as well, following the MentionsSearchSourceOptions pattern already in the repo: the source-specific keys are peeled off and the rest forwarded to super. - ChannelSearchSourceOptions, UserSearchSourceOptions and ChannelMemberSearchSourceOptions add filters, sort and searchOptions - MessageSearchSourceOptions adds the six messageSearch*/channelQuery* parameters - MentionsSearchSourceOptions adds userFilters, userSort, memberFilters, memberSort and searchOptions Additive only. The properties stay public and mutable, since changing them at runtime is what FilterBuilder.updateContext and reactive filter UIs rely on, and widening a parameter type keeps existing callers and subclass constructors compiling. Minor release.
MartinCupela
approved these changes
Aug 14, 2026
…nt overrides Addresses review feedback. MentionsSearchSourceOptions gained the query parameters in the previous commit, which also widened the public `config` property because it is declared with that same type. The constructor only ever writes four keys and the query path reads the instance fields, so `config.userFilters = x` typechecked and was silently ignored. config is now a Pick of the four keys that are actually read. Note this narrows `config` from 13 readable keys to 4, so reads of `config.pageSize`, `config.debounceMs`, `config.transliterate` and the rest stop compiling. All of them were already undefined at runtime, since the constructor never populated them, so this turns a silently-wrong read into a compile error rather than changing behaviour. ChannelMemberSearchSource and MentionsSearchSource both overrode canExecuteQuery with what reduced, after the canDispatchQuery extraction, to the base rule minus its allowEmptySearchString term. Both overrides are gone and the flag is pinned in the constructor instead, which is exactly equivalent: the overrides ignored the flag, and spreading it after restOptions keeps a caller from turning it off. Also aligns two JSDoc lines that still described the parameter as "the api request options", the wording the RequestOptions rename removed.
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 14, 2026
## [9.51.0](v9.50.3...v9.51.0) (2026-08-14) ### Features * **search:** adapt debounce to query length and cancel superseded requests ([#1823](#1823)) ([1ca8bf7](1ca8bf7)) ### Chores * **deps:** bump axios, form-data and dev dependencies ([#1825](#1825)) ([940f2bc](940f2bc)) ### Performance Improvements * introduce event sync limit for offline db ([#1826](#1826)) ([46cb7df](46cb7df))
|
🎉 This PR is included in version 9.51.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CLA
Description of the changes, What, Why and How?
Short search queries (1-2 characters) have low selectivity, so the backend
searchandQueryChannelsendpoints are slow or time out on them. Two things made that worse than it needed to be:debounce()was built once in the constructor with a fixed 300ms, so a 1-character query fired just as eagerly as a precise one.cancelScheduledQuery()only cancelled a scheduled call. An already-dispatched request ran to completion and wrote its result into state via afinallyblock. Worse,canExecuteQueryhad a hard!this.isLoadingguard, so a new search string typed while a request was in flight was silently dropped and the UI kept showing stale results.Dynamic debounce
debounce()insrc/utils.tsnow accepts a function for its timeout, resolved on every call right beforesetTimeout. That is the whole switching mechanism: each keystroke clears the pending timer and reschedules using the delay computed from that call's arguments.shortQueryDebounceMsshortQueryMaxLengthcharacterslongQueryDebounceMsshortQueryMaxLengthTyping
a->ab->abcschedules at 500ms, 500ms, then 300ms, and only the last runs. An explicitdebounceMsapplies to both buckets, so anyone who already tuned it (saydebounceMs: 800) keeps exactly their interval rather than being sped up to 500ms on short queries.Request cancellation
Each source holds one
AbortController, aborted at the top of every dispatch. The state write stays infinallybut is skipped when the signal is aborted, so a late response cannot clobber the newer query.cancelScheduledQuery()andresetState()abort as well.I did not use
withCancellationfromsrc/utils/concurrency.ts: it serializes same-tag callbacks, which would make the new query queue behind the aborted one. A per-instanceAbortControlleris a few lines and has no such coupling.The signal reaches axios through a new optional trailing
ApiRequestOptionsonsearch,queryUsers,queryChannels(via the non-serializedChannelStateOptions),searchRoles,searchUserGroupsandchannel.queryMembers, plus an optionalconfigargument onclient.get/post, which were previously dropping any axios config.axios.isCancelerrors are no longer logged or counted toward the token-refresh backoff.Applies to
MessageSearchSource,ChannelSearchSource,UserSearchSource,ChannelMemberSearchSourceandMentionsSearchSourceall inherit this from the two base classes.$autocomplete/$qfilter generation is unchanged.CommandSearchSourceresolves locally, so it gets the dynamic debounce but no cancellation.Short queries are not blocked, only debounced harder.
Behaviour changes worth calling out for downstream SDKs
canExecuteQuerysemantics changed: a new search query now preempts an in-flight request instead of being dropped. Pagination still waits. This is a public method thatstream-chat-reactand custom sources both call and override.query()implementations with their owntry/catchwill start seeingCanceledError.All additions are optional parameters or widened optional fields, so this is a minor release, not a breaking one.
Verification
yarn lint,yarn types,yarn test(3064 passing, 77 files) andyarn buildare clean. 22 new tests cover the threshold switch in both directions, the config resolution rules including legacydebounceMs, stale-response discard,isLoadingrelease on cancel, and the signal reaching axios.Changelog
shortQueryDebounceMs,longQueryDebounceMsandshortQueryMaxLengthsearch,queryUsers,queryChannels,searchRoles,searchUserGroupsandchannel.queryMembersaccept an abort signal