Skip to content

feat(r3-a): prove the offline PMTiles map on macOS - #62

Merged
Morialkar merged 6 commits into
mainfrom
feat/r3-a-pmtiles-probe
Aug 4, 2026
Merged

feat(r3-a): prove the offline PMTiles map on macOS#62
Morialkar merged 6 commits into
mainfrom
feat/r3-a-pmtiles-probe

Conversation

@Morialkar

Copy link
Copy Markdown
Owner

Third and last capability R3-A had to demonstrate. macOS now passes all three; the target matrix is still open, so no Go/No-go is declared.

Result

The probe runs when the native window starts and records its verdict in the spike database, so the Windows, iOS and Android smoke runs can read it without driving a UI:

{
  "ok": true,
  "sourceFeatures": 87,
  "renderedFeatures": 7,
  "blockedRequests": [],
  "transport": "tauri-fs"
}

87 features decoded from the bundled archive and 7 actually painted — the map draws, it does not merely load. blockedRequests: [] means no network resource was even asked for; the style deliberately carries no glyphs or sprites, which would be hidden network dependencies.

The fixture

Built from the Natural Earth country geometry already vendored for the gazetteer — public domain, so redistributable, and nothing is downloaded. No OSM-derived data, no tile-server prefetching, as the spec requires.

packages/core/scripts/build-pmtiles-fixture.mjs writes the PMTiles v3 container directly instead of shelling out to tippecanoe, so the archive rebuilds from a clean checkout with pnpm alone. 598 KiB, zooms 0–4.

It is validated two ways: a test asserts the v3 header, MVT tile type, declared layer and size; and every tile was parsed with @mapbox/vector-tile (7/7 decoded with a populated countries layer) before any of this was blamed on Tauri.

Two platform findings

Tauri's app protocol does not do HTTP byte serving. PMTiles reads an archive as byte ranges. tauri://localhost answers Range: bytes=0-126 with 200, no Content-Range, and all 612 390 bytes — and the reader refuses such a backend outright ("Check that your storage backend supports HTTP Byte Serving"). Serving the archive over tauri:// does not work. It ships as a bundle resource and is read through the fs plugin instead, which is closer to what a local-first build would do anyway.

That native path needs its own ACL grants — fs:allow-open, fs:allow-seek, fs:allow-read, fs:allow-fstat — separate from the readFile/writeTextFile helpers. Note fs:allow-close does not exist and fails the build.

Reads must be serialized. A seek followed by a read is two round trips over one shared cursor, and MapLibre requests tiles concurrently. Interleaved seeks hand each reader another range's bytes; the symptom appears far from the cause, as a corrupt tile ("Extra bytes past the end").

Both are written up in the evidence bundle so the mobile targets do not rediscover them.

Verification

  • native verdict above, read back from probe_verdicts in the spike database
  • 82 client tests (4 new on the fixture), tsc -b, ESLint, Prettier clean

Scope

Spike-only and disposable. The production map still uses OSM raster tiles over the network; this probe deliberately does not, because the spike has to show the map standing up with no connectivity.

Remaining before any decision: Windows, iOS simulator, Android emulator.

Third and last capability the spike had to demonstrate. The probe runs on
window start and records its verdict in the spike database, so the remaining
targets can read it without driving a UI:

  ok, 87 source features, 7 painted, blockedRequests: [], transport: tauri-fs

The fixture is built from the Natural Earth country geometry already vendored
for the gazetteer — public domain, so redistributable, and nothing is fetched.
build-pmtiles-fixture.mjs writes the PMTiles v3 container directly rather than
shelling out to tippecanoe, so it rebuilds from a clean checkout with pnpm
alone. 598 KiB, zooms 0-4. The style carries no glyphs or sprites, which would
be hidden network dependencies.

Two platform findings, both recorded in the evidence bundle:

Tauri's app protocol does not support HTTP byte serving. It answers a Range
request with the whole file and a 200, and the PMTiles reader refuses such a
backend outright. The archive therefore ships as a bundle resource and is read
through the fs plugin, which is closer to what a local-first build would do.
That path needs its own ACL commands (open/seek/read/fstat); fs:allow-close
does not exist and breaks the build.

Reading it also has to be serialized: seek and read are two round trips over
one shared cursor, and MapLibre requests tiles concurrently, so interleaved
seeks hand each reader another range's bytes. The symptom surfaces far from
the cause as a corrupt tile.
Both mobile targets run the offline map and SQLite probes for real:

  iOS 26.3.1 simulator  ok, 48 source features, 3 painted, no network
  Android API 36.1      ok, 48 source features, 3 painted, no network

The same TauriFileSource works on all three platforms despite different
address shapes: macOS and iOS resolve the bundled archive to a filesystem
path, Android to asset://localhost/... — the fs plugin opens and range-reads
both without adaptation.

Vault is not exercised on mobile: it depends on the folder picker, which
needs interaction. The spec only asks for a capability check there, and that
check is still outstanding — the evidence bundle says so rather than implying
coverage.

Also stop destroying the map once the probe has measured it. Tearing it down
made it flash and vanish exactly when it succeeded, which is the opposite of
what a proof screen should do.

Generated mobile projects are not versioned: 855 MB of build scaffolding that
"tauri ios init" and "tauri android init" reproduce. The evidence bundle
carries the commands and the toolchain constraints found along the way —
rustup rather than Homebrew rust, the foreground requirement for the iOS
build, and Gradle's refusal of JDK 26.
@Morialkar
Morialkar force-pushed the feat/r3-a-pmtiles-probe branch from 58cf739 to 292fdea Compare August 4, 2026 01:49
Android running the probes on a Chromium webview is the closest available
signal for WebView2, so the residual Windows risk is materially lower than
when only WKWebView had been exercised. Record that, without upgrading an
untested target to a proven one.
…raint

The last outstanding combination in the target matrix. The check runs
unattended so a smoke script can perform it, and reports both what
app-scoped storage can do and whether a directory picker exists.

Result on all three platforms:

  macOS    app-scoped write PASS, directory picker supported
  iOS      app-scoped write PASS, directory picker UNSUPPORTED
  Android  app-scoped write PASS, directory picker UNSUPPORTED

"Folder picker is not implemented on mobile" — so the desktop vault model,
an arbitrary user-chosen folder such as an Obsidian directory, has no
equivalent on iOS or Android with this stack. Mobile can write Markdown, but
only inside storage that is private to the app and removed with it.

That does not sink Tauri; it forces a decision about what a mobile vault is,
which the evidence bundle now states with the three options rather than
leaving it implied. The spec asks for the constraint to be recorded instead
of faking desktop semantics.

Writing into app data also needed fs:allow-appdata-write-recursive; without
it the write is rejected as a forbidden path even with allow-write-text-file
granted.
Declared on the evidence gathered: the three required capabilities pass on
macOS, the iOS simulator and the Android emulator, across two webview
engines. Tauri is adopted; the D8 fallback of Capacitor plus Electron is not
taken.

Two points ride along with the decision rather than being closed by it:
Windows was never executed, and the mobile vault model still has to be
chosen, since no directory picker exists on either mobile platform.
directoryPicker is assigned on every path through the try/catch, so seeding
it with 'unknown' was never read. I had re-run tsc and Prettier after adding
the capability probe but not ESLint, which is why CI caught it and I did not.
@Morialkar
Morialkar merged commit bd8b765 into main Aug 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant