Skip to content

fix(runtime): the editor keeps waking during a drag on a preview it did not build - #3850

Merged
miguel-heygen merged 1 commit into
mainfrom
fix-gesture-watch-realm
Sep 10, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
fix-gesture-watch-realm

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

main is red. The preview-guard lint added in #3848 bans realm-bound DOM instanceof under src/runtime, and the gesture watch merged in #3845 has one at manualEditGestureWatch.ts:56. Each PR was green alone; they collide only once both are on main.

It is not only a lint failure. The composition body is adopted into the preview frame, so its nodes carry another realm's prototypes and target instanceof Element is false for every mutation record. The watch therefore never reports a gesture, and the transport it gates does not wake while the user drags a layer.

Change

The record target goes through isElementNode from domRealm.ts, with a comment naming why an identity check cannot work here.

Verification

Check Result
manualEditGestureWatch.test.ts 5 passed, exit 0
same file with instanceof Element restored 1 failed, exit 1, the new test
test:hyperframe-runtime-ci typecheck plus preview-guard lint plus runtime tests pass, 569 tests
oxlint, oxfmt on both files 0 errors

The new test adopts an element from a second realm, asserts el instanceof Element is false, then asserts the watch sees the marker appear and clear.

test:runtime-coverage fails on this machine for thirty-odd untouched files with a module-mocking error, identically with and without this change, so it is not from this diff.

…id not build

The manual-edit gesture watch tested mutation targets with `instanceof
Element`. The composition body is adopted into the preview frame, so its
nodes answer to another realm's Element and the check is false for every
one of them: the watch never sees a gesture, and the paused transport it
gates does not wake while the user drags.

Routes the check through the runtime's structural predicate, which is
what the preview-guard lint added alongside it now requires. Main is red
on that lint for this line, so this also unbreaks it.

Test adopts an element from a second realm and asserts the marker is
seen and cleared; it fails with the identity check restored.
@miguel-heygen
miguel-heygen enabled auto-merge (squash) September 10, 2026 16:15

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at 02743c710a57483c4e2bc53c2573043a1d259d14 — unbreaks main's Test: runtime contract lint by fixing #3845's instanceof Element at manualEditGestureWatch.ts:56 via the structural predicate #3848 shipped, and closes a real cross-realm correctness gap (the gesture watch has been silently skipping every mutation record from adopted preview nodes since #3845 landed).

Author + scope. miguel-heygen (trust-listed). Head 02743c710. Base main. +28/-1 across 2 files (manualEditGestureWatch.ts + manualEditGestureWatch.test.ts). mergeStateStatus BLOCKED (missing approval — first at-head review, zero others). No CI failures at snapshot; some checks still queued.

The one-line prod change

packages/core/src/runtime/manualEditGestureWatch.ts:56 swaps if (!(target instanceof Element)) continue; for if (!isElementNode(target)) continue;, importing isElementNode from ./domRealm (line 2 — the module #3848 introduced). Structural check is nodeType === 1 per domRealm.ts:50 — cross-realm safe because Node.ELEMENT_NODE === 1 is a numeric constant identical across realms. Same tag-identity as the OLD instanceof Element on same-realm nodes; strictly wider on cross-realm nodes, which is the point.

Why this is both lint AND correctness

  • Lint: #3848's packages/core/scripts/lint-runtime-preview-guards.ts bans instanceof Element in src/runtime via /\binstanceof\s+(?:Element|Node|Text|Document|DocumentFragment|ShadowRoot|HTML[A-Za-z]*Element|SVG[A-Za-z]*Element)\b/. manualEditGestureWatch.ts:56 in #3845 predates that rule but landed on main after; each was green alone, they collide on the merge — classic same-cause independent-branch collision.
  • Correctness: the composition body is document.adoptNode-d into the preview frame, so its element nodes carry the SOURCE realm's Element prototype. target instanceof Element compares against the PREVIEW realm's Element → false for every mutation record from the composition body. The gesture watch's ingest loop skipped 100% of those records, so isActive() stayed false for every drag whose marker was set on an adopted node, and the parked runtime transport (init.ts:3410's canParkTransport) never woke on drag start. Same class of bug #3848 was built to end — this just retires the one instance that snuck into the same runtime module in a sibling PR.

Test (the invariant Miguel's fix pins)

manualEditGestureWatch.test.ts:37-58 adds sees a gesture on an element adopted from another realm:

  • Builds a foreign JSDOM realm, creates a div in it, document.adoptNode-s it into the test-doc's body.
  • Line 49 explicit pin: expect(el instanceof Element).toBe(false) — proves the test exercises the bug scenario (adoption doesn't rewrite the prototype chain; the element's Element still refers to the foreign realm).
  • Sets the gesture marker → watch.isActive() returns true (mutation reached ingest, added to marked Set).
  • Removes the marker → watch.isActive() returns false (mutation reached ingest, cleared from marked Set).

Reverting the swap to instanceof Element flips the first expect(watch.isActive()).toBe(true) red — the ingest loop skips the record, marked stays empty. Reverting only the CLEAR-path handling (e.g., skipping the marked.delete(target) branch) flips the second .toBe(false) red. Both directions of the predicate matter and both are pinned.

Test file itself contains el instanceof Element — that's fine, the lint's isScannableSource filter skips *.test.ts, so the assertion doesn't re-red the guard.

No orthogonal changes, no scope creep

Diff is exactly two files, exactly the two mechanisms above. No shared-decorator hooks touched, no adjacent predicate migrations, no shape changes.

Miguel's callouts acknowledged

  • #3846 retarget note: understood — you retargeted #3846 to main at d9081f03d (unchanged head), CI fails the same lint through its base, will go green after this merges. I'll stamp #3846 at d9081f03d as soon as this lands + CI turns green on it.
  • #3845 follow-ups noted post-merge: the explicit park-then-event interleaving test AND the init.ts:3499 double-arm timer overwrite — both accepted as post-merge follow-ups. Fine.

— Review by tai (pr-review)

@miguel-heygen
miguel-heygen merged commit c752b89 into main Sep 10, 2026
59 checks passed
@miguel-heygen
miguel-heygen deleted the fix-gesture-watch-realm branch September 10, 2026 16:29
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.

2 participants