Skip to content

fix(browser): make waitFor options-only and race-safe#114

Open
Alezander9 wants to merge 1 commit into
mainfrom
waitfor-options-only
Open

fix(browser): make waitFor options-only and race-safe#114
Alezander9 wants to merge 1 commit into
mainfrom
waitfor-options-only

Conversation

@Alezander9

@Alezander9 Alezander9 commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

Replaces #111. Fixes #110.

waitFor's second parameter is now an options object — waitFor(method, { predicate?, timeoutMs? }) — and the positional (method, predicate?, timeoutMs?) form is removed. The skill's navigation examples register the load waiter before Page.navigate so a fast load event can't be missed.

Why this shape instead of #111

Both real bugs from #110 are fixed here; the difference is how much API we own afterwards.

The evidence that settled the signature: in observed agent traces, a model that wanted a navigation timeout wrote waitFor("Page.loadEventFired", { timeoutMs: 15000 }).catch(() => {}) — under the positional signature the object became the predicate, the predicate throw was swallowed, the waiter ran to the default 30s, and the defensive .catch masked the timeout. A silent flat 30s tax on every navigation. The options object is the shape models assume from priors (Puppeteer/Playwright), so we make the one invented signature in session.ts match the prior instead of asking models to learn ours.

Given that, the overload machinery in #111 (positional + options forms, argument-shape validation ladder) isn't needed — there is exactly one call shape, and the only guard is a synchronous TypeError when a function is passed where options belong, so stragglers fail loud at the call site instead of stalling silently. Sync-throw also can't be swallowed by a .catch on the returned promise.

Also intentionally not carried over from #111:

  • Snippet-side void loaded.catch(() => {}) incantationwaitFor now pre-observes its own promise, so an abandoned waiter times out without an unhandled rejection. Fixed once in the session layer instead of taught in the skill forever.
  • loaderId / same-document branching in the skill examples — the canonical example stays the 3-line happy path; Page.navigate's errorText behavior is noted in one line (that's real CDP knowledge worth teaching, the branching isn't).
  • Active-session event filtering in waitFor — real question, but it's a semantics change beyond fix(browser): make waitFor options explicit and race-safe #110's scope; should be its own decision if traces show it biting.
  • navigate() wrappersession.ts stays a transport shim; navigation composition belongs to the snippet, guided by the skill.

Predicate-throw → reject (with unsubscribe) is kept from #111, as is the mock-WS test structure (credited in the test file).

Validation

  • bun test test/cdp-session.test.ts — 4 pass
  • bun test (package) — 16 pass, 8 env-gated skips, 1 pre-existing unrelated failure (edit + cache-bust workspace import; fails identically on main)
  • bun typecheck from packages/bcode-browser — clean

Summary by cubic

Make waitFor options-only and race-safe. Prevents silent 30s stalls and ensures navigation waiters catch fast load events.

  • Bug Fixes

    • waitFor(method, { predicate?, timeoutMs? }) is the only supported form; passing a function as the second arg now throws synchronously.
    • Abandoned waiters no longer create unhandled rejections; predicate errors reject immediately and unsubscribe.
    • Examples/tests now register the load waiter before Page.navigate to avoid missing a fast Page.loadEventFired.
  • Migration

    • Replace waitFor(method, predicate?, timeoutMs?) with waitFor(method, { predicate, timeoutMs }).
    • Register const loaded = session.waitFor("Page.loadEventFired", { timeoutMs }) before Page.navigate({ url }), then await loaded.
    • Remove waitFor(...).catch(() => {}) that was only used to silence unhandled rejections.

Written for commit 089681b. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/bcode-browser/src/cdp/session.ts">

<violation number="1" location="packages/bcode-browser/src/cdp/session.ts:199">
P2: The removed positional form with an omitted predicate is not rejected: `waitFor(method, undefined, timeoutMs)` falls through the function-only guard, defaults to `{}`, and silently ignores `timeoutMs`, restoring the 30-second default. This makes JavaScript/snippet callers fail with an unexpectedly long timeout instead of getting the documented migration error; checking for extra arguments (for example `arguments.length > 2`) would make the old form consistently fail.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

* await loaded
*/
waitFor<T = unknown>(method: string, opts: { predicate?: (params: T) => boolean; timeoutMs?: number } = {}): Promise<T> {
if (typeof opts === 'function') {

@cubic-dev-ai cubic-dev-ai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The removed positional form with an omitted predicate is not rejected: waitFor(method, undefined, timeoutMs) falls through the function-only guard, defaults to {}, and silently ignores timeoutMs, restoring the 30-second default. This makes JavaScript/snippet callers fail with an unexpectedly long timeout instead of getting the documented migration error; checking for extra arguments (for example arguments.length > 2) would make the old form consistently fail.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/src/cdp/session.ts, line 199:

<comment>The removed positional form with an omitted predicate is not rejected: `waitFor(method, undefined, timeoutMs)` falls through the function-only guard, defaults to `{}`, and silently ignores `timeoutMs`, restoring the 30-second default. This makes JavaScript/snippet callers fail with an unexpectedly long timeout instead of getting the documented migration error; checking for extra arguments (for example `arguments.length > 2`) would make the old form consistently fail.</comment>

<file context>
@@ -188,21 +188,42 @@ export class Session implements Transport {
+   *   await loaded
+   */
+  waitFor<T = unknown>(method: string, opts: { predicate?: (params: T) => boolean; timeoutMs?: number } = {}): Promise<T> {
+    if (typeof opts === 'function') {
+      throw new TypeError('waitFor(method, { predicate, timeoutMs }) — pass the predicate in the options object');
+    }
</file context>
Suggested change
if (typeof opts === 'function') {
if (arguments.length > 2 || typeof opts === 'function') {
Fix with cubic

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.

fix(browser): make waitFor options explicit and race-safe

1 participant