test(bindx): pin the store's silent-write notification gaps - #80
Closed
matej21 wants to merge 1 commit into
Closed
Conversation
Four SnapshotStore write paths were suspected of mutating state without notifying subscribers. Investigating each settles them: - createEntity - CONFIRMED, user-visible. roots.register() runs after setEntityData and setExistsOnServer have already notified, and it is the write that makes the entity count as a create. A save indicator built on store.subscribe + getAllDirtyEntities().length renders 0 while the store holds a dirty create. - clearAllServerErrors - CONFIRMED. Its sibling clearAllErrors notifies for the same write. Both in-tree callers happen to self-heal through ordering, so this reaches users through the exported action only. - sweepUnreachableCreated - NOT a bug. Its only mutation is removeEntity, which notifies. - unregisterRootEntity - NOT a bug. Both callers sweep on the next line, and the sweep notifies. A fourth suspect, unregisterParentChild, no longer exists: ae80e75 removed it. docs/issues/032-memory-leaks.md still prescribes wiring it. The two confirmed gaps are pinned with test.failing rather than left as red reproducers, so they can live on main. Bun reports such a test as passing while the bug is present, and fails the run the moment the behaviour is fixed, forcing the marker off. The characterization tests around them record why the other two suspects are non-issues. No fix is included - this commit establishes the evidence.
This was referenced Aug 20, 2026
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.
Investigation only — no fix is included. Four
SnapshotStorewrite paths were suspected of mutating state without notifying subscribers. This settles all four, two of them as non-issues.createEntityclearAllServerErrorssweepUnreachableCreatedunregisterRootEntityunregisterParentChildcreateEntity— the real oneThe root registration is the write that makes the entity count as a
create— the comment directly above it says so. Both notifications therefore carry the pre-registration value.A save indicator built on
store.subscribe+getAllDirtyEntities().length— the shapeusePersistuses — renders0after mounting a plain<Entity create>, whilestore.getAllDirtyEntities()returns[{ changeType: 'create' }]. A barestore.notify()flips the DOM to1, confirming it is purely a missing notification.This method was originally written off as internal bookkeeping. It turned out to be the actual bug of the batch.
clearAllServerErrorsIts sibling
clearAllErrorsnotifies for the identical write; this one does not, andstore.getVersion()does not move either, so no consumer — entity, relation or global — can observe the clear.Impact is narrower than it first looks, and the tests record why: both in-tree callers self-heal by accident of ordering.
BatchPersisterdispatches a notifyingsetPersisting(true)immediately before the clear, andmapServerErrorsfollows every clear with a notifyingaddFieldError. So this reaches users through the exported action only.The two non-issues
sweepUnreachableCreatedhas nonotifyof its own, but its only mutation isremoveEntity, whose last statement notifies entity subscribers, live parents and global subscribers. Sweeping nothing changes nothing.unregisterRootEntityis silent, but both callers (Entity.tsx:206-207,useEntityList.ts:303-308) sweep on the very next line, and the set of entities whose observable state un-rooting changes is exactly what the sweep removes and notifies for. Un-rooting a persisted entity is inert anyway —ReachabilityAnalyzer.walk()seeds everyexistsOnServerentity as a root regardless ofRootRegistry.Recording these matters as much as the confirmations: both were on the suspect list, and neither needs a change.
Why
test.failinginstead of red reproducersThe two confirmed gaps are pinned with
test.failing, which gives the xfail contract in both directions on bun 1.3.14:mainthis test is marked as failing but it passed. Remove .failing if tested behavior now worksThat matters because #77 widens CI from an enumerated directory list to everything outside
tests/browser. A permanently-red file would not be mergeable under that gate; a pin is, and it turns red by itself the day someone fixes the underlying bug.Assertions are byte-identical to the versions that failed — only
test(→test.failing(and the titles changed.Gates
bun test tests/unit/store/190 pass / 0 fail ·bun test tests/react/storeNotifications/4 pass / 0 fail · typecheck clean.Follow-ups this turned up
SnapshotStore.createEntity— announce the root registration (notify after it, or register beforesetExistsOnServerso the last notification carries the finished value). This is a real user-facing bug and is worth its own PR.SnapshotStore.clearAllServerErrors— one line, mirroringclearAllErrors.registerParentChildhas the same silent-tail-write shape (roots.unregisterwith no notify, and connect/add paths notify before calling it). Worth an audit.docs/issues/032-memory-leaks.mdstill prescribes wiringunregisterParentChild, which no longer exists.useEntityErrorsis not re-exported frombindx-react's root, so apps cannot import it, and itsgetSnapshotallocates a fresh object per call — the React 18 "getSnapshot should be cached" hazard.