Files
roam/homeyemulatorjs_1.org
2026-08-29 06:59:35 +03:00

5.2 KiB

Part of homey. EmulatorJS self-hosted web game emulator at games.zakobar.com (port 8085), Authelia one_factor. A Python stdlib sidecar (modules/services/emulatorjs-server.py) serves the launcher, ROMs, per-user saves, and a PWA; static assets/cores come from GitHub via fetchFromGitHub (modules/services/emulatorjs.nix, currently v4.2.3). ROMs/saves/thumbnails live on the external HD under /mnt/data/emulatorjs/, outside Nix.

Architecture — offline PWA (added 2026-07-31)

Installable PWA with offline play, deliberately network-first: the Service Worker only reads cache when the network is unreachable, so offline support can never alter or break online play. (An earlier cache-first version rebuilt/buffered responses, broke EmulatorJS game loading, and was ripped out — the working tree that "fixed" it was simply the SW removed. Rebuilding responses via arrayBuffer()=+=new Response() also stripped range-request support and OOM'd large loads on iOS.)

Server serves /sw.js (Service Worker, Service-Worker-Allowed: /), /manifest.json, and a stdlib-generated /icon-256.png (PNG encoded by hand with zlib=/=struct, built once at startup — no image deps). Both HTML pages carry a shared PWA head fragment.

Two SW caches:

  • emujs-shell-<BUILD_ID> — app shell + /static/ cores, cache-first. BUILD_ID = short sha256 of the server .py file + STATIC_DIR, so a redeploy auto-invalidates stale assets (no manual cache-version bump, unlike the old CACHE_NAME'v4'=). NOTE: BUILD_ID changes on any edit to the .py file, so every deploy drops the shell cache and it must be repopulated by one online play.
  • emujs-roms — persistent across builds. ROMs are NOT auto-cached; download is opt-in per game via the launcher's ⬇ button (also caches the thumbnail + the /play/<name> document). Non-downloaded games dim out when offline.

Core caching (the non-obvious bit — fixed 2026-07-31): the emulator core (e.g. mgba-legacy-wasm.data, shared by GB/GBC/GBA) is NOT reliably cached by the Service Worker, because on the first visit the SW is not yet controlling the page, so the core fetched during that first online play is never intercepted. Instead the player page caches it directly: after game start, cacheLoadedStatic() reads performance.getEntriesByType('resource'), filters to same-origin /static/ URLs (the real files the browser loaded), and writes them into the shell cache from the page — bypassing SW-interception timing entirely and version-proof. Runs on game start (+5s delayed pass) and on the online event.

Full design rationale is in the module docstring at the top of emulatorjs-server.py.

Conventions

Adding/removing GB/GBC/GBA ROMs: just drop/delete files in /mnt/data/emulatorjs/roms/. The /roms/ listing does a live os.listdir on every request, so a page refresh picks up changes — no restart, no nixos-rebuild. Filename (minus extension) is the display title. Optional cover art: <title>.png in /mnt/data/emulatorjs/thumbnails/ (else 🎮 placeholder). Files must be readable by the emulatorjs service user.

Upgrading EmulatorJS assets: bump rev in emulatorjs.nix, re-prefetch (nix-prefetch-url --unpack), convert to SRI, update hash. Instructions are in a comment in the file.

To make a game fully offline-ready: (1) tap ⬇ to download the ROM, and (2) play it once while online so the core caches. After a redeploy, step 2 must be repeated (shell cache is build-scoped).

Gotchas

Save sync: saves are independent of ROM download state. Two-tier: online → PUT /saves/<game> (keyed by Remote-User); offline/failed → queued in IndexedDB (pending-saves store). Queue flushes via syncPendingSaves() on the online event, on EJS_onGameStart, and on launcher load. NO Background Sync API — sync only runs while a page is open / on next app open. Last-write-wins, no timestamp merge (both deemed acceptable by owner 2026-07-31).

Core not cached → "Error downloading core (…-wasm.data)" offline: symptom of the SW-timing gap above. Fixed by page-side cacheLoadedStatic(); requires one successful online play (after each deploy) to populate.

Filename charset: a ROM loads only if its name matches SAFE_RE ([A-Za-z0-9 _\-\.\(\)\[\]',!?:&+#]). A name with other characters (accents, ~, @, %, ;) still appears in the listing but 404s/400s on open. Keep names in that set.

Offline library staleness: the /roms/ listing is network-first cached, so an offline device shows its last-cached list until it's online once.

Future — hosting disc-based systems (e.g. PS1)

Not done (owner declined 2026-07-31), but scoped. Would require: (1) map disc extensions to the psx core in ROM_SYSTEMS + zip detection (prefer single-file .chd; .bin=/.iso= are ambiguous across systems); (2) a user-supplied BIOS (copyrighted) served via a new /bios/ route + EJS_biosUrl; (3) stream ROM serving — send_body currently does f.read(), loading the whole file into RAM, so a 600 MB disc image spikes 600 MB per request on the Pi. Fine for tiny cartridge ROMs; must switch to chunked/Range serving for large images. Offline download also impractical for 100s-of-MB ROMs (iOS Cache API quota).