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;