Conversation
…pa-clawbot#2095) loadChannels() replaced the channels array with the server snapshot and carried nothing across, and mergeUserChannels() only ran from init(). So a region-filter change or the show-encrypted toggle destroyed every field that exists only in the tab: the My Channels section, the user's own labels, and all unread badges. If the selected channel was a PSK row, reconcileSelectionAfterChannelRefresh() could not find its user:* hash in the snapshot and nulled the selection, emptied messages and rewrote the URL, closing the open conversation. No race needed. mergeClientChannelState() mirrors mergeWsAppendedIntoRest(), which already does this job for messages (Kpa-clawbot#1498). It enriches only rows the snapshot already contains: carrying a missing row over would resurrect channels the region filter just excluded, which is worse than the bug being fixed. A row genuinely dropped by the in-flight race reappears on that channel's next packet. mergeUserChannels() now runs inside loadChannels(), before the render and before the reconcile, so every call site gets it rather than init() alone. Tests drive the real loadChannels() through its existing test hook with a stubbed api(), so they prove the helper is wired in and not merely present. Verified by mutation: with the wiring removed the three reproduction tests fail with exactly the reported symptoms (My Channels reduced to the server rows, unread undefined, selectedHash null), while the region-filter test stays green. Not done: no browser validation. The change is frontend JS covered by unit tests that call the production function; I did not run Playwright against a server. init() still calls mergeUserChannels() and renderChannelList() after loadChannels() resolves, which is now redundant. Left alone deliberately: it is idempotent, and the comment there records a regression it was added to fix.
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.
Fixes #2095.
loadChannels()replaced thechannelsarray with the server snapshot and carried nothing across, andmergeUserChannels()only ever ran frominit(). So any refresh destroyed every field that exists only in the tab.No race needed for the worst of it. Change the region filter or toggle show-encrypted with a PSK channel open, and:
renderChannelList()derives it aschannels.filter(c => c.userAdded === true), and the server returns neitheruserAddednor auser:-prefixed hashreconcileSelectionAfterChannelRefresh()cannot find theuser:*hash in the snapshot, so it nullsselectedHash, setsmessages = [], rewrites the URL to#/channelsand replaces the open conversation with "Choose a channel from the sidebar"What this does
mergeClientChannelState(fresh, prev)mirrorsmergeWsAppendedIntoRest(), which already does exactly this job formessages(#1498). Same shape: pure, takes both arrays as parameters, returns a fresh array, never aliases or mutates an input.It carries
unread,userAddedanduserLabelby hash, and keepslastActivityMs/lastSender/lastMessage/messageCountwhen a WebSocket batch landed while the request was in flight and is therefore newer than the snapshot. Those four move together: a sender without its message reads as a different message.mergeUserChannels()now runs insideloadChannels(), before the render and before the reconcile, so all three call sites get it instead ofinit()alone.What it deliberately does not do
It does not resurrect a channel the snapshot left out. A region-filter change legitimately narrows the list, so carrying survivors over would defeat the filter, which is a worse bug than the one being fixed. The helper only enriches rows already present in the fresh snapshot.
That leaves half of finding 2 in the issue unfixed: a channel pushed by the WebSocket handler during the initial in-flight window is still dropped. That one self-heals on the channel's next packet, and the reverted preview line is overwritten by the next WS batch. Fixing it properly needs a way to tell "dropped because the snapshot is stale" from "dropped because the filter excludes it", which is a larger change than this.
Tests
tests/unit/test-issue-2095-channels-client-state.js, 11 cases, registered intest-all.sh.Part 1 exercises the helper directly. Part 2 drives the real
loadChannels()through the existing_channelsLoadChannelsForTesthook with a stubbedapi(), which is what proves the helper is wired in rather than merely defined.Verified by mutation. With the wiring removed from
loadChannels()but the helper left in place, the three reproduction cases fail with exactly the reported symptoms:The fourth case, "a refresh still drops a channel the server filtered out", stays green throughout, so the fix cannot be defeating the region filter.
Full frontend suite green:
sh test-all.shexits 0.Not done
No browser validation. This is frontend JS covered by unit tests that call the production function through its own hook, but I did not run Playwright against a server, so I am not claiming a browser check I did not do.
init()still callsmergeUserChannels()andrenderChannelList()afterloadChannels()resolves, which is now redundant and costs one extra full sidebar render per page load. Left alone deliberately: it is idempotent, and the comment there records the regression it was added to fix (test-channel-issue-1111-e2e.js, case 2). Worth removing separately with that e2e test watched.