Skip to content

Add IndexedDB persistence for browser flag configurations#186

Merged
leoromanovsky merged 9 commits intomainfrom
leo/add-indexeddb-flags-persistence
Feb 28, 2026
Merged

Add IndexedDB persistence for browser flag configurations#186
leoromanovsky merged 9 commits intomainfrom
leo/add-indexeddb-flags-persistence

Conversation

@leoromanovsky
Copy link
Copy Markdown
Collaborator

@leoromanovsky leoromanovsky commented Feb 26, 2026

Motivation

The browser SDK currently has no disk persistence for flag configurations. If the network is unavailable during a page load (backend outage, flaky connection, CDN maintenance), users get no flags and fall back to programmatic defaults. iOS and Android SDKs both have disk-based caching that makes them resilient to short outages — this brings the browser SDK to parity.

Changes

The provider now stores the full precomputed flag configuration as a JSON blob in IndexedDB after every successful fetch. On the next page load, it reads the cached configuration from IndexedDB before hitting the network. If the network request fails but cached flags exist, the provider goes to READY state using the cached data instead of throwing an error. If IndexedDB is unavailable (private browsing, older browsers), the SDK works exactly as it does today — no behavior change.

Decisions

  • IndexedDB over localStorage — localStorage is synchronous and blocks the main thread on read/write. It also has a ~5MB limit shared across all keys for the origin, and we'd be competing with the exposure assignment cache that already uses localStorage. IndexedDB is async, has much higher storage limits (~50MB+), and keeps flag persistence isolated from other SDK storage.
  • Single JSON blob, not individual flags in IndexedDB — Simpler, matches mobile SDK patterns, and the payload is small (a few KB). No need for indexed queries on individual flags.
  • Direct JSON.stringify/JSON.parse instead of the wire format — The existing configurationToString/configurationFromString has a round-trip bug where it nests the response one level deep on deserialization. Direct serialization avoids this.
  • Network-first, cache-fallback — Always attempt a fresh fetch. IndexedDB is only used as a fallback during initialize() when the network fails.
  • No TTL — Matches iOS and Android. Cached data is used until a successful network fetch replaces it.
  • Graceful degradation — If IndexedDB is unavailable, flagsCache is undefined and all optional-chain calls are no-ops.

Question for reviewers

This is a non-breaking additive feature (no API changes, no behavior change when IndexedDB is unavailable). Should this ship as a patch release or a minor release?

Store precomputed flag assignments in IndexedDB so the browser SDK
can serve cached flags across page loads, surviving short backend
outages (e.g., CDN maintenance windows) instead of falling back to
programmatic defaults.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds IndexedDB-based persistence for flag configurations in the browser SDK, bringing it to feature parity with the iOS and Android SDKs. When the network is unavailable during initialization, the provider can now fall back to previously cached flags instead of failing, making the SDK more resilient to temporary outages.

Changes:

  • Added IndexedDBFlagsCache class to persist and retrieve flag configurations from IndexedDB
  • Modified DatadogProvider to load cached flags on initialization and persist successful network fetches
  • Added comprehensive test coverage for cache operations and fallback behavior

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/browser/src/cache/indexeddb-flags-cache.ts New cache implementation for storing flag configurations in IndexedDB with graceful error handling
packages/browser/src/cache/helpers.ts Added hasIndexedDB() helper for feature detection with try-catch protection
packages/browser/src/openfeature/provider.ts Integrated cache loading on initialization and persistence after successful fetches, with fallback logic when network fails
packages/browser/test/cache/indexeddb-flags-cache.spec.ts Comprehensive unit tests for cache operations including error scenarios and data validation
packages/browser/test/openfeature/provider-persistence.spec.ts Integration tests verifying persistence behavior, network-first strategy, and graceful degradation
packages/browser/package.json Added fake-indexeddb dev dependency for testing
yarn.lock Added fake-indexeddb package resolution
LICENSE-3rdparty.csv Added fake-indexeddb license attribution

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Resolve on tx.oncomplete instead of request.onsuccess so that writes
are guaranteed to be persisted. A request can succeed but the
transaction can still fail (e.g. quota errors), which would silently
lose data under the old pattern.
private flagsConfiguration: FlagsConfiguration = {}
private configuration?: FlaggingConfiguration
private exposureCache: AssignmentCache | undefined
private flagsCache: IndexedDBFlagsCache | undefined
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll probably refactor this into an abstract type

@leoromanovsky leoromanovsky marked this pull request as ready for review February 27, 2026 07:18
@leoromanovsky leoromanovsky requested a review from a team as a code owner February 27, 2026 07:18
Copy link
Copy Markdown
Collaborator

@greghuels greghuels left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably worth addressing the comments on how this.flagsConfiguration is set.

…fig guards

Prevent cross-user flag leakage on shared computers by validating
targetingKey before using cached data, skip cache when caller provides
initialFlagsConfiguration, and isolate cache entries per SDK key.
…cache, STALE status

- Store FlagsConfiguration objects directly in IndexedDB (no JSON
  stringify/parse — IndexedDB uses the structured clone algorithm).
- Key cache entries by clientToken + hashed EvaluationContext, so
  different contexts don't collide. Removes the targetingKey comparison
  guard in provider.ts.
- Make set() fire-and-forget — cache writes never block initialize or
  onContextChange, and never put the provider into ERROR state.
- Exposure cache init runs concurrently with the network fetch.
- Emit ProviderStatus.STALE / ProviderEvents.Stale when serving cached
  config after a fetch failure (instead of READY).
- Cache read now runs concurrently with the network fetch instead of
  blocking it. Cached config is only used as fallback when fetch fails.
- Use full MD5 hash for context key (no truncation) to avoid collisions.
- Sort context keys before hashing so {a:1, b:2} and {b:2, a:1}
  produce the same cache key.
@leoromanovsky leoromanovsky merged commit 0b2a418 into main Feb 28, 2026
4 checks passed
@leoromanovsky leoromanovsky deleted the leo/add-indexeddb-flags-persistence branch February 28, 2026 20:40
@leoromanovsky leoromanovsky mentioned this pull request Mar 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants