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
13 changes: 11 additions & 2 deletions .github/workflows/browser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,21 @@ jobs:
echo $! > /tmp/playwright.pid
sleep 5 # Give the browser time to initialize and connect via WebSocket

- name: Run contract tests
- name: Run contract tests (FDv1)
uses: launchdarkly/gh-actions/actions/contract-tests@d271978e893b5b9facb9f000414e9fcd62e1f78b
with:
test_service_port: 8000
token: ${{ secrets.GITHUB_TOKEN }}
extra_params: '--skip-from=${{ github.workspace }}/packages/sdk/browser/contract-tests/suppressions.txt --stop-service-at-end'
stop_service: 'false'
extra_params: '--skip-from=${{ github.workspace }}/packages/sdk/browser/contract-tests/suppressions.txt'

- name: Run contract tests (FDv2)
uses: launchdarkly/gh-actions/actions/contract-tests@d271978e893b5b9facb9f000414e9fcd62e1f78b
with:
test_service_port: 8000
token: ${{ secrets.GITHUB_TOKEN }}
version: v3
extra_params: '--skip-from=${{ github.workspace }}/packages/sdk/browser/contract-tests/suppressions_datamode_changes.txt'

- name: Print logs on failure
if: failure()
Expand Down
11 changes: 4 additions & 7 deletions .github/workflows/react-native-contract-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,13 @@ jobs:
sleep 1
done

- name: Download contract test harness
run: |
# https://github.com/launchdarkly/sdk-test-harness/releases/tag/v2.34.0
curl -sL -o sdk-test-harness.tar.gz "https://github.com/launchdarkly/sdk-test-harness/releases/download/v2.34.0/sdk-test-harness_Linux_x86_64.tar.gz"
tar -xzf sdk-test-harness.tar.gz sdk-test-harness
chmod +x sdk-test-harness

