Skip to content

Commit 11c047e

Browse files
committed
Allow custom config fetcher implementations
1 parent 757df53 commit 11c047e

39 files changed

+688
-413
lines changed

src/AutoPollConfigService.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { AutoPollOptions } from "./ConfigCatClientOptions";
22
import type { LoggerWrapper } from "./ConfigCatLogger";
3-
import type { IConfigFetcher } from "./ConfigFetcher";
43
import type { IConfigService, RefreshResult } from "./ConfigServiceBase";
54
import { ClientCacheState, ConfigServiceBase } from "./ConfigServiceBase";
65
import type { ProjectConfig } from "./ProjectConfig";
@@ -18,9 +17,9 @@ export class AutoPollConfigService extends ConfigServiceBase<AutoPollOptions> im
1817
private readonly pollExpirationMs: number;
1918
readonly readyPromise: Promise<ClientCacheState>;
2019

21-
constructor(configFetcher: IConfigFetcher, options: AutoPollOptions) {
20+
constructor(options: AutoPollOptions) {
2221

23-
super(configFetcher, options);
22+
super(options);
2423

2524
this.pollIntervalMs = options.pollIntervalSeconds * 1000;
2625
// Due to the inaccuracy of the timer, some tolerance should be allowed when checking for

src/ConfigCatClient.ts

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { AutoPollConfigService } from "./AutoPollConfigService";
2-
import type { IConfigCache } from "./ConfigCatCache";
3-
import type { ConfigCatClientOptions, OptionsBase, OptionsForPollingMode } from "./ConfigCatClientOptions";
2+
import type { ConfigCatClientOptions, IConfigCatKernel, OptionsBase, OptionsForPollingMode } from "./ConfigCatClientOptions";
43
import { AutoPollOptions, LazyLoadOptions, ManualPollOptions, PollingMode } from "./ConfigCatClientOptions";
54
import type { LoggerWrapper } from "./ConfigCatLogger";
6-
import type { IConfigFetcher } from "./ConfigFetcher";
75
import type { IConfigService } from "./ConfigServiceBase";
86
import { ClientCacheState, RefreshResult } from "./ConfigServiceBase";
9-
import type { IEventEmitter } from "./EventEmitter";
107
import { nameOfOverrideBehaviour, OverrideBehaviour } from "./FlagOverrides";
118
import type { HookEvents, Hooks, IProvidesHooks } from "./Hooks";
129
import { LazyLoadConfigService } from "./LazyLoadConfigService";
@@ -170,18 +167,10 @@ export interface IConfigCatClientSnapshot {
170167
getValueDetails<T extends SettingValue>(key: string, defaultValue: T, user?: IUser): IEvaluationDetails<SettingTypeOf<T>>;
171168
}
172169

173-
export interface IConfigCatKernel {
174-
configFetcher: IConfigFetcher;
175-
sdkType: string;
176-
sdkVersion: string;
177-
defaultCacheFactory?: (options: OptionsBase) => IConfigCache;
178-
eventEmitterFactory?: () => IEventEmitter;
179-
}
180-
181170
export class ConfigCatClientCache {
182171
private readonly instances: Record<string, [WeakRef<ConfigCatClient>, object]> = {};
183172

184-
getOrCreate(options: ConfigCatClientOptions, configCatKernel: IConfigCatKernel): [ConfigCatClient, boolean] {
173+
getOrCreate(options: ConfigCatClientOptions): [ConfigCatClient, boolean] {
185174
let instance: ConfigCatClient | undefined;
186175

187176
const cachedInstance = this.instances[options.sdkKey];
@@ -194,7 +183,7 @@ export class ConfigCatClientCache {
194183
}
195184

196185
const token = {};
197-
instance = new ConfigCatClient(options, configCatKernel, token);
186+
instance = new ConfigCatClient(options, token);
198187
const weakRefCtor = isWeakRefAvailable() ? WeakRef : getWeakRefStub();
199188
this.instances[options.sdkKey] = [new weakRefCtor(instance), token];
200189
return [instance, false];
@@ -250,20 +239,17 @@ export class ConfigCatClient implements IConfigCatClient {
250239
throw new Error(invalidSdkKeyError);
251240
}
252241

253-
const optionsClass =
254-
pollingMode === PollingMode.AutoPoll ? AutoPollOptions
255-
: pollingMode === PollingMode.ManualPoll ? ManualPollOptions
256-
: pollingMode === PollingMode.LazyLoad ? LazyLoadOptions
242+
const actualOptions =
243+
pollingMode === PollingMode.AutoPoll ? new AutoPollOptions(sdkKey, configCatKernel, options)
244+
: pollingMode === PollingMode.ManualPoll ? new ManualPollOptions(sdkKey, configCatKernel, options)
245+
: pollingMode === PollingMode.LazyLoad ? new LazyLoadOptions(sdkKey, configCatKernel, options)
257246
: throwError(new Error("Invalid 'pollingMode' value"));
258247

259-
const actualOptions = new optionsClass(sdkKey, configCatKernel.sdkType, configCatKernel.sdkVersion, options,
260-
configCatKernel.defaultCacheFactory, configCatKernel.eventEmitterFactory);
261-
262248
if (actualOptions.flagOverrides?.behaviour !== OverrideBehaviour.LocalOnly && !isValidSdkKey(sdkKey, actualOptions.baseUrlOverriden)) {
263249
throw new Error(invalidSdkKeyError);
264250
}
265251

266-
const [instance, instanceAlreadyCreated] = clientInstanceCache.getOrCreate(actualOptions, configCatKernel);
252+
const [instance, instanceAlreadyCreated] = clientInstanceCache.getOrCreate(actualOptions);
267253

268254
if (instanceAlreadyCreated && options) {
269255
actualOptions.logger.clientIsAlreadyCreated(sdkKey);
@@ -274,7 +260,6 @@ export class ConfigCatClient implements IConfigCatClient {
274260

275261
constructor(
276262
options: ConfigCatClientOptions,
277-
configCatKernel: IConfigCatKernel,
278263
private readonly cacheToken?: object) {
279264

280265
if (!options) {
@@ -285,14 +270,6 @@ export class ConfigCatClient implements IConfigCatClient {
285270

286271
this.options.logger.debug("Initializing ConfigCatClient. Options: " + JSON.stringify(this.options));
287272

288-
if (!configCatKernel) {
289-
throw new Error("Invalid 'configCatKernel' value");
290-
}
291-
292-
if (!configCatKernel.configFetcher) {
293-
throw new Error("Invalid 'configCatKernel.configFetcher' value");
294-
}
295-
296273
// To avoid possible memory leaks, the components of the client should not hold a strong reference to the hooks object (see also SafeHooksWrapper).
297274
this.hooks = options.yieldHooks();
298275

@@ -304,9 +281,9 @@ export class ConfigCatClient implements IConfigCatClient {
304281

305282
if (options.flagOverrides?.behaviour !== OverrideBehaviour.LocalOnly) {
306283
this.configService =
307-
options instanceof AutoPollOptions ? new AutoPollConfigService(configCatKernel.configFetcher, options)
308-
: options instanceof ManualPollOptions ? new ManualPollConfigService(configCatKernel.configFetcher, options)
309-
: options instanceof LazyLoadOptions ? new LazyLoadConfigService(configCatKernel.configFetcher, options)
284+
options instanceof AutoPollOptions ? new AutoPollConfigService(options)
285+
: options instanceof ManualPollOptions ? new ManualPollConfigService(options)
286+
: options instanceof LazyLoadOptions ? new LazyLoadConfigService(options)
310287
: throwError(new Error("Invalid 'options' value"));
311288
} else {
312289
this.hooks.emit("clientReady", ClientCacheState.HasLocalOverrideFlagDataOnly);

src/ConfigCatClientOptions.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { IConfigCache, IConfigCatCache } from "./ConfigCatCache";
22
import { ExternalConfigCache, InMemoryConfigCache } from "./ConfigCatCache";
33
import type { IConfigCatLogger } from "./ConfigCatLogger";
44
import { ConfigCatConsoleLogger, LoggerWrapper } from "./ConfigCatLogger";
5+
import type { IConfigCatConfigFetcher } from "./ConfigFetcher";
56
import type { IEventEmitter } from "./EventEmitter";
67
import { NullEventEmitter } from "./EventEmitter";
78
import type { FlagOverrides } from "./FlagOverrides";
@@ -66,6 +67,14 @@ export interface IOptions {
6667
*/
6768
cache?: IConfigCatCache | null;
6869

70+
/**
71+
* The config fetcher implementation to use for performing ConfigCat config fetch operations.
72+
*
73+
* If not set, a default implementation will be used depending on the current platform.
74+
* If you want to use custom a config fetcher, you can provide an implementation of `IConfigCatConfigFetcher`.
75+
*/
76+
configFetcher?: IConfigCatConfigFetcher | null;
77+
6978
/** The flag override to use. If not set, no flag override will be used. */
7079
flagOverrides?: FlagOverrides | null;
7180

@@ -123,6 +132,14 @@ export type OptionsForPollingMode<TMode extends PollingMode | undefined> =
123132
TMode extends undefined ? IAutoPollOptions :
124133
never;
125134

135+
export interface IConfigCatKernel {
136+
sdkType: string;
137+
sdkVersion: string;
138+
eventEmitterFactory?: () => IEventEmitter;
139+
defaultCacheFactory?: (options: OptionsBase) => IConfigCache;
140+
configFetcherFactory: (options: OptionsBase) => IConfigCatConfigFetcher;
141+
}
142+
126143
/* eslint-disable @typescript-eslint/no-inferrable-types */
127144

128145
export abstract class OptionsBase {
@@ -145,6 +162,8 @@ export abstract class OptionsBase {
145162

146163
cache: IConfigCache;
147164

165+
configFetcher: IConfigCatConfigFetcher;
166+
148167
flagOverrides?: FlagOverrides;
149168

150169
defaultUser?: IUser;
@@ -153,9 +172,7 @@ export abstract class OptionsBase {
153172

154173
hooks: SafeHooksWrapper;
155174

156-
constructor(sdkKey: string, clientVersion: string, options?: IOptions | null,
157-
defaultCacheFactory?: ((options: OptionsBase) => IConfigCache) | null,
158-
eventEmitterFactory?: (() => IEventEmitter) | null) {
175+
constructor(sdkKey: string, kernel: IConfigCatKernel, clientVersion: string, options?: IOptions | null) {
159176

160177
if (!sdkKey) {
161178
throw new Error("Invalid 'sdkKey' value");
@@ -175,7 +192,7 @@ export abstract class OptionsBase {
175192
break;
176193
}
177194

178-
const eventEmitter = eventEmitterFactory?.() ?? new NullEventEmitter();
195+
const eventEmitter = kernel.eventEmitterFactory?.() ?? new NullEventEmitter();
179196
const hooks = new Hooks(eventEmitter);
180197
const hooksWeakRef = new (isWeakRefAvailable() ? WeakRef : getWeakRefStub())(hooks);
181198
this.hooks = <SafeHooksWrapper & { hooksWeakRef: WeakRef<Hooks> }>{
@@ -188,10 +205,12 @@ export abstract class OptionsBase {
188205

189206
let logger: IConfigCatLogger | null | undefined;
190207
let cache: IConfigCatCache | null | undefined;
208+
let configFetcher: IConfigCatConfigFetcher | null | undefined;
191209

192210
if (options) {
193211
logger = options.logger;
194212
cache = options.cache;
213+
configFetcher = options.configFetcher;
195214

196215
if (options.requestTimeoutMs) {
197216
if (options.requestTimeoutMs < 0) {
@@ -225,7 +244,9 @@ export abstract class OptionsBase {
225244

226245
this.cache = cache
227246
? new ExternalConfigCache(cache, this.logger)
228-
: (defaultCacheFactory ? defaultCacheFactory(this) : new InMemoryConfigCache());
247+
: (kernel.defaultCacheFactory ? kernel.defaultCacheFactory(this) : new InMemoryConfigCache());
248+
249+
this.configFetcher = configFetcher ?? kernel.configFetcherFactory(this);
229250
}
230251

231252
yieldHooks(): Hooks {
@@ -250,11 +271,9 @@ export class AutoPollOptions extends OptionsBase {
250271

251272
maxInitWaitTimeSeconds: number = 5;
252273

253-
constructor(sdkKey: string, sdkType: string, sdkVersion: string, options?: IAutoPollOptions | null,
254-
defaultCacheFactory?: ((options: OptionsBase) => IConfigCache) | null,
255-
eventEmitterFactory?: (() => IEventEmitter) | null) {
274+
constructor(sdkKey: string, kernel: IConfigCatKernel, options?: IAutoPollOptions | null) {
256275

257-
super(sdkKey, sdkType + "/a-" + sdkVersion, options, defaultCacheFactory, eventEmitterFactory);
276+
super(sdkKey, kernel, kernel.sdkType + "/a-" + kernel.sdkVersion, options);
258277

259278
if (options) {
260279

@@ -282,23 +301,19 @@ export class AutoPollOptions extends OptionsBase {
282301
}
283302

284303
export class ManualPollOptions extends OptionsBase {
285-
constructor(sdkKey: string, sdkType: string, sdkVersion: string, options?: IManualPollOptions | null,
286-
defaultCacheFactory?: ((options: OptionsBase) => IConfigCache) | null,
287-
eventEmitterFactory?: (() => IEventEmitter) | null) {
304+
constructor(sdkKey: string, kernel: IConfigCatKernel, options?: IManualPollOptions | null) {
288305

289-
super(sdkKey, sdkType + "/m-" + sdkVersion, options, defaultCacheFactory, eventEmitterFactory);
306+
super(sdkKey, kernel, kernel.sdkType + "/m-" + kernel.sdkVersion, options);
290307
}
291308
}
292309

293310
export class LazyLoadOptions extends OptionsBase {
294311

295312
cacheTimeToLiveSeconds: number = 60;
296313

297-
constructor(sdkKey: string, sdkType: string, sdkVersion: string, options?: ILazyLoadingOptions | null,
298-
defaultCacheFactory?: ((options: OptionsBase) => IConfigCache) | null,
299-
eventEmitterFactory?: (() => IEventEmitter) | null) {
314+
constructor(sdkKey: string, kernel: IConfigCatKernel, options?: ILazyLoadingOptions | null) {
300315

301-
super(sdkKey, sdkType + "/l-" + sdkVersion, options, defaultCacheFactory, eventEmitterFactory);
316+
super(sdkKey, kernel, kernel.sdkType + "/l-" + kernel.sdkVersion, options);
302317

303318
if (options) {
304319
if (options.cacheTimeToLiveSeconds != null) {

src/ConfigFetcher.ts

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { OptionsBase } from "./ConfigCatClientOptions";
21
import type { ProjectConfig } from "./ProjectConfig";
32

43
export const enum FetchStatus {
@@ -28,11 +27,56 @@ export class FetchResult {
2827
}
2928
}
3029

31-
export interface IFetchResponse {
32-
statusCode: number;
33-
reasonPhrase: string;
34-
eTag?: string;
35-
body?: string;
30+
/** The request parameters for a ConfigCat config fetch operation. */
31+
export class FetchRequest {
32+
constructor(
33+
/** The URL of the config. */
34+
readonly url: string,
35+
/**
36+
* The value of the `ETag` HTTP response header received during the last successful request (if any).
37+
* If available, should be included in the HTTP request, either in the `If-None-Match` header or in the `ccetag` query string parameter.
38+
*
39+
* @remarks In browser runtime environments the `If-None-Match` header should be avoided because that may cause unnecessary CORS preflight requests.
40+
*/
41+
readonly lastETag: string | undefined,
42+
/** Additional HTTP request headers. Should be included in every HTTP request. */
43+
readonly headers: ReadonlyArray<[name: string, value: string]>,
44+
/** The request timeout to apply, configured via `IOptions.requestTimeoutMs`. */
45+
readonly timeoutMs: number
46+
) {
47+
}
48+
}
49+
50+
/** The response data of a ConfigCat config fetch operation. */
51+
export class FetchResponse {
52+
/** The value of the `ETag` HTTP response header. */
53+
readonly eTag?: string;
54+
55+
protected readonly rayId?: string;
56+
57+
constructor(
58+
/** The HTTP status code. */
59+
readonly statusCode: number,
60+
/** The HTTP reason phrase. */
61+
readonly reasonPhrase: string,
62+
/** The HTTP response headers. */
63+
headers: ReadonlyArray<[name: string, value: string]>,
64+
/** The response body. */
65+
readonly body?: string
66+
) {
67+
let eTag: string | undefined, rayId: string | undefined;
68+
69+
for (const [name, value] of headers) {
70+
const normalizedName = name.toLowerCase();
71+
if (eTag == null && normalizedName === "etag") {
72+
this.eTag = eTag = value;
73+
if (rayId != null) break;
74+
} else if (rayId == null && normalizedName === "cf-ray") {
75+
this.rayId = rayId = value;
76+
if (eTag != null) break;
77+
}
78+
}
79+
}
3680
}
3781

3882
export type FetchErrorCauses = {
@@ -74,6 +118,13 @@ export class FetchError<TCause extends keyof FetchErrorCauses = keyof FetchError
74118
}
75119
}
76120

77-
export interface IConfigFetcher {
78-
fetchLogic(options: OptionsBase, lastEtag: string | null): Promise<IFetchResponse>;
121+
/** Defines the interface used by the ConfigCat SDK to perform ConfigCat config fetch operations. */
122+
export interface IConfigCatConfigFetcher {
123+
/**
124+
* Fetches the JSON content of the requested config asynchronously.
125+
* @param request The fetch request.
126+
* @returns A promise that fulfills the fetch response.
127+
* @throws {FetchErrorException} The fetch operation failed.
128+
*/
129+
fetchAsync(request: FetchRequest): Promise<FetchResponse>;
79130
}

0 commit comments

Comments
 (0)