feat: thread paginator and manager revamp - #1888
Conversation
|
Open question with some context: Initially, Shall we maybe take a similar approach to |
| @@ -0,0 +1,154 @@ | |||
| # stream-chat-react: moving to the new thread manager and `ThreadPaginator` | |||
There was a problem hiding this comment.
i'll update this with any further changes to the PR and we can remove it once it's no longer needed (purely here for stream-chat-react migration guidelines)
…ginator # Conflicts: # src/pagination/paginators/BasePaginator.ts
## [10.0.0-rc.16](v10.0.0-rc.15...v10.0.0-rc.16) (2026-09-30) ### ⚠ BREAKING CHANGES * thread paginator and manager revamp (#1888) ### Bug Fixes * channel paginator merging bugs ([#1894](#1894)) ([dbbbd75](dbbbd75)), closes [#1865](#1865) ### Features * thread paginator and manager revamp ([#1888](#1888)) ([d1562aa](d1562aa))
|
🎉 This PR is included in version 10.0.0-rc.16 🎉 The release is available on: Your semantic-release bot 📦🚀 |
CLA
Linear ticket: https://linear.app/stream/issue/REACT-1187/integrate-entitystore-and-paginators-to-thread-manager
Description of the changes, What, Why and How?
client.threadsnow has one store of liveThreadinstances, and the thread list is a normal paginator on top of it, the same setup as channel lists.threadsByIdwas built from the thread list, so it only knew about listed threads. Anything that resolves "the live thread for this id" missed a thread that was open but not listed: hard deletes, offline replay, connection recovery, the composer's submit target. UI SDKs worked around it by pushing opened threads into the list.Changes:
threadStore(EntityStore<Thread>) is the only store. It holds every live thread and counts who holds it. There are two holders:ThreadPaginatorbuilds aStoreBackedItemIndexover the store;REGISTERED_HOLDER: an opened thread (ensure()or its firstactivate()).One instance per id, always.
client.threads.paginator(ThreadPaginator):reload()swaps the list in place and is also the first load. A failed reload keeps the list, same asmaster.client.threads.ensure({ channel, parentMessage })is how UI SDKs open a thread. It returns the stored thread or builds one, and registers it either way, so a list query landing beforeactivate()can't create a duplicate. A thread it builds starts stale and loads once, on first open.Opened threads stay registered for the session.
deactivate()only flipsactive, like channels. A recovery pass marks inactive registered threads stale, and they reload on their nextactivate().Teardown follows the channel. There's one local
pendingDisposallistener per channel. When the channel is disposed (deleted, user removed, disconnect), both its listed and opened threads are released. No extra event handlers.Thread.hydrateState: withreconcileit merges, asreload()does. Without it, as on the list path, it replaces the replies, as onmaster. This fixes stale and deleted replies coming back after a reconnect.EntityStoregetsisHeldBy(id, holder)andvalues().Composer, message operations, offline queue and recovery now resolve threads through
client.threads.get(id).Also fixed here:
localMessageToNewMessagePayload, so it sent server-owned fields such astype. It now usestoUpdatedMessagePayload. Regression from refactor: unify message operation apis #1882.keepPreviousItemsrefresh kept a stalehasMoreHead: true, so a channel that moved to the top went into a hidden interval.postQueryReconcilenow anchors a first page'shasMoreHeadfrom the start offset.Changelog
client.threads.threadsByIdremoved. Useclient.threads.get(id)to resolve a thread andclient.threads.paginator.getItem(id)for list membership.client.threads.paginator(ThreadPaginator).state.threads,state.paginationandstate.readyare removed; readpaginator.stateinstead.client.threads.loadNextPage()andclient.threads.queryThreads()removed; usepaginator.toTail()andclient.queryThreadsAndHydrate().client.threads.ensure({ channel, parentMessage })for opening a thread with one instance per id.