CODAP-1117/1178: fix MobX constraint violations and stale computeds in DataSet#2401
CODAP-1117/1178: fix MobX constraint violations and stale computeds in DataSet#2401
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2401 +/- ##
========================================
Coverage 85.96% 85.96%
========================================
Files 768 768
Lines 42884 42899 +15
Branches 10262 10675 +413
========================================
+ Hits 36865 36878 +13
+ Misses 6007 6006 -1
- Partials 12 15 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
69dc918 to
8f98f5c
Compare
Replace 3 observable boxes (_validationCount, _isValidCases, _isValidItemIds) with plain variables and a single _caseValidationVersion observable counter, following the same pattern used in the Collection fix. Move invalidateCases() and invalidateItemIds() from views to actions, wrap selection cleanup in runInAction(), and fix use-rows.ts reaction to maintain MobX dependency tracking when isValidCases is no longer observable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The isValidCases view getter read a plain variable (_isValidCases) with no observable dependency, so MobX cached it permanently when observed by a reaction. invalidateCases() set the plain variable to false, but MobX never re-evaluated the computed — validateCases() saw cached true and short-circuited, leaving stale case groups after hierarchical changes. Fix: read _caseValidationVersion in isValidCases (gives MobX a trackable dependency) and bump it in invalidateCases() (signals staleness). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
validateCases() was writing to self.selection (an observable set) via runInAction from a view — the same anti-pattern this branch eliminates. Defer the selection deletes to a plain array and flush them in the existing runInAction in setValidCases(), consolidating all observable writes from views into a single location. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Verify validation reaction fires after invalidateCases + validateCases (reproduces the stale-computed bug when a MobX reaction observes isValidCases) - Verify reaction fires end-to-end after moveAttributeToNewCollection - Verify removed items are deselected via deferred selection cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… implicit validation - Collection: verify MobX reactions fire on caseGroups/cases when the version counter bumps after invalidation + rebuild - DataSet: verify prepareSnapshot forces case validation when invalidated - DataSet: verify getCasesForCollection implicitly validates after sequential moveAttributeToNewCollection calls Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1483a07 to
f8301cf
Compare
codap-v3
|
||||||||||||||||||||||||||||
| Project |
codap-v3
|
| Branch Review |
main
|
| Run status |
|
| Run duration | 02m 08s |
| Commit |
|
| Committer | github-actions[bot] |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
4
|
| View all changes introduced in this branch ↗︎ | |
There was a problem hiding this comment.
Pull request overview
This PR updates DataSet case/item-id validation caching to avoid MobX constraint violations by replacing multiple observable cache guards with plain variables and a single observable “version” counter, aligning with the recently introduced Collection._cacheVersion pattern.
Changes:
- Replace
observable.boxcache guards inDataSet.extend()with plain variables +_caseValidationVersionobservable counter. - Move
invalidateCases()/invalidateItemIds()to actions and adjust validation/selection cleanup behavior. - Update case-table
useRowsreaction logic and add regression tests around MobX reaction behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| v3/src/models/data/data-set.ts | Reworks validation cache/invalidation logic; adds version counter and deferred selection cleanup. |
| v3/src/models/data/data-set-collections.test.ts | Adds tests asserting MobX reactions fire correctly across invalidation/validation and hierarchy changes. |
| v3/src/models/data/collection.test.ts | Adds reaction regression test for the collection version-counter approach. |
| v3/src/components/case-table/use-rows.ts | Updates reaction expression to maintain dependency tracking with new non-observable guards. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
invalidateItemIds() (the public action) did not bump _caseValidationVersion, so the isValidItemIds computed would return a stale cached value if called standalone (not via invalidateCases). Flagged by Copilot review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The items/itemIds view getters call _validateItemIds(), which is a no-op when the cache is valid. After an append operation, MobX re-evaluates the computed during the no-op, tracks zero dependencies, and permanently seals the cached value. Subsequent changes to self._itemIds (delete, insert) are invisible — the computed returns a stale array reference. Fix: read _caseValidationVersion.get() in the getters to maintain a stable MobX dependency that survives the cache-valid no-op. Same pattern used for isValidCases, isValidItemIds, and validationCount. Fixes the Sampler plugin bug where Clear Data left orphaned items and new samples were grouped under previous parents instead of creating new groups. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors DataSet cache/validation tracking to avoid MobX “observable writes during computed/view evaluation” and fixes stale computed behavior that could permanently seal cached results (notably impacting the Sampler plugin). It also updates a useRows reaction pattern to preserve MobX dependency tracking, and adds Jest tests to cover the new reactivity guarantees.
Changes:
- Replaces multiple
observable.boxcache guards inDataSet.extend()with plain variables + a single observable version counter used to keep computeds/reactors reactive. - Moves invalidation helpers into actions and defers selection cleanup so observable writes are consolidated into a single
runInActionflush after validation. - Updates
use-rows.tsreaction derivation and adds new Jest tests for DataSet/Collection MobX reactivity and the Sampler oscillation scenario.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
v3/src/models/data/data-set.ts |
Refactors case/item-id validation caches and invalidation, introduces _caseValidationVersion, and defers selection cleanup to a controlled flush. |
v3/src/components/case-table/use-rows.ts |
Ensures the validation reaction always reads validationCount first to maintain MobX dependencies. |
v3/src/models/data/data-set-collections.test.ts |
Adds tests for invalidate/validate reactivity, selection cleanup deferral, snapshot validation behavior, and the Sampler oscillation regression. |
v3/src/models/data/collection.test.ts |
Adds a regression test ensuring reactions fire via Collection’s cache version counter. |
Comments suppressed due to low confidence (1)
v3/src/models/data/data-set.ts:328
appendItemIdsToCache()mutates_cachedItemIds/_cachedItemsin place. SinceitemIds/itemsare MobX computed getters returning these arrays by reference, observers (e.g. Reactobserverrenders orreaction(() => data.items.length)) may not be notified on subsequent appends—especially after the first cache-valid re-evaluation drops the_itemIdsdependency and the computed becomes effectively tied only to_caseValidationVersion(which isn’t bumped here). Consider (1) updating the cache via replacement (new array references, like Collection’s append path) and (2) bumping an observable version counter when the cache is updated so appends reliably trigger observers.
appendItemIdsToCache(itemIds: string[]) {
if (_isValidItemIds) {
const visibleIds = itemIds.filter(itemId =>
!self.setAsideItemIdsSet.has(itemId) && !self.filteredOutItemIds.has(itemId)
)
_cachedItemIds.push(...visibleIds)
_cachedItems.push(...visibleIds.map(id => ({ __id__: id })))
// Invalidate the hash caches so they recompute from the updated _cachedItemIds.
// The invalidation is observable, so MobX reactions tracking the hashes will fire.
_itemIdsHash.invalidate()
_itemIdsOrderedHash.invalidate()
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Fixes MobX constraint violations and stale computed bugs in the DataSet
.extend()block, completing the work started in PRs #2393 (cachedFnWithArgsFactory) and #2398 (Collection.completeCaseGroups). Also fixes the Sampler plugin stale cache bug (CODAP-1178).CODAP-1117: MobX constraint violations
observable.boxvariables (_validationCount,_isValidCases,_isValidItemIds) with plain variables + a single_caseValidationVersionobservable counter, following the same pattern as the Collection_cacheVersionfixinvalidateCases()andinvalidateItemIds()from views to actionsisValidCases,isValidItemIds, andvalidationCountgetters now read_caseValidationVersion.get()to establish an observable dependency, andinvalidateCases()/invalidateItemIds()bump the countervalidateCases()tosetValidCases(), consolidating all observable writes from views into a singlerunInActionuse-rows.tsreaction to always evaluatevalidationCountfirst so MobX dependency tracking is maintainedCODAP-1178: Sampler plugin stale cache bug
items/itemIdscomputeds caused by MobX dependency oscillation: after an append operation,_validateItemIds()is a no-op (cache valid), so MobX drops all tracked dependencies and permanently seals the cached array reference. Subsequent deletes/inserts are invisible._caseValidationVersion.get()in theitemsanditemIdsgetters -- same one-line pattern used for all other getters in the.extend()blockRoot cause details
CODAP-1117 (stale
isValidCases): TheisValidCasesview getter read a plainletvariable with no observable dependency. When observed by a reaction, MobX cached it permanently.invalidateCases()set the variable tofalse, but MobX never re-evaluated the computed, sovalidateCases()short-circuited and hierarchical table operations returned stale data.CODAP-1178 (stale
items/itemIds): Theitems/itemIdsgetters call_validateItemIds(), which readsself._itemIdswhen the cache is invalid but is a no-op when valid. After an append, MobX re-evaluates during the no-op, tracks zero dependencies, and seals the computed. This was a heisenbug -- debug logging accidentally readself._itemIds.lengthoutsideuntracked(), creating a dependency that prevented the oscillation.Fixes CODAP-1117 and CODAP-1178.
Test plan
invalidateCases+validateCasesmoveAttributeToNewCollectionsetValidCasesprepareSnapshotforces validation when invalidatedgetCasesForCollectionitemsgetter survives dependency oscillation (CODAP-1178)🤖 Generated with Claude Code