Lumen/SPIKE-01-INDEXEDDB-IOS.md
Lumen Stage1 c0bfd413ff Stage 1: project foundation (strict TS, lint boundaries B-1..B-7, directory structure, CI, boundary tests)
- dedicated git repo at /home/avi/Projects/Lumen (main)
- TypeScript strict (target ES2022, bundler, exactOptionalPropertyTypes, noUncheckedIndexedAccess)
- ESLint 9 + typescript-eslint strictTypeChecked + eslint-plugin-boundaries for B-1..B-7, no-restricted-globals/syntax for B-1/B-7
- Prettier 3.5
- Structure per IMPLEMENTATION-CONTRACT.md §4 (src/platform/idb|cache|sw, storage, data, sync/{transport,verifier}, domain/{emergency,schedule,map,festival,readiness,clock,favorites}, ui/{components,views,router,render}, app, emergency-baseline, assets, public, content, pipeline, tests, scripts)
- CI: .github/workflows/ci.yml (typecheck + lint + format + test)
- Boundary tests: tests/unit/boundaries.test.ts (4 tests) + scripts/check-boundaries.ts
- No feature code, no PWA/IDB/sync/mesh/accounts per contract Stage 1
2026-08-30 23:25:35 -05:00

9.6 KiB
Raw Permalink Blame History

