From f48d049372270d0841aea3af2c1b9e84bddca07a Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Tue, 25 Jul 2023 12:22:10 +0200 Subject: [PATCH] Allow making snapshots and evaluating settings on snapshots --- src/ConfigCatClient.ts | 244 ++++++++++++++++------------ src/index.ts | 2 + test/ConfigCatClientOptionsTests.ts | 2 +- test/ConfigCatClientTests.ts | 101 ++++++------ test/DataGovernanceTests.ts | 2 +- 5 files changed, 197 insertions(+), 154 deletions(-) diff --git a/src/ConfigCatClient.ts b/src/ConfigCatClient.ts index a627879..4d5b44a 100644 --- a/src/ConfigCatClient.ts +++ b/src/ConfigCatClient.ts @@ -8,11 +8,12 @@ import type { IConfigService } from "./ConfigServiceBase"; import { RefreshResult } from "./ConfigServiceBase"; import type { IEventEmitter } from "./EventEmitter"; import { OverrideBehaviour } from "./FlagOverrides"; -import { ClientReadyState, HookEvents, Hooks, IProvidesHooks } from "./Hooks"; +import type { HookEvents, Hooks, IProvidesHooks } from "./Hooks"; +import { ClientReadyState } from "./Hooks"; import { LazyLoadConfigService } from "./LazyLoadConfigService"; import { ManualPollConfigService } from "./ManualPollConfigService"; import { getWeakRefStub, isWeakRefAvailable } from "./Polyfills"; -import type { ProjectConfig, RolloutPercentageItem, RolloutRule, Setting, SettingValue } from "./ProjectConfig"; +import type { IConfig, ProjectConfig, RolloutPercentageItem, RolloutRule, Setting, SettingValue } from "./ProjectConfig"; import type { IEvaluationDetails, IRolloutEvaluator, SettingTypeOf, User } from "./RolloutEvaluator"; import { RolloutEvaluator, checkSettingsAvailable, evaluate, evaluateAll, evaluationDetailsFromDefaultValue, getTimestampAsDate, isAllowedValue } from "./RolloutEvaluator"; import { errorToString } from "./Utils"; @@ -34,20 +35,6 @@ export interface IConfigCatClient extends IProvidesHooks { */ getValueAsync(key: string, defaultValue: T, user?: User): Promise>; - /** - * Returns the value of a feature flag or setting identified by `key` synchronously from the cache. - * @remarks - * It is important to provide an argument for the `defaultValue` parameter that matches the type of the feature flag or setting you are evaluating. - * Please refer to {@link https://configcat.com/docs/sdk-reference/js/#setting-type-mapping | this table} for the corresponding types. - * @param key Key of the feature flag or setting. - * @param defaultValue In case of failure, this value will be returned. Only the following types are allowed: `string`, `boolean`, `number`, `null` and `undefined`. - * @param user The User Object to use for evaluating targeting rules and percentage options. - * @returns The cached value of the feature flag or setting. - * @throws {Error} `key` is empty. - * @throws {TypeError} `defaultValue` is not of an allowed type. - */ - getValue(key: string, defaultValue: T, user?: User): SettingTypeOf; - /** * Returns the value along with evaluation details of a feature flag or setting identified by `key`. * @remarks @@ -62,20 +49,6 @@ export interface IConfigCatClient extends IProvidesHooks { */ getValueDetailsAsync(key: string, defaultValue: T, user?: User): Promise>>; - /** - * Returns the value along with evaluation details of a feature flag or setting identified by `key` synchronously from the cache. - * @remarks - * It is important to provide an argument for the `defaultValue` parameter that matches the type of the feature flag or setting you are evaluating. - * Please refer to {@link https://configcat.com/docs/sdk-reference/js/#setting-type-mapping | this table} for the corresponding types. - * @param key Key of the feature flag or setting. - * @param defaultValue In case of failure, this value will be returned. Only the following types are allowed: `string`, `boolean`, `number`, `null` and `undefined`. - * @param user The User Object to use for evaluating targeting rules and percentage options. - * @returns The cached value along with the details of evaluation of the feature flag or setting. - * @throws {Error} `key` is empty. - * @throws {TypeError} `defaultValue` is not of an allowed type. - */ - getValueDetails(key: string, defaultValue: T, user?: User): IEvaluationDetails>; - /** * Returns all setting keys. * @returns A promise that fulfills with the array of keys. @@ -117,6 +90,12 @@ export interface IConfigCatClient extends IProvidesHooks { */ waitForReady(): Promise; + /** + * Captures the current state of the client. + * The resulting snapshot can be used to synchronously evaluate feature flags and settings based on the captured state. + */ + snapshot(): IConfigCatClientSnapshot; + /** * Sets the default user. * @param defaultUser The default User Object to use for evaluating targeting rules and percentage options. @@ -149,6 +128,46 @@ export interface IConfigCatClient extends IProvidesHooks { dispose(): void; } +/** Represents the state of `IConfigCatClient` captured at a specific point in time. */ +export interface IConfigCatClientSnapshot { + /** The latest config which has been fetched from the remote server. */ + readonly remoteConfig: IConfig | null; + + /** + * Returns the available setting keys. + * (In case the client is configured to use flag override, this will also include the keys provided by the flag override). + */ + getAllKeys(): ReadonlyArray; + + /** + * Returns the value of a feature flag or setting identified by `key` synchronously, based on the snapshot. + * @remarks + * It is important to provide an argument for the `defaultValue` parameter that matches the type of the feature flag or setting you are evaluating. + * Please refer to {@link https://configcat.com/docs/sdk-reference/js/#setting-type-mapping | this table} for the corresponding types. + * @param key Key of the feature flag or setting. + * @param defaultValue In case of failure, this value will be returned. Only the following types are allowed: `string`, `boolean`, `number`, `null` and `undefined`. + * @param user The User Object to use for evaluating targeting rules and percentage options. + * @returns The cached value of the feature flag or setting. + * @throws {Error} `key` is empty. + * @throws {TypeError} `defaultValue` is not of an allowed type. + */ + getValue(key: string, defaultValue: T, user?: User): SettingTypeOf; + + /** + * Returns the value along with evaluation details of a feature flag or setting identified by `key` synchronously, based on the snapshot. + * @remarks + * It is important to provide an argument for the `defaultValue` parameter that matches the type of the feature flag or setting you are evaluating. + * Please refer to {@link https://configcat.com/docs/sdk-reference/js/#setting-type-mapping | this table} for the corresponding types. + * @param key Key of the feature flag or setting. + * @param defaultValue In case of failure, this value will be returned. Only the following types are allowed: `string`, `boolean`, `number`, `null` and `undefined`. + * @param user The User Object to use for evaluating targeting rules and percentage options. + * @returns The cached value along with the details of evaluation of the feature flag or setting. + * @throws {Error} `key` is empty. + * @throws {TypeError} `defaultValue` is not of an allowed type. + */ + getValueDetails(key: string, defaultValue: T, user?: User): IEvaluationDetails>; +} + export interface IConfigCatKernel { configFetcher: IConfigFetcher; sdkType: string; @@ -270,13 +289,14 @@ export class ConfigCatClient implements IConfigCatClient { this.evaluator = new RolloutEvaluator(options.logger); if (options.flagOverrides?.behaviour !== OverrideBehaviour.LocalOnly) { - this.configService = options instanceof AutoPollOptions ? new AutoPollConfigService(configCatKernel.configFetcher, options) : - options instanceof ManualPollOptions ? new ManualPollConfigService(configCatKernel.configFetcher, options) : - options instanceof LazyLoadOptions ? new LazyLoadConfigService(configCatKernel.configFetcher, options) : + this.configService = + options instanceof AutoPollOptions ? new AutoPollConfigService(configCatKernel.configFetcher, options) : + options instanceof ManualPollOptions ? new ManualPollConfigService(configCatKernel.configFetcher, options) : + options instanceof LazyLoadOptions ? new LazyLoadConfigService(configCatKernel.configFetcher, options) : (() => { throw new Error("Invalid 'options' value"); })(); } else { - this.options.signalReadyState(ClientReadyState.HasLocalOverrideFlagDataOnly); + this.options.hooks.emit("clientReady", ClientReadyState.HasLocalOverrideFlagDataOnly); } this.suppressFinalize = registerForFinalization(this, { sdkKey: options.apiKey, cacheToken, configService: this.configService, logger: options.logger }); @@ -358,31 +378,6 @@ export class ConfigCatClient implements IConfigCatClient { return value; } - getValue(key: string, defaultValue: T, user?: User): SettingTypeOf { - this.options.logger.debug("getValue() called."); - - validateKey(key); - ensureAllowedDefaultValue(defaultValue); - - let value: SettingTypeOf, evaluationDetails: IEvaluationDetails>; - let remoteConfig: ProjectConfig | null = null; - user ??= this.defaultUser; - try { - let settings: { [name: string]: Setting } | null; - [settings, remoteConfig] = this.getSettingsFromCache(); - evaluationDetails = evaluate(this.evaluator, settings, key, defaultValue, user, remoteConfig, this.options.logger); - value = evaluationDetails.value; - } - catch (err) { - this.options.logger.settingEvaluationErrorSingle("getValue", key, "defaultValue", defaultValue, err); - evaluationDetails = evaluationDetailsFromDefaultValue(key, defaultValue, getTimestampAsDate(remoteConfig), user, errorToString(err), err); - value = defaultValue as SettingTypeOf; - } - - this.options.hooks.emit("flagEvaluated", evaluationDetails); - return value; - } - async getValueDetailsAsync(key: string, defaultValue: T, user?: User): Promise>> { this.options.logger.debug("getValueDetailsAsync() called."); @@ -406,29 +401,6 @@ export class ConfigCatClient implements IConfigCatClient { return evaluationDetails; } - getValueDetails(key: string, defaultValue: T, user?: User): IEvaluationDetails> { - this.options.logger.debug("getValueDetails() called."); - - validateKey(key); - ensureAllowedDefaultValue(defaultValue); - - let evaluationDetails: IEvaluationDetails>; - let remoteConfig: ProjectConfig | null = null; - user ??= this.defaultUser; - try { - let settings: { [name: string]: Setting } | null; - [settings, remoteConfig] = this.getSettingsFromCache(); - evaluationDetails = evaluate(this.evaluator, settings, key, defaultValue, user, remoteConfig, this.options.logger); - } - catch (err) { - this.options.logger.settingEvaluationErrorSingle("getValueDetails", key, "defaultValue", defaultValue, err); - evaluationDetails = evaluationDetailsFromDefaultValue(key, defaultValue, getTimestampAsDate(remoteConfig), user, errorToString(err), err); - } - - this.options.hooks.emit("flagEvaluated", evaluationDetails); - return evaluationDetails; - } - async getAllKeysAsync(): Promise { this.options.logger.debug("getAllKeysAsync() called."); @@ -592,40 +564,39 @@ export class ConfigCatClient implements IConfigCatClient { return this.options.readyPromise; } - private async getSettingsAsync(): Promise { - this.options.logger.debug("getSettingsAsync() called."); - - const getRemoteConfigAsync: () => Promise = async () => { - const config = await this.configService!.getConfig(); + snapshot(): IConfigCatClientSnapshot { + const getRemoteConfig: () => SettingsWithRemoteConfig = () => { + const config = this.options.cache.getInMemory(); const settings = !config.isEmpty ? config.config!.settings : null; return [settings, config]; }; + let remoteSettings: { [name: string]: Setting } | null; + let remoteConfig: ProjectConfig | null; const flagOverrides = this.options?.flagOverrides; if (flagOverrides) { - let remoteSettings: { [name: string]: Setting } | null; - let remoteConfig: ProjectConfig | null; - const localSettings = await flagOverrides.dataSource.getOverrides(); + const localSettings = flagOverrides.dataSource.getOverridesSync(); switch (flagOverrides.behaviour) { case OverrideBehaviour.LocalOnly: - return [localSettings, null]; + return new Snapshot(localSettings, null, this); case OverrideBehaviour.LocalOverRemote: - [remoteSettings, remoteConfig] = await getRemoteConfigAsync(); - return [{ ...(remoteSettings ?? {}), ...localSettings }, remoteConfig]; + [remoteSettings, remoteConfig] = getRemoteConfig(); + return new Snapshot({ ...(remoteSettings ?? {}), ...localSettings }, remoteConfig, this); case OverrideBehaviour.RemoteOverLocal: - [remoteSettings, remoteConfig] = await getRemoteConfigAsync(); - return [{ ...localSettings, ...(remoteSettings ?? {}) }, remoteConfig]; + [remoteSettings, remoteConfig] = getRemoteConfig(); + return new Snapshot({ ...localSettings, ...(remoteSettings ?? {}) }, remoteConfig, this); } } - return await getRemoteConfigAsync(); + [remoteSettings, remoteConfig] = getRemoteConfig(); + return new Snapshot(remoteSettings, remoteConfig, this); } - private getSettingsFromCache(): SettingsWithRemoteConfig { - this.options.logger.debug("getSettingsFromCache() called."); + private async getSettingsAsync(): Promise { + this.options.logger.debug("getSettingsAsync() called."); - const getRemoteConfig: () => SettingsWithRemoteConfig = () => { - const config = this.options.cache.getInMemory(); + const getRemoteConfigAsync: () => Promise = async () => { + const config = await this.configService!.getConfig(); const settings = !config.isEmpty ? config.config!.settings : null; return [settings, config]; }; @@ -634,20 +605,20 @@ export class ConfigCatClient implements IConfigCatClient { if (flagOverrides) { let remoteSettings: { [name: string]: Setting } | null; let remoteConfig: ProjectConfig | null; - const localSettings = flagOverrides.dataSource.getOverridesSync(); + const localSettings = await flagOverrides.dataSource.getOverrides(); switch (flagOverrides.behaviour) { case OverrideBehaviour.LocalOnly: return [localSettings, null]; case OverrideBehaviour.LocalOverRemote: - [remoteSettings, remoteConfig] = getRemoteConfig(); + [remoteSettings, remoteConfig] = await getRemoteConfigAsync(); return [{ ...(remoteSettings ?? {}), ...localSettings }, remoteConfig]; case OverrideBehaviour.RemoteOverLocal: - [remoteSettings, remoteConfig] = getRemoteConfig(); + [remoteSettings, remoteConfig] = await getRemoteConfigAsync(); return [{ ...localSettings, ...(remoteSettings ?? {}) }, remoteConfig]; } } - return getRemoteConfig(); + return await getRemoteConfigAsync(); } /** @inheritdoc */ @@ -696,6 +667,71 @@ export class ConfigCatClient implements IConfigCatClient { } } +class Snapshot implements IConfigCatClientSnapshot { + private readonly defaultUser: User | undefined; + private readonly evaluator: IRolloutEvaluator; + private readonly options: ConfigCatClientOptions; + + constructor( + private readonly mergedSettings: { [name: string]: Setting } | null, + private readonly cachedConfig: ProjectConfig | null, + client: ConfigCatClient) { + + this.defaultUser = client["defaultUser"]; + this.evaluator = client["evaluator"]; + this.options = client["options"]; + } + + get remoteConfig() { + const config = this.cachedConfig; + return config && !config.isEmpty ? config.config! : null; + } + + getAllKeys() { return this.mergedSettings ? Object.keys(this.mergedSettings) : []; } + + getValue(key: string, defaultValue: T, user?: User): SettingTypeOf { + this.options.logger.debug("Snapshot.getValue() called."); + + validateKey(key); + ensureAllowedDefaultValue(defaultValue); + + let value: SettingTypeOf, evaluationDetails: IEvaluationDetails>; + user ??= this.defaultUser; + try { + evaluationDetails = evaluate(this.evaluator, this.mergedSettings, key, defaultValue, user, this.cachedConfig, this.options.logger); + value = evaluationDetails.value; + } + catch (err) { + this.options.logger.settingEvaluationErrorSingle("getValue", key, "defaultValue", defaultValue, err); + evaluationDetails = evaluationDetailsFromDefaultValue(key, defaultValue, getTimestampAsDate(this.cachedConfig), user, errorToString(err), err); + value = defaultValue as SettingTypeOf; + } + + this.options.hooks.emit("flagEvaluated", evaluationDetails); + return value; + } + + getValueDetails(key: string, defaultValue: T, user?: User): IEvaluationDetails> { + this.options.logger.debug("Snapshot.getValueDetails() called."); + + validateKey(key); + ensureAllowedDefaultValue(defaultValue); + + let evaluationDetails: IEvaluationDetails>; + user ??= this.defaultUser; + try { + evaluationDetails = evaluate(this.evaluator, this.mergedSettings, key, defaultValue, user, this.cachedConfig, this.options.logger); + } + catch (err) { + this.options.logger.settingEvaluationErrorSingle("getValueDetails", key, "defaultValue", defaultValue, err); + evaluationDetails = evaluationDetailsFromDefaultValue(key, defaultValue, getTimestampAsDate(this.cachedConfig), user, errorToString(err), err); + } + + this.options.hooks.emit("flagEvaluated", evaluationDetails); + return evaluationDetails; + } +} + /** Setting key-value pair. */ export class SettingKeyValue { constructor( diff --git a/src/index.ts b/src/index.ts index 31b2181..77e21bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -88,6 +88,8 @@ export { SettingType, Comparator } from "./ProjectConfig"; export type { IConfigCatClient }; +export type { IConfigCatClientSnapshot } from "./ConfigCatClient"; + export { SettingKeyValue } from "./ConfigCatClient"; export type { IEvaluationDetails, SettingTypeOf } from "./RolloutEvaluator"; diff --git a/test/ConfigCatClientOptionsTests.ts b/test/ConfigCatClientOptionsTests.ts index 4ca2dc3..1cbb56d 100644 --- a/test/ConfigCatClientOptionsTests.ts +++ b/test/ConfigCatClientOptionsTests.ts @@ -409,7 +409,7 @@ class FakeCache implements IConfigCache { throw new Error("Method not implemented."); } getInMemory(): ProjectConfig { - throw new Error("Method not implemented."); + throw new Error("Method not implemented."); } } diff --git a/test/ConfigCatClientTests.ts b/test/ConfigCatClientTests.ts index 572fd31..566f681 100644 --- a/test/ConfigCatClientTests.ts +++ b/test/ConfigCatClientTests.ts @@ -7,6 +7,7 @@ import { AutoPollOptions, IAutoPollOptions, IManualPollOptions, LazyLoadOptions, import { LogLevel } from "../src/ConfigCatLogger"; import { IFetchResponse } from "../src/ConfigFetcher"; import { ConfigServiceBase, IConfigService, RefreshResult } from "../src/ConfigServiceBase"; +import { MapOverrideDataSource, OverrideBehaviour } from "../src/FlagOverrides"; import { ClientReadyState, IProvidesHooks } from "../src/Hooks"; import { LazyLoadConfigService } from "../src/LazyLoadConfigService"; import { isWeakRefAvailable, setupPolyfills } from "../src/Polyfills"; @@ -16,7 +17,6 @@ import { delay } from "../src/Utils"; import "./helpers/ConfigCatClientCacheExtensions"; import { FakeCache, FakeConfigCatKernel, FakeConfigFetcher, FakeConfigFetcherBase, FakeConfigFetcherWithAlwaysVariableEtag, FakeConfigFetcherWithNullNewConfig, FakeConfigFetcherWithPercantageRules, FakeConfigFetcherWithRules, FakeConfigFetcherWithTwoCaseSensitiveKeys, FakeConfigFetcherWithTwoKeys, FakeConfigFetcherWithTwoKeysAndRules, FakeExternalCacheWithInitialData, FakeLogger } from "./helpers/fakes"; import { allowEventLoop } from "./helpers/utils"; -import { MapOverrideDataSource, OverrideBehaviour } from "../src/FlagOverrides"; describe("ConfigCatClient", () => { it("Initialization With AutoPollOptions should create an instance, getValueAsync works", async () => { @@ -549,106 +549,108 @@ describe("ConfigCatClient", () => { assert.equal(actualValue, false); }); - describe("Initialization - with waitForReady", async () => { + describe("Initialization - with waitForReady", () => { const maxInitWaitTimeSeconds = 1; - const configFetcher = new FakeConfigFetcherBase("{}", 0, (lastConfig, lastETag) => ({ - statusCode: 500, - reasonPhrase: "", - eTag: (lastETag as any | 0) + 1 + "", - body: lastConfig - } as IFetchResponse)); - + const configFetcher = new FakeConfigFetcherBase("{}", 0, (lastConfig, lastETag) => ({ + statusCode: 500, + reasonPhrase: "", + eTag: (lastETag as any | 0) + 1 + "", + body: lastConfig + } as IFetchResponse)); it("AutoPoll - should wait", async () => { const configCatKernel: FakeConfigCatKernel = { configFetcher: new FakeConfigFetcher(), sdkType: "common", sdkVersion: "1.0.0" }; - const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", { }, null); + const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", {}, null); const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); const state = await client.waitForReady(); assert.equal(state, ClientReadyState.HasUpToDateFlagData); - assert.equal(client.getValue("debug", false), true); + assert.equal(client.snapshot().getValue("debug", false), true); }); it("AutoPoll - should wait for maxInitWaitTimeSeconds", async () => { const configCatKernel: FakeConfigCatKernel = { configFetcher: new FakeConfigFetcherWithNullNewConfig(), sdkType: "common", sdkVersion: "1.0.0" }; const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", { maxInitWaitTimeSeconds: maxInitWaitTimeSeconds }, null); - + const startDate: number = new Date().getTime(); const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); const state = await client.waitForReady(); const ellapsedMilliseconds: number = new Date().getTime() - startDate; - + assert.isAtLeast(ellapsedMilliseconds, maxInitWaitTimeSeconds); assert.isAtMost(ellapsedMilliseconds, (maxInitWaitTimeSeconds * 1000) + 50); // 50 ms for tolerance - + assert.equal(state, ClientReadyState.NoFlagData); - assert.equal(client.getValue("debug", false), false); + assert.equal(client.snapshot().getValue("debug", false), false); }); - + it("AutoPoll - should wait for maxInitWaitTimeSeconds and return cached", async () => { - + const configCatKernel: FakeConfigCatKernel = { configFetcher: configFetcher, sdkType: "common", sdkVersion: "1.0.0" }; - const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", { - maxInitWaitTimeSeconds: maxInitWaitTimeSeconds, - cache: new FakeExternalCacheWithInitialData(120_000) + const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", { + maxInitWaitTimeSeconds: maxInitWaitTimeSeconds, + cache: new FakeExternalCacheWithInitialData(120_000) }, null); - + const startDate: number = new Date().getTime(); const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); const state = await client.waitForReady(); const ellapsedMilliseconds: number = new Date().getTime() - startDate; - + assert.isAtLeast(ellapsedMilliseconds, maxInitWaitTimeSeconds); assert.isAtMost(ellapsedMilliseconds, (maxInitWaitTimeSeconds * 1000) + 50); // 50 ms for tolerance - + assert.equal(state, ClientReadyState.HasCachedFlagDataOnly); - assert.equal(client.getValue("debug", false), true); - }); + assert.equal(client.snapshot().getValue("debug", false), true); + }); it("LazyLoad - return cached", async () => { - + const configCatKernel: FakeConfigCatKernel = { configFetcher: configFetcher, sdkType: "common", sdkVersion: "1.0.0" }; - const options: LazyLoadOptions = new LazyLoadOptions("APIKEY", "common", "1.0.0", { + const options: LazyLoadOptions = new LazyLoadOptions("APIKEY", "common", "1.0.0", { cache: new FakeExternalCacheWithInitialData() }, null); - + const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); const state = await client.waitForReady(); - + assert.equal(state, ClientReadyState.HasUpToDateFlagData); - assert.equal(client.getValue("debug", false), true); - }); + assert.equal(client.snapshot().getValue("debug", false), true); + }); it("LazyLoad - expired, return cached", async () => { - + const configCatKernel: FakeConfigCatKernel = { configFetcher: configFetcher, sdkType: "common", sdkVersion: "1.0.0" }; - const options: LazyLoadOptions = new LazyLoadOptions("APIKEY", "common", "1.0.0", { - cache: new FakeExternalCacheWithInitialData(120_000) + const options: LazyLoadOptions = new LazyLoadOptions("APIKEY", "common", "1.0.0", { + cache: new FakeExternalCacheWithInitialData(120_000) }, null); - + const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); const state = await client.waitForReady(); - + assert.equal(state, ClientReadyState.HasCachedFlagDataOnly); - assert.equal(client.getValue("debug", false), true); - }); - + assert.equal(client.snapshot().getValue("debug", false), true); + }); + it("ManualPoll - return cached", async () => { - + const configCatKernel: FakeConfigCatKernel = { configFetcher: configFetcher, sdkType: "common", sdkVersion: "1.0.0" }; - const options: ManualPollOptions = new ManualPollOptions("APIKEY", "common", "1.0.0", { + const options: ManualPollOptions = new ManualPollOptions("APIKEY", "common", "1.0.0", { cache: new FakeExternalCacheWithInitialData() }, null); - + const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); const state = await client.waitForReady(); - + assert.equal(state, ClientReadyState.HasCachedFlagDataOnly); - assert.equal(client.getValue("debug", false), true); - }); + const snapshot = client.snapshot(); + assert.equal(snapshot.getValue("debug", false), true); + assert.deepEqual(snapshot.getAllKeys(), ["debug"]); + assert.isNotNull(snapshot.remoteConfig); + }); it("ManualPoll - flag override - local only", async () => { const configCatKernel: FakeConfigCatKernel = { @@ -664,12 +666,15 @@ describe("ConfigCatClient", () => { behaviour: OverrideBehaviour.LocalOnly } }, null); - + const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); const state = await client.waitForReady(); - + assert.equal(state, ClientReadyState.HasLocalOverrideFlagDataOnly); - assert.equal(client.getValue("fakeKey", false), true); + const snapshot = client.snapshot(); + assert.equal(snapshot.getValue("fakeKey", false), true); + assert.deepEqual(snapshot.getAllKeys(), ["fakeKey"]); + assert.isNull(snapshot.remoteConfig); }); }); diff --git a/test/DataGovernanceTests.ts b/test/DataGovernanceTests.ts index a1d10b5..487c935 100644 --- a/test/DataGovernanceTests.ts +++ b/test/DataGovernanceTests.ts @@ -3,8 +3,8 @@ import "mocha"; import { DataGovernance, OptionsBase } from "../src/ConfigCatClientOptions"; import { FetchResult, IConfigFetcher, IFetchResponse } from "../src/ConfigFetcher"; import { ConfigServiceBase } from "../src/ConfigServiceBase"; -import { Config, ProjectConfig } from "../src/ProjectConfig"; import { ClientReadyState } from "../src/Hooks"; +import { Config, ProjectConfig } from "../src/ProjectConfig"; const globalUrl = "https://cdn-global.configcat.com"; const euOnlyUrl = "https://cdn-eu.configcat.com";