Skip to content

Commit 3103f2c

Browse files
authored
feat: added local persistence to ofrep-web provider (#1508)
Signed-off-by: Salaber <jason.salaber@dynatrace.com>
1 parent 4bf078d commit 3103f2c

9 files changed

Lines changed: 982 additions & 69 deletions

File tree

libs/providers/ofrep-web/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,33 @@ OpenFeature.setProvider(
8282
);
8383
```
8484

85+
### Caching
86+
87+
The provider supports persistent local caching via `localStorage` to reduce latency on startup and improve resilience to transient network failures. Caching is controlled with three options.
88+
89+
**`cacheMode`** — controls the startup strategy:
90+
91+
- `'local-cache-first'` _(default)_`initialize()` resolves immediately from the persisted cache if one exists, then refreshes from the network in the background. Evaluations served before the refresh completes will have reason `CACHED`.
92+
- `'network-first'``initialize()` blocks on the network request. The persisted cache is used as a fallback only on transient failures (network unavailable, timeout, 5xx). Auth and configuration errors (400, 401, 403, 404) are always surfaced immediately and never masked by cached values.
93+
- `'disabled'` — no persistence. `initialize()` always blocks on the network and the other cache options have no effect.
94+
95+
**`cacheTTL`** — maximum age in seconds of a persisted cache entry before it is treated as a miss and removed. Defaults to `2_592_000` (30 days).
96+
97+
**`cacheKeyPrefix`** — a string included in the cache key to avoid collisions when multiple provider instances share the same browser origin. A good value is the OFREP base URL or a project key.
98+
99+
```ts
100+
import { OFREPWebProvider } from '@openfeature/ofrep-web-provider';
101+
102+
OpenFeature.setProvider(
103+
new OFREPWebProvider({
104+
baseUrl: 'https://localhost:8080',
105+
cacheMode: 'local-cache-first',
106+
cacheTTL: 3600, // 1 hour
107+
cacheKeyPrefix: 'my-app',
108+
}),
109+
);
110+
```
111+
85112
### Fetch implementation
86113

87114
If needed, a custom fetch implementation can be injected, if e.g. the platform does not have fetch built in.

libs/providers/ofrep-web/jest.polyfills.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,25 @@ Object.defineProperties(globalThis, {
3030
Request: { value: Request },
3131
Response: { value: Response },
3232
});
33+
34+
const localStorageData = new Map();
35+
// eslint-disable-next-line no-undef
36+
Object.defineProperty(globalThis, 'localStorage', {
37+
value: {
38+
getItem: (key) => (localStorageData.has(key) ? localStorageData.get(key) : null),
39+
setItem: (key, value) => {
40+
localStorageData.set(String(key), String(value));
41+
},
42+
removeItem: (key) => {
43+
localStorageData.delete(String(key));
44+
},
45+
clear: () => {
46+
localStorageData.clear();
47+
},
48+
key: (index) => Array.from(localStorageData.keys())[index] ?? null,
49+
get length() {
50+
return localStorageData.size;
51+
},
52+
},
53+
writable: true,
54+
});
Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,51 @@
11
import type { OFREPProviderBaseOptions } from '@openfeature/ofrep-core';
22

3+
export type CacheMode = 'local-cache-first' | 'network-first' | 'disabled';
4+
5+
/** Default cache TTL: 30 days in seconds. */
6+
export const DEFAULT_CACHE_TTL_SECONDS = 30 * 24 * 60 * 60;
7+
38
export type OFREPWebProviderOptions = OFREPProviderBaseOptions & {
49
/**
5-
* pollInterval is the time in milliseconds to wait between we call the OFREP
10+
* pollInterval is the time in milliseconds to wait between calls to the OFREP
611
* API to get the latest evaluation of your flags.
712
*
8-
* If a negative number is provided, the provider will not poll the OFREP API.
13+
* If the value is 0 or negative, polling is disabled.
914
* Default: 30000
1015
*/
11-
pollInterval?: number; // in milliseconds
16+
pollInterval?: number;
17+
18+
/**
19+
* cacheMode controls whether and how the provider uses local persistent storage.
20+
*
21+
* - `'local-cache-first'` (default): load from the persisted cache immediately on startup
22+
* so `initialize()` can return right away, then refresh from the network in the background.
23+
* - `'network-first'`: block `initialize()` on the network request and only fall back to the
24+
* persisted cache on transient or server errors (network unavailable, 5xx, timeout).
25+
* Auth and configuration errors (401, 403, 400) are surfaced immediately and never masked
26+
* by cached values.
27+
* - `'disabled'`: no persistence at all. `initialize()` always blocks on the network.
28+
* Persistence-related options have no effect.
29+
*
30+
* Default: `'local-cache-first'`
31+
*/
32+
cacheMode?: CacheMode;
33+
34+
/**
35+
* cacheTTL is the maximum age (in seconds) of a persisted cache entry before it is
36+
* treated as a cache miss. Expired entries are removed from storage on read.
37+
*
38+
* Default: 2_592_000 (30 days)
39+
*/
40+
cacheTTL?: number;
41+
42+
/**
43+
* cacheKeyPrefix is included in the cache key hash to prevent collisions when multiple
44+
* OFREP provider instances share the same storage partition (e.g. the same browser origin).
45+
* When set, the cache key becomes `hash(cacheKeyPrefix + ":" + targetingKey)`.
46+
*
47+
* A sensible value is the OFREP base URL, a project key, or any other string that
48+
* uniquely identifies this provider instance.
49+
*/
50+
cacheKeyPrefix?: string;
1251
};

0 commit comments

Comments
 (0)