From 5de5d07c24c276b7b3f108e76b0b3e91f48f1fd8 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:59:51 +0000 Subject: [PATCH] render: support streamed framework hydration Make attribute maps live, preserve parser script execution state, and expose element scrolling used by React commit callbacks. Connect fallback web streams so React Router and Remix hydration payloads flow through codec transforms. --- crates/obscura-browser/src/page.rs | 16 ++ crates/obscura-js/js/bootstrap.js | 401 +++++++++++++++++++++++------ crates/obscura-js/src/runtime.rs | 144 +++++++++++ 3 files changed, 488 insertions(+), 73 deletions(-) diff --git a/crates/obscura-browser/src/page.rs b/crates/obscura-browser/src/page.rs index 30ef124..e79f7a7 100644 --- a/crates/obscura-browser/src/page.rs +++ b/crates/obscura-browser/src/page.rs @@ -679,6 +679,22 @@ impl Page { None => return, }; + // HTML scripts have an "already started" flag. Mark every + // parser-discovered script before running page code so React/Next + // hydration can move or hoist those nodes without appendChild + // executing them a second time. + if let Some(js) = &mut self.js { + let ids = all_scripts + .iter() + .map(|script| script.nid.to_string()) + .collect::>() + .join(","); + let _ = js.execute_script( + "", + &format!("globalThis.__markParserScripts([{}]);", ids), + ); + } + let mut regular = Vec::new(); let mut deferred = Vec::new(); let mut async_scripts = Vec::new(); diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index b15060d..7146e97 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -16,7 +16,7 @@ '__obscura_objects', '__obscura_oid', '__obscura_ua', '__obscura_platform', '__obscura_ua_platform', '__obscura_ua_platform_version', '__obscura_stealth', '__obscura_markTrusted', - '__obscura_hw', '__obscura_mem', + '__obscura_hw', '__obscura_mem', '__markParserScripts', '__documentReadyState__', '__currentUrl', // internal helpers (var-declared throughout the file) '__processDynScriptQueue', '_decodeDataScriptUrl', '_markNative', '_fpRand', '_fpNoise', @@ -45,7 +45,7 @@ // the later `globalThis.X = X` assignments only update the value. 'Node', 'Element', 'Document', 'DocumentFragment', 'DocumentType', 'Text', 'Comment', 'CDATASection', 'ProcessingInstruction', 'CharacterData', - 'CSSStyleDeclaration', 'DOMTokenList', 'Screen', 'NetworkInformation', + 'CSSStyleDeclaration', 'DOMTokenList', 'NamedNodeMap', 'Screen', 'NetworkInformation', 'MessageChannel', 'MessagePort', 'CustomElementRegistry', 'XMLHttpRequestEventTarget', 'HTMLMediaElement', 'HTMLVideoElement', 'HTMLAudioElement', 'WebGL2RenderingContext', @@ -227,6 +227,14 @@ function _decodeDataScriptUrl(url) { } return new TextDecoder().decode(new Uint8Array(bytes)); } +// A script element executes at most once. Parser-discovered scripts are marked +// before page execution starts, and dynamically inserted scripts are marked +// when they are queued. Framework hydration may move existing