pcbjam/docs/features/ysync-review/19-undo-option1-feasibility.md
Gergő Törcsvári cd68114cdc
docs(ysync-review): 19 — miss-09 option 1 (local-ops-only undo) feasibility research
Verdict: ~3-5 days. pcbnew undo already UUID-guards stale pointers; core
work = porting that guard to eeschema, splitting connectivity out of the
SCH SKIP_UNDO gate, plugging the SKIP_UNDO removed-item leak, silencing
the incomplete-undo modal. Cross-linked from 09 and the overview index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ejJEvS7ogef2o9gVTXjmp
2026-07-07 21:09:24 +02:00

123 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Research 19 — Feasibility of miss-09 option 1: exclude remote applies from undo
**Status:** research (2026-07-07), not implemented
**Verdict:** moderately hard but tractable — ~3–5 days. The pointer-invalidation risk
[09](09-miss-undo-not-collab-aware.md) flagged is already solved on the pcbnew side;
the core work is porting that resilience to eeschema, plus two small fork fixes to
make `SKIP_UNDO` safe for our use.
## Question
Miss 09's option 1 is "local-ops-only undo": remote applies stop landing on the
receiving editor's undo stack, so Ctrl+Z can never revert a peer's work. Both commit
classes support a `SKIP_UNDO` push flag — but 09 warned that KiCad's undo machinery
assumes the stack mirrors model history, and that `PICKED_ITEMS_LIST` holds raw
pointers which remote applies could strand. How bad is that really?
## What makes it easier than 09 feared
**The flag exists on both sides and skips only what we want (almost — see fix 2).**
`SKIP_UNDO` is honored by both commit classes (`pcbnew/board_commit.h:40`,
`eeschema/sch_commit.h:40`; base signature `include/commit.h:132`). Under it the
commit still does everything remote applies go through a real commit *for*:
board/screen add/remove, `view->Add/Remove/Update`, connectivity dirty-marking and
ratsnest (`board_commit.cpp:463-467,516`), teardrops, listener callbacks
(`OnItemsCompositeUpdate` / `OnSchItems*`), and the model-change events. Only the
undo-picker creation and the final `SaveCopyInUndoList` are gated
(`board_commit.cpp:325,356,455,606`; `sch_commit.cpp:266,295,363,448`).
**Exactly four Push sites to change, each already remote-tagged.** All remote data
funnels through `doApply` / `doApplyItems` per editor
(`wasm/bindings/eeschema_embind.cpp:1093,1218`;
`wasm/bindings/pcbnew_embind.cpp:965,1061`), every one bracketed by the
`s_applyingRemote` guard — the exact local/remote signal, available at the Push call.
Deletes, adopt, and the parked-dirty catch-up all funnel through `doApplyItems`;
there is no fifth production path.
**Change detection is unaffected.** The local-edit→yjs emit path is the
listener-`trigger()` + post-settle `flushDiff`, suppressed during remote applies by
`s_applyingRemote` (`eeschema_embind.cpp:652`, `pcbnew_embind.cpp:871`) — entirely
independent of whether the commit is undoable. Skipping undo disturbs neither the
suppression during applies nor the emit on genuine local edits (including the emit of
a local undo itself, which is what makes undo propagate today).
**Pcbnew undo already survives items vanishing behind its back.** Our fork's
`PutDataInPreviousState` checks every picker against `BOARD::ResolveItem( uuid )` and
drops entries whose items no longer exist (`pcbnew/undo_redo.cpp:428-437`), and even
re-resolves stale `CHANGED` pointers by UUID before swapping the clone image back in
(`:504`, comment cites ExchangeFootprint-style swaps). Group parent/member pointers
are rebuilt from KIIDs cached in the picker itself
(`undo_redo_container.h:114-115`, rebuilt at `undo_redo.cpp:633-658`). So on the PCB
side, a remote apply deleting or replacing an item referenced by a local undo entry
degrades gracefully (entry dropped) instead of crashing.
## The actual work
1. **Port the UUID guard to eeschema — the core task.**
`eeschema/schematic_undo_redo.cpp::PutDataInPreviousState` (`:246`) has *no*
existence or re-resolution check: it dereferences the stored raw `EDA_ITEM*`
directly (`:270,399,431`); only the group-relink post-pass uses
`Schematic().ResolveItem` (`:485,504`). Since `doApplyItems` upserts full item
subtrees as remove-by-uuid + re-add, a remote **change** (not just a delete) to an
item referenced by a local undo entry means a dangling pointer → crash on Ctrl+Z.
Fix: mirror pcbnew's pattern — existence-check each picker via
`SCHEMATIC::ResolveItem` (`eeschema/schematic.h:129`), drop missing entries,
re-resolve `CHANGED` pointers before `SwapItemData`. ~50–100 lines, follows an
in-tree precedent, plausibly upstreamable. This is the bulk of the effort.
2. **Un-bundle connectivity from `SKIP_UNDO` on the SCH side.** Unlike pcbnew,
`SCH_COMMIT::pushSchEdit` gates `RecalculateConnections` inside the same
`if( !( aCommitFlags & SKIP_UNDO ) )` block as `SaveCopyInUndoList`
(`sch_commit.cpp:448-463`). Remote applies need that recalc — it is the reason
they go through real commits. Small fork change: split the gate (or run the recalc
from the binding after Push).
3. **Plug the removed-item leak under `SKIP_UNDO`.** In the REMOVE branch the live
item is flagged `UR_TRANSIENT` (`board_commit.cpp:447`) but with `SKIP_UNDO` no
picker takes ownership and the clone is deleted (`:490`), so the removed item
leaks; same shape on the SCH side (`sch_commit.cpp:409-410`). Existing `SKIP_UNDO`
callers (DRC-marker flows, `drc_tool.cpp:208` etc.) never remove user items, so
upstream never hit this. Fix: delete removed items in the commit when the flag is
set (or in the binding, which knows the removed set).
4. **Silence the "incomplete undo" modal.** When pcbnew drops a stranded entry it
pops a blocking `wxMessageBox( "Incomplete undo/redo operation: some items not
found" )` (`pcbnew/undo_redo.cpp:628`). In a collab session that is routine, not
exceptional — downgrade to a log line (or toast) in the fork, and use the same
quiet path in the eeschema port.
Plus wasm-side flag changes at the four Push sites (~one line each) and multi-client
e2e coverage for undo-after-remote scenarios (delete-under-undo, change-under-undo,
adopt-while-history-nonempty), which given the two-tab harness is roughly another day.
## Residual semantic gaps (accepted, not blockers)
- **Full-clone restore clobbers concurrent field edits.** Undoing a local move of an
item restores the whole old item image (`CHANGED` swaps the full clone), so a
peer's concurrent edit to another property of the same item is reverted too.
Last-writer-wins collab undo — same as Figma; converges; not an operational rebase.
Same granularity trade-off as [14](14-opt-item-granularity-bandwidth.md).
- **Duplicate-KIID re-add edge.** Local delete parks the item on the local undo stack
(owned, `DELETED` pickers skip the existence check — `undo_redo.cpp:423`); if a
peer re-adds the same uuid and the local user then undoes the delete, the stored
item is re-added alongside the peer's copy with the same KIID (`:569-572`). Rare;
the differ keys by uuid so it likely reconciles to one, but needs a test.
- **Pre-existing local entries at adopt time** mostly reference replaced items and
get dropped by the UUID guard on first undo — acceptable, and strictly better than
the current adopt undo bomb.
## Effort summary
| Piece | Where | Size |
|---|---|---|
| `SKIP_UNDO` at the 4 remote Push sites | wasm bindings | trivial |
| SCH connectivity/`SKIP_UNDO` split | kicad fork (sch_commit) | small |
| Removed-item leak fix | kicad fork (both commits) | small |
| eeschema UUID guard port | kicad fork (schematic_undo_redo) | 1–2 days, core |
| Modal → quiet drop | kicad fork (undo_redo) | trivial |
| Two-client undo e2e | tests/ | ~1 day |
All fork touches are small and upstream-plausible, consistent with keeping the fork
close to upstream. Miss 09's option 3 (clear undo stack on adopt) becomes largely
moot: with `SKIP_UNDO`, adopt creates no undo entry at all, and stranded pre-adopt
entries are handled by the UUID guard.