From 7802ff53b2f17ccccf6e2589b59f9513202f7e07 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sat, 10 Oct 2026 19:11:58 -0700 Subject: [PATCH] fix(web): rearm browser waits after early timers A browser timer can fire before its requested Instant deadline. One-shot armed flags then left waits pending forever without another wake, stranding durability retries and Live Share response timeouts. Rearm remaining waits on each pending poll and round host millisecond delays upward. Force an early retry timer in the actual-wasm receipt probe and require both accepted edit and later barrier to stay pending through storage failures, then recover with one publication and retained dropped-owner ownership. Validation: actual wasm probe passed with 3 synchronous host throws, 1 Promise rejection, forced early timer, 1 publication, responsive reads, and dropped-owner busy/reopen. Native all-feature notebook+snowbound check, wasm wgpu/live check, JavaScript 9/9, and formatting passed on the identical combined source tree. Fixes #113 Assisted-by: gpt-6.1-sol --- crates/notebook/src/task.rs | 15 ++++++--------- tools/web/receipt_probe.mjs | 12 ++++++++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/notebook/src/task.rs b/crates/notebook/src/task.rs index df2ac9396dd820c2fa73cfe9e332e139c2090771..add6487ca0fa71ab48de3100682528584d5c993b 100644 --- a/crates/notebook/src/task.rs +++ b/crates/notebook/src/task.rs @@ -151,7 +151,6 @@ mod web { timeout: Duration, ) -> Result { let deadline = web_time::Instant::now() + timeout; - let mut armed = false; let mut answer = std::pin::pin!(receive(receiver)); std::future::poll_fn(|context| { if let Poll::Ready(answer) = answer.as_mut().poll(context) { @@ -162,9 +161,10 @@ mod web { if web_time::Instant::now() >= deadline { return Poll::Ready(Err(std::sync::mpsc::RecvTimeoutError::Timeout)); } - if !std::mem::replace(&mut armed, true) { - after(timeout, context.waker().clone()); - } + after( + deadline.saturating_duration_since(web_time::Instant::now()), + context.waker().clone(), + ); Poll::Pending }) .await @@ -227,12 +227,11 @@ mod web { return waker.wake(); }; let wake = Closure::once_into_js(move || waker.wake()); - let _ = set_timeout.call2(&global, &wake, &(delay.as_secs_f64() * 1e3).into()); + let _ = set_timeout.call2(&global, &wake, &(delay.as_secs_f64() * 1e3).ceil().into()); } pub(crate) async fn wait(receiver: &Receiver<()>, timeout: Option) { let deadline = timeout.map(|timeout| web_time::Instant::now() + timeout); - let mut armed = false; std::future::poll_fn(|context| { let Ok(mut bell) = receiver.0.lock() else { return Poll::Ready(()); @@ -243,9 +242,7 @@ mod web { return Poll::Ready(()); } bell.waiting = Some(context.waker().clone()); - if let Some(deadline) = deadline - && !std::mem::replace(&mut armed, true) - { + if let Some(deadline) = deadline { after( deadline.saturating_duration_since(web_time::Instant::now()), context.waker().clone(), diff --git a/tools/web/receipt_probe.mjs b/tools/web/receipt_probe.mjs index 5fc7b43f1186a27220944792f80246385f843d07..7670a358025a190ea45b320cd9d88bfb328ec55c 100644 --- a/tools/web/receipt_probe.mjs +++ b/tools/web/receipt_probe.mjs @@ -7,7 +7,14 @@ import { setTimeout } from 'node:timers'; globalThis.crypto ??= webcrypto; let failure = 0; let phase = 'initializing wasm'; -globalThis.setTimeout = (callback, delay, ...args) => setTimeout(callback, delay, ...args).unref(); +let early = true; +globalThis.setTimeout = (callback, delay, ...args) => { + if (early && failure === 2 && delay <= 1000) { + early = false; + delay = 0; + } + return setTimeout(callback, delay, ...args).unref(); +}; const failures = [0, 0, 0]; globalThis.snowboundReceiptPhase = value => { phase = value; }; globalThis.snowboundReceiptFailure = value => { failure = value; }; @@ -26,7 +33,8 @@ try { await wasm.receipt_probe(); assert.ok(failures[1] >= 2, 'The active and dropped owner each retry synchronous storage failure'); assert.ok(failures[2] >= 1, 'An asynchronous storage rejection also keeps the receipt pending'); - console.log(`Actual wasm receipts passed: ${failures[1]} throws, ${failures[2]} rejections, one publication, responsive reads, retained ownership`); + assert.equal(early, false, 'A retry timer fired before its deadline'); + console.log(`Actual wasm receipts passed: ${failures[1]} throws, ${failures[2]} rejections, early timer, one publication, responsive reads, retained ownership`); } finally { clearTimeout(timeout); } -- 2.54.0