feat: add IndexedDB storage engine as alternative to localStorage#270
feat: add IndexedDB storage engine as alternative to localStorage#270leoromanovsky wants to merge 5 commits intomainfrom
Conversation
Add IndexedDBStorageEngine as an opt-in alternative to localStorage for storing flag configurations. IndexedDB provides significantly larger storage capacity (~50MB+) compared to localStorage (~5-10MB). Key changes: - New IndexedDBStorageEngine implementing IStringStorageEngine interface - Uses LZ-string compression like LocalStorageEngine for consistency - Add hasIndexedDB() detection helper to configuration-factory - Update configurationStorageFactory to support IndexedDB with useIndexedDB flag - Add useIndexedDB configuration option to IClientConfig - Proper error handling for quota exceeded and IndexedDB-specific errors IndexedDB is opt-in only. LocalStorage remains the default storage mechanism, ensuring no breaking changes for existing users. Storage priority when useIndexedDB=true: 1. Custom persistentStore (if provided) 2. IndexedDB (if available and opted in) 3. Chrome storage (if available) 4. LocalStorage (if available) 5. Memory-only (fallback)
Extend IndexedDB implementation to support the assignment cache in addition to the configuration store. The assignment cache is stored as a blob (serialized Map) for efficient storage and retrieval. Key changes: - New IndexedDBAssignmentShim implementing Map<string, string> interface - Stores assignment cache as blob in IndexedDB 'assignments' object store - New IndexedDBAssignmentCache wrapper class - Update assignmentCacheFactory to support IndexedDB with useIndexedDB flag - Add useIndexedDB to IPrecomputedClientConfig interface - IndexedDB database now has three object stores: - 'contents': compressed flag configuration - 'meta': configuration metadata - 'assignments': assignment cache blob Storage priority for assignment cache when useIndexedDB=true: 1. Chrome storage (if available) 2. IndexedDB (if available and opted in) 3. LocalStorage (if available) 4. Memory-only (fallback) Assignment cache data is stored as a serialized array of Map entries for efficient blob storage, providing ~50MB+ capacity vs localStorage's ~5-10MB limit.
| * IndexedDB-backed Map implementation for assignment cache. | ||
| * | ||
| * Stores the entire assignment cache as a single blob in IndexedDB for efficiency. | ||
| * This provides better storage capacity (~50MB+) compared to localStorage (~5-10MB) |
There was a problem hiding this comment.
It's actually much larger for most browsers: https://rxdb.info/articles/indexeddb-max-storage-limit.html#browser-specific-indexeddb-limits
There was a problem hiding this comment.
Yea thanks for mentioning that; I remember reading that it's basically using the host's storage and should be closer to GBs.
| /** | ||
| * IndexedDB-backed Map implementation for assignment cache. | ||
| * | ||
| * Stores the entire assignment cache as a single blob in IndexedDB for efficiency. |
There was a problem hiding this comment.
Have you considered storing each key individually? IndexedDB is essentially a key-value store and we can use arrays as keys (e.g., [keySuffix, flagKey, subjectKey]), so that should be quite efficient and we don't need to load megabytes into main memory.
There was a problem hiding this comment.
I did - we used to do this with local storage (during each flag key) but this is challenging because of the way we receive configuration updates; we need to deleting any flag keys that are absent in the received configuration payload. Without doing this we'd have stale flag configuration. This was challenging in local storage since we can't iterate over keys but now that you mention it should be doable in IDB.
There was a problem hiding this comment.
I decided not to do per-key because we are using the HybridStore abstraction; each of these AsyncStores is paired with a sync serving store; on SDK start the serving store (in-memory) is populated; however seems if we switch to per-flag storage we can simplify this and serve directly but I'll leave that for another PR or the datadog implementation.
I did change it to store native javascript objects though instead of serialized json string and no more compression.
| servingStoreUpdateStrategy = 'always', | ||
| hasChromeStorage = false, | ||
| hasWindowLocalStorage = false, | ||
| hasIndexedDB = false, |
There was a problem hiding this comment.
minor: a bit surprised to see hasIndexedDB as a parameter here and as a function below. Are there any cases when we don't want to use that function?
…sion - Remove LZ-string compression from IndexedDBStorageEngine - Store configuration as native JavaScript objects using structured clone - Update JSDoc to reflect accurate storage capacity (gigabytes vs ~50MB) - Add unit tests for IndexedDB native object storage
Simplify IndexedDBStorageEngine to store JSON strings directly instead of parsing to native objects. This matches the IStringStorageEngine interface contract and removes unnecessary JSON.parse/stringify roundtrip overhead.
Changes from Code Review (Jan 13)Reverted IndexedDBStorageEngine to store JSON strings directly instead of native JavaScript objects. ContextThe initial implementation stored configuration as native objects in IndexedDB, taking advantage of IndexedDB's Storing native objects meant we were:
This added overhead without benefit since the interfaces force us back to strings anyway. What I changedSimplified to store JSON strings directly, matching what the interface expects. This removes the unnecessary Future considerationTo fully leverage IndexedDB's native object storage (avoiding JSON parse/stringify entirely), we'd need a larger |
aarsilv
left a comment
There was a problem hiding this comment.
Nice work! Exciting to potentially take advantage of better tech and fold these learnings into Datadog SDKs as well.
| if (chromeStorage) { | ||
| const chromeStorageCache = new ChromeStorageAssignmentCache(chromeStorage); | ||
| return new HybridAssignmentCache(simpleCache, chromeStorageCache); | ||
| } else if (useIndexedDB && hasIndexedDB()) { |
| return new Promise<Map<string, string>>((resolve, reject) => { | ||
| request.onsuccess = () => { | ||
| const data = request.result; | ||
| if (data && Array.isArray(data)) { |
There was a problem hiding this comment.
Minor but Array.isArray(data) will always return false if data is falsey so need for the first check in that conditional.
if (Array.isArray(data)) {| /** Returns whether IndexedDB is available */ | ||
| export function hasIndexedDB(): boolean { | ||
| try { | ||
| return typeof window !== 'undefined' && typeof window.indexedDB !== 'undefined' && !!window.indexedDB; |
There was a problem hiding this comment.
Minor but I don't think we need the typeof window.indexedDB !== 'undefined' check as we'll be covered by !!window.indexedDB
| overridesStorageKey?: string; | ||
|
|
||
| /** | ||
| * Use IndexedDB for storing flag configurations and assignment cache instead of localStorage. |
There was a problem hiding this comment.
We may want to say something Use IndexedDB if available and then alter say how we'll fall back to other options if its not to be more clear on what happens.
| mockTransaction.objectStore.mockReturnValue(mockStore); | ||
| mockDB.transaction.mockReturnValue(mockTransaction); | ||
|
|
||
| // Simulate IndexedDB storage |
There was a problem hiding this comment.
The joys of testing in node!
Motivation
We've encountered storage quota limitations with localStorage in production environments, particularly when storing large flag configurations. LocalStorage has a typical storage limit of ~5-10MB, which can be insufficient for customers with extensive feature flag setups or large configuration payloads.
IndexedDB provides significantly larger storage capacity (~50MB+ in most browsers, often much more) and offers better performance characteristics for large datasets through its async-first API that doesn't block t
he main thread.
This change adds IndexedDB as an opt-in alternative to localStorage, allowing customers experiencing quota issues to switch to a more robust storage backend without requiring changes to their existing integrat
ion.
Changes
Add IndexedDBStorageEngine as an opt-in alternative to localStorage for storing flag configurations. IndexedDB provides significantly larger storage capacity (~50MB+) compared to localStorage (~5-10MB).
Key changes:
IndexedDB is opt-in only. LocalStorage remains the default storage mechanism, ensuring no breaking changes for existing users.
Storage Priority Order
When
useIndexedDB: trueis configured:persistentStore(if provided)When
useIndexedDB: falseor not set (default):persistentStore(if provided)Usage
Rollout Plan
Phase 1: Internal Dogfooding
useIndexedDB: trueenabledPhase 2: Customer Notification
useIndexedDBoptionPhase 3: Datadog Implementation Rollout
labels: mergeable
Eppo Internal
//: # (Link to the issue or doc corresponding to this chunk of work)
🎟️ Fixes: #issue
📜 Design Doc (if applicable)
Motivation and Context
Description
How has this been documented?
How has this been tested?