@vercel/flags-core 1.8.1 → 1.9.0-0fd2f41-20260915094243

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.
@@ -558,7 +558,7 @@ function bulkEvaluate(flags, shared) {
558
558
  }
559
559
 
560
560
  // package.json
561
- var version = "1.8.1";
561
+ var version = "1.9.0-0fd2f41-20260915094243";
562
562
 
563
563
  // src/lib/report-value.ts
564
564
  function internalReportValue(key, value, data) {
@@ -956,6 +956,12 @@ function createCreateRawClient(fns) {
956
956
  };
957
957
  }
958
958
 
959
+ // src/utils/debug.ts
960
+ function debugLog(source, message, details = {}) {
961
+ if (!process.env.DEBUG?.includes("@vercel/flags-core")) return;
962
+ console.log(`@vercel/flags-core [${source}] ${message}`, details);
963
+ }
964
+
959
965
  // src/utils/read-bundled-definitions.ts
960
966
  var sdkKeyHashCache = /* @__PURE__ */ new Map();
961
967
  async function computeHash(sdkKey) {
@@ -1036,7 +1042,7 @@ var MAX_EVENTS_PER_REQUEST = 2e3;
1036
1042
  var EVALUATING_OIDC_TOKEN_HEADER = "X-Vercel-Flags-OIDC-Token";
1037
1043
  var FLUSH_REASON_HEADER = "X-Vercel-Flags-Flush-Reason";
1038
1044
  var isDebugMode = process.env.DEBUG?.includes("@vercel/flags-core");
1039
- var debugLog = (...args) => {
1045
+ var debugLog2 = (...args) => {
1040
1046
  if (!isDebugMode) return;
1041
1047
  console.log(...args);
1042
1048
  };
@@ -1082,7 +1088,7 @@ async function sendIngestChunk(options, eventsToSend, flushId, flushReason) {
1082
1088
  headers: await getIngestHeaders(options, flushReason),
1083
1089
  body: JSON.stringify(eventsToSend)
1084
1090
  });
1085
- debugLog(
1091
+ debugLog2(
1086
1092
  `@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
1087
1093
  );
1088
1094
  if (response.ok) {
@@ -1479,6 +1485,178 @@ async function fetchDatafile(options) {
1479
1485
  }
1480
1486
  }
1481
1487
 
1488
+ // src/controller/header-source.ts
1489
+ import { waitUntil as waitUntil3 } from "@vercel/functions";
1490
+
1491
+ // src/controller/tagged-data.ts
1492
+ function tagData(data, origin) {
1493
+ return Object.assign(data, { _origin: origin });
1494
+ }
1495
+ function originToMetricsSource(origin) {
1496
+ switch (origin) {
1497
+ case "stream":
1498
+ case "poll":
1499
+ case "provided":
1500
+ return "in-memory";
1501
+ case "fetched":
1502
+ return "remote";
1503
+ case "bundled":
1504
+ return "embedded";
1505
+ }
1506
+ }
1507
+
1508
+ // src/controller/typed-emitter.ts
1509
+ var TypedEmitter = class {
1510
+ handlers = /* @__PURE__ */ new Map();
1511
+ on(event, handler) {
1512
+ let set = this.handlers.get(event);
1513
+ if (!set) {
1514
+ set = /* @__PURE__ */ new Set();
1515
+ this.handlers.set(event, set);
1516
+ }
1517
+ set.add(handler);
1518
+ }
1519
+ off(event, handler) {
1520
+ this.handlers.get(event)?.delete(handler);
1521
+ }
1522
+ emit(event, ...args) {
1523
+ const set = this.handlers.get(event);
1524
+ if (set) {
1525
+ for (const handler of set) {
1526
+ handler(...args);
1527
+ }
1528
+ }
1529
+ }
1530
+ };
1531
+
1532
+ // src/controller/header-source.ts
1533
+ var HeaderSource = class extends TypedEmitter {
1534
+ options;
1535
+ abortController;
1536
+ promise;
1537
+ constructor(options) {
1538
+ super();
1539
+ this.options = options;
1540
+ }
1541
+ fetchDatafile() {
1542
+ if (this.promise) {
1543
+ debugLog("header-source", "Reusing pending refresh");
1544
+ return this.promise;
1545
+ }
1546
+ debugLog("header-source", "Starting refresh");
1547
+ const abortController = new AbortController();
1548
+ this.abortController = abortController;
1549
+ this.promise = fetchDatafile({
1550
+ ...this.options,
1551
+ signal: abortController.signal
1552
+ }).then((data) => {
1553
+ abortController.signal.throwIfAborted();
1554
+ debugLog("header-source", "Refresh completed", {
1555
+ projectId: data.projectId,
1556
+ configUpdatedAt: Number(data.configUpdatedAt),
1557
+ revision: data.revision
1558
+ });
1559
+ this.emit("data", data);
1560
+ return data;
1561
+ }).catch((error) => {
1562
+ debugLog("header-source", "Refresh failed", {
1563
+ aborted: abortController.signal.aborted
1564
+ });
1565
+ throw error;
1566
+ }).finally(() => {
1567
+ if (this.abortController === abortController) {
1568
+ this.promise = void 0;
1569
+ this.abortController = void 0;
1570
+ }
1571
+ });
1572
+ return this.promise;
1573
+ }
1574
+ getUpdatedAtHeader(projectId) {
1575
+ const ctx = getRequestContext();
1576
+ const headerName = ctx.headers?.["x-vercel-edge-config-versions"] != null ? "x-vercel-edge-config-versions" : "edge-config-versions";
1577
+ const header = ctx.headers?.[headerName];
1578
+ if (!header) {
1579
+ debugLog("header-source", "Header unavailable", {
1580
+ projectId,
1581
+ reason: "missing-header"
1582
+ });
1583
+ return;
1584
+ }
1585
+ const prefix = `flags_${projectId}=`;
1586
+ const value = header.split(";").map((part) => part.trim()).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1587
+ const timestamp = Number(value);
1588
+ if (!Number.isFinite(timestamp) || timestamp <= 0) {
1589
+ debugLog("header-source", "Header unavailable", {
1590
+ projectId,
1591
+ headerName,
1592
+ reason: value === void 0 ? "project-not-found" : "invalid-timestamp"
1593
+ });
1594
+ return;
1595
+ }
1596
+ debugLog("header-source", "Header version available", {
1597
+ projectId,
1598
+ headerName,
1599
+ timestamp
1600
+ });
1601
+ return timestamp;
1602
+ }
1603
+ async resolveData(currentData) {
1604
+ if (!currentData.configUpdatedAt) {
1605
+ debugLog("header-source", "Skipping header refresh", {
1606
+ projectId: currentData.projectId,
1607
+ reason: "missing-data-timestamp"
1608
+ });
1609
+ return;
1610
+ }
1611
+ const updatedAtHeader = this.getUpdatedAtHeader(currentData.projectId);
1612
+ if (!updatedAtHeader) {
1613
+ return;
1614
+ }
1615
+ const currentUpdatedAt = Number(currentData.configUpdatedAt);
1616
+ const deltaMs = updatedAtHeader - currentUpdatedAt;
1617
+ debugLog("header-source", "Freshness decision", {
1618
+ projectId: currentData.projectId,
1619
+ currentUpdatedAt,
1620
+ headerUpdatedAt: updatedAtHeader,
1621
+ deltaMs,
1622
+ action: updatedAtHeader <= currentUpdatedAt ? "serve-cached" : updatedAtHeader <= currentUpdatedAt + 1e4 ? "background-refresh" : "blocking-refresh"
1623
+ });
1624
+ if (updatedAtHeader <= currentUpdatedAt) {
1625
+ return [currentData, "HIT"];
1626
+ }
1627
+ if (updatedAtHeader <= currentUpdatedAt + 1e4) {
1628
+ const pending = this.fetchDatafile();
1629
+ const signal = this.abortController?.signal;
1630
+ const background = pending.catch((error) => {
1631
+ if (!signal?.aborted) {
1632
+ console.error("@vercel/flags-core: Header refresh failed:", error);
1633
+ }
1634
+ });
1635
+ waitUntil3(background);
1636
+ return [currentData, "STALE"];
1637
+ }
1638
+ const data = await this.fetchDatafile();
1639
+ return [tagData(data, "fetched"), "MISS"];
1640
+ }
1641
+ read(currentData) {
1642
+ return this.resolveData(currentData);
1643
+ }
1644
+ isAvailable(projectId) {
1645
+ return !!this.getUpdatedAtHeader(projectId);
1646
+ }
1647
+ /**
1648
+ * Abort the current header-driven fetch and discard its pending work.
1649
+ */
1650
+ stop() {
1651
+ debugLog("header-source", "Stopping header refresh", {
1652
+ pending: this.promise !== void 0
1653
+ });
1654
+ this.abortController?.abort();
1655
+ this.abortController = void 0;
1656
+ this.promise = void 0;
1657
+ }
1658
+ };
1659
+
1482
1660
  // src/controller/normalized-options.ts
1483
1661
  var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
1484
1662
  var DEFAULT_POLLING_INTERVAL_MS = 3e4;
@@ -1530,30 +1708,6 @@ function normalizeOptions(options) {
1530
1708
  };
1531
1709
  }
1532
1710
 
1533
- // src/controller/typed-emitter.ts
1534
- var TypedEmitter = class {
1535
- handlers = /* @__PURE__ */ new Map();
1536
- on(event, handler) {
1537
- let set = this.handlers.get(event);
1538
- if (!set) {
1539
- set = /* @__PURE__ */ new Set();
1540
- this.handlers.set(event, set);
1541
- }
1542
- set.add(handler);
1543
- }
1544
- off(event, handler) {
1545
- this.handlers.get(event)?.delete(handler);
1546
- }
1547
- emit(event, ...args) {
1548
- const set = this.handlers.get(event);
1549
- if (set) {
1550
- for (const handler of set) {
1551
- handler(...args);
1552
- }
1553
- }
1554
- }
1555
- };
1556
-
1557
1711
  // src/controller/polling-source.ts
1558
1712
  var PollingSource = class extends TypedEmitter {
1559
1713
  config;
@@ -1839,7 +1993,11 @@ var StreamSource = class extends TypedEmitter {
1839
1993
  * If already started, returns the existing promise.
1840
1994
  */
1841
1995
  start() {
1842
- if (this.promise) return this.promise;
1996
+ if (this.promise) {
1997
+ debugLog("stream-source", "Reusing stream connection");
1998
+ return this.promise;
1999
+ }
2000
+ debugLog("stream-source", "Starting stream connection");
1843
2001
  const abortController = new AbortController();
1844
2002
  this.abortController = abortController;
1845
2003
  abortController.signal.addEventListener(
@@ -1863,21 +2021,41 @@ var StreamSource = class extends TypedEmitter {
1863
2021
  },
1864
2022
  {
1865
2023
  onDatafile: (newData) => {
2024
+ debugLog("stream-source", "Connected with datafile", {
2025
+ projectId: newData.projectId,
2026
+ configUpdatedAt: Number(newData.configUpdatedAt),
2027
+ revision: newData.revision
2028
+ });
1866
2029
  this.emit("data", newData);
1867
2030
  this.emit("connected");
1868
2031
  },
1869
2032
  onPrimed: (message) => {
2033
+ debugLog("stream-source", "Connected with current revision", {
2034
+ projectId: message.projectId,
2035
+ revision: message.revision
2036
+ });
1870
2037
  this.emit("primed", message);
1871
2038
  this.emit("connected");
1872
2039
  },
1873
2040
  onDisconnect: () => {
2041
+ debugLog("stream-source", "Disconnected", {
2042
+ aborted: abortController.signal.aborted
2043
+ });
1874
2044
  this.emit("disconnected");
1875
2045
  }
1876
2046
  }
1877
2047
  );
1878
- this.promise = promise;
1879
- return promise;
2048
+ this.promise = promise.catch((error) => {
2049
+ debugLog("stream-source", "Stream initialization failed", {
2050
+ aborted: abortController.signal.aborted
2051
+ });
2052
+ throw error;
2053
+ });
2054
+ return this.promise;
1880
2055
  } catch (error) {
2056
+ debugLog("stream-source", "Stream initialization failed", {
2057
+ aborted: abortController.signal.aborted
2058
+ });
1881
2059
  this.promise = void 0;
1882
2060
  this.abortController = void 0;
1883
2061
  throw error;
@@ -1887,29 +2065,15 @@ var StreamSource = class extends TypedEmitter {
1887
2065
  * Stop the stream connection.
1888
2066
  */
1889
2067
  stop() {
2068
+ debugLog("stream-source", "Stopping stream connection", {
2069
+ active: this.abortController !== void 0
2070
+ });
1890
2071
  this.abortController?.abort();
1891
2072
  this.abortController = void 0;
1892
2073
  this.promise = void 0;
1893
2074
  }
1894
2075
  };
1895
2076
 
1896
- // src/controller/tagged-data.ts
1897
- function tagData(data, origin) {
1898
- return Object.assign(data, { _origin: origin });
1899
- }
1900
- function originToMetricsSource(origin) {
1901
- switch (origin) {
1902
- case "stream":
1903
- case "poll":
1904
- case "provided":
1905
- return "in-memory";
1906
- case "fetched":
1907
- return "remote";
1908
- case "bundled":
1909
- return "embedded";
1910
- }
1911
- }
1912
-
1913
2077
  // src/controller/index.ts
1914
2078
  function parseConfigUpdatedAt(value) {
1915
2079
  if (typeof value === "number") return value;
@@ -1934,6 +2098,7 @@ var Controller = class {
1934
2098
  streamSource;
1935
2099
  pollingSource;
1936
2100
  bundledSource;
2101
+ headerSource;
1937
2102
  // Usage tracking
1938
2103
  usageTracker;
1939
2104
  isFirstGetData = true;
@@ -1953,6 +2118,7 @@ var Controller = class {
1953
2118
  auth: this.options.auth,
1954
2119
  readBundledDefinitions
1955
2120
  });
2121
+ this.headerSource = new HeaderSource(this.options);
1956
2122
  this.wireSourceEvents();
1957
2123
  if (this.options.datafile) {
1958
2124
  this.data = tagData(this.options.datafile, "provided");
@@ -1988,6 +2154,11 @@ var Controller = class {
1988
2154
  onPollError = (error) => {
1989
2155
  console.error("@vercel/flags-core: Poll failed:", error);
1990
2156
  };
2157
+ onFetchedData = (data) => {
2158
+ if (this.isNewerData(data)) {
2159
+ this.data = tagData(data, "fetched");
2160
+ }
2161
+ };
1991
2162
  // ---------------------------------------------------------------------------
1992
2163
  // Source event wiring
1993
2164
  // ---------------------------------------------------------------------------
@@ -1998,6 +2169,7 @@ var Controller = class {
1998
2169
  this.streamSource.on("disconnected", this.onStreamDisconnected);
1999
2170
  this.pollingSource.on("data", this.onPollData);
2000
2171
  this.pollingSource.on("error", this.onPollError);
2172
+ this.headerSource.on("data", this.onFetchedData);
2001
2173
  }
2002
2174
  unwireSourceEvents() {
2003
2175
  this.streamSource.off("data", this.onStreamData);
@@ -2006,11 +2178,18 @@ var Controller = class {
2006
2178
  this.streamSource.off("disconnected", this.onStreamDisconnected);
2007
2179
  this.pollingSource.off("data", this.onPollData);
2008
2180
  this.pollingSource.off("error", this.onPollError);
2181
+ this.headerSource.off("data", this.onFetchedData);
2009
2182
  }
2010
2183
  // ---------------------------------------------------------------------------
2011
2184
  // State machine
2012
2185
  // ---------------------------------------------------------------------------
2013
2186
  transition(to) {
2187
+ debugLog("controller", "State changed", {
2188
+ from: this.state,
2189
+ to,
2190
+ projectId: this.data?.projectId,
2191
+ origin: this.data?._origin
2192
+ });
2014
2193
  this.state = to;
2015
2194
  }
2016
2195
  get isConnected() {
@@ -2023,6 +2202,8 @@ var Controller = class {
2023
2202
  return "streaming";
2024
2203
  case "polling":
2025
2204
  return "polling";
2205
+ case "vercel":
2206
+ return "vercel";
2026
2207
  default:
2027
2208
  return "offline";
2028
2209
  }
@@ -2039,6 +2220,14 @@ var Controller = class {
2039
2220
  * Offline mode (neither): datafile → bundled → one-time fetch
2040
2221
  */
2041
2222
  async initialize() {
2223
+ debugLog("controller", "Initializing", {
2224
+ buildStep: this.options.buildStep,
2225
+ streamEnabled: this.options.stream.enabled,
2226
+ pollingEnabled: this.options.polling.enabled,
2227
+ hasData: this.data !== void 0,
2228
+ projectId: this.data?.projectId,
2229
+ origin: this.data?._origin
2230
+ });
2042
2231
  if (this.options.buildStep) {
2043
2232
  this.transition("build:loading");
2044
2233
  await this.initializeForBuildStep();
@@ -2058,7 +2247,9 @@ var Controller = class {
2058
2247
  }
2059
2248
  }
2060
2249
  if (this.data) {
2061
- if (this.options.stream.enabled) {
2250
+ if (this.headerSource.isAvailable(this.data.projectId)) {
2251
+ this.transition("vercel");
2252
+ } else if (this.options.stream.enabled) {
2062
2253
  this.transition("initializing:stream");
2063
2254
  await this.tryInitializeStream();
2064
2255
  } else if (this.options.polling.enabled) {
@@ -2069,6 +2260,9 @@ var Controller = class {
2069
2260
  }
2070
2261
  return;
2071
2262
  }
2263
+ debugLog("controller", "Header mode unavailable", {
2264
+ reason: "no-definitions"
2265
+ });
2072
2266
  if (this.options.stream.enabled) {
2073
2267
  this.transition("initializing:stream");
2074
2268
  const streamSuccess = await this.tryInitializeStream();
@@ -2097,6 +2291,15 @@ var Controller = class {
2097
2291
  const [result, cacheStatus] = await this.resolveData();
2098
2292
  const readMs = Date.now() - startTime;
2099
2293
  const source = originToMetricsSource(result._origin);
2294
+ debugLog("controller", "Read resolved", {
2295
+ projectId: result.projectId,
2296
+ mode: this.mode,
2297
+ source,
2298
+ origin: result._origin,
2299
+ cacheStatus,
2300
+ configUpdatedAt: parseConfigUpdatedAt(result.configUpdatedAt),
2301
+ revision: result.revision
2302
+ });
2100
2303
  this.trackRead(startTime, cacheHadDefinitions, isFirstRead, source);
2101
2304
  if (this.dataViewSource !== result) {
2102
2305
  const { _origin, ...rest } = result;
@@ -2121,6 +2324,7 @@ var Controller = class {
2121
2324
  this.unwireSourceEvents();
2122
2325
  this.streamSource.stop();
2123
2326
  this.pollingSource.stop();
2327
+ this.headerSource.stop();
2124
2328
  this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
2125
2329
  this.transition("shutdown");
2126
2330
  await this.usageTracker.shutdown();
@@ -2164,6 +2368,15 @@ var Controller = class {
2164
2368
  }
2165
2369
  }
2166
2370
  const source = originToMetricsSource(result._origin);
2371
+ debugLog("controller", "Datafile resolved", {
2372
+ projectId: result.projectId,
2373
+ mode: this.mode,
2374
+ source,
2375
+ origin: result._origin,
2376
+ cacheStatus,
2377
+ configUpdatedAt: parseConfigUpdatedAt(result.configUpdatedAt),
2378
+ revision: result.revision
2379
+ });
2167
2380
  if (this.dataViewSource !== result) {
2168
2381
  const { _origin, ...rest } = result;
2169
2382
  this.dataViewBase = rest;
@@ -2202,6 +2415,12 @@ var Controller = class {
2202
2415
  return this.resolveDataForBuildStep();
2203
2416
  }
2204
2417
  if (this.data) {
2418
+ if (this.mode === "vercel") {
2419
+ const result = await this.headerSource.read(this.data);
2420
+ if (result) {
2421
+ return result;
2422
+ }
2423
+ }
2205
2424
  const cacheStatus = this.isConnected ? "HIT" : "STALE";
2206
2425
  return [this.data, cacheStatus];
2207
2426
  }
@@ -2240,6 +2459,14 @@ var Controller = class {
2240
2459
  ]);
2241
2460
  clearTimeout(timeoutId);
2242
2461
  if (result === "timeout") {
2462
+ debugLog(
2463
+ "controller",
2464
+ "Stream initialization timed out; using fallback",
2465
+ {
2466
+ timeoutMs: this.options.stream.initTimeoutMs,
2467
+ origin: this.data?._origin
2468
+ }
2469
+ );
2243
2470
  console.warn(
2244
2471
  "@vercel/flags-core: Stream initialization timeout, falling back while continuing to connect in the background"
2245
2472
  );
@@ -2683,4 +2910,4 @@ export {
2683
2910
  resetDefaultFlagsClient,
2684
2911
  createClient
2685
2912
  };
2686
- //# sourceMappingURL=chunk-DYSF37WV.js.map
2913
+ //# sourceMappingURL=chunk-XFCAUSUZ.js.map