engine: large compat + reliability pass (#206)

CDP / navigation:
- Route all Page.navigate through process_with_interception so the
  parent dispatcher stays responsive during long navs (fixes
  Target.createTarget timeouts after a handful of newPage calls).
- Default waitUntil to domcontentloaded. Puppeteer and Playwright
  don't pass it server-side and wait client-side via lifecycleEvent,
  so the old Load default blocked nearly every nav on slow sites.
- 30s navigation hard ceiling, OBSCURA_NAV_TIMEOUT_MS to override.
- execute_scripts loop now checks wall-clock deadline per iteration.
- Emit Network.requestWillBeSent before Page.frameNavigated so
  Playwright's FrameManager attaches the navigation response (#189).
- Use loaderId as the Document request id so page.goto() resolves
  to a Response instead of null (#189).
- Fresh executionContextId per navigation, no more 'Cannot find
  context with specified id' after the first nav (#192).
- POST navigation params plumbed through the intercept path.

DOM / Web API surface:
- MutationObserver: subtree filter walks parent chain, fires on
  textContent / CharacterData.data / innerHTML, type filter respected,
  oldValue passed.
- IntersectionObserver: multi-fire (initial + 120/500/1500ms),
  disconnect / unobserve / takeRecords, root / rootMargin / thresholds.
- ResizeObserver: per-target firing from getBoundingClientRect, drop
  the no-op override that was clobbering the real class.
- crypto.subtle.digest: real SHA-1/256/384/512 via op_subtle_digest.
- DOMParser / document.implementation.createHTMLDocument /
  createDocument: detached doc with body, head, documentElement,
  querySelector*, getElementById, getElementsByTagName.
- WebSocket / EventSource / BroadcastChannel: real EventTarget,
  open fires on microtask.
- indexedDB: request, object store, transaction, database with
  async onsuccess and in-memory Map storage.
- getComputedStyle: width/height/top/left from bbox so React
  virtualization libs render items.

Site compatibility went from 3/40 to 175/175 reachable sites in
the sweep. Perf is equal or faster than pristine main on every
site tested (DCL default removes the block on long-tail subresources).

28/28 integration tests pass.
This commit is contained in:
SGavrl authored and GitHub committed 2026-05-30 10:21:50 +02:00
1 parent c9f32aa2f0
commit b98e457428
9 files changed
+979 -295

No files matched your search

+20 -1
View File
@@ -777,7 +777,7 @@ async fn op_sleep(#[number] millis: u64) {
// Records a binding call from page JS. The CDP layer drains this queue
// after every dispatch and emits one `Runtime.bindingCalled` event per
// entry — that's how puppeteer's `page.exposeFunction` callbacks fire.
// entry, that's how puppeteer's `page.exposeFunction` callbacks fire.
#[op2(fast)]
fn op_binding_called(state: &OpState, #[string] name: &str, #[string] payload: &str) {
let gs = state.borrow::<SharedState>().clone();
@@ -785,6 +785,24 @@ fn op_binding_called(state: &OpState, #[string] name: &str, #[string] payload: &
gs.pending_binding_calls.push((name.to_string(), payload.to_string()));
}
/// Real WebCrypto `crypto.subtle.digest`. `algorithm` is the SubtleCrypto
/// algorithm name (`SHA-1` / `SHA-256` / `SHA-384` / `SHA-512`); unknown
/// names fall through to SHA-256 to match the previous JS fallback. Returns
/// the raw digest bytes so the JS shim can hand them back as an ArrayBuffer.
#[op2]
#[buffer]
fn op_subtle_digest(#[string] algorithm: &str, #[buffer] data: &[u8]) -> Vec<u8> {
use sha1::Digest as _;
let alg = algorithm.to_ascii_uppercase();
match alg.as_str() {
"SHA-1" => sha1::Sha1::digest(data).to_vec(),
"SHA-256" => sha2::Sha256::digest(data).to_vec(),
"SHA-384" => sha2::Sha384::digest(data).to_vec(),
"SHA-512" => sha2::Sha512::digest(data).to_vec(),
_ => sha2::Sha256::digest(data).to_vec(),
}
}
pub fn build_extension() -> Extension {
Extension {
name: "obscura_dom",
@@ -797,6 +815,7 @@ pub fn build_extension() -> Extension {
op_navigate(),
op_sleep(),
op_binding_called(),
op_subtle_digest(),
]),
..Default::default()
}