diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index c468791..98ea994 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -693,7 +693,8 @@ const _styleProxy = (decl) => new Proxy(decl, { if (typeof p === "symbol") { t[p] = v; return true; } if (p === "_loaded") { t._loaded = v; return true; } if (p === "cssText") { t.cssText = v; return true; } - if (/^\d+$/.test(p) || p in Object.getPrototypeOf(t)) return true; + if (p in t) { Reflect.set(t, p, v); return true; } + if (/^\d+$/.test(p)) return true; t.setProperty(p, v); return true; }, diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index d1b1106..ea2f291 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -881,10 +881,18 @@ impl ObscuraJsRuntime { } pub async fn run_event_loop(&mut self) -> Result<(), String> { - self.runtime + // A browser performs a microtask checkpoint at the end of each task. + // deno_core's event loop may return immediately when no async op is + // pending, leaving an already-resolved Promise continuation stranded + // (document.fonts.load(...).then(...), framework post-render hooks, + // and hydration follow-ups all rely on this boundary). + self.runtime.v8_isolate().perform_microtask_checkpoint(); + let result = self.runtime .run_event_loop(deno_core::PollEventLoopOptions::default()) .await - .map_err(|e| format!("Event loop error: {}", e)) + .map_err(|e| format!("Event loop error: {}", e)); + self.runtime.v8_isolate().perform_microtask_checkpoint(); + result } /// Whether the serialized dynamic-script queue is still fetching or @@ -1845,6 +1853,24 @@ mod tests { ); } + #[tokio::test(flavor = "current_thread")] + async fn idle_event_loop_flushes_resolved_promise_continuations() { + let mut rt = setup_runtime("