fix(web): serialize live topology mutations + surface failures loudly

Live MazeNET edits fired their mutations fire-and-forget: each canvas
action enqueued immediately and never awaited the result. Two failures
followed from that:

- expected_version is bumped at ENQUEUE (not at apply), so two ops fired
  back-to-back raced — the second carried a stale version and 409'd.
  Edits only worked when hand-paced (an SSE refetch landed between them).
- A failed mutation degrades the topology, but the only signal was a 4s
  toast, so the user saw DEGRADED with no cause.

useTopologyEditor now routes every live op through a serialized submit
queue: one enqueue in flight at a time (submission order preserved), an
optimistic expected_version cursor advanced per enqueue so back-to-back
ops (e.g. reparent's detach+attach) don't need a refetch between them,
and each mutation awaited to a terminal state. A 'failed' row throws
MutationFailedError, which the page pins as a persistent UPDATE FAILED
banner instead of a vanishing toast.

Slice 1 of the live-edit rework; stage+UPDATE-button batching and louder
backend materialisation reporting to follow.
This commit is contained in:
2026-06-16 12:44:34 -04:00
parent 5505de782f
commit f18bfee746
5 changed files with 216 additions and 28 deletions

View File

@@ -181,7 +181,7 @@ const MazeNET: React.FC = () => {
const {
nets, setNets, nodes, setNodes, edges, setEdges,
topoMeta, services, archetypes,
loadErr, actionErr, flashErr,
loadErr, actionErr, commitErr, clearCommitErr, flashErr,
deploying, onDeploy,
streamLive, lastEventAt, streamEnabled,
refetch,
@@ -560,6 +560,17 @@ const MazeNET: React.FC = () => {
)}
{loadErr && <span className="alert-text"> · {loadErr}</span>}
{actionErr && <span className="alert-text"> · {actionErr}</span>}
{commitErr && (
<span className="alert-text"> · UPDATE FAILED: {commitErr}
<button
type="button"
className="maze-btn ghost"
style={{ marginLeft: 6, padding: '0 6px' }}
onClick={clearCommitErr}
title="Dismiss"
></button>
</span>
)}
</div>
</div>
<div className="maze-page-actions">