Skip to content

Selection-aware parent propagation to stop hub fan-out #86

Description

@matej21

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions