@vercel/flags-core 1.1.0-cea788b-20260305194413 → 1.1.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +8 -2
  2. package/README.md +2 -0
  3. package/dist/{chunk-EB6T2UJ6.js → chunk-J33BA25D.js} +958 -527
  4. package/dist/chunk-J33BA25D.js.map +1 -0
  5. package/dist/{chunk-326YN2YT.cjs → chunk-LBP3OTYA.cjs} +965 -535
  6. package/dist/chunk-LBP3OTYA.cjs.map +1 -0
  7. package/dist/{chunk-IBIJOSJL.js → chunk-UYFYFLI7.js} +953 -523
  8. package/dist/chunk-UYFYFLI7.js.map +1 -0
  9. package/dist/{chunk-WN6UYCJZ.cjs → chunk-ZLGLABMG.cjs} +969 -538
  10. package/dist/chunk-ZLGLABMG.cjs.map +1 -0
  11. package/dist/index.default.cjs +3 -2
  12. package/dist/index.default.cjs.map +1 -1
  13. package/dist/index.default.d.cts +78 -83
  14. package/dist/index.default.d.ts +78 -83
  15. package/dist/index.default.js +4 -3
  16. package/dist/index.next-js.cjs +3 -2
  17. package/dist/index.next-js.cjs.map +1 -1
  18. package/dist/index.next-js.d.cts +78 -83
  19. package/dist/index.next-js.d.ts +78 -83
  20. package/dist/index.next-js.js +4 -3
  21. package/dist/openfeature.default.cjs +2 -2
  22. package/dist/openfeature.default.d.cts +1 -1
  23. package/dist/openfeature.default.d.ts +1 -1
  24. package/dist/openfeature.default.js +1 -1
  25. package/dist/openfeature.next-js.cjs +2 -2
  26. package/dist/openfeature.next-js.d.cts +1 -1
  27. package/dist/openfeature.next-js.d.ts +1 -1
  28. package/dist/openfeature.next-js.js +1 -1
  29. package/dist/{types-CpRz8o4d.d.cts → types-DtnecWgl.d.cts} +8 -4
  30. package/dist/{types-CpRz8o4d.d.ts → types-DtnecWgl.d.ts} +8 -4
  31. package/package.json +3 -4
  32. package/dist/chunk-326YN2YT.cjs.map +0 -1
  33. package/dist/chunk-EB6T2UJ6.js.map +0 -1
  34. package/dist/chunk-IBIJOSJL.js.map +0 -1
  35. package/dist/chunk-WN6UYCJZ.cjs.map +0 -1
@@ -1,9 +1,6 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;// src/index.next-js.ts
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3;// src/index.next-js.ts
2
2
  var _cache = require('next/cache');
3
3
 
4
- // src/client-map.ts
5
- var clientMap = /* @__PURE__ */ new Map();
6
-
7
4
  // src/evaluate.ts
8
5
  var _jsxxhash = require('js-xxhash');
9
6
 
@@ -33,12 +30,11 @@ var Packed;
33
30
  })(AccessorType = Packed2.AccessorType || (Packed2.AccessorType = {}));
34
31
  })(Packed || (Packed = {}));
35
32
 
36
- // src/utils.ts
33
+ // src/evaluate.ts
34
+ var MAX_REGEX_INPUT_LENGTH = 1e4;
37
35
  function exhaustivenessCheck(_) {
38
36
  throw new Error("Exhaustiveness check failed");
39
37
  }
40
-
41
- // src/evaluate.ts
42
38
  function getProperty(obj, pathArray) {
43
39
  return pathArray.reduce((acc, key) => {
44
40
  if (acc && key in acc) {
@@ -83,10 +79,12 @@ var IGNORE_CASE_COMPARATORS = /* @__PURE__ */ new Set([
83
79
  "!contains" /* NOT_CONTAINS */
84
80
  ]);
85
81
  function matchTargetList(targets, params) {
86
- for (const [kind, attributes] of Object.entries(targets)) {
87
- for (const [attribute, values] of Object.entries(attributes)) {
82
+ for (const kind in targets) {
83
+ const attributes = targets[kind];
84
+ for (const attribute in attributes) {
88
85
  const entity = access([kind, attribute], params);
89
- if (isString(entity) && values.includes(entity)) return true;
86
+ if (isString(entity) && attributes[attribute].includes(entity))
87
+ return true;
90
88
  }
91
89
  }
92
90
  return false;
@@ -219,12 +217,12 @@ function matchConditions(conditions, params) {
219
217
  if (lhs === null || lhs === void 0) return false;
220
218
  return (isNumber(rhs) || isString(rhs)) && lhs <= rhs;
221
219
  case "regex" /* REGEX */:
222
- if (isString(lhs) && typeof rhs === "object" && !Array.isArray(rhs) && _optionalChain([rhs, 'optionalAccess', _12 => _12.type]) === "regex") {
220
+ if (isString(lhs) && lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === "object" && !Array.isArray(rhs) && _optionalChain([rhs, 'optionalAccess', _12 => _12.type]) === "regex") {
223
221
  return new RegExp(rhs.pattern, rhs.flags).test(lhs);
224
222
  }
225
223
  return false;
226
224
  case "!regex" /* NOT_REGEX */:
227
- if (isString(lhs) && typeof rhs === "object" && !Array.isArray(rhs) && _optionalChain([rhs, 'optionalAccess', _13 => _13.type]) === "regex") {
225
+ if (isString(lhs) && lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === "object" && !Array.isArray(rhs) && _optionalChain([rhs, 'optionalAccess', _13 => _13.type]) === "regex") {
228
226
  return !new RegExp(rhs.pattern, rhs.flags).test(lhs);
229
227
  }
230
228
  return false;
@@ -315,7 +313,7 @@ function handleOutcome(params, outcome) {
315
313
  }
316
314
  }
317
315
  }
318
- function evaluate(params) {
316
+ function evaluate(params, _visited) {
319
317
  const envConfig = params.definition.environments[params.environment];
320
318
  if (typeof envConfig === "number") {
321
319
  return Object.assign(handleOutcome(params, envConfig), {
@@ -336,7 +334,16 @@ function evaluate(params) {
336
334
  `Could not find envConfig for "${envConfig.reuse}" when reusing`
337
335
  );
338
336
  }
339
- return evaluate({ ...params, environment: envConfig.reuse });
337
+ const visited = _nullishCoalesce(_visited, () => ( /* @__PURE__ */ new Set()));
338
+ if (visited.has(envConfig.reuse)) {
339
+ return {
340
+ reason: "error" /* ERROR */,
341
+ errorMessage: `Circular environment reuse detected: "${envConfig.reuse}"`,
342
+ value: params.defaultValue
343
+ };
344
+ }
345
+ visited.add(params.environment);
346
+ return evaluate({ ...params, environment: envConfig.reuse }, visited);
340
347
  }
341
348
  if (envConfig.targets) {
342
349
  const matchedIndex = envConfig.targets.findIndex(
@@ -369,7 +376,7 @@ function findWeightedIndex(weights, value, maxValue) {
369
376
  }
370
377
 
371
378
  // package.json
372
- var version = "1.1.0-cea788b-20260305194413";
379
+ var version = "1.1.0";
373
380
 
374
381
  // src/lib/report-value.ts
375
382
  function internalReportValue(key, value, data) {
@@ -381,37 +388,67 @@ function internalReportValue(key, value, data) {
381
388
  })]);
382
389
  }
383
390
 
384
- // src/client-fns.ts
391
+ // src/controller-fns.ts
392
+ var controllerInstanceMap = /* @__PURE__ */ new Map();
393
+ function getInstance(id) {
394
+ const instance = controllerInstanceMap.get(id);
395
+ if (!instance) {
396
+ throw new Error(
397
+ `@vercel/flags-core: Client instance ${id} not found. It may have been shut down.`
398
+ );
399
+ }
400
+ return instance;
401
+ }
385
402
  function initialize(id) {
386
- return clientMap.get(id).dataSource.initialize();
403
+ return getInstance(id).controller.initialize();
387
404
  }
388
405
  function shutdown(id) {
389
- return clientMap.get(id).dataSource.shutdown();
406
+ return getInstance(id).controller.shutdown();
390
407
  }
391
408
  function getDatafile(id) {
392
- return clientMap.get(id).dataSource.getDatafile();
409
+ return getInstance(id).controller.getDatafile();
393
410
  }
394
411
  function getFallbackDatafile(id) {
395
- const ds = clientMap.get(id).dataSource;
412
+ const ds = getInstance(id).controller;
396
413
  if (ds.getFallbackDatafile) return ds.getFallbackDatafile();
397
414
  throw new Error("flags: This data source does not support fallbacks");
398
415
  }
399
416
  async function evaluate2(id, flagKey, defaultValue, entities) {
400
- const ds = clientMap.get(id).dataSource;
401
- const datafile = await ds.read();
417
+ const controller = getInstance(id).controller;
418
+ let datafile;
419
+ try {
420
+ datafile = await controller.read();
421
+ } catch (error) {
422
+ if (defaultValue !== void 0) {
423
+ return {
424
+ value: defaultValue,
425
+ reason: "error" /* ERROR */,
426
+ errorMessage: error instanceof Error ? error.message : "Failed to read datafile"
427
+ };
428
+ }
429
+ throw error;
430
+ }
402
431
  const flagDefinition = datafile.definitions[flagKey];
403
432
  if (flagDefinition === void 0) {
433
+ if (datafile.projectId) {
434
+ internalReportValue(flagKey, defaultValue, {
435
+ originProjectId: datafile.projectId,
436
+ originProvider: "vercel",
437
+ reason: "error" /* ERROR */
438
+ });
439
+ }
404
440
  return {
405
441
  value: defaultValue,
406
442
  reason: "error" /* ERROR */,
407
443
  errorCode: "FLAG_NOT_FOUND" /* FLAG_NOT_FOUND */,
408
- errorMessage: `Definition not found for flag "${flagKey}"`,
444
+ errorMessage: `@vercel/flags-core: Definition not found for flag "${flagKey}"`,
409
445
  metrics: {
410
446
  evaluationMs: 0,
411
447
  readMs: datafile.metrics.readMs,
412
448
  source: datafile.metrics.source,
413
449
  cacheStatus: datafile.metrics.cacheStatus,
414
- connectionState: datafile.metrics.connectionState
450
+ connectionState: datafile.metrics.connectionState,
451
+ mode: datafile.metrics.mode
415
452
  }
416
453
  };
417
454
  }
@@ -438,7 +475,8 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
438
475
  readMs: datafile.metrics.readMs,
439
476
  source: datafile.metrics.source,
440
477
  cacheStatus: datafile.metrics.cacheStatus,
441
- connectionState: datafile.metrics.connectionState
478
+ connectionState: datafile.metrics.connectionState,
479
+ mode: datafile.metrics.mode
442
480
  }
443
481
  });
444
482
  }
@@ -456,18 +494,22 @@ async function performInitialize(instance, initFn) {
456
494
  }
457
495
  function createCreateRawClient(fns) {
458
496
  return function createRawClient({
459
- dataSource,
497
+ controller,
460
498
  origin
461
499
  }) {
462
500
  const id = idCount++;
463
- clientMap.set(id, { dataSource, initialized: false, initPromise: null });
501
+ controllerInstanceMap.set(id, {
502
+ controller,
503
+ initialized: false,
504
+ initPromise: null
505
+ });
464
506
  const api = {
465
507
  origin,
466
508
  initialize: async () => {
467
- let instance = clientMap.get(id);
509
+ let instance = controllerInstanceMap.get(id);
468
510
  if (!instance) {
469
- instance = { dataSource, initialized: false, initPromise: null };
470
- clientMap.set(id, instance);
511
+ instance = { controller, initialized: false, initPromise: null };
512
+ controllerInstanceMap.set(id, instance);
471
513
  }
472
514
  if (instance.initialized) return;
473
515
  if (!instance.initPromise) {
@@ -480,15 +522,29 @@ function createCreateRawClient(fns) {
480
522
  },
481
523
  shutdown: async () => {
482
524
  await fns.shutdown(id);
483
- clientMap.delete(id);
525
+ controllerInstanceMap.delete(id);
526
+ },
527
+ getDatafile: async () => {
528
+ const instance = controllerInstanceMap.get(id);
529
+ if (_optionalChain([instance, 'optionalAccess', _21 => _21.initPromise])) {
530
+ try {
531
+ await instance.initPromise;
532
+ } catch (e) {
533
+ }
534
+ }
535
+ return fns.getDatafile(id);
484
536
  },
485
- getDatafile: () => fns.getDatafile(id),
486
537
  getFallbackDatafile: () => {
487
538
  return fns.getFallbackDatafile(id);
488
539
  },
489
540
  evaluate: async (flagKey, defaultValue, entities) => {
490
- const instance = clientMap.get(id);
491
- if (!_optionalChain([instance, 'optionalAccess', _21 => _21.initialized])) await api.initialize();
541
+ const instance = controllerInstanceMap.get(id);
542
+ if (!_optionalChain([instance, 'optionalAccess', _22 => _22.initialized])) {
543
+ try {
544
+ await api.initialize();
545
+ } catch (e2) {
546
+ }
547
+ }
492
548
  return fns.evaluate(id, flagKey, defaultValue, entities);
493
549
  }
494
550
  };
@@ -496,20 +552,6 @@ function createCreateRawClient(fns) {
496
552
  };
497
553
  }
498
554
 
499
- // src/errors.ts
500
- var FallbackNotFoundError = class extends Error {
501
- constructor() {
502
- super("@vercel/flags-core: Bundled definitions file not found.");
503
- this.name = "FallbackNotFoundError";
504
- }
505
- };
506
- var FallbackEntryNotFoundError = class extends Error {
507
- constructor() {
508
- super("@vercel/flags-core: No bundled definitions found for SDK key.");
509
- this.name = "FallbackEntryNotFoundError";
510
- }
511
- };
512
-
513
555
  // src/utils/read-bundled-definitions.ts
514
556
  var sdkKeyHashCache = /* @__PURE__ */ new Map();
515
557
  async function computeHash(sdkKey) {
@@ -554,27 +596,22 @@ async function readBundledDefinitions(sdkKey) {
554
596
  return { definitions: null, state: "missing-entry" };
555
597
  }
556
598
 
557
- // src/utils/sleep.ts
558
- function sleep(ms) {
559
- return new Promise((resolve) => setTimeout(resolve, ms));
560
- }
561
-
562
599
  // src/utils/usage-tracker.ts
563
600
  var _functions = require('@vercel/functions');
564
601
  var RESOLVED_VOID = Promise.resolve();
565
- var isDebugMode = _optionalChain([process, 'access', _22 => _22.env, 'access', _23 => _23.DEBUG, 'optionalAccess', _24 => _24.includes, 'call', _25 => _25("@vercel/flags-core")]);
602
+ var isDebugMode = _optionalChain([process, 'access', _23 => _23.env, 'access', _24 => _24.DEBUG, 'optionalAccess', _25 => _25.includes, 'call', _26 => _26("@vercel/flags-core")]);
566
603
  var debugLog = (...args) => {
567
604
  if (!isDebugMode) return;
568
605
  console.log(...args);
569
606
  };
607
+ var MAX_RETRIES = 3;
570
608
  var MAX_BATCH_SIZE = 50;
571
609
  var MAX_BATCH_WAIT_MS = 5e3;
572
- var trackedRequests = /* @__PURE__ */ new WeakSet();
573
610
  var SYMBOL_FOR_REQ_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context");
574
611
  var fromSymbol = globalThis;
575
612
  function getRequestContext() {
576
613
  try {
577
- const ctx = _optionalChain([fromSymbol, 'access', _26 => _26[SYMBOL_FOR_REQ_CONTEXT], 'optionalAccess', _27 => _27.get, 'optionalCall', _28 => _28()]);
614
+ const ctx = _optionalChain([fromSymbol, 'access', _27 => _27[SYMBOL_FOR_REQ_CONTEXT], 'optionalAccess', _28 => _28.get, 'optionalCall', _29 => _29()]);
578
615
  if (ctx && Object.hasOwn(ctx, "headers")) {
579
616
  return {
580
617
  ctx,
@@ -582,29 +619,35 @@ function getRequestContext() {
582
619
  };
583
620
  }
584
621
  return { ctx, headers: void 0 };
585
- } catch (e) {
622
+ } catch (e3) {
586
623
  return { ctx: void 0, headers: void 0 };
587
624
  }
588
625
  }
589
626
  var UsageTracker = (_class = class {
627
+ __init() {this.flushCounter = 0}
590
628
 
591
-
592
- __init() {this.batcher = {
629
+ __init2() {this.trackedRequests = /* @__PURE__ */ new WeakSet()}
630
+ __init3() {this.batcher = {
593
631
  events: [],
594
632
  resolveWait: null,
595
633
  pending: null
596
634
  }}
597
- constructor(options) {;_class.prototype.__init.call(this);
598
- this.sdkKey = options.sdkKey;
599
- this.host = options.host;
635
+ constructor(options) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);
636
+ this.options = options;
600
637
  }
601
638
  /**
602
639
  * Triggers an immediate flush of any pending events.
603
640
  * Returns a promise that resolves when the flush completes.
604
641
  */
605
642
  flush() {
606
- _optionalChain([this, 'access', _29 => _29.batcher, 'access', _30 => _30.resolveWait, 'optionalCall', _31 => _31()]);
607
- return _nullishCoalesce(this.batcher.pending, () => ( RESOLVED_VOID));
643
+ if (this.batcher.pending) {
644
+ _optionalChain([this, 'access', _30 => _30.batcher, 'access', _31 => _31.resolveWait, 'optionalCall', _32 => _32()]);
645
+ return this.batcher.pending;
646
+ }
647
+ if (this.batcher.events.length > 0) {
648
+ return this.flushEvents();
649
+ }
650
+ return RESOLVED_VOID;
608
651
  }
609
652
  /**
610
653
  * Tracks a config read event. Deduplicates by request context.
@@ -613,8 +656,8 @@ var UsageTracker = (_class = class {
613
656
  try {
614
657
  const { ctx, headers } = getRequestContext();
615
658
  if (ctx) {
616
- if (trackedRequests.has(ctx)) return;
617
- trackedRequests.add(ctx);
659
+ if (this.trackedRequests.has(ctx)) return;
660
+ this.trackedRequests.add(ctx);
618
661
  }
619
662
  const event = {
620
663
  type: "FLAGS_CONFIG_READ",
@@ -633,6 +676,9 @@ var UsageTracker = (_class = class {
633
676
  if (options.cacheStatus !== void 0) {
634
677
  event.payload.cacheStatus = options.cacheStatus;
635
678
  }
679
+ if (options.cacheAction !== void 0) {
680
+ event.payload.cacheAction = options.cacheAction;
681
+ }
636
682
  if (options.cacheIsFirstRead !== void 0) {
637
683
  event.payload.cacheIsFirstRead = options.cacheIsFirstRead;
638
684
  }
@@ -645,6 +691,16 @@ var UsageTracker = (_class = class {
645
691
  if (options.configUpdatedAt !== void 0) {
646
692
  event.payload.configUpdatedAt = options.configUpdatedAt;
647
693
  }
694
+ if (options.mode !== void 0) {
695
+ event.payload.mode = options.mode;
696
+ }
697
+ if (options.revision !== void 0) {
698
+ event.payload.revision = String(options.revision);
699
+ }
700
+ }
701
+ const environment = process.env.VERCEL_ENV || process.env.NODE_ENV || void 0;
702
+ if (environment) {
703
+ event.payload.environment = environment;
648
704
  }
649
705
  this.batcher.events.push(event);
650
706
  this.scheduleFlush();
@@ -665,52 +721,315 @@ var UsageTracker = (_class = class {
665
721
  if (timeout) clearTimeout(timeout);
666
722
  await this.flushEvents();
667
723
  })();
668
- _functions.waitUntil.call(void 0, pending);
724
+ try {
725
+ _functions.waitUntil.call(void 0, pending);
726
+ } catch (e4) {
727
+ }
669
728
  this.batcher.pending = pending;
670
729
  }
671
730
  if (this.batcher.events.length >= MAX_BATCH_SIZE) {
672
- _optionalChain([this, 'access', _32 => _32.batcher, 'access', _33 => _33.resolveWait, 'optionalCall', _34 => _34()]);
731
+ _optionalChain([this, 'access', _33 => _33.batcher, 'access', _34 => _34.resolveWait, 'optionalCall', _35 => _35()]);
673
732
  }
674
733
  }
675
734
  async flushEvents() {
676
735
  if (this.batcher.events.length === 0) return;
677
736
  const eventsToSend = this.batcher.events;
678
737
  this.batcher.events = [];
679
- try {
680
- const response = await fetch(`${this.host}/v1/ingest`, {
681
- method: "POST",
682
- headers: {
683
- "Content-Type": "application/json",
684
- Authorization: `Bearer ${this.sdkKey}`,
685
- "User-Agent": `VercelFlagsCore/${version}`,
686
- ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
687
- },
688
- body: JSON.stringify(eventsToSend)
689
- });
690
- debugLog(
691
- `@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
692
- );
693
- if (!response.ok) {
738
+ const flushId = ++this.flushCounter;
739
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
740
+ try {
741
+ const response = await this.options.fetch(
742
+ `${this.options.host}/v1/ingest`,
743
+ {
744
+ method: "POST",
745
+ headers: {
746
+ "Content-Type": "application/json",
747
+ Authorization: `Bearer ${this.options.sdkKey}`,
748
+ "User-Agent": `VercelFlagsCore/${version}`,
749
+ ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
750
+ },
751
+ body: JSON.stringify(eventsToSend)
752
+ }
753
+ );
694
754
  debugLog(
695
- "@vercel/flags-core: Failed to send events:",
696
- response.statusText
755
+ `@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
697
756
  );
757
+ if (response.ok) {
758
+ break;
759
+ }
760
+ throw new Error(
761
+ `Ingest endpoint responded with status ${response.status} for ${eventsToSend.length} events on request ${response.headers.get("x-vercel-id")}.
762
+ Response body: ${await response.text().catch(() => null)}`
763
+ );
764
+ } catch (error) {
765
+ console.error(
766
+ `@vercel/flags-core: Error sending events (attempt=${attempt}/${MAX_RETRIES} flushId=${flushId}):`,
767
+ error
768
+ );
769
+ if (attempt < MAX_RETRIES) {
770
+ await new Promise((res) => setTimeout(res, attempt * 100));
771
+ }
698
772
  }
699
- } catch (error) {
700
- debugLog("@vercel/flags-core: Error sending events:", error);
701
773
  }
702
774
  }
703
775
  }, _class);
704
776
 
705
- // src/data-source/stream-connection.ts
777
+ // src/errors.ts
778
+ var FallbackNotFoundError = class extends Error {
779
+ constructor() {
780
+ super("@vercel/flags-core: Bundled definitions file not found.");
781
+ this.name = "FallbackNotFoundError";
782
+ }
783
+ };
784
+ var FallbackEntryNotFoundError = class extends Error {
785
+ constructor() {
786
+ super("@vercel/flags-core: No bundled definitions found for SDK key.");
787
+ this.name = "FallbackEntryNotFoundError";
788
+ }
789
+ };
790
+
791
+ // src/controller/bundled-source.ts
792
+ var BundledSource = class {
793
+
794
+
795
+ constructor(options) {
796
+ this.options = options;
797
+ }
798
+ /**
799
+ * Load bundled definitions.
800
+ * Throws if bundled definitions are not available.
801
+ */
802
+ async load() {
803
+ const result = await this.getResult();
804
+ if (result.state === "ok" && result.definitions) {
805
+ return result.definitions;
806
+ }
807
+ throw new Error(
808
+ "@vercel/flags-core: No flag definitions available. Bundled definitions not found."
809
+ );
810
+ }
811
+ /**
812
+ * Get the raw BundledDefinitions (for getFallbackDatafile).
813
+ * Throws typed errors if not available.
814
+ */
815
+ async getRaw() {
816
+ const result = await this.getResult();
817
+ switch (result.state) {
818
+ case "ok":
819
+ return result.definitions;
820
+ case "missing-file":
821
+ throw new FallbackNotFoundError();
822
+ case "missing-entry":
823
+ throw new FallbackEntryNotFoundError();
824
+ case "unexpected-error":
825
+ throw new Error(
826
+ "@vercel/flags-core: Failed to read bundled definitions: " + String(result.error)
827
+ );
828
+ }
829
+ }
830
+ /**
831
+ * Check if bundled definitions loaded successfully (without throwing).
832
+ */
833
+ async tryLoad() {
834
+ const result = await this.getResult();
835
+ if (_optionalChain([result, 'optionalAccess', _36 => _36.state]) === "ok" && result.definitions) {
836
+ return result.definitions;
837
+ }
838
+ return void 0;
839
+ }
840
+ getResult() {
841
+ if (!this.promise) {
842
+ this.promise = this.options.readBundledDefinitions(this.options.sdkKey);
843
+ }
844
+ return this.promise;
845
+ }
846
+ };
847
+
848
+ // src/controller/fetch-datafile.ts
849
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
850
+ async function fetchDatafile(options) {
851
+ const controller = new AbortController();
852
+ const timeoutId = setTimeout(
853
+ () => controller.abort(),
854
+ DEFAULT_FETCH_TIMEOUT_MS
855
+ );
856
+ const onExternalAbort = () => controller.abort();
857
+ if (options.signal) {
858
+ if (options.signal.aborted) {
859
+ clearTimeout(timeoutId);
860
+ throw new Error("Fetch aborted");
861
+ }
862
+ options.signal.addEventListener("abort", onExternalAbort, { once: true });
863
+ }
864
+ try {
865
+ const res = await options.fetch(`${options.host}/v1/datafile`, {
866
+ headers: {
867
+ Authorization: `Bearer ${options.sdkKey}`,
868
+ "User-Agent": `VercelFlagsCore/${version}`
869
+ },
870
+ signal: controller.signal
871
+ });
872
+ clearTimeout(timeoutId);
873
+ _optionalChain([options, 'access', _37 => _37.signal, 'optionalAccess', _38 => _38.removeEventListener, 'call', _39 => _39("abort", onExternalAbort)]);
874
+ if (!res.ok) {
875
+ throw new Error(`Failed to fetch data: ${res.statusText}`);
876
+ }
877
+ return res.json();
878
+ } catch (error) {
879
+ clearTimeout(timeoutId);
880
+ _optionalChain([options, 'access', _40 => _40.signal, 'optionalAccess', _41 => _41.removeEventListener, 'call', _42 => _42("abort", onExternalAbort)]);
881
+ throw error instanceof Error ? error : new Error("Unknown fetch error");
882
+ }
883
+ }
884
+
885
+ // src/controller/normalized-options.ts
886
+ var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
887
+ var DEFAULT_POLLING_INTERVAL_MS = 3e4;
888
+ var MIN_POLLING_INTERVAL_MS = 3e4;
889
+ var DEFAULT_POLLING_INIT_TIMEOUT_MS = 3e3;
890
+ function normalizeOptions(options) {
891
+ const autoDetectedBuildStep = process.env.CI === "1" || process.env.NEXT_PHASE === "phase-production-build";
892
+ const buildStep = _nullishCoalesce(options.buildStep, () => ( autoDetectedBuildStep));
893
+ let stream;
894
+ if (options.stream === void 0 || options.stream === true) {
895
+ stream = { enabled: true, initTimeoutMs: DEFAULT_STREAM_INIT_TIMEOUT_MS };
896
+ } else if (options.stream === false) {
897
+ stream = { enabled: false, initTimeoutMs: 0 };
898
+ } else {
899
+ stream = { enabled: true, initTimeoutMs: options.stream.initTimeoutMs };
900
+ }
901
+ let polling;
902
+ if (options.polling === void 0 || options.polling === true) {
903
+ polling = {
904
+ enabled: true,
905
+ intervalMs: DEFAULT_POLLING_INTERVAL_MS,
906
+ initTimeoutMs: DEFAULT_POLLING_INIT_TIMEOUT_MS
907
+ };
908
+ } else if (options.polling === false) {
909
+ polling = { enabled: false, intervalMs: 0, initTimeoutMs: 0 };
910
+ } else {
911
+ if (options.polling.intervalMs < MIN_POLLING_INTERVAL_MS) {
912
+ throw new Error(
913
+ `@vercel/flags-core: Polling interval must be at least ${MIN_POLLING_INTERVAL_MS}ms, got ${options.polling.intervalMs}ms.`
914
+ );
915
+ }
916
+ polling = {
917
+ enabled: true,
918
+ intervalMs: options.polling.intervalMs,
919
+ initTimeoutMs: options.polling.initTimeoutMs
920
+ };
921
+ }
922
+ return {
923
+ sdkKey: options.sdkKey,
924
+ datafile: options.datafile,
925
+ stream,
926
+ polling,
927
+ buildStep,
928
+ fetch: _nullishCoalesce(options.fetch, () => ( globalThis.fetch)),
929
+ host: "https://flags.vercel.com"
930
+ };
931
+ }
932
+
933
+ // src/controller/typed-emitter.ts
934
+ var TypedEmitter = (_class2 = class {constructor() { _class2.prototype.__init4.call(this); }
935
+ __init4() {this.handlers = /* @__PURE__ */ new Map()}
936
+ on(event, handler) {
937
+ let set = this.handlers.get(event);
938
+ if (!set) {
939
+ set = /* @__PURE__ */ new Set();
940
+ this.handlers.set(event, set);
941
+ }
942
+ set.add(handler);
943
+ }
944
+ off(event, handler) {
945
+ _optionalChain([this, 'access', _43 => _43.handlers, 'access', _44 => _44.get, 'call', _45 => _45(event), 'optionalAccess', _46 => _46.delete, 'call', _47 => _47(handler)]);
946
+ }
947
+ emit(event, ...args) {
948
+ const set = this.handlers.get(event);
949
+ if (set) {
950
+ for (const handler of set) {
951
+ handler(...args);
952
+ }
953
+ }
954
+ }
955
+ }, _class2);
956
+
957
+ // src/controller/polling-source.ts
958
+ var PollingSource = class extends TypedEmitter {
959
+
960
+
961
+
962
+ constructor(config) {
963
+ super();
964
+ this.config = config;
965
+ }
966
+ /**
967
+ * Perform a single poll request.
968
+ * Emits 'data' on success, 'error' on failure.
969
+ */
970
+ async poll() {
971
+ if (_optionalChain([this, 'access', _48 => _48.abortController, 'optionalAccess', _49 => _49.signal, 'access', _50 => _50.aborted])) return;
972
+ try {
973
+ const data = await fetchDatafile({
974
+ ...this.config,
975
+ signal: _optionalChain([this, 'access', _51 => _51.abortController, 'optionalAccess', _52 => _52.signal])
976
+ });
977
+ this.emit("data", data);
978
+ } catch (error) {
979
+ const err = error instanceof Error ? error : new Error("Unknown poll error");
980
+ this.emit("error", err);
981
+ }
982
+ }
983
+ /**
984
+ * Start interval-based polling.
985
+ * Polls at the configured interval. Does not perform an initial poll —
986
+ * callers should call poll() first if an immediate poll is needed.
987
+ */
988
+ startInterval() {
989
+ if (this.intervalId) return;
990
+ this.abortController = new AbortController();
991
+ this.intervalId = setInterval(
992
+ () => void this.poll(),
993
+ this.config.polling.intervalMs
994
+ );
995
+ }
996
+ /**
997
+ * Stop interval-based polling.
998
+ */
999
+ stop() {
1000
+ if (this.intervalId) {
1001
+ clearInterval(this.intervalId);
1002
+ this.intervalId = void 0;
1003
+ }
1004
+ _optionalChain([this, 'access', _53 => _53.abortController, 'optionalAccess', _54 => _54.abort, 'call', _55 => _55()]);
1005
+ this.abortController = void 0;
1006
+ }
1007
+ };
1008
+
1009
+ // src/utils/sleep.ts
1010
+ function sleep(ms) {
1011
+ return new Promise((resolve) => setTimeout(resolve, ms));
1012
+ }
1013
+
1014
+ // src/controller/stream-connection.ts
706
1015
  var MAX_RETRY_COUNT = 15;
707
- var BASE_DELAY_MS = 1e3;
708
- var MAX_DELAY_MS = 6e4;
1016
+ var BASE_RETRY_DELAY_MS = 1e3;
1017
+ var MAX_RETRY_DELAY_MS = 6e4;
1018
+ var PING_TIMEOUT_MS = 9e4;
709
1019
  function backoff(retryCount) {
710
1020
  if (retryCount === 1) return 0;
711
- const delay = Math.min(BASE_DELAY_MS * 2 ** (retryCount - 2), MAX_DELAY_MS);
1021
+ const delay = Math.min(
1022
+ BASE_RETRY_DELAY_MS * 2 ** (retryCount - 2),
1023
+ MAX_RETRY_DELAY_MS
1024
+ );
712
1025
  return delay + Math.random() * 1e3;
713
1026
  }
1027
+ var UnauthorizedError = class extends Error {
1028
+ constructor() {
1029
+ super("stream: unauthorized (401)");
1030
+ this.name = "UnauthorizedError";
1031
+ }
1032
+ };
714
1033
  async function connectStream(config, callbacks) {
715
1034
  const {
716
1035
  host,
@@ -718,8 +1037,9 @@ async function connectStream(config, callbacks) {
718
1037
  abortController,
719
1038
  fetch: fetchFn = globalThis.fetch
720
1039
  } = config;
721
- const { onMessage, onDisconnect } = callbacks;
1040
+ const { onDatafile, onPrimed, onDisconnect } = callbacks;
722
1041
  let retryCount = 0;
1042
+ let lastAttemptTime = 0;
723
1043
  let resolveInit;
724
1044
  let rejectInit;
725
1045
  const initPromise = new Promise((resolve, reject) => {
@@ -731,200 +1051,356 @@ async function connectStream(config, callbacks) {
731
1051
  while (!abortController.signal.aborted) {
732
1052
  if (retryCount > MAX_RETRY_COUNT) {
733
1053
  console.error("@vercel/flags-core: Max retry count exceeded");
1054
+ if (!initialDataReceived) {
1055
+ rejectInit(
1056
+ new Error("stream: max retry count exceeded before receiving data")
1057
+ );
1058
+ }
734
1059
  abortController.abort();
735
1060
  break;
736
1061
  }
1062
+ const connectionAbort = new AbortController();
1063
+ const onMainAbort = () => connectionAbort.abort();
1064
+ abortController.signal.addEventListener("abort", onMainAbort, {
1065
+ once: true
1066
+ });
1067
+ let pingTimeoutId;
1068
+ let responseBody;
1069
+ const resetPingTimeout = () => {
1070
+ if (pingTimeoutId !== void 0) clearTimeout(pingTimeoutId);
1071
+ if (!initialDataReceived) return;
1072
+ pingTimeoutId = setTimeout(() => {
1073
+ _optionalChain([responseBody, 'optionalAccess', _56 => _56.cancel, 'call', _57 => _57(), 'access', _58 => _58.catch, 'call', _59 => _59(() => {
1074
+ })]);
1075
+ connectionAbort.abort();
1076
+ }, PING_TIMEOUT_MS);
1077
+ };
737
1078
  try {
1079
+ lastAttemptTime = Date.now();
1080
+ const headers = {
1081
+ Authorization: `Bearer ${sdkKey}`,
1082
+ "User-Agent": `VercelFlagsCore/${version}`,
1083
+ "X-Retry-Attempt": String(retryCount)
1084
+ };
1085
+ const revision = _optionalChain([config, 'access', _60 => _60.revision, 'optionalCall', _61 => _61()]);
1086
+ if (revision !== void 0) {
1087
+ headers["X-Revision"] = String(revision);
1088
+ }
738
1089
  const response = await fetchFn(`${host}/v1/stream`, {
739
- headers: {
740
- Authorization: `Bearer ${sdkKey}`,
741
- "User-Agent": `VercelFlagsCore/${version}`,
742
- "X-Retry-Attempt": String(retryCount)
743
- },
744
- signal: abortController.signal
1090
+ headers,
1091
+ signal: connectionAbort.signal
745
1092
  });
746
1093
  if (!response.ok) {
747
1094
  if (response.status === 401) {
1095
+ if (!initialDataReceived) {
1096
+ rejectInit(new UnauthorizedError());
1097
+ }
748
1098
  abortController.abort();
1099
+ break;
749
1100
  }
750
1101
  throw new Error(`stream was not ok: ${response.status}`);
751
1102
  }
752
1103
  if (!response.body) {
753
1104
  throw new Error("stream body was not present");
754
1105
  }
1106
+ responseBody = response.body;
1107
+ const reader = response.body.getReader();
755
1108
  const decoder = new TextDecoder();
756
- let buffer = "";
757
- for await (const chunk of response.body) {
758
- if (abortController.signal.aborted) break;
759
- buffer += decoder.decode(chunk, { stream: true });
760
- const lines = buffer.split("\n");
761
- buffer = lines.pop();
762
- for (const line of lines) {
763
- if (line === "") continue;
764
- let message;
765
- try {
766
- message = JSON.parse(line);
767
- } catch (e2) {
768
- console.warn(
769
- "@vercel/flags-core: Failed to parse stream message, skipping"
770
- );
771
- continue;
772
- }
773
- if (message.type === "datafile") {
774
- onMessage(message.data);
775
- retryCount = 0;
776
- if (!initialDataReceived) {
777
- initialDataReceived = true;
778
- resolveInit();
1109
+ const bufferChunks = [];
1110
+ const onConnectionAbort = () => {
1111
+ reader.cancel().catch(() => {
1112
+ });
1113
+ };
1114
+ connectionAbort.signal.addEventListener("abort", onConnectionAbort, {
1115
+ once: true
1116
+ });
1117
+ try {
1118
+ while (true) {
1119
+ const { done, value: chunk } = await reader.read();
1120
+ if (done || abortController.signal.aborted) break;
1121
+ bufferChunks.push(decoder.decode(chunk, { stream: true }));
1122
+ const combined = bufferChunks.join("");
1123
+ bufferChunks.length = 0;
1124
+ const lines = combined.split("\n");
1125
+ bufferChunks.push(lines.pop());
1126
+ for (const line of lines) {
1127
+ if (line === "") continue;
1128
+ let message;
1129
+ try {
1130
+ message = JSON.parse(line);
1131
+ } catch (e5) {
1132
+ console.warn(
1133
+ "@vercel/flags-core: Failed to parse stream message, skipping"
1134
+ );
1135
+ continue;
1136
+ }
1137
+ if (message.type === "datafile") {
1138
+ onDatafile(message.data);
1139
+ retryCount = 0;
1140
+ if (!initialDataReceived) {
1141
+ initialDataReceived = true;
1142
+ resolveInit();
1143
+ }
1144
+ resetPingTimeout();
1145
+ }
1146
+ if (message.type === "primed") {
1147
+ _optionalChain([onPrimed, 'optionalCall', _62 => _62(message)]);
1148
+ retryCount = 0;
1149
+ if (!initialDataReceived) {
1150
+ initialDataReceived = true;
1151
+ resolveInit();
1152
+ }
1153
+ resetPingTimeout();
1154
+ }
1155
+ if (message.type === "ping" && initialDataReceived) {
1156
+ retryCount = 0;
1157
+ resetPingTimeout();
779
1158
  }
780
1159
  }
781
1160
  }
1161
+ } finally {
1162
+ connectionAbort.signal.removeEventListener(
1163
+ "abort",
1164
+ onConnectionAbort
1165
+ );
782
1166
  }
1167
+ clearTimeout(pingTimeoutId);
1168
+ abortController.signal.removeEventListener("abort", onMainAbort);
783
1169
  if (!abortController.signal.aborted) {
784
- _optionalChain([onDisconnect, 'optionalCall', _35 => _35()]);
1170
+ _optionalChain([onDisconnect, 'optionalCall', _63 => _63()]);
785
1171
  retryCount++;
786
- await sleep(backoff(retryCount));
1172
+ const elapsed = Date.now() - lastAttemptTime;
1173
+ const minGap = Math.max(0, BASE_RETRY_DELAY_MS - elapsed);
1174
+ await sleep(Math.max(backoff(retryCount), minGap));
787
1175
  continue;
788
1176
  }
789
1177
  } catch (error) {
1178
+ clearTimeout(pingTimeoutId);
1179
+ abortController.signal.removeEventListener("abort", onMainAbort);
790
1180
  if (abortController.signal.aborted) {
791
1181
  break;
792
1182
  }
793
- console.error("@vercel/flags-core: Stream error", error);
794
- _optionalChain([onDisconnect, 'optionalCall', _36 => _36()]);
795
- if (!initialDataReceived) {
796
- rejectInit(error);
797
- break;
1183
+ if (!connectionAbort.signal.aborted) {
1184
+ console.error("@vercel/flags-core: Stream error", error);
798
1185
  }
1186
+ _optionalChain([onDisconnect, 'optionalCall', _64 => _64()]);
799
1187
  retryCount++;
800
- await sleep(backoff(retryCount));
1188
+ const elapsed = Date.now() - lastAttemptTime;
1189
+ const minGap = Math.max(0, BASE_RETRY_DELAY_MS - elapsed);
1190
+ await sleep(Math.max(backoff(retryCount), minGap));
801
1191
  }
802
1192
  }
1193
+ if (!initialDataReceived) {
1194
+ rejectInit(new Error("stream: aborted before receiving data"));
1195
+ }
803
1196
  })();
804
1197
  return initPromise;
805
1198
  }
806
1199
 
807
- // src/data-source/flag-network-data-source.ts
808
- var FLAGS_HOST = "https://flags.vercel.com";
809
- var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
810
- var DEFAULT_POLLING_INTERVAL_MS = 3e4;
811
- var DEFAULT_POLLING_INIT_TIMEOUT_MS = 3e3;
812
- var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
813
- var MAX_FETCH_RETRIES = 3;
814
- var FETCH_RETRY_BASE_DELAY_MS = 500;
815
- function normalizeOptions(options) {
816
- const autoDetectedBuildStep = process.env.CI === "1" || process.env.NEXT_PHASE === "phase-production-build";
817
- const buildStep = _nullishCoalesce(options.buildStep, () => ( autoDetectedBuildStep));
818
- let stream;
819
- if (options.stream === void 0 || options.stream === true) {
820
- stream = { enabled: true, initTimeoutMs: DEFAULT_STREAM_INIT_TIMEOUT_MS };
821
- } else if (options.stream === false) {
822
- stream = { enabled: false, initTimeoutMs: 0 };
823
- } else {
824
- stream = { enabled: true, initTimeoutMs: options.stream.initTimeoutMs };
825
- }
826
- let polling;
827
- if (options.polling === void 0 || options.polling === true) {
828
- polling = {
829
- enabled: true,
830
- intervalMs: DEFAULT_POLLING_INTERVAL_MS,
831
- initTimeoutMs: DEFAULT_POLLING_INIT_TIMEOUT_MS
832
- };
833
- } else if (options.polling === false) {
834
- polling = { enabled: false, intervalMs: 0, initTimeoutMs: 0 };
835
- } else {
836
- polling = {
837
- enabled: true,
838
- intervalMs: options.polling.intervalMs,
839
- initTimeoutMs: options.polling.initTimeoutMs
840
- };
1200
+ // src/controller/stream-source.ts
1201
+ var StreamSource = class extends TypedEmitter {
1202
+
1203
+
1204
+
1205
+
1206
+ constructor(options, revision) {
1207
+ super();
1208
+ this.options = options;
1209
+ this.revision = revision;
841
1210
  }
842
- return {
843
- sdkKey: options.sdkKey,
844
- datafile: options.datafile,
845
- stream,
846
- polling,
847
- buildStep,
848
- fetch: _nullishCoalesce(options.fetch, () => ( globalThis.fetch))
849
- };
850
- }
851
- async function fetchDatafile(host, sdkKey, fetchFn) {
852
- let lastError;
853
- for (let attempt = 0; attempt < MAX_FETCH_RETRIES; attempt++) {
854
- const controller = new AbortController();
855
- const timeoutId = setTimeout(
856
- () => controller.abort(),
857
- DEFAULT_FETCH_TIMEOUT_MS
1211
+ /**
1212
+ * Start the stream connection.
1213
+ * Returns a promise that resolves when the first datafile or primed message arrives.
1214
+ * If already started, returns the existing promise.
1215
+ */
1216
+ start() {
1217
+ if (this.promise) return this.promise;
1218
+ const abortController = new AbortController();
1219
+ this.abortController = abortController;
1220
+ abortController.signal.addEventListener(
1221
+ "abort",
1222
+ () => {
1223
+ if (this.abortController === abortController) {
1224
+ this.promise = void 0;
1225
+ this.abortController = void 0;
1226
+ }
1227
+ },
1228
+ { once: true }
858
1229
  );
859
- let shouldRetry = true;
860
1230
  try {
861
- const res = await fetchFn(`${host}/v1/datafile`, {
862
- headers: {
863
- Authorization: `Bearer ${sdkKey}`,
864
- "User-Agent": `VercelFlagsCore/${version}`
1231
+ const promise = connectStream(
1232
+ {
1233
+ host: this.options.host,
1234
+ sdkKey: this.options.sdkKey,
1235
+ abortController,
1236
+ fetch: this.options.fetch,
1237
+ revision: this.revision
865
1238
  },
866
- signal: controller.signal
867
- });
868
- clearTimeout(timeoutId);
869
- if (!res.ok) {
870
- if (res.status >= 400 && res.status < 500 && res.status !== 429) {
871
- shouldRetry = false;
1239
+ {
1240
+ onDatafile: (newData) => {
1241
+ this.emit("data", newData);
1242
+ this.emit("connected");
1243
+ },
1244
+ onPrimed: (message) => {
1245
+ this.emit("primed", message);
1246
+ this.emit("connected");
1247
+ },
1248
+ onDisconnect: () => {
1249
+ this.emit("disconnected");
1250
+ }
872
1251
  }
873
- throw new Error(`Failed to fetch data: ${res.statusText}`);
874
- }
875
- return res.json();
1252
+ );
1253
+ this.promise = promise;
1254
+ return promise;
876
1255
  } catch (error) {
877
- clearTimeout(timeoutId);
878
- lastError = error instanceof Error ? error : new Error("Unknown fetch error");
879
- if (!shouldRetry) throw lastError;
880
- if (attempt < MAX_FETCH_RETRIES - 1) {
881
- const delay = FETCH_RETRY_BASE_DELAY_MS * 2 ** attempt + Math.random() * 500;
882
- await sleep(delay);
883
- }
1256
+ this.promise = void 0;
1257
+ this.abortController = void 0;
1258
+ throw error;
884
1259
  }
885
1260
  }
886
- throw _nullishCoalesce(lastError, () => ( new Error("Failed to fetch data after retries")));
1261
+ /**
1262
+ * Stop the stream connection.
1263
+ */
1264
+ stop() {
1265
+ _optionalChain([this, 'access', _65 => _65.abortController, 'optionalAccess', _66 => _66.abort, 'call', _67 => _67()]);
1266
+ this.abortController = void 0;
1267
+ this.promise = void 0;
1268
+ }
1269
+ };
1270
+
1271
+ // src/controller/tagged-data.ts
1272
+ function tagData(data, origin) {
1273
+ return Object.assign(data, { _origin: origin });
1274
+ }
1275
+ function originToMetricsSource(origin) {
1276
+ switch (origin) {
1277
+ case "stream":
1278
+ case "poll":
1279
+ case "provided":
1280
+ return "in-memory";
1281
+ case "fetched":
1282
+ return "remote";
1283
+ case "bundled":
1284
+ return "embedded";
1285
+ }
887
1286
  }
888
- var FlagNetworkDataSource = (_class2 = class _FlagNetworkDataSource {
889
-
890
- __init2() {this.host = FLAGS_HOST}
891
- // Data state
892
-
1287
+
1288
+ // src/controller/index.ts
1289
+ function parseConfigUpdatedAt(value) {
1290
+ if (typeof value === "number") return value;
1291
+ if (typeof value === "string") {
1292
+ const parsed = Number(value);
1293
+ return Number.isNaN(parsed) ? void 0 : parsed;
1294
+ }
1295
+ return void 0;
1296
+ }
1297
+ var Controller = (_class3 = class {
893
1298
 
894
- // Stream state
1299
+ // State machine
1300
+ __init5() {this.state = "idle"}
1301
+ // Data state — tagged with origin
895
1302
 
1303
+ // Sources (I/O delegates)
896
1304
 
897
- __init3() {this.isStreamConnected = false}
898
- __init4() {this.hasWarnedAboutStaleData = false}
899
- // Polling state
900
1305
 
901
1306
 
902
- // Initialization state — suppresses onDisconnect from starting polling
903
- // while initialize() is still running its own fallback chain
904
- __init5() {this.isInitializing = false}
905
1307
  // Usage tracking
906
1308
 
907
1309
  __init6() {this.isFirstGetData = true}
908
- /**
909
- * Creates a new FlagNetworkDataSource instance.
910
- */
911
- constructor(options) {;_class2.prototype.__init2.call(this);_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this);
1310
+ // Build-step deduplication
1311
+ __init7() {this.buildDataPromise = null}
1312
+ __init8() {this.buildReadTracked = false}
1313
+ // Suppresses usage tracking when the SDK key is unauthorized
1314
+ __init9() {this.unauthorized = false}
1315
+ 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);
912
1316
  if (!options.sdkKey || typeof options.sdkKey !== "string" || !options.sdkKey.startsWith("vf_")) {
913
1317
  throw new Error(
914
1318
  '@vercel/flags-core: SDK key must be a string starting with "vf_"'
915
1319
  );
916
1320
  }
917
1321
  this.options = normalizeOptions(options);
918
- this.bundledDefinitionsPromise = readBundledDefinitions(
919
- this.options.sdkKey
1322
+ this.streamSource = new StreamSource(
1323
+ this.options,
1324
+ () => _optionalChain([this, 'access', _68 => _68.data, 'optionalAccess', _69 => _69.revision])
920
1325
  );
921
- if (this.options.datafile) {
922
- this.data = this.options.datafile;
923
- }
924
- this.usageTracker = new UsageTracker({
1326
+ this.pollingSource = new PollingSource(this.options);
1327
+ this.bundledSource = new BundledSource({
925
1328
  sdkKey: this.options.sdkKey,
926
- host: this.host
1329
+ readBundledDefinitions
927
1330
  });
1331
+ this.wireSourceEvents();
1332
+ if (this.options.datafile) {
1333
+ this.data = tagData(this.options.datafile, "provided");
1334
+ }
1335
+ this.usageTracker = new UsageTracker(this.options);
1336
+ }
1337
+ // Source event handlers (stored for cleanup)
1338
+ __init10() {this.onStreamData = (data) => {
1339
+ if (this.isNewerData(data)) {
1340
+ this.data = tagData(data, "stream");
1341
+ }
1342
+ }}
1343
+ __init11() {this.onStreamPrimed = () => {
1344
+ if (this.state === "degraded" || this.state === "initializing:stream") {
1345
+ this.transition("streaming");
1346
+ }
1347
+ }}
1348
+ __init12() {this.onStreamConnected = () => {
1349
+ if (this.state === "degraded" || this.state === "initializing:stream") {
1350
+ this.transition("streaming");
1351
+ }
1352
+ }}
1353
+ __init13() {this.onStreamDisconnected = () => {
1354
+ if (this.state === "streaming") {
1355
+ this.transition("degraded");
1356
+ }
1357
+ }}
1358
+ __init14() {this.onPollData = (data) => {
1359
+ if (this.isNewerData(data)) {
1360
+ this.data = tagData(data, "poll");
1361
+ }
1362
+ }}
1363
+ __init15() {this.onPollError = (error) => {
1364
+ console.error("@vercel/flags-core: Poll failed:", error);
1365
+ }}
1366
+ // ---------------------------------------------------------------------------
1367
+ // Source event wiring
1368
+ // ---------------------------------------------------------------------------
1369
+ wireSourceEvents() {
1370
+ this.streamSource.on("data", this.onStreamData);
1371
+ this.streamSource.on("primed", this.onStreamPrimed);
1372
+ this.streamSource.on("connected", this.onStreamConnected);
1373
+ this.streamSource.on("disconnected", this.onStreamDisconnected);
1374
+ this.pollingSource.on("data", this.onPollData);
1375
+ this.pollingSource.on("error", this.onPollError);
1376
+ }
1377
+ unwireSourceEvents() {
1378
+ this.streamSource.off("data", this.onStreamData);
1379
+ this.streamSource.off("primed", this.onStreamPrimed);
1380
+ this.streamSource.off("connected", this.onStreamConnected);
1381
+ this.streamSource.off("disconnected", this.onStreamDisconnected);
1382
+ this.pollingSource.off("data", this.onPollData);
1383
+ this.pollingSource.off("error", this.onPollError);
1384
+ }
1385
+ // ---------------------------------------------------------------------------
1386
+ // State machine
1387
+ // ---------------------------------------------------------------------------
1388
+ transition(to) {
1389
+ this.state = to;
1390
+ }
1391
+ get isConnected() {
1392
+ return this.state === "streaming";
1393
+ }
1394
+ get mode() {
1395
+ if (this.options.buildStep) return "build";
1396
+ switch (this.state) {
1397
+ case "streaming":
1398
+ return "streaming";
1399
+ case "polling":
1400
+ return "polling";
1401
+ default:
1402
+ return "offline";
1403
+ }
928
1404
  }
929
1405
  // ---------------------------------------------------------------------------
930
1406
  // Public API (DataSource interface)
@@ -932,155 +1408,187 @@ var FlagNetworkDataSource = (_class2 = class _FlagNetworkDataSource {
932
1408
  /**
933
1409
  * Initializes the data source.
934
1410
  *
935
- * Build step: datafile → bundled → fetch
936
- * Runtime: stream → poll → datafile → bundled
1411
+ * Build step: datafile → bundled → one-time fetch
1412
+ * Streaming mode: stream → datafile → bundled
1413
+ * Polling mode (no stream): poll → datafile → bundled
1414
+ * Offline mode (neither): datafile → bundled → one-time fetch
937
1415
  */
938
1416
  async initialize() {
939
1417
  if (this.options.buildStep) {
1418
+ this.transition("build:loading");
940
1419
  await this.initializeForBuildStep();
1420
+ this.transition("build:ready");
941
1421
  return;
942
1422
  }
943
1423
  if (!this.data && this.options.datafile) {
944
- this.data = this.options.datafile;
1424
+ this.data = tagData(this.options.datafile, "provided");
1425
+ }
1426
+ if (!this.data) {
1427
+ try {
1428
+ const bundled = await this.bundledSource.tryLoad();
1429
+ if (bundled) {
1430
+ this.data = tagData(bundled, "bundled");
1431
+ }
1432
+ } catch (e6) {
1433
+ }
945
1434
  }
946
1435
  if (this.data) {
947
- this.startBackgroundUpdates();
1436
+ if (this.options.stream.enabled) {
1437
+ this.transition("initializing:stream");
1438
+ await this.tryInitializeStream();
1439
+ } else if (this.options.polling.enabled) {
1440
+ this.transition("initializing:polling");
1441
+ await this.tryInitializePolling();
1442
+ } else {
1443
+ this.transition("degraded");
1444
+ }
948
1445
  return;
949
1446
  }
950
- this.isInitializing = true;
951
- try {
952
- if (this.options.stream.enabled) {
953
- const streamSuccess = await this.tryInitializeStream();
954
- if (streamSuccess) return;
1447
+ if (this.options.stream.enabled) {
1448
+ this.transition("initializing:stream");
1449
+ const streamSuccess = await this.tryInitializeStream();
1450
+ if (streamSuccess) {
1451
+ this.transition("streaming");
1452
+ return;
955
1453
  }
956
- if (this.options.polling.enabled) {
957
- const pollingSuccess = await this.tryInitializePolling();
958
- if (pollingSuccess) return;
1454
+ } else if (this.options.polling.enabled) {
1455
+ this.transition("initializing:polling");
1456
+ const pollingSuccess = await this.tryInitializePolling();
1457
+ if (pollingSuccess) {
1458
+ this.transition("polling");
1459
+ return;
959
1460
  }
960
- if (this.data) return;
961
- await this.initializeFromBundled();
962
- } finally {
963
- this.isInitializing = false;
964
1461
  }
1462
+ await this.initializeFromFallbacks();
965
1463
  }
966
1464
  /**
967
1465
  * Reads the current datafile with metrics.
968
1466
  */
969
1467
  async read() {
970
1468
  const startTime = Date.now();
971
- const cachedData = this.data;
972
- const cacheHadDefinitions = cachedData !== void 0;
1469
+ const cacheHadDefinitions = this.data !== void 0;
973
1470
  const isFirstRead = this.isFirstGetData;
974
1471
  this.isFirstGetData = false;
975
- let result;
976
- let source;
977
- let cacheStatus;
978
- if (this.options.buildStep) {
979
- [result, source, cacheStatus] = await this.getDataForBuildStep();
980
- } else if (cachedData) {
981
- [result, source, cacheStatus] = this.getDataFromCache(cachedData);
982
- } else {
983
- [result, source, cacheStatus] = await this.getDataWithFallbacks();
984
- }
1472
+ const [result, cacheStatus] = await this.resolveData();
985
1473
  const readMs = Date.now() - startTime;
1474
+ const { _origin, ...data } = result;
1475
+ const source = originToMetricsSource(_origin);
986
1476
  this.trackRead(startTime, cacheHadDefinitions, isFirstRead, source);
987
- return Object.assign(result, {
1477
+ return {
1478
+ ...data,
988
1479
  metrics: {
989
1480
  readMs,
990
1481
  source,
991
1482
  cacheStatus,
992
- connectionState: this.isStreamConnected ? "connected" : "disconnected"
1483
+ connectionState: this.isConnected ? "connected" : "disconnected",
1484
+ mode: this.mode
993
1485
  }
994
- });
1486
+ };
995
1487
  }
996
1488
  /**
997
1489
  * Shuts down the data source and releases resources.
998
1490
  */
999
1491
  async shutdown() {
1000
- this.stopStream();
1001
- this.stopPolling();
1002
- this.data = this.options.datafile;
1003
- this.isInitializing = false;
1004
- this.isStreamConnected = false;
1005
- this.hasWarnedAboutStaleData = false;
1492
+ this.unwireSourceEvents();
1493
+ this.streamSource.stop();
1494
+ this.pollingSource.stop();
1495
+ this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
1496
+ this.transition("shutdown");
1006
1497
  await this.usageTracker.flush();
1007
1498
  }
1008
1499
  /**
1009
1500
  * Returns the datafile with metrics.
1010
- *
1011
- * During builds this will read from the bundled file if available.
1012
- *
1013
- * This method never opens a streaming connection, but will read from
1014
- * the stream if it is already open. Otherwise it fetches over the network.
1501
+ * Uses in-memory data if available, otherwise falls back to bundled,
1502
+ * then to a one-time fetch if called without prior initialization.
1015
1503
  */
1016
1504
  async getDatafile() {
1017
1505
  const startTime = Date.now();
1506
+ this.isFirstGetData = false;
1018
1507
  let result;
1019
- let source;
1020
1508
  let cacheStatus;
1021
1509
  if (this.options.buildStep) {
1022
- [result, source, cacheStatus] = await this.getDataForBuildStep();
1023
- } else if (this.isStreamConnected && this.data) {
1024
- [result, source, cacheStatus] = this.getDataFromCache();
1510
+ [result, cacheStatus] = await this.resolveDataForBuildStep();
1511
+ } else if (this.data) {
1512
+ cacheStatus = this.isConnected ? "HIT" : "STALE";
1513
+ result = this.data;
1025
1514
  } else {
1026
- const fetched = await fetchDatafile(
1027
- this.host,
1028
- this.options.sdkKey,
1029
- this.options.fetch
1030
- );
1031
- if (this.isNewerData(fetched)) {
1032
- this.data = fetched;
1515
+ const bundled = await this.bundledSource.tryLoad();
1516
+ if (bundled) {
1517
+ this.data = tagData(bundled, "bundled");
1518
+ result = this.data;
1519
+ cacheStatus = "MISS";
1520
+ } else {
1521
+ try {
1522
+ const fetched = await fetchDatafile({
1523
+ host: this.options.host,
1524
+ sdkKey: this.options.sdkKey,
1525
+ fetch: this.options.fetch
1526
+ });
1527
+ this.data = tagData(fetched, "fetched");
1528
+ result = this.data;
1529
+ cacheStatus = "MISS";
1530
+ } catch (e7) {
1531
+ throw new Error(
1532
+ "@vercel/flags-core: No flag definitions available. Initialize the client or provide a datafile."
1533
+ );
1534
+ }
1033
1535
  }
1034
- [result, source, cacheStatus] = [_nullishCoalesce(this.data, () => ( fetched)), "remote", "MISS"];
1035
1536
  }
1036
- return Object.assign(result, {
1537
+ const source = originToMetricsSource(result._origin);
1538
+ return {
1539
+ ...result,
1037
1540
  metrics: {
1038
1541
  readMs: Date.now() - startTime,
1039
1542
  source,
1040
1543
  cacheStatus,
1041
- connectionState: this.isStreamConnected ? "connected" : "disconnected"
1544
+ connectionState: this.isConnected ? "connected" : "disconnected",
1545
+ mode: this.mode
1042
1546
  }
1043
- });
1547
+ };
1044
1548
  }
1045
1549
  /**
1046
1550
  * Returns the bundled fallback datafile.
1047
1551
  */
1048
1552
  async getFallbackDatafile() {
1049
- if (!this.bundledDefinitionsPromise) {
1050
- throw new FallbackNotFoundError();
1051
- }
1052
- const bundledResult = await this.bundledDefinitionsPromise;
1053
- if (!bundledResult) {
1054
- throw new FallbackNotFoundError();
1553
+ return this.bundledSource.getRaw();
1554
+ }
1555
+ // ---------------------------------------------------------------------------
1556
+ // Data resolution (shared by read() and getDatafile())
1557
+ // ---------------------------------------------------------------------------
1558
+ /**
1559
+ * Resolves the current data, using the appropriate strategy for the
1560
+ * current mode. Returns tagged data and cache status.
1561
+ *
1562
+ * Build step: cached → bundled → one-time fetch
1563
+ * Runtime with cache: return cached data
1564
+ * Runtime without cache: stream/poll → datafile → bundled → fetch → throw
1565
+ */
1566
+ async resolveData() {
1567
+ if (this.options.buildStep) {
1568
+ return this.resolveDataForBuildStep();
1055
1569
  }
1056
- switch (bundledResult.state) {
1057
- case "ok":
1058
- return bundledResult.definitions;
1059
- case "missing-file":
1060
- throw new FallbackNotFoundError();
1061
- case "missing-entry":
1062
- throw new FallbackEntryNotFoundError();
1063
- case "unexpected-error":
1064
- throw new Error(
1065
- "@vercel/flags-core: Failed to read bundled definitions: " + String(bundledResult.error)
1066
- );
1570
+ if (this.data) {
1571
+ const cacheStatus = this.isConnected ? "HIT" : "STALE";
1572
+ return [this.data, cacheStatus];
1067
1573
  }
1574
+ return this.resolveDataWithFallbacks();
1068
1575
  }
1069
1576
  // ---------------------------------------------------------------------------
1070
- // Stream management
1577
+ // Stream initialization
1071
1578
  // ---------------------------------------------------------------------------
1072
1579
  /**
1073
1580
  * Attempts to initialize via stream with timeout.
1074
1581
  * Returns true if stream connected successfully within timeout.
1075
1582
  */
1076
1583
  async tryInitializeStream() {
1077
- let streamPromise;
1078
1584
  if (this.options.stream.initTimeoutMs <= 0) {
1079
1585
  try {
1080
- streamPromise = this.startStream();
1081
- await streamPromise;
1586
+ await this.streamSource.start();
1082
1587
  return true;
1083
- } catch (e3) {
1588
+ } catch (error) {
1589
+ if (error instanceof UnauthorizedError) {
1590
+ this.unauthorized = true;
1591
+ }
1084
1592
  return false;
1085
1593
  }
1086
1594
  }
@@ -1092,91 +1600,48 @@ var FlagNetworkDataSource = (_class2 = class _FlagNetworkDataSource {
1092
1600
  );
1093
1601
  });
1094
1602
  try {
1095
- streamPromise = this.startStream();
1096
- const result = await Promise.race([streamPromise, timeoutPromise]);
1603
+ const result = await Promise.race([
1604
+ this.streamSource.start(),
1605
+ timeoutPromise
1606
+ ]);
1097
1607
  clearTimeout(timeoutId);
1098
1608
  if (result === "timeout") {
1099
1609
  console.warn(
1100
1610
  "@vercel/flags-core: Stream initialization timeout, falling back"
1101
1611
  );
1612
+ void this.streamSource.start().catch(() => {
1613
+ });
1102
1614
  return false;
1103
1615
  }
1104
1616
  return true;
1105
- } catch (e4) {
1617
+ } catch (error) {
1106
1618
  clearTimeout(timeoutId);
1619
+ if (error instanceof Error && error.message.includes("401")) {
1620
+ this.unauthorized = true;
1621
+ }
1107
1622
  return false;
1108
1623
  }
1109
1624
  }
1110
- /**
1111
- * Starts the stream connection with callbacks for data and disconnect.
1112
- */
1113
- startStream() {
1114
- if (this.streamPromise) return this.streamPromise;
1115
- this.streamAbortController = new AbortController();
1116
- this.isStreamConnected = false;
1117
- this.hasWarnedAboutStaleData = false;
1118
- try {
1119
- const streamPromise = connectStream(
1120
- {
1121
- host: this.host,
1122
- sdkKey: this.options.sdkKey,
1123
- abortController: this.streamAbortController,
1124
- fetch: this.options.fetch
1125
- },
1126
- {
1127
- onMessage: (newData) => {
1128
- if (this.isNewerData(newData)) {
1129
- this.data = newData;
1130
- }
1131
- this.isStreamConnected = true;
1132
- this.hasWarnedAboutStaleData = false;
1133
- if (this.pollingIntervalId) {
1134
- this.stopPolling();
1135
- }
1136
- },
1137
- onDisconnect: () => {
1138
- this.isStreamConnected = false;
1139
- if (this.options.polling.enabled && !this.pollingIntervalId && !this.isInitializing) {
1140
- this.startPolling();
1141
- }
1142
- }
1143
- }
1144
- );
1145
- this.streamPromise = streamPromise;
1146
- return streamPromise;
1147
- } catch (error) {
1148
- this.streamPromise = void 0;
1149
- this.streamAbortController = void 0;
1150
- throw error;
1151
- }
1152
- }
1153
- /**
1154
- * Stops the stream connection.
1155
- */
1156
- stopStream() {
1157
- _optionalChain([this, 'access', _37 => _37.streamAbortController, 'optionalAccess', _38 => _38.abort, 'call', _39 => _39()]);
1158
- this.streamAbortController = void 0;
1159
- this.streamPromise = void 0;
1160
- }
1161
1625
  // ---------------------------------------------------------------------------
1162
- // Polling management
1626
+ // Polling initialization
1163
1627
  // ---------------------------------------------------------------------------
1164
1628
  /**
1165
1629
  * Attempts to initialize via polling with timeout.
1166
1630
  * Returns true if first poll succeeded within timeout.
1631
+ *
1632
+ * Only used when streaming is disabled and polling is the primary source.
1167
1633
  */
1168
1634
  async tryInitializePolling() {
1169
- this.pollingAbortController = new AbortController();
1170
- const pollPromise = this.performPoll();
1635
+ const pollPromise = this.pollingSource.poll();
1171
1636
  if (this.options.polling.initTimeoutMs <= 0) {
1172
1637
  try {
1173
1638
  await pollPromise;
1174
1639
  if (this.data) {
1175
- this.startPollingInterval();
1640
+ this.pollingSource.startInterval();
1176
1641
  return true;
1177
1642
  }
1178
1643
  return false;
1179
- } catch (e5) {
1644
+ } catch (e8) {
1180
1645
  return false;
1181
1646
  }
1182
1647
  }
@@ -1197,77 +1662,15 @@ var FlagNetworkDataSource = (_class2 = class _FlagNetworkDataSource {
1197
1662
  return false;
1198
1663
  }
1199
1664
  if (this.data) {
1200
- this.startPollingInterval();
1665
+ this.pollingSource.startInterval();
1201
1666
  return true;
1202
1667
  }
1203
1668
  return false;
1204
- } catch (e6) {
1669
+ } catch (e9) {
1205
1670
  clearTimeout(timeoutId);
1206
1671
  return false;
1207
1672
  }
1208
1673
  }
1209
- /**
1210
- * Starts polling (initial poll + interval).
1211
- */
1212
- startPolling() {
1213
- if (this.pollingIntervalId) return;
1214
- this.pollingAbortController = new AbortController();
1215
- void this.performPoll();
1216
- this.startPollingInterval();
1217
- }
1218
- /**
1219
- * Starts the polling interval (without initial poll).
1220
- */
1221
- startPollingInterval() {
1222
- if (this.pollingIntervalId) return;
1223
- this.pollingIntervalId = setInterval(
1224
- () => void this.performPoll(),
1225
- this.options.polling.intervalMs
1226
- );
1227
- }
1228
- /**
1229
- * Stops polling.
1230
- */
1231
- stopPolling() {
1232
- if (this.pollingIntervalId) {
1233
- clearInterval(this.pollingIntervalId);
1234
- this.pollingIntervalId = void 0;
1235
- }
1236
- _optionalChain([this, 'access', _40 => _40.pollingAbortController, 'optionalAccess', _41 => _41.abort, 'call', _42 => _42()]);
1237
- this.pollingAbortController = void 0;
1238
- }
1239
- /**
1240
- * Performs a single poll request.
1241
- */
1242
- async performPoll() {
1243
- if (_optionalChain([this, 'access', _43 => _43.pollingAbortController, 'optionalAccess', _44 => _44.signal, 'access', _45 => _45.aborted])) return;
1244
- try {
1245
- const data = await fetchDatafile(
1246
- this.host,
1247
- this.options.sdkKey,
1248
- this.options.fetch
1249
- );
1250
- if (this.isNewerData(data)) {
1251
- this.data = data;
1252
- }
1253
- } catch (error) {
1254
- console.error("@vercel/flags-core: Poll failed:", error);
1255
- }
1256
- }
1257
- // ---------------------------------------------------------------------------
1258
- // Background updates
1259
- // ---------------------------------------------------------------------------
1260
- /**
1261
- * Starts background updates (stream or polling) without blocking.
1262
- * Used when we already have data from provided datafile.
1263
- */
1264
- startBackgroundUpdates() {
1265
- if (this.options.stream.enabled) {
1266
- void this.startStream();
1267
- } else if (this.options.polling.enabled) {
1268
- this.startPolling();
1269
- }
1270
- }
1271
1674
  // ---------------------------------------------------------------------------
1272
1675
  // Build step helpers
1273
1676
  // ---------------------------------------------------------------------------
@@ -1276,116 +1679,139 @@ var FlagNetworkDataSource = (_class2 = class _FlagNetworkDataSource {
1276
1679
  */
1277
1680
  async initializeForBuildStep() {
1278
1681
  if (this.data) return;
1279
- if (this.bundledDefinitionsPromise) {
1280
- const bundledResult = await this.bundledDefinitionsPromise;
1281
- if (_optionalChain([bundledResult, 'optionalAccess', _46 => _46.state]) === "ok" && bundledResult.definitions) {
1282
- this.data = bundledResult.definitions;
1283
- return;
1284
- }
1682
+ if (!this.buildDataPromise) {
1683
+ this.buildDataPromise = this.loadBuildData();
1285
1684
  }
1286
- this.data = await fetchDatafile(
1287
- this.host,
1288
- this.options.sdkKey,
1289
- this.options.fetch
1290
- );
1685
+ this.data = await this.buildDataPromise;
1291
1686
  }
1292
1687
  /**
1293
1688
  * Retrieves data during build steps.
1689
+ * Concurrent callers share a single load promise. The first caller to
1690
+ * populate `this.data` gets cacheStatus MISS; subsequent callers get HIT.
1294
1691
  */
1295
- async getDataForBuildStep() {
1692
+ async resolveDataForBuildStep() {
1296
1693
  if (this.data) {
1297
- return [this.data, "in-memory", "HIT"];
1694
+ return [this.data, "HIT"];
1298
1695
  }
1299
- if (this.bundledDefinitionsPromise) {
1300
- const bundledResult = await this.bundledDefinitionsPromise;
1301
- if (_optionalChain([bundledResult, 'optionalAccess', _47 => _47.state]) === "ok" && bundledResult.definitions) {
1302
- this.data = bundledResult.definitions;
1303
- return [this.data, "embedded", "MISS"];
1304
- }
1696
+ if (!this.buildDataPromise) {
1697
+ this.buildDataPromise = this.loadBuildData();
1698
+ }
1699
+ const data = await this.buildDataPromise;
1700
+ if (!this.data) {
1701
+ this.data = data;
1702
+ return [data, "MISS"];
1305
1703
  }
1306
- this.data = await fetchDatafile(
1307
- this.host,
1308
- this.options.sdkKey,
1309
- this.options.fetch
1704
+ return [this.data, "HIT"];
1705
+ }
1706
+ /**
1707
+ * Loads data for a build step: bundled → one-time fetch.
1708
+ */
1709
+ async loadBuildData() {
1710
+ const bundled = await this.bundledSource.tryLoad();
1711
+ if (bundled) return tagData(bundled, "bundled");
1712
+ try {
1713
+ const fetched = await fetchDatafile({
1714
+ host: this.options.host,
1715
+ sdkKey: this.options.sdkKey,
1716
+ fetch: this.options.fetch
1717
+ });
1718
+ return tagData(fetched, "fetched");
1719
+ } catch (e10) {
1720
+ }
1721
+ throw new Error(
1722
+ "@vercel/flags-core: No flag definitions available during build. Provide a datafile or bundled definitions."
1310
1723
  );
1311
- return [this.data, "remote", "MISS"];
1312
1724
  }
1313
1725
  // ---------------------------------------------------------------------------
1314
- // Runtime helpers
1726
+ // Fallback helpers
1315
1727
  // ---------------------------------------------------------------------------
1316
1728
  /**
1317
- * Returns data from the in-memory cache.
1729
+ * Shared fallback chain used by both initialize() and resolveData().
1318
1730
  */
1319
- getDataFromCache(cachedData) {
1320
- const data = _nullishCoalesce(cachedData, () => ( this.data));
1321
- this.warnIfDisconnected();
1322
- const cacheStatus = this.isStreamConnected ? "HIT" : "STALE";
1323
- return [data, "in-memory", cacheStatus];
1731
+ async initializeFromFallbacks() {
1732
+ this.transition("initializing:fallback");
1733
+ if (this.data) {
1734
+ this.transition("degraded");
1735
+ return;
1736
+ }
1737
+ const bundled = await this.bundledSource.tryLoad();
1738
+ if (bundled) {
1739
+ this.data = tagData(bundled, "bundled");
1740
+ this.transition("degraded");
1741
+ return;
1742
+ }
1743
+ if (!this.options.stream.enabled && !this.options.polling.enabled) {
1744
+ try {
1745
+ const fetched = await fetchDatafile({
1746
+ host: this.options.host,
1747
+ sdkKey: this.options.sdkKey,
1748
+ fetch: this.options.fetch
1749
+ });
1750
+ this.data = tagData(fetched, "fetched");
1751
+ this.transition("degraded");
1752
+ return;
1753
+ } catch (e11) {
1754
+ }
1755
+ }
1756
+ throw new Error(
1757
+ "@vercel/flags-core: No flag definitions available. Bundled definitions not found."
1758
+ );
1324
1759
  }
1325
1760
  /**
1326
- * Retrieves data using the fallback chain.
1761
+ * Retrieves data using the fallback chain (called when no cached data exists).
1762
+ * Streaming mode: stream → datafile → bundled.
1763
+ * Polling mode: poll → datafile → bundled.
1764
+ * Offline mode: datafile → bundled → one-time fetch.
1327
1765
  */
1328
- async getDataWithFallbacks() {
1766
+ async resolveDataWithFallbacks() {
1329
1767
  if (this.options.stream.enabled) {
1768
+ this.transition("initializing:stream");
1330
1769
  const streamSuccess = await this.tryInitializeStream();
1331
1770
  if (streamSuccess && this.data) {
1332
- return [this.data, "in-memory", "MISS"];
1771
+ this.transition("streaming");
1772
+ return [this.data, "MISS"];
1333
1773
  }
1334
- }
1335
- if (this.options.polling.enabled) {
1774
+ } else if (this.options.polling.enabled) {
1775
+ this.transition("initializing:polling");
1336
1776
  const pollingSuccess = await this.tryInitializePolling();
1337
1777
  if (pollingSuccess && this.data) {
1338
- return [this.data, "remote", "MISS"];
1778
+ this.transition("polling");
1779
+ return [this.data, "MISS"];
1339
1780
  }
1340
1781
  }
1782
+ this.transition("initializing:fallback");
1341
1783
  if (this.options.datafile) {
1342
- this.data = this.options.datafile;
1343
- return [this.data, "in-memory", "STALE"];
1784
+ this.data = tagData(this.options.datafile, "provided");
1785
+ this.transition("degraded");
1786
+ return [this.data, "STALE"];
1344
1787
  }
1345
- if (this.bundledDefinitionsPromise) {
1346
- const bundledResult = await this.bundledDefinitionsPromise;
1347
- if (_optionalChain([bundledResult, 'optionalAccess', _48 => _48.state]) === "ok" && bundledResult.definitions) {
1348
- console.warn(
1349
- "@vercel/flags-core: Using bundled definitions as fallback"
1350
- );
1351
- this.data = bundledResult.definitions;
1352
- return [this.data, "embedded", "STALE"];
1353
- }
1354
- }
1355
- throw new Error(
1356
- "@vercel/flags-core: No flag definitions available. Ensure streaming/polling is enabled or provide a datafile."
1357
- );
1358
- }
1359
- /**
1360
- * Initializes from bundled definitions.
1361
- */
1362
- async initializeFromBundled() {
1363
- if (!this.bundledDefinitionsPromise) {
1364
- throw new Error(
1365
- "@vercel/flags-core: No flag definitions available. Ensure streaming/polling is enabled or provide a datafile."
1366
- );
1788
+ const bundled = await this.bundledSource.tryLoad();
1789
+ if (bundled) {
1790
+ console.warn("@vercel/flags-core: Using bundled definitions as fallback");
1791
+ this.data = tagData(bundled, "bundled");
1792
+ this.transition("degraded");
1793
+ return [this.data, "STALE"];
1367
1794
  }
1368
- const bundledResult = await this.bundledDefinitionsPromise;
1369
- if (_optionalChain([bundledResult, 'optionalAccess', _49 => _49.state]) === "ok" && bundledResult.definitions) {
1370
- this.data = bundledResult.definitions;
1371
- return;
1795
+ if (!this.options.stream.enabled && !this.options.polling.enabled) {
1796
+ try {
1797
+ const fetched = await fetchDatafile({
1798
+ host: this.options.host,
1799
+ sdkKey: this.options.sdkKey,
1800
+ fetch: this.options.fetch
1801
+ });
1802
+ this.data = tagData(fetched, "fetched");
1803
+ this.transition("degraded");
1804
+ return [this.data, "MISS"];
1805
+ } catch (e12) {
1806
+ }
1372
1807
  }
1373
1808
  throw new Error(
1374
- "@vercel/flags-core: No flag definitions available. Bundled definitions not found."
1809
+ "@vercel/flags-core: No flag definitions available. Provide a datafile or bundled definitions."
1375
1810
  );
1376
1811
  }
1377
- /**
1378
- * Parses a configUpdatedAt value (number or string) into a numeric timestamp.
1379
- * Returns undefined if the value is missing or cannot be parsed.
1380
- */
1381
- static parseConfigUpdatedAt(value) {
1382
- if (typeof value === "number") return value;
1383
- if (typeof value === "string") {
1384
- const parsed = Number(value);
1385
- return Number.isNaN(parsed) ? void 0 : parsed;
1386
- }
1387
- return void 0;
1388
- }
1812
+ // ---------------------------------------------------------------------------
1813
+ // Data comparison
1814
+ // ---------------------------------------------------------------------------
1389
1815
  /**
1390
1816
  * Checks if the incoming data is newer than the current in-memory data.
1391
1817
  * Returns true if the update should proceed, false if it should be skipped.
@@ -1395,56 +1821,53 @@ var FlagNetworkDataSource = (_class2 = class _FlagNetworkDataSource {
1395
1821
  * - The current data has no configUpdatedAt
1396
1822
  * - The incoming data has no configUpdatedAt
1397
1823
  *
1398
- * Skips the update only when both have configUpdatedAt and incoming is older.
1824
+ * Skips the update only when both have configUpdatedAt and incoming is not newer.
1399
1825
  */
1400
1826
  isNewerData(incoming) {
1401
1827
  if (!this.data) return true;
1402
- const currentTs = _FlagNetworkDataSource.parseConfigUpdatedAt(
1403
- this.data.configUpdatedAt
1404
- );
1405
- const incomingTs = _FlagNetworkDataSource.parseConfigUpdatedAt(
1406
- incoming.configUpdatedAt
1407
- );
1828
+ const currentTs = parseConfigUpdatedAt(this.data.configUpdatedAt);
1829
+ const incomingTs = parseConfigUpdatedAt(incoming.configUpdatedAt);
1408
1830
  if (currentTs === void 0 || incomingTs === void 0) {
1409
1831
  return true;
1410
1832
  }
1411
- return incomingTs >= currentTs;
1412
- }
1413
- /**
1414
- * Logs a warning if returning cached data while stream is disconnected.
1415
- */
1416
- warnIfDisconnected() {
1417
- if (!this.isStreamConnected && !this.hasWarnedAboutStaleData) {
1418
- this.hasWarnedAboutStaleData = true;
1419
- console.warn(
1420
- "@vercel/flags-core: Returning in-memory flag definitions while stream is disconnected. Data may be stale."
1421
- );
1422
- }
1833
+ return incomingTs > currentTs;
1423
1834
  }
1424
1835
  // ---------------------------------------------------------------------------
1425
1836
  // Usage tracking
1426
1837
  // ---------------------------------------------------------------------------
1427
1838
  /**
1428
1839
  * Tracks a read operation for usage analytics.
1840
+ * During build steps, only the first read is tracked.
1429
1841
  */
1430
1842
  trackRead(startTime, cacheHadDefinitions, isFirstRead, source) {
1843
+ if (this.unauthorized) return;
1844
+ if (this.options.buildStep && this.buildReadTracked) return;
1845
+ if (this.options.buildStep) this.buildReadTracked = true;
1431
1846
  const configOrigin = source === "embedded" ? "embedded" : "in-memory";
1847
+ const cacheAction = this.state === "streaming" ? "FOLLOWING" : this.state === "polling" ? "REFRESHING" : "NONE";
1848
+ const mode = this.mode;
1432
1849
  const trackOptions = {
1433
1850
  configOrigin,
1434
1851
  cacheStatus: cacheHadDefinitions ? "HIT" : "MISS",
1852
+ cacheAction,
1435
1853
  cacheIsBlocking: !cacheHadDefinitions,
1436
- duration: Date.now() - startTime
1854
+ duration: Date.now() - startTime,
1855
+ mode: mode === "streaming" ? "stream" : mode === "polling" ? "poll" : mode
1437
1856
  };
1438
- const configUpdatedAt = _optionalChain([this, 'access', _50 => _50.data, 'optionalAccess', _51 => _51.configUpdatedAt]);
1857
+ const configUpdatedAt = _optionalChain([this, 'access', _70 => _70.data, 'optionalAccess', _71 => _71.configUpdatedAt]);
1439
1858
  if (typeof configUpdatedAt === "number") {
1440
1859
  trackOptions.configUpdatedAt = configUpdatedAt;
1441
1860
  }
1861
+ const revision = _optionalChain([this, 'access', _72 => _72.data, 'optionalAccess', _73 => _73.revision]);
1862
+ if (typeof revision === "number") {
1863
+ trackOptions.revision = revision;
1864
+ }
1442
1865
  if (isFirstRead) {
1443
1866
  trackOptions.cacheIsFirstRead = true;
1444
1867
  }
1445
1868
  this.usageTracker.trackRead(trackOptions);
1446
1869
  }
1447
- }, _class2);
1870
+ }, _class3);
1448
1871
 
1449
1872
  // src/utils/sdk-keys.ts
1450
1873
  function parseSdkKeyFromFlagsConnectionString(text) {
@@ -1453,7 +1876,7 @@ function parseSdkKeyFromFlagsConnectionString(text) {
1453
1876
  if (!text.startsWith("flags:")) return null;
1454
1877
  const params = new URLSearchParams(text.slice(6));
1455
1878
  return params.get("sdkKey");
1456
- } catch (e7) {
1879
+ } catch (e13) {
1457
1880
  }
1458
1881
  return null;
1459
1882
  }
@@ -1462,16 +1885,23 @@ function parseSdkKeyFromFlagsConnectionString(text) {
1462
1885
  function make(createRawClient) {
1463
1886
  let _defaultFlagsClient = null;
1464
1887
  function createClient2(sdkKeyOrConnectionString, options) {
1465
- if (!sdkKeyOrConnectionString) throw new Error("flags: Missing sdkKey");
1888
+ if (!sdkKeyOrConnectionString)
1889
+ throw new Error("@vercel/flags-core: Missing sdkKey");
1890
+ if (typeof sdkKeyOrConnectionString !== "string")
1891
+ throw new Error(
1892
+ `@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`
1893
+ );
1466
1894
  const sdkKey = parseSdkKeyFromFlagsConnectionString(
1467
1895
  sdkKeyOrConnectionString
1468
1896
  );
1469
1897
  if (!sdkKey) {
1470
- throw new Error("flags: Missing sdkKey in connection string");
1898
+ throw new Error(
1899
+ "@vercel/flags-core: Missing sdkKey in connection string"
1900
+ );
1471
1901
  }
1472
- const dataSource = new FlagNetworkDataSource({ sdkKey, ...options });
1902
+ const controller = new Controller({ sdkKey, ...options });
1473
1903
  return createRawClient({
1474
- dataSource,
1904
+ controller,
1475
1905
  origin: { provider: "vercel", sdkKey }
1476
1906
  });
1477
1907
  }
@@ -1486,7 +1916,7 @@ function make(createRawClient) {
1486
1916
  }
1487
1917
  const sdkKey = parseSdkKeyFromFlagsConnectionString(process.env.FLAGS);
1488
1918
  if (!sdkKey) {
1489
- throw new Error("flags: Missing sdkKey");
1919
+ throw new Error("@vercel/flags-core: Missing sdkKey");
1490
1920
  }
1491
1921
  _defaultFlagsClient = createClient2(sdkKey);
1492
1922
  }
@@ -1505,7 +1935,7 @@ function setCacheLife() {
1505
1935
  try {
1506
1936
  _cache.cacheLife.call(void 0, { revalidate: 0, expire: 0 });
1507
1937
  _cache.cacheLife.call(void 0, { stale: 60 });
1508
- } catch (e8) {
1938
+ } catch (e14) {
1509
1939
  }
1510
1940
  }
1511
1941
  var cachedFns = {
@@ -1548,5 +1978,5 @@ var { flagsClient, resetDefaultFlagsClient, createClient } = make(
1548
1978
 
1549
1979
 
1550
1980
 
1551
- exports.ResolutionReason = ResolutionReason; exports.evaluate = evaluate; exports.FallbackNotFoundError = FallbackNotFoundError; exports.FallbackEntryNotFoundError = FallbackEntryNotFoundError; exports.FlagNetworkDataSource = FlagNetworkDataSource; exports.flagsClient = flagsClient; exports.resetDefaultFlagsClient = resetDefaultFlagsClient; exports.createClient = createClient;
1552
- //# sourceMappingURL=chunk-326YN2YT.cjs.map
1981
+ exports.ResolutionReason = ResolutionReason; exports.evaluate = evaluate; exports.FallbackNotFoundError = FallbackNotFoundError; exports.FallbackEntryNotFoundError = FallbackEntryNotFoundError; exports.Controller = Controller; exports.flagsClient = flagsClient; exports.resetDefaultFlagsClient = resetDefaultFlagsClient; exports.createClient = createClient;
1982
+ //# sourceMappingURL=chunk-LBP3OTYA.cjs.map