Skip to content

Commit 2f117f7

Browse files
authored
Configurable cache behavior (#73)
* cache racing start in place * additional test * clean up debug logs * cleanup from self-review of PR * clean up annotations * bump version of commons * working to mirror android behavior * local storage can expire too * isolated hybrid store * configuration factory uses update strategy * failing test in place * test passing * minor comment changes from self-review of PR * TODO for test as well * test in place for useExpiredCache * expired cache allowed * composition over inheritance * additional feedback from PR
1 parent 61475b0 commit 2f117f7

23 files changed

+873
-270
lines changed

docs/js-client-sdk.eppojsclient.getboolassignment.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
**Signature:**
88

99
```typescript
10-
getBoolAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: boolean): boolean;
10+
getBoolAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: boolean): boolean;
1111
```
1212

1313
## Parameters
@@ -16,7 +16,7 @@ getBoolAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record
1616
| --- | --- | --- |
1717
| flagKey | string | |
1818
| subjectKey | string | |
19-
| subjectAttributes | Record&lt;string, any&gt; | |
19+
| subjectAttributes | Record&lt;string, AttributeType&gt; | |
2020
| defaultValue | boolean | |
2121

2222
**Returns:**

docs/js-client-sdk.eppojsclient.getintegerassignment.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
**Signature:**
88

99
```typescript
10-
getIntegerAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: number): number;
10+
getIntegerAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: number): number;
1111
```
1212

1313
## Parameters
@@ -16,7 +16,7 @@ getIntegerAssignment(flagKey: string, subjectKey: string, subjectAttributes: Rec
1616
| --- | --- | --- |
1717
| flagKey | string | |
1818
| subjectKey | string | |
19-
| subjectAttributes | Record&lt;string, any&gt; | |
19+
| subjectAttributes | Record&lt;string, AttributeType&gt; | |
2020
| defaultValue | number | |
2121

2222
**Returns:**

docs/js-client-sdk.eppojsclient.getjsonassignment.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
**Signature:**
88

99
```typescript
10-
getJSONAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: object): object;
10+
getJSONAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: object): object;
1111
```
1212

1313
## Parameters
@@ -16,7 +16,7 @@ getJSONAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record
1616
| --- | --- | --- |
1717
| flagKey | string | |
1818
| subjectKey | string | |
19-
| subjectAttributes | Record&lt;string, any&gt; | |
19+
| subjectAttributes | Record&lt;string, AttributeType&gt; | |
2020
| defaultValue | object | |
2121

2222
**Returns:**

docs/js-client-sdk.eppojsclient.getnumericassignment.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
**Signature:**
88

99
```typescript
10-
getNumericAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: number): number;
10+
getNumericAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: number): number;
1111
```
1212

1313
## Parameters
@@ -16,7 +16,7 @@ getNumericAssignment(flagKey: string, subjectKey: string, subjectAttributes: Rec
1616
| --- | --- | --- |
1717
| flagKey | string | |
1818
| subjectKey | string | |
19-
| subjectAttributes | Record&lt;string, any&gt; | |
19+
| subjectAttributes | Record&lt;string, AttributeType&gt; | |
2020
| defaultValue | number | |
2121

2222
**Returns:**

docs/js-client-sdk.eppojsclient.getstringassignment.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
**Signature:**
88

99
```typescript
10-
getStringAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: string): string;
10+
getStringAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: string): string;
1111
```
1212

1313
## Parameters
@@ -16,7 +16,7 @@ getStringAssignment(flagKey: string, subjectKey: string, subjectAttributes: Reco
1616
| --- | --- | --- |
1717
| flagKey | string | |
1818
| subjectKey | string | |
19-
| subjectAttributes | Record&lt;string, any&gt; | |
19+
| subjectAttributes | Record&lt;string, AttributeType&gt; | |
2020
| defaultValue | string | |
2121

2222
**Returns:**

js-client-sdk.api.md

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,43 @@
66

77
/// <reference types="chrome" />
88

9+
import { AttributeType } from '@eppo/js-client-sdk-common';
910
import { EppoClient } from '@eppo/js-client-sdk-common';
1011
import { Flag } from '@eppo/js-client-sdk-common';
1112
import { IAssignmentEvent } from '@eppo/js-client-sdk-common';
1213
import { IAssignmentLogger } from '@eppo/js-client-sdk-common';
1314
import { IAsyncStore } from '@eppo/js-client-sdk-common';
1415
import { IEppoClient } from '@eppo/js-client-sdk-common';
1516

16-
// @public (undocumented)
17-
export class ChromeStorageAsyncStore<T> implements IAsyncStore<T> {
18-
constructor(storageArea: chrome.storage.StorageArea, cooldownSeconds?: number | undefined);
17+
// Warning: (ae-forgotten-export) The symbol "IStringStorageEngine" needs to be exported by the entry point index.d.ts
18+
//
19+
// @public
20+
export class ChromeStorageEngine implements IStringStorageEngine {
21+
constructor(storageArea: chrome.storage.StorageArea);
1922
// (undocumented)
20-
getEntries(): Promise<Record<string, T>>;
23+
getContentsJsonString: () => Promise<string | null>;
2124
// (undocumented)
22-
isExpired(): Promise<boolean>;
25+
getMetaJsonString: () => Promise<string | null>;
2326
// (undocumented)
24-
isInitialized(): boolean;
27+
setContentsJsonString: (configurationJsonString: string) => Promise<void>;
2528
// (undocumented)
26-
setEntries(entries: Record<string, T>): Promise<void>;
29+
setMetaJsonString: (metaJsonString: string) => Promise<void>;
2730
}
2831

2932
// @public
3033
export class EppoJSClient extends EppoClient {
31-
// (undocumented)
32-
getBoolAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: boolean): boolean;
34+
// @deprecated (undocumented)
35+
getBoolAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: boolean): boolean;
3336
// (undocumented)
3437
getBooleanAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: boolean): boolean;
3538
// (undocumented)
36-
getIntegerAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: number): number;
39+
getIntegerAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: number): number;
3740
// (undocumented)
38-
getJSONAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: object): object;
41+
getJSONAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: object): object;
3942
// (undocumented)
40-
getNumericAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: number): number;
43+
getNumericAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: number): number;
4144
// (undocumented)
42-
getStringAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, any>, defaultValue: string): string;
45+
getStringAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: string): string;
4346
// (undocumented)
4447
static initialized: boolean;
4548
// (undocumented)
@@ -60,6 +63,7 @@ export interface IClientConfig {
6063
apiKey: string;
6164
assignmentLogger: IAssignmentLogger;
6265
baseUrl?: string;
66+
maxCacheAgeSeconds?: number;
6367
numInitialRequestRetries?: number;
6468
numPollRequestRetries?: number;
6569
persistentStore?: IAsyncStore<Flag>;
@@ -68,6 +72,9 @@ export interface IClientConfig {
6872
requestTimeoutMs?: number;
6973
skipInitialRequest?: boolean;
7074
throwOnFailedInitialization?: boolean;
75+
// Warning: (ae-forgotten-export) The symbol "ServingStoreUpdateStrategy" needs to be exported by the entry point index.d.ts
76+
updateOnFetch?: ServingStoreUpdateStrategy;
77+
useExpiredCache?: boolean;
7178
}
7279

7380
export { IEppoClient }

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
"@microsoft/api-extractor": "^7.38.0",
3535
"@types/chrome": "^0.0.268",
3636
"@types/jest": "^29.5.11",
37-
"@types/md5": "^2.3.2",
3837
"@typescript-eslint/eslint-plugin": "^5.13.0",
3938
"@typescript-eslint/parser": "^5.13.0",
4039
"eslint": "^8.17.0",

src/chrome-storage-engine.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { CONFIGURATION_KEY, META_KEY } from './storage-key-constants';
2+
import { IStringStorageEngine } from './string-valued.store';
3+
4+
/**
5+
* Chrome storage implementation of a string-valued store for storing a configuration and its metadata.
6+
*
7+
* This serializes the entire configuration object into a string and then stores it to a single key
8+
* within the object for another single top-level key.
9+
* Same with metadata about the store (e.g., when it was last updated).
10+
*/
11+
export class ChromeStorageEngine implements IStringStorageEngine {
12+
private chromeStorageKey = 'eppo-sdk';
13+
14+
public constructor(private storageArea: chrome.storage.StorageArea) {}
15+
16+
public getContentsJsonString = async (): Promise<string | null> => {
17+
const storage = await this.storageArea.get(this.chromeStorageKey);
18+
return storage?.[CONFIGURATION_KEY] ?? null;
19+
};
20+
21+
public getMetaJsonString = async (): Promise<string | null> => {
22+
const storage = await this.storageArea.get(this.chromeStorageKey);
23+
return storage?.[META_KEY] ?? null;
24+
};
25+
26+
public setContentsJsonString = async (configurationJsonString: string): Promise<void> => {
27+
await this.storageArea.set({
28+
[CONFIGURATION_KEY]: configurationJsonString,
29+
});
30+
};
31+
32+
public setMetaJsonString = async (metaJsonString: string): Promise<void> => {
33+
await this.storageArea.set({
34+
[META_KEY]: metaJsonString,
35+
});
36+
};
37+
}

src/chrome.configuration-store.spec.ts renamed to src/chrome-storage.store.spec.ts

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,24 @@
1-
import { ChromeStorageAsyncStore } from './chrome.configuration-store';
1+
import { IAsyncStore } from '@eppo/js-client-sdk-common';
22

3-
describe('ChromeStore', () => {
3+
import { ChromeStorageEngine } from './chrome-storage-engine';
4+
import { StringValuedAsyncStore } from './string-valued.store';
5+
6+
import StorageArea = chrome.storage.StorageArea;
7+
8+
describe('ChromeStorageStore', () => {
49
const mockEntries: Record<string, string> = { key1: 'value1', key2: 'value2' };
5-
let chromeStore: ChromeStorageAsyncStore<string>;
6-
let extendedStorageLocal: chrome.storage.StorageArea;
10+
const storageGetFake = jest.fn();
11+
const chromeStorageEngine = new ChromeStorageEngine({
12+
get: storageGetFake,
13+
set: jest.fn(),
14+
} as unknown as StorageArea);
15+
let chromeStore: IAsyncStore<unknown>;
716
let now: number;
817

918
beforeEach(() => {
1019
now = Date.now();
11-
1220
jest.useFakeTimers();
13-
14-
const get = jest.fn();
15-
const set = jest.fn();
16-
extendedStorageLocal = {
17-
set,
18-
get,
19-
clear: jest.fn(),
20-
remove: jest.fn(),
21-
getBytesInUse: jest.fn(),
22-
setAccessLevel: jest.fn(),
23-
onChanged: {
24-
addListener: jest.fn(),
25-
removeListener: jest.fn(),
26-
getRules: jest.fn(),
27-
hasListener: jest.fn(),
28-
removeRules: jest.fn(),
29-
addRules: jest.fn(),
30-
hasListeners: jest.fn(),
31-
},
32-
};
33-
34-
chromeStore = new ChromeStorageAsyncStore(extendedStorageLocal);
21+
chromeStore = new StringValuedAsyncStore(chromeStorageEngine);
3522
});
3623

3724
afterEach(() => {
@@ -40,14 +27,14 @@ describe('ChromeStore', () => {
4027
});
4128

4229
it('is always expired without cooldown', async () => {
43-
chromeStore = new ChromeStorageAsyncStore(extendedStorageLocal, undefined);
30+
chromeStore = new StringValuedAsyncStore(chromeStorageEngine, undefined);
4431
expect(await chromeStore.isExpired()).toBe(true);
4532
});
4633

4734
it('is not expired with cooldown', async () => {
48-
chromeStore = new ChromeStorageAsyncStore(extendedStorageLocal, 10);
35+
chromeStore = new StringValuedAsyncStore(chromeStorageEngine, 10);
4936

50-
(extendedStorageLocal.get as jest.Mock).mockImplementation(() => {
37+
storageGetFake.mockImplementation(() => {
5138
return Promise.resolve({
5239
['eppo-configuration']: JSON.stringify(mockEntries),
5340
['eppo-configuration-meta']: JSON.stringify({
@@ -60,9 +47,9 @@ describe('ChromeStore', () => {
6047
});
6148

6249
it('is expired after cooldown', async () => {
63-
chromeStore = new ChromeStorageAsyncStore(extendedStorageLocal, 10);
50+
chromeStore = new StringValuedAsyncStore(chromeStorageEngine, 10);
6451

65-
(extendedStorageLocal.get as jest.Mock).mockImplementation(() => {
52+
storageGetFake.mockImplementation(() => {
6653
return Promise.resolve({
6754
['eppo-configuration']: JSON.stringify(mockEntries),
6855
['eppo-configuration-meta']: JSON.stringify({
@@ -86,7 +73,7 @@ describe('ChromeStore', () => {
8673
});
8774

8875
it('should get entries', async () => {
89-
(extendedStorageLocal.get as jest.Mock).mockImplementation(() => {
76+
storageGetFake.mockImplementation(() => {
9077
return Promise.resolve({ ['eppo-configuration']: JSON.stringify(mockEntries) });
9178
});
9279

src/chrome.configuration-store.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)