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