Offline planning is useful only after the browser has a verified planner image. The experiment therefore has two separate promises: static assets can be served from the service worker cache, and graph search can run in a worker without blocking the page. Neither promise means that the first visit works without a network, or that live bus information is available offline.

The current implementation does not use a planner-specific Dexie or IndexedDB store. Workbox precaches the application assets that match the web build configuration, including JSON and wayplan files. A normal fetch can then be satisfied by CacheStorage after the service worker has installed and cached the current release.

Image readiness is a state

The page creates one shared trip worker. It fetches the manifest and the content-addressed image, checks the byte count and SHA-256 digest, and sends the image buffer to the worker with a transfer list. The worker verifies the image again, mounts it, and announces IMAGE_READY. A planning request is not sent until that promise resolves.

The failure state is intentionally visible. A missing manifest, a byte-length mismatch, a bad digest, a truncated header, or an image that does not match the worker's format contract prevents readiness. The current client rejects the warm-up promise; it does not quietly plan from an unverified image.

What happens on a normal load

On a successful load, the page obtains the same static image whether the response came from the service worker cache or the network, then gives ownership of the buffer to the planner worker.

The two digest checks have different jobs. The page checks what it downloaded against the manifest. The worker checks what it received before interpreting it. The transfer list avoids a structured clone of the initial ArrayBuffer message. Mounting still allocates runtime arrays, so “transferable” is not the same as “zero-copy planning.”

The page-side loader implements the first check. The excerpt is partial; it stops after the returned request object begins:

example.ts
1 const imageBytes = await imageResponse.arrayBuffer()
2 if (imageBytes.byteLength !== manifest.bytes) {
3 throw new Error("Planner image byte length does not match its manifest")
4 }
5 await verifyPlannerImage(new Uint8Array(imageBytes), manifest.sha256)
6 return { imageVersion: manifest.sha256, imageBytes }

Source: planner-image-transport.ts. The manifest names a file whose SHA-256 is part of the file name. The worker still runs the same verification path before it calls mountPlannerImage.

The request that wins

The worker is shared across planner visits, and planning requests can arrive close together when a rider edits an origin or destination. The current client keeps one pending request and rejects it when a newer request becomes the latest one. The following excerpt is partial; it stops inside the request payload.

example.ts
1 async planTrip(request: TripRequest): Promise<TripResult> {
2 nextRequestId += 1
3 const requestId = String(nextRequestId)
4 latestRequestId = requestId
5 pending?.reject(staleRequestError())
6 pending = null
7 await ready
8 if (requestId !== latestRequestId) throw staleRequestError()
9 return new Promise<TripResult>((resolve, reject) => {
10 pending = { requestId, resolve, reject }
11 worker.postMessage({
12 type: "PLAN_TRIP",
13 request: {
14 requestId,
15 queryTimeMs: Date.now(),

Source: trip-prepare-client.ts. A late worker message is also ignored when its request ID is no longer the latest ID. This prevents a slower search for an old pair of pins from replacing the result for the pair the rider is looking at now.

The worker does not cancel computation already in progress. It lets the old search finish and discards its response at the client boundary. That is simpler than interrupting the graph search, but it means rapid edits can still spend worker time on results no one will see.

Update and corruption cases

The image name changes with its content hash. A new manifest points to a new file, while the build script removes older matching image files from the public directory after it publishes the new pair. Workbox is configured for automatic updates and includes wayplan files in its precache glob. The current code does not contain a separate planner database migration.

An update can still fail at several boundaries. The page can receive a manifest whose file is absent. It can receive the wrong number of bytes. The digest can disagree with either the manifest or the bytes embedded in the image. The worker can reject a validly hashed image because its format version or required sections do not match the runtime. These errors become planner-unavailable behavior in the web page.

This experiment does not claim that every service-worker update race has been tested. The repository proves the manifest/hash checks, worker initialization, and stale-result handling. It does not provide a device matrix for browser storage eviction, an interrupted service-worker upgrade, or an old worker paired with a new image. Those remain release checks rather than facts to hide behind the word offline.

What remains offline

Once the image is available and mounted, the planner can use static services, geometry, transfers, and the service-area rings without a network request for the route calculation. The current browser request sends liveBusArrivals as an empty list, so live bus timing does not change this result. Place-name resolution can still need a network unless the user already has coordinates or a local place entry.

The image was introduced alongside the compiled planner in commit 89cad3b, the worker path was added in 8366918, image externalization followed in ff57ec5, and the Workbox wayplan precache adjustment is in 42970ea. Those commits show why the lifecycle exists. They do not prove browser latency, storage lifetime, or universal offline availability.

For the graph and selection work behind a mounted image, read Trip Planner Engine. For the artifact format and publication order, read Planner Image Compilation Pipeline.