Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
36 changes: 35 additions & 1 deletion packages/sdk/react-native/src/RNOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ConnectionMode, LDOptions } from '@launchdarkly/js-client-sdk-common';
import {
ConnectionMode,
LDClientDataSystemOptions,
LDOptions,
} from '@launchdarkly/js-client-sdk-common';

import { LDPlugin } from './LDPlugin';

Expand Down Expand Up @@ -60,6 +64,20 @@ export interface RNStorage {
clear: (key: string) => Promise<void>;
}

/**
* Data system options for the React Native SDK.
*
* Note: Network-based automatic mode switching is not yet supported.
* Lifecycle-based switching is.
*
* This interface is not stable, and not subject to any backwards compatibility
* guarantees or semantic versioning. It is in early access. If you want access
* to this feature please join the EAP.
* https://launchdarkly.com/docs/sdk/features/data-saving-mode
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface RNDataSystemOptions extends LDClientDataSystemOptions {}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This may not be strictly required. But if we decide to add RN specific options, then we will have a type to do it in. Where if we use LDClientDataSystemOptions, then we have to do name shenanigans. Or risk breaking things.


export interface RNSpecificOptions {
/**
* Some platforms (windows, web, mac, linux) can continue executing code
Expand Down Expand Up @@ -118,6 +136,22 @@ export interface RNSpecificOptions {
* Plugin support is currently experimental and subject to change.
*/
plugins?: LDPlugin[];

/**
* @internal
*
* This feature is experimental and should NOT be considered ready for
* production use. It may change or be removed without notice and is not
* subject to backwards compatibility guarantees.
*
* Configuration for the FDv2 data system. When present, the SDK uses
* the FDv2 protocol for flag delivery instead of the default FDv1
* protocol.
*
* Note: Network-based automatic mode switching is not yet supported.
* Lifecycle-based switching (foreground/background) is fully functional.
*/
dataSystem?: RNDataSystemOptions;
}

export default interface RNOptions extends LDOptions, RNSpecificOptions {}
233 changes: 173 additions & 60 deletions packages/sdk/react-native/src/ReactNativeLDClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
BasicLogger,
type Configuration,
ConnectionMode,
createDefaultSourceFactoryProvider,
createFDv2DataManagerBase,
FDv2ConnectionMode,
type FDv2DataManagerControl,
FlagManager,
internal,
LDClientImpl,
Expand All @@ -12,13 +16,21 @@ import {
LDHeaders,
LDPluginEnvironmentMetadata,
MOBILE_DATA_SYSTEM_DEFAULTS,
MOBILE_TRANSITION_TABLE,
mobileFdv1Endpoints,
MODE_TABLE,
resolveForegroundMode,
} from '@launchdarkly/js-client-sdk-common';

import MobileDataManager from './MobileDataManager';
import validateOptions, { filterToBaseOptions } from './options';
import createPlatform from './platform';
import { ConnectionDestination, ConnectionManager } from './platform/ConnectionManager';
import {
ApplicationState,
ConnectionDestination,
ConnectionManager,
NetworkState,
} from './platform/ConnectionManager';
import LDOptions from './RNOptions';
import RNStateDetector from './RNStateDetector';

