@statsig/client-core 3.8.3 → 3.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statsig/client-core",
3
- "version": "3.8.3",
3
+ "version": "3.9.0",
4
4
  "dependencies": {},
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -19,3 +19,4 @@ export type ExperimentEvaluationOptions = EvaluationOptionsCommon & {
19
19
  };
20
20
  export type LayerEvaluationOptions = EvaluationOptionsCommon & {};
21
21
  export type ParameterStoreEvaluationOptions = EvaluationOptionsCommon & {};
22
+ export type AnyEvaluationOptions = FeatureGateEvaluationOptions | DynamicConfigEvaluationOptions | ExperimentEvaluationOptions | LayerEvaluationOptions | ParameterStoreEvaluationOptions;
@@ -19,11 +19,11 @@ const StatsigEvent_1 = require("./StatsigEvent");
19
19
  const StorageProvider_1 = require("./StorageProvider");
20
20
  const UrlConfiguration_1 = require("./UrlConfiguration");
21
21
  const VisibilityObserving_1 = require("./VisibilityObserving");
22
- const DEFAULT_QUEUE_SIZE = 50;
22
+ const DEFAULT_QUEUE_SIZE = 100;
23
23
  const DEFAULT_FLUSH_INTERVAL_MS = 10000;
24
24
  const MAX_DEDUPER_KEYS = 1000;
25
- const DEDUPER_WINDOW_DURATION_MS = 60000;
26
- const MAX_FAILED_LOGS = 500;
25
+ const DEDUPER_WINDOW_DURATION_MS = 600000;
26
+ const MAX_FAILED_LOGS = 1000;
27
27
  const QUICK_FLUSH_WINDOW_MS = 200;
28
28
  const EVENT_LOGGER_MAP = {};
