@featbit/js-client-sdk 3.0.13 → 3.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +301 -301
  3. package/dist/esm/IFbClientCore.d.ts +1 -1
  4. package/dist/esm/IFbClientCore.d.ts.map +1 -1
  5. package/dist/esm/version.d.ts +1 -1
  6. package/dist/esm/version.js +1 -1
  7. package/dist/umd/{featbit-js-client-sdk-3.0.13.js → featbit-js-client-sdk-3.0.14.js} +2 -2
  8. package/dist/umd/featbit-js-client-sdk-3.0.14.js.map +1 -0
  9. package/dist/umd/featbit-js-client-sdk.js +1 -1
  10. package/dist/umd/featbit-js-client-sdk.js.map +1 -1
  11. package/package.json +46 -46
  12. package/src/Configuration.ts +232 -232
  13. package/src/Context.ts +61 -61
  14. package/src/FbClientBuilder.ts +167 -167
  15. package/src/FbClientCore.ts +405 -405
  16. package/src/IContextProperty.ts +3 -3
  17. package/src/IDataKind.ts +11 -11
  18. package/src/IFbClient.ts +29 -29
  19. package/src/IFbClientCore.ts +290 -290
  20. package/src/IVersionedData.ts +18 -18
  21. package/src/bootstrap/IBootstrapProvider.ts +4 -4
  22. package/src/bootstrap/JsonBootstrapProvider.ts +34 -34
  23. package/src/bootstrap/NullBootstrapProvider.ts +20 -20
  24. package/src/bootstrap/index.ts +2 -2
  25. package/src/constants.ts +1 -1
  26. package/src/data-sources/DataSourceUpdates.ts +116 -116
  27. package/src/data-sources/createStreamListeners.ts +67 -67
  28. package/src/data-sources/index.ts +1 -1
  29. package/src/data-sync/DataSyncMode.ts +3 -3
  30. package/src/data-sync/IDataSynchronizer.ts +15 -15
  31. package/src/data-sync/IRequestor.ts +10 -10
  32. package/src/data-sync/NullDataSynchronizer.ts +14 -14
  33. package/src/data-sync/PollingDataSynchronizer.ts +125 -125
  34. package/src/data-sync/Requestor.ts +61 -61
  35. package/src/data-sync/WebSocketDataSynchronizer.ts +77 -77
  36. package/src/data-sync/index.ts +8 -8
  37. package/src/data-sync/types.ts +19 -19
  38. package/src/data-sync/utils.ts +31 -31
  39. package/src/errors.ts +47 -47
  40. package/src/evaluation/EvalResult.ts +35 -35
  41. package/src/evaluation/Evaluator.ts +26 -26
  42. package/src/evaluation/IEvalDetail.ts +23 -23
  43. package/src/evaluation/ReasonKinds.ts +9 -9
  44. package/src/evaluation/data/IFlag.ts +29 -29
  45. package/src/evaluation/index.ts +4 -4
  46. package/src/events/DefaultEventProcessor.ts +83 -83
  47. package/src/events/DefaultEventQueue.ts +49 -49
  48. package/src/events/DefaultEventSender.ts +73 -73
  49. package/src/events/DefaultEventSerializer.ts +11 -11
  50. package/src/events/EventDispatcher.ts +127 -127
  51. package/src/events/EventSerializer.ts +4 -4
  52. package/src/events/IEventProcessor.ts +8 -8
  53. package/src/events/IEventQueue.ts +16 -16
  54. package/src/events/IEventSender.ts +13 -13
  55. package/src/events/NullEventProcessor.ts +15 -15
  56. package/src/events/event.ts +129 -129
  57. package/src/events/index.ts +11 -11
  58. package/src/index.ts +21 -21
  59. package/src/integrations/TestLogger.ts +24 -24
  60. package/src/integrations/index.ts +1 -1
  61. package/src/integrations/test_data/FlagBuilder.ts +59 -59
  62. package/src/integrations/test_data/TestData.ts +57 -57
  63. package/src/integrations/test_data/TestDataSynchronizer.ts +49 -49
  64. package/src/integrations/test_data/index.ts +4 -4
  65. package/src/logging/BasicLogger.ts +108 -108
  66. package/src/logging/IBasicLoggerOptions.ts +46 -46
  67. package/src/logging/ILogger.ts +49 -49
  68. package/src/logging/LogLevel.ts +8 -8
  69. package/src/logging/SafeLogger.ts +69 -69
  70. package/src/logging/format.ts +154 -154
  71. package/src/logging/index.ts +5 -5
  72. package/src/options/ClientContext.ts +39 -39
  73. package/src/options/IClientContext.ts +53 -53
  74. package/src/options/IOptions.ts +123 -123
  75. package/src/options/IUser.ts +6 -6
  76. package/src/options/IValidatedOptions.ts +29 -29
  77. package/src/options/OptionMessages.ts +35 -35
  78. package/src/options/UserBuilder.ts +35 -35
  79. package/src/options/Validators.ts +300 -300
  80. package/src/options/index.ts +7 -7
  81. package/src/platform/IInfo.ts +102 -102
  82. package/src/platform/IPlatform.ts +20 -20
  83. package/src/platform/IStore.ts +112 -112
  84. package/src/platform/IWebSocket.ts +22 -22
  85. package/src/platform/browser/BrowserInfo.ts +24 -24
  86. package/src/platform/browser/BrowserPlatform.ts +19 -19
  87. package/src/platform/browser/BrowserRequests.ts +6 -6
  88. package/src/platform/browser/BrowserWebSocket.ts +147 -147
  89. package/src/platform/browser/FbClient.ts +65 -65
  90. package/src/platform/browser/LocalStorageStore.ts +59 -59
  91. package/src/platform/index.ts +11 -11
  92. package/src/platform/requests.ts +76 -76
  93. package/src/store/BaseStore.ts +125 -125
  94. package/src/store/DataKinds.ts +6 -6
  95. package/src/store/IDataSourceUpdates.ts +68 -68
  96. package/src/store/InMemoryStore.ts +36 -36
  97. package/src/store/index.ts +5 -5
  98. package/src/store/serialization.ts +52 -52
  99. package/src/store/store.ts +37 -37
  100. package/src/utils/Emits.ts +75 -75
  101. package/src/utils/EventEmitter.ts +128 -128
  102. package/src/utils/IEventEmitter.ts +14 -14
  103. package/src/utils/Regex.ts +21 -21
  104. package/src/utils/ValueConverters.ts +55 -55
  105. package/src/utils/canonicalizeUri.ts +3 -3
  106. package/src/utils/debounce.ts +33 -33
  107. package/src/utils/http.ts +40 -40
  108. package/src/utils/index.ts +5 -5
  109. package/src/utils/isNullOrUndefined.ts +2 -2
  110. package/src/utils/serializeUser.ts +27 -27
  111. package/src/utils/sleep.ts +5 -5
  112. package/src/version.ts +1 -1
  113. package/dist/umd/featbit-js-client-sdk-3.0.13.js.map +0 -1
