Context
Since #85, a write to an entity notifies the entity's subscribers and then walks its live relation edges upward, bumping every ancestor (SubscriptionManager.notifyEntityAndParentSubscribers). This is what makes $connect(id) re-point sibling subscriptions and lets a parent re-render when a nested entity changes.
The walk is a transitive closure over all live edges, with no knowledge of which fields each subscriber actually reads.
Problem
An entity that many rows point at acts as a hub. Typical shape:
Article(1..n) --author--> Author(x) --articles--> Article(1..n)
When Author.articles is loaded (a lookup select, a count column, an inline list), editing Article 1 bumps Author x, and through Author.articles every other article that points at the same author. Every row re-renders on every keystroke in any row.
The ancestor dedup added in #85 (sharedAncestorBumpOnce.test.ts) bounds this to one bump per entity per write, so the cost is O(reachable entities), not O(edges) — but it is still the whole connected component.
See the doc comment on notifyEntityAndParentSubscribers in packages/bindx/src/store/SubscriptionManager.ts.
Proposed fix: selection-aware edge index
A parent only needs a bump when the child's change can be visible through the parent's selection.
- Record, per relation edge, the child selection the parent was loaded with (handles already carry
SelectionMeta; HasOneHandle / HasManyListHandle know the sub-selection they pass to child handles).
- On a child write, propagate along an edge only if the written path intersects the edge's selection. A relation-structure change (
add, remove, connect, disconnect) always propagates along that edge.
- Edges with no recorded selection (raw store writes, tests) keep the current behavior.
Open questions:
- Where to key the index: per
(parentKey, fieldName) or per subscriber. Per-edge is cheaper and matches how RelationStore is keyed today.
- Whether to stop at the first ancestor whose selection does not include the changed path, or keep walking for structural changes only.
- Interaction with
$connect(id) re-pointing: the sibling that just got connected has no edge to the child yet, so the connect itself must still notify through the parent.
Acceptance
- Diamond and hub tests in
tests/unit/store/ keep passing.
- A new test: two
Article rows sharing an Author with Author.articles loaded, row 1 subscribes to title; editing Article 2.title does not notify row 1.
- No change to the React hook contract (
useSyncExternalStore subscriptions stay per entity key).
Context
Since #85, a write to an entity notifies the entity's subscribers and then walks its live relation edges upward, bumping every ancestor (
SubscriptionManager.notifyEntityAndParentSubscribers). This is what makes$connect(id)re-point sibling subscriptions and lets a parent re-render when a nested entity changes.The walk is a transitive closure over all live edges, with no knowledge of which fields each subscriber actually reads.
Problem
An entity that many rows point at acts as a hub. Typical shape:
When
Author.articlesis loaded (a lookup select, a count column, an inline list), editingArticle 1bumpsAuthor x, and throughAuthor.articlesevery other article that points at the same author. Every row re-renders on every keystroke in any row.The ancestor dedup added in #85 (
sharedAncestorBumpOnce.test.ts) bounds this to one bump per entity per write, so the cost is O(reachable entities), not O(edges) — but it is still the whole connected component.See the doc comment on
notifyEntityAndParentSubscribersinpackages/bindx/src/store/SubscriptionManager.ts.Proposed fix: selection-aware edge index
A parent only needs a bump when the child's change can be visible through the parent's selection.
SelectionMeta;HasOneHandle/HasManyListHandleknow the sub-selection they pass to child handles).add,remove,connect,disconnect) always propagates along that edge.Open questions:
(parentKey, fieldName)or per subscriber. Per-edge is cheaper and matches howRelationStoreis keyed today.$connect(id)re-pointing: the sibling that just got connected has no edge to the child yet, so the connect itself must still notify through the parent.Acceptance
tests/unit/store/keep passing.Articlerows sharing anAuthorwithAuthor.articlesloaded, row 1 subscribes totitle; editingArticle 2.titledoes not notify row 1.useSyncExternalStoresubscriptions stay per entity key).