- 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
9.6 KiB
9.6 KiB
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/onabortsignal 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
awaitof non-IDB work. - P2: Activation is exactly one transaction on the system DB.
- P3: Every IDB call is wrapped;
QuotaExceededErroraborts 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)
- Fresh install → full prep of a real-size (~40 MB) package → force-quit and reboot → data intact, boot ≤ budget.
- Leave unused in Safari tab for > 7 days vs installed PWA → confirm tab data evicted, installed data survives.
navigator.storage.persist()grant outcome when installed.- Write dataset while device storage is nearly full → observe QuotaExceededError handling (no crash, active preserved).
- Kill app mid-download repeatedly → resume completes; active never partial.
- Blob read-back: open map with ~28 MB assets → time-to-interactive.
Android (2021 mid-range, Chrome ~110 + latest)
- Same fresh-install/reboot cycle with 40 MB package.
- Storage-pressure eviction simulation (devtools / fill disk) → RECOVERY path.
- IDB write throughput for 40 MB staging; per-file transaction timing.
- 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.