@vercel/flags-core 1.9.0 → 1.10.0-0037f35-20261006132750

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 (33) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +184 -0
  3. package/dist/{chunk-TDZBUVBG.js → chunk-D4522FGO.js} +1295 -367
  4. package/dist/chunk-D4522FGO.js.map +1 -0
  5. package/dist/{chunk-PGKH4XQ7.cjs → chunk-ITB5XQD3.cjs} +1335 -407
  6. package/dist/chunk-ITB5XQD3.cjs.map +1 -0
  7. package/dist/{chunk-SLNLPFKU.js → chunk-SYXANNZ4.js} +1295 -367
  8. package/dist/chunk-SYXANNZ4.js.map +1 -0
  9. package/dist/{chunk-W742GGZU.cjs → chunk-W6776MKF.cjs} +1336 -408
  10. package/dist/chunk-W6776MKF.cjs.map +1 -0
  11. package/dist/index.default.cjs +2 -2
  12. package/dist/index.default.d.cts +104 -39
  13. package/dist/index.default.d.ts +104 -39
  14. package/dist/index.default.js +1 -1
  15. package/dist/index.next-js.cjs +2 -2
  16. package/dist/index.next-js.d.cts +104 -39
  17. package/dist/index.next-js.d.ts +104 -39
  18. package/dist/index.next-js.js +1 -1
  19. package/dist/openfeature.default.cjs +2 -2
  20. package/dist/openfeature.default.d.cts +1 -1
  21. package/dist/openfeature.default.d.ts +1 -1
  22. package/dist/openfeature.default.js +1 -1
  23. package/dist/openfeature.next-js.cjs +2 -2
  24. package/dist/openfeature.next-js.d.cts +1 -1
  25. package/dist/openfeature.next-js.d.ts +1 -1
  26. package/dist/openfeature.next-js.js +1 -1
  27. package/dist/{types-1OK52NNi.d.cts → types-DpVlT3fI.d.cts} +7 -1
  28. package/dist/{types-1OK52NNi.d.ts → types-DpVlT3fI.d.ts} +7 -1
  29. package/package.json +1 -1
  30. package/dist/chunk-PGKH4XQ7.cjs.map +0 -1
  31. package/dist/chunk-SLNLPFKU.js.map +0 -1
  32. package/dist/chunk-TDZBUVBG.js.map +0 -1
  33. package/dist/chunk-W742GGZU.cjs.map +0 -1
@@ -1,4 +1,4 @@
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; var _class4; var _class5; var _class6;// 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; var _class4; var _class5; var _class6; var _class7; var _class8; var _class9; var _class10;// src/index.next-js.ts
2
2
  var _cache = require('next/cache');
3
3
  var _server = require('next/server');
4
4
 
@@ -559,7 +559,7 @@ function bulkEvaluate(flags, shared) {
559
559
  }
560
560
 
561
561
  // package.json
562
- var version = "1.9.0";
562
+ var version = "1.10.0-0037f35-20261006132750";
563
563
 
564
564
  // src/lib/report-value.ts
565
565
  function internalReportValue(key, value, data) {
@@ -1028,25 +1028,16 @@ var _oidc = require('@vercel/oidc');
1028
1028
 
1029
1029
  // src/utils/sdk-keys.ts
1030
1030
  var SDK_KEY_REGEX = /^vf_(?:server|client)_/;
1031
- function isValidSdkKey(value) {
1032
- return SDK_KEY_REGEX.test(value);
1033
- }
1034
- var PROJECT_ID_REGEX = /^[A-Za-z0-9_]{1,64}$/;
1035
- function isValidProjectId(value) {
1036
- return PROJECT_ID_REGEX.test(value);
1037
- }
1038
- function parseFlagsConnectionString(text) {
1039
- if (SDK_KEY_REGEX.test(text)) return { sdkKey: text, projectId: null };
1040
- if (!text.startsWith("flags:")) return null;
1031
+ function parseSdkKeyFromFlagsConnectionString(text) {
1032
+ if (SDK_KEY_REGEX.test(text)) return text;
1041
1033
  try {
1034
+ if (!text.startsWith("flags:")) return null;
1042
1035
  const params = new URLSearchParams(text.slice(6));
1043
- return {
1044
- sdkKey: params.get("sdkKey") || null,
1045
- projectId: params.get("projectId") || null
1046
- };
1036
+ const sdkKey = params.get("sdkKey");
1037
+ if (sdkKey && SDK_KEY_REGEX.test(sdkKey)) return sdkKey;
1047
1038
  } catch (e5) {
1048
- return null;
1049
1039
  }
1040
+ return null;
1050
1041
  }
1051
1042
 
1052
1043
  // src/controller/auth.ts
@@ -1058,8 +1049,10 @@ function authHeaders(token, sourceProjectId) {
1058
1049
  };
1059
1050
  }
1060
1051
  function unauthorizedMessage(sourceProjectId) {
1061
- if (!sourceProjectId) return "unauthorized (401)";
1062
- return `unauthorized (401): this deployment is not allowed to read the flags of project "${sourceProjectId}". Check the connection string and that the project is connected.`;
1052
+ if (!sourceProjectId) {
1053
+ return "unauthorized (401)";
1054
+ }
1055
+ return `unauthorized (401): this deployment is not allowed to read the flags of project "${sourceProjectId}". Check that the project is connected.`;
1063
1056
  }
1064
1057
  async function getOidcToken() {
1065
1058
  try {
@@ -1099,27 +1092,13 @@ var Authentication = class {
1099
1092
  `@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`
1100
1093
  );
1101
1094
  }
1102
- const parsed = parseFlagsConnectionString(sdkKeyOrConnectionString);
1103
- if (_optionalChain([parsed, 'optionalAccess', _31 => _31.sdkKey]) && parsed.projectId) {
1104
- throw new Error(
1105
- "@vercel/flags-core: A connection string must contain either sdkKey or projectId, not both"
1106
- );
1107
- }
1108
- if (_optionalChain([parsed, 'optionalAccess', _32 => _32.sdkKey])) {
1109
- if (!isValidSdkKey(parsed.sdkKey)) {
1110
- throw new Error("@vercel/flags-core: Missing sdkKey");
1111
- }
1112
- this.sdkKey = parsed.sdkKey;
1113
- } else if (_optionalChain([parsed, 'optionalAccess', _33 => _33.projectId])) {
1114
- if (!isValidProjectId(parsed.projectId)) {
1115
- throw new Error(
1116
- "@vercel/flags-core: Invalid projectId in connection string"
1117
- );
1118
- }
1119
- this.sourceProjectId = parsed.projectId;
1120
- } else {
1095
+ const parsed = parseSdkKeyFromFlagsConnectionString(
1096
+ sdkKeyOrConnectionString
1097
+ );
1098
+ if (!parsed) {
1121
1099
  throw new Error("@vercel/flags-core: Missing sdkKey");
1122
1100
  }
1101
+ this.sdkKey = parsed;
1123
1102
  }
1124
1103
  }
1125
1104
  async resolveToken() {
@@ -1158,6 +1137,43 @@ function getJitteredWaitMs(baseMs, ratio) {
1158
1137
  return Math.floor(min + Math.random() * span);
1159
1138
  }
1160
1139
 
1140
+ // src/utils/debug-enabled.ts
1141
+ var NAMESPACE = "@vercel/flags-core";
1142
+ var lastValue;
1143
+ var lastEnabled = false;
1144
+ function matches(pattern, namespace) {
1145
+ if (!pattern.includes("*")) {
1146
+ return pattern === namespace;
1147
+ }
1148
+ const expression = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
1149
+ return new RegExp(`^${expression}$`).test(namespace);
1150
+ }
1151
+ function isDebugEnabled() {
1152
+ const value = process.env.DEBUG;
1153
+ if (value === lastValue) {
1154
+ return lastEnabled;
1155
+ }
1156
+ let enabled = false;
1157
+ if (value) {
1158
+ for (const entry of value.split(/[\s,]+/)) {
1159
+ if (!entry) {
1160
+ continue;
1161
+ }
1162
+ if (entry.startsWith("-")) {
1163
+ if (matches(entry.slice(1), NAMESPACE)) {
1164
+ enabled = false;
1165
+ break;
1166
+ }
1167
+ } else if (matches(entry, NAMESPACE)) {
1168
+ enabled = true;
1169
+ }
1170
+ }
1171
+ }
1172
+ lastValue = value;
1173
+ lastEnabled = enabled;
1174
+ return enabled;
1175
+ }
1176
+
1161
1177
  // src/utils/runtime-ingest.ts
1162
1178
  var FLAGS_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("@vercel/flags-context");
1163
1179
  function getRuntimeIngestFor(auth) {
@@ -1166,7 +1182,7 @@ function getRuntimeIngestFor(auth) {
1166
1182
  function getRuntimeIngest() {
1167
1183
  try {
1168
1184
  const context = globalThis[FLAGS_CONTEXT_SYMBOL];
1169
- return typeof _optionalChain([context, 'optionalAccess', _34 => _34.ingest]) === "function" ? context.ingest : void 0;
1185
+ return typeof _optionalChain([context, 'optionalAccess', _31 => _31.ingest]) === "function" ? context.ingest : void 0;
1170
1186
  } catch (e7) {
1171
1187
  return void 0;
1172
1188
  }
@@ -1177,9 +1193,8 @@ var MAX_RETRIES = 3;
1177
1193
  var MAX_EVENTS_PER_REQUEST = 2e3;
1178
1194
  var EVALUATING_OIDC_TOKEN_HEADER = "X-Vercel-Flags-OIDC-Token";
1179
1195
  var FLUSH_REASON_HEADER = "X-Vercel-Flags-Flush-Reason";
1180
- var isDebugMode = _optionalChain([process, 'access', _35 => _35.env, 'access', _36 => _36.DEBUG, 'optionalAccess', _37 => _37.includes, 'call', _38 => _38("@vercel/flags-core")]);
1181
1196
  var debugLog = (...args) => {
1182
- if (!isDebugMode) return;
1197
+ if (!isDebugEnabled()) return;
1183
1198
  console.log(...args);
1184
1199
  };
1185
1200
  async function getEvaluatingOidcToken(auth) {
@@ -1202,7 +1217,7 @@ async function getIngestHeaders(options, flushReason) {
1202
1217
  "X-Vercel-Env": _nullishCoalesce(options.metricEnvironment, () => ( process.env.VERCEL_ENV))
1203
1218
  } : null,
1204
1219
  ...evaluatingOidcToken ? { [EVALUATING_OIDC_TOKEN_HEADER]: evaluatingOidcToken } : null,
1205
- ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
1220
+ ...isDebugEnabled() ? { "x-vercel-debug-ingest": "1" } : null
1206
1221
  };
1207
1222
  }
1208
1223
  function getRuntimeIngestHeaders(options, flushReason) {
@@ -1214,7 +1229,7 @@ function getRuntimeIngestHeaders(options, flushReason) {
1214
1229
  ..._nullishCoalesce(options.metricEnvironment, () => ( process.env.VERCEL_ENV)) ? {
1215
1230
  "X-Vercel-Env": _nullishCoalesce(options.metricEnvironment, () => ( process.env.VERCEL_ENV))
1216
1231
  } : null,
1217
- ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
1232
+ ...isDebugEnabled() ? { "x-vercel-debug-ingest": "1" } : null
1218
1233
  };
1219
1234
  }
1220
1235
  async function sendIngestEvents(options, events, flushId, flushReason) {
@@ -1276,7 +1291,7 @@ var SYMBOL_FOR_REQ_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context
1276
1291
  var fromSymbol = globalThis;
1277
1292
  function getRequestContext() {
1278
1293
  try {
1279
- const ctx = _optionalChain([fromSymbol, 'access', _39 => _39[SYMBOL_FOR_REQ_CONTEXT], 'optionalAccess', _40 => _40.get, 'optionalCall', _41 => _41()]);
1294
+ const ctx = _optionalChain([fromSymbol, 'access', _32 => _32[SYMBOL_FOR_REQ_CONTEXT], 'optionalAccess', _33 => _33.get, 'optionalCall', _34 => _34()]);
1280
1295
  if (ctx && Object.hasOwn(ctx, "headers")) {
1281
1296
  return {
1282
1297
  ctx,
@@ -1343,7 +1358,7 @@ var Scheduler = (_class = class {
1343
1358
  }
1344
1359
  resolveScheduledFlush(reason) {
1345
1360
  this.clearTimeouts();
1346
- _optionalChain([this, 'access', _42 => _42.resolveWait, 'optionalCall', _43 => _43(reason)]);
1361
+ _optionalChain([this, 'access', _35 => _35.resolveWait, 'optionalCall', _36 => _36(reason)]);
1347
1362
  }
1348
1363
  reset() {
1349
1364
  this.pending = null;
@@ -1538,7 +1553,7 @@ var UsageTracker = (_class4 = class {
1538
1553
  this.inflightFlushes.add(flush);
1539
1554
  void flush.finally(() => this.inflightFlushes.delete(flush));
1540
1555
  try {
1541
- _optionalChain([this, 'access', _44 => _44.waitUntil, 'optionalCall', _45 => _45(flush)]);
1556
+ _optionalChain([this, 'access', _37 => _37.waitUntil, 'optionalCall', _38 => _38(flush)]);
1542
1557
  } catch (e11) {
1543
1558
  }
1544
1559
  } else {
@@ -1617,7 +1632,7 @@ var BundledSource = class {
1617
1632
  */
1618
1633
  async tryLoad() {
1619
1634
  const result = await this.getResult();
1620
- if (_optionalChain([result, 'optionalAccess', _46 => _46.state]) === "ok" && result.definitions) {
1635
+ if (_optionalChain([result, 'optionalAccess', _39 => _39.state]) === "ok" && result.definitions) {
1621
1636
  return result.definitions;
1622
1637
  }
1623
1638
  return void 0;
@@ -1630,6 +1645,391 @@ var BundledSource = class {
1630
1645
  }
1631
1646
  };
1632
1647
 
1648
+ // src/controller/debug.ts
1649
+ function debug(clientName, event, details) {
1650
+ if (!isDebugEnabled()) {
1651
+ return;
1652
+ }
1653
+ try {
1654
+ console.debug("@vercel/flags-core", { event, clientName, ..._optionalChain([details, 'optionalCall', _40 => _40()]) });
1655
+ } catch (e12) {
1656
+ }
1657
+ }
1658
+
1659
+ // src/controller/tagged-data.ts
1660
+ function tagData(data, origin) {
1661
+ return Object.assign(data, { _origin: origin });
1662
+ }
1663
+ function originToMetricsSource(origin) {
1664
+ switch (origin) {
1665
+ case "stream":
1666
+ case "poll":
1667
+ case "provided":
1668
+ return "in-memory";
1669
+ case "fetched":
1670
+ return "remote";
1671
+ case "bundled":
1672
+ return "embedded";
1673
+ }
1674
+ }
1675
+
1676
+ // src/controller/datafile-cache.ts
1677
+ var SOURCE_CONFIRMED = new Error(
1678
+ "Refresh superseded by a source confirmation"
1679
+ );
1680
+ function parseConfigUpdatedAt(value) {
1681
+ if (typeof value === "number") return value;
1682
+ if (typeof value === "string") {
1683
+ const parsed = Number(value);
1684
+ return Number.isNaN(parsed) ? void 0 : parsed;
1685
+ }
1686
+ return void 0;
1687
+ }
1688
+ var DatafileCache = (_class5 = class {
1689
+ constructor(fetch, staleIfErrorMs = Infinity, waitUntil2 = () => {
1690
+ }, clientName) {;_class5.prototype.__init12.call(this);_class5.prototype.__init13.call(this);
1691
+ this.fetch = fetch;
1692
+ this.staleIfErrorMs = staleIfErrorMs;
1693
+ this.waitUntil = waitUntil2;
1694
+ this.clientName = clientName;
1695
+ }
1696
+
1697
+
1698
+
1699
+
1700
+
1701
+ // Confirmations refresh age without rewriting the datafile's persisted fetch time.
1702
+
1703
+
1704
+ __init12() {this.abortController = new AbortController()}
1705
+
1706
+ __init13() {this.debugState = () => ({
1707
+ projectId: _optionalChain([this, 'access', _49 => _49.data, 'optionalAccess', _50 => _50.projectId]),
1708
+ environment: _optionalChain([this, 'access', _51 => _51.data, 'optionalAccess', _52 => _52.environment]),
1709
+ hasData: this.hasData,
1710
+ origin: _optionalChain([this, 'access', _53 => _53.data, 'optionalAccess', _54 => _54._origin]),
1711
+ revision: this.revision,
1712
+ configUpdatedAt: parseConfigUpdatedAt(_optionalChain([this, 'access', _55 => _55.data, 'optionalAccess', _56 => _56.configUpdatedAt])),
1713
+ ageMs: this.ageMs,
1714
+ failed: this.failure !== void 0,
1715
+ failureAgeMs: this.failure ? Date.now() - this.failure.startedAt : void 0,
1716
+ staleIfErrorMs: this.staleIfErrorMs,
1717
+ canServe: this.hasData && this.canServe(),
1718
+ fetching: this.fetching !== void 0
1719
+ })}
1720
+ /** Expired data still exists; fallback loading must not bypass its failure policy. */
1721
+ get hasData() {
1722
+ return this.data !== void 0;
1723
+ }
1724
+ /** Retained revisions remain available for reconnecting after serving expires. */
1725
+ get revision() {
1726
+ return _optionalChain([this, 'access', _57 => _57.data, 'optionalAccess', _58 => _58.revision]);
1727
+ }
1728
+ /** Time since the latest freshness evidence, if known. */
1729
+ get ageMs() {
1730
+ return this.freshAt === void 0 ? Infinity : Math.max(0, Date.now() - this.freshAt);
1731
+ }
1732
+ /** Records freshness evidence without confirming recovery from a failure. */
1733
+ resetAge() {
1734
+ if (this.data) this.freshAt = Date.now();
1735
+ }
1736
+ /** Freshness checks can inspect retained metadata even after serving expires. */
1737
+ get metadata() {
1738
+ if (!this.data) return void 0;
1739
+ const { projectId, environment, configUpdatedAt, revision } = this.data;
1740
+ return {
1741
+ projectId,
1742
+ environment,
1743
+ configUpdatedAt,
1744
+ revision,
1745
+ ageMs: this.ageMs
1746
+ };
1747
+ }
1748
+ /** Stores initial or fallback data without confirming recovery from a failure. */
1749
+ seed(data) {
1750
+ this.data = data;
1751
+ this.freshAt = typeof data.fetchedAt === "number" && Number.isFinite(data.fetchedAt) && data.fetchedAt >= 0 ? data.fetchedAt : void 0;
1752
+ debug(this.clientName, "cache.seed", this.debugState);
1753
+ }
1754
+ /** Stores data and returns it through the serving boundary. */
1755
+ seedAndRead(data) {
1756
+ this.seed(data);
1757
+ const seeded = this.read();
1758
+ if (!seeded) {
1759
+ throw new Error("@vercel/flags-core: Seeded definitions unavailable");
1760
+ }
1761
+ return seeded;
1762
+ }
1763
+ /**
1764
+ * Accepts a newer source response, or keeps the stored data when the version
1765
+ * guard rejects it. Every successful response proves the source is reachable,
1766
+ * so it renews cache age and clears the failure either way.
1767
+ */
1768
+ updateFromSource(incoming, origin) {
1769
+ if (this.isNewerData(incoming)) {
1770
+ this.data = tagData({ ...incoming, fetchedAt: Date.now() }, origin);
1771
+ this.confirm(origin);
1772
+ debug(this.clientName, "cache.update.accepted", this.debugState);
1773
+ return;
1774
+ }
1775
+ this.confirm(origin);
1776
+ debug(this.clientName, "cache.update.ignored", () => ({
1777
+ ...this.debugState(),
1778
+ incomingOrigin: origin,
1779
+ incomingRevision: incoming.revision,
1780
+ incomingConfigUpdatedAt: parseConfigUpdatedAt(incoming.configUpdatedAt)
1781
+ }));
1782
+ }
1783
+ /** Confirms a same-version message without replacing stored data. */
1784
+ tryConfirm(incoming, version2 = "configUpdatedAt", source = "header") {
1785
+ if (!this.matchesStored(incoming, version2)) {
1786
+ return false;
1787
+ }
1788
+ this.confirm(source);
1789
+ debug(this.clientName, "cache.confirmed", () => ({
1790
+ version: version2,
1791
+ source,
1792
+ ...this.debugState()
1793
+ }));
1794
+ return true;
1795
+ }
1796
+ /** Whether a message refers to the stored entry: finite equal version and same identity. */
1797
+ matchesStored(incoming, version2) {
1798
+ if (!this.data) {
1799
+ return false;
1800
+ }
1801
+ const currentTs = version2 === "revision" ? this.data.revision : parseConfigUpdatedAt(this.data.configUpdatedAt);
1802
+ const incomingTs = version2 === "revision" ? incoming.revision : parseConfigUpdatedAt(incoming.configUpdatedAt);
1803
+ return Number.isFinite(currentTs) && Number.isFinite(incomingTs) && currentTs === incomingTs && this.data.projectId === incoming.projectId && this.data.environment === incoming.environment;
1804
+ }
1805
+ /** Renews freshness and retires HTTP work superseded by stream evidence. */
1806
+ confirm(source = "header") {
1807
+ const recovered = this.failure !== void 0;
1808
+ this.resetAge();
1809
+ this.failure = void 0;
1810
+ if (recovered) {
1811
+ debug(this.clientName, "cache.recovered", this.debugState);
1812
+ }
1813
+ if (source === "stream" && this.fetching) {
1814
+ debug(this.clientName, "cache.fetch.cancel", () => ({
1815
+ reason: "source-confirmed",
1816
+ ...this.debugState()
1817
+ }));
1818
+ this.abortController.abort(SOURCE_CONFIRMED);
1819
+ this.abortController = new AbortController();
1820
+ this.fetching = void 0;
1821
+ }
1822
+ }
1823
+ /** Preserves existing acceptance, including missing or unparseable versions. */
1824
+ isNewerData(incoming) {
1825
+ if (!this.data) return true;
1826
+ const currentTs = parseConfigUpdatedAt(this.data.configUpdatedAt);
1827
+ const incomingTs = parseConfigUpdatedAt(incoming.configUpdatedAt);
1828
+ if (currentTs === void 0 || incomingTs === void 0) {
1829
+ return true;
1830
+ }
1831
+ return incomingTs > currentTs;
1832
+ }
1833
+ fail(error) {
1834
+ const repeated = this.failure !== void 0;
1835
+ this.failure ??= { error, startedAt: Date.now() };
1836
+ debug(this.clientName, "cache.failure", () => ({
1837
+ repeated,
1838
+ ...this.debugState()
1839
+ }));
1840
+ }
1841
+ canServe() {
1842
+ if (!this.failure || this.staleIfErrorMs === Infinity) return true;
1843
+ return this.staleIfErrorMs > 0 && Date.now() - this.failure.startedAt <= this.staleIfErrorMs;
1844
+ }
1845
+ /** The serving boundary for both snapshot and policy-driven reads. */
1846
+ read() {
1847
+ if (!this.data) return void 0;
1848
+ if (!this.canServe()) {
1849
+ debug(this.clientName, "cache.stale-if-error.expired", this.debugState);
1850
+ throw this.failure.error;
1851
+ }
1852
+ return this.data;
1853
+ }
1854
+ async resolve(policy) {
1855
+ const metadata = this.metadata;
1856
+ if (metadata) {
1857
+ const { status, confirmed } = policy.assess(metadata);
1858
+ if (status === "error") {
1859
+ debug(this.clientName, "cache.source-error", this.debugState);
1860
+ return {
1861
+ data: this.canServe() ? this.read() : void 0,
1862
+ status: "STALE",
1863
+ hasError: true
1864
+ };
1865
+ }
1866
+ if (confirmed) this.confirm();
1867
+ debug(this.clientName, "cache.freshness", () => ({
1868
+ status,
1869
+ confirmed: confirmed === true,
1870
+ ...this.debugState()
1871
+ }));
1872
+ if (status === "fresh" || status === "unknown") {
1873
+ return {
1874
+ data: this.read(),
1875
+ status: status === "fresh" && !this.failure ? "HIT" : "STALE"
1876
+ };
1877
+ }
1878
+ if (this.failure) {
1879
+ if (!policy.retryOnFailure) {
1880
+ debug(this.clientName, "cache.stale-if-error", () => ({
1881
+ ...this.debugState(),
1882
+ recovery: "scheduled-source-update"
1883
+ }));
1884
+ return { data: this.read(), status: "STALE" };
1885
+ }
1886
+ if (this.canServe()) {
1887
+ const stale = this.read();
1888
+ debug(this.clientName, "cache.refresh.background", () => ({
1889
+ ...this.debugState(),
1890
+ reason: "retry-after-failure"
1891
+ }));
1892
+ this.fetchInBackground();
1893
+ return { data: stale, status: "STALE" };
1894
+ }
1895
+ }
1896
+ if (status === "stale" && this.canServe()) {
1897
+ const stale = this.read();
1898
+ if (!policy.revalidate) {
1899
+ debug(this.clientName, "cache.refresh.background", () => ({
1900
+ ...this.debugState(),
1901
+ reason: "stale"
1902
+ }));
1903
+ this.fetchInBackground();
1904
+ }
1905
+ return { data: stale, status: "STALE" };
1906
+ }
1907
+ if (policy.revalidate) {
1908
+ return this.resolveThroughSource(policy);
1909
+ }
1910
+ }
1911
+ debug(this.clientName, "cache.refresh.blocking", () => ({
1912
+ ...this.debugState(),
1913
+ reason: !metadata ? "empty-cache" : "expired"
1914
+ }));
1915
+ const { promise, signal } = this.startFetch("fetched");
1916
+ try {
1917
+ await promise;
1918
+ signal.throwIfAborted();
1919
+ } catch (error) {
1920
+ if (signal.aborted) {
1921
+ if (signal.reason === SOURCE_CONFIRMED && this.data) {
1922
+ debug(this.clientName, "cache.refresh.superseded", this.debugState);
1923
+ return { data: this.read(), status: "HIT" };
1924
+ }
1925
+ throw error;
1926
+ }
1927
+ const stale = this.read();
1928
+ if (!stale) {
1929
+ throw error;
1930
+ }
1931
+ debug(this.clientName, "cache.stale-if-error", () => ({
1932
+ ...this.debugState(),
1933
+ recovery: "refresh-failed"
1934
+ }));
1935
+ return { data: stale, status: "STALE" };
1936
+ }
1937
+ if (!metadata && this.metadata) {
1938
+ const { confirmed } = policy.assess(this.metadata);
1939
+ if (confirmed) this.confirm();
1940
+ }
1941
+ const data = this.read();
1942
+ if (!data) {
1943
+ throw new Error("@vercel/flags-core: Fetch returned no definitions");
1944
+ }
1945
+ return { data, status: "MISS" };
1946
+ }
1947
+ /**
1948
+ * Waits for the live source to confirm or replace the cache, then serves
1949
+ * whatever it left behind. A failed or silent source leaves the failure
1950
+ * policy to decide; the source's own fallback path takes over from there.
1951
+ */
1952
+ async resolveThroughSource(policy) {
1953
+ await _optionalChain([policy, 'access', _59 => _59.revalidate, 'optionalCall', _60 => _60(), 'access', _61 => _61.catch, 'call', _62 => _62(() => {
1954
+ })]);
1955
+ const data = this.read();
1956
+ const metadata = this.metadata;
1957
+ if (!data || !metadata) {
1958
+ throw new Error("@vercel/flags-core: Definitions unavailable");
1959
+ }
1960
+ const { status } = policy.assess(metadata);
1961
+ return {
1962
+ data,
1963
+ status: status === "fresh" && !this.failure ? "HIT" : "STALE"
1964
+ };
1965
+ }
1966
+ /** Runs the one shared datafile refresh used by reads and polling. */
1967
+ refresh(origin = "fetched") {
1968
+ return this.startFetch(origin).promise;
1969
+ }
1970
+ startFetch(origin) {
1971
+ const { signal } = this.abortController;
1972
+ if (this.fetching) {
1973
+ debug(this.clientName, "cache.fetch.shared", () => ({
1974
+ requestedOrigin: origin,
1975
+ ...this.debugState()
1976
+ }));
1977
+ return { promise: this.fetching, signal };
1978
+ }
1979
+ debug(this.clientName, "cache.fetch.start", () => ({
1980
+ requestedOrigin: origin,
1981
+ ...this.debugState()
1982
+ }));
1983
+ const promise = Promise.resolve().then(() => {
1984
+ signal.throwIfAborted();
1985
+ return this.fetch(signal);
1986
+ }).then((data) => {
1987
+ signal.throwIfAborted();
1988
+ this.updateFromSource(data, origin);
1989
+ debug(this.clientName, "cache.fetch.applied", this.debugState);
1990
+ }).catch((error) => {
1991
+ if (signal.aborted) {
1992
+ debug(this.clientName, "cache.fetch.aborted", () => ({
1993
+ reason: signal.reason === SOURCE_CONFIRMED ? "source-confirmed" : "cache-cleared"
1994
+ }));
1995
+ }
1996
+ signal.throwIfAborted();
1997
+ const err = error instanceof Error ? error : new Error("Unknown fetch error");
1998
+ this.fail(err);
1999
+ debug(this.clientName, "cache.fetch.failed", this.debugState);
2000
+ throw err;
2001
+ }).finally(() => {
2002
+ if (this.abortController.signal === signal) {
2003
+ this.fetching = void 0;
2004
+ }
2005
+ });
2006
+ this.fetching = promise;
2007
+ return { promise, signal };
2008
+ }
2009
+ fetchInBackground() {
2010
+ const { promise, signal } = this.startFetch("fetched");
2011
+ const background = promise.catch((error) => {
2012
+ if (!signal.aborted) {
2013
+ console.error("@vercel/flags-core: Revalidation failed:", error);
2014
+ }
2015
+ });
2016
+ try {
2017
+ this.waitUntil(background);
2018
+ } catch (e13) {
2019
+ }
2020
+ }
2021
+ /** Resets storage, age, and the failure deadline so a restarted client begins clean. */
2022
+ clear() {
2023
+ debug(this.clientName, "cache.clear", this.debugState);
2024
+ this.abortController.abort();
2025
+ this.abortController = new AbortController();
2026
+ this.fetching = void 0;
2027
+ this.data = void 0;
2028
+ this.freshAt = void 0;
2029
+ this.failure = void 0;
2030
+ }
2031
+ }, _class5);
2032
+
1633
2033
  // src/controller/fetch-datafile.ts
1634
2034
  var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
1635
2035
  var DEFAULT_MAX_ATTEMPTS = 3;
@@ -1644,11 +2044,15 @@ var DatafileHttpError = class extends Error {
1644
2044
  }
1645
2045
  };
1646
2046
  async function fetchDatafile(options) {
1647
- _optionalChain([options, 'access', _47 => _47.signal, 'optionalAccess', _48 => _48.throwIfAborted, 'call', _49 => _49()]);
2047
+ _optionalChain([options, 'access', _63 => _63.signal, 'optionalAccess', _64 => _64.throwIfAborted, 'call', _65 => _65()]);
1648
2048
  const maxAttempts = _nullishCoalesce(options.maxAttempts, () => ( DEFAULT_MAX_ATTEMPTS));
1649
2049
  if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
1650
2050
  throw new Error("maxAttempts must be a positive integer");
1651
2051
  }
2052
+ debug(options.clientName, "datafile.fetch.start", () => ({
2053
+ maxAttempts,
2054
+ timeoutMs: DEFAULT_FETCH_TIMEOUT_MS
2055
+ }));
1652
2056
  const controller = new AbortController();
1653
2057
  const { signal } = controller;
1654
2058
  let rejectAbort;
@@ -1657,17 +2061,26 @@ async function fetchDatafile(options) {
1657
2061
  });
1658
2062
  const onAbort = () => rejectAbort(signal.reason);
1659
2063
  signal.addEventListener("abort", onAbort, { once: true });
1660
- const onExternalAbort = () => controller.abort(_optionalChain([options, 'access', _50 => _50.signal, 'optionalAccess', _51 => _51.reason]));
1661
- _optionalChain([options, 'access', _52 => _52.signal, 'optionalAccess', _53 => _53.addEventListener, 'call', _54 => _54("abort", onExternalAbort, { once: true })]);
1662
- const timeoutId = setTimeout(
1663
- () => controller.abort(
2064
+ const onExternalAbort = () => controller.abort(_optionalChain([options, 'access', _66 => _66.signal, 'optionalAccess', _67 => _67.reason]));
2065
+ _optionalChain([options, 'access', _68 => _68.signal, 'optionalAccess', _69 => _69.addEventListener, 'call', _70 => _70("abort", onExternalAbort, { once: true })]);
2066
+ const timeoutId = setTimeout(() => {
2067
+ debug(options.clientName, "datafile.fetch.timeout", () => ({
2068
+ timeoutMs: DEFAULT_FETCH_TIMEOUT_MS
2069
+ }));
2070
+ controller.abort(
1664
2071
  new Error("@vercel/flags-core: Datafile fetch deadline exceeded")
1665
- ),
1666
- DEFAULT_FETCH_TIMEOUT_MS
1667
- );
2072
+ );
2073
+ }, DEFAULT_FETCH_TIMEOUT_MS);
1668
2074
  let delay;
1669
- const fetchAttempt = async () => {
1670
- const token = await options.auth.resolveToken();
2075
+ const fetchAttempt = async (attempt) => {
2076
+ debug(options.clientName, "datafile.fetch.attempt", () => ({
2077
+ attempt,
2078
+ maxAttempts
2079
+ }));
2080
+ const token = await options.auth.resolveToken().catch((error) => {
2081
+ debug(options.clientName, "datafile.auth.failed", () => ({ attempt }));
2082
+ throw error;
2083
+ });
1671
2084
  signal.throwIfAborted();
1672
2085
  const res = await options.fetch(`${options.host}/v1/datafile`, {
1673
2086
  headers: {
@@ -1678,8 +2091,12 @@ async function fetchDatafile(options) {
1678
2091
  signal
1679
2092
  });
1680
2093
  signal.throwIfAborted();
2094
+ debug(options.clientName, "datafile.fetch.response", () => ({
2095
+ attempt,
2096
+ status: res.status
2097
+ }));
1681
2098
  if (!res.ok) {
1682
- void _optionalChain([res, 'access', _55 => _55.body, 'optionalAccess', _56 => _56.cancel, 'call', _57 => _57(), 'access', _58 => _58.catch, 'call', _59 => _59(() => {
2099
+ void _optionalChain([res, 'access', _71 => _71.body, 'optionalAccess', _72 => _72.cancel, 'call', _73 => _73(), 'access', _74 => _74.catch, 'call', _75 => _75(() => {
1683
2100
  })]);
1684
2101
  throw new DatafileHttpError(
1685
2102
  res.status,
@@ -1688,6 +2105,12 @@ async function fetchDatafile(options) {
1688
2105
  }
1689
2106
  const data = await res.json();
1690
2107
  signal.throwIfAborted();
2108
+ debug(options.clientName, "datafile.fetch.complete", () => ({
2109
+ attempt,
2110
+ projectId: data.projectId,
2111
+ revision: data.revision,
2112
+ configUpdatedAt: Number(data.configUpdatedAt)
2113
+ }));
1691
2114
  return data;
1692
2115
  };
1693
2116
  try {
@@ -1702,29 +2125,112 @@ async function fetchDatafile(options) {
1702
2125
  }
1703
2126
  signal.throwIfAborted();
1704
2127
  try {
1705
- return await Promise.race([fetchAttempt(), aborted]);
2128
+ return await Promise.race([fetchAttempt(attempt + 1), aborted]);
1706
2129
  } catch (error) {
1707
2130
  signal.throwIfAborted();
1708
2131
  if (attempt === maxAttempts - 1 || error instanceof DatafileHttpError && !error.retryable) {
1709
2132
  throw error instanceof Error ? error : new Error("Unknown fetch error");
1710
2133
  }
2134
+ debug(options.clientName, "datafile.fetch.retry", () => ({
2135
+ nextAttempt: attempt + 2,
2136
+ delayMs: 100 * 2 ** attempt,
2137
+ status: error instanceof DatafileHttpError ? error.status : void 0
2138
+ }));
1711
2139
  }
1712
2140
  }
2141
+ } catch (error) {
2142
+ if (signal.aborted) {
2143
+ debug(options.clientName, "datafile.fetch.aborted", () => ({
2144
+ // The internal deadline aborts too; only the caller's signal is external.
2145
+ reason: _optionalChain([options, 'access', _76 => _76.signal, 'optionalAccess', _77 => _77.aborted]) ? "external" : "timeout"
2146
+ }));
2147
+ } else {
2148
+ debug(options.clientName, "datafile.fetch.failed", () => ({
2149
+ status: error instanceof DatafileHttpError ? error.status : void 0
2150
+ }));
2151
+ }
2152
+ throw error;
1713
2153
  } finally {
1714
2154
  clearTimeout(timeoutId);
1715
2155
  clearTimeout(delay);
1716
2156
  signal.removeEventListener("abort", onAbort);
1717
- _optionalChain([options, 'access', _60 => _60.signal, 'optionalAccess', _61 => _61.removeEventListener, 'call', _62 => _62("abort", onExternalAbort)]);
2157
+ _optionalChain([options, 'access', _78 => _78.signal, 'optionalAccess', _79 => _79.removeEventListener, 'call', _80 => _80("abort", onExternalAbort)]);
1718
2158
  }
1719
2159
  }
1720
2160
 
2161
+ // src/controller/header-source.ts
2162
+ var HeaderSource = (_class6 = class {
2163
+ constructor(options) {;_class6.prototype.__init14.call(this);
2164
+ this.options = options;
2165
+ }
2166
+
2167
+ __init14() {this.highestObserved = 0}
2168
+ getVersionHeader() {
2169
+ const { headers } = getRequestContext();
2170
+ return _nullishCoalesce(_optionalChain([headers, 'optionalAccess', _81 => _81["x-vercel-flags-config-versions"]]), () => ( _optionalChain([headers, 'optionalAccess', _82 => _82["flags-config-versions"]])));
2171
+ }
2172
+ /** Capture the header now so a shared fetch cannot switch the request being assessed. */
2173
+ getAssessment() {
2174
+ const header = this.getVersionHeader();
2175
+ return (data) => {
2176
+ const headerTs = this.getUpdatedAtHeader(data.projectId, header);
2177
+ debug(this.options.clientName, "header.observed", () => ({
2178
+ projectId: data.projectId,
2179
+ hasHeader: Boolean(header),
2180
+ headerTimestamp: headerTs,
2181
+ configUpdatedAt: Number(data.configUpdatedAt),
2182
+ previousHighestObserved: this.highestObserved,
2183
+ staleWhileRevalidateMs: this.options.staleWhileRevalidateMs
2184
+ }));
2185
+ if (headerTs === void 0) {
2186
+ return { status: "error" };
2187
+ }
2188
+ const currentTs = Number(data.configUpdatedAt);
2189
+ this.highestObserved = Math.max(this.highestObserved, headerTs);
2190
+ if (!Number.isFinite(currentTs) || currentTs <= 0) {
2191
+ return { status: "error" };
2192
+ }
2193
+ if (headerTs <= currentTs) {
2194
+ return {
2195
+ status: "fresh",
2196
+ confirmed: headerTs === currentTs && headerTs === this.highestObserved
2197
+ };
2198
+ }
2199
+ const { staleWhileRevalidateMs } = this.options;
2200
+ return {
2201
+ status: staleWhileRevalidateMs > 0 && data.ageMs <= staleWhileRevalidateMs ? "stale" : "expired"
2202
+ };
2203
+ };
2204
+ }
2205
+ getUpdatedAtHeader(projectId, header) {
2206
+ if (!header) return;
2207
+ const prefix = `flags_${projectId}=`;
2208
+ const value = _optionalChain([header, 'access', _83 => _83.split, 'call', _84 => _84(";"), 'access', _85 => _85.map, 'call', _86 => _86((part) => part.trim()), 'access', _87 => _87.find, 'call', _88 => _88((part) => part.startsWith(prefix)), 'optionalAccess', _89 => _89.slice, 'call', _90 => _90(prefix.length)]);
2209
+ const timestamp = Number(value);
2210
+ return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : void 0;
2211
+ }
2212
+ isEnabled() {
2213
+ return this.options.vercel && (this.options.stream.enabled || this.options.polling.enabled);
2214
+ }
2215
+ stop() {
2216
+ this.highestObserved = 0;
2217
+ }
2218
+ }, _class6);
2219
+
1721
2220
  // src/controller/normalized-options.ts
1722
2221
 
1723
2222
  var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
1724
2223
  var DEFAULT_POLLING_INTERVAL_MS = 3e4;
1725
2224
  var MIN_POLLING_INTERVAL_MS = 3e4;
1726
2225
  var DEFAULT_POLLING_INIT_TIMEOUT_MS = 3e3;
2226
+ var DEFAULT_STALE_WHILE_REVALIDATE = 10;
1727
2227
  function normalizeOptions(options) {
2228
+ const staleIfError = _nullishCoalesce(options.staleIfError, () => ( Infinity));
2229
+ if (typeof staleIfError !== "number" || !(staleIfError >= 0)) {
2230
+ throw new Error(
2231
+ "@vercel/flags-core: staleIfError must be a nonnegative number of seconds or Infinity."
2232
+ );
2233
+ }
1728
2234
  const autoDetectedBuildStep = process.env.CI === "1" || process.env.NEXT_PHASE === "phase-production-build";
1729
2235
  const buildStep = _nullishCoalesce(options.buildStep, () => ( autoDetectedBuildStep));
1730
2236
  let stream;
@@ -1756,11 +2262,20 @@ function normalizeOptions(options) {
1756
2262
  initTimeoutMs: options.polling.initTimeoutMs
1757
2263
  };
1758
2264
  }
2265
+ const staleWhileRevalidate = _nullishCoalesce(options.staleWhileRevalidate, () => ( DEFAULT_STALE_WHILE_REVALIDATE));
2266
+ if (!Number.isFinite(staleWhileRevalidate) || staleWhileRevalidate < 0) {
2267
+ throw new Error(
2268
+ "@vercel/flags-core: staleWhileRevalidate must be a finite, non-negative number of seconds."
2269
+ );
2270
+ }
1759
2271
  return {
1760
2272
  auth: options.auth,
1761
2273
  datafile: options.datafile,
1762
2274
  stream,
1763
2275
  polling,
2276
+ vercel: _nullishCoalesce(options.vercel, () => ( process.env.VERCEL === "1")),
2277
+ staleWhileRevalidateMs: staleWhileRevalidate * 1e3,
2278
+ staleIfErrorMs: staleIfError * 1e3,
1764
2279
  buildStep,
1765
2280
  fetch: _nullishCoalesce(options.fetch, () => ( globalThis.fetch)),
1766
2281
  waitUntil: _nullishCoalesce(options.waitUntil, () => ( _functions.waitUntil)),
@@ -1772,8 +2287,8 @@ function normalizeOptions(options) {
1772
2287
  }
1773
2288
 
1774
2289
  // src/controller/typed-emitter.ts
1775
- var TypedEmitter = (_class5 = class {constructor() { _class5.prototype.__init12.call(this); }
1776
- __init12() {this.handlers = /* @__PURE__ */ new Map()}
2290
+ var TypedEmitter = (_class7 = class {constructor() { _class7.prototype.__init15.call(this); }
2291
+ __init15() {this.handlers = /* @__PURE__ */ new Map()}
1777
2292
  on(event, handler) {
1778
2293
  let set = this.handlers.get(event);
1779
2294
  if (!set) {
@@ -1798,36 +2313,70 @@ var TypedEmitter = (_class5 = class {constructor() { _class5.prototype.__init12.
1798
2313
  }
1799
2314
  }
1800
2315
  }
1801
- }, _class5);
2316
+ }, _class7);
1802
2317
 
1803
2318
  // src/controller/polling-source.ts
1804
- var PollingSource = class extends TypedEmitter {
2319
+ var PollingSource = (_class8 = class extends TypedEmitter {
2320
+
1805
2321
 
1806
2322
 
1807
2323
 
1808
2324
  constructor(config) {
1809
- super();
2325
+ super();_class8.prototype.__init16.call(this);;
1810
2326
  this.config = config;
1811
2327
  }
2328
+ /** Allow the scheduled poll its entire fetch deadline before revalidating. */
2329
+ get staleAfterMs() {
2330
+ return this.config.polling.intervalMs + DEFAULT_FETCH_TIMEOUT_MS;
2331
+ }
2332
+ /** Age after which reads block on a refresh. */
2333
+ get expiresAfterMs() {
2334
+ return this.staleAfterMs + this.config.staleWhileRevalidateMs;
2335
+ }
2336
+ __init16() {this.assess = ({ ageMs }) => {
2337
+ if (ageMs === Infinity) {
2338
+ return { status: "unknown" };
2339
+ }
2340
+ if (ageMs <= this.staleAfterMs) {
2341
+ return { status: "fresh" };
2342
+ }
2343
+ if (ageMs <= this.expiresAfterMs) {
2344
+ return { status: "stale" };
2345
+ }
2346
+ return { status: "expired" };
2347
+ }}
1812
2348
  /**
1813
2349
  * Perform a single poll request.
1814
- * Emits 'data' on success, 'error' on failure.
2350
+ * Updates the cache on success; emits 'error' and rejects on failure.
1815
2351
  */
1816
2352
  async poll() {
1817
- if (_optionalChain([this, 'access', _63 => _63.abortController, 'optionalAccess', _64 => _64.signal, 'access', _65 => _65.aborted])) return;
2353
+ if (this.polling) {
2354
+ debug(this.config.clientName, "poll.shared");
2355
+ return this.polling;
2356
+ }
2357
+ if (_optionalChain([this, 'access', _91 => _91.abortController, 'optionalAccess', _92 => _92.signal, 'access', _93 => _93.aborted])) return;
1818
2358
  this.abortController ??= new AbortController();
1819
2359
  const controller = this.abortController;
1820
- try {
1821
- const data = await fetchDatafile({
1822
- ...this.config,
1823
- signal: controller.signal
1824
- });
1825
- this.emit("data", data);
1826
- } catch (error) {
1827
- controller.signal.throwIfAborted();
1828
- const err = error instanceof Error ? error : new Error("Unknown poll error");
1829
- this.emit("error", err);
1830
- }
2360
+ debug(this.config.clientName, "poll.start");
2361
+ this.polling = (async () => {
2362
+ try {
2363
+ await this.config.refresh();
2364
+ controller.signal.throwIfAborted();
2365
+ debug(this.config.clientName, "poll.complete");
2366
+ } catch (error) {
2367
+ debug(
2368
+ this.config.clientName,
2369
+ controller.signal.aborted ? "poll.aborted" : "poll.failed"
2370
+ );
2371
+ controller.signal.throwIfAborted();
2372
+ const err = error instanceof Error ? error : new Error("Unknown poll error");
2373
+ this.emit("error", err);
2374
+ throw err;
2375
+ }
2376
+ })().finally(() => {
2377
+ if (this.abortController === controller) this.polling = void 0;
2378
+ });
2379
+ return this.polling;
1831
2380
  }
1832
2381
  /**
1833
2382
  * Start interval-based polling.
@@ -1836,6 +2385,11 @@ var PollingSource = class extends TypedEmitter {
1836
2385
  */
1837
2386
  startInterval() {
1838
2387
  if (this.intervalId) return;
2388
+ debug(this.config.clientName, "poll.interval.start", () => ({
2389
+ intervalMs: this.config.polling.intervalMs,
2390
+ staleAfterMs: this.config.polling.intervalMs + DEFAULT_FETCH_TIMEOUT_MS,
2391
+ expiresAfterMs: 2 * this.config.polling.intervalMs + DEFAULT_FETCH_TIMEOUT_MS
2392
+ }));
1839
2393
  this.intervalId = setInterval(
1840
2394
  () => void this.poll().catch(() => {
1841
2395
  }),
@@ -1846,14 +2400,20 @@ var PollingSource = class extends TypedEmitter {
1846
2400
  * Stop interval-based polling.
1847
2401
  */
1848
2402
  stop() {
2403
+ if (this.intervalId || this.polling) {
2404
+ debug(this.config.clientName, "poll.stop", () => ({
2405
+ pendingPoll: this.polling !== void 0
2406
+ }));
2407
+ }
1849
2408
  if (this.intervalId) {
1850
2409
  clearInterval(this.intervalId);
1851
2410
  this.intervalId = void 0;
1852
2411
  }
1853
- _optionalChain([this, 'access', _66 => _66.abortController, 'optionalAccess', _67 => _67.abort, 'call', _68 => _68()]);
2412
+ _optionalChain([this, 'access', _94 => _94.abortController, 'optionalAccess', _95 => _95.abort, 'call', _96 => _96()]);
1854
2413
  this.abortController = void 0;
2414
+ this.polling = void 0;
1855
2415
  }
1856
- };
2416
+ }, _class8);
1857
2417
 
1858
2418
  // src/utils/runtime.ts
1859
2419
  function isBun() {
@@ -1869,7 +2429,9 @@ function sleep(ms) {
1869
2429
  var MAX_RETRY_COUNT = 15;
1870
2430
  var BASE_RETRY_DELAY_MS = 1e3;
1871
2431
  var MAX_RETRY_DELAY_MS = 6e4;
1872
- var PING_TIMEOUT_MS = 9e4;
2432
+ var PING_MS = 3e4;
2433
+ var PING_TIMEOUT_MS = PING_MS * 3;
2434
+ var PING_TIMEOUT = new Error("stream: ping timeout");
1873
2435
  function backoff(retryCount) {
1874
2436
  if (retryCount === 1) return 0;
1875
2437
  const delay = Math.min(
@@ -1892,9 +2454,15 @@ var TokenResolutionError = class extends Error {
1892
2454
  };
1893
2455
  async function connectStream(config, callbacks) {
1894
2456
  const { host, abortController, fetch: fetchFn = globalThis.fetch } = config;
1895
- const { onDatafile, onPrimed, onDisconnect } = callbacks;
2457
+ const { onDatafile, onPrimed, onPing, onDisconnect, onError, onConnection } = callbacks;
1896
2458
  let retryCount = 0;
1897
2459
  let lastAttemptTime = 0;
2460
+ const reportError = (error) => {
2461
+ if (abortController.signal.aborted) return;
2462
+ _optionalChain([onError, 'optionalCall', _97 => _97(
2463
+ error instanceof Error ? error : new Error("Unknown stream error", { cause: error })
2464
+ )]);
2465
+ };
1898
2466
  let resolveInit;
1899
2467
  let rejectInit;
1900
2468
  const initPromise = new Promise((resolve, reject) => {
@@ -1906,6 +2474,9 @@ async function connectStream(config, callbacks) {
1906
2474
  let lastError;
1907
2475
  while (!abortController.signal.aborted) {
1908
2476
  if (retryCount > MAX_RETRY_COUNT) {
2477
+ debug(config.clientName, "stream.retries.exhausted", () => ({
2478
+ retryCount
2479
+ }));
1909
2480
  console.error(
1910
2481
  "@vercel/flags-core: Max retry count exceeded",
1911
2482
  _nullishCoalesce(lastError, () => ( "stream closed repeatedly without an error"))
@@ -1915,6 +2486,8 @@ async function connectStream(config, callbacks) {
1915
2486
  new Error("stream: max retry count exceeded before receiving data")
1916
2487
  );
1917
2488
  }
2489
+ reportError(new Error("stream: max retry count exceeded"));
2490
+ _optionalChain([onDisconnect, 'optionalCall', _98 => _98()]);
1918
2491
  abortController.abort();
1919
2492
  break;
1920
2493
  }
@@ -1923,20 +2496,24 @@ async function connectStream(config, callbacks) {
1923
2496
  abortController.signal.addEventListener("abort", onMainAbort, {
1924
2497
  once: true
1925
2498
  });
2499
+ _optionalChain([onConnection, 'optionalCall', _99 => _99(() => connectionAbort.abort(PING_TIMEOUT))]);
1926
2500
  let pingTimeoutId;
1927
- let responseBody;
1928
2501
  const resetPingTimeout = () => {
1929
2502
  if (pingTimeoutId !== void 0) clearTimeout(pingTimeoutId);
1930
2503
  if (!initialDataReceived) return;
1931
2504
  pingTimeoutId = setTimeout(() => {
1932
- _optionalChain([responseBody, 'optionalAccess', _69 => _69.cancel, 'call', _70 => _70(), 'access', _71 => _71.catch, 'call', _72 => _72(() => {
1933
- })]);
1934
- connectionAbort.abort();
2505
+ debug(config.clientName, "stream.ping.timeout", () => ({
2506
+ timeoutMs: PING_TIMEOUT_MS
2507
+ }));
2508
+ lastError = PING_TIMEOUT;
2509
+ connectionAbort.abort(PING_TIMEOUT);
1935
2510
  }, PING_TIMEOUT_MS);
1936
2511
  };
1937
2512
  try {
2513
+ debug(config.clientName, "stream.connect", () => ({ retryCount }));
1938
2514
  lastAttemptTime = Date.now();
1939
2515
  const token = await config.resolveToken().catch((error) => {
2516
+ debug(config.clientName, "stream.auth.failed");
1940
2517
  throw new TokenResolutionError(error);
1941
2518
  });
1942
2519
  const headers = {
@@ -1951,7 +2528,7 @@ async function connectStream(config, callbacks) {
1951
2528
  if (vercelEnv) {
1952
2529
  headers["X-Vercel-Env"] = vercelEnv;
1953
2530
  }
1954
- const revision = _optionalChain([config, 'access', _73 => _73.revision, 'optionalCall', _74 => _74()]);
2531
+ const revision = _optionalChain([config, 'access', _100 => _100.revision, 'optionalCall', _101 => _101()]);
1955
2532
  if (revision !== void 0) {
1956
2533
  headers["X-Revision"] = String(revision);
1957
2534
  }
@@ -1959,20 +2536,26 @@ async function connectStream(config, callbacks) {
1959
2536
  headers,
1960
2537
  signal: connectionAbort.signal
1961
2538
  });
1962
- if (!response.ok) {
1963
- if (response.status === 401) {
1964
- if (!initialDataReceived) {
1965
- rejectInit(new UnauthorizedError(config.sourceProjectId));
1966
- }
1967
- abortController.abort();
1968
- break;
2539
+ debug(config.clientName, "stream.response", () => ({
2540
+ status: response.status,
2541
+ revision
2542
+ }));
2543
+ if (!response.ok && response.status === 401) {
2544
+ const error = new UnauthorizedError(config.sourceProjectId);
2545
+ reportError(error);
2546
+ if (!initialDataReceived) {
2547
+ rejectInit(error);
1969
2548
  }
2549
+ _optionalChain([onDisconnect, 'optionalCall', _102 => _102()]);
2550
+ abortController.abort();
2551
+ break;
2552
+ }
2553
+ if (!response.ok) {
1970
2554
  throw new Error(`stream was not ok: ${response.status}`);
1971
2555
  }
1972
2556
  if (!response.body) {
1973
2557
  throw new Error("stream body was not present");
1974
2558
  }
1975
- responseBody = response.body;
1976
2559
  const reader = response.body.getReader();
1977
2560
  const decoder = new TextDecoder();
1978
2561
  const bufferChunks = [];
@@ -1983,10 +2566,13 @@ async function connectStream(config, callbacks) {
1983
2566
  connectionAbort.signal.addEventListener("abort", onConnectionAbort, {
1984
2567
  once: true
1985
2568
  });
2569
+ resetPingTimeout();
1986
2570
  try {
1987
2571
  while (true) {
1988
2572
  const { done, value: chunk } = await reader.read();
1989
- if (done || abortController.signal.aborted) break;
2573
+ if (done || connectionAbort.signal.aborted) {
2574
+ break;
2575
+ }
1990
2576
  bufferChunks.push(decoder.decode(chunk, { stream: true }));
1991
2577
  const combined = bufferChunks.join("");
1992
2578
  bufferChunks.length = 0;
@@ -1997,7 +2583,7 @@ async function connectStream(config, callbacks) {
1997
2583
  let message;
1998
2584
  try {
1999
2585
  message = JSON.parse(line);
2000
- } catch (e12) {
2586
+ } catch (e14) {
2001
2587
  console.warn(
2002
2588
  "@vercel/flags-core: Failed to parse stream message, skipping"
2003
2589
  );
@@ -2013,7 +2599,7 @@ async function connectStream(config, callbacks) {
2013
2599
  resetPingTimeout();
2014
2600
  }
2015
2601
  if (message.type === "primed") {
2016
- _optionalChain([onPrimed, 'optionalCall', _75 => _75(message)]);
2602
+ _optionalChain([onPrimed, 'optionalCall', _103 => _103(message)]);
2017
2603
  retryCount = 0;
2018
2604
  if (!initialDataReceived) {
2019
2605
  initialDataReceived = true;
@@ -2022,6 +2608,7 @@ async function connectStream(config, callbacks) {
2022
2608
  resetPingTimeout();
2023
2609
  }
2024
2610
  if (message.type === "ping" && initialDataReceived) {
2611
+ _optionalChain([onPing, 'optionalCall', _104 => _104()]);
2025
2612
  retryCount = 0;
2026
2613
  resetPingTimeout();
2027
2614
  }
@@ -2036,11 +2623,19 @@ async function connectStream(config, callbacks) {
2036
2623
  clearTimeout(pingTimeoutId);
2037
2624
  abortController.signal.removeEventListener("abort", onMainAbort);
2038
2625
  if (!abortController.signal.aborted) {
2039
- _optionalChain([onDisconnect, 'optionalCall', _76 => _76()]);
2626
+ if (connectionAbort.signal.reason !== PING_TIMEOUT) {
2627
+ _optionalChain([onDisconnect, 'optionalCall', _105 => _105()]);
2628
+ }
2040
2629
  retryCount++;
2041
2630
  const elapsed = Date.now() - lastAttemptTime;
2042
2631
  const minGap = Math.max(0, BASE_RETRY_DELAY_MS - elapsed);
2043
- await sleep(Math.max(backoff(retryCount), minGap));
2632
+ const delayMs = Math.max(backoff(retryCount), minGap);
2633
+ debug(config.clientName, "stream.reconnect", () => ({
2634
+ retryCount,
2635
+ delayMs,
2636
+ reason: connectionAbort.signal.reason === PING_TIMEOUT ? "ping-timeout" : "connection-closed"
2637
+ }));
2638
+ await sleep(delayMs);
2044
2639
  continue;
2045
2640
  }
2046
2641
  } catch (error) {
@@ -2049,19 +2644,28 @@ async function connectStream(config, callbacks) {
2049
2644
  if (abortController.signal.aborted) {
2050
2645
  break;
2051
2646
  }
2647
+ if (connectionAbort.signal.reason !== PING_TIMEOUT) {
2648
+ reportError(error);
2649
+ }
2052
2650
  if (error instanceof TokenResolutionError && !initialDataReceived) {
2053
2651
  rejectInit(error.cause);
2054
2652
  abortController.abort();
2055
2653
  break;
2056
2654
  }
2057
- if (!connectionAbort.signal.aborted) {
2655
+ if (connectionAbort.signal.reason !== PING_TIMEOUT) {
2058
2656
  lastError = error;
2657
+ _optionalChain([onDisconnect, 'optionalCall', _106 => _106()]);
2059
2658
  }
2060
- _optionalChain([onDisconnect, 'optionalCall', _77 => _77()]);
2061
2659
  retryCount++;
2062
2660
  const elapsed = Date.now() - lastAttemptTime;
2063
2661
  const minGap = Math.max(0, BASE_RETRY_DELAY_MS - elapsed);
2064
- await sleep(Math.max(backoff(retryCount), minGap));
2662
+ const delayMs = Math.max(backoff(retryCount), minGap);
2663
+ debug(config.clientName, "stream.reconnect", () => ({
2664
+ retryCount,
2665
+ delayMs,
2666
+ reason: connectionAbort.signal.reason === PING_TIMEOUT ? "ping-timeout" : "connection-error"
2667
+ }));
2668
+ await sleep(delayMs);
2065
2669
  }
2066
2670
  }
2067
2671
  if (!initialDataReceived) {
@@ -2072,16 +2676,105 @@ async function connectStream(config, callbacks) {
2072
2676
  }
2073
2677
 
2074
2678
  // src/controller/stream-source.ts
2075
- var StreamSource = class extends TypedEmitter {
2679
+ var STREAM_FRESH_MS = PING_MS * 2;
2680
+ var StreamSource = (_class9 = class extends TypedEmitter {
2681
+
2682
+
2076
2683
 
2077
2684
 
2078
2685
 
2686
+ __init17() {this.connectionStartedAt = 0}
2079
2687
 
2080
2688
  constructor(options, revision) {
2081
- super();
2689
+ super();_class9.prototype.__init17.call(this);_class9.prototype.__init18.call(this);;
2082
2690
  this.options = options;
2083
2691
  this.revision = revision;
2084
2692
  }
2693
+ /** Age after which reads revalidate in the background. */
2694
+ get staleAfterMs() {
2695
+ return STREAM_FRESH_MS;
2696
+ }
2697
+ /** Age after which reads block on a refresh. */
2698
+ get expiresAfterMs() {
2699
+ return STREAM_FRESH_MS + this.options.staleWhileRevalidateMs;
2700
+ }
2701
+ /** The connection loop is connecting or connected; false once it gave up or was stopped. */
2702
+ get active() {
2703
+ return this.abortController !== void 0;
2704
+ }
2705
+ /**
2706
+ * Drops a connection that has been silent beyond the fresh window and
2707
+ * resolves once the stream delivers any message, which confirms or replaces
2708
+ * the cache through the usual events. A younger connection, such as the
2709
+ * replacement a ping timeout just opened, is kept and awaited instead.
2710
+ * Rejects when the stream disconnects, gives up, is stopped, or stays silent
2711
+ * for the fetch deadline. Concurrent callers share one wait.
2712
+ */
2713
+ revalidate() {
2714
+ if (this.revalidation) {
2715
+ return this.revalidation.promise;
2716
+ }
2717
+ if (!this.active) {
2718
+ return Promise.reject(new Error("stream: not active"));
2719
+ }
2720
+ let cleanup = () => {
2721
+ };
2722
+ let fail = (_error) => {
2723
+ };
2724
+ const promise = new Promise((resolve, reject) => {
2725
+ const confirmed = () => {
2726
+ cleanup();
2727
+ resolve();
2728
+ };
2729
+ fail = (error) => {
2730
+ cleanup();
2731
+ reject(error);
2732
+ };
2733
+ const interrupted = () => {
2734
+ fail(new Error("stream: revalidation interrupted"));
2735
+ };
2736
+ const timer = setTimeout(() => {
2737
+ fail(new Error("stream: revalidation timed out"));
2738
+ }, DEFAULT_FETCH_TIMEOUT_MS);
2739
+ cleanup = () => {
2740
+ clearTimeout(timer);
2741
+ this.off("data", confirmed);
2742
+ this.off("primed", confirmed);
2743
+ this.off("ping", confirmed);
2744
+ this.off("disconnected", interrupted);
2745
+ this.off("exhausted", interrupted);
2746
+ };
2747
+ this.on("data", confirmed);
2748
+ this.on("primed", confirmed);
2749
+ this.on("ping", confirmed);
2750
+ this.on("disconnected", interrupted);
2751
+ this.on("exhausted", interrupted);
2752
+ });
2753
+ const revalidation = { promise, fail };
2754
+ this.revalidation = revalidation;
2755
+ void promise.catch(() => {
2756
+ }).finally(() => {
2757
+ if (this.revalidation === revalidation) {
2758
+ this.revalidation = void 0;
2759
+ }
2760
+ });
2761
+ if (Date.now() - this.connectionStartedAt >= STREAM_FRESH_MS) {
2762
+ _optionalChain([this, 'access', _107 => _107.reconnect, 'optionalCall', _108 => _108()]);
2763
+ }
2764
+ return promise;
2765
+ }
2766
+ __init18() {this.assess = ({ ageMs }) => {
2767
+ if (ageMs === Infinity) {
2768
+ return { status: "unknown" };
2769
+ }
2770
+ if (ageMs <= this.staleAfterMs) {
2771
+ return { status: "fresh" };
2772
+ }
2773
+ if (ageMs <= this.expiresAfterMs) {
2774
+ return { status: "stale" };
2775
+ }
2776
+ return { status: "expired" };
2777
+ }}
2085
2778
  /**
2086
2779
  * Start the stream connection.
2087
2780
  * Returns a promise that resolves when the first datafile or primed message arrives.
@@ -2089,6 +2782,11 @@ var StreamSource = class extends TypedEmitter {
2089
2782
  */
2090
2783
  start() {
2091
2784
  if (this.promise) return this.promise;
2785
+ debug(this.options.clientName, "stream.start", () => ({
2786
+ revision: this.revision(),
2787
+ staleAfterMs: this.staleAfterMs,
2788
+ expiresAfterMs: this.expiresAfterMs
2789
+ }));
2092
2790
  const abortController = new AbortController();
2093
2791
  this.abortController = abortController;
2094
2792
  abortController.signal.addEventListener(
@@ -2097,6 +2795,7 @@ var StreamSource = class extends TypedEmitter {
2097
2795
  if (this.abortController === abortController) {
2098
2796
  this.promise = void 0;
2099
2797
  this.abortController = void 0;
2798
+ this.emit("exhausted");
2100
2799
  }
2101
2800
  },
2102
2801
  { once: true }
@@ -2105,6 +2804,7 @@ var StreamSource = class extends TypedEmitter {
2105
2804
  const promise = connectStream(
2106
2805
  {
2107
2806
  host: this.options.host,
2807
+ clientName: this.options.clientName,
2108
2808
  resolveToken: () => this.options.auth.resolveToken(),
2109
2809
  sourceProjectId: this.options.auth.sourceProjectId,
2110
2810
  abortController,
@@ -2120,8 +2820,16 @@ var StreamSource = class extends TypedEmitter {
2120
2820
  this.emit("primed", message);
2121
2821
  this.emit("connected");
2122
2822
  },
2823
+ onPing: () => this.emit("ping"),
2123
2824
  onDisconnect: () => {
2124
2825
  this.emit("disconnected");
2826
+ },
2827
+ onError: (error) => {
2828
+ this.emit("error", error);
2829
+ },
2830
+ onConnection: (reconnect) => {
2831
+ this.reconnect = reconnect;
2832
+ this.connectionStartedAt = Date.now();
2125
2833
  }
2126
2834
  }
2127
2835
  );
@@ -2137,111 +2845,167 @@ var StreamSource = class extends TypedEmitter {
2137
2845
  * Stop the stream connection.
2138
2846
  */
2139
2847
  stop() {
2140
- _optionalChain([this, 'access', _78 => _78.abortController, 'optionalAccess', _79 => _79.abort, 'call', _80 => _80()]);
2848
+ if (this.abortController) {
2849
+ debug(this.options.clientName, "stream.stop");
2850
+ }
2851
+ const abortController = this.abortController;
2141
2852
  this.abortController = void 0;
2142
2853
  this.promise = void 0;
2854
+ this.reconnect = void 0;
2855
+ _optionalChain([this, 'access', _109 => _109.revalidation, 'optionalAccess', _110 => _110.fail, 'call', _111 => _111(new Error("stream: stopped"))]);
2856
+ _optionalChain([abortController, 'optionalAccess', _112 => _112.abort, 'call', _113 => _113()]);
2143
2857
  }
2144
- };
2145
-
2146
- // src/controller/tagged-data.ts
2147
- function tagData(data, origin) {
2148
- return Object.assign(data, { _origin: origin });
2149
- }
2150
- function originToMetricsSource(origin) {
2151
- switch (origin) {
2152
- case "stream":
2153
- case "poll":
2154
- case "provided":
2155
- return "in-memory";
2156
- case "fetched":
2157
- return "remote";
2158
- case "bundled":
2159
- return "embedded";
2160
- }
2161
- }
2858
+ }, _class9);
2162
2859
 
2163
2860
  // src/controller/index.ts
2164
- function parseConfigUpdatedAt(value) {
2165
- if (typeof value === "number") return value;
2166
- if (typeof value === "string") {
2167
- const parsed = Number(value);
2168
- return Number.isNaN(parsed) ? void 0 : parsed;
2861
+ var UNKNOWN_FRESHNESS = {
2862
+ assess: () => ({ status: "unknown" })
2863
+ };
2864
+ function isUnauthorizedError(error) {
2865
+ if (error instanceof UnauthorizedError) {
2866
+ return true;
2169
2867
  }
2170
- return void 0;
2868
+ if (!(error instanceof Error)) {
2869
+ return false;
2870
+ }
2871
+ return error.message.includes("401") || "status" in error && error.status === 401;
2171
2872
  }
2172
- var Controller = (_class6 = class {
2873
+ var Controller = (_class10 = class {
2173
2874
 
2174
2875
  // State machine
2175
- __init13() {this.state = "idle"}
2876
+ __init19() {this.state = "idle"}
2176
2877
  // Data state — tagged with origin
2177
2878
 
2178
2879
  // Memoized data spread for read() / getDatafile().
2179
- // Rebuilt only when `this.data` reference changes (e.g. on stream/poll update).
2880
+ // Rebuilt only when the cached data reference changes (e.g. on stream/poll update).
2180
2881
  // Holds the result of stripping `_origin`; metrics are appended per-call.
2181
- __init14() {this.dataViewSource = void 0}
2182
- __init15() {this.dataViewBase = void 0}
2882
+ __init20() {this.dataViewSource = void 0}
2883
+ __init21() {this.dataViewBase = void 0}
2183
2884
  // Sources (I/O delegates)
2184
2885
 
2185
2886
 
2186
2887
 
2888
+
2889
+
2890
+ // Read policy while no live source confirms the cache; derived once from the options.
2891
+
2187
2892
  // Usage tracking
2188
2893
 
2189
- __init16() {this.isFirstGetData = true}
2894
+ __init22() {this.isFirstGetData = true}
2190
2895
  // Build-step deduplication
2191
- __init17() {this.buildDataPromise = null}
2192
- __init18() {this.buildReadTracked = false}
2896
+ __init23() {this.buildDataPromise = null}
2897
+ __init24() {this.buildReadTracked = false}
2193
2898
  // Suppresses usage tracking while the credential is rejected. Set on a 401
2194
2899
  // from any source, cleared as soon as a source delivers data again.
2195
- __init19() {this.unauthorized = false}
2196
- constructor(options) {;_class6.prototype.__init13.call(this);_class6.prototype.__init14.call(this);_class6.prototype.__init15.call(this);_class6.prototype.__init16.call(this);_class6.prototype.__init17.call(this);_class6.prototype.__init18.call(this);_class6.prototype.__init19.call(this);_class6.prototype.__init20.call(this);_class6.prototype.__init21.call(this);_class6.prototype.__init22.call(this);_class6.prototype.__init23.call(this);_class6.prototype.__init24.call(this);_class6.prototype.__init25.call(this);
2900
+ __init25() {this.unauthorized = false}
2901
+ constructor(options) {;_class10.prototype.__init19.call(this);_class10.prototype.__init20.call(this);_class10.prototype.__init21.call(this);_class10.prototype.__init22.call(this);_class10.prototype.__init23.call(this);_class10.prototype.__init24.call(this);_class10.prototype.__init25.call(this);_class10.prototype.__init26.call(this);_class10.prototype.__init27.call(this);_class10.prototype.__init28.call(this);_class10.prototype.__init29.call(this);_class10.prototype.__init30.call(this);_class10.prototype.__init31.call(this);_class10.prototype.__init32.call(this);_class10.prototype.__init33.call(this);_class10.prototype.__init34.call(this);
2197
2902
  this.options = normalizeOptions(options);
2903
+ this.cache = new DatafileCache(
2904
+ async (signal) => {
2905
+ try {
2906
+ const data = await fetchDatafile({ ...this.options, signal });
2907
+ signal.throwIfAborted();
2908
+ this.unauthorized = false;
2909
+ return data;
2910
+ } catch (error) {
2911
+ signal.throwIfAborted();
2912
+ this.noteUnauthorized(error);
2913
+ throw error;
2914
+ }
2915
+ },
2916
+ this.options.staleIfErrorMs,
2917
+ this.options.waitUntil,
2918
+ this.options.clientName
2919
+ );
2198
2920
  this.streamSource = new StreamSource(
2199
2921
  this.options,
2200
- () => _optionalChain([this, 'access', _81 => _81.data, 'optionalAccess', _82 => _82.revision])
2922
+ () => this.cache.revision
2201
2923
  );
2202
- this.pollingSource = new PollingSource(this.options);
2924
+ this.pollingSource = new PollingSource({
2925
+ clientName: this.options.clientName,
2926
+ polling: this.options.polling,
2927
+ staleWhileRevalidateMs: this.options.staleWhileRevalidateMs,
2928
+ refresh: () => this.cache.refresh("poll")
2929
+ });
2930
+ this.headerSource = new HeaderSource(this.options);
2931
+ this.degradedReadPolicy = this.createDegradedReadPolicy();
2203
2932
  this.bundledSource = new BundledSource({
2204
2933
  auth: this.options.auth,
2205
2934
  readBundledDefinitions
2206
2935
  });
2207
2936
  this.wireSourceEvents();
2208
2937
  if (this.options.datafile) {
2209
- this.data = tagData(this.options.datafile, "provided");
2938
+ this.cache.seed(tagData({ ...this.options.datafile }, "provided"));
2210
2939
  }
2211
2940
  this.usageTracker = new UsageTracker(this.options);
2941
+ debug(this.options.clientName, "client.created", () => ({
2942
+ ...this.debugState(),
2943
+ buildStep: this.options.buildStep,
2944
+ vercel: this.options.vercel,
2945
+ stream: this.options.stream.enabled,
2946
+ streamInitTimeoutMs: this.options.stream.initTimeoutMs,
2947
+ polling: this.options.polling.enabled,
2948
+ pollingIntervalMs: this.options.polling.intervalMs,
2949
+ pollingInitTimeoutMs: this.options.polling.initTimeoutMs,
2950
+ staleWhileRevalidateMs: this.options.staleWhileRevalidateMs,
2951
+ staleIfErrorMs: this.options.staleIfErrorMs
2952
+ }));
2212
2953
  }
2213
2954
  // Source event handlers (stored for cleanup)
2214
- __init20() {this.onStreamData = (data) => {
2955
+ __init26() {this.onStreamData = (data) => {
2956
+ debug(this.options.clientName, "stream.data", () => ({
2957
+ projectId: data.projectId,
2958
+ revision: data.revision,
2959
+ configUpdatedAt: Number(data.configUpdatedAt)
2960
+ }));
2215
2961
  this.unauthorized = false;
2216
- if (this.isNewerData(data)) {
2217
- this.data = tagData(data, "stream");
2218
- }
2962
+ this.cache.updateFromSource(data, "stream");
2219
2963
  }}
2220
- __init21() {this.onStreamPrimed = () => {
2964
+ __init27() {this.onStreamPrimed = (message) => {
2221
2965
  this.unauthorized = false;
2966
+ const confirmed = this.cache.tryConfirm(message, "revision", "stream");
2967
+ debug(this.options.clientName, "stream.primed", () => ({
2968
+ ...this.debugState(),
2969
+ incomingRevision: message.revision,
2970
+ confirmed
2971
+ }));
2222
2972
  if (this.state === "degraded" || this.state === "initializing:stream") {
2223
- this.transition("streaming");
2973
+ this.transition("streaming", "stream-primed");
2224
2974
  }
2225
2975
  }}
2226
- __init22() {this.onStreamConnected = () => {
2976
+ __init28() {this.onStreamPing = () => {
2977
+ debug(this.options.clientName, "stream.ping", this.debugState);
2978
+ this.cache.confirm("stream");
2979
+ }}
2980
+ __init29() {this.onStreamConnected = () => {
2227
2981
  if (this.state === "degraded" || this.state === "initializing:stream") {
2228
- this.transition("streaming");
2982
+ this.transition("streaming", "stream-connected");
2229
2983
  }
2230
2984
  }}
2231
- __init23() {this.onStreamDisconnected = () => {
2985
+ __init30() {this.onStreamDisconnected = () => {
2986
+ debug(this.options.clientName, "stream.disconnected", this.debugState);
2987
+ this.cache.fail(new Error("stream: disconnected"));
2232
2988
  if (this.state === "streaming") {
2233
- this.transition("degraded");
2989
+ this.transition("degraded", "stream-disconnected");
2234
2990
  }
2235
2991
  }}
2236
- __init24() {this.onPollData = (data) => {
2237
- this.unauthorized = false;
2238
- if (this.isNewerData(data)) {
2239
- this.data = tagData(data, "poll");
2992
+ __init31() {this.onStreamExhausted = () => {
2993
+ if (this.state !== "streaming" && this.state !== "degraded") {
2994
+ return;
2240
2995
  }
2996
+ void this.activateFallbackSource().catch(() => {
2997
+ });
2241
2998
  }}
2242
- __init25() {this.onPollError = (error) => {
2999
+ __init32() {this.onSourceError = (error) => {
2243
3000
  this.noteUnauthorized(error);
2244
- console.error("@vercel/flags-core: Poll failed:", error);
3001
+ this.cache.fail(error);
3002
+ }}
3003
+ __init33() {this.onPollingError = (error) => {
3004
+ this.onSourceError(error);
3005
+ if (this.state === "polling" && isUnauthorizedError(error)) {
3006
+ this.pollingSource.stop();
3007
+ this.transition("degraded", "polling-unauthorized");
3008
+ }
2245
3009
  }}
2246
3010
  // ---------------------------------------------------------------------------
2247
3011
  // Source event wiring
@@ -2249,24 +3013,49 @@ var Controller = (_class6 = class {
2249
3013
  wireSourceEvents() {
2250
3014
  this.streamSource.on("data", this.onStreamData);
2251
3015
  this.streamSource.on("primed", this.onStreamPrimed);
3016
+ this.streamSource.on("ping", this.onStreamPing);
2252
3017
  this.streamSource.on("connected", this.onStreamConnected);
2253
3018
  this.streamSource.on("disconnected", this.onStreamDisconnected);
2254
- this.pollingSource.on("data", this.onPollData);
2255
- this.pollingSource.on("error", this.onPollError);
3019
+ this.streamSource.on("exhausted", this.onStreamExhausted);
3020
+ this.streamSource.on("error", this.onSourceError);
3021
+ this.pollingSource.on("error", this.onPollingError);
2256
3022
  }
2257
3023
  unwireSourceEvents() {
2258
3024
  this.streamSource.off("data", this.onStreamData);
2259
3025
  this.streamSource.off("primed", this.onStreamPrimed);
3026
+ this.streamSource.off("ping", this.onStreamPing);
2260
3027
  this.streamSource.off("connected", this.onStreamConnected);
2261
3028
  this.streamSource.off("disconnected", this.onStreamDisconnected);
2262
- this.pollingSource.off("data", this.onPollData);
2263
- this.pollingSource.off("error", this.onPollError);
3029
+ this.streamSource.off("exhausted", this.onStreamExhausted);
3030
+ this.streamSource.off("error", this.onSourceError);
3031
+ this.pollingSource.off("error", this.onPollingError);
2264
3032
  }
2265
3033
  // ---------------------------------------------------------------------------
2266
3034
  // State machine
2267
3035
  // ---------------------------------------------------------------------------
2268
- transition(to) {
3036
+ __init34() {this.debugState = () => ({
3037
+ state: this.state,
3038
+ mode: this.mode,
3039
+ projectId: _optionalChain([this, 'access', _117 => _117.cache, 'access', _118 => _118.metadata, 'optionalAccess', _119 => _119.projectId]),
3040
+ hasData: this.cache.hasData,
3041
+ revision: this.cache.revision,
3042
+ ageMs: this.cache.ageMs,
3043
+ unauthorized: this.unauthorized
3044
+ })}
3045
+ transition(to, reason) {
3046
+ const from = this.state;
2269
3047
  this.state = to;
3048
+ if (from !== to) {
3049
+ debug(this.options.clientName, "client.state", () => ({
3050
+ ...this.debugState(),
3051
+ from,
3052
+ to,
3053
+ reason
3054
+ }));
3055
+ }
3056
+ }
3057
+ get isShutdown() {
3058
+ return this.state === "shutdown";
2270
3059
  }
2271
3060
  get isConnected() {
2272
3061
  return this.state === "streaming";
@@ -2278,6 +3067,8 @@ var Controller = (_class6 = class {
2278
3067
  return "streaming";
2279
3068
  case "polling":
2280
3069
  return "polling";
3070
+ case "vercel":
3071
+ return "vercel";
2281
3072
  default:
2282
3073
  return "offline";
2283
3074
  }
@@ -2291,193 +3082,344 @@ var Controller = (_class6 = class {
2291
3082
  * Build step: datafile → bundled → one-time fetch
2292
3083
  * Streaming mode: stream → datafile → bundled
2293
3084
  * Polling mode (no stream): poll → datafile → bundled
3085
+ * Vercel mode: datafile → bundled; fetch only on a read
2294
3086
  * Offline mode (neither): datafile → bundled → one-time fetch
2295
3087
  */
2296
3088
  async initialize() {
3089
+ debug(this.options.clientName, "client.initialize", this.debugState);
2297
3090
  if (this.options.buildStep) {
2298
- this.transition("build:loading");
3091
+ this.transition("build:loading", "build-initialize");
2299
3092
  await this.initializeForBuildStep();
2300
- this.transition("build:ready");
3093
+ this.transition("build:ready", "build-data-ready");
2301
3094
  return;
2302
3095
  }
2303
- if (!this.data && this.options.datafile) {
2304
- this.data = tagData(this.options.datafile, "provided");
3096
+ if (this.isShutdown) {
3097
+ this.wireSourceEvents();
3098
+ this.transition("idle", "restart-after-shutdown");
2305
3099
  }
2306
- if (!this.data) {
3100
+ if (!this.cache.hasData && this.options.datafile) {
3101
+ this.cache.seed(tagData({ ...this.options.datafile }, "provided"));
3102
+ }
3103
+ if (!this.cache.hasData) {
2307
3104
  try {
2308
3105
  const bundled = await this.bundledSource.tryLoad();
2309
- if (bundled) {
2310
- this.data = tagData(bundled, "bundled");
2311
- }
2312
- } catch (e13) {
3106
+ if (bundled) this.cache.seed(tagData({ ...bundled }, "bundled"));
3107
+ } catch (e15) {
2313
3108
  }
2314
3109
  }
2315
- if (this.data) {
2316
- if (this.options.stream.enabled) {
2317
- this.transition("initializing:stream");
2318
- await this.tryInitializeStream();
2319
- } else if (this.options.polling.enabled) {
2320
- this.transition("initializing:polling");
2321
- await this.tryInitializePolling();
2322
- } else {
2323
- this.transition("degraded");
2324
- }
3110
+ if (this.headerSource.isEnabled()) {
3111
+ this.transition("vercel", "header-mode-enabled");
2325
3112
  return;
2326
3113
  }
2327
- if (this.options.stream.enabled) {
2328
- this.transition("initializing:stream");
2329
- const streamSuccess = await this.tryInitializeStream();
2330
- if (streamSuccess) {
2331
- this.transition("streaming");
2332
- return;
2333
- }
2334
- } else if (this.options.polling.enabled) {
2335
- this.transition("initializing:polling");
2336
- const pollingSuccess = await this.tryInitializePolling();
2337
- if (pollingSuccess) {
2338
- this.transition("polling");
2339
- return;
2340
- }
3114
+ if (!this.options.stream.enabled && !this.options.polling.enabled) {
3115
+ await this.initializeFromFallbacks();
3116
+ return;
3117
+ }
3118
+ await this.activateFallbackSource();
3119
+ if (this.cache.hasData) return;
3120
+ if (this.unauthorized) {
3121
+ throw this.noDefinitionsError(
3122
+ ". Provide a datafile or bundled definitions."
3123
+ );
3124
+ }
3125
+ const fetched = await this.cache.resolve(this.cacheReadPolicy);
3126
+ if (!fetched.data) {
3127
+ await this.initializeFromFallbacks();
2341
3128
  }
2342
- await this.initializeFromFallbacks();
2343
3129
  }
2344
3130
  /**
2345
3131
  * Reads the current datafile with metrics.
2346
3132
  */
2347
3133
  async read() {
2348
3134
  const startTime = Date.now();
2349
- const cacheHadDefinitions = this.data !== void 0;
3135
+ const cacheHadDefinitions = this.cache.hasData;
2350
3136
  const isFirstRead = this.isFirstGetData;
2351
3137
  this.isFirstGetData = false;
2352
- const [result, cacheStatus] = await this.resolveData();
2353
- const readMs = Date.now() - startTime;
2354
- const source = originToMetricsSource(result._origin);
2355
- this.trackRead(startTime, cacheHadDefinitions, isFirstRead, source);
2356
- if (this.dataViewSource !== result) {
2357
- const { _origin, ...rest } = result;
2358
- this.dataViewBase = rest;
2359
- this.dataViewSource = result;
2360
- }
2361
- return {
2362
- ...this.dataViewBase,
2363
- metrics: {
2364
- readMs,
2365
- source,
2366
- cacheStatus,
2367
- connectionState: this.isConnected ? "connected" : "disconnected",
2368
- mode: this.mode
2369
- }
2370
- };
3138
+ debug(this.options.clientName, "client.read.start", this.debugState);
3139
+ const [result, cacheStatus] = await this.resolveData().catch((error) => {
3140
+ debug(this.options.clientName, "client.read.failed", this.debugState);
3141
+ throw error;
3142
+ });
3143
+ const datafile = this.toDatafile(result, cacheStatus, startTime);
3144
+ debug(this.options.clientName, "client.read", () => ({
3145
+ ...this.debugState(),
3146
+ ...datafile.metrics
3147
+ }));
3148
+ this.trackRead(startTime, cacheHadDefinitions, isFirstRead, datafile);
3149
+ return datafile;
2371
3150
  }
2372
3151
  /**
2373
3152
  * Shuts down the data source and releases resources.
2374
3153
  */
2375
3154
  async shutdown() {
3155
+ debug(this.options.clientName, "client.shutdown.start", this.debugState);
2376
3156
  this.unwireSourceEvents();
2377
3157
  this.streamSource.stop();
2378
3158
  this.pollingSource.stop();
2379
- this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
2380
- this.transition("shutdown");
3159
+ this.headerSource.stop();
3160
+ this.cache.clear();
3161
+ if (this.options.datafile) {
3162
+ this.cache.seed(tagData({ ...this.options.datafile }, "provided"));
3163
+ }
3164
+ this.transition("shutdown", "shutdown");
2381
3165
  await this.usageTracker.shutdown();
3166
+ debug(this.options.clientName, "client.shutdown.complete", this.debugState);
2382
3167
  }
2383
3168
  /**
2384
- * Returns the datafile with metrics.
2385
- * Uses in-memory data if available, otherwise falls back to bundled,
2386
- * then to a one-time fetch if called without prior initialization.
3169
+ * Returns the datafile with metrics as a snapshot: it never starts streaming
3170
+ * or polling. Cached data is served through the active source policy
3171
+ * (header checks and HTTP revalidation); an empty cache loads bundled
3172
+ * definitions, then performs a one-time fetch.
2387
3173
  */
2388
3174
  async getDatafile() {
3175
+ debug(this.options.clientName, "client.getDatafile.start", this.debugState);
2389
3176
  const startTime = Date.now();
2390
3177
  this.isFirstGetData = false;
2391
- let result;
2392
- let cacheStatus;
2393
- if (this.options.buildStep) {
2394
- [result, cacheStatus] = await this.resolveDataForBuildStep();
2395
- } else if (this.data) {
2396
- cacheStatus = this.isConnected ? "HIT" : "STALE";
2397
- result = this.data;
2398
- } else {
2399
- const bundled = await this.bundledSource.tryLoad();
2400
- if (bundled) {
2401
- this.data = tagData(bundled, "bundled");
2402
- result = this.data;
2403
- cacheStatus = "MISS";
2404
- } else {
2405
- try {
2406
- const fetched = await fetchDatafile({
2407
- host: this.options.host,
2408
- auth: this.options.auth,
2409
- fetch: this.options.fetch
2410
- });
2411
- this.data = tagData(fetched, "fetched");
2412
- result = this.data;
2413
- cacheStatus = "MISS";
2414
- } catch (error) {
2415
- this.noteUnauthorized(error);
2416
- throw this.noDefinitionsError(
2417
- ". Initialize the client or provide a datafile."
2418
- );
2419
- }
3178
+ const [result, cacheStatus] = await this.resolveSnapshot().catch(
3179
+ (error) => {
3180
+ debug(
3181
+ this.options.clientName,
3182
+ "client.getDatafile.failed",
3183
+ this.debugState
3184
+ );
3185
+ throw error;
2420
3186
  }
2421
- }
2422
- const source = originToMetricsSource(result._origin);
3187
+ );
3188
+ const datafile = this.toDatafile(result, cacheStatus, startTime);
3189
+ debug(this.options.clientName, "client.getDatafile", () => ({
3190
+ ...this.debugState(),
3191
+ cacheStatus,
3192
+ origin: result._origin
3193
+ }));
3194
+ return datafile;
3195
+ }
3196
+ /**
3197
+ * Returns the bundled fallback datafile.
3198
+ */
3199
+ async getFallbackDatafile() {
3200
+ return this.bundledSource.getRaw();
3201
+ }
3202
+ // ---------------------------------------------------------------------------
3203
+ // Data resolution
3204
+ // ---------------------------------------------------------------------------
3205
+ toDatafile(result, cacheStatus, startTime) {
2423
3206
  if (this.dataViewSource !== result) {
2424
3207
  const { _origin, ...rest } = result;
2425
3208
  this.dataViewBase = rest;
2426
3209
  this.dataViewSource = result;
2427
3210
  }
3211
+ debug(this.options.clientName, "client.getDatafile", () => ({
3212
+ ...this.debugState(),
3213
+ cacheStatus,
3214
+ origin: result._origin
3215
+ }));
2428
3216
  return {
2429
3217
  ...this.dataViewBase,
2430
3218
  metrics: {
2431
3219
  readMs: Date.now() - startTime,
2432
- source,
3220
+ source: originToMetricsSource(result._origin),
2433
3221
  cacheStatus,
2434
3222
  connectionState: this.isConnected ? "connected" : "disconnected",
2435
3223
  mode: this.mode
2436
3224
  }
2437
3225
  };
2438
3226
  }
2439
- /**
2440
- * Returns the bundled fallback datafile.
2441
- */
2442
- async getFallbackDatafile() {
2443
- return this.bundledSource.getRaw();
2444
- }
2445
- // ---------------------------------------------------------------------------
2446
- // Data resolution (shared by read() and getDatafile())
2447
- // ---------------------------------------------------------------------------
2448
3227
  /**
2449
3228
  * Resolves the current data, using the appropriate strategy for the
2450
3229
  * current mode. Returns tagged data and cache status.
2451
3230
  *
2452
3231
  * Build step: cached → bundled → one-time fetch
2453
- * Runtime with cache: return cached data
2454
- * Runtime without cache: stream/poll → datafile → bundled → fetch → throw
3232
+ * Runtime: source policy chooses cached data or refresh; fall back if empty.
2455
3233
  */
2456
3234
  async resolveData() {
2457
3235
  if (this.options.buildStep) {
2458
3236
  return this.resolveDataForBuildStep();
2459
3237
  }
2460
- if (this.data) {
2461
- const cacheStatus = this.isConnected ? "HIT" : "STALE";
2462
- return [this.data, cacheStatus];
3238
+ return this.resolveRuntimeData();
3239
+ }
3240
+ /** Initializes the active runtime source, then resolves through its policy. */
3241
+ async resolveRuntimeData() {
3242
+ if (this.sourceStartup) {
3243
+ debug(this.options.clientName, "source.startup.shared", this.debugState);
3244
+ await this.sourceStartup;
3245
+ }
3246
+ if (!this.cache.hasData && !this.options.stream.enabled && !this.options.polling.enabled) {
3247
+ return this.resolveStaticFallbackData();
3248
+ }
3249
+ const result = await this.cache.resolve(this.cacheReadPolicy);
3250
+ if (result.hasError || !result.data) {
3251
+ debug(this.options.clientName, "header.fallback", () => ({
3252
+ ...this.debugState(),
3253
+ reason: "source-assessment-error"
3254
+ }));
3255
+ await this.activateFallbackSource();
3256
+ return this.resolveRuntimeData();
2463
3257
  }
2464
- return this.resolveDataWithFallbacks();
3258
+ return [result.data, result.status];
3259
+ }
3260
+ /**
3261
+ * Resolves data for getDatafile() without starting a runtime source.
3262
+ * Build step: cached → bundled → one-time fetch
3263
+ * Runtime: cached data through the source policy; otherwise bundled → one-time fetch → throw
3264
+ */
3265
+ async resolveSnapshot() {
3266
+ if (this.options.buildStep) {
3267
+ return this.resolveDataForBuildStep();
3268
+ }
3269
+ if (this.sourceStartup) {
3270
+ await this.sourceStartup.catch(() => {
3271
+ });
3272
+ }
3273
+ if (this.state === "idle" && this.headerSource.isEnabled()) {
3274
+ this.transition("vercel", "header-mode-enabled");
3275
+ }
3276
+ if (!this.cache.hasData) {
3277
+ const bundled = await this.bundledSource.tryLoad();
3278
+ if (bundled) {
3279
+ this.cache.seed(tagData({ ...bundled }, "bundled"));
3280
+ }
3281
+ }
3282
+ if (this.cache.hasData || this.state === "vercel") {
3283
+ const result = await this.cache.resolve(this.cacheReadPolicy);
3284
+ const cached = _nullishCoalesce(result.data, () => ( this.cache.read()));
3285
+ if (cached) {
3286
+ return [cached, result.status];
3287
+ }
3288
+ }
3289
+ try {
3290
+ await this.cache.refresh();
3291
+ } catch (e16) {
3292
+ throw this.noDefinitionsError(
3293
+ ". Initialize the client or provide a datafile."
3294
+ );
3295
+ }
3296
+ const fetched = this.cache.read();
3297
+ if (!fetched) {
3298
+ throw this.noDefinitionsError(
3299
+ ". Initialize the client or provide a datafile."
3300
+ );
3301
+ }
3302
+ return [fetched, "MISS"];
3303
+ }
3304
+ get cacheReadPolicy() {
3305
+ if (this.state === "vercel") {
3306
+ return {
3307
+ assess: this.headerSource.getAssessment(),
3308
+ retryOnFailure: true
3309
+ };
3310
+ }
3311
+ if (this.state === "streaming") {
3312
+ return {
3313
+ assess: this.streamSource.assess,
3314
+ revalidate: () => this.streamSource.revalidate()
3315
+ };
3316
+ }
3317
+ if (this.state === "polling") {
3318
+ return { assess: this.pollingSource.assess };
3319
+ }
3320
+ if (this.state === "degraded") {
3321
+ return this.degradedReadPolicy;
3322
+ }
3323
+ return UNKNOWN_FRESHNESS;
3324
+ }
3325
+ /**
3326
+ * Without a live source nothing confirms the cache, so reads apply plain
3327
+ * stale-while-revalidate over HTTP from the two public windows: data
3328
+ * refreshed within `staleWhileRevalidateMs` is served as is, older data is
3329
+ * served while refreshing in the background, and data older than that
3330
+ * window plus `staleIfErrorMs` is expired and waits for the refresh. An
3331
+ * unknown age is not known to be old, so it refreshes in the background.
3332
+ * Offline clients never refresh.
3333
+ */
3334
+ createDegradedReadPolicy() {
3335
+ if (!this.options.stream.enabled && !this.options.polling.enabled) {
3336
+ return UNKNOWN_FRESHNESS;
3337
+ }
3338
+ const { staleWhileRevalidateMs, staleIfErrorMs } = this.options;
3339
+ const expiresAfterMs = staleWhileRevalidateMs + staleIfErrorMs;
3340
+ return {
3341
+ assess: ({ ageMs }) => {
3342
+ if (ageMs <= staleWhileRevalidateMs) {
3343
+ return { status: "fresh" };
3344
+ }
3345
+ if (ageMs === Infinity || ageMs <= expiresAfterMs) {
3346
+ return { status: "stale" };
3347
+ }
3348
+ return { status: "expired" };
3349
+ },
3350
+ retryOnFailure: true
3351
+ };
3352
+ }
3353
+ /**
3354
+ * Advances through the runtime source chain. Every caller uses the same path:
3355
+ * request headers → stream → polling → direct cache refresh.
3356
+ */
3357
+ activateFallbackSource() {
3358
+ if (this.sourceStartup) {
3359
+ debug(this.options.clientName, "source.startup.shared", this.debugState);
3360
+ return this.sourceStartup;
3361
+ }
3362
+ debug(this.options.clientName, "source.startup", this.debugState);
3363
+ const startup = this.startFallbackSource().finally(() => {
3364
+ if (this.sourceStartup === startup) {
3365
+ this.sourceStartup = void 0;
3366
+ }
3367
+ debug(this.options.clientName, "source.startup.settled", this.debugState);
3368
+ });
3369
+ this.sourceStartup = startup;
3370
+ return startup;
3371
+ }
3372
+ async startFallbackSource() {
3373
+ if (this.isShutdown) {
3374
+ throw new Error("@vercel/flags-core: Client is shut down");
3375
+ }
3376
+ if ((this.state === "idle" || this.state === "vercel") && this.options.stream.enabled) {
3377
+ this.transition("initializing:stream", "start-stream");
3378
+ const outcome = await this.tryInitializeStream();
3379
+ if (this.isShutdown) {
3380
+ throw new Error("@vercel/flags-core: Client is shut down");
3381
+ }
3382
+ if (outcome === "connected" || this.isConnected) {
3383
+ this.transition("streaming", "stream-initialized");
3384
+ return;
3385
+ }
3386
+ if (outcome === "connecting" && this.streamSource.active) {
3387
+ this.transition("degraded", "stream-still-connecting");
3388
+ return;
3389
+ }
3390
+ }
3391
+ if (this.options.polling.enabled) {
3392
+ if (this.state !== "polling") {
3393
+ this.transition("polling", "start-polling");
3394
+ this.pollingSource.startInterval();
3395
+ }
3396
+ await this.initializePolling();
3397
+ if (this.isShutdown) {
3398
+ throw new Error("@vercel/flags-core: Client is shut down");
3399
+ }
3400
+ return;
3401
+ }
3402
+ this.transition("degraded", "no-active-update-source");
2465
3403
  }
2466
3404
  // ---------------------------------------------------------------------------
2467
3405
  // Stream initialization
2468
3406
  // ---------------------------------------------------------------------------
2469
3407
  /**
2470
- * Attempts to initialize via stream with timeout.
2471
- * Returns true if stream connected successfully within timeout.
3408
+ * Attempts to initialize via stream with timeout. A timeout leaves the
3409
+ * stream connecting in the background; a rejection means it gave up.
2472
3410
  */
2473
3411
  async tryInitializeStream() {
3412
+ debug(this.options.clientName, "stream.initialize", () => ({
3413
+ timeoutMs: this.options.stream.initTimeoutMs,
3414
+ ...this.debugState()
3415
+ }));
2474
3416
  if (this.options.stream.initTimeoutMs <= 0) {
2475
3417
  try {
2476
3418
  await this.streamSource.start();
2477
- return true;
3419
+ return "connected";
2478
3420
  } catch (error) {
2479
3421
  this.noteUnauthorized(error);
2480
- return false;
3422
+ return "failed";
2481
3423
  }
2482
3424
  }
2483
3425
  let timeoutId;
@@ -2494,72 +3436,80 @@ var Controller = (_class6 = class {
2494
3436
  ]);
2495
3437
  clearTimeout(timeoutId);
2496
3438
  if (result === "timeout") {
3439
+ debug(this.options.clientName, "stream.initialize.timeout", () => ({
3440
+ timeoutMs: this.options.stream.initTimeoutMs,
3441
+ ...this.debugState()
3442
+ }));
2497
3443
  console.warn(
2498
3444
  "@vercel/flags-core: Stream initialization timeout, falling back while continuing to connect in the background"
2499
3445
  );
2500
- void this.streamSource.start().catch(() => {
2501
- });
2502
- return false;
3446
+ if (this.streamSource.active) {
3447
+ void this.streamSource.start().catch(() => {
3448
+ });
3449
+ }
3450
+ return "connecting";
2503
3451
  }
2504
- return true;
3452
+ return "connected";
2505
3453
  } catch (error) {
2506
3454
  clearTimeout(timeoutId);
2507
3455
  this.noteUnauthorized(error);
2508
- return false;
3456
+ return "failed";
2509
3457
  }
2510
3458
  }
2511
3459
  // ---------------------------------------------------------------------------
2512
3460
  // Polling initialization
2513
3461
  // ---------------------------------------------------------------------------
2514
3462
  /**
2515
- * Attempts to initialize via polling with timeout.
2516
- * Returns true if first poll succeeded within timeout.
2517
- *
2518
- * Only used when streaming is disabled and polling is the primary source.
3463
+ * Waits for the first poll whenever polling becomes the active runtime source.
3464
+ * On timeout, initialization falls back while the pending poll and interval
3465
+ * continue in the background. Poll errors propagate if no data is cached or
3466
+ * the client is shutting down.
2519
3467
  */
2520
- async tryInitializePolling() {
2521
- const pollPromise = this.pollingSource.poll();
2522
- if (this.options.polling.initTimeoutMs <= 0) {
2523
- try {
2524
- await pollPromise;
2525
- if (this.data) {
2526
- this.pollingSource.startInterval();
2527
- return true;
2528
- }
2529
- return false;
2530
- } catch (e14) {
2531
- return false;
3468
+ async initializePolling() {
3469
+ debug(this.options.clientName, "poll.initialize", () => ({
3470
+ timeoutMs: this.options.polling.initTimeoutMs,
3471
+ ...this.debugState()
3472
+ }));
3473
+ const poll = this.pollingSource.poll().catch((error) => {
3474
+ if (!this.cache.hasData || this.isShutdown) {
3475
+ throw error;
2532
3476
  }
2533
- }
2534
- let timeoutId;
2535
- const timeoutPromise = new Promise((resolve) => {
2536
- timeoutId = setTimeout(
2537
- () => resolve("timeout"),
2538
- this.options.polling.initTimeoutMs
3477
+ debug(
3478
+ this.options.clientName,
3479
+ "poll.initialize.fallback",
3480
+ this.debugState
2539
3481
  );
2540
3482
  });
3483
+ const timeoutMs = this.options.polling.initTimeoutMs;
3484
+ if (timeoutMs <= 0) {
3485
+ await poll;
3486
+ return;
3487
+ }
3488
+ let timeoutId;
2541
3489
  try {
2542
- const result = await Promise.race([pollPromise, timeoutPromise]);
2543
- clearTimeout(timeoutId);
2544
- if (result === "timeout") {
3490
+ const outcome = await Promise.race([
3491
+ poll,
3492
+ new Promise((resolve) => {
3493
+ timeoutId = setTimeout(() => resolve("timeout"), timeoutMs);
3494
+ })
3495
+ ]);
3496
+ if (outcome === "timeout") {
3497
+ debug(this.options.clientName, "poll.initialize.timeout", () => ({
3498
+ timeoutMs,
3499
+ ...this.debugState()
3500
+ }));
2545
3501
  console.warn(
2546
3502
  "@vercel/flags-core: Polling initialization timeout, falling back while continuing to poll in the background"
2547
3503
  );
2548
- return false;
2549
3504
  }
2550
- if (this.data) {
2551
- this.pollingSource.startInterval();
2552
- return true;
2553
- }
2554
- return false;
2555
- } catch (e15) {
3505
+ } finally {
2556
3506
  clearTimeout(timeoutId);
2557
- return false;
2558
3507
  }
2559
3508
  }
2560
3509
  noteUnauthorized(error) {
2561
- if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("401")) {
3510
+ if (isUnauthorizedError(error)) {
2562
3511
  this.unauthorized = true;
3512
+ debug(this.options.clientName, "client.unauthorized", this.debugState);
2563
3513
  }
2564
3514
  }
2565
3515
  // ---------------------------------------------------------------------------
@@ -2569,44 +3519,41 @@ var Controller = (_class6 = class {
2569
3519
  * Initializes data for build step environments.
2570
3520
  */
2571
3521
  async initializeForBuildStep() {
2572
- if (this.data) return;
3522
+ if (this.cache.hasData) return;
2573
3523
  if (!this.buildDataPromise) {
2574
3524
  this.buildDataPromise = this.loadBuildData();
2575
3525
  }
2576
- this.data = await this.buildDataPromise;
3526
+ this.cache.seed(await this.buildDataPromise);
2577
3527
  }
2578
3528
  /**
2579
3529
  * Retrieves data during build steps.
2580
3530
  * Concurrent callers share a single load promise. The first caller to
2581
- * populate `this.data` gets cacheStatus MISS; subsequent callers get HIT.
3531
+ * populate the cache gets cacheStatus MISS; subsequent callers get HIT.
2582
3532
  */
2583
3533
  async resolveDataForBuildStep() {
2584
- if (this.data) {
2585
- return [this.data, "HIT"];
3534
+ const cached = this.cache.read();
3535
+ if (cached) {
3536
+ return [cached, "HIT"];
2586
3537
  }
2587
3538
  if (!this.buildDataPromise) {
2588
3539
  this.buildDataPromise = this.loadBuildData();
2589
3540
  }
2590
3541
  const data = await this.buildDataPromise;
2591
- if (!this.data) {
2592
- this.data = data;
2593
- return [data, "MISS"];
3542
+ const loaded = this.cache.read();
3543
+ if (loaded) {
3544
+ return [loaded, "HIT"];
2594
3545
  }
2595
- return [this.data, "HIT"];
3546
+ return [this.cache.seedAndRead(data), "MISS"];
2596
3547
  }
2597
3548
  /**
2598
3549
  * Loads data for a build step: bundled → one-time fetch.
2599
3550
  */
2600
3551
  async loadBuildData() {
2601
3552
  const bundled = await this.bundledSource.tryLoad();
2602
- if (bundled) return tagData(bundled, "bundled");
3553
+ if (bundled) return tagData({ ...bundled }, "bundled");
2603
3554
  try {
2604
- const fetched = await fetchDatafile({
2605
- host: this.options.host,
2606
- auth: this.options.auth,
2607
- fetch: this.options.fetch
2608
- });
2609
- return tagData(fetched, "fetched");
3555
+ const fetched = await this.fetchOnce();
3556
+ return tagData({ ...fetched, fetchedAt: Date.now() }, "fetched");
2610
3557
  } catch (error) {
2611
3558
  this.noteUnauthorized(error);
2612
3559
  }
@@ -2617,32 +3564,47 @@ var Controller = (_class6 = class {
2617
3564
  // ---------------------------------------------------------------------------
2618
3565
  // Fallback helpers
2619
3566
  // ---------------------------------------------------------------------------
3567
+ fetchOnce() {
3568
+ return fetchDatafile({
3569
+ host: this.options.host,
3570
+ clientName: this.options.clientName,
3571
+ auth: this.options.auth,
3572
+ fetch: this.options.fetch
3573
+ });
3574
+ }
3575
+ /**
3576
+ * Stores a last-resort fetch as a source response, which clears any failure,
3577
+ * and serves it through the cache boundary.
3578
+ */
3579
+ acceptLastResortFetch(fetched) {
3580
+ this.cache.updateFromSource(fetched, "fetched");
3581
+ const remote = this.cache.read();
3582
+ if (!remote) {
3583
+ throw new Error("@vercel/flags-core: Fetch returned no definitions");
3584
+ }
3585
+ return remote;
3586
+ }
2620
3587
  /**
2621
3588
  * Shared fallback chain used by both initialize() and resolveData().
2622
3589
  */
2623
3590
  async initializeFromFallbacks() {
2624
- this.transition("initializing:fallback");
2625
- if (this.data) {
2626
- this.transition("degraded");
3591
+ this.transition("initializing:fallback", "initialize-static-fallback");
3592
+ if (this.cache.hasData) {
3593
+ this.transition("degraded", "cached-fallback");
2627
3594
  return;
2628
3595
  }
2629
3596
  const bundled = await this.bundledSource.tryLoad();
2630
3597
  if (bundled) {
2631
- this.data = tagData(bundled, "bundled");
2632
- this.transition("degraded");
3598
+ this.cache.seed(tagData({ ...bundled }, "bundled"));
3599
+ this.transition("degraded", "bundled-fallback");
2633
3600
  return;
2634
3601
  }
2635
3602
  if (!this.options.stream.enabled && !this.options.polling.enabled) {
2636
3603
  try {
2637
- const fetched = await fetchDatafile({
2638
- host: this.options.host,
2639
- auth: this.options.auth,
2640
- fetch: this.options.fetch
2641
- });
2642
- this.data = tagData(fetched, "fetched");
2643
- this.transition("degraded");
3604
+ this.acceptLastResortFetch(await this.fetchOnce());
3605
+ this.transition("degraded", "fetched-fallback");
2644
3606
  return;
2645
- } catch (e16) {
3607
+ } catch (e17) {
2646
3608
  }
2647
3609
  }
2648
3610
  throw this.noDefinitionsError(". Bundled definitions not found.");
@@ -2652,6 +3614,7 @@ var Controller = (_class6 = class {
2652
3614
  * starts with either "." or " during build.".
2653
3615
  */
2654
3616
  noDefinitionsError(detail) {
3617
+ debug(this.options.clientName, "client.no-definitions", this.debugState);
2655
3618
  const { sourceProjectId } = this.options.auth;
2656
3619
  const reason = this.unauthorized && sourceProjectId ? ` Request was ${unauthorizedMessage(sourceProjectId)}` : "";
2657
3620
  return new Error(
@@ -2659,51 +3622,39 @@ var Controller = (_class6 = class {
2659
3622
  );
2660
3623
  }
2661
3624
  /**
2662
- * Retrieves data using the fallback chain (called when no cached data exists).
2663
- * Streaming mode: stream → datafile → bundled.
2664
- * Polling mode: poll → datafile → bundled.
2665
- * Offline mode: datafile → bundled → one-time fetch.
3625
+ * Retrieves data when the cache is empty in offline mode:
3626
+ * datafile → bundled → one-time fetch.
2666
3627
  */
2667
- async resolveDataWithFallbacks() {
2668
- if (this.options.stream.enabled) {
2669
- this.transition("initializing:stream");
2670
- const streamSuccess = await this.tryInitializeStream();
2671
- if (streamSuccess && this.data) {
2672
- this.transition("streaming");
2673
- return [this.data, "MISS"];
2674
- }
2675
- } else if (this.options.polling.enabled) {
2676
- this.transition("initializing:polling");
2677
- const pollingSuccess = await this.tryInitializePolling();
2678
- if (pollingSuccess && this.data) {
2679
- this.transition("polling");
2680
- return [this.data, "MISS"];
2681
- }
2682
- }
2683
- this.transition("initializing:fallback");
3628
+ async resolveStaticFallbackData() {
3629
+ const cached = this.cache.read();
3630
+ if (cached) return [cached, "STALE"];
3631
+ this.transition("initializing:fallback", "read-static-fallback");
2684
3632
  if (this.options.datafile) {
2685
- this.data = tagData(this.options.datafile, "provided");
2686
- this.transition("degraded");
2687
- return [this.data, "STALE"];
3633
+ const provided = this.cache.seedAndRead(
3634
+ tagData({ ...this.options.datafile }, "provided")
3635
+ );
3636
+ this.transition("degraded", "provided-fallback");
3637
+ return [provided, "STALE"];
2688
3638
  }
2689
3639
  const bundled = await this.bundledSource.tryLoad();
2690
3640
  if (bundled) {
2691
3641
  console.warn("@vercel/flags-core: Using bundled definitions as fallback");
2692
- this.data = tagData(bundled, "bundled");
2693
- this.transition("degraded");
2694
- return [this.data, "STALE"];
3642
+ const embedded = this.cache.seedAndRead(
3643
+ tagData({ ...bundled }, "bundled")
3644
+ );
3645
+ this.transition("degraded", "bundled-fallback");
3646
+ return [embedded, "STALE"];
2695
3647
  }
2696
3648
  if (!this.options.stream.enabled && !this.options.polling.enabled) {
3649
+ let fetched;
2697
3650
  try {
2698
- const fetched = await fetchDatafile({
2699
- host: this.options.host,
2700
- auth: this.options.auth,
2701
- fetch: this.options.fetch
2702
- });
2703
- this.data = tagData(fetched, "fetched");
2704
- this.transition("degraded");
2705
- return [this.data, "MISS"];
2706
- } catch (e17) {
3651
+ fetched = await this.fetchOnce();
3652
+ } catch (e18) {
3653
+ }
3654
+ if (fetched) {
3655
+ const remote = this.acceptLastResortFetch(fetched);
3656
+ this.transition("degraded", "fetched-fallback");
3657
+ return [remote, "MISS"];
2707
3658
  }
2708
3659
  }
2709
3660
  throw this.noDefinitionsError(
@@ -2711,40 +3662,17 @@ var Controller = (_class6 = class {
2711
3662
  );
2712
3663
  }
2713
3664
  // ---------------------------------------------------------------------------
2714
- // Data comparison
2715
- // ---------------------------------------------------------------------------
2716
- /**
2717
- * Checks if the incoming data is newer than the current in-memory data.
2718
- * Returns true if the update should proceed, false if it should be skipped.
2719
- *
2720
- * Always accepts the update if:
2721
- * - There is no current data
2722
- * - The current data has no configUpdatedAt
2723
- * - The incoming data has no configUpdatedAt
2724
- *
2725
- * Skips the update only when both have configUpdatedAt and incoming is not newer.
2726
- */
2727
- isNewerData(incoming) {
2728
- if (!this.data) return true;
2729
- const currentTs = parseConfigUpdatedAt(this.data.configUpdatedAt);
2730
- const incomingTs = parseConfigUpdatedAt(incoming.configUpdatedAt);
2731
- if (currentTs === void 0 || incomingTs === void 0) {
2732
- return true;
2733
- }
2734
- return incomingTs > currentTs;
2735
- }
2736
- // ---------------------------------------------------------------------------
2737
3665
  // Usage tracking
2738
3666
  // ---------------------------------------------------------------------------
2739
3667
  /**
2740
3668
  * Tracks a read operation for usage analytics.
2741
3669
  * During build steps, only the first read is tracked.
2742
3670
  */
2743
- trackRead(startTime, cacheHadDefinitions, isFirstRead, source) {
3671
+ trackRead(startTime, cacheHadDefinitions, isFirstRead, datafile) {
2744
3672
  if (this.unauthorized) return;
2745
3673
  if (this.options.buildStep && this.buildReadTracked) return;
2746
3674
  if (this.options.buildStep) this.buildReadTracked = true;
2747
- const configOrigin = source === "embedded" ? "embedded" : "in-memory";
3675
+ const configOrigin = datafile.metrics.source === "embedded" ? "embedded" : "in-memory";
2748
3676
  const cacheAction = this.state === "streaming" ? "FOLLOWING" : this.state === "polling" ? "REFRESHING" : "NONE";
2749
3677
  const mode = this.mode;
2750
3678
  const trackOptions = {
@@ -2755,11 +3683,11 @@ var Controller = (_class6 = class {
2755
3683
  duration: Date.now() - startTime,
2756
3684
  mode: mode === "streaming" ? "stream" : mode === "polling" ? "poll" : mode
2757
3685
  };
2758
- const configUpdatedAt = _optionalChain([this, 'access', _83 => _83.data, 'optionalAccess', _84 => _84.configUpdatedAt]);
3686
+ const configUpdatedAt = datafile.configUpdatedAt;
2759
3687
  if (typeof configUpdatedAt === "number") {
2760
3688
  trackOptions.configUpdatedAt = configUpdatedAt;
2761
3689
  }
2762
- const revision = _optionalChain([this, 'access', _85 => _85.data, 'optionalAccess', _86 => _86.revision]);
3690
+ const revision = datafile.revision;
2763
3691
  if (typeof revision === "number") {
2764
3692
  trackOptions.revision = revision;
2765
3693
  }
@@ -2778,7 +3706,7 @@ var Controller = (_class6 = class {
2778
3706
  clientName: _nullishCoalesce(options.clientName, () => ( this.options.clientName))
2779
3707
  });
2780
3708
  }
2781
- }, _class6);
3709
+ }, _class10);
2782
3710
 
2783
3711
  // src/index.make.ts
2784
3712
  function make(createRawClient, defaults) {
@@ -2829,7 +3757,7 @@ function setCacheLife() {
2829
3757
  try {
2830
3758
  _cache.cacheLife.call(void 0, { revalidate: 0, expire: 0 });
2831
3759
  _cache.cacheLife.call(void 0, { stale: 60 });
2832
- } catch (e18) {
3760
+ } catch (e19) {
2833
3761
  }
2834
3762
  }
2835
3763
  var cachedFns = {
@@ -2879,4 +3807,4 @@ var { flagsClient, resetDefaultFlagsClient, createClient } = make(
2879
3807
 
2880
3808
 
2881
3809
  exports.ResolutionReason = ResolutionReason; exports.evaluate = evaluate; exports.FallbackNotFoundError = FallbackNotFoundError; exports.FallbackEntryNotFoundError = FallbackEntryNotFoundError; exports.Controller = Controller; exports.flagsClient = flagsClient; exports.resetDefaultFlagsClient = resetDefaultFlagsClient; exports.createClient = createClient;
2882
- //# sourceMappingURL=chunk-W742GGZU.cjs.map
3810
+ //# sourceMappingURL=chunk-W6776MKF.cjs.map