@@ -1,405 +1,405 @@
1
- import { IFbClientCore } from "./IFbClientCore";
2
- import { IPlatform } from "./platform/IPlatform";
3
- import Configuration from "./Configuration";
4
- import { ILogger } from "./logging/ILogger";
5
- import ClientContext from "./options/ClientContext";
6
- import DataSourceUpdates from "./data-sources/DataSourceUpdates";
7
- import { createStreamListeners } from "./data-sources/createStreamListeners";
8
- import { IEvalDetail } from "./evaluation/IEvalDetail";
9
- import WebSocketDataSynchronizer from "./data-sync/WebSocketDataSynchronizer";
10
- import PollingDataSynchronizer from "./data-sync/PollingDataSynchronizer";
11
- import Requestor from "./data-sync/Requestor";
12
- import { IDataSynchronizer } from "./data-sync/IDataSynchronizer";
13
- import DataKinds from "./store/DataKinds";
14
- import Evaluator from "./evaluation/Evaluator";
15
- import { ReasonKinds } from "./evaluation/ReasonKinds";
16
- import { ClientError, TimeoutError } from "./errors";
17
- import Context from "./Context";
18
- import { IConvertResult, ValueConverters } from "./utils/ValueConverters";
19
- import { NullDataSynchronizer } from "./data-sync/NullDataSynchronizer";
20
- import { IEventProcessor } from "./events/IEventProcessor";
21
- import { NullEventProcessor } from "./events/NullEventProcessor";
22
- import { DefaultEventProcessor } from "./events/DefaultEventProcessor";
23
- import { IStore } from "./platform/IStore";
24
- import { IOptions } from "./options/IOptions";
25
- import { MetricEvent } from "./events/event";
26
- import { DataSyncModeEnum } from "./data-sync/DataSyncMode";
27
- import { IUser } from "./options/IUser";
28
- import { UserValidator } from "./options/Validators";
29
-
30
- enum ClientState {
31
- Initializing,
32
- Initialized,
33
- Failed,
34
- }
35
-
36
- export interface IClientCallbacks {
37
- onError: (err: Error) => void;
38
- onFailed: (err: Error) => void;
39
- onReady: () => void;
40
- // Called whenever flags change, if there are listeners.
41
- onUpdate: (keys: string[]) => void;
42
- // Method to check if event listeners have been registered.
43
- // If none are registered, then onUpdate will never be called.
44
- hasEventListeners: () => boolean;
45
- }
46
-
47
- export class FbClientCore implements IFbClientCore {
48
- private state: ClientState = ClientState.Initializing;
49
-
50
- private store?: IStore;
51
-
52
- private dataSynchronizer?: IDataSynchronizer;
53
-
54
- private eventProcessor?: IEventProcessor;
55
-
56
- private evaluator?: Evaluator;
57
-
58
- private initResolve?: (value: IFbClientCore | PromiseLike<IFbClientCore>) => void;
59
-
60
- private initReject?: (err: Error) => void;
61
-
62
- private rejectionReason: Error | undefined;
63
-
64
- private initializedPromise?: Promise<IFbClientCore>;
65
-
66
- private config: Configuration;
67
-
68
- private dataSourceUpdates?: DataSourceUpdates;
69
-
70
- private onError: (err: Error) => void;
71
-
72
- private onFailed: (err: Error) => void;
73
-
74
- private onReady: () => void;
75
-
76
- logger?: ILogger;
77
-
78
- constructor(
79
- private options: IOptions,
80
- private platform: IPlatform,
81
- callbacks: IClientCallbacks
82
- ) {
83
- this.onError = callbacks.onError;
84
- this.onFailed = callbacks.onFailed;
85
- this.onReady = callbacks.onReady;
86
-
87
- const {onUpdate, hasEventListeners} = callbacks;
88
- const config = new Configuration(options);
89
-
90
- if (!config.sdkKey && !config.offline) {
91
- throw new Error('You must configure the client with an SDK key');
92
- }
93
-
94
- if (!config.user) {
95
- throw new Error('You must configure the client with a user');
96
- }
97
-
98
- this.config = config;
99
- this.logger = config.logger;
100
-
101
- this.init(onUpdate, hasEventListeners);
102
- }
103
-
104
- private async init(onUpdate: (keys: string[]) => void, hasEventListeners: () => boolean) {
105
- const clientContext = new ClientContext(this.config.sdkKey, this.config, this.platform);
106
- this.store = this.config.storeFactory(clientContext);
107
- await this.store.identify(this.config.user);
108
- this.dataSourceUpdates = new DataSourceUpdates(this.store, hasEventListeners, onUpdate);
109
- this.evaluator = new Evaluator(this.store);
110
-
111
- // use bootstrap provider to populate store
112
- await this.config.bootstrapProvider.populate(this.config.user.keyId, this.dataSourceUpdates);
113
-
114
- if (this.config.offline) {
115
- this.eventProcessor = new NullEventProcessor();
116
- this.dataSynchronizer = new NullDataSynchronizer();
117
-
118
- this.initSuccess();
119
- } else {
120
- this.eventProcessor = new DefaultEventProcessor(clientContext);
121
-
122
- const listeners = createStreamListeners(this.dataSourceUpdates, this.logger, {
123
- put: () => this.initSuccess(),
124
- patch: () => this.initSuccess()
125
- });
126
-
127
- const dataSynchronizer = this.config.dataSyncMode === DataSyncModeEnum.STREAMING
128
- ? new WebSocketDataSynchronizer(
129
- this.config.sdkKey,
130
- this.config.user,
131
- clientContext,
132
- this.platform.webSocket,
133
- () => this.store!.version,
134
- listeners,
135
- this.config.webSocketPingInterval
136
- )
137
- : new PollingDataSynchronizer(
138
- this.config,
139
- new Requestor(this.config.sdkKey, this.config, this.platform.info, this.platform.requests),
140
- () => this.store!.version,
141
- listeners,
142
- (e) => this.dataSourceErrorHandler(e),
143
- );
144
-
145
- this.dataSynchronizer = this.config.dataSynchronizerFactory?.(
146
- clientContext,
147
- this.store,
148
- this.dataSourceUpdates,
149
- () => this.initSuccess(),
150
- (e) => this.dataSourceErrorHandler(e),
151
- ) ?? dataSynchronizer;
152
- }
153
-
154
- this.start();
155
- }
156
-
157
- async identify(user: IUser) {
158
- const validator = new UserValidator();
159
- if (!validator.is(user)) {
160
- validator.messages.forEach((error: string) => {
161
- this.logger?.warn(error);
162
- });
163
-
164
- return;
165
- }
166
-
167
- const [oldFlags, oldVersion] = this.store!.all(DataKinds.Flags);
168
- const oldData = {
169
- flags: {...oldFlags},
170
- version: oldVersion
171
- }
172
-
173
- this.config.user = user;
174
-
175
- await this.store!.identify(user);
176
-
177
- await this.dataSynchronizer!.identify(user);
178
-
179
- const [ newFlags, newVersion ] = this.store!.all(DataKinds.Flags);
180
- const newData = {
181
- flags: {...newFlags},
182
- version: newVersion
183
- }
184
- if (Object.keys(newFlags).length === 0) {
185
- await this.config.bootstrapProvider.populate(user.keyId, this.dataSourceUpdates!);
186
- } else {
187
- this.dataSourceUpdates?.checkUpdates(oldData, newData);
188
- }
189
- }
190
-
191
- private start() {
192
- if (this.config.offline) {
193
- return;
194
- }
195
-
196
- this.dataSynchronizer!.start();
197
- setTimeout(() => {
198
- if (!this.initialized()) {
199
- const msg = `FbClient failed to start successfully within ${ this.config.startWaitTime } milliseconds. ` +
200
- 'This error usually indicates a connection issue with FeatBit or an invalid sdkKey.' +
201
- 'Please double-check your sdkKey and streamingUri/pollingUri configuration. ' +
202
- 'We will continue to initialize the FbClient, it still have a chance to get to work ' +
203
- 'if it\'s a temporary network issue';
204
-
205
- const error = new TimeoutError(msg);
206
- this.state = ClientState.Failed;
207
- this.rejectionReason = error;
208
- this.initReject?.(error);
209
-
210
- return this.logger?.warn(msg);
211
- }
212
- }, this.config.startWaitTime);
213
- }
214
-
215
- initialized(): boolean {
216
- return this.state === ClientState.Initialized;
217
- }
218
-
219
- waitForInitialization(): Promise<IFbClientCore> {
220
- // An initialization promise is only created if someone is going to use that promise.
221
- // If we always created an initialization promise, and there was no call waitForInitialization
222
- // by the time the promise was rejected, then that would result in an unhandled promise
223
- // rejection.
224
-
225
- // Initialization promise was created by a previous call to waitForInitialization.
226
- if (this.initializedPromise) {
227
- return this.initializedPromise;
228
- }
229
-
230
- // Initialization completed before waitForInitialization was called, so we have completed
231
- // and there was no promise. So we make a resolved promise and return it.
232
- if (this.state === ClientState.Initialized) {
233
- this.initializedPromise = Promise.resolve(this);
234
- return this.initializedPromise;
235
- }
236
-
237
- // Initialization failed before waitForInitialization was called, so we have completed
238
- // and there was no promise. So we make a rejected promise and return it.
239
- if (this.state === ClientState.Failed) {
240
- this.initializedPromise = Promise.reject(this.rejectionReason);
241
- return this.initializedPromise;
242
- }
243
-
244
- if (!this.initializedPromise) {
245
- this.initializedPromise = new Promise((resolve, reject) => {
246
- this.initResolve = resolve;
247
- this.initReject = reject;
248
- });
249
- }
250
- return this.initializedPromise;
251
- }
252
-
253
- boolVariation(
254
- key: string,
255
- defaultValue: boolean
256
- ): boolean {
257
- return this.evaluateCore(key, defaultValue, ValueConverters.bool).value!;
258
- }
259
-
260
- boolVariationDetail(
261
- key: string,
262
- defaultValue: boolean
263
- ): IEvalDetail<boolean> {
264
- return this.evaluateCore(key, defaultValue, ValueConverters.bool);
265
- }
266
-
267
- jsonVariation(key: string, defaultValue: any): any {
268
- return this.evaluateCore(key, defaultValue, ValueConverters.json).value!;
269
- }
270
-
271
- jsonVariationDetail(key: string, defaultValue: any): IEvalDetail<any> {
272
- return this.evaluateCore(key, defaultValue, ValueConverters.json);
273
- }
274
-
275
- numberVariation(key: string, defaultValue: number): number {
276
- return this.evaluateCore(key, defaultValue, ValueConverters.number).value!;
277
- }
278
-
279
- numberVariationDetail(key: string, defaultValue: number): IEvalDetail<number> {
280
- return this.evaluateCore(key, defaultValue, ValueConverters.number);
281
- }
282
-
283
- stringVariation(key: string, defaultValue: string): string {
284
- return this.evaluateCore(key, defaultValue, ValueConverters.string).value!;
285
- }
286
-
287
- stringVariationDetail(key: string, defaultValue: string): IEvalDetail<string> {
288
- return this.evaluateCore(key, defaultValue, ValueConverters.string);
289
- }
290
-
291
- variation(key: string, defaultValue: string): string {
292
- return this.evaluateCore(key, defaultValue, ValueConverters.string).value!;
293
- }
294
-
295
- variationDetail(key: string, defaultValue: string): IEvalDetail<string> {
296
- return this.evaluateCore(key, defaultValue, ValueConverters.string);
297
- }
298
-
299
- getAllVariations(): Promise<IEvalDetail<string>[]> {
300
- const context = Context.fromUser(this.config.user);
301
- if (!context.valid) {
302
- const error = new ClientError(
303
- `${ context.message ?? 'User not valid;' } returning default value.`,
304
- );
305
- this.onError(error);
306
-
307
- return Promise.resolve([]);
308
- }
309
-
310
- const [flags, _] = this.store!.all(DataKinds.Flags);
311
- const result = Object.keys(flags).map(flagKey => {
312
- const evalResult = this.evaluator!.evaluate(flagKey);
313
- return {flagKey, kind: evalResult.kind, reason: evalResult.reason, value: evalResult.value?.variation};
314
- });
315
-
316
- return Promise.resolve(result);
317
- }
318
-
319
- async close(): Promise<void> {
320
- await this.eventProcessor!.close();
321
- this.dataSynchronizer?.close();
322
- this.store!.close();
323
- }
324
-
325
- track(eventName: string, metricValue?: number | undefined): void {
326
- const metricEvent = new MetricEvent(this.config.user, eventName, this.platform.info.appType, metricValue ?? 1);
327
- this.eventProcessor!.record(metricEvent);
328
- return;
329
- }
330
-
331
- async flush(callback?: (res: boolean) => void): Promise<boolean> {
332
- try {
333
- await this.eventProcessor!.flush();
334
- callback?.(true);
335
- return true;
336
- } catch (err) {
337
- callback?.(false);
338
- return false;
339
- }
340
- }
341
-
342
- evaluateCore<TValue>(
343
- flagKey: string,
344
- defaultValue: TValue,
345
- typeConverter: (value: string) => IConvertResult<TValue>
346
- ): IEvalDetail<TValue> {
347
- const context = Context.fromUser(this.config.user);
348
- if (!context.valid) {
349
- const error = new ClientError(
350
- `${ context.message ?? 'User not valid;' } returning default value.`,
351
- );
352
- this.onError(error);
353
-
354
- return {flagKey, kind: ReasonKinds.Error, reason: error.message, value: defaultValue};
355
- }
356
-
357
- const evalResult = this.evaluator!.evaluate(flagKey);
358
-
359
- if (evalResult.kind === ReasonKinds.FlagNotFound) {
360
- // flag not found, return default value
361
- const error = new ClientError(evalResult.reason!);
362
- this.onError(error);
363
-
364
- return {flagKey, kind: evalResult.kind, reason: evalResult.reason, value: defaultValue};
365
- }
366
-
367
- if (!this.initialized()) {
368
- this.logger?.warn(
369
- 'Variation called before FeatBit client initialization completed (did you wait for the' +
370
- "'ready' event?)",
371
- );
372
- } else {
373
- // send event
374
- this.eventProcessor!.record(evalResult.toEvalEvent(this.config.user));
375
- }
376
-
377
- const {isSucceeded, value} = typeConverter(evalResult.value?.variation!);
378
- return isSucceeded
379
- ? {flagKey, kind: evalResult.kind, reason: evalResult.reason, value}
380
- : {flagKey, kind: ReasonKinds.WrongType, reason: 'type mismatch', value: defaultValue};
381
- }
382
-
383
- private dataSourceErrorHandler(e: any) {
384
- const error =
385
- e.code === 401 ? new Error('Authentication failed. Double check your SDK key.') : e;
386
-
387
- this.onError(error);
388
- this.onFailed(error);
389
-
390
- if (!this.initialized()) {
391
- this.state = ClientState.Failed;
392
- this.rejectionReason = error;
393
- this.initReject?.(error);
394
- }
395
- }
396
-
397
- private initSuccess() {
398
- if (!this.initialized()) {
399
- this.state = ClientState.Initialized;
400
- this.logger?.info('FbClient started successfully.');
401
- this.initResolve?.(this);
402
- this.onReady();
403
- }
404
- }
405
- }
1
+ import { IFbClientCore } from "./IFbClientCore";
2
+ import { IPlatform } from "./platform/IPlatform";
3
+ import Configuration from "./Configuration";
4
+ import { ILogger } from "./logging/ILogger";
5
+ import ClientContext from "./options/ClientContext";
6
+ import DataSourceUpdates from "./data-sources/DataSourceUpdates";
7
+ import { createStreamListeners } from "./data-sources/createStreamListeners";
8
+ import { IEvalDetail } from "./evaluation/IEvalDetail";
9
+ import WebSocketDataSynchronizer from "./data-sync/WebSocketDataSynchronizer";
10
+ import PollingDataSynchronizer from "./data-sync/PollingDataSynchronizer";
11
+ import Requestor from "./data-sync/Requestor";
12
+ import { IDataSynchronizer } from "./data-sync/IDataSynchronizer";
13
+ import DataKinds from "./store/DataKinds";
14
+ import Evaluator from "./evaluation/Evaluator";
15
+ import { ReasonKinds } from "./evaluation/ReasonKinds";
16
+ import { ClientError, TimeoutError } from "./errors";
17
+ import Context from "./Context";
18
+ import { IConvertResult, ValueConverters } from "./utils/ValueConverters";
19
+ import { NullDataSynchronizer } from "./data-sync/NullDataSynchronizer";
20
+ import { IEventProcessor } from "./events/IEventProcessor";
21
+ import { NullEventProcessor } from "./events/NullEventProcessor";
22
+ import { DefaultEventProcessor } from "./events/DefaultEventProcessor";
23
+ import { IStore } from "./platform/IStore";
24
+ import { IOptions } from "./options/IOptions";
25
+ import { MetricEvent } from "./events/event";
26
+ import { DataSyncModeEnum } from "./data-sync/DataSyncMode";
27
+ import { IUser } from "./options/IUser";
28
+ import { UserValidator } from "./options/Validators";
29
+
30
+ enum ClientState {
31
+ Initializing,
32
+ Initialized,
33
+ Failed,
34
+ }
35
+
36
+ export interface IClientCallbacks {
37
+ onError: (err: Error) => void;
38
+ onFailed: (err: Error) => void;
39
+ onReady: () => void;
40
+ // Called whenever flags change, if there are listeners.
41
+ onUpdate: (keys: string[]) => void;
42
+ // Method to check if event listeners have been registered.
43
+ // If none are registered, then onUpdate will never be called.
44
+ hasEventListeners: () => boolean;
45
+ }
46
+
47
+ export class FbClientCore implements IFbClientCore {
48
+ private state: ClientState = ClientState.Initializing;
49
+
50
+ private store?: IStore;
51
+
52
+ private dataSynchronizer?: IDataSynchronizer;
53
+
54
+ private eventProcessor?: IEventProcessor;
55
+
56
+ private evaluator?: Evaluator;
57
+
58
+ private initResolve?: (value: IFbClientCore | PromiseLike<IFbClientCore>) => void;
59
+
60
+ private initReject?: (err: Error) => void;
61
+
62
+ private rejectionReason: Error | undefined;
63
+
64
+ private initializedPromise?: Promise<IFbClientCore>;
65
+
66
+ private config: Configuration;
67
+
68
+ private dataSourceUpdates?: DataSourceUpdates;
69
+
70
+ private onError: (err: Error) => void;
71
+
72
+ private onFailed: (err: Error) => void;
73
+
74
+ private onReady: () => void;
75
+
76
+ logger?: ILogger;
77
+
78
+ constructor(
79
+ private options: IOptions,
80
+ private platform: IPlatform,
81
+ callbacks: IClientCallbacks
82
+ ) {
83
+ this.onError = callbacks.onError;
84
+ this.onFailed = callbacks.onFailed;
85
+ this.onReady = callbacks.onReady;
86
+
87
+ const {onUpdate, hasEventListeners} = callbacks;
88
+ const config = new Configuration(options);
89
+
90
+ if (!config.sdkKey && !config.offline) {
91
+ throw new Error('You must configure the client with an SDK key');
92
+ }
93
+
94
+ if (!config.user) {
95
+ throw new Error('You must configure the client with a user');
96
+ }
97
+
98
+ this.config = config;
99
+ this.logger = config.logger;
100
+
101
+ this.init(onUpdate, hasEventListeners);
102
+ }
103
+
104
+ private async init(onUpdate: (keys: string[]) => void, hasEventListeners: () => boolean) {
105
+ const clientContext = new ClientContext(this.config.sdkKey, this.config, this.platform);
106
+ this.store = this.config.storeFactory(clientContext);
107
+ await this.store.identify(this.config.user);
108
+ this.dataSourceUpdates = new DataSourceUpdates(this.store, hasEventListeners, onUpdate);
109
+ this.evaluator = new Evaluator(this.store);
110
+
111
+ // use bootstrap provider to populate store
112
+ await this.config.bootstrapProvider.populate(this.config.user.keyId, this.dataSourceUpdates);
113
+
114
+ if (this.config.offline) {
115
+ this.eventProcessor = new NullEventProcessor();
116
+ this.dataSynchronizer = new NullDataSynchronizer();
117
+
118
+ this.initSuccess();
119
+ } else {
120
+ this.eventProcessor = new DefaultEventProcessor(clientContext);
121
+
122
+ const listeners = createStreamListeners(this.dataSourceUpdates, this.logger, {
123
+ put: () => this.initSuccess(),
124
+ patch: () => this.initSuccess()
125
+ });
126
+
127
+ const dataSynchronizer = this.config.dataSyncMode === DataSyncModeEnum.STREAMING
128
+ ? new WebSocketDataSynchronizer(
129
+ this.config.sdkKey,
130
+ this.config.user,
131
+ clientContext,
132
+ this.platform.webSocket,
133
+ () => this.store!.version,
134
+ listeners,
135
+ this.config.webSocketPingInterval
136
+ )
137
+ : new PollingDataSynchronizer(
138
+ this.config,
139
+ new Requestor(this.config.sdkKey, this.config, this.platform.info, this.platform.requests),
140
+ () => this.store!.version,
141
+ listeners,
142
+ (e) => this.dataSourceErrorHandler(e),
143
+ );
144
+
145
+ this.dataSynchronizer = this.config.dataSynchronizerFactory?.(
146
+ clientContext,
147
+ this.store,
148
+ this.dataSourceUpdates,
149
+ () => this.initSuccess(),
150
+ (e) => this.dataSourceErrorHandler(e),
151
+ ) ?? dataSynchronizer;
152
+ }
153
+
154
+ this.start();
155
+ }
156
+
157
+ async identify(user: IUser) {
158
+ const validator = new UserValidator();
159
+ if (!validator.is(user)) {
160
+ validator.messages.forEach((error: string) => {
161
+ this.logger?.warn(error);
162
+ });
163
+
164
+ return;
165
+ }
166
+
167
+ const [oldFlags, oldVersion] = this.store!.all(DataKinds.Flags);
168
+ const oldData = {
169
+ flags: {...oldFlags},
170
+ version: oldVersion
171
+ }
172
+
173
+ this.config.user = user;
174
+
175
+ await this.store!.identify(user);
176
+
177
+ await this.dataSynchronizer!.identify(user);
178
+
179
+ const [ newFlags, newVersion ] = this.store!.all(DataKinds.Flags);
180
+ const newData = {
181
+ flags: {...newFlags},
182
+ version: newVersion
183
+ }
184
+ if (Object.keys(newFlags).length === 0) {
185
+ await this.config.bootstrapProvider.populate(user.keyId, this.dataSourceUpdates!);
186
+ } else {
187
+ this.dataSourceUpdates?.checkUpdates(oldData, newData);
188
+ }
189
+ }
190
+
191
+ private start() {
192
+ if (this.config.offline) {
193
+ return;
194
+ }
195
+
196
+ this.dataSynchronizer!.start();
197
+ setTimeout(() => {
198
+ if (!this.initialized()) {
199
+ const msg = `FbClient failed to start successfully within ${ this.config.startWaitTime } milliseconds. ` +
200
+ 'This error usually indicates a connection issue with FeatBit or an invalid sdkKey.' +
201
+ 'Please double-check your sdkKey and streamingUri/pollingUri configuration. ' +
202
+ 'We will continue to initialize the FbClient, it still have a chance to get to work ' +
203
+ 'if it\'s a temporary network issue';
204
+
205
+ const error = new TimeoutError(msg);
206
+ this.state = ClientState.Failed;
207
+ this.rejectionReason = error;
208
+ this.initReject?.(error);
209
+
210
+ return this.logger?.warn(msg);
211
+ }
212
+ }, this.config.startWaitTime);
213
+ }
214
+
215
+ initialized(): boolean {
216
+ return this.state === ClientState.Initialized;
217
+ }
218
+
219
+ waitForInitialization(): Promise<IFbClientCore> {
220
+ // An initialization promise is only created if someone is going to use that promise.
221
+ // If we always created an initialization promise, and there was no call waitForInitialization
222
+ // by the time the promise was rejected, then that would result in an unhandled promise
223
+ // rejection.
224
+
225
+ // Initialization promise was created by a previous call to waitForInitialization.
226
+ if (this.initializedPromise) {
227
+ return this.initializedPromise;
228
+ }
229
+
230
+ // Initialization completed before waitForInitialization was called, so we have completed
231
+ // and there was no promise. So we make a resolved promise and return it.
232
+ if (this.state === ClientState.Initialized) {
233
+ this.initializedPromise = Promise.resolve(this);
234
+ return this.initializedPromise;
235
+ }
236
+
237
+ // Initialization failed before waitForInitialization was called, so we have completed
238
+ // and there was no promise. So we make a rejected promise and return it.
239
+ if (this.state === ClientState.Failed) {
240
+ this.initializedPromise = Promise.reject(this.rejectionReason);
241
+ return this.initializedPromise;
242
+ }
243
+
244
+ if (!this.initializedPromise) {
245
+ this.initializedPromise = new Promise((resolve, reject) => {
246
+ this.initResolve = resolve;
247
+ this.initReject = reject;
248
+ });
249
+ }
250
+ return this.initializedPromise;
251
+ }
252
+
253
+ boolVariation(
254
+ key: string,
255
+ defaultValue: boolean
256
+ ): boolean {
257
+ return this.evaluateCore(key, defaultValue, ValueConverters.bool).value!;
258
+ }
259
+
260
+ boolVariationDetail(
261
+ key: string,
262
+ defaultValue: boolean
263
+ ): IEvalDetail<boolean> {
264
+ return this.evaluateCore(key, defaultValue, ValueConverters.bool);
265
+ }
266
+
267
+ jsonVariation(key: string, defaultValue: any): any {
268
+ return this.evaluateCore(key, defaultValue, ValueConverters.json).value!;
269
+ }
270
+
271
+ jsonVariationDetail(key: string, defaultValue: any): IEvalDetail<any> {
272
+ return this.evaluateCore(key, defaultValue, ValueConverters.json);
273
+ }
274
+
275
+ numberVariation(key: string, defaultValue: number): number {
276
+ return this.evaluateCore(key, defaultValue, ValueConverters.number).value!;
277
+ }
278
+
279
+ numberVariationDetail(key: string, defaultValue: number): IEvalDetail<number> {
280
+ return this.evaluateCore(key, defaultValue, ValueConverters.number);
281
+ }
282
+
283
+ stringVariation(key: string, defaultValue: string): string {
284
+ return this.evaluateCore(key, defaultValue, ValueConverters.string).value!;
285
+ }
286
+
287
+ stringVariationDetail(key: string, defaultValue: string): IEvalDetail<string> {
288
+ return this.evaluateCore(key, defaultValue, ValueConverters.string);
289
+ }
290
+
291
+ variation(key: string, defaultValue: string): string {
292
+ return this.evaluateCore(key, defaultValue, ValueConverters.string).value!;
293
+ }
294
+
295
+ variationDetail(key: string, defaultValue: string): IEvalDetail<string> {
296
+ return this.evaluateCore(key, defaultValue, ValueConverters.string);
297
+ }
298
+
299
+ getAllVariations(): Promise<IEvalDetail<string>[]> {
300
+ const context = Context.fromUser(this.config.user);
301
+ if (!context.valid) {
302
+ const error = new ClientError(
303
+ `${ context.message ?? 'User not valid;' } returning default value.`,
304
+ );
305
+ this.onError(error);
306
+
307
+ return Promise.resolve([]);
308
+ }
309
+
310
+ const [flags, _] = this.store!.all(DataKinds.Flags);
311
+ const result = Object.keys(flags).map(flagKey => {
312
+ const evalResult = this.evaluator!.evaluate(flagKey);
313
+ return {flagKey, kind: evalResult.kind, reason: evalResult.reason, value: evalResult.value?.variation};
314
+ });
315
+
316
+ return Promise.resolve(result);
317
+ }
318
+
319
+ async close(): Promise<void> {
320
+ await this.eventProcessor!.close();
321
+ this.dataSynchronizer?.close();
322
+ this.store!.close();
323
+ }
324
+
325
+ track(eventName: string, metricValue?: number | undefined): void {
326
+ const metricEvent = new MetricEvent(this.config.user, eventName, this.platform.info.appType, metricValue ?? 1);
327
+ this.eventProcessor!.record(metricEvent);
328
+ return;
329
+ }
330
+
331
+ async flush(callback?: (res: boolean) => void): Promise<boolean> {
332
+ try {
333
+ await this.eventProcessor!.flush();
334
+ callback?.(true);
335
+ return true;
336
+ } catch (err) {
337
+ callback?.(false);
338
+ return false;
339
+ }
340
+ }
341
+
342
+ evaluateCore<TValue>(
343
+ flagKey: string,
344
+ defaultValue: TValue,
345
+ typeConverter: (value: string) => IConvertResult<TValue>
346
+ ): IEvalDetail<TValue> {
347
+ const context = Context.fromUser(this.config.user);
348
+ if (!context.valid) {
349
+ const error = new ClientError(
350
+ `${ context.message ?? 'User not valid;' } returning default value.`,
351
+ );
352
+ this.onError(error);
353
+
354
+ return {flagKey, kind: ReasonKinds.Error, reason: error.message, value: defaultValue};
355
+ }
356
+
357
+ const evalResult = this.evaluator!.evaluate(flagKey);
358
+
359
+ if (evalResult.kind === ReasonKinds.FlagNotFound) {
360
+ // flag not found, return default value
361
+ const error = new ClientError(evalResult.reason!);
362
+ this.onError(error);
363
+
364
+ return {flagKey, kind: evalResult.kind, reason: evalResult.reason, value: defaultValue};
365
+ }
366
+
367
+ if (!this.initialized()) {
368
+ this.logger?.warn(
369
+ 'Variation called before FeatBit client initialization completed (did you wait for the' +
370
+ "'ready' event?)",
371
+ );
372
+ } else {
373
+ // send event
374
+ this.eventProcessor!.record(evalResult.toEvalEvent(this.config.user));
375
+ }
376
+
377
+ const {isSucceeded, value} = typeConverter(evalResult.value?.variation!);
378
+ return isSucceeded
379
+ ? {flagKey, kind: evalResult.kind, reason: evalResult.reason, value}
380
+ : {flagKey, kind: ReasonKinds.WrongType, reason: 'type mismatch', value: defaultValue};
381
+ }
382
+
383
+ private dataSourceErrorHandler(e: any) {
384
+ const error =
385
+ e.code === 401 ? new Error('Authentication failed. Double check your SDK key.') : e;
386
+
387
+ this.onError(error);
388
+ this.onFailed(error);
389
+
390
+ if (!this.initialized()) {
391
+ this.state = ClientState.Failed;
392
+ this.rejectionReason = error;
393
+ this.initReject?.(error);
394
+ }
395
+ }
396
+
397
+ private initSuccess() {
398
+ if (!this.initialized()) {
399
+ this.state = ClientState.Initialized;
400
+ this.logger?.info('FbClient started successfully.');
401
+ this.initResolve?.(this);
402
+ this.onReady();
403
+ }
404
+ }
405
+ }