Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/js-client-sdk.iclientconfigsync.configfetchedat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@eppo/js-client-sdk](./js-client-sdk.md) &gt; [IClientConfigSync](./js-client-sdk.iclientconfigsync.md) &gt; [configFetchedAt](./js-client-sdk.iclientconfigsync.configfetchedat.md)

## IClientConfigSync.configFetchedAt property

**Signature:**

```typescript
configFetchedAt?: string;
```
11 changes: 11 additions & 0 deletions docs/js-client-sdk.iclientconfigsync.configpublishedat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@eppo/js-client-sdk](./js-client-sdk.md) &gt; [IClientConfigSync](./js-client-sdk.iclientconfigsync.md) &gt; [configPublishedAt](./js-client-sdk.iclientconfigsync.configpublishedat.md)

## IClientConfigSync.configPublishedAt property

**Signature:**

```typescript
configPublishedAt?: string;
```
38 changes: 38 additions & 0 deletions docs/js-client-sdk.iclientconfigsync.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,44 @@ IBanditLogger
_(Optional)_


</td></tr>
<tr><td>

[configFetchedAt?](./js-client-sdk.iclientconfigsync.configfetchedat.md)


</td><td>


</td><td>

string


</td><td>

_(Optional)_


</td></tr>
<tr><td>

[configPublishedAt?](./js-client-sdk.iclientconfigsync.configpublishedat.md)


</td><td>


</td><td>

string


</td><td>

_(Optional)_


</td></tr>
<tr><td>

Expand Down
4 changes: 4 additions & 0 deletions js-client-sdk.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ export interface IClientConfigSync {
// (undocumented)
banditLogger?: IBanditLogger;
// (undocumented)
configFetchedAt?: string;
// (undocumented)
configPublishedAt?: string;
// (undocumented)
enableOverrides?: boolean;
// (undocumented)
flagsConfiguration: Record<string, Flag | ObfuscatedFlag>;
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eppo/js-client-sdk",
"version": "3.16.1",
"version": "3.17.0-alpha.1",
"description": "Eppo SDK for client-side JavaScript applications",
"main": "dist/index.js",
"files": [
Expand Down Expand Up @@ -62,5 +62,9 @@
"@types/chrome": "^0.0.313",
"@eppo/js-client-sdk-common": "4.15.1"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"volta": {
"node": "22.18.0",
"yarn": "1.22.22"
}
}
78 changes: 78 additions & 0 deletions src/configuration-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,45 @@
expect(result).toBeInstanceOf(HybridConfigurationStore);
});

it('is a HybridConfigurationStore with Web Cache API when hasWebCacheAPI is true', () => {
// Mock caches API
const mockCache = {
match: jest.fn(),
put: jest.fn(),
add: jest.fn(),
addAll: jest.fn(),
delete: jest.fn(),
keys: jest.fn(),
};
const mockCaches = {
open: jest.fn().mockResolvedValue(mockCache),
delete: jest.fn(),
has: jest.fn(),
keys: jest.fn(),
match: jest.fn(),
};
const originalCaches = (global as any).caches;

Check warning on line 75 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Unexpected any. Specify a different type

Check warning on line 75 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Unexpected any. Specify a different type

Check warning on line 75 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Unexpected any. Specify a different type

Check warning on line 75 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Unexpected any. Specify a different type
(global as any).caches = mockCaches;

Check warning on line 76 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Unexpected any. Specify a different type

Check warning on line 76 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Unexpected any. Specify a different type

Check warning on line 76 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Unexpected any. Specify a different type

Check warning on line 76 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Unexpected any. Specify a different type

const mockLocalStorage = {
clear: jest.fn(),
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
key: jest.fn(),
length: 0,
};

const result = configurationStorageFactory(
{ hasWebCacheAPI: true },
{ windowLocalStorage: mockLocalStorage },
);
expect(result).toBeInstanceOf(HybridConfigurationStore);

// Clean up
(global as any).caches = originalCaches;

Check warning on line 94 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Unexpected any. Specify a different type

Check warning on line 94 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Unexpected any. Specify a different type

Check warning on line 94 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Unexpected any. Specify a different type

Check warning on line 94 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Unexpected any. Specify a different type
});

