Skip to content

Unify the Each reconciler's keyed/positional paths into one planner + executor - #22

Merged
khusmann merged 4 commits into
mainfrom
plan-reconcile
Jun 13, 2026
Merged

Unify the Each reconciler's keyed/positional paths into one planner + executor#22
khusmann merged 4 commits into
mainfrom
plan-reconcile

Conversation

@khusmann

@khusmann khusmann commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Unifies the Each reconciler's keyed and positional paths into a single pure planner and one mode-agnostic executor.

This PR landed in two stages on the branch:

  1. Extract pure planners — pulled the decision logic (which entries to remove/add/keep/rebuild) out of the observer into pure functions taking shape signatures as data.
  2. Unify the two paths (this is the headline change) — collapsed the keyed/positional fork into one planner + one executor.

The core insight

Positional Each is keyed Each whose id is the row position. With ids as stable strings — the key in keyed mode, as.character(index) in positional mode — every mode difference collapses to set algebra over old_ids / new_ids.

What changed (all in mount.R)

  • One planner replaces two. plan_reconcile(old_ids, new_ids, old_sigs, new_sigs) returns removed/added/kept, a derived order (or NULL), and a build_index mapping added ids to their positions. No session, no build_entry, no DOM messages.
  • Derived order policy. order is computed from the client's natural insert order (survivors in old order ++ added at the tail) and sent only when it differs from new_ids — no mode flag. This reproduces both modes' historical behavior and, as a free correct improvement, omits order on keyed tail-appends (smaller payloads). No irid.js / wire change required.
  • Mode-agnostic executor. run_reconcile_plan() does teardown → build → irid-mutate → mount → reposition against a unified container. The pos_rv guard makes kept-repositioning a no-op for positional entries.
  • The observer fork shrank to one line — the new_ids computation (as.character(by(x)) vs seq_along). Everything after is shared.
  • Container unified from a dense-list / sparse-map split into a single named map keyed by string id, plus an ordered current_ids vector.
  • build_entry stays mode-aware — its late-bound-key vs captured-index value access is the identity model, not incidental duplication.

Tests

Planner unit tests exercise the decision logic directly with plain data, including the derived order policy: append-no-order, reorder-order, mid-list-rebuild-order, keyed-tail-append-no-order, shrink-no-order, and build_index. The mount-based integration tests (the reconciler guardrail) pass untouched. 706 tests, 0 failures, plus a live browser check of the keyed reorder/focus path.

Follow-up

The client currently applies order with O(n) DOM moves; a client-only O(moved) optimization is tracked separately in #23 and deliberately decoupled from this change.

@khusmann khusmann changed the title Extract pure plan_reconcile planners from the Each reconciler Unify the Each reconciler's keyed/positional paths into one planner + executor Jun 13, 2026
@khusmann
khusmann merged commit ad78ccd into main Jun 13, 2026
3 checks passed
@khusmann
khusmann deleted the plan-reconcile branch June 19, 2026 04:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant