Files
bitvid/docs/events-cache-store-overview.md
T

97 lines
4.1 KiB
Markdown

# EventsCacheStore Overview
The `EventsCacheStore` is a specialized persistence layer designed to handle the application's critical state: thousands of video events and their deletion status (tombstones). It uses **IndexedDB** as the primary storage backend, falling back to `localStorage` (via `PersistenceManager`) only if IDB is unavailable.
## 1. Problem Statement
Nostr clients must handle high-volume event streams. Storing every event naively to disk on every change causes:
- **Write Trashing**: Frequent, redundant writes degrade performance and battery life.
- **I/O Blocking**: Large JSON serializations on the main thread cause UI jank.
- **Startup Latency**: Reading thousands of small files or one giant blob is slow.
## 2. Architecture: Incremental Persistence
`EventsCacheStore` solves this with a **Differential State Snapshot** approach.
### Key Concepts
1. **Memory Fingerprints**:
- The store maintains a `Map<ID, Fingerprint>` in memory, mirroring what is currently on disk.
- `Event Fingerprint`: `JSON.stringify(videoObject)` (fast enough for 1000s of items).
- `Tombstone Fingerprint`: `ts:<timestamp>`.
2. **Debounced Snapshots**:
- `PersistenceManager` debounces writes (e.g., 75ms). When a write triggers, it passes the *entire* current state (`allEvents`) plus a set of `dirtyKeys` (IDs that changed since last save).
3. **The Diff Algorithm (`_diffAndStore`)**:
- Instead of clearing and rewriting the DB, it iterates over the `dirtyKeys`.
- It computes the new fingerprint.
- **Optimization**: If `newFingerprint === persistedFingerprint`, the write is skipped.
- If different, it performs a `put()` operation and updates the memory fingerprint map.
4. **Deletion Detection**:
- After processing updates, it compares the size of the memory state (`items.size`) vs the persisted state (`fingerprintsMap.size`).
- Since `fingerprintsMap` is updated during the loop to include new items, if `items.size < fingerprintsMap.size`, it implies some items in the persisted map are no longer in memory.
- In this case, it iterates the *entire* fingerprint map to find and `delete()` the missing keys.
### Flow Diagram
```mermaid
sequenceDiagram
participant App as NostrClient
participant PM as PersistenceManager
participant ECS as EventsCacheStore
participant IDB as IndexedDB
App->>App: Event arrives (updates allEvents)
App->>PM: saveLocalData(reason, dirtyKeys)
PM->>PM: Debounce (75ms)
PM->>ECS: persistSnapshot(payload, dirtyKeys)
activate ECS
ECS->>ECS: Ensure fingerprints loaded (once)
loop For each Dirty Key
ECS->>ECS: Compute Fingerprint
alt Fingerprint Changed
ECS->>IDB: put(record)
ECS->>ECS: Update Fingerprint Map
else Same Fingerprint
ECS->>ECS: Skip
end
end
alt items.size < fingerprintsMap.size
loop For each Persisted Key
alt Not in Memory
ECS->>IDB: delete(key)
ECS->>ECS: Remove from Fingerprint Map
end
end
end
ECS-->>PM: { writes, deletes, skipped }
deactivate ECS
```
## 3. Data Schema
The IndexedDB `bitvid-events-cache` (v1) contains three object stores:
| Store | Key Path | Value Description |
| :--- | :--- | :--- |
| `events` | `id` | `{ id, video, fingerprint }` |
| `tombstones` | `key` | `{ key, timestamp, fingerprint }` |
| `meta` | `key` | `{ key: "meta", savedAt: <ts>, version: 1 }` |
## 4. Invariants & Assumptions
- **Single Threaded**: The store assumes it is the only writer. It does not handle cross-tab locking (though IDB transactions provide atomicity).
- **Memory-First**: The `allEvents` Map in memory is the source of truth. IDB is a cache.
- **TTL**: On `restoreSnapshot()`, if the data is older than the TTL (e.g. 10 minutes), the entire cache is wiped to ensure freshness.
## 5. Maintenance
- **Adding Fields**: If the `Video` object shape changes, the fingerprint (JSON string) automatically changes, forcing a rewrite. No migration needed for content changes.
- **Schema Changes**: To add a new store, increment `EVENTS_CACHE_DB_VERSION` and update `onupgradeneeded` in `getDb()`.