it('is a HybridConfigurationStore with a LocalStorageBackedAsyncStore persistentStore when window local storage is available', () => {
const mockLocalStorage = {
clear: jest.fn(),
Expand All @@ -72,6 +111,45 @@
expect(result).toBeInstanceOf(HybridConfigurationStore);
});

it('prefers Web Cache API over localStorage when both are available', () => {
// Mock caches API
const mockCache = {
match: jest.fn(),
put: jest.fn(),
add: jest.fn(),
addAll: jest.fn(),
delete: jest.fn(),
keys: jest.fn(),
};
const mockCaches = {
open: jest.fn().mockResolvedValue(mockCache),
delete: jest.fn(),
has: jest.fn(),
keys: jest.fn(),
match: jest.fn(),
};
const originalCaches = (global as any).caches;

Check warning on line 131 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Unexpected any. Specify a different type

Check warning on line 131 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Unexpected any. Specify a different type

Check warning on line 131 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Unexpected any. Specify a different type

Check warning on line 131 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Unexpected any. Specify a different type
(global as any).caches = mockCaches;

Check warning on line 132 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Unexpected any. Specify a different type

Check warning on line 132 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Unexpected any. Specify a different type

Check warning on line 132 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Unexpected any. Specify a different type

Check warning on line 132 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Unexpected any. Specify a different type

const mockLocalStorage = {
clear: jest.fn(),
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
key: jest.fn(),
length: 0,
};

const result = configurationStorageFactory(
{ hasWebCacheAPI: true, hasWindowLocalStorage: true },
{ windowLocalStorage: mockLocalStorage },
);
expect(result).toBeInstanceOf(HybridConfigurationStore);

// Clean up
(global as any).caches = originalCaches;

Check warning on line 150 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Unexpected any. Specify a different type

Check warning on line 150 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Unexpected any. Specify a different type

Check warning on line 150 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Unexpected any. Specify a different type

Check warning on line 150 in src/configuration-factory.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Unexpected any. Specify a different type
});

it('falls back to MemoryOnlyConfigurationStore when no persistence options are available', () => {
const result = configurationStorageFactory({});
expect(result).toBeInstanceOf(MemoryOnlyConfigurationStore);
Expand Down
31 changes: 31 additions & 0 deletions src/configuration-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import {

import ChromeStorageAsyncMap from './cache/chrome-storage-async-map';
import { ChromeStorageEngine } from './chrome-storage-engine';
import { ConfigurationAsyncStore } from './configuration-store';
import {
IsolatableHybridConfigurationStore,
ServingStoreUpdateStrategy,
} from './isolatable-hybrid.store';
import { LocalStorageEngine } from './local-storage-engine';
import { MigrationManager } from './migrations';
import { OVERRIDES_KEY } from './storage-key-constants';
import { StringValuedAsyncStore } from './string-valued.store';
import { WebCacheStorageEngine } from './web-cache-storage-engine';

export function precomputedFlagsStorageFactory(): IConfigurationStore<PrecomputedFlag> {
return new MemoryOnlyConfigurationStore();
Expand All @@ -33,13 +36,15 @@ export function configurationStorageFactory(
maxAgeSeconds = 0,
servingStoreUpdateStrategy = 'always',
hasChromeStorage = false,
hasWebCacheAPI = false,
hasWindowLocalStorage = false,
persistentStore = undefined,
forceMemoryOnly = false,
}: {
maxAgeSeconds?: number;
servingStoreUpdateStrategy?: ServingStoreUpdateStrategy;
hasChromeStorage?: boolean;
hasWebCacheAPI?: boolean;
hasWindowLocalStorage?: boolean;
persistentStore?: IAsyncStore<Flag>;
forceMemoryOnly?: boolean;
Expand Down Expand Up @@ -73,6 +78,22 @@ export function configurationStorageFactory(
new StringValuedAsyncStore<Flag>(chromeStorageEngine, maxAgeSeconds),
servingStoreUpdateStrategy,
);
} else if (hasWebCacheAPI) {
// Web Cache API is available and preferred for better storage limits
// Run migration from localStorage to Cache API only if localStorage exists
if (windowLocalStorage) {
const migrationManager = new MigrationManager(windowLocalStorage);
migrationManager
.runPendingMigrations()
.catch((error) => console.warn('Storage migration failed:', error));
}

const webCacheEngine = new WebCacheStorageEngine(storageKeySuffix ?? '');
return new IsolatableHybridConfigurationStore(
new MemoryStore<Flag>(),
new ConfigurationAsyncStore<Flag>(webCacheEngine, maxAgeSeconds),
servingStoreUpdateStrategy,
);
} else if (hasWindowLocalStorage && windowLocalStorage) {
// window.localStorage is available, use it as a fallback
const localStorageEngine = new LocalStorageEngine(windowLocalStorage, storageKeySuffix ?? '');
Expand Down Expand Up @@ -135,3 +156,13 @@ export function hasWindowLocalStorage(): boolean {
export function localStorageIfAvailable(): Storage | undefined {
return hasWindowLocalStorage() ? window.localStorage : undefined;
}

/** Returns whether Web Cache API is available */
export function hasWebCacheAPI(): boolean {
try {
return typeof caches !== 'undefined' && typeof caches.open === 'function';
} catch {
// Some environments may throw when accessing caches
return false;
}
}
67 changes: 67 additions & 0 deletions src/configuration-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { IAsyncStore } from '@eppo/js-client-sdk-common';

/**
* Simplified storage engine interface for configuration data with built-in timestamps.
* Unlike IStringStorageEngine, this doesn't require separate metadata storage since
* configuration responses include createdAt timestamps.
*/
export interface IConfigurationStorageEngine {
getContentsJsonString: () => Promise<string | null>;
setContentsJsonString: (configurationJsonString: string) => Promise<void>;
}

/**
* Configuration store that works with storage engines optimized for configuration data.
* Uses the configuration response's built-in createdAt timestamp for expiration logic
* instead of maintaining separate metadata.
*/
export class ConfigurationAsyncStore<T> implements IAsyncStore<T> {
private initialized = false;

public constructor(
private storageEngine: IConfigurationStorageEngine,
private cooldownSeconds = 0,
) {}

public isInitialized(): boolean {
return this.initialized;
}

public async isExpired(): Promise<boolean> {
if (!this.cooldownSeconds) {
return true;
}

try {
const contentsJsonString = await this.storageEngine.getContentsJsonString();
if (!contentsJsonString) {
return true;
}

const contents = JSON.parse(contentsJsonString);

Check warning on line 42 in src/configuration-store.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Delete `Β·Β·Β·Β·Β·Β·`

Check warning on line 42 in src/configuration-store.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Delete `Β·Β·Β·Β·Β·Β·`

Check warning on line 42 in src/configuration-store.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Delete `Β·Β·Β·Β·Β·Β·`

Check warning on line 42 in src/configuration-store.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Delete `Β·Β·Β·Β·Β·Β·`
// Check if this is a configuration response with createdAt
if (contents.createdAt) {
const createdAtMs = new Date(contents.createdAt).getTime();
return Date.now() - createdAtMs > this.cooldownSeconds * 1000;
}

// Fallback: if no createdAt, consider expired
return true;
} catch (error) {
console.warn('Failed to check expiration:', error);
return true;
}
}

public async entries(): Promise<Record<string, T>> {
const contentsJsonString = await this.storageEngine.getContentsJsonString();
return contentsJsonString ? JSON.parse(contentsJsonString) : {};
}

public async setEntries(entries: Record<string, T>): Promise<void> {
// Store the entire configuration response as-is
await this.storageEngine.setContentsJsonString(JSON.stringify(entries));
this.initialized = true;
}
}

Check warning on line 67 in src/configuration-store.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Insert `⏎`

Check warning on line 67 in src/configuration-store.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Insert `⏎`

Check warning on line 67 in src/configuration-store.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Insert `⏎`

Check warning on line 67 in src/configuration-store.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Insert `⏎`
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
chromeStorageIfAvailable,
configurationStorageFactory,
hasChromeStorage,
hasWebCacheAPI,
hasWindowLocalStorage,
localStorageIfAvailable,
overrideStorageFactory,
Expand Down Expand Up @@ -330,6 +331,7 @@ export class EppoJSClient extends EppoClient {
servingStoreUpdateStrategy: updateOnFetch,
persistentStore,
hasChromeStorage: hasChromeStorage(),
hasWebCacheAPI: hasWebCacheAPI(),
hasWindowLocalStorage: hasWindowLocalStorage(),
},
{
Expand Down
Loading
Loading