Skip to content

fix: store predefined filters in db - #3804

Merged
isekovanic merged 1 commit into
V10from
fix/store-predefined-filters-in-db
Sep 16, 2026
Merged

isekovanic merged 1 commit into
V10from
fix/store-predefined-filters-in-db

Conversation

@isekovanic

Copy link
Copy Markdown
Contributor

🎯 Goal

A channel list using predefined_filter sorted and filtered by the wrong rule on cold start, until
the 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 query
runs, so until then effectiveFilters falls back to the local filters ({} in SampleApp, which
matches anything) and effectiveSort falls back to DEFAULT_BACKEND_SORT, which has no pinned_at
term.

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 syncStatus false for seconds, which is exactly when live ingests arrive.

develop has the same problem by another route: channel_manager.ts reads pinnedAtSort from
pagination.responseSort ?? pagination.sort, and responseSort is 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-js counterpart, see Dependencies below.

channelQueries gets a predefinedFilter TEXT column and SqliteClient.dbVersion goes from 17 to
18. 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.

upsertCidsForQuery takes predefinedFilter and writes either the JSON or an explicit null. It
can't write undefined, because upsertStatementParts filters undefined columns out and the old rule
would then survive the order it described. There's a test for that.

queries/selectChannelIdsForFilterSort.ts is now selectChannelQueryForFilterSort.ts and returns
{ cids, predefinedFilter }. It's internal (apis/queries/* isn't re-exported), so the rename isn't
a public break.

The read goes through an isPredefinedFilterResponse guard and a try/catch, so a half-written or
stale-shaped cell gives back undefined instead of a malformed rule for the whole list to sort by.

getChannelsForFilterSort returns DBGetChannelsForQueryResult | null so the channels and the rule
come back in one read. OfflineDB.getChannelsForQuery forwards it.

Breaking change

OfflineDBApi.getChannelsForQuery returns an object instead of an array.

Before: Promise<Omit<ChannelStateResponseFields, 'duration'>[] | null>
After: Promise<DBGetChannelsForQueryResult | null>, i.e. { channels, predefinedFilter? }

This affects custom AbstractOfflineDB implementations and callers of
OfflineStoreApis.getChannelsForFilterSort. Read .channels, and pass predefinedFilter through
upsertCidsForQuery and back out of getChannelsForQuery.

Also in here

store/apis/deleteReactions.ts had a console.log that shipped in release builds. It now uses
SqliteClient.logger?.('info', ...) like the rest of that folder.

Dependencies

Needs two stream-chat-js commits from fix/predefined-filters-in-db:

  • 98ca3cba, the LLC side of this change
  • 0ccf86d9, the empty-sort fix that makes [] resolve to DEFAULT_BACKEND_SORT

Until an rc with both is published, CI fails on two things: a typecheck error in
getChannelsForFilterSort.ts for the missing DBGetChannelsForQueryResult export, and
ChannelList › should float the channel to the top on a new message with the default sort.
package/package.json is on ^10.0.0-rc.10, and that range accepts 10.0.0-rc.11, so there's
nothing to edit once the rc lands.

That ChannelList test is rewritten here. It used to assert that the default sort leaves the order
alone, which was the empty-sort bug written down as a contract. With [] resolving to the backend
default, a message.new moves its channel to the top on recency, so the test is renamed and
re-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):

  • an install at user_version = 17 came up at 18 with the new column and no leftover rows
  • the column held what the server resolved: filter
    {type: messaging, archived: false, members: {$in: [<user>]}} rather than the app's {}, and sort
    pinned_at desc, last_message_at desc, updated_at desc
  • at seed time, before hydration, effectiveSort was [pinned_at, last_message_at, updated_at] and
    effectiveFilters was [archived, members, type]
  • online load, airplane mode, 3 queued sends in an unpinned channel, force stop, reconnect: the tasks
    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

  • I have signed the Stream CLA (required)
  • PR targets the V10 branch, not develop
  • Documentation is updated. BC72 is logged but not yet in ai-docs/ai-migration-v9-to-v10.md
  • New code is tested in main example apps, including all possible scenarios
    • SampleApp Android, debug and release
    • SampleApp iOS, not run
    • Expo iOS and Android, not run

@isekovanic
isekovanic requested a review from szuperaz September 14, 2026 22:59
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

-
@isekovanic
isekovanic merged commit cce7b31 into V10 Sep 16, 2026
2 of 4 checks passed
@isekovanic
isekovanic deleted the fix/store-predefined-filters-in-db branch September 16, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant