Skip to content

🧰🎨: extract morphic changes with synchronous metadata callers - #1847

Open
merryman wants to merge 13 commits into
robin/layout-spec-fidelityfrom
robin/morphic-change-engine
Open

merryman wants to merge 13 commits into
robin/layout-spec-fidelityfrom
robin/morphic-change-engine

Conversation

@merryman

@merryman merryman commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Extend ChangeManager and UndoManager with synchronous atomic transactions and exact replay. Supported morph edits can commit as one undoable unit or recover their captured state when a mutation or reaction fails.

  • transaction(opts, fn) captures property, structural and typed text operations before their effects, including partially failing primitives. Nested transactions provide savepoints. Complete rollback rethrows the original error by identity; incomplete recovery reports MorphicRollbackError. A caught incomplete nested rollback prevents the outer transaction from committing.
  • applyChangeSet replays without creating history. Synchronous reactions run normally; their writes are journaled, repaired and verified against the intended state. Value snapshots preserve mutable graphics values, container prototypes, own __proto__ fields, sparse arrays and symbol keys.
  • Text operations retain compact range content, attributes, selection and embedded-morph recovery data. Exact replay of the full-text listener view requires its matching receipt and rejects missing receipts before mutation.
  • Commit tokens and replaceRecordedChanges preserve chronological history ownership. compensate reverses rejected committed work exactly once; abortGroup reverses an owned active group without a redo entry. performEdit enlists domain edits with recovery receipts.
  • Detached-object handles track retention across history replacement, pruning and clearing. Per-node disposal receipts support idempotent cleanup under a caller-supplied policy. historyStats() reports retained objects and estimated payload; clearHistory() releases history handles. Attached objects are never disposed.

Committed listeners receive one delivery after a successful outer transaction and none after rollback. Synchronous onChange observers can see intermediate writes and their inverses. Metadata callbacks propagate exceptions after restoring their scope. Existing recording batches retain their non-atomic behavior, and caller adoption remains opt-in.

Transactions cannot span asynchronous work. Returning a thenable rolls back synchronously and throws MorphicTransactionAsyncError; later continuations run outside that transaction. The state guard rejects direct state-object writes, while slot replacement and retained shallow aliases are checked at scope exit. Deep alias mutations, never-enlisted morphs, uninstrumented document methods, arbitrary DOM/JavaScript state and external effects remain outside the guarantee.

Browser regressions cover savepoints, partial failures, late-enlisted guards, reaction recovery, text and embedded morphs, history ownership, domain receipts and retained-object lifetimes. Focused transaction/foundation tests pass, including snapshot regressions that fail without the own-key correction.

Refs #104: this supplies the change foundation; world-level direct-manipulation adoption and the CRDT experiment remain open.

Extract operation/change-set recording, structural undo and transaction tests.
Ship the synchronous metadata contract with its rich-text callers and the
two synchronous legacy fixture callbacks. Move the existing browser test
cache bypass to the first slice whose CI runs the full IDE suite.

Retain main text before-values while the legacy tracker remains. The
committed-listener snapshot optimization and its test ship with S7.
Capture setters and structural primitives before effects; commit outer scopes
once and verify reverse recovery on failure. Add synchronous savepoints,
typed replay without history, committed/deferred delivery and change-set undo.

Absorb the generic F5/F6 history and notification mechanics, and remove the
parallel transaction classes after migrating their contract tests. Keep the
ordinary recording path unchanged outside atomic frames.
Reverse the captured endpoints without materializing them. A domain
materializer can return the current cached object, replacing both endpoints
with its present state if invoked while planning an inverse.

Keep materialization at replay and retain exact precondition and recovery
checks. Cover cached identity adoption and stale replay rejection in a
standalone morphic regression.
Clear the recording token after its joined entries are copied into the
composite history entry. This restores the F5 cleanup at both normal
closure and the next undoStart boundary without losing replay history.

Add standalone coverage for both closure paths.
Temporarily proxy enlisted state during synchronous atomic scopes.
Reject direct proxy writes and recover shallow alias or state-slot damage.
Restore the original state objects and revoke captured views on every exit.
Capture supported Text edits as compact ReplaceText operations before document mutation. Keep the legacy notification view and use its matching commit receipt for exact attachment replay. Preserve existing non-transaction Text behavior and selection rules.

Journal synchronous reactions with their source and reconcile replay and rollback to exact unit targets. Compose same-Text edits from their recorded ranges into one covering-span repair, preserving attributes, anchors and embedded ownership. Verify recovery and report incomplete rollback under C3.

Keep source-unit parent and replay-journal state on the atomic frame, so a completed scope leaves no new manager slots. Cover commit, failure, thenable, nested and replay cleanup without changing ordinary recording or delivery.
Replace contiguous legacy records atomically at their chronological slot,
with pending-only ownership transfer authenticated by a live commit token.
Stage provisional history before committed consumers so only the final
owner publishes an entry, and reject incomplete or interleaved ownership.

Compensate committed sets or legacy receipts exactly once, discarding owned
and inverse records and cancelling only genuinely empty groups. Keep
ordinary committed-context construction unchanged and create ownership
state only when requested through commitTokenFor(context).
Register lifetime handles only while rooted, release unclaimed context
handles on scope exit, sweep dead registry references incrementally, and
reuse the supplied operation context during replay. History-owned handles
and per-node disposal receipts keep their existing release semantics.

Consume rejected withMetaDo thenables before rejecting asynchronous use,
hold rollback diagnostics weakly, and reject undoStop during replay before
it can discard an active gesture recorder.

Add regressions for bounded registry size, context reuse and cleanup,
metadata rejection handling, diagnostic retention and replay ownership.
Main now caches static text lines (#1826) and lets static text embed
morphs without a document (#1825). Transactional text replay has to
honor both:

- applying a text edit to a document-free text invalidates the cached
  static lines and the document backup before assigning the new text,
  so undo, redo and replay refresh static text and survive switching
  back to editable mode;
- only doits force a document; embedded morphs stay document-free and
  are attached through the regular embedded-morph update.

The change engine tests that create their own world now restore the
previous $world, so later suites keep rendering into the active world.

The text transaction tests cover read-only round trips and keeping
static text embedding document-free across undo and redo.
@merryman
merryman force-pushed the robin/morphic-change-engine branch from cdedfc3 to e30452d Compare November 5, 2026 10:46

This branch has not been deployed

No deployments
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