Improve float, gradient, and script rendering parity
This commit is contained in:
1 parent
0562fb4921
commit
03f506b01a
7 files changed
+1005
-117
No files matched your search
Vendored
+85
-44
@@ -189,13 +189,20 @@ if (_origStackDesc && _origStackDesc.get) {
|
||||
}
|
||||
|
||||
let _fpSeed = 0;
|
||||
// Dynamic script import queue — serializes concurrent import() calls
|
||||
// to prevent re-entrant RefCell panic in deno_core's futures_unordered_driver
|
||||
// when SPAs dynamically insert multiple <script module> tags at once.
|
||||
// Dynamic module/in-order script queue. Module evaluation remains serialized
|
||||
// to prevent a re-entrant RefCell panic in deno_core's
|
||||
// futures_unordered_driver when SPAs insert multiple <script type=module>
|
||||
// elements at once. Ordinary dynamically inserted classic scripts are async
|
||||
// by default, so their fetches run independently and execute when ready just
|
||||
// like browser ScriptRunner tasks; serializing those fetches made unrelated
|
||||
// analytics/widgets form one long load-blocking waterfall.
|
||||
let __dynScriptQueue = [];
|
||||
let __dynScriptBusy = false;
|
||||
let __dynClassicPending = 0;
|
||||
Object.defineProperty(globalThis, '__obscura_hasPendingDynamicScripts', {
|
||||
value: function() { return __dynScriptBusy || __dynScriptQueue.length > 0; },
|
||||
value: function() {
|
||||
return __dynClassicPending > 0 || __dynScriptBusy || __dynScriptQueue.length > 0;
|
||||
},
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
@@ -251,50 +258,72 @@ function _decodeDataScriptUrl(url) {
|
||||
globalThis.__markParserScripts = function(nids) {
|
||||
for (const nid of nids || []) Deno.core.ops.op_script_mark_started(+nid);
|
||||
};
|
||||
async function __runDynScriptTask(task) {
|
||||
try {
|
||||
if (task.isModule) {
|
||||
await import(task.url);
|
||||
} else {
|
||||
let body;
|
||||
if (task.url.startsWith('data:')) {
|
||||
body = _decodeDataScriptUrl(task.url);
|
||||
} else {
|
||||
const raw = await Deno.core.ops.op_fetch_url(
|
||||
task.url, "GET", "{}", "", task.pageOrigin, "no-cors", "same-origin"
|
||||
);
|
||||
const parsed = JSON.parse(raw);
|
||||
// The HTML script-fetch algorithm treats an unsuccessful HTTP
|
||||
// response as a network error. Evaluating its response body is both
|
||||
// observably unlike browsers and dangerous: JSON error payloads and
|
||||
// diagnostic HTML must never become script source.
|
||||
if (!(parsed.status >= 200 && parsed.status <= 299)) {
|
||||
throw new Error('HTTP ' + (parsed.status || 0));
|
||||
}
|
||||
body = parsed.body;
|
||||
}
|
||||
if (body) {
|
||||
// A fetched async script is executed by a ScriptRunner task, not by
|
||||
// the fetch promise's microtask continuation. Besides matching event
|
||||
// loop ordering, this prevents a batch of concurrently completed
|
||||
// third-party scripts from being charged to (and pinning) whichever
|
||||
// parser script happened to trigger the microtask checkpoint.
|
||||
await new Promise(resolve => {
|
||||
const execute = () => {
|
||||
globalThis.__currentScriptNid = task.nid;
|
||||
try { (0, eval)(body); }
|
||||
catch(e) { console.error('Dynamic script error (' + task.url + '):', e.message); }
|
||||
finally { globalThis.__currentScriptNid = task.prevNid || 0; }
|
||||
resolve();
|
||||
};
|
||||
if (_scheduleAfter(0, execute) === undefined) execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
// Fire load via dispatchEvent only: it invokes the element's onload
|
||||
// property handler and any addEventListener('load') listeners, read live
|
||||
// off the element. Calling onload separately would double-fire it.
|
||||
try { task.dispatchEvent(new Event('load')); } catch(e) {}
|
||||
} catch(e) {
|
||||
console.error('Dynamic script fetch error:', e.message);
|
||||
try { task.dispatchEvent(new Event('error')); } catch(ex) {}
|
||||
}
|
||||
}
|
||||
async function __runAsyncClassicScript(task) {
|
||||
__dynClassicPending++;
|
||||
try {
|
||||
await __runDynScriptTask(task);
|
||||
} finally {
|
||||
__dynClassicPending--;
|
||||
}
|
||||
}
|
||||
async function __processDynScriptQueue() {
|
||||
if (__dynScriptBusy) return;
|
||||
__dynScriptBusy = true;
|
||||
// try/finally so the busy flag is always cleared even if a task throws
|
||||
// outside its own guard; otherwise the queue would wedge and silently
|
||||
// block every later dynamic script on the page.
|
||||
// block every later module or explicitly in-order script on the page.
|
||||
try {
|
||||
while (__dynScriptQueue.length > 0) {
|
||||
const task = __dynScriptQueue.shift();
|
||||
try {
|
||||
if (task.isModule) {
|
||||
await import(task.url);
|
||||
} else {
|
||||
let body;
|
||||
if (task.url.startsWith('data:')) {
|
||||
body = _decodeDataScriptUrl(task.url);
|
||||
} else {
|
||||
const raw = await Deno.core.ops.op_fetch_url(task.url, "GET", "{}", "", task.pageOrigin, "no-cors", "same-origin");
|
||||
const parsed = JSON.parse(raw);
|
||||
// The HTML script-fetch algorithm treats an unsuccessful HTTP
|
||||
// response as a network error. Evaluating its response body is both
|
||||
// observably unlike browsers and dangerous: JSON error payloads
|
||||
// become JavaScript source, while servers can return diagnostic
|
||||
// HTML that mutates global state before throwing.
|
||||
if (!(parsed.status >= 200 && parsed.status <= 299)) {
|
||||
throw new Error('HTTP ' + (parsed.status || 0));
|
||||
}
|
||||
body = parsed.body;
|
||||
}
|
||||
if (body) {
|
||||
globalThis.__currentScriptNid = task.nid;
|
||||
try { (0, eval)(body); }
|
||||
catch(e) { console.error('Dynamic script error (' + task.url + '):', e.message); }
|
||||
finally { globalThis.__currentScriptNid = task.prevNid || 0; }
|
||||
}
|
||||
}
|
||||
// Fire load via dispatchEvent only: it invokes the element's onload
|
||||
// property handler and any addEventListener('load') listeners, read
|
||||
// live off the element. Calling onload separately would double-fire it.
|
||||
try { task.dispatchEvent(new Event('load')); } catch(e) {}
|
||||
} catch(e) {
|
||||
console.error('Dynamic script fetch error:', e.message);
|
||||
try { task.dispatchEvent(new Event('error')); } catch(ex) {}
|
||||
}
|
||||
await __runDynScriptTask(__dynScriptQueue.shift());
|
||||
}
|
||||
} finally {
|
||||
__dynScriptBusy = false;
|
||||
@@ -1290,15 +1319,27 @@ function __prepareInsertedScript(script) {
|
||||
fullUrl = src;
|
||||
}
|
||||
const pageOrigin = (function() { try { return new URL(baseUrl).origin; } catch(e) { return ""; } })();
|
||||
__dynScriptQueue.push({
|
||||
const task = {
|
||||
url: fullUrl,
|
||||
isModule,
|
||||
nid: script._nid,
|
||||
prevNid,
|
||||
pageOrigin,
|
||||
dispatchEvent: (ev) => { try { script.dispatchEvent(ev); } catch(e) {} },
|
||||
});
|
||||
__processDynScriptQueue();
|
||||
};
|
||||
// A non-parser-inserted classic script is force-async unless script code
|
||||
// explicitly assigned `.async = false`. Keep that opt-out in insertion
|
||||
// order; default/async=true scripts fetch concurrently and execute as soon
|
||||
// as each response is ready.
|
||||
const explicitlyInOrder = !isModule
|
||||
&& Object.prototype.hasOwnProperty.call(script, 'async')
|
||||
&& script.async === false;
|
||||
if (!isModule && !explicitlyInOrder) {
|
||||
__runAsyncClassicScript(task);
|
||||
} else {
|
||||
__dynScriptQueue.push(task);
|
||||
__processDynScriptQueue();
|
||||
}
|
||||
} else if (isModule) {
|
||||
const dataUrl = 'data:text/javascript;base64,' + btoa(unescape(encodeURIComponent(code)));
|
||||
__dynScriptQueue.push({
|
||||
|
||||
@@ -10730,6 +10730,64 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn dynamic_classic_scripts_are_async_by_default_but_honor_async_false_order() {
|
||||
let mut rt = setup_runtime("<html><head></head><body></body></html>");
|
||||
let result = rt
|
||||
.call_function_on_for_cdp(
|
||||
r#"async () => {
|
||||
const originalFetchOp = Deno.core.ops.op_fetch_url;
|
||||
const runPair = async (explicitlyInOrder) => {
|
||||
globalThis.__dynamicOrder = [];
|
||||
Deno.core.ops.op_fetch_url = (url) => new Promise(resolve => {
|
||||
const slow = url.includes("slow");
|
||||
setTimeout(() => resolve(JSON.stringify({
|
||||
status: 200,
|
||||
headers: {"content-type": "text/javascript"},
|
||||
body: `globalThis.__dynamicOrder.push("${slow ? "slow" : "fast"}")`,
|
||||
url,
|
||||
})), slow ? 30 : 1);
|
||||
});
|
||||
const load = name => new Promise(resolve => {
|
||||
const script = document.createElement("script");
|
||||
if (explicitlyInOrder) script.async = false;
|
||||
script.src = `/${name}.js`;
|
||||
script.onload = resolve;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
await Promise.all([load("slow"), load("fast")]);
|
||||
return globalThis.__dynamicOrder.slice();
|
||||
};
|
||||
try {
|
||||
const asyncOrder = await runPair(false);
|
||||
const inOrder = await runPair(true);
|
||||
return {
|
||||
asyncOrder,
|
||||
inOrder,
|
||||
pending: globalThis.__obscura_hasPendingDynamicScripts(),
|
||||
};
|
||||
} finally {
|
||||
Deno.core.ops.op_fetch_url = originalFetchOp;
|
||||
}
|
||||
}"#,
|
||||
None,
|
||||
&[],
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result.value.unwrap(),
|
||||
serde_json::json!({
|
||||
"asyncOrder": ["fast", "slow"],
|
||||
"inOrder": ["slow", "fast"],
|
||||
"pending": false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn test_response_array_buffer_preserves_typed_array_view() {
|
||||
let mut rt = setup_runtime("<html><body></body></html>");
|
||||
|
||||
Reference in new issue
Block a user