fuzzyworld

tower / commit

fix(terrain): bundle the terrain worker so builds stop hanging on "Reading the plans"

3413c443aa18667ac995a6c4a3cac857297443fd · Pavel Safronov · 2026-09-18 17:26:42 -0700 · parents 51c39d8bb65b

fix(terrain): bundle the terrain worker so builds stop hanging on "Reading the plans"

The worker URL was built with `new URL('./worker.ts', import.meta.url)` held in
a field, which Vite does not recognise as a worker. Builds copied worker.ts raw,
inlined as a data:video/mp2t URL; the worker never started and the first compile
waited forever. Dev served the file transformed, so only deployed builds broke.

Load it through `?worker&url` instead, and fail the Pages job if the build has
no worker chunk.

Usage: 0.1K in / 24.2K out / 2.8M cached (session tally)
rtk:   3.3M saved, 36.9% over 3563 cmds (global/lifetime)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
 .gitlab-ci.yml         |  3 +++
 src/terrain/service.ts | 11 ++++++++++-
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cd728c4..529dfa6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,6 +18,9 @@ pages:
     # Fail loudly rather than publishing a 404. `mv` silently nests when the
     # destination exists, which is exactly how this broke the first time.
     - test -f public/index.html || (echo "no index.html at artifact root" && exit 1)
+    # The terrain worker must be bundled as its own chunk. When it was not, the build
+    # passed and the site hung forever on "Reading the plans" (service.ts).
+    - ls public/assets/worker-*.js || (echo "terrain worker was not bundled" && exit 1)
   artifacts:
     paths: [public]
   rules:
diff --git a/src/terrain/service.ts b/src/terrain/service.ts
index 94c327a..a6b7196 100644
--- a/src/terrain/service.ts
+++ b/src/terrain/service.ts
@@ -18,6 +18,15 @@ import type { CompiledWorld } from './compile.js'
 import { type Runtime, prepare } from './field.js'
 import type { TileData, TileGrid, TileKey } from './tile.js'
 import type { WorkerApi } from './worker.js'
+/**
+ * The worker, bundled as a worker, and its URL. This must go through Vite's `?worker`
+ * import: a bare `new URL('./worker.ts', import.meta.url)` held in a variable is not
+ * recognised as a worker, so the build copied worker.ts raw as an asset — inlined as a
+ * `data:video/mp2t` URL, since `.ts` is also the MPEG transport stream extension. The
+ * worker failed to start, nothing listened for that, and the first compile call waited
+ * forever on "Reading the plans". Dev served the file transformed, so only builds broke.
+ */
+import workerUrl from './worker.ts?worker&url'
 
 /** The map image, matching v1's raster so the map reads at the same resolution. */
 export const MAP_WIDTH = 2048
@@ -63,7 +72,7 @@ export class TerrainService {
 
   /** The compiled world was read from the cache last time rather than compiled. */
   lastFromCache = false
-  private readonly workerUrl = new URL('./worker.ts', import.meta.url)
+  private readonly workerUrl = new URL(workerUrl, import.meta.url)
   /**
    * Whether the compiled world may be cached. In a build the worker's URL carries a hash
    * of its code, so a new build can never read an old build's world. In dev the URL never