Skip to content

Commit e27e49a

Browse files
Adds LocalStorageAssignmentCache with persistent implementation between sessions to lower assignment logging; enabled by default (FF-839) (#43)
* Adds LocalStorageAssignmentCache with persistent implementation between sessions to lower assignment logging; enabled by default (FF-839) * use commons 2.0.0 * augment test
1 parent 2b5715c commit e27e49a

File tree

6 files changed

+116
-16
lines changed

6 files changed

+116
-16
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"xhr-mock": "^2.5.1"
5858
},
5959
"dependencies": {
60-
"@eppo/js-client-sdk-common": "^2.0.0",
60+
"@eppo/js-client-sdk-common": "2.0.0",
6161
"axios": "^1.6.0",
6262
"md5": "^2.3.0"
6363
}

src/index.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from '../test/testHelpers';
1616

1717
import { EppoLocalStorage } from './local-storage';
18+
import { LocalStorageAssignmentCache } from './local-storage-assignment-cache';
1819

1920
import { EppoJSClient, IAssignmentLogger, IEppoClient, init } from './index';
2021

@@ -256,6 +257,53 @@ describe('EppoJSClient E2E test', () => {
256257
});
257258
});
258259

260+
describe('LocalStorageAssignmentCache', () => {
261+
it('typical behavior', () => {
262+
const cache = new LocalStorageAssignmentCache();
263+
expect(
264+
cache.hasLoggedAssignment({
265+
subjectKey: 'subject-1',
266+
flagKey: 'flag-1',
267+
allocationKey: 'allocation-1',
268+
variationValue: EppoValue.String('control'),
269+
}),
270+
).toEqual(false);
271+
272+
cache.setLastLoggedAssignment({
273+
subjectKey: 'subject-1',
274+
flagKey: 'flag-1',
275+
allocationKey: 'allocation-1',
276+
variationValue: EppoValue.String('control'),
277+
});
278+
279+
expect(
280+
cache.hasLoggedAssignment({
281+
subjectKey: 'subject-1',
282+
flagKey: 'flag-1',
283+
allocationKey: 'allocation-1',
284+
variationValue: EppoValue.String('control'),
285+
}),
286+
).toEqual(true); // this key has been logged
287+
288+
// change variation
289+
cache.setLastLoggedAssignment({
290+
subjectKey: 'subject-1',
291+
flagKey: 'flag-1',
292+
allocationKey: 'allocation-1',
293+
variationValue: EppoValue.String('variant'),
294+
});
295+
296+
expect(
297+
cache.hasLoggedAssignment({
298+
subjectKey: 'subject-1',
299+
flagKey: 'flag-1',
300+
allocationKey: 'allocation-1',
301+
variationValue: EppoValue.String('control'),
302+
}),
303+
).toEqual(false); // this key has not been logged
304+
});
305+
});
306+
259307
function getAssignmentsWithSubjectAttributes(
260308
subjectsWithAttributes: {
261309
subjectKey: string;

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import axios from 'axios';
1212

1313
import { EppoLocalStorage } from './local-storage';
14+
import { LocalStorageAssignmentCache } from './local-storage-assignment-cache';
1415
import { sdkName, sdkVersion } from './sdk-data';
1516

1617
/**
@@ -150,9 +151,9 @@ export async function init(config: IClientConfig): Promise<IEppoClient> {
150151
});
151152
EppoJSClient.instance.setLogger(config.assignmentLogger);
152153

153-
// default behavior is to use a non-expiring cache.
154+
// default behavior is to use a LocalStorage-based assignment cache.
154155
// this can be overridden after initialization.
155-
EppoJSClient.instance.useNonExpiringInMemoryAssignmentCache();
156+
EppoJSClient.instance.useCustomAssignmentCache(new LocalStorageAssignmentCache());
156157

157158
const configurationRequestor = new ExperimentConfigurationRequestor(localStorage, httpClient);
158159
await configurationRequestor.fetchAndStoreConfigurations();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { AssignmentCache } from '@eppo/js-client-sdk-common';
2+
3+
import { hasWindowLocalStorage } from './local-storage';
4+
5+
class LocalStorageAssignmentShim {
6+
LOCAL_STORAGE_KEY = 'EPPO_LOCAL_STORAGE_ASSIGNMENT_CACHE';
7+
8+
public has(key: string): boolean {
9+
if (!hasWindowLocalStorage()) {
10+
return false;
11+
}
12+
13+
return this.getCache().has(key);
14+
}
15+
16+
public get(key: string): string {
17+
if (!hasWindowLocalStorage()) {
18+
return null;
19+
}
20+
21+
return this.getCache().get(key);
22+
}
23+
24+
public set(key: string, value: string) {
25+
if (!hasWindowLocalStorage()) {
26+
return;
27+
}
28+
29+
const cache = this.getCache();
30+
cache.set(key, value);
31+
this.setCache(cache);
32+
}
33+
34+
private getCache(): Map<string, string> {
35+
const cache = window.localStorage.getItem(this.LOCAL_STORAGE_KEY);
36+
return cache ? new Map(JSON.parse(cache)) : new Map();
37+
}
38+
39+
private setCache(cache: Map<string, string>) {
40+
window.localStorage.setItem(
41+
this.LOCAL_STORAGE_KEY,
42+
JSON.stringify(Array.from(cache.entries())),
43+
);
44+
}
45+
}
46+
47+
export class LocalStorageAssignmentCache extends AssignmentCache<LocalStorageAssignmentShim> {
48+
constructor() {
49+
super(new LocalStorageAssignmentShim());
50+
}
51+
}

src/local-storage.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export class EppoLocalStorage {
22
public get<T>(key: string): T {
3-
if (this.hasWindowLocalStorage()) {
3+
if (hasWindowLocalStorage()) {
44
const serializedEntry = window.localStorage.getItem(key);
55
if (serializedEntry) {
66
return JSON.parse(serializedEntry);
@@ -9,21 +9,21 @@ export class EppoLocalStorage {
99
return null;
1010
}
1111

12-
// Checks whether local storage is enabled in the browser (the user might have disabled it).
13-
private hasWindowLocalStorage(): boolean {
14-
try {
15-
return typeof window !== 'undefined' && !!window.localStorage;
16-
} catch {
17-
// Chrome throws an error if local storage is disabled and you try to access it
18-
return false;
19-
}
20-
}
21-
2212
public setEntries<T>(entries: Record<string, T>) {
23-
if (this.hasWindowLocalStorage()) {
13+
if (hasWindowLocalStorage()) {
2414
Object.entries(entries).forEach(([key, val]) => {
2515
window.localStorage.setItem(key, JSON.stringify(val));
2616
});
2717
}
2818
}
2919
}
20+
21+
// Checks whether local storage is enabled in the browser (the user might have disabled it).
22+
export function hasWindowLocalStorage(): boolean {
23+
try {
24+
return typeof window !== 'undefined' && !!window.localStorage;
25+
} catch {
26+
// Chrome throws an error if local storage is disabled and you try to access it
27+
return false;
28+
}
29+
}

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@
373373
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
374374
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
375375

376-
"@eppo/js-client-sdk-common@^2.0.0":
376+
377377
version "2.0.0"
378378
resolved "https://registry.yarnpkg.com/@eppo/js-client-sdk-common/-/js-client-sdk-common-2.0.0.tgz#562006a128fb33653ca8c5e17df7680f7d91d0b4"
379379
integrity sha512-yHi4BFb6N7jISPK4y++N9OwjJBQAONSLqqCoCUHAyFSlNmUm+aNc/a+j/HHPsnXYZ1GUBWAZJOIQDNne4svfPg==

0 commit comments

Comments
 (0)