Skip to content

Add hybrid script cancellation for browserjs() with cooperative + V8 termination#1

Open
hjanuschka wants to merge 1 commit intomainfrom
hja
Open

Add hybrid script cancellation for browserjs() with cooperative + V8 termination#1
hjanuschka wants to merge 1 commit intomainfrom
hja

Conversation

@hjanuschka
Copy link
Copy Markdown
Collaborator

@hjanuschka hjanuschka commented Nov 20, 2025

Summary

Implements reliable script cancellation for browserjs() using a hybrid dual-strategy approach:

Technical Details

Why Cooperative Cancellation Works

By wrapping the Promise constructor, we inject cancellation checks into every promise creation:

window.Promise = function (executor) {
  return new OriginalPromise((resolve, reject) => {
    if (window.__sitegeist_cancelled) {
      reject(new Error("Script execution was cancelled"));
      return;
    }
    
    const wrappedResolve = (value) => {
      if (window.__sitegeist_cancelled) {
        reject(new Error("Script execution was cancelled"));
      } else {
        resolve(value);
      }
    };
    
    executor(wrappedResolve, reject);
  });
};

This means any await becomes a cancellation checkpoint automatically!


Related Work


Migration Guide

No migration needed! Existing code automatically benefits from cooperative cancellation. For long synchronous loops, optionally add explicit yields:

// Before (still works, but better with yield):
await browserjs(async () => {
  for (let i = 0; i < 10000; i++) {
    // Synchronous work
    processItem(i);
  }
});

// After (recommended for loops > 100 iterations):
await browserjs(async () => {
  for (let i = 0; i < 10000; i++) {
    processItem(i);
    
    // Add yield every 50 iterations
    if (i % 50 === 0) await __sitegeist_yield();
  }
});

…termination

This implements reliable script cancellation for browserjs() using a dual-strategy approach:
1. Cooperative cancellation via Promise wrapping (works on all Chrome versions)
2. V8 termination via chrome.userScripts.terminate() (Chrome 138+ only)

## Key Changes

### Cooperative Cancellation (Primary Method)
- Wraps native Promise constructor to inject cancellation checks
- Sets window.__sitegeist_cancelled flag when abort is triggered
- Every await statement becomes a cancellation checkpoint automatically
- Works 100% reliably on all Chrome versions

### V8 Termination (Backup Method - Chrome 138+)
- Removed setTimeout wrapper that was causing execution IDs to become stale
- Scripts now stay tracked in Chromium's executions_by_id_ map during execution
- Allows chrome.userScripts.terminate() to find and interrupt executions
- Guarded with feature detection for backward compatibility

### Implementation Details
- src/tools/repl/userscripts-helpers.ts:
  * Added cancellation flag injection
  * Wrapped Promise constructor to check flag at every await
  * Added __sitegeist_yield() helper for explicit cancellation points
  * Removed setTimeout wrapper to keep execution tracked
  * Reduced timeout from 120s to 30s

- src/tools/repl/runtime-providers.ts:
  * Implemented hybrid abort handler (cooperative + V8 termination)
  * Added feature detection for chrome.userScripts.terminate()
  * Only uses V8 termination on Chrome 138+
  * Improved logging to show cancellation mode

- src/prompts/prompts.ts:
  * Added documentation about script cancellation
  * Documented __sitegeist_yield() usage for long loops
  * Provided examples of proper yield usage

- SCRIPT_CANCELLATION.md:
  * Comprehensive technical documentation
  * Analysis of Chrome 138+ terminate API limitations
  * Explanation of cooperative vs V8 termination approaches

## Benefits
- ✅ Reliable cancellation on all Chrome versions
- ✅ No "execution_id not found in map" errors
- ✅ Scripts stop immediately at next await
- ✅ Backward compatible with Chrome < 138
- ✅ Hybrid approach provides multiple cancellation layers

## Testing
Tested on Chrome 138+ with scripts containing loops and async operations.
Cancellation works immediately when user clicks stop button.

Related to Chromium CL 7110745 (per-execution termination API)
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.

1 participant