@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.
- package/LICENSE +21 -21
- package/README.md +301 -301
- package/dist/esm/IFbClientCore.d.ts +1 -1
- package/dist/esm/IFbClientCore.d.ts.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/{featbit-js-client-sdk-3.0.13.js → featbit-js-client-sdk-3.0.14.js} +2 -2
- package/dist/umd/featbit-js-client-sdk-3.0.14.js.map +1 -0
- package/dist/umd/featbit-js-client-sdk.js +1 -1
- package/dist/umd/featbit-js-client-sdk.js.map +1 -1
- package/package.json +46 -46
- package/src/Configuration.ts +232 -232
- package/src/Context.ts +61 -61
- package/src/FbClientBuilder.ts +167 -167
- package/src/FbClientCore.ts +405 -405
- package/src/IContextProperty.ts +3 -3
- package/src/IDataKind.ts +11 -11
- package/src/IFbClient.ts +29 -29
- package/src/IFbClientCore.ts +290 -290
- package/src/IVersionedData.ts +18 -18
- package/src/bootstrap/IBootstrapProvider.ts +4 -4
- package/src/bootstrap/JsonBootstrapProvider.ts +34 -34
- package/src/bootstrap/NullBootstrapProvider.ts +20 -20
- package/src/bootstrap/index.ts +2 -2
- package/src/constants.ts +1 -1
- package/src/data-sources/DataSourceUpdates.ts +116 -116
- package/src/data-sources/createStreamListeners.ts +67 -67
- package/src/data-sources/index.ts +1 -1
- package/src/data-sync/DataSyncMode.ts +3 -3
- package/src/data-sync/IDataSynchronizer.ts +15 -15
- package/src/data-sync/IRequestor.ts +10 -10
- package/src/data-sync/NullDataSynchronizer.ts +14 -14
- package/src/data-sync/PollingDataSynchronizer.ts +125 -125
- package/src/data-sync/Requestor.ts +61 -61
- package/src/data-sync/WebSocketDataSynchronizer.ts +77 -77
- package/src/data-sync/index.ts +8 -8
- package/src/data-sync/types.ts +19 -19
- package/src/data-sync/utils.ts +31 -31
- package/src/errors.ts +47 -47
- package/src/evaluation/EvalResult.ts +35 -35
- package/src/evaluation/Evaluator.ts +26 -26
- package/src/evaluation/IEvalDetail.ts +23 -23
- package/src/evaluation/ReasonKinds.ts +9 -9
- package/src/evaluation/data/IFlag.ts +29 -29
- package/src/evaluation/index.ts +4 -4
- package/src/events/DefaultEventProcessor.ts +83 -83
- package/src/events/DefaultEventQueue.ts +49 -49
- package/src/events/DefaultEventSender.ts +73 -73
- package/src/events/DefaultEventSerializer.ts +11 -11
- package/src/events/EventDispatcher.ts +127 -127
- package/src/events/EventSerializer.ts +4 -4
- package/src/events/IEventProcessor.ts +8 -8
- package/src/events/IEventQueue.ts +16 -16
- package/src/events/IEventSender.ts +13 -13
- package/src/events/NullEventProcessor.ts +15 -15
- package/src/events/event.ts +129 -129
- package/src/events/index.ts +11 -11
- package/src/index.ts +21 -21
- package/src/integrations/TestLogger.ts +24 -24
- package/src/integrations/index.ts +1 -1
- package/src/integrations/test_data/FlagBuilder.ts +59 -59
- package/src/integrations/test_data/TestData.ts +57 -57
- package/src/integrations/test_data/TestDataSynchronizer.ts +49 -49
- package/src/integrations/test_data/index.ts +4 -4
- package/src/logging/BasicLogger.ts +108 -108
- package/src/logging/IBasicLoggerOptions.ts +46 -46
- package/src/logging/ILogger.ts +49 -49
- package/src/logging/LogLevel.ts +8 -8
- package/src/logging/SafeLogger.ts +69 -69
- package/src/logging/format.ts +154 -154
- package/src/logging/index.ts +5 -5
- package/src/options/ClientContext.ts +39 -39
- package/src/options/IClientContext.ts +53 -53
- package/src/options/IOptions.ts +123 -123
- package/src/options/IUser.ts +6 -6
- package/src/options/IValidatedOptions.ts +29 -29
- package/src/options/OptionMessages.ts +35 -35
- package/src/options/UserBuilder.ts +35 -35
- package/src/options/Validators.ts +300 -300
- package/src/options/index.ts +7 -7
- package/src/platform/IInfo.ts +102 -102
- package/src/platform/IPlatform.ts +20 -20
- package/src/platform/IStore.ts +112 -112
- package/src/platform/IWebSocket.ts +22 -22
- package/src/platform/browser/BrowserInfo.ts +24 -24
- package/src/platform/browser/BrowserPlatform.ts +19 -19
- package/src/platform/browser/BrowserRequests.ts +6 -6
- package/src/platform/browser/BrowserWebSocket.ts +147 -147
- package/src/platform/browser/FbClient.ts +65 -65
- package/src/platform/browser/LocalStorageStore.ts +59 -59
- package/src/platform/index.ts +11 -11
- package/src/platform/requests.ts +76 -76
- package/src/store/BaseStore.ts +125 -125
- package/src/store/DataKinds.ts +6 -6
- package/src/store/IDataSourceUpdates.ts +68 -68
- package/src/store/InMemoryStore.ts +36 -36
- package/src/store/index.ts +5 -5
- package/src/store/serialization.ts +52 -52
- package/src/store/store.ts +37 -37
- package/src/utils/Emits.ts +75 -75
- package/src/utils/EventEmitter.ts +128 -128
- package/src/utils/IEventEmitter.ts +14 -14
- package/src/utils/Regex.ts +21 -21
- package/src/utils/ValueConverters.ts +55 -55
- package/src/utils/canonicalizeUri.ts +3 -3
- package/src/utils/debounce.ts +33 -33
- package/src/utils/http.ts +40 -40
- package/src/utils/index.ts +5 -5
- package/src/utils/isNullOrUndefined.ts +2 -2
- package/src/utils/serializeUser.ts +27 -27
- package/src/utils/sleep.ts +5 -5
- package/src/version.ts +1 -1
- package/dist/umd/featbit-js-client-sdk-3.0.13.js.map +0 -1
package/src/FbClientCore.ts
CHANGED
|
@@ -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
|
+
}
|