SPIKE-01 — IndexedDB + iOS Reliability

  • Phase: Architecture Validation
  • Date: 2026-08-30
  • Decision under test: ADR-004 (IndexedDB as primary local storage; A/B slot layout) and the storage foundations of ADR-006.
  • Environment limitation (declared up front): No iOS hardware or iOS Simulator is reachable from this Linux dev box. Nothing iOS-specific below was observed here. iOS items are labelled INFERRED (from WebKit's own published storage policy) or UNVERIFIED (needs a physical iPhone). This spike does not pretend otherwise.

1. Question

Is IndexedDB, organized as A/B dataset slots + a system-meta store + a user-data store, a sound foundation for Lumen on iOS Safari and Android Chrome, across large datasets, large map assets, crashes, restarts, storage pressure, and eviction?

2. Workload being validated

Dimension Lumen value (budget, ARCH §10.6)
Structured section JSON ≤ 3 MB total
Map assets (WebP) ≤ 28 MB
Other assets ≤ 6 MB
Total dataset ≤ 40 MB target / 50 MB ceiling
Worst case on device (A/B + shell + user) ≈ 2×40 MB + 1 MB + ε < 90 MB
Object stores files, assets per slot DB; meta in system DB; favorites, prefs, diag in user DB
Largest single record ≤ 6 MB (per-file cap, ARCH §10.6)

3. Findings, item by item

Each item is labelled. Citations are to vendor/spec documentation current as of 2026 (see DISCOVERY Appendix B for the underlying sources).

3.1 Large festival datasets (tens of MB)

  • INFERRED. IndexedDB is explicitly the browser's intended store for large structured data; quotas on both targets are expressed as a fraction of disk (WebKit: up to ~60% of disk per origin for browser apps since Safari 17; Chromium similar). A 40 MB dataset is a tiny fraction of any realistic quota.
  • Risk is not quota size; it is eviction policy (see 3.9) and write interruption (3.10).

3.2 Multiple object stores / multiple databases

  • CONFIRMED (spec): multiple databases and multiple object stores per origin are standard; transactions are scoped to one database.
  • INFERRED: our layout (two slot DBs + system DB + user DB) is well within normal use. Note: there is no cross-database transaction — the A/B design already accounts for this (activation is a single-DB transaction on the system DB; see SPIKE-02).

3.3 Large map assets (multi-MB Blobs in IDB)

  • INFERRED. IDB stores Blobs; multi-MB records are supported. We cap single records at 6 MB to bound transaction duration and memory.
  • UNVERIFIED (both platforms): sustained read-back performance of ~28 MB of WebP blobs into object URLs, and behavior when writing them near quota. Must be measured on real devices (Android low-end and iPhone).

3.4 Transaction behavior

  • CONFIRMED (spec): IDB transactions are atomic per database; a transaction either fully applies or does not. oncomplete/onerror/onabort signal the outcome. Versionchange transactions can block reads/writes.
  • INFERRED: keeping transactions short (one file + its progress record per transaction) avoids pathological lock holding and keeps the A/B flip cheap.

3.5 Atomicity

  • CONFIRMED (spec): within one database, a transaction is all-or-nothing. This is exactly what ADR-006's activation flip relies on (single transaction on lumen-system).
  • CONFIRMED (spec): atomicity does not span databases. The architecture must not assume it does (SPIKE-02 validates the ordering that makes this safe).

3.6 Browser termination (tab/process killed)

  • INFERRED. If the process dies mid-transaction, the transaction does not commit; durable state reverts to the last committed transaction. This is the standard crash-consistency model for IDB and is what EXP-2's crash points simulate.
  • UNVERIFIED (iOS): exact behavior when Safari force-quits or iOS jetsam kills the PWA process mid-write is expected to follow the same model but must be observed on device.

3.7 Service worker termination

  • CONFIRMED (architecture): dataset staging runs in the page context, not the SW (constraint C-21), so SW termination cannot interrupt dataset writes. The SW only handles shell caching.
  • INFERRED: iOS aggressively terminates idle SWs; because our design keeps no dataset state in the SW, this is a non-issue for data integrity.

3.8 Browser restart / phone restart

  • INFERRED. Committed IDB data survives browser and device restart; the SW and caches re-register/rehydrate on next launch. Boot-time light verification (SPIKE-05) is the mechanism that confirms this at runtime.
  • UNVERIFIED (iOS): post-restart cold-start latency and SW re-registration timing on real iPhones.

3.9 Storage pressure & eviction

  • CONFIRMED (WebKit policy): storage is best-effort. Under pressure, WebKit evicts on an LRU basis across origins; navigator.storage.persist() is a request granted by heuristic (home-screen install helps), not a guarantee. Eviction is per-origin and all-or-nothing (IDB + Cache + SW registration together).
  • CONFIRMED (Chromium policy): similar best-effort LRU eviction; installed PWAs with persistence granted are skipped first.
  • INFERRED: an installed PWA with persist() granted and regular use is unlikely to be evicted during a festival weekend, but nothing guarantees it. Therefore eviction is treated as an expected failure mode with a recovery path (RECOVERY state + embedded emergency baseline), never as an impossible one.

3.10 Interrupted writes

  • CONFIRMED (spec): an interrupted/uncommitted write leaves prior committed state intact. Combined with per-file staging transactions (SPIKE-02), an interrupted download can leave a slot partially staged but can never corrupt the active dataset.
  • INFERRED: resume logic keyed on committed per-file progress records recovers cleanly; EXP-2 exercises this.

3.11 Read consistency

  • CONFIRMED (spec): reads inside a transaction see a consistent snapshot; reads outside a transaction see the latest committed state. Our read paths (DatasetStore) read committed state; there are no mid-update reads of the inactive slot by features.

3.12 iOS Safari specifics

  • INFERRED (WebKit documented): 7-day ITP cap applies to Safari-tab usage and is exempt for home-screen-installed PWAs; Safari 17+ quota is a disk fraction; eviction all-or-nothing.
  • UNVERIFIED: all of the above as experienced by our specific app — install exemption in practice, persist() grant rate, near-quota write behavior, Blob read-back speed, post-restart rehydration. Requires physical iOS.

3.13 Android Chrome specifics

  • INFERRED (Chromium documented): generous disk-fraction quotas; LRU eviction under pressure; installed PWAs + persist() favored.
  • UNVERIFIED: low-end-device IDB write throughput and Blob handling for a 40 MB dataset; behavior under actual storage pressure on a 32–64 GB device nearly full.

4. Defensive protocol derived from this spike

These rules are the "changes" attached to ACCEPT-WITH-CHANGES and are carried into the ADR-004 addendum:

  • P1: One short transaction per staged file (bytes + progress record commit together). No transaction spans an await of non-IDB work.
  • P2: Activation is exactly one transaction on the system DB.
  • P3: Every IDB call is wrapped; QuotaExceededError aborts staging and keeps the active dataset (never a crash).
  • P4: Pre-stage free-space check via navigator.storage.estimate(); refuse to stage if free < 2× package size.
  • P5: Single record ≤ 6 MB (bounds transaction memory/duration).
  • P6: Request navigator.storage.persist() once after READY; never rely on the grant.
  • P7: Boot-time light verification assumes storage may have vanished (eviction) — detection, not prevention.
  • P8: No dataset state in the Service Worker.

5. Must test on physical devices before production

iOS (iPhone, iOS 16.4 low bound + latest)

  1. Fresh install → full prep of a real-size (~40 MB) package → force-quit and reboot → data intact, boot ≤ budget.
  2. Leave unused in Safari tab for > 7 days vs installed PWA → confirm tab data evicted, installed data survives.
  3. navigator.storage.persist() grant outcome when installed.
  4. Write dataset while device storage is nearly full → observe QuotaExceededError handling (no crash, active preserved).
  5. Kill app mid-download repeatedly → resume completes; active never partial.
  6. Blob read-back: open map with ~28 MB assets → time-to-interactive.

Android (2021 mid-range, Chrome ~110 + latest)

  1. Same fresh-install/reboot cycle with 40 MB package.
  2. Storage-pressure eviction simulation (devtools / fill disk) → RECOVERY path.
  3. IDB write throughput for 40 MB staging; per-file transaction timing.
  4. Map asset Blob read-back performance on low-end GPU/RAM.

6. Verdict

ACCEPT WITH CHANGES.

  • IndexedDB is the correct primary store (structure, transactions, Blob support, quota scale all fit). No alternative (OPFS, SQLite-WASM, localStorage) is better for this workload (see ADR-004).
  • The A/B layout is sound and its atomicity claims are validated at the design-logic level by EXP-2 (SPIKE-02).
  • Changes: adopt defensive protocol P1–P8 (added to ADR-004).
  • Condition: the physical-device tests in §5 are mandatory before the storage layer is declared production-ready. Until then the decision stands as the best-supported choice, with the platform-specific behaviors above explicitly UNVERIFIED, not confirmed.