Skip to content
Merged
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
70 changes: 70 additions & 0 deletions packages/sdk/react-native/src/RNOptions.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally the options.ts file validates options.

The validation is only cursory and many examples are available in the common client Configuration.ts.

In this instance we should validate that storage is an object.

The other thing we need to do is remove it from the options passed to the base configuration. Otherwise it will log that it has received an unknown configuration.

Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
import { LDOptions } from '@launchdarkly/js-client-sdk-common';

/**
* Interface for providing custom storage implementations for react Native.
*
* This interface should only be used when customizing the storage mechanism
* used by the SDK. Typical usage of the SDK does not require implementing
* this interface.
*
* Implementations may not throw exceptions.
*
* The SDK assumes that the persistence is only being used by a single instance
* of the SDK per SDK key (two different SDK instances, with 2 different SDK
* keys could use the same persistence instance).
*
* The SDK, with correct usage, will not have overlapping writes to the same
* key.
*
* This interface does not depend on the ability to list the contents of the
* store or namespaces. This is to maintain the simplicity of implementing a
* key-value store on many platforms.
*/
export interface RNStorage {
/**
* Implementation Note: This is the same as the platform storage interface.
* The implementation is duplicated to avoid exposing the internal platform
* details from implementors. This allows for us to modify the internal
* interface without breaking external implementations.
*/

/**
* Get a value from the storage.
*
* @param key The key to get a value for.
* @returns A promise which resolves to the value for the specified key, or
* null if there is no value for the key.
*/
get: (key: string) => Promise<string | null>;

/**
* Set the given key to the specified value.
*
* @param key The key to set a value for.
* @param value The value to set for the key.
* @returns A promise that resolves after the operation completes.
*/
set: (key: string, value: string) => Promise<void>;

/**
* Clear the value associated with a given key.
*
* After clearing a key subsequent calls to the get function should return
* null for that key.
*
* @param key The key to clear the value for.
* @returns A promise that resolves after that operation completes.
*/
clear: (key: string) => Promise<void>;
}

export interface RNSpecificOptions {
/**
* Some platforms (windows, web, mac, linux) can continue executing code
Expand All @@ -25,6 +83,18 @@ export interface RNSpecificOptions {
* Defaults to true.
*/
readonly automaticBackgroundHandling?: boolean;

/**
* Custom storage implementation.
*
* Typical SDK usage will not involve using customized storage.
*
* Storage is used used for caching flag values for context as well as persisting generated
* identifiers. Storage could be used for additional features in the future.
*
* Defaults to @react-native-async-storage/async-storage.
*/
readonly storage?: RNStorage;
}

export default interface RNOptions extends LDOptions, RNSpecificOptions {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AutoEnvAttributes, LDLogger } from '@launchdarkly/js-client-sdk-common';

import ReactNativeLDClient from './ReactNativeLDClient';

it('uses custom storage', async () => {
// This test just validates that the custom storage instance is being called.
// Other tests validate how the SDK interacts with storage generally.
const logger: LDLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
const myStorage = {
get: jest.fn(),
set: jest.fn(),
clear: jest.fn(),
};
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
sendEvents: false,
initialConnectionMode: 'offline',
logger,
storage: myStorage,
});

await client.identify({ key: 'potato', kind: 'user' }, { timeout: 15 });
expect(myStorage.get).toHaveBeenCalled();
expect(myStorage.clear).not.toHaveBeenCalled();
// Ensure the base client is not emitting a warning for this.
expect(logger.warn).not.toHaveBeenCalled();
});
5 changes: 3 additions & 2 deletions packages/sdk/react-native/src/ReactNativeLDClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ export default class ReactNativeLDClient extends LDClientImpl {
highTimeoutThreshold: 15,
};

const validatedRnOptions = validateOptions(options, logger);

super(
sdkKey,
autoEnvAttributes,
createPlatform(logger),
createPlatform(logger, validatedRnOptions.storage),
{ ...filterToBaseOptions(options), logger },
internalOptions,
);
Expand All @@ -78,7 +80,6 @@ export default class ReactNativeLDClient extends LDClientImpl {
},
};

const validatedRnOptions = validateOptions(options, logger);
const initialConnectionMode = options.initialConnectionMode ?? 'streaming';
this.connectionManager = new ConnectionManager(
logger,
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/react-native/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
* @packageDocumentation
*/
import ReactNativeLDClient from './ReactNativeLDClient';
import RNOptions from './RNOptions';
import RNOptions, { RNStorage } from './RNOptions';

export * from '@launchdarkly/js-client-sdk-common';

export * from './hooks';
export * from './provider';
export { ReactNativeLDClient, RNOptions as LDOptions };
export { ReactNativeLDClient, RNOptions as LDOptions, RNStorage };
35 changes: 34 additions & 1 deletion packages/sdk/react-native/src/options.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LDLogger } from '@launchdarkly/js-client-sdk-common';

import validateOptions, { filterToBaseOptions } from './options';
import { RNStorage } from './RNOptions';

it('logs no warnings when all configuration is valid', () => {
const logger: LDLogger = {
Expand All @@ -10,11 +11,24 @@ it('logs no warnings when all configuration is valid', () => {
error: jest.fn(),
};

const storage: RNStorage = {
get(_key: string): Promise<string | null> {
throw new Error('Function not implemented.');
},
set(_key: string, _value: string): Promise<void> {
throw new Error('Function not implemented.');
},
clear(_key: string): Promise<void> {
throw new Error('Function not implemented.');
},
};

validateOptions(
{
runInBackground: true,
automaticBackgroundHandling: true,
automaticNetworkHandling: true,
storage,
},
logger,
);
Expand All @@ -41,11 +55,13 @@ it('warns for invalid configuration', () => {
automaticBackgroundHandling: 42,
// @ts-ignore
automaticNetworkHandling: {},
// @ts-ignore
storage: 'potato',
},
logger,
);

expect(logger.warn).toHaveBeenCalledTimes(3);
expect(logger.warn).toHaveBeenCalledTimes(4);
expect(logger.warn).toHaveBeenCalledWith(
'Config option "runInBackground" should be of type boolean, got string, using default value',
);
Expand All @@ -55,6 +71,9 @@ it('warns for invalid configuration', () => {
expect(logger.warn).toHaveBeenCalledWith(
'Config option "automaticNetworkHandling" should be of type boolean, got object, using default value',
);
expect(logger.warn).toHaveBeenCalledWith(
'Config option "storage" should be of type object, got string, using default value',
);
});

it('applies default options', () => {
Expand All @@ -69,6 +88,7 @@ it('applies default options', () => {
expect(opts.runInBackground).toBe(false);
expect(opts.automaticBackgroundHandling).toBe(true);
expect(opts.automaticNetworkHandling).toBe(true);
expect(opts.storage).toBeUndefined();

expect(logger.debug).not.toHaveBeenCalled();
expect(logger.info).not.toHaveBeenCalled();
Expand All @@ -83,11 +103,24 @@ it('filters to base options', () => {
warn: jest.fn(),
error: jest.fn(),
};
const storage: RNStorage = {
get(_key: string): Promise<string | null> {
throw new Error('Function not implemented.');
},
set(_key: string, _value: string): Promise<void> {
throw new Error('Function not implemented.');
},
clear(_key: string): Promise<void> {
throw new Error('Function not implemented.');
},
};

const opts = {
debug: false,
runInBackground: true,
automaticBackgroundHandling: true,
automaticNetworkHandling: true,
storage,
};

const baseOpts = filterToBaseOptions(opts);
Expand Down
5 changes: 4 additions & 1 deletion packages/sdk/react-native/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,27 @@ import {
TypeValidators,
} from '@launchdarkly/js-client-sdk-common';

import RNOptions from './RNOptions';
import RNOptions, { RNStorage } from './RNOptions';

export interface ValidatedOptions {
runInBackground: boolean;
automaticNetworkHandling: boolean;
automaticBackgroundHandling: boolean;
storage?: RNStorage;
}

const optDefaults = {
runInBackground: false,
automaticNetworkHandling: true,
automaticBackgroundHandling: true,
storage: undefined,
};

const validators: { [Property in keyof RNOptions]: TypeValidator | undefined } = {
runInBackground: TypeValidators.Boolean,
automaticNetworkHandling: TypeValidators.Boolean,
automaticBackgroundHandling: TypeValidators.Boolean,
storage: TypeValidators.Object,
};

export function filterToBaseOptions(opts: RNOptions): LDOptions {
Expand Down
6 changes: 3 additions & 3 deletions packages/sdk/react-native/src/platform/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { LDLogger, Platform } from '@launchdarkly/js-client-sdk-common';
import { LDLogger, Platform, Storage } from '@launchdarkly/js-client-sdk-common';

import PlatformCrypto from './crypto';
import PlatformEncoding from './PlatformEncoding';
import PlatformInfo from './PlatformInfo';
import PlatformRequests from './PlatformRequests';
import PlatformStorage from './PlatformStorage';

const createPlatform = (logger: LDLogger): Platform => ({
const createPlatform = (logger: LDLogger, storage?: Storage): Platform => ({
crypto: new PlatformCrypto(),
info: new PlatformInfo(logger),
requests: new PlatformRequests(logger),
encoding: new PlatformEncoding(),
storage: new PlatformStorage(logger),
storage: storage ?? new PlatformStorage(logger),
});

export default createPlatform;