refactor: replace legacy ChannelState message/thread/pinned storage with paginators#1806
Merged
isekovanic merged 26 commits intoJul 22, 2026
Conversation
Checkpoint before deleting legacy ChannelState message-list + threads storage. Covers Tasks 8-10 and Task 11 steps 1-2; Task 11 step 3+ follows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…annelstate-removal-wip
Complete the test surgery for the channel_state main message-list removal (caba066): delete message-set machinery specs (pruning, merges, latestMessages, messagePagination, main-list addMessagesSorted/findMessage/reactions), and re-scope the surviving deleteUser/ updateUser and #1736 self-quote coverage to threads + pinned. Migrate channel/client event and query/queryChannels tests to assert against channel.messagePaginator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the remaining ChannelState message-storage removal into 3a (main message list, threads retained — DONE, both suites green) and 3b (threads + full method delete — next). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…state to Thread Delete the ChannelState thread shadow and its thread-only methods (removeMessage, findMessage, _updateQuotedMessageReferences, _addReactionToState/_removeReactionFromState); scope _updateMessage, updateUserMessages, deleteUserMessages and addReaction/removeReaction to the pinned-message cache. addMessagesSorted stays as a slim channel-meta path (last_message_at plus the user-reference map). Thread reply state is now owned entirely by the Thread object: reply own_reactions are preserved on message.updated and applied on reaction events via reflectReaction, and a new user.messages.deleted/user.deleted subscription applies bans via applyMessageDeletionForUser. channel.ts no longer handles thread-reply events and never reaches into a Thread instance (getReplies, _extendEventWithOwnReactions and the reaction/delete handlers are main-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
channel.state.threads removed, thread-only methods deleted, reply own_reactions and user-deletion re-homed onto the Thread object; both suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fault and search util Resolve the ChannelState.formatMessage type anchor to LocalMessage and remove the leftover message-set machinery now that the channel message list lives in the paginators: the MessageSet object type (public), the internal DEFAULT_MESSAGE_SET_PAGINATION constant, and the now-unused binarySearchByDateEqualOrNearestGreater util (public). MessageSetType stays as the type of channel.query's messageSetToAddToIfDoesNotExist param. BREAKING CHANGE: ChannelState no longer stores the channel message list or thread replies. channel.state.messages / latestMessages / messageSets, the MessageSet type, and binarySearchByDateEqualOrNearestGreater are removed. Read messages from channel.messagePaginator and thread replies from the Thread object. addMessageSorted/addMessagesSorted no longer maintain a message list (they record channel-meta only: last_message_at and the user-reference map). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the types/exports cleanup (MessageSet, DEFAULT_MESSAGE_SET_PAGINATION, binarySearchByDateEqualOrNearestGreater removed; MessageSetType/isLatestMessageSet kept) and add Task 19 for a reactive headItems property on PaginatorState at the BasePaginator level. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…inator headItems is the newest-loaded window (T[] | undefined, undefined until first load), materialized on every state emission via a StateStore preprocessor so UI can subscribe to latest-item / latest-messages changes. Interval mode derives it from latestItems (the head-most loaded interval, so it tracks the newest window even when the active window is a jumped-to older interval); flat mode uses the emitted items. Reference-stable via per-index identity so a headItems selector does not re-fire on unrelated state changes. latestItems/latestItem getters are unchanged and stay consistent with the field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…up note Task 19 (reactive headItems) done. Add Task 20 (unify ChannelPaginator/ReminderPaginator/ UserGroupPaginator on itemIndex + interval storage) and note that Task 18 unblocks removing the utils.addToMessageList helper (its last caller is ChannelState._addToMessageList). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…agePaginator Split the message-interval mechanics (interval store, ingestItem, reflect*, jump navigation, truncate, focus signals, query/filters/sort/cursor scaffolding) into a new MessageIntervalPaginator base. MessagePaginator now extends it and adds only the unread/live-view concern (unreadStateSnapshot, liveViewState, seed/set/clearUnreadSnapshot, isViewingLive/setViewingLive, jumpToTheFirstUnreadMessage, and the postQueryReconcile/clearStateAndCache overrides). Pure refactor: MessagePaginator's public surface is unchanged (shared types re-exported), so no consumer changes. This gives PinnedMessagePaginator an unread-free base to extend by construction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New PinnedMessagePaginator extends the unread-free MessageIntervalPaginator base, so it never
carries the read/unread surface (pinned messages are a subset of the channel and must not
participate in read/delivery tracking). It fetches from the /pinned_messages endpoint via
config.doRequest, orders by pinned_at ascending, filters on { cid, pinned: true } (so ingestItem
auto-adds on pin and auto-removes on unpin), and includes only pinned, non-shadowed messages on the
query path. jumpToMessage/reflect*/truncate/applyMessageDeletionForUser are inherited.
Not yet wired into Channel — purely additive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…parity) Create channel.pinnedMessagesPaginator, seed it from state.pinned_messages in _initializeState, and feed it alongside the legacy channel.state.pinnedMessages at every mutation site: message.new/ message.updated (ingestItem auto-adds on pin / auto-removes on unpin), message.deleted, channel. truncated, channel.hidden clear_history, the reaction events (reflectReaction), and the channel- and client-level user.messages.deleted/user.deleted (applyMessageDeletionForUser). Parity-only: both stores run in parallel; nothing reads the paginator yet. Readers migrate and the legacy store is removed in the next step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed-only methods The pinned-message list is now owned by channel.pinnedMessagesPaginator. Delete channel.state .pinnedMessages plus addPinnedMessage(s)/removePinnedMessage and the now-orphaned pinned-only methods (addReaction/removeReaction, _updateMessage, updateUserMessages, deleteUserMessages, _addToMessageList, removeMessageFromArray, clearMessages), and the orphaned utils.addToMessageList / utils.deleteUserMessages helpers. ChannelState now holds no message/thread/pinned storage. Per-user pinned updates re-home to pinnedMessagesPaginator.reflectUserUpdate; user deletions to applyMessageDeletionForUser (channel + client level). channel.ts/client.ts drop the legacy calls; the paginator feeds added earlier are the sole handling. Tests: reorganize channel.ts _handleChannelEvent tests into one describe per WS event, each asserting the effect on messagePaginator, pinnedMessagesPaginator and channel state (test bodies moved verbatim, no assertion lost). Add parity coverage for the pinned paginator on channel WS events (truncate/delete/user-deletion soft+hard/reaction) and client-level cross-channel user deletion (cid-guard, soft, hard, main + pinned) and user.updated propagation. BREAKING CHANGE: channel.state.pinnedMessages and ChannelState.addPinnedMessage(s)/ removePinnedMessage/addReaction/removeReaction/updateUserMessages/deleteUserMessages are removed. Read pinned messages from channel.pinnedMessagesPaginator. utils.addToMessageList and utils.deleteUserMessages are removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
channel.state.pinnedMessages removed; PinnedMessagePaginator (unread-free MessageIntervalPaginator subclass) is the source of truth, fed by channel/client WS-event handlers. React reads it via usePinnedMessagesCount. Both suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…3 audit) The test-types message reply generator read channel.state.messages (removed) for the thread parent — use response.message.id directly (the reply's parent_id). A channel last_message_at test passed a stale 'new' positional arg (old message-set type) to addMessagesSorted — removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 13 (test close-out audit) complete — only two stale channel.state references remained, now fixed; parity suites confirmed. Correct Task 11/12 statuses to committed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Date and _trackLatestMessage Continue the ChannelState message-storage removal: the message paginator is the single source of truth for the channel's latest-message metadata, and several legacy ChannelState surfaces are gone. - MessageIntervalPaginator now tracks the latest message (latestMessageId + a latestMessage getter), advanced monotonically on ingest and mirrored into paginator state via a preprocessor. All message paginators auto-track on ingest, so it is robust to the active window and to interval/item sort orientation (the reply list backing ThreadListItemUI included). - channel.state.last_message_at is now a read-only getter derived from messagePaginator.latestMessage; the writable setter and its backing field are removed. - Remove channel.state.isUpToDate / setIsUpToDate. Not disrupting a scrolled-away view on a live message is handled structurally by the paginator's interval routing, not a flag. - Remove Channel._trackLatestMessage. user.updated / user.deleted propagation now scans active channels (reflectUserUpdate / applyMessageDeletionForUser already self-filter by author id); a targeted user-reference index is planned in specs/user-reference-index. - Remove ChannelState.addMessagesSorted / addMessageSorted and their last remaining callers. - Offline pending-message replay tracks the latest via messagePaginator.trackLatestMessage. - Docs: docs/breaking-changes-v14-v15.md. BREAKING CHANGE: channel.state.last_message_at is read-only (derived from the message paginator). channel.state.isUpToDate / setIsUpToDate are removed (use messagePaginator.isActiveIntervalAtHead, hasMoreHead, jumpToTheLatestMessage). ChannelState.addMessagesSorted / addMessageSorted are removed. See docs/breaking-changes-v14-v15.md for migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Channel.on / StreamChat.on JSDoc examples logged the removed channel.state.messages; use channel.messagePaginator.state.items instead. - CLAUDE.md: note that messages / thread replies / pinned messages live in the paginators (channel.messagePaginator / thread.messagePaginator / channel.pinnedMessagesPaginator) as the single source of truth, not channel.state; how to read/mutate; the read-only derived last_message_at; pointer to docs/breaking-changes-v14-v15.md. - Mark Task 14 done (JS) in the remove-legacy-channelstate-messages spec. The React CLAUDE.md stale guidance remains and is tracked separately on the React branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MartinCupela
requested review from
arnautov-anton,
isekovanic,
oliverlaz,
santhoshvai,
szuperaz and
vishalnarkhede
as code owners
July 21, 2026 13:08
Contributor
|
Size Change: -10.9 kB (-2.11%) Total Size: 505 kB 📦 View Changed
|
isekovanic
reviewed
Jul 21, 2026
MartinCupela
commented
Jul 21, 2026
isekovanic
reviewed
Jul 21, 2026
…interval storage and replace channel.state.last_message_at with messagePaginator.aggregateState
isekovanic
approved these changes
Jul 22, 2026
isekovanic
left a comment
Contributor
There was a problem hiding this comment.
Thanks @MartinCupela !
isekovanic
merged commit Jul 22, 2026
3d2a56a
into
feat/message-paginator-master-merge
4 checks passed
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.
Summary
Completes the
message-paginatorinitiative on the LLC side: the channel's messages, thread replies, and pinned messages are no longer stored onchannel.state. Each list now lives in a paginator that is the single source of truth (interval storage + a canonicalItemIndex):channel.messagePaginatorthread.messagePaginator(owned by theThreadobject)channel.pinnedMessagesPaginator(newPinnedMessagePaginator)What changed
ChannelStatestorage removed:messages,latestMessages,messageSets,messagePagination,threads,pinnedMessages, and their mutators (addMessageSorted/addMessagesSorted,removeMessage,findMessage,findMessageByTimestamp,filterErrorMessages,loadMessageIntoState,clearMessages,initMessages,pruneOldest,addReaction/removeReaction,updateUserMessages,deleteUserMessages,addPinnedMessages/addPinnedMessage/removePinnedMessage,removeQuotedMessageReferences, + internal helpers).PinnedMessagePaginatoradded and populated from channel events; pin/unpin falls out ofingestItem+ a{ pinned: true }filter (no bespoke branching).MessageIntervalPaginatorextracted as the unread-free base ofMessagePaginator; it tracks the latest message (latestMessageId+latestMessage), advanced on ingest and mirrored into reactive state.channel.state.last_message_atis now a read-only getter derived frommessagePaginator.latestMessage(setter + backing field removed).channel.state.isUpToDate/setIsUpToDateremoved — live-message routing (don't disrupt a scrolled-away view) is handled structurally by the paginator's interval model.Channel._trackLatestMessageremoved.user.updated/user.deletedpropagation now scans active channels (reflectUserUpdate/applyMessageDeletionForUserself-filter by author id). A targeted user-reference index is planned (specs/user-reference-index).utilscleanup: removedaddToMessageList,messageSetPagination,binarySearchByDateEqualOrNearestGreater,deleteUserMessages, and theMessageSet/ message-set pagination types.headItemsadded toPaginatorState(newest-loaded window).MessageReplyPaginator(dead sibling; the thread reply list usesMessagePaginator).Breaking changes
Full list + before → after migration table in
docs/breaking-changes-v14-v15.md. Highlights:channel.state.messageschannel.messagePaginator.state.items/.itemschannel.state.threads[parentId]thread.messagePaginator.state.itemschannel.state.pinnedMessageschannel.pinnedMessagesPaginator.state.itemschannel.state.addMessageSorted(m)channel.messagePaginator.ingestItem(m)channel.state.removeMessage({ id })channel.messagePaginator.removeItem({ id })channel.state.findMessage(id)channel.messagePaginator.getItem(id)channel.state.isUpToDate/setIsUpToDatemessagePaginator.isActiveIntervalAtHead/hasMoreHead/jumpToTheLatestMessage()channel.state.last_message_at = …Behavioral note:
last_message_atnow advances on every incoming message regardless of the viewer's scroll position (the oldisUpToDatesuppression is gone) — it's a channel-level fact.Follow-ups (not in this PR)
specs/user-reference-index— replace the active-channel scan onuser.updated/user.deletedwith auserId → message-referenceindex (design-gated).