flipflag-sdk 1.1.21 → 1.1.23
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/dist/index.js +134 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +134 -16
- package/dist/index.mjs.map +1 -1
- package/dist/js.js +144 -16
- package/dist/js.js.map +1 -1
- package/dist/js.mjs +144 -16
- package/dist/js.mjs.map +1 -1
- package/dist/next.js +203 -82
- package/dist/next.js.map +1 -1
- package/dist/next.mjs +203 -82
- package/dist/next.mjs.map +1 -1
- package/dist/react-native.js +134 -16
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +134 -16
- package/dist/react-native.mjs.map +1 -1
- package/dist/react.js +291 -120
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +292 -121
- package/dist/react.mjs.map +1 -1
- package/dist/types/core/FlipFlagSDK.d.ts +23 -2
- package/dist/types/js/featureFlagManager.d.ts +4 -0
- package/dist/types/next/server-utils.d.ts +2 -2
- package/dist/types/next/types.d.ts +1 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/vue.js +134 -16
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +134 -16
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/next.js
CHANGED
|
@@ -8,6 +8,9 @@ class FlipFlagSDK {
|
|
|
8
8
|
this.cache = new Map();
|
|
9
9
|
this.defaultBaseUrl = "https://app.flipflag.ru";
|
|
10
10
|
this.stateListeners = new Set();
|
|
11
|
+
this.maxListeners = 100; // Maximum number of listeners to prevent memory leaks
|
|
12
|
+
// Validate required configuration
|
|
13
|
+
this.validateConfig(config);
|
|
11
14
|
this.config = {
|
|
12
15
|
baseUrl: this.defaultBaseUrl,
|
|
13
16
|
cacheTimeout: 30000, // 30 seconds
|
|
@@ -32,9 +35,18 @@ class FlipFlagSDK {
|
|
|
32
35
|
* Identify user for A/B testing and user-specific flags
|
|
33
36
|
*/
|
|
34
37
|
identifyUser(options) {
|
|
38
|
+
if (!options || typeof options !== "object") {
|
|
39
|
+
throw new Error("options must be an object");
|
|
40
|
+
}
|
|
41
|
+
if (!options.userId || typeof options.userId !== "string") {
|
|
42
|
+
throw new Error("userId is required and must be a string");
|
|
43
|
+
}
|
|
44
|
+
if (options.userProperties && typeof options.userProperties !== "object") {
|
|
45
|
+
throw new Error("userProperties must be an object");
|
|
46
|
+
}
|
|
35
47
|
this.config.userId = options.userId;
|
|
36
48
|
// Clear A/B test cache since user changed
|
|
37
|
-
this.
|
|
49
|
+
this.clearAllABTestCache();
|
|
38
50
|
// If we have a user, we might want to refresh immediately
|
|
39
51
|
if (this.config.pullInterval && this.config.pullInterval > 0) {
|
|
40
52
|
this.pullData();
|
|
@@ -50,6 +62,9 @@ class FlipFlagSDK {
|
|
|
50
62
|
* Subscribe to state changes
|
|
51
63
|
*/
|
|
52
64
|
subscribe(callback) {
|
|
65
|
+
if (this.stateListeners.size >= this.maxListeners) {
|
|
66
|
+
console.warn(`Maximum number of listeners (${this.maxListeners}) reached. This may cause memory leaks.`);
|
|
67
|
+
}
|
|
53
68
|
this.stateListeners.add(callback);
|
|
54
69
|
// Return unsubscribe function
|
|
55
70
|
return () => {
|
|
@@ -160,6 +175,8 @@ class FlipFlagSDK {
|
|
|
160
175
|
}
|
|
161
176
|
catch (error) {
|
|
162
177
|
console.error("State listener error:", error);
|
|
178
|
+
// Remove faulty listener to prevent repeated errors
|
|
179
|
+
this.stateListeners.delete(callback);
|
|
163
180
|
}
|
|
164
181
|
});
|
|
165
182
|
}
|
|
@@ -196,9 +213,9 @@ class FlipFlagSDK {
|
|
|
196
213
|
}
|
|
197
214
|
}
|
|
198
215
|
/**
|
|
199
|
-
* Clear A/B test cache
|
|
216
|
+
* Clear all A/B test cache (private method)
|
|
200
217
|
*/
|
|
201
|
-
|
|
218
|
+
clearAllABTestCache() {
|
|
202
219
|
const abTestKeys = Array.from(this.cache.keys()).filter((key) => key.startsWith("abtest:"));
|
|
203
220
|
abTestKeys.forEach((key) => this.cache.delete(key));
|
|
204
221
|
}
|
|
@@ -224,6 +241,15 @@ class FlipFlagSDK {
|
|
|
224
241
|
*/
|
|
225
242
|
async getFlag(flagName, projectId = this.config.projectId, environment) {
|
|
226
243
|
var _a, _b;
|
|
244
|
+
if (!flagName || typeof flagName !== "string") {
|
|
245
|
+
throw new Error("flagName is required and must be a string");
|
|
246
|
+
}
|
|
247
|
+
if (!projectId || typeof projectId !== "string") {
|
|
248
|
+
throw new Error("projectId must be a string");
|
|
249
|
+
}
|
|
250
|
+
if (environment && typeof environment !== "string") {
|
|
251
|
+
throw new Error("environment must be a string");
|
|
252
|
+
}
|
|
227
253
|
// If we have the flag in state, return it immediately
|
|
228
254
|
if (this.state.flags[flagName] !== undefined) {
|
|
229
255
|
return {
|
|
@@ -235,20 +261,18 @@ class FlipFlagSDK {
|
|
|
235
261
|
};
|
|
236
262
|
}
|
|
237
263
|
// Check if we're currently loading data (pullData in progress)
|
|
238
|
-
// In this case, wait
|
|
264
|
+
// In this case, wait for loading to complete using Promise
|
|
239
265
|
if (this.state.isLoading) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
};
|
|
251
|
-
}
|
|
266
|
+
await this.waitForLoadingComplete();
|
|
267
|
+
// After waiting, check state again
|
|
268
|
+
if (this.state.flags[flagName] !== undefined) {
|
|
269
|
+
return {
|
|
270
|
+
projectId: this.config.projectId,
|
|
271
|
+
flagName,
|
|
272
|
+
environment: this.config.environment || "all",
|
|
273
|
+
value: this.state.flags[flagName],
|
|
274
|
+
timestamp: ((_b = this.state.lastFetch) === null || _b === void 0 ? void 0 : _b.toISOString()) || new Date().toISOString(),
|
|
275
|
+
};
|
|
252
276
|
}
|
|
253
277
|
}
|
|
254
278
|
const cacheKey = `flag:${projectId}:${flagName}:${environment || "all"}`;
|
|
@@ -264,10 +288,76 @@ class FlipFlagSDK {
|
|
|
264
288
|
this.setCache(cacheKey, response, this.config.cacheTimeout);
|
|
265
289
|
return response;
|
|
266
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Validate configuration
|
|
293
|
+
*/
|
|
294
|
+
validateConfig(config) {
|
|
295
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
296
|
+
throw new Error("apiKey is required and must be a string");
|
|
297
|
+
}
|
|
298
|
+
if (!config.projectId || typeof config.projectId !== "string") {
|
|
299
|
+
throw new Error("projectId is required and must be a string");
|
|
300
|
+
}
|
|
301
|
+
if (config.baseUrl && typeof config.baseUrl !== "string") {
|
|
302
|
+
throw new Error("baseUrl must be a string");
|
|
303
|
+
}
|
|
304
|
+
if (config.environment && typeof config.environment !== "string") {
|
|
305
|
+
throw new Error("environment must be a string");
|
|
306
|
+
}
|
|
307
|
+
if (config.userId && typeof config.userId !== "string") {
|
|
308
|
+
throw new Error("userId must be a string");
|
|
309
|
+
}
|
|
310
|
+
if (config.cacheTimeout &&
|
|
311
|
+
(typeof config.cacheTimeout !== "number" || config.cacheTimeout < 0)) {
|
|
312
|
+
throw new Error("cacheTimeout must be a non-negative number");
|
|
313
|
+
}
|
|
314
|
+
if (config.retryAttempts &&
|
|
315
|
+
(typeof config.retryAttempts !== "number" || config.retryAttempts < 0)) {
|
|
316
|
+
throw new Error("retryAttempts must be a non-negative number");
|
|
317
|
+
}
|
|
318
|
+
if (config.retryDelay &&
|
|
319
|
+
(typeof config.retryDelay !== "number" || config.retryDelay < 0)) {
|
|
320
|
+
throw new Error("retryDelay must be a non-negative number");
|
|
321
|
+
}
|
|
322
|
+
if (config.pullInterval &&
|
|
323
|
+
(typeof config.pullInterval !== "number" || config.pullInterval < 0)) {
|
|
324
|
+
throw new Error("pullInterval must be a non-negative number");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Wait for loading to complete with timeout
|
|
329
|
+
*/
|
|
330
|
+
async waitForLoadingComplete(timeout = 5000) {
|
|
331
|
+
const startTime = Date.now();
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
const checkLoading = () => {
|
|
334
|
+
if (!this.state.isLoading) {
|
|
335
|
+
resolve();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (Date.now() - startTime > timeout) {
|
|
339
|
+
reject(new Error("Timeout waiting for SDK loading to complete"));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Check again after a short delay
|
|
343
|
+
setTimeout(checkLoading, 50);
|
|
344
|
+
};
|
|
345
|
+
checkLoading();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
267
348
|
/**
|
|
268
349
|
* Get A/B test variant for a user
|
|
269
350
|
*/
|
|
270
351
|
async getAbTestVariant(testName, userId, projectId = this.config.projectId) {
|
|
352
|
+
if (!testName || typeof testName !== "string") {
|
|
353
|
+
throw new Error("testName is required and must be a string");
|
|
354
|
+
}
|
|
355
|
+
if (!userId || typeof userId !== "string") {
|
|
356
|
+
throw new Error("userId is required and must be a string");
|
|
357
|
+
}
|
|
358
|
+
if (!projectId || typeof projectId !== "string") {
|
|
359
|
+
throw new Error("projectId must be a string");
|
|
360
|
+
}
|
|
271
361
|
// If we have the A/B test in state, return it immediately
|
|
272
362
|
const stateVariant = this.state.abTests[testName];
|
|
273
363
|
if (stateVariant && stateVariant.testName === testName) {
|
|
@@ -331,6 +421,34 @@ class FlipFlagSDK {
|
|
|
331
421
|
this.cache.clear();
|
|
332
422
|
}
|
|
333
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Clear cache for specific flag
|
|
426
|
+
*/
|
|
427
|
+
clearFlagCache(flagName, environment) {
|
|
428
|
+
const cacheKey = `flag:${this.config.projectId}:${flagName}:${environment || "all"}`;
|
|
429
|
+
this.cache.delete(cacheKey);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Clear cache for all flags
|
|
433
|
+
*/
|
|
434
|
+
clearFlagsCache(environment) {
|
|
435
|
+
const cacheKey = `flags:${this.config.projectId}:${environment || "all"}`;
|
|
436
|
+
this.cache.delete(cacheKey);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Clear cache for A/B test variant
|
|
440
|
+
*/
|
|
441
|
+
clearABTestCache(testName, userId) {
|
|
442
|
+
if (testName && userId) {
|
|
443
|
+
const cacheKey = `abtest:${this.config.projectId}:${testName}:${userId}`;
|
|
444
|
+
this.cache.delete(cacheKey);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// Clear all A/B test cache
|
|
448
|
+
const abTestKeys = Array.from(this.cache.keys()).filter((key) => key.startsWith("abtest:"));
|
|
449
|
+
abTestKeys.forEach((key) => this.cache.delete(key));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
334
452
|
/**
|
|
335
453
|
* Update configuration
|
|
336
454
|
*/
|
|
@@ -483,32 +601,38 @@ function useFeatureFlag(flagName, options = {}) {
|
|
|
483
601
|
finally {
|
|
484
602
|
setLoading(false);
|
|
485
603
|
}
|
|
486
|
-
}, [
|
|
487
|
-
sdk,
|
|
488
|
-
flagName,
|
|
489
|
-
options.fallbackValue,
|
|
490
|
-
options.enabled,
|
|
491
|
-
shouldSkipInitialFetch,
|
|
492
|
-
]);
|
|
604
|
+
}, [sdk, flagName, options.fallbackValue, options.enabled]);
|
|
493
605
|
const refetch = react.useCallback(async () => {
|
|
494
606
|
sdk.clearCache(`flag:${sdk["config"].projectId}:${flagName}:${sdk["config"].environment || "all"}`);
|
|
495
607
|
await fetchFlag();
|
|
496
608
|
}, [sdk, flagName, fetchFlag]);
|
|
497
609
|
// Subscribe to SDK state changes for automatic updates via pullInterval
|
|
498
610
|
react.useEffect(() => {
|
|
611
|
+
let lastFetchTime = null;
|
|
499
612
|
const unsubscribe = sdk.subscribe((state) => {
|
|
500
|
-
// Only update if this is
|
|
501
|
-
if (state.lastFetch &&
|
|
613
|
+
// Only update if this is a new fetch and has flag data for this specific flag
|
|
614
|
+
if (state.lastFetch &&
|
|
615
|
+
state.lastFetch !== lastFetchTime &&
|
|
616
|
+
state.flags[flagName] !== undefined) {
|
|
617
|
+
lastFetchTime = state.lastFetch;
|
|
502
618
|
setValue(state.flags[flagName]);
|
|
503
619
|
setLoading(false);
|
|
504
|
-
setError(
|
|
620
|
+
setError(state.error);
|
|
505
621
|
}
|
|
506
622
|
});
|
|
507
623
|
return unsubscribe;
|
|
508
624
|
}, [sdk, flagName]);
|
|
625
|
+
// Effect to handle initial fetch and server-side state changes
|
|
509
626
|
react.useEffect(() => {
|
|
510
|
-
|
|
511
|
-
|
|
627
|
+
// Only fetch if we don't have server-side data or if server-side data becomes invalid
|
|
628
|
+
if (!shouldSkipInitialFetch) {
|
|
629
|
+
fetchFlag();
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
// If we have server-side data, just set loading to false
|
|
633
|
+
setLoading(false);
|
|
634
|
+
}
|
|
635
|
+
}, [shouldSkipInitialFetch, fetchFlag]);
|
|
512
636
|
return {
|
|
513
637
|
value,
|
|
514
638
|
loading,
|
|
@@ -577,18 +701,16 @@ function useFeatureFlags(flagNames, options = {}) {
|
|
|
577
701
|
finally {
|
|
578
702
|
setLoading(false);
|
|
579
703
|
}
|
|
580
|
-
}, [
|
|
581
|
-
sdk,
|
|
582
|
-
memoizedFlagNames,
|
|
583
|
-
memoizedFallbackValues,
|
|
584
|
-
options.enabled,
|
|
585
|
-
shouldSkipInitialFetch,
|
|
586
|
-
]);
|
|
704
|
+
}, [sdk, memoizedFlagNames, memoizedFallbackValues, options.enabled]);
|
|
587
705
|
// Subscribe to SDK state changes for automatic updates via pullInterval
|
|
588
706
|
react.useEffect(() => {
|
|
707
|
+
let lastFetchTime = null;
|
|
589
708
|
const unsubscribe = sdk.subscribe((state) => {
|
|
590
|
-
// Only update if this is
|
|
591
|
-
if (state.lastFetch &&
|
|
709
|
+
// Only update if this is a new fetch (different from last one) and has flag data
|
|
710
|
+
if (state.lastFetch &&
|
|
711
|
+
state.lastFetch !== lastFetchTime &&
|
|
712
|
+
Object.keys(state.flags).length > 0) {
|
|
713
|
+
lastFetchTime = state.lastFetch;
|
|
592
714
|
const newFlags = {};
|
|
593
715
|
memoizedFlagNames.forEach((flagName) => {
|
|
594
716
|
var _a;
|
|
@@ -602,18 +724,32 @@ function useFeatureFlags(flagNames, options = {}) {
|
|
|
602
724
|
});
|
|
603
725
|
setFlags(newFlags);
|
|
604
726
|
setLoading(false);
|
|
605
|
-
setError(
|
|
727
|
+
setError(state.error);
|
|
606
728
|
}
|
|
607
729
|
});
|
|
608
730
|
return unsubscribe;
|
|
609
731
|
}, [sdk, memoizedFlagNames, memoizedFallbackValues]);
|
|
732
|
+
// Effect to handle initial fetch and server-side state changes
|
|
610
733
|
react.useEffect(() => {
|
|
611
|
-
|
|
612
|
-
|
|
734
|
+
// Only fetch if we don't have server-side data or if server-side data becomes invalid
|
|
735
|
+
if (!shouldSkipInitialFetch) {
|
|
736
|
+
fetchFlags();
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
// If we have server-side data, just set loading to false
|
|
740
|
+
setLoading(false);
|
|
741
|
+
}
|
|
742
|
+
}, [shouldSkipInitialFetch, fetchFlags]);
|
|
743
|
+
const refetch = react.useCallback(async () => {
|
|
744
|
+
// Clear cache for flags
|
|
745
|
+
sdk.clearCache(`flags:${sdk["config"].projectId}:${sdk["config"].environment || "all"}`);
|
|
746
|
+
await fetchFlags();
|
|
747
|
+
}, [sdk, fetchFlags]);
|
|
613
748
|
return {
|
|
614
749
|
flags,
|
|
615
750
|
loading,
|
|
616
751
|
error,
|
|
752
|
+
refetch,
|
|
617
753
|
};
|
|
618
754
|
}
|
|
619
755
|
|
|
@@ -644,7 +780,7 @@ function useABTest(testName, options) {
|
|
|
644
780
|
finally {
|
|
645
781
|
setLoading(false);
|
|
646
782
|
}
|
|
647
|
-
}, [sdk, testName, options.userId
|
|
783
|
+
}, [sdk, testName, options.userId]);
|
|
648
784
|
const recordEvent = react.useCallback(async (event, eventData) => {
|
|
649
785
|
if (!variantId) {
|
|
650
786
|
console.warn("Cannot record event: no variant assigned yet");
|
|
@@ -659,21 +795,33 @@ function useABTest(testName, options) {
|
|
|
659
795
|
}, [sdk, testName, options.userId, variantId]);
|
|
660
796
|
// Subscribe to SDK state changes for automatic updates via pullInterval
|
|
661
797
|
react.useEffect(() => {
|
|
798
|
+
let lastFetchTime = null;
|
|
662
799
|
const unsubscribe = sdk.subscribe((state) => {
|
|
663
|
-
|
|
664
|
-
|
|
800
|
+
if (!options.enabled && options.enabled !== undefined) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
// Only update if this is a new fetch and has A/B test data
|
|
804
|
+
if (state.lastFetch &&
|
|
805
|
+
state.lastFetch !== lastFetchTime &&
|
|
806
|
+
state.abTests[testName]) {
|
|
807
|
+
lastFetchTime = state.lastFetch;
|
|
665
808
|
const newABTest = state.abTests[testName];
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
809
|
+
if (newABTest.variantId !== variantId) {
|
|
810
|
+
setVariant(newABTest.variant.value);
|
|
811
|
+
setVariantId(newABTest.variantId);
|
|
812
|
+
setLoading(false);
|
|
813
|
+
setError(state.error);
|
|
814
|
+
}
|
|
670
815
|
}
|
|
671
816
|
});
|
|
672
817
|
return unsubscribe;
|
|
673
|
-
}, [sdk, testName]);
|
|
818
|
+
}, [sdk, testName, options.enabled, variantId]);
|
|
819
|
+
// Initial fetch effect
|
|
674
820
|
react.useEffect(() => {
|
|
675
|
-
|
|
676
|
-
|
|
821
|
+
if (options.enabled !== false) {
|
|
822
|
+
fetchVariant();
|
|
823
|
+
}
|
|
824
|
+
}, [options.enabled, fetchVariant]);
|
|
677
825
|
return {
|
|
678
826
|
variant,
|
|
679
827
|
variantId,
|
|
@@ -686,20 +834,8 @@ function useABTest(testName, options) {
|
|
|
686
834
|
/**
|
|
687
835
|
* Fetch feature flags on the server side for SSR
|
|
688
836
|
*/
|
|
689
|
-
async function getServerSideFlags(
|
|
690
|
-
|
|
691
|
-
let flagList;
|
|
692
|
-
// Handle different calling patterns
|
|
693
|
-
if (Array.isArray(configOrFlagNames)) {
|
|
694
|
-
// Called with flagNames only - this should not happen in normal usage
|
|
695
|
-
// We'll need to get config from somewhere else, but for now throw error
|
|
696
|
-
throw new Error("getServerSideFlags must be called with config when used outside of FlipflagProvider context");
|
|
697
|
-
}
|
|
698
|
-
else if (configOrFlagNames) {
|
|
699
|
-
config = configOrFlagNames;
|
|
700
|
-
flagList = flagNames;
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
837
|
+
async function getServerSideFlags(config, flagNames) {
|
|
838
|
+
if (!config) {
|
|
703
839
|
throw new Error("getServerSideFlags requires config parameter");
|
|
704
840
|
}
|
|
705
841
|
const sdk = new FlipFlagSDK({
|
|
@@ -711,31 +847,16 @@ async function getServerSideFlags(configOrFlagNames, flagNames) {
|
|
|
711
847
|
});
|
|
712
848
|
try {
|
|
713
849
|
const response = await sdk.getFlags();
|
|
714
|
-
let flags = response.flags;
|
|
715
|
-
// If specific flag names are requested, filter the response
|
|
716
|
-
if (flagList && flagList.length > 0) {
|
|
717
|
-
flags = {};
|
|
718
|
-
flagList.forEach((flagName) => {
|
|
719
|
-
var _a;
|
|
720
|
-
flags[flagName] = (_a = response.flags[flagName]) !== null && _a !== void 0 ? _a : false;
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
850
|
return {
|
|
724
|
-
flags,
|
|
851
|
+
flags: response.flags,
|
|
725
852
|
timestamp: Date.now(),
|
|
726
853
|
};
|
|
727
854
|
}
|
|
728
855
|
catch (error) {
|
|
729
856
|
console.error("Failed to fetch server-side flags:", error);
|
|
730
|
-
// Return
|
|
731
|
-
const fallbackFlags = {};
|
|
732
|
-
if (flagList) {
|
|
733
|
-
flagList.forEach((flagName) => {
|
|
734
|
-
fallbackFlags[flagName] = false;
|
|
735
|
-
});
|
|
736
|
-
}
|
|
857
|
+
// Return empty flags object on error
|
|
737
858
|
return {
|
|
738
|
-
flags:
|
|
859
|
+
flags: {},
|
|
739
860
|
timestamp: Date.now(),
|
|
740
861
|
};
|
|
741
862
|
}
|