@featbit/js-client-sdk 2.0.0
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 +201 -0
- package/README.md +289 -0
- package/esm/constants.d.ts +7 -0
- package/esm/constants.js +8 -0
- package/esm/constants.js.map +1 -0
- package/esm/events.d.ts +14 -0
- package/esm/events.js +27 -0
- package/esm/events.js.map +1 -0
- package/esm/featbit.d.ts +34 -0
- package/esm/featbit.js +382 -0
- package/esm/featbit.js.map +1 -0
- package/esm/index.d.ts +4 -0
- package/esm/index.js +5 -0
- package/esm/index.js.map +1 -0
- package/esm/logger.d.ts +4 -0
- package/esm/logger.js +24 -0
- package/esm/logger.js.map +1 -0
- package/esm/network.service.d.ts +27 -0
- package/esm/network.service.js +288 -0
- package/esm/network.service.js.map +1 -0
- package/esm/optionMessages.d.ts +5 -0
- package/esm/optionMessages.js +16 -0
- package/esm/optionMessages.js.map +1 -0
- package/esm/queue.d.ts +8 -0
- package/esm/queue.js +35 -0
- package/esm/queue.js.map +1 -0
- package/esm/store.d.ts +20 -0
- package/esm/store.js +143 -0
- package/esm/store.js.map +1 -0
- package/esm/throttleutil.d.ts +9 -0
- package/esm/throttleutil.js +133 -0
- package/esm/throttleutil.js.map +1 -0
- package/esm/types.d.ts +94 -0
- package/esm/types.js +24 -0
- package/esm/types.js.map +1 -0
- package/esm/umd.d.ts +2 -0
- package/esm/utils.d.ts +11 -0
- package/esm/utils.js +142 -0
- package/esm/utils.js.map +1 -0
- package/package.json +48 -0
- package/src/constants.ts +7 -0
- package/src/events.ts +29 -0
- package/src/featbit.ts +343 -0
- package/src/index.ts +6 -0
- package/src/logger.ts +18 -0
- package/src/network.service.ts +223 -0
- package/src/optionMessages.ts +13 -0
- package/src/queue.ts +23 -0
- package/src/store.ts +169 -0
- package/src/throttleutil.ts +72 -0
- package/src/types.ts +113 -0
- package/src/umd.ts +15 -0
- package/src/utils.ts +173 -0
- package/umd/featbit-js-client-sdk-2.0.0.js +2 -0
- package/umd/featbit-js-client-sdk-2.0.0.js.map +1 -0
- package/umd/featbit-js-client-sdk.js +2 -0
- package/umd/featbit-js-client-sdk.js.map +1 -0
package/src/featbit.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { eventHub } from "./events";
|
|
2
|
+
import { logger } from "./logger";
|
|
3
|
+
import store from "./store";
|
|
4
|
+
import { networkService } from "./network.service";
|
|
5
|
+
import {
|
|
6
|
+
FeatureFlagValue,
|
|
7
|
+
ICustomEvent,
|
|
8
|
+
IFeatureFlag,
|
|
9
|
+
IFeatureFlagBase,
|
|
10
|
+
IFeatureFlagSet,
|
|
11
|
+
IFeatureFlagVariationBuffer,
|
|
12
|
+
IInsight,
|
|
13
|
+
InsightType,
|
|
14
|
+
IOption,
|
|
15
|
+
IStreamResponse,
|
|
16
|
+
IUser,
|
|
17
|
+
StreamResponseEventType,
|
|
18
|
+
VariationDataType
|
|
19
|
+
} from "./types";
|
|
20
|
+
import {
|
|
21
|
+
generateorGetGuid,
|
|
22
|
+
isNullOrUndefinedOrWhiteSpace,
|
|
23
|
+
parseVariation,
|
|
24
|
+
serializeUser,
|
|
25
|
+
validateOption,
|
|
26
|
+
validateUser
|
|
27
|
+
} from "./utils";
|
|
28
|
+
import { Queue } from "./queue";
|
|
29
|
+
import {
|
|
30
|
+
currentUserStorageKey,
|
|
31
|
+
featureFlagEvaluatedBufferTopic,
|
|
32
|
+
featureFlagEvaluatedTopic,
|
|
33
|
+
insightsFlushTopic,
|
|
34
|
+
insightsTopic,
|
|
35
|
+
websocketReconnectTopic
|
|
36
|
+
} from "./constants";
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
function createOrGetAnonymousUser(): IUser {
|
|
40
|
+
const sessionId = generateorGetGuid();
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
name: sessionId,
|
|
44
|
+
keyId: sessionId
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mapFeatureFlagsToFeatureFlagBaseList(featureFlags: { [key: string]: IFeatureFlag }): IFeatureFlagBase[] {
|
|
49
|
+
return Object.keys(featureFlags).map((cur) => {
|
|
50
|
+
const {id, variation} = featureFlags[cur];
|
|
51
|
+
const variationType = featureFlags[cur].variationType || VariationDataType.string;
|
|
52
|
+
return {id, variation: parseVariation(variationType, variation), variationType};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class FB {
|
|
57
|
+
private _readyEventEmitted: boolean = false;
|
|
58
|
+
private _readyPromise: Promise<IFeatureFlagBase[]>;
|
|
59
|
+
|
|
60
|
+
private _insightsQueue: Queue<IInsight> = new Queue<IInsight>(1, insightsFlushTopic);
|
|
61
|
+
private _featureFlagEvaluationBuffer: Queue<IFeatureFlagVariationBuffer> = new Queue<IFeatureFlagVariationBuffer>();
|
|
62
|
+
private _option: IOption = {
|
|
63
|
+
secret: '',
|
|
64
|
+
api: '',
|
|
65
|
+
streamingUri: '',
|
|
66
|
+
eventsUri: '',
|
|
67
|
+
enableDataSync: true,
|
|
68
|
+
appType: 'javascript'
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
constructor() {
|
|
72
|
+
this._readyPromise = new Promise<IFeatureFlagBase[]>((resolve, reject) => {
|
|
73
|
+
this.on('ready', () => {
|
|
74
|
+
const featureFlags = store.getFeatureFlags();
|
|
75
|
+
resolve(mapFeatureFlagsToFeatureFlagBaseList(featureFlags));
|
|
76
|
+
if (this._option.enableDataSync) {
|
|
77
|
+
const buffered = this._featureFlagEvaluationBuffer.flush().map(f => {
|
|
78
|
+
const featureFlag = featureFlags[f.id];
|
|
79
|
+
if (!featureFlag) {
|
|
80
|
+
logger.log(`Called unexisting feature flag: ${ f.id }`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const variation = featureFlag.variationOptions.find(o => o.value === f.variationValue);
|
|
85
|
+
if (!variation) {
|
|
86
|
+
logger.log(`Sent buffered insight for feature flag: ${ f.id } with unexisting default variation: ${ f.variationValue }`);
|
|
87
|
+
} else {
|
|
88
|
+
logger.logDebug(`Sent buffered insight for feature flag: ${ f.id } with variation: ${ variation.value }`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
insightType: InsightType.featureFlagUsage,
|
|
93
|
+
id: featureFlag.id,
|
|
94
|
+
timestamp: f.timestamp,
|
|
95
|
+
sendToExperiment: featureFlag.sendToExperiment,
|
|
96
|
+
variation: variation || {id: -1, value: f.variationValue}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
networkService.sendInsights(buffered.filter(x => !!x));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// reconnect to websocket
|
|
106
|
+
eventHub.subscribe(websocketReconnectTopic, async () => {
|
|
107
|
+
try {
|
|
108
|
+
logger.logDebug('reconnecting');
|
|
109
|
+
await this.dataSync();
|
|
110
|
+
if (!this._readyEventEmitted) {
|
|
111
|
+
this._readyEventEmitted = true;
|
|
112
|
+
eventHub.emit('ready', mapFeatureFlagsToFeatureFlagBaseList(store.getFeatureFlags()));
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
logger.log('data sync error', err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
eventHub.subscribe(featureFlagEvaluatedBufferTopic, (data: IFeatureFlagVariationBuffer) => {
|
|
120
|
+
this._featureFlagEvaluationBuffer.add(data);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// track feature flag usage data
|
|
124
|
+
eventHub.subscribe(insightsFlushTopic, () => {
|
|
125
|
+
if (this._option.enableDataSync) {
|
|
126
|
+
networkService.sendInsights(this._insightsQueue.flush());
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
eventHub.subscribe(featureFlagEvaluatedTopic, (data: IInsight) => {
|
|
131
|
+
this._insightsQueue.add(data);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
eventHub.subscribe(insightsTopic, (data: IInsight) => {
|
|
135
|
+
this._insightsQueue.add(data);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
on(name: string, cb: Function) {
|
|
140
|
+
eventHub.subscribe(name, cb);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
off(name: string, cb: Function) {
|
|
144
|
+
eventHub.unsubscribe(name, cb);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
waitUntilReady(): Promise<IFeatureFlagBase[]> {
|
|
148
|
+
return this._readyPromise;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async init(option: IOption) {
|
|
152
|
+
const validateOptionResult = validateOption({...this._option, ...option});
|
|
153
|
+
if (validateOptionResult !== null) {
|
|
154
|
+
logger.log(validateOptionResult);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this._option = {
|
|
159
|
+
...this._option,
|
|
160
|
+
...option,
|
|
161
|
+
...{
|
|
162
|
+
api: (option.api || this._option.api)?.replace(/\/$/, '')
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (this._option.enableDataSync) {
|
|
167
|
+
networkService.init(this._option.streamingUri!, this._option.eventsUri!, this._option.secret, this._option.appType!);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await this.identify(option.user || createOrGetAnonymousUser());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async identify(user: IUser): Promise<void> {
|
|
174
|
+
const validateUserResult = validateUser(user);
|
|
175
|
+
if (validateUserResult !== null) {
|
|
176
|
+
logger.log(validateUserResult);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
user.customizedProperties = user.customizedProperties?.map(p => ({name: p.name, value: `${ p.value }`}));
|
|
181
|
+
|
|
182
|
+
const isUserChanged = serializeUser(user) !== localStorage.getItem(currentUserStorageKey);
|
|
183
|
+
this._option.user = Object.assign({}, user);
|
|
184
|
+
localStorage.setItem(currentUserStorageKey, serializeUser(this._option.user));
|
|
185
|
+
|
|
186
|
+
store.userId = this._option.user.keyId;
|
|
187
|
+
networkService.identify(this._option.user, isUserChanged);
|
|
188
|
+
|
|
189
|
+
await this.bootstrap(this._option.bootstrap, isUserChanged);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async logout(): Promise<IUser> {
|
|
193
|
+
const anonymousUser = createOrGetAnonymousUser();
|
|
194
|
+
await this.identify(anonymousUser);
|
|
195
|
+
return anonymousUser;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* bootstrap with predefined feature flags.
|
|
200
|
+
* @param {array} featureFlags the predefined feature flags.
|
|
201
|
+
* @param {boolean} forceFullFetch if a forced full fetch should be made.
|
|
202
|
+
* @return {Promise<void>} nothing.
|
|
203
|
+
*/
|
|
204
|
+
async bootstrap(featureFlags?: IFeatureFlag[], forceFullFetch?: boolean): Promise<void> {
|
|
205
|
+
featureFlags = featureFlags || this._option.bootstrap;
|
|
206
|
+
if (featureFlags && featureFlags.length > 0) {
|
|
207
|
+
const data = {
|
|
208
|
+
featureFlags: featureFlags.reduce((res, curr) => {
|
|
209
|
+
const {id, variation, timestamp, variationOptions, sendToExperiment, variationType} = curr;
|
|
210
|
+
res[id] = {
|
|
211
|
+
id,
|
|
212
|
+
variation,
|
|
213
|
+
timestamp,
|
|
214
|
+
variationOptions: variationOptions || [{id: 1, value: variation}],
|
|
215
|
+
sendToExperiment,
|
|
216
|
+
variationType: variationType || VariationDataType.string
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return res;
|
|
220
|
+
}, {} as { [key: string]: IFeatureFlag })
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
store.setFullData(data);
|
|
224
|
+
logger.logDebug('bootstrapped with full data');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this._option.enableDataSync) {
|
|
228
|
+
// start data sync
|
|
229
|
+
try {
|
|
230
|
+
await this.dataSync(forceFullFetch);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
logger.log('data sync error', err);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!this._readyEventEmitted) {
|
|
237
|
+
this._readyEventEmitted = true;
|
|
238
|
+
eventHub.emit('ready', mapFeatureFlagsToFeatureFlagBaseList(store.getFeatureFlags()));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async dataSync(forceFullFetch?: boolean): Promise<any> {
|
|
243
|
+
return new Promise<void>((resolve, reject) => {
|
|
244
|
+
const timestamp = forceFullFetch ? 0 : Math.max(...Object.values(store.getFeatureFlags()).map(ff => ff.timestamp), 0);
|
|
245
|
+
|
|
246
|
+
networkService.createConnection(timestamp, (message: IStreamResponse) => {
|
|
247
|
+
if (message && message.userKeyId === this._option.user?.keyId) {
|
|
248
|
+
const {featureFlags} = message;
|
|
249
|
+
|
|
250
|
+
switch (message.eventType) {
|
|
251
|
+
case StreamResponseEventType.full: // full data
|
|
252
|
+
case StreamResponseEventType.patch: // partial data
|
|
253
|
+
const data = {
|
|
254
|
+
featureFlags: featureFlags.reduce((res, curr) => {
|
|
255
|
+
const {id, variation, timestamp, variationOptions, sendToExperiment, variationType} = curr;
|
|
256
|
+
res[id] = {
|
|
257
|
+
id,
|
|
258
|
+
variation,
|
|
259
|
+
timestamp,
|
|
260
|
+
variationOptions,
|
|
261
|
+
sendToExperiment,
|
|
262
|
+
variationType: variationType || VariationDataType.string
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return res;
|
|
266
|
+
}, {} as { [key: string]: IFeatureFlag })
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (message.eventType === StreamResponseEventType.full) {
|
|
270
|
+
store.setFullData(data);
|
|
271
|
+
logger.logDebug('synchonized with full data');
|
|
272
|
+
} else {
|
|
273
|
+
store.updateBulkFromRemote(data);
|
|
274
|
+
logger.logDebug('synchonized with partial data');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
break;
|
|
278
|
+
default:
|
|
279
|
+
logger.logDebug('invalid stream event type: ' + message.eventType);
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
resolve();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
variation(key: string, defaultResult: FeatureFlagValue): FeatureFlagValue {
|
|
290
|
+
const variation = variationWithInsightBuffer(key, defaultResult);
|
|
291
|
+
return variation === undefined ? defaultResult : variation;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* deprecated, you should use variation method directly
|
|
296
|
+
*/
|
|
297
|
+
boolVariation(key: string, defaultResult: boolean): boolean {
|
|
298
|
+
const variation = variationWithInsightBuffer(key, defaultResult);
|
|
299
|
+
return variation === undefined ? defaultResult : variation?.toLocaleLowerCase() === 'true';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
getUser(): IUser {
|
|
303
|
+
return {...this._option.user!};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
sendCustomEvent(data: ICustomEvent[]): void {
|
|
307
|
+
(data || []).forEach(d => this._insightsQueue.add({
|
|
308
|
+
insightType: InsightType.customEvent,
|
|
309
|
+
timestamp: Date.now(),
|
|
310
|
+
type: 'CustomEvent',
|
|
311
|
+
...d
|
|
312
|
+
}))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
sendFeatureFlagInsight(key: string, variation: string) {
|
|
316
|
+
this.variation(key, variation);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
getAllFeatureFlags(): IFeatureFlagSet {
|
|
320
|
+
const flags = store.getFeatureFlags();
|
|
321
|
+
|
|
322
|
+
return Object.values(flags).reduce((acc, curr) => {
|
|
323
|
+
acc[curr.id] = parseVariation(curr.variationType, curr.variation);
|
|
324
|
+
return acc;
|
|
325
|
+
}, {});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const variationWithInsightBuffer = (key: string, defaultResult: string | boolean) => {
|
|
330
|
+
const variation = store.getVariation(key);
|
|
331
|
+
if (variation === undefined) {
|
|
332
|
+
eventHub.emit(featureFlagEvaluatedBufferTopic, {
|
|
333
|
+
id: key,
|
|
334
|
+
timestamp: Date.now(),
|
|
335
|
+
variationValue: `${ defaultResult }`
|
|
336
|
+
} as IFeatureFlagVariationBuffer);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return variation;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export default new FB();
|
|
343
|
+
|
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { debugModeQueryStr } from "./constants";
|
|
2
|
+
|
|
3
|
+
// get debug mode from query string
|
|
4
|
+
const queryString = window.location.search;
|
|
5
|
+
const urlParams = new URLSearchParams(queryString);
|
|
6
|
+
const debugModeParam = urlParams.get(debugModeQueryStr);
|
|
7
|
+
|
|
8
|
+
export const logger = {
|
|
9
|
+
logDebug(...args) {
|
|
10
|
+
if (debugModeParam === 'true') {
|
|
11
|
+
console.log(...args);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
log(...args) {
|
|
16
|
+
console.log(...args);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { websocketReconnectTopic } from "./constants";
|
|
2
|
+
import { eventHub } from "./events";
|
|
3
|
+
import { logger } from "./logger";
|
|
4
|
+
import { IInsight, InsightType, IStreamResponse, IUser } from "./types";
|
|
5
|
+
import { generateConnectionToken } from "./utils";
|
|
6
|
+
import throttleUtil from "./throttleutil";
|
|
7
|
+
|
|
8
|
+
const socketConnectionIntervals = [250, 500, 1000, 2000, 4000, 8000];
|
|
9
|
+
|
|
10
|
+
class NetworkService {
|
|
11
|
+
private user: IUser | undefined;
|
|
12
|
+
private streamingUri: string | undefined;
|
|
13
|
+
private eventsUri: string | undefined;
|
|
14
|
+
private secret: string | undefined;
|
|
15
|
+
private appType: string | undefined;
|
|
16
|
+
|
|
17
|
+
private retryCounter = 0;
|
|
18
|
+
|
|
19
|
+
constructor(){}
|
|
20
|
+
|
|
21
|
+
init(streamingUri: string, eventsUri: string, secret: string, appType: string) {
|
|
22
|
+
this.streamingUri = streamingUri;
|
|
23
|
+
this.eventsUri = eventsUri;
|
|
24
|
+
this.secret = secret;
|
|
25
|
+
this.appType = appType;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
identify(user: IUser, sendIdentifyMessage: boolean) {
|
|
29
|
+
this.user = { ...user };
|
|
30
|
+
throttleUtil.setKey(this.user?.keyId);
|
|
31
|
+
|
|
32
|
+
if (sendIdentifyMessage && this.socket) {
|
|
33
|
+
this.sendUserIdentifyMessage(0);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private sendUserIdentifyMessage(timestamp: number) {
|
|
38
|
+
const { name, keyId, customizedProperties } = this.user!;
|
|
39
|
+
const payload = {
|
|
40
|
+
messageType: 'data-sync',
|
|
41
|
+
data: {
|
|
42
|
+
user: {
|
|
43
|
+
name,
|
|
44
|
+
keyId,
|
|
45
|
+
customizedProperties,
|
|
46
|
+
},
|
|
47
|
+
timestamp
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
53
|
+
logger.logDebug('sending user identify message');
|
|
54
|
+
this.socket?.send(JSON.stringify(payload));
|
|
55
|
+
} else {
|
|
56
|
+
logger.logDebug(`didn't send user identify message because socket not open`);
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
logger.logDebug(err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private socket: WebSocket | undefined | any;
|
|
64
|
+
|
|
65
|
+
private reconnect() {
|
|
66
|
+
this.socket = null;
|
|
67
|
+
const waitTime = socketConnectionIntervals[Math.min(this.retryCounter++, socketConnectionIntervals.length - 1)];
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
logger.logDebug('emit reconnect event');
|
|
70
|
+
eventHub.emit(websocketReconnectTopic, {});
|
|
71
|
+
}, waitTime);
|
|
72
|
+
logger.logDebug(waitTime);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private sendPingMessage() {
|
|
76
|
+
const payload = {
|
|
77
|
+
messageType: 'ping',
|
|
78
|
+
data: null
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
try {
|
|
83
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
84
|
+
logger.logDebug('sending ping')
|
|
85
|
+
this.socket.send(JSON.stringify(payload));
|
|
86
|
+
this.sendPingMessage();
|
|
87
|
+
} else {
|
|
88
|
+
logger.logDebug(`socket closed at ${new Date()}`);
|
|
89
|
+
this.reconnect();
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
logger.logDebug(err);
|
|
93
|
+
}
|
|
94
|
+
}, 18000);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
createConnection(timestamp: number, onMessage: (response: IStreamResponse) => any) {
|
|
98
|
+
const that = this;
|
|
99
|
+
if (that.socket) {
|
|
100
|
+
onMessage({} as IStreamResponse);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const startTime = Date.now();
|
|
105
|
+
// Create WebSocket connection.
|
|
106
|
+
const url = `${this.streamingUri}/streaming?type=client&token=${generateConnectionToken(this.secret!)}`;
|
|
107
|
+
that.socket = new WebSocket(url);
|
|
108
|
+
|
|
109
|
+
// Connection opened
|
|
110
|
+
that.socket.addEventListener('open', function (this: WebSocket, event) {
|
|
111
|
+
that.retryCounter = 0;
|
|
112
|
+
// this is the websocket instance to which the current listener is binded to, it's different from that.socket
|
|
113
|
+
logger.logDebug(`Connection time: ${Date.now() - startTime} ms`);
|
|
114
|
+
that.sendUserIdentifyMessage(timestamp);
|
|
115
|
+
that.sendPingMessage();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Connection closed
|
|
119
|
+
that.socket.addEventListener('close', function (event) {
|
|
120
|
+
logger.logDebug('close');
|
|
121
|
+
if (event.code === 4003) { // do not reconnect when 4003
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
that.reconnect();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Connection error
|
|
129
|
+
that.socket!.addEventListener('error', function (event) {
|
|
130
|
+
// reconnect
|
|
131
|
+
logger.logDebug('error');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Listen for messages
|
|
135
|
+
that.socket.addEventListener('message', function (event) {
|
|
136
|
+
const message = JSON.parse(event.data);
|
|
137
|
+
if (message.messageType === 'data-sync') {
|
|
138
|
+
onMessage(message.data);
|
|
139
|
+
if (message.data.featureFlags.length > 0) {
|
|
140
|
+
logger.logDebug('socket push update time(ms): ', Date.now() - message.data.featureFlags[0].timestamp);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private __getUserInfo(): any {
|
|
147
|
+
const { name, keyId, customizedProperties } = this.user!;
|
|
148
|
+
return {
|
|
149
|
+
name: name,
|
|
150
|
+
keyId: keyId,
|
|
151
|
+
customizedProperties: customizedProperties,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
sendInsights = throttleUtil.throttleAsync(async (data: IInsight[]): Promise<void> => {
|
|
156
|
+
if (!this.secret || !this.user || !data || data.length === 0) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const payload = [{
|
|
162
|
+
user: this.__getUserInfo(),
|
|
163
|
+
variations: data.filter(d => d.insightType === InsightType.featureFlagUsage).map(v => ({
|
|
164
|
+
featureFlagKey: v.id,
|
|
165
|
+
sendToExperiment: v.sendToExperiment,
|
|
166
|
+
timestamp: v.timestamp,
|
|
167
|
+
variation: {
|
|
168
|
+
id: v.variation!.id,
|
|
169
|
+
value: v.variation!.value
|
|
170
|
+
}
|
|
171
|
+
})),
|
|
172
|
+
metrics: data.filter(d => d.insightType !== InsightType.featureFlagUsage).map(d => ({
|
|
173
|
+
route: location.pathname,
|
|
174
|
+
timestamp: d.timestamp,
|
|
175
|
+
numericValue: d.numericValue === null || d.numericValue === undefined? 1 : d.numericValue,
|
|
176
|
+
appType: this.appType,
|
|
177
|
+
eventName: d.eventName,
|
|
178
|
+
type: d.type
|
|
179
|
+
}))
|
|
180
|
+
}];
|
|
181
|
+
|
|
182
|
+
await post(`${this.eventsUri}/api/public/insight/track`, payload, { Authorization: this.secret });
|
|
183
|
+
} catch (err) {
|
|
184
|
+
logger.logDebug(err);
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const networkService = new NetworkService();
|
|
190
|
+
|
|
191
|
+
export async function post(url: string = '', data: any = {}, headers: { [key: string]: string } = {}) {
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetch(url, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: Object.assign({
|
|
196
|
+
'Content-Type': 'application/json'
|
|
197
|
+
}, headers),
|
|
198
|
+
body: JSON.stringify(data) // body data type must match "Content-Type" header
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return response.status === 200 ? response.json() : {};
|
|
202
|
+
} catch (err) {
|
|
203
|
+
logger.logDebug(err);
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function get(url: string = '', headers: { [key: string]: string } = {}) {
|
|
209
|
+
try {
|
|
210
|
+
const response = await fetch(url, {
|
|
211
|
+
method: 'GET',
|
|
212
|
+
headers: Object.assign({
|
|
213
|
+
'Accept': 'application/json',
|
|
214
|
+
'Content-Type': 'application/json'
|
|
215
|
+
}, headers)
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return response.status === 200 ? response.json() : {};
|
|
219
|
+
} catch (err) {
|
|
220
|
+
logger.logDebug(err);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default class OptionMessages {
|
|
2
|
+
static partialEndpoint(name: string): string {
|
|
3
|
+
return `You have set custom uris without specifying the ${ name } URI; connections may not work properly`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
static invalidParam(name: string): string {
|
|
7
|
+
return `The ${ name } option is not passed in or its value is invalid`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static mandatory(name: string): string {
|
|
11
|
+
return `${ name } is mandatory`;
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/queue.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { eventHub } from "./events";
|
|
2
|
+
|
|
3
|
+
export class Queue<T> {
|
|
4
|
+
private queue: T[];
|
|
5
|
+
// flushLimit === 0 means no limit
|
|
6
|
+
// and
|
|
7
|
+
constructor(private flushLimit: number = 0, private arriveflushLimitTopic: string = '') {
|
|
8
|
+
this.queue = [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
add(element: T): void {
|
|
12
|
+
this.queue.push(element);
|
|
13
|
+
if (this.flushLimit > 0 && this.queue.length >= this.flushLimit) {
|
|
14
|
+
eventHub.emit(this.arriveflushLimitTopic, {});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
flush(): T[] {
|
|
19
|
+
const allElements = [...this.queue];
|
|
20
|
+
this.queue = [];
|
|
21
|
+
return allElements;
|
|
22
|
+
}
|
|
23
|
+
}
|