Expand All @@ -36,7 +48,8 @@ import RNStateDetector from './RNStateDetector';
* ```
*/
export default class ReactNativeLDClient extends LDClientImpl {
private _connectionManager: ConnectionManager;
private _connectionManager?: ConnectionManager;

/**
* Creates an instance of the LaunchDarkly client.
*
Expand Down Expand Up @@ -72,62 +85,111 @@ export default class ReactNativeLDClient extends LDClientImpl {
const platform = createPlatform(logger, options, validatedRnOptions.storage);
const endpoints = mobileFdv1Endpoints();

const dataManagerFactory = (
flagManager: FlagManager,
configuration: Configuration,
baseHeaders: LDHeaders,
emitter: LDEmitter,
diagnosticsManager?: internal.DiagnosticsManager,
) => {
if (configuration.dataSystem) {
return createFDv2DataManagerBase({
platform,
flagManager,
credential: sdkKey,
config: configuration,
baseHeaders,
emitter,
transitionTable: MOBILE_TRANSITION_TABLE,
foregroundMode: resolveForegroundMode(
configuration.dataSystem,
MOBILE_DATA_SYSTEM_DEFAULTS,
),
backgroundMode: configuration.dataSystem.backgroundConnectionMode ?? 'background',
modeTable: MODE_TABLE,
sourceFactoryProvider: createDefaultSourceFactoryProvider(),
fdv1Endpoints: mobileFdv1Endpoints(),
buildQueryParams: () => [], // Mobile uses Authorization header, not query params
});
}

return new MobileDataManager(
platform,
flagManager,
sdkKey,
configuration,
validatedRnOptions,
endpoints.polling,
endpoints.streaming,
baseHeaders,
emitter,
diagnosticsManager,
);
};

super(
sdkKey,
autoEnvAttributes,
platform,
{ ...filterToBaseOptions(options), logger },
(
flagManager: FlagManager,
configuration: Configuration,
baseHeaders: LDHeaders,
emitter: LDEmitter,
diagnosticsManager?: internal.DiagnosticsManager,
) =>
new MobileDataManager(
platform,
flagManager,
sdkKey,
configuration,
validatedRnOptions,
endpoints.polling,
endpoints.streaming,
baseHeaders,
emitter,
diagnosticsManager,
),
dataManagerFactory,
internalOptions,
);

this.setEventSendingEnabled(!this.isOffline(), false);

const dataManager = this.dataManager as MobileDataManager;
const destination: ConnectionDestination = {
setNetworkAvailability: (available: boolean) => {
dataManager.setNetworkAvailability(available);
},
setEventSendingEnabled: (enabled: boolean, flush: boolean) => {
this.setEventSendingEnabled(enabled, flush);
},
setConnectionMode: async (mode: ConnectionMode) => {
// Pass the connection mode to the base implementation.
// The RN implementation will pass the connection mode through the connection manager.
dataManager.setConnectionMode(mode);
},
};
if (this.isFDv2) {
const fdv2DataManager = this.dataManager as FDv2DataManagerControl;

this.setEventSendingEnabled(true, false);
fdv2DataManager.setFlushCallback(() => this.flush());

// Wire state detection directly to FDv2 data manager.
const stateDetector = new RNStateDetector();

if (validatedRnOptions.automaticBackgroundHandling) {
stateDetector.setApplicationStateListener((state) => {
fdv2DataManager.setLifecycleState(
state === ApplicationState.Foreground ? 'foreground' : 'background',
);
});
}

if (validatedRnOptions.automaticNetworkHandling) {
stateDetector.setNetworkStateListener((state) => {
fdv2DataManager.setNetworkState(
state === NetworkState.Available ? 'available' : 'unavailable',
);
});
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
} else {
const initialConnectionMode = options.initialConnectionMode ?? 'streaming';
this.setEventSendingEnabled(initialConnectionMode !== 'offline', false);

const dataManager = this.dataManager as MobileDataManager;
const destination: ConnectionDestination = {
setNetworkAvailability: (available: boolean) => {
dataManager.setNetworkAvailability(available);
},
setEventSendingEnabled: (enabled: boolean, flush: boolean) => {
this.setEventSendingEnabled(enabled, flush);
},
setConnectionMode: async (mode: ConnectionMode) => {
dataManager.setConnectionMode(mode);
},
};

this._connectionManager = new ConnectionManager(
logger,
{
initialConnectionMode,
automaticNetworkHandling: validatedRnOptions.automaticNetworkHandling,
automaticBackgroundHandling: validatedRnOptions.automaticBackgroundHandling,
runInBackground: validatedRnOptions.runInBackground,
},
destination,
new RNStateDetector(),
);
}

const initialConnectionMode = options.initialConnectionMode ?? 'streaming';
this._connectionManager = new ConnectionManager(
logger,
{
initialConnectionMode,
automaticNetworkHandling: validatedRnOptions.automaticNetworkHandling,
automaticBackgroundHandling: validatedRnOptions.automaticBackgroundHandling,
runInBackground: validatedRnOptions.runInBackground,
},
destination,
new RNStateDetector(),
);
internal.safeRegisterPlugins(
logger,
this.environmentMetadata,
Expand All @@ -136,24 +198,75 @@ export default class ReactNativeLDClient extends LDClientImpl {
);
}

async setConnectionMode(mode: ConnectionMode): Promise<void> {
// Set the connection mode before setting offline, in case there is any mode transition work
// such as flushing on entering the background.
this._connectionManager.setConnectionMode(mode);
// For now the data source connection and the event processing state are connected.
this._connectionManager.setOffline(mode === 'offline');
/**
* Sets the SDK connection mode.
*
* @param mode The connection mode to use (`'streaming'`, `'polling'`, or `'offline'`).
*/
async setConnectionMode(mode: ConnectionMode): Promise<void>;
/**
* @internal
*
* This overload is experimental and should NOT be considered ready for
* production use. It may change or be removed without notice and is not
* subject to backwards compatibility guarantees.
*
* Sets the connection mode for the FDv2 data system.
*
* When the FDv2 data system is enabled (`dataSystem` option), this method
* additionally accepts `'one-shot'` and `'background'` modes. Pass
* `undefined` to clear an explicit override and return to automatic mode
* selection.
*
* @param mode The connection mode to use, or `undefined` to clear the
* override (FDv2 only).
*/
async setConnectionMode(mode?: FDv2ConnectionMode): Promise<void>;
async setConnectionMode(mode?: ConnectionMode | FDv2ConnectionMode): Promise<void> {
if (this.isFDv2) {
// FDv2 path
if (mode !== undefined && !(mode in MODE_TABLE)) {
this.logger.warn(
`setConnectionMode called with invalid mode '${mode}'. ` +
`Valid modes: ${Object.keys(MODE_TABLE).join(', ')}.`,
);
return;
}
(this.dataManager as FDv2DataManagerControl).setConnectionMode(
mode as FDv2ConnectionMode | undefined,
);
} else {
// FDv1 path
if (mode === undefined || mode === 'one-shot' || mode === 'background') {
this.logger.warn(
`setConnectionMode('${mode}') is only supported with the FDv2 data system (dataSystem option).`,
);
return;
}
this._connectionManager?.setConnectionMode(mode as ConnectionMode);
this._connectionManager?.setOffline(mode === 'offline');
}
}

/**
* Gets the SDK connection mode.
*/
getConnectionMode(): ConnectionMode {
const dataManager = this.dataManager as MobileDataManager;
return dataManager.getConnectionMode();
getConnectionMode(): ConnectionMode;
/**
* @internal
*/
getConnectionMode(): FDv2ConnectionMode;
getConnectionMode(): ConnectionMode | FDv2ConnectionMode {
if (this.isFDv2) {
return (this.dataManager as FDv2DataManagerControl).getCurrentMode();
}
return (this.dataManager as MobileDataManager).getConnectionMode();
}

isOffline() {
const dataManager = this.dataManager as MobileDataManager;
return dataManager.getConnectionMode() === 'offline';
if (this.isFDv2) {
return (this.dataManager as FDv2DataManagerControl).getCurrentMode() === 'offline';
}
return (this.dataManager as MobileDataManager).getConnectionMode() === 'offline';
}
}
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,7 +6,7 @@
* @packageDocumentation
*/
import ReactNativeLDClient from './ReactNativeLDClient';
import RNOptions, { RNStorage } from './RNOptions';
import RNOptions, { RNDataSystemOptions, RNStorage } from './RNOptions';

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

Expand All @@ -21,4 +21,4 @@ export type {
LDEvaluationDetail,
} from './hooks/variation/LDEvaluationDetail';

export { ReactNativeLDClient, RNOptions as LDOptions, RNStorage };
export { ReactNativeLDClient, RNOptions as LDOptions, RNDataSystemOptions, RNStorage };
2 changes: 2 additions & 0 deletions packages/shared/sdk-client/src/LDClientImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult {
private _eventSendingEnabled: boolean = false;
private _baseHeaders: LDHeaders;
protected dataManager: DataManager;
protected readonly isFDv2: boolean;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is so we don't have to infer from other signal. We just know.

protected readonly environmentMetadata: LDPluginEnvironmentMetadata;
private _hookRunner: HookRunner;
private _inspectorManager: InspectorManager;
Expand Down Expand Up @@ -161,6 +162,7 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult {
this.emitter,
this._diagnosticsManager,
);
this.isFDv2 = !!this._config.dataSystem;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Validated data system config, not the input options.


const hooks: Hook[] = [...this._config.hooks];

Expand Down
Loading