- name: Run contract tests on Android emulator
# https://github.com/ReactiveCircus/android-emulator-runner/releases/tag/v2.34.0
uses: reactivecircus/android-emulator-runner@f0d1ed2dcad93c7479e8b2f2226c83af54494915 # v2
env:
# The contract-test runner script downloads the harness via the
# official downloader; the token avoids GitHub API rate limits.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
api-level: 31
arch: x86_64
Expand Down
3 changes: 0 additions & 3 deletions packages/sdk/browser/example-fdv2/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ const main = async () => {
buildUI();

const client = createClient(clientSideID, contexts[currentContextIndex], {
// @ts-ignore dataSystem is @internal — experimental FDv2 opt-in
dataSystem: {},
logger: basicLogger({ level: 'debug' }),
});
Expand Down Expand Up @@ -284,14 +283,12 @@ const main = async () => {
];
connectionModes.forEach((mode) => {
document.getElementById(`btn-mode-${mode}`)!.addEventListener('click', () => {
// @ts-ignore setConnectionMode is @internal — experimental FDv2 opt-in
client.setConnectionMode(mode);
updateModeStatus(mode);
log(`setConnectionMode('${mode}')`);
});
});
document.getElementById('btn-mode-clear')!.addEventListener('click', () => {
// @ts-ignore setConnectionMode is @internal — experimental FDv2 opt-in
client.setConnectionMode(undefined);
updateModeStatus(undefined);
log('setConnectionMode(undefined)');
Expand Down
11 changes: 5 additions & 6 deletions packages/sdk/browser/src/LDClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ export type LDClient = Omit<CommonClient, 'getConnectionMode' | 'getOffline' | '
* from the interface.
*/
/**
* @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.
*
* Sets the connection mode for the SDK's data system.
*
* When set, this mode is used exclusively, overriding all automatic mode
Expand All @@ -54,6 +48,11 @@ export type LDClient = Omit<CommonClient, 'getConnectionMode' | 'getOffline' | '
* This method requires the FDv2 data system (`dataSystem` option). If
* FDv2 is not enabled, the call logs a warning and has no effect.
*
* This method 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
*
* @param mode The connection mode to use, or `undefined` to clear the override.
*/
setConnectionMode(mode?: FDv2ConnectionMode): void;
Expand Down
13 changes: 6 additions & 7 deletions packages/sdk/browser/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,18 @@ export interface BrowserDataSystemOptions extends Omit<
*/
export interface BrowserOptions extends Omit<LDOptionsBase, 'initialConnectionMode'> {
/**
* @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.
*
* The browser SDK restricts `automaticModeSwitching` to `false` or
* {@link ManualModeSwitching} only automatic switching has no effect
* {@link ManualModeSwitching} only -- automatic switching has no effect
* in browser environments.
*
* This option 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
*/
dataSystem?: BrowserDataSystemOptions;
/**
Expand Down
139 changes: 127 additions & 12 deletions packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {
CommandType,
CreateInstanceParams,
makeLogger,
SDKConfigDataInitializer,
SDKConfigDataSynchronizer,
SDKConfigModeDefinition,
SDKConfigParams,
ClientSideTestHook as TestHook,
ValueType,
Expand All @@ -16,6 +19,59 @@ import {
export const badCommandError = new Error('unsupported command');
export const malformedCommand = new Error('command was malformed');

function translateInitializer(init: SDKConfigDataInitializer): any | undefined {
if (init.polling) {
return {
type: 'polling',
...(init.polling.pollIntervalMs !== undefined && {
pollInterval: init.polling.pollIntervalMs / 1000,
}),
...(init.polling.baseUri && {
endpoints: { pollingBaseUri: init.polling.baseUri },
}),
};
}
return undefined;
}

function translateSynchronizer(sync: SDKConfigDataSynchronizer): any | undefined {
if (sync.streaming) {
return {
type: 'streaming',
...(sync.streaming.initialRetryDelayMs !== undefined && {
initialReconnectDelay: sync.streaming.initialRetryDelayMs / 1000,
}),
...(sync.streaming.baseUri && {
endpoints: { streamingBaseUri: sync.streaming.baseUri },
}),
};
}
if (sync.polling) {
return {
type: 'polling',
...(sync.polling.pollIntervalMs !== undefined && {
pollInterval: sync.polling.pollIntervalMs / 1000,
}),
...(sync.polling.baseUri && {
endpoints: { pollingBaseUri: sync.polling.baseUri },
}),
};
}
return undefined;
}

function translateModeDefinition(modeDef: SDKConfigModeDefinition): any {
const initializers = (modeDef.initializers ?? [])
.map(translateInitializer)
.filter((x) => x !== undefined);

const synchronizers = (modeDef.synchronizers ?? [])
.map(translateSynchronizer)
.filter((x) => x !== undefined);

return { initializers, synchronizers };
}

function makeSdkConfig(options: SDKConfigParams, tag: string) {
if (!options.clientSide) {
throw new Error('configuration did not include clientSide options');
Expand All @@ -39,21 +95,80 @@ function makeSdkConfig(options: SDKConfigParams, tag: string) {
cf.eventsUri = options.serviceEndpoints.events;
}

if (options.polling) {
if (options.polling.baseUri) {
cf.baseUri = options.polling.baseUri;
}
cf.initialConnectionMode = 'polling';
if (options.dataSystem?.payloadFilter) {
cf.payloadFilterKey = options.dataSystem.payloadFilter;
}

// Can contain streaming and polling, if streaming is set override the initial connection
// mode.
if (options.streaming) {
if (options.streaming.baseUri) {
cf.streamUri = options.streaming.baseUri;
if (options.dataSystem) {
const dataSystem: any = {};

// Helper to apply endpoint overrides from a mode definition to global URIs.
const applyEndpointOverrides = (modeDef: SDKConfigModeDefinition) => {
(modeDef.synchronizers ?? []).forEach((sync) => {
if (sync.streaming?.baseUri) {
cf.streamUri = sync.streaming.baseUri;
cf.streamInitialReconnectDelay = maybeTime(sync.streaming.initialRetryDelayMs);
}
if (sync.polling?.baseUri) {
cf.baseUri = sync.polling.baseUri;
}
});
(modeDef.initializers ?? []).forEach((init) => {
if (init.polling?.baseUri) {
cf.baseUri = init.polling.baseUri;
}
});
};

if (options.dataSystem.connectionModeConfig) {
const connMode = options.dataSystem.connectionModeConfig;
dataSystem.automaticModeSwitching = connMode.initialConnectionMode
? { type: 'manual', initialConnectionMode: connMode.initialConnectionMode }
: false;

if (connMode.customConnectionModes) {
const connectionModes: Record<string, any> = {};
Object.entries(connMode.customConnectionModes).forEach(([modeName, modeDef]) => {
connectionModes[modeName] = translateModeDefinition(modeDef);
applyEndpointOverrides(modeDef);
});
dataSystem.connectionModes = connectionModes;
}
} else if (options.dataSystem.initializers || options.dataSystem.synchronizers) {
// Top-level initializers/synchronizers (no connection modes). Wrap them
// into a single 'streaming' connection mode.
const modeDef: SDKConfigModeDefinition = {
initializers: options.dataSystem.initializers,
synchronizers: options.dataSystem.synchronizers,
};
dataSystem.automaticModeSwitching = {
type: 'manual',
initialConnectionMode: 'streaming',
};
dataSystem.connectionModes = {
streaming: translateModeDefinition(modeDef),
};
applyEndpointOverrides(modeDef);
}

cf.dataSystem = dataSystem;
} else {
if (options.polling) {
if (options.polling.baseUri) {
cf.baseUri = options.polling.baseUri;
}
cf.initialConnectionMode = 'polling';
}

// Can contain streaming and polling, if streaming is set override the initial connection
// mode.
if (options.streaming) {
if (options.streaming.baseUri) {
cf.streamUri = options.streaming.baseUri;
}
cf.initialConnectionMode = 'streaming';
cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs);
}
cf.initialConnectionMode = 'streaming';
cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs);
}

if (options.events) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,32 @@ while [ "$i" -lt 30 ]; do
sleep 2
done

# Run the contract test harness
SUPPRESSIONS_FILE="$SCRIPT_DIR/suppressions.txt"
EXTRA_ARGS=""
if [ -s "$SUPPRESSIONS_FILE" ]; then
EXTRA_ARGS="--skip-from=$SUPPRESSIONS_FILE"
# Fetch the official contract-test-harness runner once. This is the same
# downloader the launchdarkly/gh-actions contract-tests action uses; VERSION
# selects the harness release (v2 -> latest v2.x for FDv1, v3 -> latest v3.x
# for FDv2), and GITHUB_TOKEN (from the workflow env) avoids API rate limits.
# This mirrors the android-client-sdk contract-test setup.
HARNESS_RUNNER=/tmp/run-test-harness.sh
curl -sf \
https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh \
-o "$HARNESS_RUNNER"

# FDv1 (v2 harness).
FDV1_SUPPRESSIONS="$SCRIPT_DIR/suppressions.txt"
FDV1_SKIP=""
if [ -s "$FDV1_SUPPRESSIONS" ]; then
FDV1_SKIP="--skip-from=$FDV1_SUPPRESSIONS"
fi

echo "=== Running FDv1 contract tests ==="
VERSION=v2 PARAMS="-url http://localhost:8000 -debug $FDV1_SKIP" sh "$HARNESS_RUNNER"

# FDv2 (v3 harness). Only the final run stops the test service.
FDV2_SUPPRESSIONS="$SCRIPT_DIR/suppressions-fdv2.txt"
FDV2_SKIP=""
if [ -s "$FDV2_SUPPRESSIONS" ]; then
FDV2_SKIP="--skip-from=$FDV2_SUPPRESSIONS"
fi

"$REPO_ROOT/sdk-test-harness" \
-url http://localhost:8000 \
-debug \
$EXTRA_ARGS
echo "=== Running FDv2 contract tests ==="
VERSION=v3 PARAMS="-url http://localhost:8000 -debug $FDV2_SKIP -stop-service-at-end" sh "$HARNESS_RUNNER"
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ echo "Port forwarding configured."
echo ""
echo "=== Starting adapter ==="
cd "$REPO_ROOT"
yarn workspace react-native-contract-test-entity run start:adapter > /tmp/rn-adapter.log 2>&1 &
yarn workspace @launchdarkly/react-native-contract-test-entity run start:adapter > /tmp/rn-adapter.log 2>&1 &
ADAPTER_PID=$!
echo "Adapter started (PID: $ADAPTER_PID)"

Expand Down
Empty file.
1 change: 0 additions & 1 deletion packages/sdk/react-native/example-fdv2/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const featureClient = new ReactNativeLDClient(LAUNCHDARKLY_MOBILE_KEY, AutoEnvAt
id: 'ld-rn-fdv2-test-app',
version: '0.0.1',
},
// @ts-ignore dataSystem is @internal
dataSystem: {},
});

Expand Down
1 change: 0 additions & 1 deletion packages/sdk/react-native/example-fdv2/src/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ export default function Welcome() {
};

const onSetConnectionMode = (mode?: FDv2ConnectionMode) => {
// @ts-ignore setConnectionMode is @internal - experimental FDv2 opt-in
ldc.setConnectionMode(mode);
setCurrentMode(mode ?? 'automatic');
};
Expand Down
11 changes: 5 additions & 6 deletions packages/sdk/react-native/src/RNOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,17 @@ export interface RNSpecificOptions {
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.
*
* This option 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
*/
dataSystem?: RNDataSystemOptions;
}
Expand Down
Loading
Loading