mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
chore(ai): doc js/nostr/managers/EventsCacheStore.js (agent)
Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
d218baae4d
commit
676f41d76d
2 files changed
+152
No files matched your search
@@ -0,0 +1,96 @@
|
||||
# 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()`.
|
||||
@@ -42,6 +42,10 @@ function waitForTransaction(tx) {
|
||||
* - "Fingerprints" (hashes/stringified) are tracked in memory to minimize I/O.
|
||||
* - Only changed items (where fingerprint differs) are written to IDB.
|
||||
* - If the TTL expires, the cache is cleared on restore to force a fresh fetch.
|
||||
*
|
||||
* **Concurrency Note:**
|
||||
* This class assumes single-threaded access (JavaScript main thread).
|
||||
* It relies on `dirtyKeys` for optimization but falls back to full scans if needed.
|
||||
*/
|
||||
export class EventsCacheStore {
|
||||
/**
|
||||
@@ -59,6 +63,10 @@ export class EventsCacheStore {
|
||||
this.hasLoadedFingerprints = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if IndexedDB is available in the current environment.
|
||||
* @returns {boolean} True if IndexedDB is supported.
|
||||
*/
|
||||
isSupported() {
|
||||
return typeof indexedDB !== "undefined";
|
||||
}
|
||||
@@ -101,6 +109,13 @@ export class EventsCacheStore {
|
||||
return this.dbPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a unique fingerprint for a video object.
|
||||
* Used to detect changes without deep comparison.
|
||||
*
|
||||
* @param {object} video - The video object to fingerprint.
|
||||
* @returns {string} The computed fingerprint (JSON string).
|
||||
*/
|
||||
computeEventFingerprint(video) {
|
||||
try {
|
||||
return JSON.stringify(video);
|
||||
@@ -110,6 +125,12 @@ export class EventsCacheStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a fingerprint for a tombstone timestamp.
|
||||
*
|
||||
* @param {number} timestamp - The timestamp.
|
||||
* @returns {string} The fingerprint.
|
||||
*/
|
||||
computeTombstoneFingerprint(timestamp) {
|
||||
return `ts:${timestamp}`;
|
||||
}
|
||||
@@ -152,6 +173,11 @@ export class EventsCacheStore {
|
||||
this.hasLoadedFingerprints = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads metadata from the store (version, savedAt).
|
||||
* @param {IDBDatabase} db - The database instance.
|
||||
* @returns {Promise<object|undefined>} The metadata object.
|
||||
*/
|
||||
async readMeta(db) {
|
||||
const tx = db.transaction(["meta"], "readonly");
|
||||
const metaStore = tx.objectStore("meta");
|
||||
@@ -228,6 +254,30 @@ export class EventsCacheStore {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Core logic for incremental persistence.
|
||||
* Compares items against `fingerprintsMap` to decide what to write/delete.
|
||||
*
|
||||
* **Optimization Logic:**
|
||||
* 1. Iterates only `dirtyKeys` (if provided) or all `items`.
|
||||
* 2. Computes current fingerprint.
|
||||
* 3. Updates IDB only if fingerprint differs from `fingerprintsMap`.
|
||||
* 4. Updates `fingerprintsMap` with the new fingerprint.
|
||||
* 5. Checks if deletions are needed (if `items.size < fingerprintsMap.size`).
|
||||
* - Note: `fingerprintsMap` grows during the loop as new items are added.
|
||||
* - This ensures that if we deleted items from `items`, `items.size` will be smaller
|
||||
* than `fingerprintsMap.size`, triggering the deletion scan.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {IDBObjectStore} params.store - The IDB store to write to.
|
||||
* @param {Map} params.items - The current state (Map of items).
|
||||
* @param {Map} params.fingerprintsMap - The map of persisted fingerprints.
|
||||
* @param {function} params.computeFingerprint - Helper to compute fingerprint.
|
||||
* @param {Set<string>|null} params.dirtyKeys - Optional set of keys that changed.
|
||||
* @param {function} params.isValid - Validator for items.
|
||||
* @param {function} params.createRecord - Helper to create IDB record object.
|
||||
* @returns {{writes: number, deletes: number, skipped: number}} Stats.
|
||||
*/
|
||||
_diffAndStore({
|
||||
store,
|
||||
items,
|
||||
@@ -267,6 +317,12 @@ export class EventsCacheStore {
|
||||
writes++;
|
||||
}
|
||||
|
||||
// Deletion detection strategy:
|
||||
// If dirtyKeys is null, we are doing a full scan, so check everything.
|
||||
// If dirtyKeys is provided, we only check deletions if 'items' is smaller than our known persisted set.
|
||||
// NOTE: fingerprintsMap is mutated in the loop above (adding new/updated items).
|
||||
// So if items were removed from 'items' map, fingerprintsMap.size (which includes the removed ones)
|
||||
// will be larger than items.size, correctly triggering the scan.
|
||||
const shouldScanDeletions = !dirtyKeys || items.size < fingerprintsMap.size;
|
||||
|
||||
if (shouldScanDeletions) {
|
||||
|
||||
Reference in new issue
Block a user