fix: spawn CDN pthread workers via a same-origin blob trampoline
Closes the doc-23 §7 KNOWN GAP that killed the editor wherever the wasm is CDN-served (staging/prod platform): emscripten 6 spawns pthread workers from _scriptName — the glue's absolute CDN URL — and new Worker(<cross-origin>) is a SecurityError, observed on staging as 'Failed to construct Worker' right after instantiation and an editor that never renders. No runtime hook exists post-mainScriptUrlOrBlob, so wrap window.Worker and redirect EXACTLY the glue-URL construction to the (formerly dormant) pthreadWorkerScript blob that importScripts() the glue — blob workers inherit the page origin, and the CDN already sends the CORP/ACAO the page's COEP requires. ?trace= now reaches pthread realms through the same blob, same-origin included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
This commit is contained in:
parent
3e53ac37f4
commit
5191a63fd8
2 changed files with 147 additions and 9 deletions
|
|
@ -1,5 +1,10 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { hasWritableLib } from "./boot";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
hasWritableLib,
|
||||
installPthreadWorkerRedirect,
|
||||
pthreadWorkerScript,
|
||||
resetWorkerRedirectForTest,
|
||||
} from "./boot";
|
||||
import type { LibInfo } from "./libs/source";
|
||||
|
||||
const lib = (type?: string): LibInfo => ({ id: `id-${type}`, name: `n-${type}`, type });
|
||||
|
|
@ -22,3 +27,85 @@ describe("hasWritableLib", () => {
|
|||
expect(hasWritableLib([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Cross-origin (CDN) pthread spawn fix — doc-23 §7 KNOWN GAP. Emscripten
|
||||
* spawns pthread workers from the glue's absolute URL; when that URL is the
|
||||
* CDN, `new Worker(...)` is a SecurityError and the editor dies right after
|
||||
* instantiation (observed on staging). The redirect substitutes a same-origin
|
||||
* blob that importScripts() the glue — for EXACTLY that URL, nothing else.
|
||||
*/
|
||||
describe("installPthreadWorkerRedirect", () => {
|
||||
class FakeWorker {
|
||||
constructor(
|
||||
public scriptURL: string | URL,
|
||||
public opts?: WorkerOptions,
|
||||
) {}
|
||||
}
|
||||
const PAGE = "https://editor.example.test/some/route";
|
||||
const CDN = "https://cdn.example.test/wasm/kicad_editor/rev";
|
||||
|
||||
function stubWindow(): { win: { location: { href: string; origin: string }; Worker: unknown } } {
|
||||
const win = {
|
||||
location: { href: PAGE, origin: "https://editor.example.test" },
|
||||
Worker: FakeWorker as unknown,
|
||||
};
|
||||
vi.stubGlobal("window", win);
|
||||
return { win };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
resetWorkerRedirectForTest();
|
||||
});
|
||||
|
||||
it("same-origin base without trace installs nothing", () => {
|
||||
const { win } = stubWindow();
|
||||
installPthreadWorkerRedirect("/wasm/kicad_editor/rev", "kicad_editor");
|
||||
expect(win.Worker).toBe(FakeWorker);
|
||||
});
|
||||
|
||||
it("cross-origin base: the glue URL is redirected to a same-origin blob; everything else passes through", () => {
|
||||
const { win } = stubWindow();
|
||||
installPthreadWorkerRedirect(CDN, "kicad_editor");
|
||||
expect(win.Worker).not.toBe(FakeWorker);
|
||||
const W = win.Worker as new (u: string | URL, o?: WorkerOptions) => FakeWorker;
|
||||
|
||||
// Exactly what emscripten does: new Worker(_scriptName) with the glue URL.
|
||||
const pthread = new W(`${CDN}/kicad_editor.js`, { name: "em-pthread" });
|
||||
expect(String(pthread.scriptURL).startsWith("blob:")).toBe(true);
|
||||
expect(pthread.opts).toEqual({ name: "em-pthread" });
|
||||
|
||||
// Unrelated workers (ngspice/occ services…) are untouched.
|
||||
const other = new W("/assets/other-worker.js");
|
||||
expect(other.scriptURL).toBe("/assets/other-worker.js");
|
||||
});
|
||||
|
||||
it("?trace= rides the blob even same-origin (worker realms need the env)", () => {
|
||||
const { win } = stubWindow();
|
||||
installPthreadWorkerRedirect(
|
||||
"/wasm/kicad_editor/rev",
|
||||
"kicad_editor",
|
||||
"KI_TRACE_SYM_CHOOSER",
|
||||
);
|
||||
const W = win.Worker as new (u: string | URL) => FakeWorker;
|
||||
const pthread = new W(
|
||||
"https://editor.example.test/wasm/kicad_editor/rev/kicad_editor.js",
|
||||
);
|
||||
expect(String(pthread.scriptURL).startsWith("blob:")).toBe(true);
|
||||
});
|
||||
|
||||
it("the blob trampoline importScripts the absolute glue URL", async () => {
|
||||
stubWindow();
|
||||
const script = pthreadWorkerScript(CDN, "kicad_editor");
|
||||
expect(script).toBeInstanceOf(Blob);
|
||||
const text = await (script as Blob).text();
|
||||
expect(text).toBe(
|
||||
`importScripts(${JSON.stringify(`${CDN}/kicad_editor.js`)});`,
|
||||
);
|
||||
// Same-origin without trace stays a plain relative URL (no blob detour).
|
||||
expect(pthreadWorkerScript("/wasm/rev", "kicad_editor")).toBe(
|
||||
"/wasm/rev/kicad_editor.js",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -154,12 +154,11 @@ function loadScript(src: string): Promise<void> {
|
|||
}
|
||||
|
||||
/**
|
||||
* DORMANT — nothing consumes this today. It built the value once passed as
|
||||
* `Module.mainScriptUrlOrBlob`, which emscripten 6 ignores: pthread child
|
||||
* workers spawn from `_scriptName` instead, which is same-origin-only. Kept
|
||||
* because the cross-origin (CDN) pthread fix — a KNOWN GAP, see
|
||||
* docs/features/async/23-jspi-runtime.md — will need exactly this plumbing:
|
||||
* a SAME-ORIGIN `blob:` worker that `importScripts()` the cross-origin glue.
|
||||
* The pthread worker-script substitute consumed by
|
||||
* {@link installPthreadWorkerRedirect} (formerly the dormant
|
||||
* `mainScriptUrlOrBlob` plumbing — emscripten 6 dropped that option, closing
|
||||
* the doc-23 §7 KNOWN GAP required this instead): a SAME-ORIGIN `blob:`
|
||||
* worker that `importScripts()` the cross-origin glue.
|
||||
* `new Worker(<cross-origin URL>)` is a SecurityError, but a `blob:` URL
|
||||
* inherits the page origin (legal), and a classic worker's `importScripts` MAY
|
||||
* load a cross-origin script when the CDN sends
|
||||
|
|
@ -185,7 +184,7 @@ function isWasmDiagnostic(line: string): boolean {
|
|||
return line.startsWith("[wx-dispatch]") || line.startsWith("[wx-timer]");
|
||||
}
|
||||
|
||||
function pthreadWorkerScript(
|
||||
export function pthreadWorkerScript(
|
||||
base: string,
|
||||
bundle: Bundle,
|
||||
traceMask?: string | null,
|
||||
|
|
@ -211,6 +210,55 @@ function pthreadWorkerScript(
|
|||
});
|
||||
}
|
||||
|
||||
let workerRedirectInstalled = false;
|
||||
|
||||
/** Test-only: clear the install latch (one boot per page in production). */
|
||||
export function resetWorkerRedirectForTest(): void {
|
||||
workerRedirectInstalled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-origin (CDN) pthread spawn fix — closes the doc-23 §7 KNOWN GAP that
|
||||
* broke the editor wherever the wasm is CDN-served (staging/prod platform):
|
||||
* emscripten 6 spawns every pthread worker from `_scriptName` (the glue's
|
||||
* absolute URL, captured at script execution) and offers no override, and
|
||||
* `new Worker(<cross-origin URL>)` is a SecurityError — observed as the tool
|
||||
* dying right after instantiation with zero pthreads spawned.
|
||||
*
|
||||
* The runtime gives us no hook, so take the one seam that exists: wrap
|
||||
* `window.Worker` and redirect EXACTLY the glue-URL construction to the
|
||||
* same-origin substitute from {@link pthreadWorkerScript}. Everything else
|
||||
* (ngspice/occ service workers, third-party code) passes through untouched.
|
||||
* Also the only way `?trace=` reaches pthread realms (the blob seeds
|
||||
* `__KICAD_TRACE__` before importScripts), so it installs for the trace case
|
||||
* even same-origin. One boot per page (see `booted`), so never uninstalled.
|
||||
*/
|
||||
export function installPthreadWorkerRedirect(
|
||||
base: string,
|
||||
bundle: Bundle,
|
||||
traceMask?: string | null,
|
||||
): void {
|
||||
const glueHref = new URL(`${base}/${bundle}.js`, window.location.href).href;
|
||||
const crossOrigin = new URL(glueHref).origin !== window.location.origin;
|
||||
if ((!crossOrigin && !traceMask) || workerRedirectInstalled) return;
|
||||
workerRedirectInstalled = true;
|
||||
const script = pthreadWorkerScript(base, bundle, traceMask);
|
||||
const substitute =
|
||||
typeof script === "string" ? script : URL.createObjectURL(script);
|
||||
const NativeWorker = window.Worker;
|
||||
window.Worker = new Proxy(NativeWorker, {
|
||||
construct(target, args: [string | URL, WorkerOptions?]) {
|
||||
let href: string;
|
||||
try {
|
||||
href = new URL(String(args[0]), window.location.href).href;
|
||||
} catch {
|
||||
href = String(args[0]);
|
||||
}
|
||||
return new target(href === glueHref ? substitute : args[0], args[1]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the tool's `.wasm` ourselves so we can report download progress (the big,
|
||||
* slow asset — 175–338 MB). Emscripten otherwise fetches it internally with no
|
||||
|
|
@ -676,6 +724,9 @@ async function doBoot(opts: BootOptions): Promise<void> {
|
|||
// tool aborts at startup with "wxDomCreateControl is not defined".
|
||||
// <tool>.js — the tool glue, whose execution captures currentScript.src as
|
||||
// Emscripten's _scriptName.
|
||||
// BEFORE the glue executes: pthread workers must spawn from a same-origin
|
||||
// script when `base` is the cross-origin CDN (and carry the trace env).
|
||||
installPthreadWorkerRedirect(base, bundle, traceMask);
|
||||
await loadScript(`${base}/wx.js`);
|
||||
await loadScript(`${base}/wx-dom.js`);
|
||||
await loadScript(`${base}/${bundle}.js`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue