fix: store predefined filters in db - #3804
Merged
Merged
Conversation
2 tasks
isekovanic
added a commit
to GetStream/stream-chat-js
that referenced
this pull request
Sep 15, 2026
## CLA - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required). - [ ] Code changes are tested ## Description of the changes, What, Why and How? LLC side PR for [this](GetStream/stream-chat-react-native#3804). ## Changelog -
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.
🎯 Goal
A channel list using
predefined_filtersorted and filtered by the wrong rule on cold start, untilthe first query came back.
For a predefined-filter list the server decides the filter and the sort, and the client only finds out
from
QueryChannelsResponse.predefined_filter. The offline cache seeds the list before any queryruns, so until then
effectiveFiltersfalls back to the local filters ({}in SampleApp, whichmatches anything) and
effectiveSortfalls back toDEFAULT_BACKEND_SORT, which has nopinned_atterm.
During that window a channel that receives a replayed message can be promoted above a pinned channel,
and a channel the predefined filter excludes can be ingested into the list. A queued pending-task
replay keeps
syncStatusfalse for seconds, which is exactly when live ingests arrive.develophas the same problem by another route:channel_manager.tsreadspinnedAtSortfrompagination.responseSort ?? pagination.sort, andresponseSortis undefined before the first query.So this isn't a V10 regression.
The cache already stored the cid order a query produced. Now it stores the rule that produced it, and
restores that rule before the seeded window is used.
🛠 Implementation details
This is the RN half. It needs the
stream-chat-jscounterpart, see Dependencies below.channelQueriesgets apredefinedFilter TEXTcolumn andSqliteClient.dbVersiongoes from 17 to18. There are no incremental migrations, so existing installs take a full DB reinit on first launch.
That's a one-time cache loss, not data loss.
upsertCidsForQuerytakespredefinedFilterand writes either the JSON or an explicitnull. Itcan't write
undefined, becauseupsertStatementPartsfilters undefined columns out and the old rulewould then survive the order it described. There's a test for that.
queries/selectChannelIdsForFilterSort.tsis nowselectChannelQueryForFilterSort.tsand returns{ cids, predefinedFilter }. It's internal (apis/queries/*isn't re-exported), so the rename isn'ta public break.
The read goes through an
isPredefinedFilterResponseguard and atry/catch, so a half-written orstale-shaped cell gives back
undefinedinstead of a malformed rule for the whole list to sort by.getChannelsForFilterSortreturnsDBGetChannelsForQueryResult | nullso the channels and the rulecome back in one read.
OfflineDB.getChannelsForQueryforwards it.Breaking change
OfflineDBApi.getChannelsForQueryreturns an object instead of an array.Before:
Promise<Omit<ChannelStateResponseFields, 'duration'>[] | null>After:
Promise<DBGetChannelsForQueryResult | null>, i.e.{ channels, predefinedFilter? }This affects custom
AbstractOfflineDBimplementations and callers ofOfflineStoreApis.getChannelsForFilterSort. Read.channels, and passpredefinedFilterthroughupsertCidsForQueryand back out ofgetChannelsForQuery.Also in here
store/apis/deleteReactions.tshad aconsole.logthat shipped in release builds. It now usesSqliteClient.logger?.('info', ...)like the rest of that folder.Dependencies
Needs two
stream-chat-jscommits fromfix/predefined-filters-in-db:98ca3cba, the LLC side of this change0ccf86d9, the empty-sort fix that makes[]resolve toDEFAULT_BACKEND_SORTUntil an rc with both is published, CI fails on two things: a typecheck error in
getChannelsForFilterSort.tsfor the missingDBGetChannelsForQueryResultexport, andChannelList › should float the channel to the top on a new message with the default sort.package/package.jsonis on^10.0.0-rc.10, and that range accepts10.0.0-rc.11, so there'snothing to edit once the rc lands.
That
ChannelListtest is rewritten here. It used to assert that the default sort leaves the orderalone, which was the empty-sort bug written down as a contract. With
[]resolving to the backenddefault, a
message.newmoves its channel to the top on recency, so the test is renamed andre-asserted.
🎨 UI Changes
Nothing visual. There's no useful before/after either: the wrong ordering only shows during the
pre-sync window, and on a normal connection that closes before you can capture it.
🧪 Testing
Unit tests in
channelQueryCids.test.ts: the rule round-trips with the cid order it produced,re-caching the same query without a rule clears the stored one, and a stored rule that's unparseable
or the wrong shape is ignored while the cids still come back.
The tests for the restore itself are in the LLC PR. Three of them fail if you disable the restore,
which I checked by disabling it.
On device (Android, SampleApp,
basic_channel_list_filter, with temporary logging since removed):user_version = 17came up at 18 with the new column and no leftover rows{type: messaging, archived: false, members: {$in: [<user>]}}rather than the app's{}, and sortpinned_at desc, last_message_at desc, updated_at desceffectiveSortwas[pinned_at, last_message_at, updated_at]andeffectiveFilterswas[archived, members, type]drained and the pinned channel stayed first, with the replayed channel second
I couldn't reproduce the original symptom on device. The replay drains too fast, so the end state
above is also the post-query state and doesn't prove much by itself. The seed-time check and the LLC
tests are what actually cover it. Seeing it visually would need far more queued sends, or a throttled
connection.
☑️ Checklist
V10branch, notdevelopai-docs/ai-migration-v9-to-v10.md