From af5249655c56235b15f1e6e133f2b67e441f5b4d Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 5 May 2026 13:22:58 -0600 Subject: [PATCH] Worker init (#3850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: - Inline the game worker into the main bundle (`?worker&inline`) instead of loading it from the CDN via a same-origin Blob trampoline. - Some users were failing to load the worker — most likely a cross-origin / CDN edge case the trampoline didn't paper over. Inlining serves the worker as a same-origin Blob from the main bundle, bypassing the cross-origin `new Worker(url)` restriction entirely. - Removes `createGameWorker()`, the trampoline script, the `trampoline_error` message type, and the `onTrampolineError` listener in `initialize()`. The 60s init timeout backstop stays. - `cdnBase` is still forwarded in the `init` message — that's for in-worker asset fetches (maps, etc.), unrelated to loading the worker module itself. Tradeoff: the worker bundles all of `src/core`, so the main JS download grows by that amount on every page load (including lobby/settings pages that never start a game). Accepted because load reliability > bundle size here. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/core/worker/WorkerClient.ts | 61 +++---------------------------- src/core/worker/WorkerMessages.ts | 15 +------- 2 files changed, 7 insertions(+), 69 deletions(-) diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index e911ce990..7ce0573ce 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -13,45 +13,10 @@ import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates"; import { ClientID, GameStartInfo, Turn } from "../Schemas"; import { generateID } from "../Util"; import { WorkerMessage } from "./WorkerMessages"; -// ?worker&url returns the worker bundle's URL as a string. We load it via a -// same-origin Blob trampoline because browsers refuse cross-origin -// `new Worker(url)` even with valid CORS+CORP. A Blob URL is same-origin to -// the page so the constructor accepts it, and dynamic `import()` inside the -// Blob IS CORS-checked and can fetch the real worker module from the CDN. -// R2 must serve the worker bundle with `Access-Control-Allow-Origin`. -import workerUrl from "./Worker.worker.ts?worker&url"; - -function createGameWorker(): Worker { - const cdnBase = getCdnBase().replace(/\/+$/, ""); - // Same-origin path (dev, or any deploy without CDN_BASE set): construct the - // worker directly. The Blob trampoline below is only needed for cross-origin - // loads — browsers refuse `new Worker(url)` cross-origin even with valid - // CORS+CORP, and Vite's dev server doesn't serve `?worker&url` URLs as - // regular ES modules so the trampoline's dynamic `import()` would hang. - if (!cdnBase) { - return new Worker(workerUrl, { type: "module" }); - } - const fullUrl = `${cdnBase}${workerUrl}`; - // Buffer-and-replay: the worker's port enables when the trampoline script - // starts, so any messages posted before the imported module attaches its - // `message` handler would dispatch to no listener and be dropped. Capture - // them here, then re-dispatch after the import resolves. - const trampoline = ` -const buffered = []; -const buffer = (e) => buffered.push(e); -self.addEventListener("message", buffer); -import(${JSON.stringify(fullUrl)}).then(() => { - self.removeEventListener("message", buffer); - for (const e of buffered) self.dispatchEvent(new MessageEvent("message", { data: e.data })); -}).catch((e) => self.postMessage({ type: "trampoline_error", message: String((e && e.message) || e) })); -`; - const blobUrl = URL.createObjectURL( - new Blob([trampoline], { type: "application/javascript" }), - ); - const worker = new Worker(blobUrl, { type: "module" }); - URL.revokeObjectURL(blobUrl); - return worker; -} +// Inlined into the main bundle as a same-origin Blob, sidestepping the +// cross-origin `new Worker(url)` restriction that would otherwise apply when +// the worker bundle is served from the CDN. +import GameWorker from "./Worker.worker.ts?worker&inline"; export class WorkerClient { private worker: Worker; @@ -65,7 +30,7 @@ export class WorkerClient { private gameStartInfo: GameStartInfo, private clientID: ClientID | undefined, ) { - this.worker = createGameWorker(); + this.worker = new GameWorker(); this.messageHandlers = new Map(); // Set up global message handler @@ -112,21 +77,8 @@ export class WorkerClient { return new Promise((resolve, reject) => { const messageId = generateID(); - const onTrampolineError = (event: MessageEvent) => { - if (event.data?.type !== "trampoline_error") return; - this.worker.removeEventListener("message", onTrampolineError); - this.messageHandlers.delete(messageId); - reject( - new Error( - `Worker trampoline import failed: ${event.data.message ?? "unknown error"}`, - ), - ); - }; - this.worker.addEventListener("message", onTrampolineError); - this.messageHandlers.set(messageId, (message) => { if (message.type === "initialized") { - this.worker.removeEventListener("message", onTrampolineError); this.isInitialized = true; resolve(); } @@ -140,11 +92,8 @@ export class WorkerClient { cdnBase: getCdnBase(), }); - // Backstop for the worker hanging after a successful import (the - // trampoline_error path handles the cross-origin / CORS load failure). setTimeout(() => { if (!this.isInitialized) { - this.worker.removeEventListener("message", onTrampolineError); this.messageHandlers.delete(messageId); reject(new Error("Worker initialization timeout")); } diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index 75fb6357c..5f4d3990f 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -28,8 +28,7 @@ export type WorkerMessageType = | "attack_clustered_positions" | "attack_clustered_positions_result" | "transport_ship_spawn" - | "transport_ship_spawn_result" - | "trampoline_error"; + | "transport_ship_spawn_result"; // Base interface for all messages interface BaseWorkerMessage { @@ -139,15 +138,6 @@ export interface TransportShipSpawnResultMessage extends BaseWorkerMessage { result: TileRef | false; } -// Posted by the Blob trampoline (see WorkerClient.createGameWorker) when the -// dynamic import of the real worker module fails. The real worker module -// never loaded, so no other message will ever arrive — initialize() must -// reject on this rather than wait out its timeout. -export interface TrampolineErrorMessage extends BaseWorkerMessage { - type: "trampoline_error"; - message: string; -} - // Union types for type safety export type MainThreadMessage = | InitMessage @@ -170,5 +160,4 @@ export type WorkerMessage = | PlayerProfileResultMessage | PlayerBorderTilesResultMessage | AttackClusteredPositionsResultMessage - | TransportShipSpawnResultMessage - | TrampolineErrorMessage; + | TransportShipSpawnResultMessage;