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

191 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
7. Same fresh-install/reboot cycle with 40 MB package.
8. Storage-pressure eviction simulation (devtools / fill disk) → RECOVERY path.
9. IDB write throughput for 40 MB staging; per-file transaction timing.
10. 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.