* fix(js): serialize dynamic module imports + re-land base href for SPA lazy chunks (#246) Add a serialized import queue (__dynScriptQueue + __processDynScriptQueue) in bootstrap.js so that dynamically inserted <script module> tags execute one at a time rather than concurrently. This prevents re-entrant RefCell panics in deno_core's futures_unordered_driver when SPAs (Angular, etc.) append multiple lazy chunks at once. Also re-land dynamic import <base href> resolution (previously reverted as 88af239). The original panic was caused by concurrency, not the URL resolution logic — now safe because imports are serialized. Inline module scripts also route through the same queue for consistency. Fixes #246 Closes #242 (dynamic import base href) * Fix base-href resolution, queue wedge, and double-fired load event - Resolve <base href> to an absolute URL before resolving the script src, and guard the new URL() call. A path-only base like <base href="/"> (the standard Angular form) was passed raw to new URL(src, base), which threw "Invalid URL" out of appendChild and broke the SPAs this change targets. - Wrap the dynamic-script queue drain in try/finally so __dynScriptBusy is always cleared. An unexpected throw used to wedge the queue and silently block every later dynamic script on the page. - Fire load/error through dispatchEvent only. dispatchEvent already invokes the element's onload property handler, so the extra explicit onload() call double-fired it. This also drops the stale enqueue-time callback snapshot and reads handlers live off the element. --------- Co-authored-by: SGavrl <[email protected]>
This commit is contained in:
1 parent
179d9f3369
commit
5225f6d985
1 file changed
+81
-26
Vendored
+81
-26
@@ -59,6 +59,46 @@ 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.
|
||||
let __dynScriptQueue = [];
|
||||
let __dynScriptBusy = false;
|
||||
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.
|
||||
try {
|
||||
while (__dynScriptQueue.length > 0) {
|
||||
const task = __dynScriptQueue.shift();
|
||||
try {
|
||||
if (task.isModule) {
|
||||
await import(task.url);
|
||||
} else {
|
||||
const raw = await Deno.core.ops.op_fetch_url(task.url, "GET", "{}", "", task.pageOrigin, "no-cors");
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed.body) {
|
||||
globalThis.__currentScriptNid = task.nid;
|
||||
try { (0, eval)(parsed.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) {}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
__dynScriptBusy = false;
|
||||
}
|
||||
}
|
||||
function _fpRand(salt) {
|
||||
let h = (_fpSeed ^ (salt || 0)) | 0;
|
||||
h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
|
||||
@@ -363,40 +403,55 @@ class Node {
|
||||
const src = c.getAttribute('src');
|
||||
const prevNid = globalThis.__currentScriptNid;
|
||||
if (src) {
|
||||
const fullUrl = src.startsWith('http') || src.startsWith('data:')
|
||||
? src
|
||||
: new URL(src, globalThis.location?.href || 'http://localhost/').href;
|
||||
const pageOrigin = (function() { try { return new URL(globalThis.location?.href || "about:blank").origin; } catch(e) { return ""; } })();
|
||||
(async () => {
|
||||
// Resolve against <base href> when present, else the document URL.
|
||||
// The base href is resolved to an absolute URL first: a bare path like
|
||||
// <base href="/"> (the common Angular form) is not a valid URL base on
|
||||
// its own and would otherwise throw. Both the base and the final
|
||||
// resolution are guarded so a bad value can never escape appendChild.
|
||||
let baseHref;
|
||||
try {
|
||||
if (isModule) {
|
||||
await import(fullUrl);
|
||||
} else {
|
||||
const raw = await Deno.core.ops.op_fetch_url(fullUrl, "GET", "{}", "", pageOrigin, "no-cors");
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed.body) {
|
||||
globalThis.__currentScriptNid = c._nid;
|
||||
try { (0, eval)(parsed.body); }
|
||||
catch(e) { console.error('Dynamic script error (' + fullUrl + '):', e.message); }
|
||||
finally { globalThis.__currentScriptNid = prevNid || 0; }
|
||||
}
|
||||
}
|
||||
if (typeof c.onload === 'function') try { c.onload(new Event('load')); } catch(e) {}
|
||||
try { c.dispatchEvent(new Event('load')); } catch(e) {}
|
||||
const baseEl = globalThis.document?.querySelector('base[href]');
|
||||
baseHref = baseEl ? baseEl.getAttribute('href') : null;
|
||||
} catch(e) { baseHref = null; }
|
||||
const docUrl = globalThis.location?.href || 'http://localhost/';
|
||||
let baseUrl;
|
||||
try { baseUrl = baseHref ? new URL(baseHref, docUrl).href : docUrl; }
|
||||
catch(e) { baseUrl = docUrl; }
|
||||
let fullUrl;
|
||||
try {
|
||||
fullUrl = src.startsWith('http') || src.startsWith('data:')
|
||||
? src
|
||||
: new URL(src, baseUrl).href;
|
||||
} catch(e) {
|
||||
console.error('Dynamic script fetch error:', e.message);
|
||||
if (typeof c.onerror === 'function') try { c.onerror(e); } catch(ex) {}
|
||||
console.error('Dynamic script URL resolve failed (' + src + '):', e.message);
|
||||
fullUrl = src;
|
||||
}
|
||||
})();
|
||||
const pageOrigin = (function() { try { return new URL(baseUrl).origin; } catch(e) { return ""; } })();
|
||||
// Enqueue — serialized via __processDynScriptQueue to prevent
|
||||
// concurrent import() calls from triggering deno_core RefCell panic.
|
||||
__dynScriptQueue.push({
|
||||
url: fullUrl,
|
||||
isModule,
|
||||
nid: c._nid,
|
||||
prevNid,
|
||||
pageOrigin,
|
||||
dispatchEvent: (ev) => { try { c.dispatchEvent(ev); } catch(e) {} },
|
||||
});
|
||||
__processDynScriptQueue();
|
||||
} else {
|
||||
const code = c.textContent;
|
||||
if (code) {
|
||||
if (isModule) {
|
||||
const dataUrl = 'data:text/javascript;base64,' + btoa(unescape(encodeURIComponent(code)));
|
||||
(async () => {
|
||||
try { await import(dataUrl); }
|
||||
catch(e) { console.error('Dynamic inline module error:', e.message); }
|
||||
})();
|
||||
__dynScriptQueue.push({
|
||||
url: dataUrl,
|
||||
isModule: true,
|
||||
nid: c._nid,
|
||||
prevNid,
|
||||
pageOrigin: "",
|
||||
dispatchEvent: (ev) => { try { c.dispatchEvent(ev); } catch(e) {} },
|
||||
});
|
||||
__processDynScriptQueue();
|
||||
} else {
|
||||
globalThis.__currentScriptNid = c._nid;
|
||||
try { (0, eval)(code); }
|
||||
|
||||
Reference in new issue
Block a user