diff --git a/__mocks__/setup.ts b/__mocks__/setup.ts index 89cc96b..5c15ac8 100644 --- a/__mocks__/setup.ts +++ b/__mocks__/setup.ts @@ -9,7 +9,7 @@ jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage); /* * Mock navigator to avoid undefined access in analytics-connector */ -global['navigator'] = { product: 'ReactNative' }; +global['navigator'] = { product: 'ReactNative' } as unknown as Navigator; /* * Mock Native Module diff --git a/__tests__/client.test.ts b/__tests__/client.test.ts index 5f4cdda..3716c08 100644 --- a/__tests__/client.test.ts +++ b/__tests__/client.test.ts @@ -8,6 +8,7 @@ import { ConnectorExposureTrackingProvider } from '../src/integration/connector' import { FetchOptions } from '../src/types/client'; import { ExposureTrackingProvider } from '../src/types/exposure'; import { Source } from '../src/types/source'; +import { Storage } from '../src/types/storage'; import { HttpClient, SimpleResponse } from '../src/types/transport'; import { ExperimentUser, ExperimentUserProvider } from '../src/types/user'; import { Variant, Variants } from '../src/types/variant'; @@ -1012,6 +1013,65 @@ describe('start', () => { expect(variant.key).toEqual('on'); expect(variant2.key).toEqual('on'); }); + + test('start with custom storage', async () => { + // Create a custom storage object + const storageObject = {}; + const storage = { + get: async (key: string) => storageObject[key], + put: async (key: string, value: string) => { + storageObject[key] = value; + }, + delete: async (key: string) => { + delete storageObject[key]; + }, + } as Storage; + + // Create a client with the custom storage object + const client = new ExperimentClient(API_KEY, { storage }); + await client.start({ device_id: 'test_device' }); + + // Check that the flags are stored in the storage object + const storageKey = `amp-exp-$default_instance-${API_KEY.substring( + API_KEY.length - 6, + )}`; + expect( + JSON.parse(storageObject[storageKey + '-flags'])[serverKey], + ).toMatchObject({ + key: serverKey, + }); + // Check that the variant is stored in the storage object + expect(JSON.parse(storageObject[storageKey])[serverKey]).toMatchObject({ + key: 'off', + }); + }); +}); + +test('fetch with custom storage', async () => { + // Create a custom storage object + const storageObject = {}; + const storage = { + get: async (key: string) => storageObject[key], + put: async (key: string, value: string) => { + storageObject[key] = value; + }, + delete: async (key: string) => { + delete storageObject[key]; + }, + } as Storage; + + // Create a client with the custom storage object + const client = new ExperimentClient(API_KEY, { storage }); + await client.fetch(testUser); + + // Check that the variant is stored in the storage object + const storageKey = `amp-exp-$default_instance-${API_KEY.substring( + API_KEY.length - 6, + )}`; + expect(JSON.parse(storageObject[storageKey])[serverKey]).toMatchObject({ + key: 'on', + value: 'on', + }); }); describe('fetch retry with different response codes', () => { diff --git a/src/experimentClient.ts b/src/experimentClient.ts index 18031a8..2cfd583 100644 --- a/src/experimentClient.ts +++ b/src/experimentClient.ts @@ -142,7 +142,7 @@ export class ExperimentClient implements Client { httpClient, ); // Storage & Caching - const storage = new LocalStorage(); + const storage = this.config.storage || new LocalStorage(); this.variants = getVariantStorage( this.apiKey, this.config.instanceName, diff --git a/src/index.ts b/src/index.ts index 09b3ed0..c588ab5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,5 +7,6 @@ export { Source } from './types/source'; export * from './types/user'; export * from './types/variant'; export * from './types/exposure'; +export * from './types/storage'; export { Logger, LogLevel } from './types/logger'; export { ConsoleLogger } from './logger/consoleLogger'; diff --git a/src/storage/cache.ts b/src/storage/cache.ts index 6c761ce..afe0f9d 100644 --- a/src/storage/cache.ts +++ b/src/storage/cache.ts @@ -3,8 +3,6 @@ import { EvaluationFlag, GetVariantsOptions } from '@amplitude/experiment-core'; import { Storage } from '../types/storage'; import { Variant } from '../types/variant'; -import { LocalStorage } from './local-storage'; - export const getVariantStorage = ( deploymentKey: string, instanceName: string, @@ -22,7 +20,7 @@ export const getVariantStorage = ( export const getFlagStorage = ( deploymentKey: string, instanceName: string, - storage: Storage = new LocalStorage(), + storage: Storage, ): LoadStoreCache => { const truncatedDeployment = deploymentKey.substring(deploymentKey.length - 6); const namespace = `amp-exp-${instanceName}-${truncatedDeployment}-flags`; @@ -32,7 +30,7 @@ export const getFlagStorage = ( export const getVariantsOptionsStorage = ( deploymentKey: string, instanceName: string, - storage: Storage = new LocalStorage(), + storage: Storage, ): SingleValueStoreCache => { const truncatedDeployment = deploymentKey.substring(deploymentKey.length - 6); const namespace = `amp-exp-${instanceName}-${truncatedDeployment}-variants-options`; diff --git a/src/storage/local-storage.ts b/src/storage/local-storage.ts index ea2ef95..1ed84b1 100644 --- a/src/storage/local-storage.ts +++ b/src/storage/local-storage.ts @@ -3,7 +3,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { Storage } from '../types/storage'; export class LocalStorage implements Storage { - async get(key: string): Promise { + async get(key: string): Promise { return await AsyncStorage.getItem(key); } diff --git a/src/types/config.ts b/src/types/config.ts index 82e08f9..7e83b74 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -3,6 +3,7 @@ import { FetchHttpClient } from '../transport/http'; import { ExposureTrackingProvider } from './exposure'; import { Logger, LogLevel } from './logger'; import { Source } from './source'; +import { Storage } from './storage'; import { HttpClient } from './transport'; import { ExperimentUserProvider } from './user'; import { Variant, Variants } from './variant'; @@ -146,6 +147,12 @@ export interface ExperimentConfig { * (Advanced) Use your own http client. */ httpClient?: HttpClient; + + /** + * (Advanced) Use your own storage implementation. + * If not provided, the client will use the default local storage implementation, which is AsyncStorage. + */ + storage?: Storage; } /** @@ -198,4 +205,5 @@ export const Defaults: ExperimentConfig = { userProvider: null, exposureTrackingProvider: null, httpClient: FetchHttpClient, + storage: null, }; diff --git a/src/types/storage.ts b/src/types/storage.ts index 36b1cb9..a4933fe 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -1,5 +1,5 @@ export interface Storage { - get(key: string): Promise; + get(key: string): Promise; put(key: string, value: string): Promise; delete(key: string): Promise; }