29
29
  const RetryFailedLogsTrigger = {
@@ -0,0 +1,2 @@
1
+ import { AnyEvaluationOptions } from './EvaluationOptions';
2
+ export declare function createMemoKey(name: string, options?: AnyEvaluationOptions): string | undefined;
package/src/MemoKey.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMemoKey = void 0;
4
+ const EXIST_KEYS = new Set([
5
+ // Add keys that should be memoized based only on their existence, not their value
6
+ ]);
7
+ const DO_NOT_MEMO_KEYS = new Set([
8
+ // Add keys that if exist, should not be memoized
9
+ 'userPersistedValues',
10
+ ]);
11
+ function createMemoKey(name, options) {
12
+ let cacheKey = name;
13
+ if (!options) {
14
+ return cacheKey;
15
+ }
16
+ for (const key of Object.keys(options)) {
17
+ if (DO_NOT_MEMO_KEYS.has(key)) {
18
+ return undefined;
19
+ }
20
+ if (EXIST_KEYS.has(key)) {
21
+ cacheKey += `${key}=true`;
22
+ }
23
+ else {
24
+ cacheKey += `${key}=${options[key]}`;
25
+ }
26
+ }
27
+ return cacheKey;
28
+ }
29
+ exports.createMemoKey = createMemoKey;
@@ -30,6 +30,7 @@ export declare class NetworkCore {
30
30
  private readonly _netConfig;
31
31
  private readonly _options;
32
32
  private readonly _fallbackResolver;
33
+ private _leakyBucket;
33
34
  private _errorBoundary;
34
35
  constructor(options: AnyStatsigOptions | null, _emitter?: StatsigClientEmitEventFunc | undefined);
35
36
  setErrorBoundary(errorBoundary: ErrorBoundary): void;
@@ -38,6 +39,7 @@ export declare class NetworkCore {
38
39
  post(args: RequestArgsWithData): Promise<NetworkResponse | null>;
39
40
  get(args: RequestArgs): Promise<NetworkResponse | null>;
40
41
  private _sendRequest;
42
+ private _isRateLimited;
41
43
  private _getPopulatedURL;
42
44
  private _getPopulatedBody;
43
45
  private _attemptToEncodeString;
@@ -26,6 +26,9 @@ const VisibilityObserving_1 = require("./VisibilityObserving");
26
26
  const DEFAULT_TIMEOUT_MS = 10000;
27
27
  const BACKOFF_BASE_MS = 500;
28
28
  const BACKOFF_MAX_MS = 30000;
29
+ const RATE_LIMIT_WINDOW_MS = 1000;
30
+ const RATE_LIMIT_MAX_REQ_COUNT = 50;
31
+ const LEAK_RATE = RATE_LIMIT_MAX_REQ_COUNT / RATE_LIMIT_WINDOW_MS;
29
32
  const RETRYABLE_CODES = new Set([408, 500, 502, 503, 504, 522, 524, 599]);
30
33
  class NetworkCore {
31
34
  constructor(options, _emitter) {
@@ -33,6 +36,7 @@ class NetworkCore {
33
36
  this._timeout = DEFAULT_TIMEOUT_MS;
34
37
  this._netConfig = {};
35
38
  this._options = {};
39
+ this._leakyBucket = {};
36
40
  this._errorBoundary = null;
37
41
  if (options) {
38
42
  this._options = options;
@@ -91,6 +95,11 @@ class NetworkCore {
91
95
  return null;
92
96
  }
93
97
  const { method, body, retries, attempt } = args;
98
+ const endpoint = args.urlConfig.endpoint;
99
+ if (this._isRateLimited(endpoint)) {
100
+ Log_1.Log.warn(`Request to ${endpoint} was blocked because you are making requests too frequently.`);
101
+ return null;
102
+ }
94
103
  const currentAttempt = attempt !== null && attempt !== void 0 ? attempt : 1;
95
104
  const abortController = typeof AbortController !== 'undefined' ? new AbortController() : null;
96
105
  const timeoutHandle = setTimeout(() => {
@@ -109,6 +118,11 @@ class NetworkCore {
109
118
  keepalive,
110
119
  };
111
120
  _tryMarkInitStart(args, currentAttempt);
121
+ const bucket = this._leakyBucket[endpoint];
122
+ if (bucket) {
123
+ bucket.lastRequestTime = Date.now();
124
+ this._leakyBucket[endpoint] = bucket;
125
+ }
112
126
  const func = (_a = this._netConfig.networkOverrideFunc) !== null && _a !== void 0 ? _a : fetch;
113
127
  response = yield func(populatedUrl, config);
114
128
  clearTimeout(timeoutHandle);
@@ -151,6 +165,24 @@ class NetworkCore {
151
165
  }
152
166
  });
153
167
  }
168
+ _isRateLimited(endpoint) {
169
+ var _a;
170
+ const now = Date.now();
171
+ const bucket = (_a = this._leakyBucket[endpoint]) !== null && _a !== void 0 ? _a : {
172
+ count: 0,
173
+ lastRequestTime: now,
174
+ };
175
+ const elapsed = now - bucket.lastRequestTime;
176
+ const leakedRequests = Math.floor(elapsed * LEAK_RATE);
177
+ bucket.count = Math.max(0, bucket.count - leakedRequests);
178
+ if (bucket.count >= RATE_LIMIT_MAX_REQ_COUNT) {
179
+ return true;
180
+ }
181
+ bucket.count += 1;
182
+ bucket.lastRequestTime = now;
183
+ this._leakyBucket[endpoint] = bucket;
184
+ return false;
185
+ }
154
186
  _getPopulatedURL(args) {
155
187
  var _a;
156
188
  return __awaiter(this, void 0, void 0, function* () {
@@ -1,6 +1,6 @@
1
1
  import './$_StatsigGlobal';
2
2
  import { ErrorBoundary } from './ErrorBoundary';
3
- import { EvaluationOptionsCommon } from './EvaluationOptions';
3
+ import { AnyEvaluationOptions, EvaluationOptionsCommon } from './EvaluationOptions';
4
4
  import { EventLogger } from './EventLogger';
5
5
  import { NetworkCore } from './NetworkCore';
6
6
  import { OverrideAdapter } from './OverrideAdapter';
@@ -26,6 +26,7 @@ export declare abstract class StatsigClientBase<TAdapter extends EvaluationsData
26
26
  protected readonly _errorBoundary: ErrorBoundary;
27
27
  protected readonly _logger: EventLogger;
28
28
  protected _initializePromise: Promise<void> | null;
29
+ protected _memoCache: Record<string, unknown>;
29
30
  private _listeners;
30
31
  constructor(sdkKey: string, adapter: TAdapter, network: NetworkCore, options: AnyStatsigOptions | null);
31
32
  /**
@@ -66,5 +67,6 @@ export declare abstract class StatsigClientBase<TAdapter extends EvaluationsData
66
67
  $emt(event: AnyStatsigClientEvent): void;
67
68
  protected _setStatus(newStatus: StatsigLoadingStatus, values: DataAdapterResult | null): void;
68
69
  protected _enqueueExposure(name: string, exposure: StatsigEventInternal, options?: EvaluationOptionsCommon): void;
70
+ protected _memoize<T, O extends AnyEvaluationOptions>(fn: (name: string, options?: O) => T): (name: string, options?: O) => T;
69
71
  protected abstract _primeReadyRipcord(): void;
70
72
  }
@@ -15,6 +15,7 @@ const __StatsigGlobal_1 = require("./$_StatsigGlobal");
15
15
  const ErrorBoundary_1 = require("./ErrorBoundary");
16
16
  const EventLogger_1 = require("./EventLogger");
17
17
  const Log_1 = require("./Log");
18
+ const MemoKey_1 = require("./MemoKey");
18
19
  const SafeJs_1 = require("./SafeJs");
19
20
  const SessionID_1 = require("./SessionID");
20
21
  const StorageProvider_1 = require("./StorageProvider");
@@ -32,6 +33,7 @@ class StatsigClientBase {
32
33
  (options === null || options === void 0 ? void 0 : options.storageProvider) && StorageProvider_1.Storage._setProvider(options.storageProvider);
33
34
  this._sdkKey = sdkKey;
34
35
  this._options = options !== null && options !== void 0 ? options : {};
36
+ this._memoCache = {};
35
37
  this.overrideAdapter = (_a = options === null || options === void 0 ? void 0 : options.overrideAdapter) !== null && _a !== void 0 ? _a : null;
36
38
  this._logger = new EventLogger_1.EventLogger(sdkKey, emitter, network, options);
37
39
  this._errorBoundary = new ErrorBoundary_1.ErrorBoundary(sdkKey, options, emitter);
@@ -134,6 +136,7 @@ class StatsigClientBase {
134
136
  }
135
137
  _setStatus(newStatus, values) {
136
138
  this.loadingStatus = newStatus;
139
+ this._memoCache = {};
137
140
  this.$emt({ name: 'values_updated', status: newStatus, values });
138
141
  }
139
142
  _enqueueExposure(name, exposure, options) {
@@ -143,6 +146,18 @@ class StatsigClientBase {
143
146
  }
144
147
  this._logger.enqueue(exposure);
145
148
  }
149
+ _memoize(fn) {
150
+ return (name, options) => {
151
+ const memoKey = (0, MemoKey_1.createMemoKey)(name, options);
152
+ if (!memoKey) {
153
+ return fn(name, options);
154
+ }
155
+ if (!(memoKey in this._memoCache)) {
156
+ this._memoCache[memoKey] = fn(name, options);
157
+ }
158
+ return this._memoCache[memoKey];
159
+ };
160
+ }
146
161
  }
147
162
  exports.StatsigClientBase = StatsigClientBase;
148
163
  function _assignGlobalInstance(sdkKey, client) {
@@ -1,4 +1,4 @@
1
- export declare const SDK_VERSION = "3.8.3";
1
+ export declare const SDK_VERSION = "3.9.0";
2
2
  export type StatsigMetadata = {
3
3
  readonly [key: string]: string | undefined;
4
4
  readonly appVersion?: string;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StatsigMetadataProvider = exports.SDK_VERSION = void 0;
4
- exports.SDK_VERSION = '3.8.3';
4
+ exports.SDK_VERSION = '3.9.0';
5
5
  let metadata = {
6
6
  sdkVersion: exports.SDK_VERSION,
7
7
  sdkType: 'js-mono', // js-mono is overwritten by Precomp and OnDevice clients
@@ -38,4 +38,4 @@ export type ParameterStore = Flatten<{
38
38
  readonly __configuration: ParamStoreConfig | null;
39
39
  }>;
40
40
  export type AnyConfigBasedStatsigType = DynamicConfig | Experiment | Layer;
41
- export type AnyStatsigType = FeatureGate | AnyConfigBasedStatsigType;
41
+ export type AnyStatsigType = FeatureGate | AnyConfigBasedStatsigType | ParameterStore;