fix(llc)!: prevent external mutation of ClientState collections#2686
fix(llc)!: prevent external mutation of ClientState collections#2686xsahil03x wants to merge 1 commit into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
c7fbb3d to
44adb3b
Compare
3da78f3 to
d03f858
Compare
`ClientState`'s `channels`, `users`, and `activeLiveLocations` getters returned live internal references, so callers could mutate the cache and silently bypass `channelsStream` / `usersStream` / `activeLiveLocationsStream`. Wrap them in unmodifiable maps/lists at write time so both the synchronous getter and the stream emit immutable values without per-read allocation. Initial `BehaviorSubject` seeds are unmodifiable too — subscribers connecting before any write still get the same contract. `removeChannel` now early-returns when the channel isn't cached and emits a freshly-built map so `.distinct()` subscribers observe the change. `dispose()` closes the channels controller first, then iterates remaining channels and disposes them — `Channel.dispose()`'s `removeChannel` call no-ops on the closed controller, so teardown produces zero subscriber emissions instead of N progressively-shrinking maps. All controller writes use `safeAdd` (no-ops on close). Marks the cache-mutation surface as `@internal`: - Setters: `channels=`, `currentUser=`, `activeLiveLocations=`, `totalUnreadCount=`, `blockedUserIds=` - Methods: `updateUsers`, `updateUser`, `addChannels`, `removeChannel` App flows (`connectUser`, `client.queryChannels`, etc.) populate the cache through these methods and are unaffected. Tests cover: read-side unmodifiability, initial-seed unmodifiability, stream-side unmodifiability, and the fresh-map emission from `removeChannel`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d03f858 to
c4d2860
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## v10.0.0 #2686 +/- ##
==========================================
Coverage ? 66.98%
==========================================
Files ? 407
Lines ? 24364
Branches ? 0
==========================================
Hits ? 16321
Misses ? 8043
Partials ? 0 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Closes FLU-500
Summary
The collection getters on
ClientState—channels,users,activeLiveLocations— returned live internal references. Callers could mutate the cache directly and silently bypasschannelsStream/usersStream/activeLiveLocationsStream. Wrap them in unmodifiable maps/lists at write time so both the synchronous getter and the stream emit immutable values, and lock down the cache-mutation surface with@internal.What changed
Read-side
state.channels,state.users,state.activeLiveLocationsnow return unmodifiable maps/lists. Calling.clear(),.remove(...),[k] = v, etc. throwsUnsupportedError.channelsStream/usersStream/activeLiveLocationsStreamalso receive unmodifiable values.BehaviorSubjectseeds are unmodifiable too — subscribers connecting before any write still see immutable values.Write-side hardening
BehaviorSubject.add(...)calls inClientStateswitched tosafeAdd(...)(the existing close-safe extension). Emissions after close are no-ops instead of throwing.removeChannelearly-returns when the channel isn't cached and emits a freshly-built map, so.distinct()subscribers correctly observe the change. Previouslychannels = channels..remove(cid)re-emitted the same reference and missed the change for.distinct().dispose()closes_channelsControllerbefore iterating the cached channels and disposing them. EachChannel.dispose()callsremoveChannel(cid)whichsafeAdd-no-ops on the closed controller, so teardown produces zero subscriber emissions (vs. N progressively-shrinking maps previously).@internalauditThe cache-mutation API surface on
ClientStateis now marked@internal. App flows (connectUser,client.queryChannels,client.startLiveLocationSharing, etc.) populate the cache through these methods and are unaffected.channels=,currentUser=,activeLiveLocations=,totalUnreadCount=,blockedUserIds=updateUsers,updateUser,addChannels,removeChannelChannelClientStateis left out of this PR — the same pattern can be applied later when threads/typing events become a priority.Tests
8 tests under
ClientState mutation guards:state.channelsreturns an unmodifiable viewstate.usersreturns an unmodifiable viewstate.activeLiveLocationsreturns an unmodifiable viewremoveChannelemits a fresh map so.distinct()subscribers see the changechannelsStreamemits unmodifiable mapsusersStreamemits unmodifiable mapsactiveLiveLocationsStreamemits unmodifiable listsFull LLC suite passes (1389/1389) on the rebased branch.
Breaking
fix(llc)!— code that was mutating these collections directly (state.channels.remove(...),state.users.clear(), etc.) will now throwUnsupportedErrorat runtime. Migration: go through the existingaddChannels/removeChannel/updateUser/updateUsersmethods (now@internal, but callable by SDK code; apps populate the cache via API methods that hit these internally).🤖 Generated with Claude Code