@featbit/js-client-sdk 3.0.12 → 3.0.13

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