diff --git a/packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts b/packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts index 0d50a90bb8..06f2e9dfb4 100644 --- a/packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts +++ b/packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts @@ -5,6 +5,11 @@ import { CreateInstanceParams, SDKConfigParams } from './ConfigParams'; import { makeLogger } from './makeLogger'; import TestHook from './TestHook'; +interface ListenerEntry { + eventName: string; + handler: (...args: any[]) => void; +} + export const badCommandError = new Error('unsupported command'); export const malformedCommand = new Error('command was malformed'); @@ -45,6 +50,30 @@ function makeSdkConfig(options: SDKConfigParams, tag: string) { cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs); } + if (options.dataSystem) { + if (options.dataSystem.initializers) { + options.dataSystem.initializers.forEach((initializer) => { + if (initializer.polling?.baseUri) { + cf.baseUri = initializer.polling.baseUri; + } + }); + } + if (options.dataSystem.synchronizers) { + options.dataSystem.synchronizers.forEach((synchronizer) => { + if (synchronizer.streaming?.baseUri) { + cf.streamUri = synchronizer.streaming.baseUri; + cf.streaming = true; + cf.streamInitialReconnectDelay = maybeTime(synchronizer.streaming.initialRetryDelayMs); + if (!cf.baseUri) { + cf.baseUri = synchronizer.streaming.baseUri; + } + } else if (synchronizer.polling?.baseUri) { + cf.baseUri = synchronizer.polling.baseUri; + } + }); + } + } + if (options.events) { if (options.events.baseUri) { cf.eventsUri = options.events.baseUri; @@ -81,12 +110,18 @@ function makeDefaultInitialContext() { } export class ClientEntity { + private readonly _listeners = new Map(); + constructor( private readonly _client: LDClient, private readonly _logger: LDLogger, ) {} close() { + this._listeners.forEach((entry) => { + this._client.off(entry.eventName, entry.handler); + }); + this._listeners.clear(); this._client.close(); this._logger.info('Test ended'); } @@ -186,6 +221,79 @@ export class ClientEntity { this._client.flush(); return undefined; + case CommandType.RegisterFlagChangeListener: { + const p = params.registerFlagChangeListener; + if (!p) { + throw malformedCommand; + } + const eventName = p.flagKey ? `change:${p.flagKey}` : 'change'; + + const handler = (...args: any[]) => { + if (!p.flagKey && args.length >= 2) { + const flagKeys = args[1] as string[]; + flagKeys.forEach((key) => { + fetch(p.callbackUri, { + method: 'POST', + body: JSON.stringify({ listenerId: p.listenerId, flagKey: key }), + }).catch(() => {}); + }); + return; + } + fetch(p.callbackUri, { + method: 'POST', + body: JSON.stringify({ listenerId: p.listenerId, flagKey: p.flagKey }), + }).catch(() => {}); + }; + + this._listeners.set(p.listenerId, { eventName, handler }); + this._client.on(eventName, handler); + return undefined; + } + + case CommandType.RegisterFlagValueChangeListener: { + const p = params.registerFlagValueChangeListener; + if (!p) { + throw malformedCommand; + } + const eventName = `change:${p.flagKey}`; + + let oldValue = this._client.variation(p.flagKey, p.defaultValue); + + const handler = () => { + const newValue = this._client.variation(p.flagKey, p.defaultValue); + if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) { + const previousValue = oldValue; + oldValue = newValue; + fetch(p.callbackUri, { + method: 'POST', + body: JSON.stringify({ + listenerId: p.listenerId, + flagKey: p.flagKey, + oldValue: previousValue, + newValue, + }), + }).catch(() => {}); + } + }; + + this._listeners.set(p.listenerId, { eventName, handler }); + this._client.on(eventName, handler); + return undefined; + } + + case CommandType.UnregisterListener: { + const p = params.unregisterListener; + if (!p) { + throw malformedCommand; + } + const entry = this._listeners.get(p.listenerId); + if (entry) { + this._client.off(entry.eventName, entry.handler); + this._listeners.delete(p.listenerId); + } + return undefined; + } + default: throw badCommandError; } diff --git a/packages/sdk/browser/contract-tests/entity/src/CommandParams.ts b/packages/sdk/browser/contract-tests/entity/src/CommandParams.ts index 11251d31ed..9fb4cb2329 100644 --- a/packages/sdk/browser/contract-tests/entity/src/CommandParams.ts +++ b/packages/sdk/browser/contract-tests/entity/src/CommandParams.ts @@ -11,6 +11,9 @@ export enum CommandType { ContextConvert = 'contextConvert', ContextComparison = 'contextComparison', SecureModeHash = 'secureModeHash', + RegisterFlagChangeListener = 'registerFlagChangeListener', + RegisterFlagValueChangeListener = 'registerFlagValueChangeListener', + UnregisterListener = 'unregisterListener', } export enum ValueType { @@ -31,6 +34,9 @@ export interface CommandParams { contextConvert?: ContextConvertParams; contextComparison?: ContextComparisonPairParams; secureModeHash?: SecureModeHashParams; + registerFlagChangeListener?: RegisterFlagChangeListenerParams; + registerFlagValueChangeListener?: RegisterFlagValueChangeListenerParams; + unregisterListener?: UnregisterListenerParams; } export interface EvaluateFlagParams { @@ -137,6 +143,23 @@ export interface SecureModeHashResponse { result: string; } +export interface RegisterFlagChangeListenerParams { + listenerId: string; + flagKey: string; + callbackUri: string; +} + +export interface RegisterFlagValueChangeListenerParams { + listenerId: string; + flagKey: string; + defaultValue: unknown; + callbackUri: string; +} + +export interface UnregisterListenerParams { + listenerId: string; +} + export enum HookStage { BeforeEvaluation = 'beforeEvaluation', AfterEvaluation = 'afterEvaluation', diff --git a/packages/sdk/browser/contract-tests/entity/src/ConfigParams.ts b/packages/sdk/browser/contract-tests/entity/src/ConfigParams.ts index 520170e82c..9652baecf0 100644 --- a/packages/sdk/browser/contract-tests/entity/src/ConfigParams.ts +++ b/packages/sdk/browser/contract-tests/entity/src/ConfigParams.ts @@ -18,6 +18,7 @@ export interface SDKConfigParams { clientSide?: SDKConfigClientSideParams; hooks?: SDKConfigHooksParams; wrapper?: SDKConfigWrapper; + dataSystem?: SDKConfigDataSystemParams; } export interface SDKConfigTLSParams { @@ -87,4 +88,28 @@ export interface SDKConfigWrapper { version: string; } +export interface SDKConfigDataSystemParams { + initializers?: SDKDataSystemInitializerParams[]; + synchronizers?: SDKDataSystemSynchronizerParams[]; +} + +export interface SDKDataSystemInitializerParams { + polling?: SDKDataSourcePollingParams; +} + +export interface SDKDataSystemSynchronizerParams { + streaming?: SDKDataSourceStreamingParams; + polling?: SDKDataSourcePollingParams; +} + +export interface SDKDataSourceStreamingParams { + baseUri?: string; + initialRetryDelayMs?: number; +} + +export interface SDKDataSourcePollingParams { + baseUri?: string; + pollIntervalMs?: number; +} + export type HookStage = 'beforeEvaluation' | 'afterEvaluation'; diff --git a/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts b/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts index 86ba4cf6ac..a1440613fe 100644 --- a/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts +++ b/packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts @@ -44,6 +44,7 @@ export default class TestHarnessWebSocket { 'client-prereq-events', 'client-per-context-summaries', 'track-hooks', + 'flag-change-listeners', ]; break;