mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:20:46 +00:00
Worker init (#3850)
## 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
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user