repl: fix cancel + immediate-resubmit race that wedged the new eval
Three converging bugs let a cancel-then-send sequence land in a broken state: 1) cancelAllPendingInActiveTab didn't touch the toolbar — the user had to wait for the cancelled promise's .catch.then to drain before send re-enabled. fox saw clicks register as noops because the button was still disabled. 2) Even after the rejection settled, the stale .then path re-enabled sendBtn from inside the FIRST run's closure — but by then the user had already submitted a SECOND run that set sendBtn disabled. The stale .then clobbered the in-flight state. 3) The OLD worker's last-queued "done" or "error" message could be delivered after a fresh run had already taken state.pending[k]; the OLD handler would then delete the NEW run's pending entry. Fixes: * cancel handler now re-enables send / disables cancel + pause synchronously and clears tab.activeInput. * The completion .then re-checks isEvalInFlight(tab) before resetting toolbar state, so a stale settle from a cancelled run can't override the fresh run's UI. * evalInTier's done/error handler only deletes state.pending[k] when the entry still has the runId we started with. Verified headlessly: long loop → cancel → submit (+ 1 2) immediately → first entry shows error: cancelled, second entry shows result 3, send re-enabled.
This commit is contained in:
parent
049a1e1935
commit
63808f72a5
3 changed files with 102 additions and 30 deletions
|
|
@ -434,11 +434,20 @@ function evalInTier(tabId, tier, src, onChunkText, onChunkEol) {
|
|||
onChunkEol && onChunkEol();
|
||||
} else if (e.data.kind === "done") {
|
||||
w.removeEventListener("message", handler);
|
||||
delete state.pending[pendingKey];
|
||||
// Only clear pending if it's still our runId — a
|
||||
// cancel-then-resubmit cycle can leave state.pending[k]
|
||||
// pointing at a fresh eval before our terminated worker
|
||||
// gets around to having its last-queued "done" delivered
|
||||
// on the main side.
|
||||
if (state.pending[pendingKey] && state.pending[pendingKey].runId === runId) {
|
||||
delete state.pending[pendingKey];
|
||||
}
|
||||
resolve(e.data.output);
|
||||
} else if (e.data.kind === "error") {
|
||||
w.removeEventListener("message", handler);
|
||||
delete state.pending[pendingKey];
|
||||
if (state.pending[pendingKey] && state.pending[pendingKey].runId === runId) {
|
||||
delete state.pending[pendingKey];
|
||||
}
|
||||
reject(new Error(e.data.message));
|
||||
}
|
||||
};
|
||||
|
|
@ -827,6 +836,15 @@ function cancelAllPendingInActiveTab() {
|
|||
delete state.pending[k];
|
||||
}
|
||||
}
|
||||
// Synchronously re-enable send so the user can submit a new job
|
||||
// immediately, without waiting for the cancelled promise chain's
|
||||
// microtasks to drain. The .then below also rechecks
|
||||
// isEvalInFlight so a follow-up sendInput's button state isn't
|
||||
// clobbered when those microtasks eventually fire.
|
||||
sendBtn.disabled = false;
|
||||
cancelBtn.disabled = true;
|
||||
pauseBtn.disabled = true;
|
||||
tab.activeInput = null;
|
||||
}
|
||||
|
||||
// ─── Render ─────────────────────────────────────────────────────────
|
||||
|
|
@ -1157,14 +1175,20 @@ async function sendInput() {
|
|||
saveSoon();
|
||||
remaining--;
|
||||
if (remaining === 0) {
|
||||
sendBtn.disabled = false;
|
||||
cancelBtn.disabled = true;
|
||||
pauseBtn.disabled = true;
|
||||
inputEl.focus();
|
||||
// Eval finished cleanly — drop the activeInput marker so
|
||||
// a subsequent tab switch doesn't try to resume something
|
||||
// that already completed.
|
||||
tab.activeInput = null;
|
||||
// Don't clobber the toolbar / activeInput marker if
|
||||
// another sendInput started while THIS one was still
|
||||
// unwinding. The cancel-then-submit-immediately race
|
||||
// used to leave send re-enabled mid-eval because the
|
||||
// first run's .then fired after the second's sendInput
|
||||
// had already disabled the buttons. Check the live
|
||||
// pending state instead of trusting "remaining===0".
|
||||
if (!isEvalInFlight(tab)) {
|
||||
sendBtn.disabled = false;
|
||||
cancelBtn.disabled = true;
|
||||
pauseBtn.disabled = true;
|
||||
inputEl.focus();
|
||||
tab.activeInput = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue