@vercel/flags-core 1.4.0 → 1.5.0-2225248-20260525123053

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/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # @vercel/flags-core
2
2
 
3
+ ## 1.5.0-2225248-20260525123053
4
+
5
+ ### Minor Changes
6
+
7
+ - [#385](https://github.com/vercel/flags/pull/385) [`5b2dc37`](https://github.com/vercel/flags/commit/5b2dc37041e24811f735beede41fc8c29f508cb7) Thanks [@dferber90](https://github.com/dferber90)! - Add `bulkEvaluate` method to `FlagsClient` for resolving multiple flags against shared entities in a single call.
8
+
9
+ ```ts
10
+ const results = await client.bulkEvaluate(
11
+ [
12
+ { key: "a", defaultValue: false },
13
+ { key: "b", defaultValue: "off" },
14
+ ],
15
+ entities
16
+ );
17
+
18
+ results.a; // EvaluationResult<boolean>
19
+ results.b; // EvaluationResult<string>
20
+ ```
21
+
22
+ Avoids the per-flag overhead of separate `evaluate()` calls — the datafile is read once, entities are resolved once, and all flags share the same environment/segments lookup. Each entry in the returned record is a full `EvaluationResult` with `value`, `reason`, `outcomeType`, and `metrics`.
23
+
24
+ - [#371](https://github.com/vercel/flags/pull/371) [`bd4d01a`](https://github.com/vercel/flags/commit/bd4d01a9b2b5d70bf7ae62cda645d8cd7292ad83) Thanks [@vincent-derks](https://github.com/vincent-derks)! - Add jitter to ingest retries and the batch-flush window.
25
+
26
+ The usage tracker now uses AWS-style "Full Jitter" exponential backoff between
27
+ retry attempts (replacing the previous deterministic 100/200ms schedule) and
28
+ randomizes the 5s batch-flush window by ±20% to desynchronize concurrent
29
+ processes. When all retry attempts are exhausted the SDK now logs a structured
30
+ warning so consumers can alert on dropped batches.
31
+
32
+ ### Patch Changes
33
+
34
+ - [#382](https://github.com/vercel/flags/pull/382) [`4d90e91`](https://github.com/vercel/flags/commit/4d90e912a4d7c9d4ef986d5e8dc609c30b203242) Thanks [@dferber90](https://github.com/dferber90)! - Speed up flag evaluation on the hot path.
35
+
36
+ - `handleOutcome` no longer recomputes `scaledWeights` on every split-outcome evaluation; the per-outcome scaled weights are cached on first call.
37
+ - `matchConditions` no longer recompiles `RegExp` on every REGEX / NOT_REGEX condition; the compiled regex is cached on first call.
38
+ - `Controller.read()` and `getDatafile()` no longer re-destructure and re-spread the in-memory datafile on every call; the result is cached and rebuilt only when stream/poll replaces the underlying data.
39
+
40
+ In micro-benchmarks the pure `evaluate()` path is ~22% faster for split outcomes and ~32% faster for regex conditions. The full `client.evaluate()` path is 14–22% faster across all scenarios.
41
+
3
42
  ## 1.4.0
4
43
 
5
44
  ### Minor Changes
@@ -7,6 +7,7 @@ var __export = (target, all) => {
7
7
  // src/controller-fns.ts
8
8
  var controller_fns_exports = {};
9
9
  __export(controller_fns_exports, {
10
+ bulkEvaluate: () => bulkEvaluate2,
10
11
  controllerInstanceMap: () => controllerInstanceMap,
11
12
  evaluate: () => evaluate2,
12
13
  getDatafile: () => getDatafile,
@@ -46,6 +47,24 @@ var Packed;
46
47
 
47
48
  // src/evaluate.ts
48
49
  var MAX_REGEX_INPUT_LENGTH = 1e4;
50
+ var UINT32_MAX = 4294967295;
51
+ var SCALED_WEIGHTS = /* @__PURE__ */ Symbol("@vercel/flags-core:scaledWeights");
52
+ var COMPILED_REGEX = /* @__PURE__ */ Symbol("@vercel/flags-core:compiledRegex");
53
+ function getScaledWeights(outcome) {
54
+ const cached = outcome[SCALED_WEIGHTS];
55
+ if (cached) return cached;
56
+ const total = sum(outcome.weights);
57
+ const scaled = outcome.weights.map((w) => w / total * UINT32_MAX);
58
+ outcome[SCALED_WEIGHTS] = scaled;
59
+ return scaled;
60
+ }
61
+ function getCompiledRegex(rhs) {
62
+ const cached = rhs[COMPILED_REGEX];
63
+ if (cached) return cached;
64
+ const compiled = new RegExp(rhs.pattern, rhs.flags);
65
+ rhs[COMPILED_REGEX] = compiled;
66
+ return compiled;
67
+ }
49
68
  function exhaustivenessCheck(_) {
50
69
  throw new Error("Exhaustiveness check failed");
51
70
  }
@@ -232,12 +251,12 @@ function matchConditions(conditions, params) {
232
251
  return (isNumber(rhs) || isString(rhs)) && lhs <= rhs;
233
252
  case "regex" /* REGEX */:
234
253
  if (isString(lhs) && lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === "object" && !Array.isArray(rhs) && _optionalChain([rhs, 'optionalAccess', _12 => _12.type]) === "regex") {
235
- return new RegExp(rhs.pattern, rhs.flags).test(lhs);
254
+ return getCompiledRegex(rhs).test(lhs);
236
255
  }
237
256
  return false;
238
257
  case "!regex" /* NOT_REGEX */:
239
258
  if (isString(lhs) && lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === "object" && !Array.isArray(rhs) && _optionalChain([rhs, 'optionalAccess', _13 => _13.type]) === "regex") {
240
- return !new RegExp(rhs.pattern, rhs.flags).test(lhs);
259
+ return !getCompiledRegex(rhs).test(lhs);
241
260
  }
242
261
  return false;
243
262
  case "before" /* BEFORE */: {
@@ -309,13 +328,9 @@ function handleOutcome(params, outcome) {
309
328
  if (typeof lhs !== "string") {
310
329
  return { value: defaultOutcome, outcomeType: "split" /* SPLIT */ };
311
330
  }
312
- const maxValue = 4294967295;
313
331
  const value = _jsxxhash.xxHash32.call(void 0, lhs, params.definition.seed);
314
- const sumOfWeights = sum(outcome.weights);
315
- const scaledWeights = outcome.weights.map(
316
- (weight) => weight / sumOfWeights * maxValue
317
- );
318
- const variantIndex = findWeightedIndex(scaledWeights, value, maxValue);
332
+ const scaledWeights = getScaledWeights(outcome);
333
+ const variantIndex = findWeightedIndex(scaledWeights, value, UINT32_MAX);
319
334
  return {
320
335
  value: variantIndex === -1 ? defaultOutcome : getVariant(params.definition.variants, variantIndex),
321
336
  outcomeType: "split" /* SPLIT */
@@ -371,9 +386,8 @@ function handleOutcome(params, outcome) {
371
386
  outcomeType: "rollout" /* ROLLOUT */
372
387
  };
373
388
  }
374
- const maxValue = 4294967295;
375
389
  const value = _jsxxhash.xxHash32.call(void 0, lhs, params.definition.seed);
376
- const threshold = currentPromille / 1e5 * maxValue;
390
+ const threshold = currentPromille / 1e5 * UINT32_MAX;
377
391
  return {
378
392
  value: value < threshold ? getVariant(params.definition.variants, outcome.rollToVariant) : getVariant(
379
393
  params.definition.variants,
@@ -440,6 +454,23 @@ function evaluate(params, _visited) {
440
454
  reason: "fallthrough" /* FALLTHROUGH */
441
455
  });
442
456
  }
457
+ function bulkEvaluate(flags, shared) {
458
+ const params = {
459
+ entities: shared.entities,
460
+ environment: shared.environment,
461
+ segments: shared.segments,
462
+ definition: void 0,
463
+ defaultValue: void 0
464
+ };
465
+ const results = {};
466
+ for (const key in flags) {
467
+ const flag = flags[key];
468
+ params.definition = flag.definition;
469
+ params.defaultValue = flag.defaultValue;
470
+ results[key] = evaluate(params);
471
+ }
472
+ return results;
473
+ }
443
474
  function findWeightedIndex(weights, value, maxValue) {
444
475
  if (value < 0 || value >= maxValue) return -1;
445
476
  let sum2 = 0;
@@ -451,7 +482,7 @@ function findWeightedIndex(weights, value, maxValue) {
451
482
  }
452
483
 
453
484
  // package.json
454
- var version = "1.4.0";
485
+ var version = "1.5.0-2225248-20260525123053";
455
486
 
456
487
  // src/lib/report-value.ts
457
488
  function internalReportValue(key, value, data) {
@@ -557,6 +588,78 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
557
588
  }
558
589
  });
559
590
  }
591
+ async function bulkEvaluate2(id, flags, entities) {
592
+ const controller = getInstance(id).controller;
593
+ let datafile;
594
+ try {
595
+ datafile = await controller.read();
596
+ } catch (error) {
597
+ const errorMessage = error instanceof Error ? error.message : "Failed to read datafile";
598
+ const results2 = {};
599
+ for (const flag of flags) {
600
+ results2[flag.key] = {
601
+ value: flag.defaultValue,
602
+ reason: "error" /* ERROR */,
603
+ errorMessage
604
+ };
605
+ }
606
+ return results2;
607
+ }
608
+ const baseMetrics = {
609
+ readMs: datafile.metrics.readMs,
610
+ source: datafile.metrics.source,
611
+ cacheStatus: datafile.metrics.cacheStatus,
612
+ connectionState: datafile.metrics.connectionState,
613
+ mode: datafile.metrics.mode
614
+ };
615
+ const projectId = datafile.projectId;
616
+ const results = {};
617
+ const toEvaluate = {};
618
+ for (const flag of flags) {
619
+ const { key, defaultValue } = flag;
620
+ const flagDefinition = datafile.definitions[key];
621
+ if (flagDefinition === void 0) {
622
+ if (projectId) {
623
+ internalReportValue(key, defaultValue, {
624
+ originProjectId: projectId,
625
+ originProvider: "vercel",
626
+ reason: "error" /* ERROR */
627
+ });
628
+ }
629
+ results[key] = {
630
+ value: defaultValue,
631
+ reason: "error" /* ERROR */,
632
+ errorCode: "FLAG_NOT_FOUND" /* FLAG_NOT_FOUND */,
633
+ errorMessage: `@vercel/flags-core: Definition not found for flag "${key}"`,
634
+ metrics: { evaluationMs: 0, ...baseMetrics }
635
+ };
636
+ continue;
637
+ }
638
+ toEvaluate[key] = { definition: flagDefinition, defaultValue };
639
+ }
640
+ const evalStartTime = Date.now();
641
+ const evaluated = bulkEvaluate(toEvaluate, {
642
+ entities: _nullishCoalesce(entities, () => ( {})),
643
+ environment: datafile.environment,
644
+ segments: datafile.segments
645
+ });
646
+ const evaluationDurationMs = Date.now() - evalStartTime;
647
+ for (const key in toEvaluate) {
648
+ const result = evaluated[key];
649
+ if (projectId) {
650
+ internalReportValue(key, result.value, {
651
+ originProjectId: projectId,
652
+ originProvider: "vercel",
653
+ reason: result.reason,
654
+ outcomeType: result.reason !== "error" /* ERROR */ ? result.outcomeType : void 0
655
+ });
656
+ }
657
+ results[key] = Object.assign(result, {
658
+ metrics: { evaluationMs: evaluationDurationMs, ...baseMetrics }
659
+ });
660
+ }
661
+ return results;
662
+ }
560
663
 
561
664
  // src/create-raw-client.ts
562
665
  var idCount = 0;
@@ -623,6 +726,16 @@ function createCreateRawClient(fns) {
623
726
  }
624
727
  }
625
728
  return fns.evaluate(id, flagKey, defaultValue, entities);
729
+ },
730
+ bulkEvaluate: async (flags, entities) => {
731
+ const instance = controllerInstanceMap.get(id);
732
+ if (!_optionalChain([instance, 'optionalAccess', _22 => _22.initialized])) {
733
+ try {
734
+ await api.initialize();
735
+ } catch (e3) {
736
+ }
737
+ }
738
+ return fns.bulkEvaluate(id, flags, entities);
626
739
  }
627
740
  };
628
741
  return api;
@@ -677,8 +790,25 @@ async function readBundledDefinitions(sdkKey) {
677
790
 
678
791
  // src/utils/usage-tracker.ts
679
792
  var _functions = require('@vercel/functions');
793
+
794
+ // src/utils/backoff.ts
795
+ var DEFAULT_BASE_MS = 250;
796
+ var DEFAULT_CAP_MS = 5e3;
797
+ function getRetryDelayMs(attempt, options = {}) {
798
+ const baseMs = _nullishCoalesce(options.baseMs, () => ( DEFAULT_BASE_MS));
799
+ const capMs = _nullishCoalesce(options.capMs, () => ( DEFAULT_CAP_MS));
800
+ const ceiling = Math.min(capMs, baseMs * 2 ** Math.max(0, attempt - 1));
801
+ return Math.floor(Math.random() * ceiling);
802
+ }
803
+ function getJitteredWaitMs(baseMs, ratio) {
804
+ const min = baseMs * (1 - ratio);
805
+ const span = baseMs * 2 * ratio;
806
+ return Math.floor(min + Math.random() * span);
807
+ }
808
+
809
+ // src/utils/usage-tracker.ts
680
810
  var RESOLVED_VOID = Promise.resolve();
681
- var isDebugMode = _optionalChain([process, 'access', _22 => _22.env, 'access', _23 => _23.DEBUG, 'optionalAccess', _24 => _24.includes, 'call', _25 => _25("@vercel/flags-core")]);
811
+ var isDebugMode = _optionalChain([process, 'access', _23 => _23.env, 'access', _24 => _24.DEBUG, 'optionalAccess', _25 => _25.includes, 'call', _26 => _26("@vercel/flags-core")]);
682
812
  var debugLog = (...args) => {
683
813
  if (!isDebugMode) return;
684
814
  console.log(...args);
@@ -686,11 +816,12 @@ var debugLog = (...args) => {
686
816
  var MAX_RETRIES = 3;
687
817
  var MAX_BATCH_SIZE = 50;
688
818
  var MAX_BATCH_WAIT_MS = 5e3;
819
+ var BATCH_WAIT_JITTER_RATIO = 0.2;
689
820
  var SYMBOL_FOR_REQ_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context");
690
821
  var fromSymbol = globalThis;
691
822
  function getRequestContext() {
692
823
  try {
693
- const ctx = _optionalChain([fromSymbol, 'access', _26 => _26[SYMBOL_FOR_REQ_CONTEXT], 'optionalAccess', _27 => _27.get, 'optionalCall', _28 => _28()]);
824
+ const ctx = _optionalChain([fromSymbol, 'access', _27 => _27[SYMBOL_FOR_REQ_CONTEXT], 'optionalAccess', _28 => _28.get, 'optionalCall', _29 => _29()]);
694
825
  if (ctx && Object.hasOwn(ctx, "headers")) {
695
826
  return {
696
827
  ctx,
@@ -698,7 +829,7 @@ function getRequestContext() {
698
829
  };
699
830
  }
700
831
  return { ctx, headers: void 0 };
701
- } catch (e3) {
832
+ } catch (e4) {
702
833
  return { ctx: void 0, headers: void 0 };
703
834
  }
704
835
  }
@@ -720,7 +851,7 @@ var UsageTracker = (_class = class {
720
851
  */
721
852
  flush() {
722
853
  if (this.batcher.pending) {
723
- _optionalChain([this, 'access', _29 => _29.batcher, 'access', _30 => _30.resolveWait, 'optionalCall', _31 => _31()]);
854
+ _optionalChain([this, 'access', _30 => _30.batcher, 'access', _31 => _31.resolveWait, 'optionalCall', _32 => _32()]);
724
855
  return this.batcher.pending;
725
856
  }
726
857
  if (this.batcher.events.length > 0) {
@@ -792,7 +923,10 @@ var UsageTracker = (_class = class {
792
923
  const pending = (async () => {
793
924
  await new Promise((res) => {
794
925
  this.batcher.resolveWait = res;
795
- timeout = setTimeout(res, MAX_BATCH_WAIT_MS);
926
+ timeout = setTimeout(
927
+ res,
928
+ getJitteredWaitMs(MAX_BATCH_WAIT_MS, BATCH_WAIT_JITTER_RATIO)
929
+ );
796
930
  });
797
931
  this.batcher.pending = null;
798
932
  this.batcher.resolveWait = null;
@@ -801,12 +935,12 @@ var UsageTracker = (_class = class {
801
935
  })();
802
936
  try {
803
937
  _functions.waitUntil.call(void 0, pending);
804
- } catch (e4) {
938
+ } catch (e5) {
805
939
  }
806
940
  this.batcher.pending = pending;
807
941
  }
808
942
  if (this.batcher.events.length >= MAX_BATCH_SIZE) {
809
- _optionalChain([this, 'access', _32 => _32.batcher, 'access', _33 => _33.resolveWait, 'optionalCall', _34 => _34()]);
943
+ _optionalChain([this, 'access', _33 => _33.batcher, 'access', _34 => _34.resolveWait, 'optionalCall', _35 => _35()]);
810
944
  }
811
945
  }
812
946
  async flushEvents() {
@@ -846,7 +980,12 @@ Response body: ${await response.text().catch(() => null)}`
846
980
  error
847
981
  );
848
982
  if (attempt < MAX_RETRIES) {
849
- await new Promise((res) => setTimeout(res, attempt * 100));
983
+ const delayMs = getRetryDelayMs(attempt);
984
+ await new Promise((res) => setTimeout(res, delayMs));
985
+ } else {
986
+ console.error(
987
+ `@vercel/flags-core: Dropped ${eventsToSend.length} events after ${MAX_RETRIES} attempts (flushId=${flushId})`
988
+ );
850
989
  }
851
990
  }
852
991
  }
@@ -911,7 +1050,7 @@ var BundledSource = class {
911
1050
  */
912
1051
  async tryLoad() {
913
1052
  const result = await this.getResult();
914
- if (_optionalChain([result, 'optionalAccess', _35 => _35.state]) === "ok" && result.definitions) {
1053
+ if (_optionalChain([result, 'optionalAccess', _36 => _36.state]) === "ok" && result.definitions) {
915
1054
  return result.definitions;
916
1055
  }
917
1056
  return void 0;
@@ -950,14 +1089,14 @@ async function fetchDatafile(options) {
950
1089
  signal: controller.signal
951
1090
  });
952
1091
  clearTimeout(timeoutId);
953
- _optionalChain([options, 'access', _36 => _36.signal, 'optionalAccess', _37 => _37.removeEventListener, 'call', _38 => _38("abort", onExternalAbort)]);
1092
+ _optionalChain([options, 'access', _37 => _37.signal, 'optionalAccess', _38 => _38.removeEventListener, 'call', _39 => _39("abort", onExternalAbort)]);
954
1093
  if (!res.ok) {
955
1094
  throw new Error(`Failed to fetch data: ${res.statusText}`);
956
1095
  }
957
1096
  return res.json();
958
1097
  } catch (error) {
959
1098
  clearTimeout(timeoutId);
960
- _optionalChain([options, 'access', _39 => _39.signal, 'optionalAccess', _40 => _40.removeEventListener, 'call', _41 => _41("abort", onExternalAbort)]);
1099
+ _optionalChain([options, 'access', _40 => _40.signal, 'optionalAccess', _41 => _41.removeEventListener, 'call', _42 => _42("abort", onExternalAbort)]);
961
1100
  throw error instanceof Error ? error : new Error("Unknown fetch error");
962
1101
  }
963
1102
  }
@@ -1022,7 +1161,7 @@ var TypedEmitter = (_class2 = class {constructor() { _class2.prototype.__init4.c
1022
1161
  set.add(handler);
1023
1162
  }
1024
1163
  off(event, handler) {
1025
- _optionalChain([this, 'access', _42 => _42.handlers, 'access', _43 => _43.get, 'call', _44 => _44(event), 'optionalAccess', _45 => _45.delete, 'call', _46 => _46(handler)]);
1164
+ _optionalChain([this, 'access', _43 => _43.handlers, 'access', _44 => _44.get, 'call', _45 => _45(event), 'optionalAccess', _46 => _46.delete, 'call', _47 => _47(handler)]);
1026
1165
  }
1027
1166
  emit(event, ...args) {
1028
1167
  const set = this.handlers.get(event);
@@ -1048,11 +1187,11 @@ var PollingSource = class extends TypedEmitter {
1048
1187
  * Emits 'data' on success, 'error' on failure.
1049
1188
  */
1050
1189
  async poll() {
1051
- if (_optionalChain([this, 'access', _47 => _47.abortController, 'optionalAccess', _48 => _48.signal, 'access', _49 => _49.aborted])) return;
1190
+ if (_optionalChain([this, 'access', _48 => _48.abortController, 'optionalAccess', _49 => _49.signal, 'access', _50 => _50.aborted])) return;
1052
1191
  try {
1053
1192
  const data = await fetchDatafile({
1054
1193
  ...this.config,
1055
- signal: _optionalChain([this, 'access', _50 => _50.abortController, 'optionalAccess', _51 => _51.signal])
1194
+ signal: _optionalChain([this, 'access', _51 => _51.abortController, 'optionalAccess', _52 => _52.signal])
1056
1195
  });
1057
1196
  this.emit("data", data);
1058
1197
  } catch (error) {
@@ -1081,7 +1220,7 @@ var PollingSource = class extends TypedEmitter {
1081
1220
  clearInterval(this.intervalId);
1082
1221
  this.intervalId = void 0;
1083
1222
  }
1084
- _optionalChain([this, 'access', _52 => _52.abortController, 'optionalAccess', _53 => _53.abort, 'call', _54 => _54()]);
1223
+ _optionalChain([this, 'access', _53 => _53.abortController, 'optionalAccess', _54 => _54.abort, 'call', _55 => _55()]);
1085
1224
  this.abortController = void 0;
1086
1225
  }
1087
1226
  };
@@ -1150,7 +1289,7 @@ async function connectStream(config, callbacks) {
1150
1289
  if (pingTimeoutId !== void 0) clearTimeout(pingTimeoutId);
1151
1290
  if (!initialDataReceived) return;
1152
1291
  pingTimeoutId = setTimeout(() => {
1153
- _optionalChain([responseBody, 'optionalAccess', _55 => _55.cancel, 'call', _56 => _56(), 'access', _57 => _57.catch, 'call', _58 => _58(() => {
1292
+ _optionalChain([responseBody, 'optionalAccess', _56 => _56.cancel, 'call', _57 => _57(), 'access', _58 => _58.catch, 'call', _59 => _59(() => {
1154
1293
  })]);
1155
1294
  connectionAbort.abort();
1156
1295
  }, PING_TIMEOUT_MS);
@@ -1166,7 +1305,7 @@ async function connectStream(config, callbacks) {
1166
1305
  if (vercelEnv) {
1167
1306
  headers["X-Vercel-Env"] = vercelEnv;
1168
1307
  }
1169
- const revision = _optionalChain([config, 'access', _59 => _59.revision, 'optionalCall', _60 => _60()]);
1308
+ const revision = _optionalChain([config, 'access', _60 => _60.revision, 'optionalCall', _61 => _61()]);
1170
1309
  if (revision !== void 0) {
1171
1310
  headers["X-Revision"] = String(revision);
1172
1311
  }
@@ -1212,7 +1351,7 @@ async function connectStream(config, callbacks) {
1212
1351
  let message;
1213
1352
  try {
1214
1353
  message = JSON.parse(line);
1215
- } catch (e5) {
1354
+ } catch (e6) {
1216
1355
  console.warn(
1217
1356
  "@vercel/flags-core: Failed to parse stream message, skipping"
1218
1357
  );
@@ -1228,7 +1367,7 @@ async function connectStream(config, callbacks) {
1228
1367
  resetPingTimeout();
1229
1368
  }
1230
1369
  if (message.type === "primed") {
1231
- _optionalChain([onPrimed, 'optionalCall', _61 => _61(message)]);
1370
+ _optionalChain([onPrimed, 'optionalCall', _62 => _62(message)]);
1232
1371
  retryCount = 0;
1233
1372
  if (!initialDataReceived) {
1234
1373
  initialDataReceived = true;
@@ -1251,7 +1390,7 @@ async function connectStream(config, callbacks) {
1251
1390
  clearTimeout(pingTimeoutId);
1252
1391
  abortController.signal.removeEventListener("abort", onMainAbort);
1253
1392
  if (!abortController.signal.aborted) {
1254
- _optionalChain([onDisconnect, 'optionalCall', _62 => _62()]);
1393
+ _optionalChain([onDisconnect, 'optionalCall', _63 => _63()]);
1255
1394
  retryCount++;
1256
1395
  const elapsed = Date.now() - lastAttemptTime;
1257
1396
  const minGap = Math.max(0, BASE_RETRY_DELAY_MS - elapsed);
@@ -1267,7 +1406,7 @@ async function connectStream(config, callbacks) {
1267
1406
  if (!connectionAbort.signal.aborted) {
1268
1407
  console.error("@vercel/flags-core: Stream error", error);
1269
1408
  }
1270
- _optionalChain([onDisconnect, 'optionalCall', _63 => _63()]);
1409
+ _optionalChain([onDisconnect, 'optionalCall', _64 => _64()]);
1271
1410
  retryCount++;
1272
1411
  const elapsed = Date.now() - lastAttemptTime;
1273
1412
  const minGap = Math.max(0, BASE_RETRY_DELAY_MS - elapsed);
@@ -1346,7 +1485,7 @@ var StreamSource = class extends TypedEmitter {
1346
1485
  * Stop the stream connection.
1347
1486
  */
1348
1487
  stop() {
1349
- _optionalChain([this, 'access', _64 => _64.abortController, 'optionalAccess', _65 => _65.abort, 'call', _66 => _66()]);
1488
+ _optionalChain([this, 'access', _65 => _65.abortController, 'optionalAccess', _66 => _66.abort, 'call', _67 => _67()]);
1350
1489
  this.abortController = void 0;
1351
1490
  this.promise = void 0;
1352
1491
  }
@@ -1384,19 +1523,24 @@ var Controller = (_class3 = class {
1384
1523
  __init5() {this.state = "idle"}
1385
1524
  // Data state — tagged with origin
1386
1525
 
1526
+ // Memoized data spread for read() / getDatafile().
1527
+ // Rebuilt only when `this.data` reference changes (e.g. on stream/poll update).
1528
+ // Holds the result of stripping `_origin`; metrics are appended per-call.
1529
+ __init6() {this.dataViewSource = void 0}
1530
+ __init7() {this.dataViewBase = void 0}
1387
1531
  // Sources (I/O delegates)
1388
1532
 
1389
1533
 
1390
1534
 
1391
1535
  // Usage tracking
1392
1536
 
1393
- __init6() {this.isFirstGetData = true}
1537
+ __init8() {this.isFirstGetData = true}
1394
1538
  // Build-step deduplication
1395
- __init7() {this.buildDataPromise = null}
1396
- __init8() {this.buildReadTracked = false}
1539
+ __init9() {this.buildDataPromise = null}
1540
+ __init10() {this.buildReadTracked = false}
1397
1541
  // Suppresses usage tracking when the SDK key is unauthorized
1398
- __init9() {this.unauthorized = false}
1399
- constructor(options) {;_class3.prototype.__init5.call(this);_class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);_class3.prototype.__init14.call(this);_class3.prototype.__init15.call(this);
1542
+ __init11() {this.unauthorized = false}
1543
+ constructor(options) {;_class3.prototype.__init5.call(this);_class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);_class3.prototype.__init14.call(this);_class3.prototype.__init15.call(this);_class3.prototype.__init16.call(this);_class3.prototype.__init17.call(this);
1400
1544
  if (!options.sdkKey || typeof options.sdkKey !== "string" || !options.sdkKey.startsWith("vf_")) {
1401
1545
  throw new Error(
1402
1546
  '@vercel/flags-core: SDK key must be a string starting with "vf_"'
@@ -1405,7 +1549,7 @@ var Controller = (_class3 = class {
1405
1549
  this.options = normalizeOptions(options);
1406
1550
  this.streamSource = new StreamSource(
1407
1551
  this.options,
1408
- () => _optionalChain([this, 'access', _67 => _67.data, 'optionalAccess', _68 => _68.revision])
1552
+ () => _optionalChain([this, 'access', _68 => _68.data, 'optionalAccess', _69 => _69.revision])
1409
1553
  );
1410
1554
  this.pollingSource = new PollingSource(this.options);
1411
1555
  this.bundledSource = new BundledSource({
@@ -1419,32 +1563,32 @@ var Controller = (_class3 = class {
1419
1563
  this.usageTracker = new UsageTracker(this.options);
1420
1564
  }
1421
1565
  // Source event handlers (stored for cleanup)
1422
- __init10() {this.onStreamData = (data) => {
1566
+ __init12() {this.onStreamData = (data) => {
1423
1567
  if (this.isNewerData(data)) {
1424
1568
  this.data = tagData(data, "stream");
1425
1569
  }
1426
1570
  }}
1427
- __init11() {this.onStreamPrimed = () => {
1571
+ __init13() {this.onStreamPrimed = () => {
1428
1572
  if (this.state === "degraded" || this.state === "initializing:stream") {
1429
1573
  this.transition("streaming");
1430
1574
  }
1431
1575
  }}
1432
- __init12() {this.onStreamConnected = () => {
1576
+ __init14() {this.onStreamConnected = () => {
1433
1577
  if (this.state === "degraded" || this.state === "initializing:stream") {
1434
1578
  this.transition("streaming");
1435
1579
  }
1436
1580
  }}
1437
- __init13() {this.onStreamDisconnected = () => {
1581
+ __init15() {this.onStreamDisconnected = () => {
1438
1582
  if (this.state === "streaming") {
1439
1583
  this.transition("degraded");
1440
1584
  }
1441
1585
  }}
1442
- __init14() {this.onPollData = (data) => {
1586
+ __init16() {this.onPollData = (data) => {
1443
1587
  if (this.isNewerData(data)) {
1444
1588
  this.data = tagData(data, "poll");
1445
1589
  }
1446
1590
  }}
1447
- __init15() {this.onPollError = (error) => {
1591
+ __init17() {this.onPollError = (error) => {
1448
1592
  console.error("@vercel/flags-core: Poll failed:", error);
1449
1593
  }}
1450
1594
  // ---------------------------------------------------------------------------
@@ -1513,7 +1657,7 @@ var Controller = (_class3 = class {
1513
1657
  if (bundled) {
1514
1658
  this.data = tagData(bundled, "bundled");
1515
1659
  }
1516
- } catch (e6) {
1660
+ } catch (e7) {
1517
1661
  }
1518
1662
  }
1519
1663
  if (this.data) {
@@ -1555,11 +1699,15 @@ var Controller = (_class3 = class {
1555
1699
  this.isFirstGetData = false;
1556
1700
  const [result, cacheStatus] = await this.resolveData();
1557
1701
  const readMs = Date.now() - startTime;
1558
- const { _origin, ...data } = result;
1559
- const source = originToMetricsSource(_origin);
1702
+ const source = originToMetricsSource(result._origin);
1560
1703
  this.trackRead(startTime, cacheHadDefinitions, isFirstRead, source);
1704
+ if (this.dataViewSource !== result) {
1705
+ const { _origin, ...rest } = result;
1706
+ this.dataViewBase = rest;
1707
+ this.dataViewSource = result;
1708
+ }
1561
1709
  return {
1562
- ...data,
1710
+ ...this.dataViewBase,
1563
1711
  metrics: {
1564
1712
  readMs,
1565
1713
  source,
@@ -1611,7 +1759,7 @@ var Controller = (_class3 = class {
1611
1759
  this.data = tagData(fetched, "fetched");
1612
1760
  result = this.data;
1613
1761
  cacheStatus = "MISS";
1614
- } catch (e7) {
1762
+ } catch (e8) {
1615
1763
  throw new Error(
1616
1764
  "@vercel/flags-core: No flag definitions available. Initialize the client or provide a datafile."
1617
1765
  );
@@ -1619,8 +1767,13 @@ var Controller = (_class3 = class {
1619
1767
  }
1620
1768
  }
1621
1769
  const source = originToMetricsSource(result._origin);
1770
+ if (this.dataViewSource !== result) {
1771
+ const { _origin, ...rest } = result;
1772
+ this.dataViewBase = rest;
1773
+ this.dataViewSource = result;
1774
+ }
1622
1775
  return {
1623
- ...result,
1776
+ ...this.dataViewBase,
1624
1777
  metrics: {
1625
1778
  readMs: Date.now() - startTime,
1626
1779
  source,
@@ -1725,7 +1878,7 @@ var Controller = (_class3 = class {
1725
1878
  return true;
1726
1879
  }
1727
1880
  return false;
1728
- } catch (e8) {
1881
+ } catch (e9) {
1729
1882
  return false;
1730
1883
  }
1731
1884
  }
@@ -1750,7 +1903,7 @@ var Controller = (_class3 = class {
1750
1903
  return true;
1751
1904
  }
1752
1905
  return false;
1753
- } catch (e9) {
1906
+ } catch (e10) {
1754
1907
  clearTimeout(timeoutId);
1755
1908
  return false;
1756
1909
  }
@@ -1800,7 +1953,7 @@ var Controller = (_class3 = class {
1800
1953
  fetch: this.options.fetch
1801
1954
  });
1802
1955
  return tagData(fetched, "fetched");
1803
- } catch (e10) {
1956
+ } catch (e11) {
1804
1957
  }
1805
1958
  throw new Error(
1806
1959
  "@vercel/flags-core: No flag definitions available during build. Provide a datafile or bundled definitions."
@@ -1834,7 +1987,7 @@ var Controller = (_class3 = class {
1834
1987
  this.data = tagData(fetched, "fetched");
1835
1988
  this.transition("degraded");
1836
1989
  return;
1837
- } catch (e11) {
1990
+ } catch (e12) {
1838
1991
  }
1839
1992
  }
1840
1993
  throw new Error(
@@ -1886,7 +2039,7 @@ var Controller = (_class3 = class {
1886
2039
  this.data = tagData(fetched, "fetched");
1887
2040
  this.transition("degraded");
1888
2041
  return [this.data, "MISS"];
1889
- } catch (e12) {
2042
+ } catch (e13) {
1890
2043
  }
1891
2044
  }
1892
2045
  throw new Error(
@@ -1938,11 +2091,11 @@ var Controller = (_class3 = class {
1938
2091
  duration: Date.now() - startTime,
1939
2092
  mode: mode === "streaming" ? "stream" : mode === "polling" ? "poll" : mode
1940
2093
  };
1941
- const configUpdatedAt = _optionalChain([this, 'access', _69 => _69.data, 'optionalAccess', _70 => _70.configUpdatedAt]);
2094
+ const configUpdatedAt = _optionalChain([this, 'access', _70 => _70.data, 'optionalAccess', _71 => _71.configUpdatedAt]);
1942
2095
  if (typeof configUpdatedAt === "number") {
1943
2096
  trackOptions.configUpdatedAt = configUpdatedAt;
1944
2097
  }
1945
- const revision = _optionalChain([this, 'access', _71 => _71.data, 'optionalAccess', _72 => _72.revision]);
2098
+ const revision = _optionalChain([this, 'access', _72 => _72.data, 'optionalAccess', _73 => _73.revision]);
1946
2099
  if (typeof revision === "number") {
1947
2100
  trackOptions.revision = revision;
1948
2101
  }
@@ -1962,7 +2115,7 @@ function parseSdkKeyFromFlagsConnectionString(text) {
1962
2115
  const params = new URLSearchParams(text.slice(6));
1963
2116
  const sdkKey = params.get("sdkKey");
1964
2117
  if (sdkKey && SDK_KEY_REGEX.test(sdkKey)) return sdkKey;
1965
- } catch (e13) {
2118
+ } catch (e14) {
1966
2119
  }
1967
2120
  return null;
1968
2121
  }
@@ -2045,4 +2198,4 @@ var {
2045
2198
 
2046
2199
 
2047
2200
  exports.ResolutionReason = ResolutionReason; exports.evaluate = evaluate; exports.FallbackNotFoundError = FallbackNotFoundError; exports.FallbackEntryNotFoundError = FallbackEntryNotFoundError; exports.Controller = Controller; exports.flagsClient = flagsClient; exports.resetDefaultFlagsClient = resetDefaultFlagsClient; exports.createClient = createClient;
2048
- //# sourceMappingURL=chunk-WKSYRFUY.cjs.map
2201
+ //# sourceMappingURL=chunk-5TVRNXPQ.cjs.map