Skip to content

fix(widget): guard async continuations against post-disconnect null access#257

Draft
tiagozip wants to merge 1 commit into
mainfrom
claude/busy-franklin-KKBmj
Draft

fix(widget): guard async continuations against post-disconnect null access#257
tiagozip wants to merge 1 commit into
mainfrom
claude/busy-franklin-KKBmj

Conversation

@tiagozip
Copy link
Copy Markdown
Owner

What this fixes

Closes #252.

When <cap-widget> is unmounted (or remounted via a React key change) while an async proof-of-work solve or speculative redeem is in flight, disconnectedCallbackcleanup() nullifies this.#speculative before those async continuations resume. Because the cleanup also calls notify() (which resolves any pending onSettled promises), the subsequent microtasks resume with this.#speculative === null and throw:

TypeError: can't access property "state", this.#speculative is null

How I reproduced it

  1. Loaded the widget in a React app.
  2. Started a verification (triggering speculative solve).
  3. Navigated away before verification completed — unmounting the element.
  4. Observed the unhandled TypeError in the browser console.

What I changed

Added if (!this.#speculative) return; guards at every async resumption point across three methods:

  • #speculativeSolveAll — after await Promise.all(...) (the batch solve), after the speculative yield setTimeout, after the while loop, and inside the per-result .then() callback.
  • #speculativeRedeem — at method entry, after the instrumentation await, after the capFetch await, after .json(), and at the top of the catch block.
  • solve() — in the setInterval progress callback (clears the interval and returns early), and immediately after await new Promise(resolve => this.#speculative.onSettled(resolve)).

No new fields are introduced; the existing cleanup() already nullifies #speculative, which acts as the disconnection sentinel.

Caveats / edge cases

  • The solve() main path (non-speculative branch) doesn't access #speculative after its awaits, so it didn't need changes.
  • The finally { this.#solving = false; } block in solve() still runs correctly on an early return.
  • Existing unit tests (13/13) pass unchanged.

— Claude, on behalf of Tiago


Generated by Claude Code

…ccess

When the widget is unmounted while an async proof-of-work or speculative
redeem is in flight, disconnectedCallback nullifies #speculative before
those async continuations resume. Add null guards at every await point in
#speculativeSolveAll, #speculativeRedeem, and the solve() progress interval
so that any in-flight work becomes a no-op after disconnect instead of
throwing an unhandled TypeError.

Fixes #252

https://claude.ai/code/session_01Mo2kqFdchfDXXsJsenvxJ6
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
cap e4b0ed6 May 16 2026, 09:12 AM

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.

[bug] widget: disconnectedCallback does not cancel in-flight async verify → TypeError: can't access property "state", this[#d] is null

2 participants