diff --git a/CHANGELOG.md b/CHANGELOG.md index 74a74827..75e1dc69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Editor and UI - The IDE now works in Firefox: a synchronous XHR with a `responseType` hangs in Firefox when the response is 1MB or more, so the build worker hung forever on platforms with a big toolchain package (Game Boy, Atari 8-bit, Apple II, ...). The worker now fetches those packages as ArrayBuffers. A worker that dies no longer leaves the IDE spinning: the error is shown and the build stops. +- Firefox no longer serves a stale build worker or toolchain package after an update: its hard reload (cmd+shift+r) doesn't revalidate requests made from a worker. The IDE now puts a build id in the worker URL, and the worker passes it on to every package and library it fetches. ## [4.0.0] - 2026-10-21 diff --git a/scripts/build.mjs b/scripts/build.mjs index 9cf5a3c3..15cdbe10 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -12,6 +12,9 @@ import esbuild from 'esbuild'; import mdPlugin from './md-loader.mjs'; +// identifies this build; the IDE puts it in the worker URL to defeat stale caches +const define = { __BUILD_ID__: JSON.stringify(Date.now().toString(36)) }; + const configs = { worker: { entryPoints: ['src/worker/workermain.ts'], @@ -54,6 +57,7 @@ for (const name of targets) { } const ctx = await esbuild.context({ ...config, + define, plugins: [mdPlugin], }); if (watch) { diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 8afa3fc4..2fd36cb8 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -47,6 +47,7 @@ import { findListingLocation as findListingLocationPure } from "./search/listing import DOMPurify = require("dompurify"); // external libs (TODO) +declare var __BUILD_ID__: string; // defined by scripts/build.mjs declare var Tour; declare var $: JQueryStatic; // use browser jquery @@ -132,7 +133,10 @@ export function getWorkerParams() { function newWorker(): Worker { // TODO: return new Worker("https://8bitworkshop.com.s3-website-us-east-1.amazonaws.com/dev/gen/worker/bundle.js"); - return new Worker("./gen/worker/bundle.js"); + // The build id in the query string makes Firefox refetch the bundle after a + // rebuild (hard reload doesn't cover workers); the worker reuses it for its + // own fetches via cacheBust(). + return new Worker("./gen/worker/bundle.js" + (typeof __BUILD_ID__ != 'undefined' ? '?v=' + __BUILD_ID__ : '')); } const hasLocalStorage: boolean = function () { diff --git a/src/worker/builder.ts b/src/worker/builder.ts index 443c9af7..2deed28b 100644 --- a/src/worker/builder.ts +++ b/src/worker/builder.ts @@ -10,6 +10,16 @@ import { TOOLS } from "./workertools"; const PSRC = "../../src/"; export const PWORKER = PSRC + "worker/"; +/** + * Append the worker's own query string (the IDE's build id, see newWorker() + * in ide/ui.ts) to a URL the worker fetches. Firefox's hard reload does not + * revalidate requests made from a worker, so without this a stale bundle or + * toolchain package survives cmd+shift+r. Empty in node tests. + */ +export function cacheBust(url: string): string { + return url + ((typeof location != 'undefined' && location.search) || ''); +} + export type FileData = string | Uint8Array; export type FileEntry = { @@ -423,7 +433,7 @@ export function populateExtraFiles(step: BuildStep, fs, extrafiles, processFn?: export function fetchLibraryFile(platform: string, name: string): Uint8Array | null { var xhr = new XMLHttpRequest(); xhr.responseType = 'arraybuffer'; - xhr.open("GET", PWORKER + "lib/" + getBasePlatform(platform) + "/" + name, false); // synchronous request + xhr.open("GET", cacheBust(PWORKER + "lib/" + getBasePlatform(platform) + "/" + name), false); // synchronous request xhr.send(null); return xhr.response && xhr.status == 200 ? new Uint8Array(xhr.response) : null; } diff --git a/src/worker/wasiutils.ts b/src/worker/wasiutils.ts index c06618d8..cd354370 100644 --- a/src/worker/wasiutils.ts +++ b/src/worker/wasiutils.ts @@ -1,7 +1,7 @@ import JSZip from 'jszip'; import { WASIRunner, WASIMemoryFilesystem } from "../common/wasi/wasishim"; import { WorkerError } from "../common/workertypes"; -import { BuildStep, store } from "./builder"; +import { BuildStep, cacheBust, store } from "./builder"; import { hasErrors, re_crlf } from "./listingutils"; import { ensureWasiFilesystem, loadWASMBinary, makeBlob } from "./wasmutils"; @@ -10,7 +10,7 @@ export function loadBlobSync(path: string) { // arraybuffer, not blob: Firefox hangs on a synchronous XHR with // responseType='blob' for responses of 1MB or more (see wasmutils.makeBlob) xhr.responseType = 'arraybuffer'; - xhr.open("GET", path, false); // synchronous request + xhr.open("GET", cacheBust(path), false); // synchronous request xhr.send(null); return makeBlob(xhr.response); } diff --git a/src/worker/wasmutils.ts b/src/worker/wasmutils.ts index fef803c3..23f8f749 100644 --- a/src/worker/wasmutils.ts +++ b/src/worker/wasmutils.ts @@ -2,7 +2,7 @@ // WebAssembly module cache // for Emscripten-compiled functions -import { BuildStep, PWORKER, endtime, starttime, putWorkFile, anyTargetChanged } from "./builder"; +import { BuildStep, PWORKER, cacheBust, endtime, starttime, putWorkFile, anyTargetChanged } from "./builder"; /// export interface EmscriptenModule { @@ -151,12 +151,12 @@ export function getFilesystemSize(name: string): number { export function loadFilesystem(name: string) { var xhr = new XMLHttpRequest(); xhr.responseType = 'arraybuffer'; - xhr.open("GET", PWORKER + "fs/fs" + name + ".data", false); // synchronous request + xhr.open("GET", cacheBust(PWORKER + "fs/fs" + name + ".data"), false); // synchronous request xhr.send(null); fsBlob[name] = makeBlob(xhr.response); xhr = new XMLHttpRequest(); xhr.responseType = 'json'; - xhr.open("GET", PWORKER + "fs/fs" + name + ".js.metadata", false); // synchronous request + xhr.open("GET", cacheBust(PWORKER + "fs/fs" + name + ".js.metadata"), false); // synchronous request xhr.send(null); fsMeta[name] = xhr.response; console.log("Loaded " + name + " filesystem", fsMeta[name].files.length, 'files', fsBlob[name].size, 'bytes'); @@ -165,7 +165,7 @@ export function loadFilesystem(name: string) { var loaded = {}; export function load(modulename: string, debug?: boolean) { if (!loaded[modulename]) { - importScripts(PWORKER + 'asmjs/' + modulename + (debug ? "." + debug + ".js" : ".js")); + importScripts(cacheBust(PWORKER + 'asmjs/' + modulename + (debug ? "." + debug + ".js" : ".js"))); loaded[modulename] = 1; } } @@ -173,7 +173,7 @@ export function loadWASMBinary(modulename: string) { if (!loaded[modulename]) { var xhr = new XMLHttpRequest(); xhr.responseType = 'arraybuffer'; - xhr.open("GET", PWORKER + "wasm/" + modulename + ".wasm", false); // synchronous request + xhr.open("GET", cacheBust(PWORKER + "wasm/" + modulename + ".wasm"), false); // synchronous request xhr.send(null); if (xhr.response) { wasmBlob[modulename] = new Uint8Array(xhr.response); @@ -187,7 +187,7 @@ export function loadWASMBinary(modulename: string) { } export function loadWASM(modulename: string, debug?: boolean) { if (!loaded[modulename]) { - importScripts(PWORKER + "wasm/" + modulename + (debug ? "." + debug + ".js" : ".js")); + importScripts(cacheBust(PWORKER + "wasm/" + modulename + (debug ? "." + debug + ".js" : ".js"))); loadWASMBinary(modulename); } }