feat(core): dynamic search debounce based on query length - #2873
feat(core): dynamic search debounce based on query length#2873xsahil03x wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughAdded adaptive debounced search and ChangesAdaptive search and stale-load handling
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant SearchInput
participant StreamUserListController
participant SearchDebounceMixin
participant StreamChatClient
SearchInput->>StreamUserListController: search(query, filter)
StreamUserListController->>SearchDebounceMixin: schedule adaptive debounce
SearchDebounceMixin->>StreamUserListController: start current load generation
StreamUserListController->>StreamChatClient: request filtered users
StreamChatClient-->>StreamUserListController: return results or error
StreamUserListController-->>SearchInput: apply only non-stale state
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
c289e72 to
ced5307
Compare
Add query-length-aware debounced search() to StreamMessageSearchListController, StreamUserListController, and StreamMemberListController. Short (<=2 char), low-selectivity queries wait 500ms before hitting the backend; longer queries use the standard 300ms. Results from superseded queries are dropped so a slower, older query cannot overwrite a newer one. Adds cancelSearch() to cancel a pending search when the input is cleared. Migrates the sample app's four hand-rolled 350ms Timer search sites to search(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ced5307 to
4b44e5e
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/stream_chat_flutter_core/lib/src/search_debounce_mixin.dart`:
- Around line 76-80: Add a Dart doc comment directly above the dispose override
in the debounce mixin, documenting that dispose cancels pending debounced
searches before completing disposal. Keep the existing _searchDebouncer.cancel()
and super.dispose() behavior unchanged.
- Around line 45-55: Update clearResults() to describe invalidating pending and
in-flight searches rather than claiming it cancels them. Keep
_searchDebouncer.cancel() for pending work and retain the _loadGeneration
increment so responses from already-issued requests are discarded.
- Line 35: Update debouncedSearch to increment _loadGeneration before invoking
_searchDebouncer, invalidating any in-flight request as soon as a new query is
scheduled. Add a regression test covering an older request completing during the
newer query’s debounce interval and verify its results are not applied.
In `@packages/stream_chat_flutter_core/pubspec.yaml`:
- Line 33: Remove the direct rate_limiter version constraint from the package
dependency list, and add or update its workspace-managed dependency entry in
melos.yaml. Then run melos bootstrap to synchronize the package manifest without
manually restoring the constraint in pubspec.yaml.
In `@sample_app/lib/widgets/channel_list.dart`:
- Around line 37-41: Add a dispose override to the State class containing
_channelQueryListener and call _messageSearchListController.dispose() before
disposing the remaining controllers, preserving the existing disposal order for
those controllers.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2f280d84-1574-42cc-8ca9-d0a42e891840
📒 Files selected for processing (15)
packages/stream_chat_flutter_core/CHANGELOG.mdpackages/stream_chat_flutter_core/lib/src/search_debounce_mixin.dartpackages/stream_chat_flutter_core/lib/src/search_debouncer.dartpackages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dartpackages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dartpackages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dartpackages/stream_chat_flutter_core/pubspec.yamlpackages/stream_chat_flutter_core/test/search_debouncer_test.dartpackages/stream_chat_flutter_core/test/stream_member_list_controller_test.dartpackages/stream_chat_flutter_core/test/stream_message_search_list_controller_test.dartpackages/stream_chat_flutter_core/test/stream_user_list_controller_test.dartsample_app/lib/pages/new_chat_screen.dartsample_app/lib/pages/new_group_chat_screen.dartsample_app/lib/widgets/add_members_sheet.dartsample_app/lib/widgets/channel_list.dart
|
|
||
| /// Schedules a debounced reload whose delay adapts to [queryLength]. | ||
| @internal | ||
| void debouncedSearch(int queryLength) => _searchDebouncer(queryLength); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Invalidate active loads when scheduling a new search.
debouncedSearch does not change _loadGeneration. If request A is in flight and query B is scheduled, request A can complete during B's 300 ms or 500 ms delay. Its isStale(generation) check then passes and applies results for the superseded query.
Increment the generation before scheduling the debounce. Add a regression test where an old request completes while a newer query is pending.
Proposed fix
- void debouncedSearch(int queryLength) => _searchDebouncer(queryLength);
+ void debouncedSearch(int queryLength) {
+ _loadGeneration += 1;
+ _searchDebouncer(queryLength);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| void debouncedSearch(int queryLength) => _searchDebouncer(queryLength); | |
| void debouncedSearch(int queryLength) { | |
| _loadGeneration += 1; | |
| _searchDebouncer(queryLength); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/stream_chat_flutter_core/lib/src/search_debounce_mixin.dart` at line
35, Update debouncedSearch to increment _loadGeneration before invoking
_searchDebouncer, invalidating any in-flight request as soon as a new query is
scheduled. Add a regression test covering an older request completing during the
newer query’s debounce interval and verify its results are not applied.
| /// Cancels any pending or in-flight search and clears the current results. | ||
| /// | ||
| /// Consider calling this when the search input is emptied, so results for an | ||
| /// abandoned query are neither shown nor repopulated by a late response. | ||
| void clearResults() { | ||
| _searchDebouncer.cancel(); | ||
| // Bump the generation so an already-running load is treated as superseded | ||
| // and cannot repopulate the cleared results when its response arrives. | ||
| _loadGeneration += 1; | ||
| value = PagedValue<Key, Value>.loading(); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Describe invalidation instead of in-flight cancellation.
_searchDebouncer.cancel() only cancels a pending timer. It does not cancel an already-issued controller request. The generation increment discards its response.
Proposed fix
- /// Cancels any pending or in-flight search and clears the current results.
+ /// Cancels any pending search, invalidates in-flight loads, and clears the
+ /// current results.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// Cancels any pending or in-flight search and clears the current results. | |
| /// | |
| /// Consider calling this when the search input is emptied, so results for an | |
| /// abandoned query are neither shown nor repopulated by a late response. | |
| void clearResults() { | |
| _searchDebouncer.cancel(); | |
| // Bump the generation so an already-running load is treated as superseded | |
| // and cannot repopulate the cleared results when its response arrives. | |
| _loadGeneration += 1; | |
| value = PagedValue<Key, Value>.loading(); | |
| } | |
| /// Cancels any pending search, invalidates in-flight loads, and clears the | |
| /// current results. | |
| /// | |
| /// Consider calling this when the search input is emptied, so results for an | |
| /// abandoned query are neither shown nor repopulated by a late response. | |
| void clearResults() { | |
| _searchDebouncer.cancel(); | |
| // Bump the generation so an already-running load is treated as superseded | |
| // and cannot repopulate the cleared results when its response arrives. | |
| _loadGeneration += 1; | |
| value = PagedValue<Key, Value>.loading(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/stream_chat_flutter_core/lib/src/search_debounce_mixin.dart` around
lines 45 - 55, Update clearResults() to describe invalidating pending and
in-flight searches rather than claiming it cancels them. Keep
_searchDebouncer.cancel() for pending work and retain the _loadGeneration
increment so responses from already-issued requests are discarded.
| @override | ||
| void dispose() { | ||
| _searchDebouncer.cancel(); | ||
| super.dispose(); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a doc comment for dispose.
dispose is a public override. Document that it cancels pending debounced searches before disposal.
As per coding guidelines, “All public APIs must have doc comments.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/stream_chat_flutter_core/lib/src/search_debounce_mixin.dart` around
lines 76 - 80, Add a Dart doc comment directly above the dispose override in the
debounce mixin, documenting that dispose cancels pending debounced searches
before completing disposal. Keep the existing _searchDebouncer.cancel() and
super.dispose() behavior unchanged.
Source: Coding guidelines
| freezed_annotation: ^3.0.0 | ||
| meta: ^1.9.1 | ||
| package_info_plus: ">=9.0.1 <11.0.0" | ||
| rate_limiter: ^1.0.0 |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Manage rate_limiter through melos.yaml.
Remove this direct constraint. Add or update the workspace dependency in melos.yaml, then run melos bootstrap.
As per coding guidelines, “Do not edit version constraints directly in individual pubspec.yaml files; update melos.yaml instead and run melos bootstrap.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/stream_chat_flutter_core/pubspec.yaml` at line 33, Remove the direct
rate_limiter version constraint from the package dependency list, and add or
update its workspace-managed dependency entry in melos.yaml. Then run melos
bootstrap to synchronize the package manifest without manually restoring the
constraint in pubspec.yaml.
Source: Coding guidelines
| void _channelQueryListener() { | ||
| if (_debounce?.isActive ?? false) _debounce!.cancel(); | ||
| _debounce = Timer(const Duration(milliseconds: 350), () { | ||
| if (mounted) { | ||
| _messageSearchListController.searchQuery = _controller.text; | ||
| setState(() { | ||
| _isSearchActive = _controller.text.isNotEmpty; | ||
| }); | ||
| if (_isSearchActive) _messageSearchListController.doInitialLoad(); | ||
| } | ||
| }); | ||
| final query = _controller.text; | ||
| setState(() => _isSearchActive = query.isNotEmpty); | ||
| if (query.isEmpty) return _messageSearchListController.clearResults(); | ||
| return _messageSearchListController.search(query); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate file =="
git ls-files | rg '(^|/)channel_list\.dart$' || true
echo "== File excerpt =="
file=$(git ls-files | rg '(^|/)channel_list\.dart$' | head -n1)
if [ -n "${file:-}" ]; then
wc -l "$file"
sed -n '1,120p' "$file" | cat -n
fi
echo "== Search for _messageSearchListController declarations/usages =="
rg -n "_messageSearchListController|class .*MessageSearchList|dispose\(\)" sample_app lib . --glob '*.dart' | head -n 200 || trueRepository: GetStream/stream-chat-flutter
Length of output: 1947
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Repository availability =="
pwd
git status --short || true
echo "== Locate file =="
git ls-files | rg '(^|/)channel_list\.dart$' || true
echo "== File excerpt =="
file=$(git ls-files | rg '(^|/)channel_list\.dart$' | head -n1)
if [ -n "${file:-}" ]; then
wc -l "$file"
sed -n '1,120p' "$file" | cat -n
fi
echo "== Search for _messageSearchListController declarations/usages =="
rg -n "_messageSearchListController|class .*MessageSearchList|dispose\(\)" sample_app lib . --glob '*.dart' | head -n 200 || trueRepository: GetStream/stream-chat-flutter
Length of output: 1947
🏁 Script executed:
#!/bin/bash
set -u
echo "== Repository availability =="
pwd
git status --short 2>/dev/null || true
echo "== Locate file =="
git ls-files 2>/dev/null | rg '(^|/)channel_list\.dart$' || true
echo "== File excerpt =="
file=$(git ls-files 2>/dev/null | rg '(^|/)channel_list\.dart$' | head -n1 || true)
if [ -n "${file:-}" ] && [ -f "$file" ]; then
wc -l "$file"
sed -n '1,140p' "$file" | cat -n
fi
echo "== Search for _messageSearchListController declarations/usages =="
rg -n "_messageSearchListController|MessageSearchList|dispose\(\)" . 2>/dev/null --glob '*.dart' | head -n 200 || trueRepository: GetStream/stream-chat-flutter
Length of output: 1947
🏁 Script executed:
#!/bin/bash
set -u
echo "== Repository availability =="
echo "PWD: $PWD"
git status --short >/tmp/ws_git_status.txt 2>&1 || true
cat /tmp/ws_git_status.txt || true
echo "== Git files availability =="
git ls-files >/tmp/ws_ls_files.txt 2>&1 || true
cat /tmp/ws_ls_files.txt | rg '(^|/)channel_list\.dart$' || true
echo "== List matching path =="
test -f sample_app/lib/widgets/channel_list.dart && echo "exists" || echo "missing"
test -f sample_app/lib/widgets/channel_list.dart && wc -n sample_app/lib/widgets/channel_list.dart || trueRepository: GetStream/stream-chat-flutter
Length of output: 1947
🌐 Web query:
GetStream stream-chat-flutter sample_app lib widgets channel_list.dart _messageSearchListController dispose
💡 Result:
In the Stream Chat Flutter SDK, the StreamMessageSearchListController is a controller class used to manage cross-channel message search operations, including pagination, filtering, and state management [1][2]. When using this controller within a StatefulWidget, it is essential to properly manage its lifecycle to avoid memory leaks [1][2][3]. The dispose method must be called when the controller is no longer needed [1][2]. The implementation pattern for disposing of the StreamMessageSearchListController is as follows: @override void dispose { messageSearchListController.dispose; super.dispose; } This ensures that any resources, such as stream subscriptions or internal state, used by the controller are correctly discarded [4]. While sample applications (such as the one found in the Stream Chat Flutter repository) often utilize StreamChannelListController for managing channel lists, the requirement to explicitly dispose of controllers remains a best practice across the SDK for any controller you instantiate within your widget's state [5][1][2].
Citations:
- 1: https://getstream.io/chat/docs/sdk/flutter/stream-chat-flutter-core/stream-message-search-list-controller/
- 2: https://getstream.io/chat/docs/sdk/flutter/v8/stream-chat-flutter-core/stream-message-search-list-controller/
- 3: https://getstream.io/chat/docs/sdk/flutter/v7/stream_chat_flutter_core/stream_message_search_list_controller/
- 4: https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/StreamMessageSearchListController-class.html
- 5: StreamChannelHeader causing error "You must have a StreamChat widget at the top of your widget tree" #1923
🌐 Web query:
site:github.com/GetStream/stream-chat-flutter sample_app lib widgets channel_list.dart StreamMessageSearchListController
💡 Result:
StreamMessageSearchListController is a controller class provided by the stream_chat_flutter_core package, which is part of the official GetStream Flutter SDK [1]. It is designed to handle the business logic for searching messages within the Stream Chat infrastructure, allowing developers to build custom search interfaces without interacting with low-level API code [1]. Key aspects of StreamMessageSearchListController include: Role and Usage: The controller is intended to be used in conjunction with the SDK's UI components or for building custom UI widgets [1]. It manages the state and logic for message search queries, including filters, sorting, and pagination [2][3]. Implementation Details: Developers typically instantiate this controller by providing a StreamChatClient and relevant search filters (e.g., filtering by channel, text content, or pinned status) [2][4]. While it simplifies building search features, developers should be aware that the underlying search functionality performs full-text search, not fuzzy search, by design [3]. Context in Sample Apps: While specific file paths like channel_list.dart can vary by implementation, the controller is central to features that require listing and searching message content [1][5]. In common patterns, it is paired with view components such as StreamMessageSearchListView to display the resulting data [6][7]. Technical Considerations: Users of this controller have reported nuances regarding pagination, sorting, and filter application in complex or high-volume datasets [2][7]. When implementing search features, developers are encouraged to review the official Stream Chat Flutter documentation for the latest best practices on query parameters and pagination configuration [7][3].
Citations:
- 1: https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_flutter_core
- 2: Missing Pinned Message when using Search Pinned Message api #2090
- 3: Message text partial search #1794
- 4: Disabled channels are not removed from persistence client #2036
- 5: https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_flutter_core/example/lib/main.dart
- 6: Stream StreamMessageSearchListController issue for search #1503
- 7: Searching channels doesn't return pagination data and does not sort properly #1563
🌐 Web query:
github.com GetStream stream-chat-flutter package stream_chat_flutter_core StreamMessageSearchListController dispose
💡 Result:
In the Stream Chat Flutter SDK, the StreamMessageSearchListController provides a dispose method designed to discard resources held by the controller when it is no longer needed [1][2]. To manage the lifecycle of the StreamMessageSearchListController correctly, you should call its dispose method within the dispose override of your State class. This prevents memory leaks by cleaning up the controller and its associated resources [3][4][5]. Recommended implementation pattern: @override void dispose { // Replace 'messageSearchListController' with your specific instance name messageSearchListController.dispose; super.dispose; } It is also standard practice to call doInitialLoad during the initState of your State class to begin fetching initial search data [3][4][6]. The controller is a subclass of PagedValueNotifier, which allows you to listen to state changes regarding the list of searched messages [5][6].
Citations:
- 1: https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/StreamMessageSearchListController-class.html
- 2: https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamMessageSearchListController-class.html
- 3: https://getstream.io/chat/docs/sdk/flutter/stream-chat-flutter-core/stream-message-search-list-controller/
- 4: https://getstream.io/chat/docs/sdk/flutter/v8/stream-chat-flutter-core/stream-message-search-list-controller/
- 5: https://getstream.io/chat/docs/sdk/flutter/v7/stream_chat_flutter_core/stream_message_search_list_controller/
- 6: https://getstream.io/chat/docs/sdk/flutter/v5/stream-chat-flutter-core/stream-message-search-list-controller/
🌐 Web query:
github.com GetStream/stream-chat-flutter/blob/master/sample_app/lib/widgets/channel_list.dart 37-41 _messageSearchListController
💡 Result:
The StreamMessageSearchListController in the GetStream stream-chat-flutter SDK is a controller class used to manage search queries, pagination, and result states for cross-channel message searching [1]. It is designed to be used in conjunction with the StreamMessageSearchListView widget to render search results, though it can also be used independently to build custom search UIs [1]. Key characteristics and usage requirements for StreamMessageSearchListController include: Controller Initialization: You must initialize the controller with a StreamChatClient instance and optionally a filter (typically to scope the search to channels the current user is a member of) [2][1]. Search Configuration: The controller requires exactly one of either searchQuery (a string for full-text search) or messageFilter (a Filter object for structured searching); providing both or neither will result in an assertion error [2]. Lifecycle Management: As with other controllers in the SDK, you must call doInitialLoad to trigger the first data fetch and ensure that dispose is called when the controller is no longer needed to prevent memory leaks [2][1]. Underlying Architecture: It functions as a PagedValueNotifier, which allows you to track changes in the search results and build reactive UIs using components like PagedValueListenableBuilder [1]. In the context of the GetStream sample application, this controller is utilized to enable the message search functionality across channels, distinguishing it from components like StreamChannelListController, which manage lists of channels rather than searching for specific message content [3][1].
Citations:
- 1: https://getstream.io/chat/docs/sdk/flutter/stream-chat-flutter-core/stream-message-search-list-controller/
- 2: https://github.com/GetStream/agent-skills/blob/fe714918218a72a80508a2c9aa5fa35b80fb4df9/skills/stream-flutter/references/CHAT-FLUTTER.md
- 3: https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_flutter_core
Dispose _messageSearchListController.
_messageSearchListController is created in State, so dispose() should call _messageSearchListController.dispose() before the remaining controller disposal order. This prevents leaking search-controller resources when the widget is removed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sample_app/lib/widgets/channel_list.dart` around lines 37 - 41, Add a dispose
override to the State class containing _channelQueryListener and call
_messageSearchListController.dispose() before disposing the remaining
controllers, preserving the existing disposal order for those controllers.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2873 +/- ##
==========================================
+ Coverage 73.18% 73.41% +0.23%
==========================================
Files 429 431 +2
Lines 27724 27791 +67
==========================================
+ Hits 20289 20403 +114
+ Misses 7435 7388 -47 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🎯 Goal
Short search queries (1–2 characters) are low-selectivity and expensive on the backend, causing high latency and timeouts on the
search/queryChannels/queryMembersendpoints. This adapts the debounce interval to the query length: short queries wait longer before hitting the backend; longer, more selective queries use the standard delay.Linear: FLU-684 · cross-SDK initiative, iOS reference: GetStream/stream-chat-swift#4198.
📝 What changed
stream_chat_flutter_core: added asearch()method toStreamMessageSearchListController,StreamUserListController, andStreamMemberListController.cancelSearch()cancels a pending search when the input is cleared.rate_limiter'sdebounce(two instances picked by length); no hand-rolledTimer.sample_app: migrated the four hand-rolled 350msTimersearch sites (channel list, add-members, new chat, new group) tosearch().✅ Non-breaking
Purely additive — new methods only. No existing signatures, defaults, or exports changed; no LLC changes.
doInitialLoadgains a "newest-wins" guard (fixes an overlapping-load race; single-load behavior is unchanged).🚫 Out of scope (follow-ups)
StreamChannelListController's filter is constructor-only; not wired as text search in Flutter).🧪 Testing
New:
search_debouncer_test.dart+ controller tests for message/user/member search (debounce timing, length policy, superseded-result guard,cancelSearch). Fullstream_chat_flutter_coresuite: 345 passing. Analyze clean, formatted.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes