diff --git a/system/package.json b/system/package.json index 397e9614a4..fd57abff01 100644 --- a/system/package.json +++ b/system/package.json @@ -22,9 +22,10 @@ "stripe-dev-ticket-micro": "stripe listen --forward-to https://localhost:8888/api/ticket/sotce --api-key $SOTCE_STRIPE_API_KEY", "media-server": "http-server public -p 8111 --cors -c-1 -i -g -b -s", "media-server-caddy-old": "caddy file-server --root public --listen :8111 --browse | jq .", - "media-server-caddy": "caddy run --config Caddyfile", - "media-server-nginx": "sudo nginx -s stop; sudo nginx -c $(realpath ./nginx.conf) && sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log", - "codespaces-dev-debug": "kill-port 8889 8888 8080 8000 8111 3333; netlify dev --debug", + "media-server-caddy": "caddy run --config Caddyfile", + "media-server-nginx": "sudo nginx -s stop; sudo nginx -c $(realpath ./nginx.conf) && sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log", + "build:disk-worker": "node scripts/build-disk-worker.mjs", + "codespaces-dev-debug": "kill-port 8889 8888 8080 8000 8111 3333; netlify dev --debug", "codespaces-dev-public": "kill-port 8080 8888 8889 8000 8111 3333; netlify dev --live", "local-dev-netlify": "http-server public -p 8080 --cors -c-1 -i -s --P https://127.0.0.1:443?", "local-dev-ssl": "http-server public -S -C ../ssl-dev/localhost.pem -K ../ssl-dev/localhost-key.pem -p 443 --cors -c-1", diff --git a/system/public/aesthetic.computer/bios.mjs b/system/public/aesthetic.computer/bios.mjs index 8251eaec54..c13deffa75 100644 --- a/system/public/aesthetic.computer/bios.mjs +++ b/system/public/aesthetic.computer/bios.mjs @@ -831,10 +831,40 @@ function consumeDiskSends(send) { USB.initialize(); // 💾 Boot the system and load a disk. -async function boot(parsed, bpm = 60, resolution, debug) { - const bootStartTime = performance.now(); - perf.markBootStart(); - headers(); // Print console headers with auto-detected theme. +async function boot(parsed, bpm = 60, resolution, debug) { + const bootStartTime = performance.now(); + perf.markBootStart(); + headers(); // Print console headers with auto-detected theme. + + const workerBundleParam = new URLSearchParams(window.location.search).get("workerbundle"); + const workerBundleRequested = + !window.acPACK_MODE && + (workerBundleParam === "1" || workerBundleParam === "true"); + const workerBundleState = (window.acWORKER_BUNDLE = { + requested: workerBundleRequested, + active: false, + ready: false, + filename: null, + fallback: null, + }); + const workerBundlePathPromise = workerBundleRequested + ? fetch("/aesthetic.computer/lib/disk-worker-manifest.json", { + cache: "no-cache", + }) + .then(async (response) => { + if (!response.ok) throw new Error(`manifest HTTP ${response.status}`); + const manifest = await response.json(); + if (!/^disk\.worker\.[a-f0-9]{12}\.mjs$/.test(manifest?.filename || "")) { + throw new Error("invalid worker bundle manifest"); + } + workerBundleState.filename = manifest.filename; + return `/aesthetic.computer/lib/${manifest.filename}`; + }) + .catch((error) => { + workerBundleState.fallback = `manifest: ${error.message}`; + return null; + }) + : Promise.resolve(null); // Expose Loop control to window for boot.mjs // Track pause state for kidlisp console snapshots @@ -4336,11 +4366,17 @@ async function boot(parsed, bpm = 60, resolution, debug) { //const worker = new Worker("./aesthetic.computer/lib/disk.js", { // type: "module", //}); - const fullPath = - (window.acPACK_MODE ? "./aesthetic.computer/lib/disk.mjs" : "/aesthetic.computer/lib/disk.mjs") + - window.location.search + - "#" + - Date.now(); // bust the cache. This prevents an error related to Safari loading workers from memory. + const standardWorkerPath = + (window.acPACK_MODE ? "./aesthetic.computer/lib/disk.mjs" : "/aesthetic.computer/lib/disk.mjs") + + window.location.search + + "#" + + Date.now(); // bust the cache. This prevents an error related to Safari loading workers from memory. + const workerBundlePath = await workerBundlePathPromise; + let usingBundledWorker = Boolean(workerBundlePath); + let activeWorkerPath = workerBundlePath + ? workerBundlePath + window.location.search + "#" + Date.now() + : standardWorkerPath; + workerBundleState.active = usingBundledWorker; const sandboxed = (window.origin === "null" || !window.origin || window.acPACK_MODE || window.acSPIDER) && !window.acVSCODE; @@ -4504,9 +4540,10 @@ async function boot(parsed, bpm = 60, resolution, debug) { perf.markBoot("worker-create-start"); let workerFailed = false; // Guard to prevent double-initialization - let workerReady = false; // Track if worker has responded - let workerInitialized = false; // Guard to prevent sending firstMessage multiple times - let retryCount = 0; + let workerReady = false; // Track if worker has responded + let workerInitialized = false; // Guard to prevent sending firstMessage multiple times + let retryCount = 0; + let activeWorker = null; const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; // On localhost, fail fast (1 retry) since HTTP proxy is flaky - noWorker fallback works fine // In production, retry more since network issues are usually transient @@ -4514,17 +4551,28 @@ async function boot(parsed, bpm = 60, resolution, debug) { // Note: Can't use blob URLs for workers because dynamic imports inside the worker // (like loading pieces) would resolve relative to blob: origin which fails. - // Workers need to load via HTTP so their dynamic imports work correctly. - - const createWorker = () => { - const worker = new Worker(new URL(fullPath, window.location.href), { - type: "module", - }); - - // Rewire things a bit if workers with modules are not supported (Firefox). - worker.onerror = async (err) => { - if (workerFailed) return; // Already handling fallback - if (workerInitialized) { + // Workers need to load via HTTP so their dynamic imports work correctly. + + const createWorker = () => { + const worker = new Worker(new URL(activeWorkerPath, window.location.href), { + type: "module", + }); + activeWorker = worker; + + // Rewire things a bit if workers with modules are not supported (Firefox). + worker.onerror = async (err) => { + if (workerFailed) return; // Already handling fallback + if (usingBundledWorker && !workerInitialized) { + worker.terminate(); + usingBundledWorker = false; + workerBundleState.active = false; + workerBundleState.fallback = `worker error: ${err.message || "unknown"}`; + activeWorkerPath = standardWorkerPath; + retryCount = 0; + createWorker(); + return; + } + if (workerInitialized) { // Worker already started successfully - this is a late error, don't double-init console.warn("🟡 Worker error after init:", err.message || "(no message)"); return; @@ -4605,9 +4653,10 @@ async function boot(parsed, bpm = 60, resolution, debug) { // Handle worker-ready signal from disk.mjs if (e.data?.type === "worker-ready") { if (workerInitialized) return; // Already initialized (shouldn't happen) - workerInitialized = true; - workerReady = true; - perf.markBoot("worker-connected"); + workerInitialized = true; + workerReady = true; + workerBundleState.ready = usingBundledWorker; + perf.markBoot("worker-connected"); // Notify parent that worker is connected const workerConnectTime = performance.now(); @@ -4633,10 +4682,23 @@ async function boot(parsed, bpm = 60, resolution, debug) { } }; - // Start the first worker attempt - createWorker(); - - // Timeout: fall back to noWorker mode if worker never responds + // Start the first worker attempt + createWorker(); + + if (usingBundledWorker) { + setTimeout(() => { + if (workerReady || workerFailed || !usingBundledWorker) return; + activeWorker?.terminate(); + usingBundledWorker = false; + workerBundleState.active = false; + workerBundleState.fallback = "worker ready timeout"; + activeWorkerPath = standardWorkerPath; + retryCount = 0; + createWorker(); + }, 4000); + } + + // Timeout: fall back to noWorker mode if worker never responds const workerTimeoutMs = isLocalhost ? 3000 : 10000; setTimeout(async () => { if (workerReady || workerFailed) return; // Already resolved diff --git a/system/public/aesthetic.computer/lib/disk.mjs b/system/public/aesthetic.computer/lib/disk.mjs index 948efbf20a..1dadda4396 100644 --- a/system/public/aesthetic.computer/lib/disk.mjs +++ b/system/public/aesthetic.computer/lib/disk.mjs @@ -321,7 +321,8 @@ async function clearFontCaches() { // 3. Force reimport of fonts.mjs by cache-busting try { const timestamp = Date.now(); - const fontsModule = await import(`../disks/common/fonts.mjs?t=${timestamp}`); + const fontsUrl = `../disks/common/fonts.mjs?t=${timestamp}`; + const fontsModule = await import(fontsUrl); console.log("🔤 fonts.mjs reloaded, MatrixChunky8 * advance:", fontsModule.MatrixChunky8?.advances?.['*']); } catch (e) { console.log("🔤 fonts.mjs reload skipped:", e.message); diff --git a/system/scripts/build-disk-worker.mjs b/system/scripts/build-disk-worker.mjs new file mode 100644 index 0000000000..1b82c3fcc4 --- /dev/null +++ b/system/scripts/build-disk-worker.mjs @@ -0,0 +1,59 @@ +import { createHash } from "node:crypto"; +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { build } from "esbuild"; + +const scriptsDir = path.dirname(fileURLToPath(import.meta.url)); +const systemDir = process.env.AC_DISK_WORKER_SYSTEM_DIR + ? path.resolve(process.env.AC_DISK_WORKER_SYSTEM_DIR) + : path.resolve(scriptsDir, ".."); +const libDir = path.join(systemDir, "public/aesthetic.computer/lib"); +const entry = path.join(libDir, "disk.mjs"); + +const preserveRuntimeImports = { + name: "preserve-runtime-imports", + setup(buildApi) { + buildApi.onResolve({ filter: /.*/ }, (args) => { + if (args.kind === "dynamic-import") { + return { path: args.path, external: true }; + } + if (["url", "module", "https"].includes(args.path)) { + return { path: args.path, external: true }; + } + return null; + }); + }, +}; + +const result = await build({ + entryPoints: [entry], + bundle: true, + format: "esm", + platform: "browser", + target: ["es2022"], + treeShaking: true, + legalComments: "none", + write: false, + plugins: [preserveRuntimeImports], +}); + +const output = result.outputFiles?.[0]?.contents; +if (!output?.length) throw new Error("Disk worker build produced no output"); + +const sha256 = createHash("sha256").update(output).digest("hex"); +const filename = `disk.worker.${sha256.slice(0, 12)}.mjs`; +const manifest = { + filename, + sha256, + bytes: output.length, +}; + +await mkdir(libDir, { recursive: true }); +await writeFile(path.join(libDir, filename), output); +await writeFile( + path.join(libDir, "disk-worker-manifest.json"), + `${JSON.stringify(manifest, null, 2)}\n`, +); + +console.log(`${filename} ${output.length} bytes`);