fix(js): serialize dynamic module imports + re-land base href for SPA lazy chunks (#246) (#275)

* 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:
zhangyang-crazy-oneandSGavrl authored and GitHub committed 2026-06-09 10:27:50 +02:00
1 parent 179d9f3369
commit 5225f6d985
1 file changed
+81 -26
+81 -26
View File
@@ -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); }