fix: keep live drops atomic across board handoff - #5634
Conversation
27fcf98 to
472a6dd
Compare
|
Here's a visual recap of what changed: Open the full interactive recap |
There was a problem hiding this comment.
Builder reviewed your changes and found 3 potential issues 🟡
Review Details
Incremental Code Review Summary
The latest update substantially addresses the prior transaction-lifecycle findings. The shared insert setter now rejects competing transaction ids, board transaction identity is retained through rollback, and the timeout path attempts to reconcile a possibly-applied DOM mutation. I verified and resolved the four previously open comments covered by these fixes. Targeted regression tests pass locally: 49/49.
New findings
- 🟡 MEDIUM — The retained board transaction id is not reset when a new board handoff begins, allowing a late acknowledgement from the previous drop to settle the new drop and cancel its timeout.
- 🟡 MEDIUM — The admission wrapper silently drops unrelated paste/create/duplicate/redo runtime inserts while a cross-screen transaction is pending, leaving callers believing their operation succeeded.
- 🟡 MEDIUM — A timeout-triggered rollback clears the insert request before ensuring the empty board iframe remains mounted, so the rollback executor can unmount before it runs and leave the transaction lock stuck.
The dev server was healthy. Full browser verification was attempted with seeded live-edit designs, but all 14 cases were blocked because Chrome automation tools were unavailable in the browser-test executor session.
🧪 Browser testing: Attempted full visual verification, but blocked by missing Chrome automation tools in the executor.
| onRuntimeStructureInsertApplied={(details) => { | ||
| const expectedTransactionId = | ||
| boardCrossScreenDropTransactionRef.current; | ||
| if ( |
There was a problem hiding this comment.
🟡 Prevent a stale acknowledgement from settling the next board drop
After a successful insert, finishBoardCrossScreenDrop({ preserveTransaction: true }) intentionally retains boardCrossScreenDropTransactionRef, but starting the next board handoff does not reset it. A late acknowledgement for the previous transaction can therefore match expectedTransactionId here, call finishBoardCrossScreenDrop(), and cancel the new drop's timeout before the new insert is admitted. Reset or sequence the expected transaction when each new board handoff begins so only the current drop can settle it.
Additional Info
New finding confirmed by one review agent; distinct from the resolved rollback cleanup comment.
| const pendingTransactionId = | ||
| runtimeStructurePendingTransactionRef.current; | ||
| if ( | ||
| resolved && | ||
| pendingTransactionId && | ||
| resolved.transactionId !== pendingTransactionId | ||
| ) { | ||
| if (DESIGN_EDITOR_DEBUG_LOGS) { | ||
| console.warn("[design] runtime structure insert admission refused", { | ||
| pendingTransactionId, | ||
| requestTransactionId: resolved.transactionId ?? null, | ||
| }); | ||
| } | ||
| return current; |
There was a problem hiding this comment.
🟡 Do not silently discard unrelated runtime inserts
While a cross-screen transaction is pending, this setter returns the old state for every request with a different transaction id, including untagged inserts from paste, primitive creation, duplicate, redo, and layer-move commands. Those callers receive no admission result and can update local state or report success even though no iframe request is sent. Expose an explicit rejection/queue path or preflight the admission in each caller so user inserts are not silently lost.
Additional Info
Found by two independent review agents; this is a new user-facing consequence of the admission wrapper, not a repost of the prior serialization comment.
| transactionId && | ||
| boardCrossScreenDropTransactionRef.current === transactionId | ||
| ) { | ||
| onBoardRuntimeStructureInsertRejectedRef.current?.( |
There was a problem hiding this comment.
🟡 Keep the board mounted while timeout rollback executes
When the timeout callback invokes the parent rejection handler, the handler schedules boardRuntimeStructureRollbackRequest and clears runtimeStructureInsertRequest. For an empty board with no acknowledged runtime content, shouldMountBoardSurface can then become false before the rollback request executes, unmounting the only iframe that can perform the rollback and release the transaction lock. Keep the board surface active until rollback settlement, or explicitly settle and release the transaction when unmounting.
Additional Info
New finding confirmed by one review agent; distinct from the resolved early-timeout-cancellation comment.

Problem
Live cross-screen drops into an empty board could unmount the inert board iframe before the asynchronous hit-test posted its runtime insert request. A second live drop could also overwrite the single pending runtime request slots, leaving a destination clone without source removal.
Fix
Validation
vitest run app/pages/design-editor/commands/cross-screen-element-drop.spec.ts app/components/design/multi-screen/board-surface-rendering.spec.tsgit diff --check