@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @vercel/flags-core
2
2
 
3
+ ## 1.9.0-0fd2f41-20260915094243
4
+
5
+ ### Minor Changes
6
+
7
+ - [#498](https://github.com/vercel/flags/pull/498) [`da0b044`](https://github.com/vercel/flags/commit/da0b044c2f2efb1c61900c43f21fa21813a3ef20) Thanks [@luismeyer](https://github.com/luismeyer)! - Add a header-driven `vercel` client mode that uses request config-version timestamps instead of streaming or polling. Reuse fresh definitions, refresh in the background when the request version is up to 10 seconds newer than the cached configuration, and block for a refresh when the gap is larger. Deduplicate concurrent refreshes, allow retries after fetch failures, and discard late responses after shutdown.
8
+
9
+ Add opt-in controller, stream, and header-source diagnostics with `DEBUG=@vercel/flags-core` to show data origins, connection lifecycle, and refresh decisions without logging credentials or flag values.
10
+
3
11
  ## 1.8.1
4
12
 
5
13
  ### Patch Changes
package/README.md CHANGED
@@ -12,18 +12,39 @@ npm i @vercel/flags-core
12
12
 
13
13
  ## Usage
14
14
 
15
+ Create a shared client at module scope, but evaluate flags inside a request handler when using Vercel OIDC authentication. `evaluate()` and `bulkEvaluate()` initialize the client automatically on first use; you do not need to call `initialize()` first.
16
+
17
+ For example, in an Express app deployed to Vercel:
18
+
15
19
  ```ts
20
+ import express from 'express';
16
21
  import { createClient } from '@vercel/flags-core';
17
22
 
18
- const client = createClient(process.env.FLAGS!);
19
-
20
- await client.initialize();
23
+ const app = express();
24
+ const client = createClient(); // Uses Vercel OIDC; does not initialize yet.
21
25
 
22
- const result = await client.evaluate<boolean>('show-new-feature', false, {
23
- user: { id: 'user-123' },
26
+ app.get('/api/feature', async (_req, res) => {
27
+ const result = await client.evaluate<boolean>('show-new-feature', false);
28
+ res.json({ enabled: result.value });
24
29
  });
30
+
31
+ export default app;
25
32
  ```
26
33
 
34
+ Outside Vercel, pass an SDK key explicitly: `createClient(process.env.FLAGS)`.
35
+
36
+ ### Initialization and request-scoped OIDC
37
+
38
+ On Vercel, the OIDC token can be supplied through the current request context (`x-vercel-oidc-token`). It is not guaranteed to be available while modules are loading. Creating the client at module scope is safe, but starting `client.initialize()` there can attempt authentication before a request exists.
39
+
40
+ Do not store a module-scope initialization promise and await it later in a handler. Calling `initialize()` starts the work immediately; awaiting the promise inside a request does not move that work into the request context. If the promise rejects, every handler awaiting that same promise will reject before reaching evaluation and its fallback handling.
41
+
42
+ This also applies when definitions are embedded at build time: with OIDC authentication, the client uses the token's `project_id` claim to select the embedded definitions. An importable `@vercel/flags-definitions` module alone is not enough.
43
+
44
+ For local development, `vercel env pull` writes an OIDC token to `.env.local`. If your local runner loads that file, the environment-variable fallback can make module-scope initialization appear to work, even though request-scoped authentication on a deployment requires deferring it.
45
+
46
+ Explicit `await client.initialize()` is optional and can be useful when you want to wait for initialization and handle its errors yourself. Only call it once authentication is available: inside a request handler for request-scoped OIDC, or during startup when using an SDK key or an already-available environment token. For normal flag evaluation, prefer calling `evaluate()` or `bulkEvaluate()` directly in the handler.
47
+
27
48
  ## Evaluation Metrics
28
49
 
29
50
  To associate evaluation metrics with an environment, pass the
@@ -38,6 +59,30 @@ const client = createClient(process.env.FLAGS!, {
38
59
  This option is sent only to the metrics ingestion endpoint. It does not select
39
60
  the environment used for flag evaluation.
40
61
 
62
+ ## Debugging data sources
63
+
64
+ Set `DEBUG=@vercel/flags-core` in your application's server environment (for
65
+ example, in `.env.local` or the Vercel project's environment variables), then
66
+ restart or redeploy the app:
67
+
68
+ ```bash
69
+ DEBUG=@vercel/flags-core pnpm dev
70
+ ```
71
+
72
+ Debug logs are written with `console.log` and labeled `[controller]`,
73
+ `[stream-source]`, or `[header-source]`. They show initialization settings and
74
+ state transitions, the origin and cache status of each read, stream connections
75
+ and disconnections, and header freshness decisions (`serve-cached`,
76
+ `background-refresh`, or `blocking-refresh`). Missing headers, unmatched projects,
77
+ and invalid timestamps are reported with a reason instead of raw header values.
78
+
79
+ The controller's `origin` distinguishes `stream`, `poll`, `provided`, `bundled`,
80
+ and `fetched` data; `mode` identifies the active update strategy. Debug metadata
81
+ includes project IDs, revisions, and timestamps, but not SDK keys, tokens, flag
82
+ values, or full datafiles. This setting also enables the existing ingest debug
83
+ logging. Unset `DEBUG` to disable debug output; normal warnings and errors are
84
+ unaffected.
85
+
41
86
  ## OpenFeature
42
87
 
43
88
  An OpenFeature-compatible provider is available at `@vercel/flags-core/openfeature`:
@@ -573,7 +573,7 @@ function bulkEvaluate(flags, shared) {
573
573
  }
574
574
 
575
575
  // package.json
576
- var version = "1.8.1";
576
+ var version = "1.9.0-0fd2f41-20260915094243";
577
577
 
578
578
  // src/lib/report-value.ts
579
579
  function internalReportValue(key, value, data) {
@@ -971,6 +971,12 @@ function createCreateRawClient(fns) {
971
971
  };
972
972
  }
973
973
 
974
+ // src/utils/debug.ts
975
+ function debugLog(source, message, details = {}) {
976
+ if (!process.env.DEBUG?.includes("@vercel/flags-core")) return;
977
+ console.log(`@vercel/flags-core [${source}] ${message}`, details);
978
+ }
979
+
974
980
  // src/utils/read-bundled-definitions.ts
975
981
  var sdkKeyHashCache = /* @__PURE__ */ new Map();
976
982
  async function computeHash(sdkKey) {
@@ -1051,7 +1057,7 @@ var MAX_EVENTS_PER_REQUEST = 2e3;
1051
1057
  var EVALUATING_OIDC_TOKEN_HEADER = "X-Vercel-Flags-OIDC-Token";
1052
1058
  var FLUSH_REASON_HEADER = "X-Vercel-Flags-Flush-Reason";
1053
1059
  var isDebugMode = process.env.DEBUG?.includes("@vercel/flags-core");
1054
- var debugLog = (...args) => {
1060
+ var debugLog2 = (...args) => {
1055
1061
  if (!isDebugMode) return;
1056
1062
  console.log(...args);
1057
1063
  };
@@ -1097,7 +1103,7 @@ async function sendIngestChunk(options, eventsToSend, flushId, flushReason) {
1097
1103
  headers: await getIngestHeaders(options, flushReason),
1098
1104
  body: JSON.stringify(eventsToSend)
1099
1105
  });
1100
- debugLog(
1106
+ debugLog2(
1101
1107
  `@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
1102
1108
  );
1103
1109
  if (response.ok) {
@@ -1494,6 +1500,178 @@ async function fetchDatafile(options) {
1494
1500
  }
1495
1501
  }
1496
1502
 
1503
+ // src/controller/header-source.ts
1504
+ import { waitUntil as waitUntil3 } from "@vercel/functions";
1505
+
1506
+ // src/controller/tagged-data.ts
1507
+ function tagData(data, origin) {
1508
+ return Object.assign(data, { _origin: origin });
1509
+ }
1510
+ function originToMetricsSource(origin) {
1511
+ switch (origin) {
1512
+ case "stream":
1513
+ case "poll":
1514
+ case "provided":
1515
+ return "in-memory";
1516
+ case "fetched":
1517
+ return "remote";
1518
+ case "bundled":
1519
+ return "embedded";
1520
+ }
1521
+ }
1522
+
1523
+ // src/controller/typed-emitter.ts
1524
+ var TypedEmitter = class {
1525
+ handlers = /* @__PURE__ */ new Map();
1526
+ on(event, handler) {
1527
+ let set = this.handlers.get(event);
1528
+ if (!set) {
1529
+ set = /* @__PURE__ */ new Set();
1530
+ this.handlers.set(event, set);
1531
+ }
1532
+ set.add(handler);
1533
+ }
1534
+ off(event, handler) {
1535
+ this.handlers.get(event)?.delete(handler);
1536
+ }
1537
+ emit(event, ...args) {
1538
+ const set = this.handlers.get(event);
1539
+ if (set) {
1540
+ for (const handler of set) {
1541
+ handler(...args);
1542
+ }
1543
+ }
1544
+ }
1545
+ };
1546
+
1547
+ // src/controller/header-source.ts
1548
+ var HeaderSource = class extends TypedEmitter {
1549
+ options;
1550
+ abortController;
1551
+ promise;
1552
+ constructor(options) {
1553
+ super();
1554
+ this.options = options;
1555
+ }
1556
+ fetchDatafile() {
1557
+ if (this.promise) {
1558
+ debugLog("header-source", "Reusing pending refresh");
1559
+ return this.promise;
1560
+ }
1561
+ debugLog("header-source", "Starting refresh");
1562
+ const abortController = new AbortController();
1563
+ this.abortController = abortController;
1564
+ this.promise = fetchDatafile({
1565
+ ...this.options,
1566
+ signal: abortController.signal
1567
+ }).then((data) => {
1568
+ abortController.signal.throwIfAborted();
1569
+ debugLog("header-source", "Refresh completed", {
1570
+ projectId: data.projectId,
1571
+ configUpdatedAt: Number(data.configUpdatedAt),
1572
+ revision: data.revision
1573
+ });
1574
+ this.emit("data", data);
1575
+ return data;
1576
+ }).catch((error) => {
1577
+ debugLog("header-source", "Refresh failed", {
1578
+ aborted: abortController.signal.aborted
1579
+ });
1580
+ throw error;
1581
+ }).finally(() => {
1582
+ if (this.abortController === abortController) {
1583
+ this.promise = void 0;
1584
+ this.abortController = void 0;
1585
+ }
1586
+ });
1587
+ return this.promise;
1588
+ }
1589
+ getUpdatedAtHeader(projectId) {
1590
+ const ctx = getRequestContext();
1591
+ const headerName = ctx.headers?.["x-vercel-edge-config-versions"] != null ? "x-vercel-edge-config-versions" : "edge-config-versions";
1592
+ const header = ctx.headers?.[headerName];
1593
+ if (!header) {
1594
+ debugLog("header-source", "Header unavailable", {
1595
+ projectId,
1596
+ reason: "missing-header"
1597
+ });
1598
+ return;
1599
+ }
1600
+ const prefix = `flags_${projectId}=`;
1601
+ const value = header.split(";").map((part) => part.trim()).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1602
+ const timestamp = Number(value);
1603
+ if (!Number.isFinite(timestamp) || timestamp <= 0) {
1604
+ debugLog("header-source", "Header unavailable", {
1605
+ projectId,
1606
+ headerName,
1607
+ reason: value === void 0 ? "project-not-found" : "invalid-timestamp"
1608
+ });
1609
+ return;
1610
+ }
1611
+ debugLog("header-source", "Header version available", {
1612
+ projectId,
1613
+ headerName,
1614
+ timestamp
1615
+ });
1616
+ return timestamp;
1617
+ }
1618
+ async resolveData(currentData) {
1619
+ if (!currentData.configUpdatedAt) {
1620
+ debugLog("header-source", "Skipping header refresh", {
1621
+ projectId: currentData.projectId,
1622
+ reason: "missing-data-timestamp"
1623
+ });
1624
+ return;
1625
+ }
1626
+ const updatedAtHeader = this.getUpdatedAtHeader(currentData.projectId);
1627
+ if (!updatedAtHeader) {
1628
+ return;
1629
+ }
1630
+ const currentUpdatedAt = Number(currentData.configUpdatedAt);
1631
+ const deltaMs = updatedAtHeader - currentUpdatedAt;
1632
+ debugLog("header-source", "Freshness decision", {
1633
+ projectId: currentData.projectId,
1634
+ currentUpdatedAt,
1635
+ headerUpdatedAt: updatedAtHeader,
1636
+ deltaMs,
1637
+ action: updatedAtHeader <= currentUpdatedAt ? "serve-cached" : updatedAtHeader <= currentUpdatedAt + 1e4 ? "background-refresh" : "blocking-refresh"
1638
+ });
1639
+ if (updatedAtHeader <= currentUpdatedAt) {
1640
+ return [currentData, "HIT"];
1641
+ }
1642
+ if (updatedAtHeader <= currentUpdatedAt + 1e4) {
1643
+ const pending = this.fetchDatafile();
1644
+ const signal = this.abortController?.signal;
1645
+ const background = pending.catch((error) => {
1646
+ if (!signal?.aborted) {
1647
+ console.error("@vercel/flags-core: Header refresh failed:", error);
1648
+ }
1649
+ });
1650
+ waitUntil3(background);
1651
+ return [currentData, "STALE"];
1652
+ }
1653
+ const data = await this.fetchDatafile();
1654
+ return [tagData(data, "fetched"), "MISS"];
1655
+ }
1656
+ read(currentData) {
1657
+ return this.resolveData(currentData);
1658
+ }
1659
+ isAvailable(projectId) {
1660
+ return !!this.getUpdatedAtHeader(projectId);
1661
+ }
1662
+ /**
1663
+ * Abort the current header-driven fetch and discard its pending work.
1664
+ */
1665
+ stop() {
1666
+ debugLog("header-source", "Stopping header refresh", {
1667
+ pending: this.promise !== void 0
1668
+ });
1669
+ this.abortController?.abort();
1670
+ this.abortController = void 0;
1671
+ this.promise = void 0;
1672
+ }
1673
+ };
1674
+
1497
1675
  // src/controller/normalized-options.ts
1498
1676
  var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
1499
1677
  var DEFAULT_POLLING_INTERVAL_MS = 3e4;
@@ -1545,30 +1723,6 @@ function normalizeOptions(options) {
1545
1723
  };
1546
1724
  }
1547
1725
 
1548
- // src/controller/typed-emitter.ts
1549
- var TypedEmitter = class {
1550
- handlers = /* @__PURE__ */ new Map();
1551
- on(event, handler) {
1552
- let set = this.handlers.get(event);
1553
- if (!set) {
1554
- set = /* @__PURE__ */ new Set();
1555
- this.handlers.set(event, set);
1556
- }
1557
- set.add(handler);
1558
- }
1559
- off(event, handler) {
1560
- this.handlers.get(event)?.delete(handler);
1561
- }
1562
- emit(event, ...args) {
1563
- const set = this.handlers.get(event);
1564
- if (set) {
1565
- for (const handler of set) {
1566
- handler(...args);
1567
- }
1568
- }
1569
- }
1570
- };
1571
-
1572
1726
  // src/controller/polling-source.ts
1573
1727
  var PollingSource = class extends TypedEmitter {
1574
1728
  config;
@@ -1854,7 +2008,11 @@ var StreamSource = class extends TypedEmitter {
1854
2008
  * If already started, returns the existing promise.
1855
2009
  */
1856
2010
  start() {
1857
- if (this.promise) return this.promise;
2011
+ if (this.promise) {
2012
+ debugLog("stream-source", "Reusing stream connection");
2013
+ return this.promise;
2014
+ }
2015
+ debugLog("stream-source", "Starting stream connection");
1858
2016
  const abortController = new AbortController();
1859
2017
  this.abortController = abortController;
1860
2018
  abortController.signal.addEventListener(
@@ -1878,21 +2036,41 @@ var StreamSource = class extends TypedEmitter {
1878
2036
  },
1879
2037
  {
1880
2038
  onDatafile: (newData) => {
2039
+ debugLog("stream-source", "Connected with datafile", {
2040
+ projectId: newData.projectId,
2041
+ configUpdatedAt: Number(newData.configUpdatedAt),
2042
+ revision: newData.revision
2043
+ });
1881
2044
  this.emit("data", newData);
1882
2045
  this.emit("connected");
1883
2046
  },
1884
2047
  onPrimed: (message) => {
2048
+ debugLog("stream-source", "Connected with current revision", {
2049
+ projectId: message.projectId,
2050
+ revision: message.revision
2051
+ });
1885
2052
  this.emit("primed", message);
1886
2053
  this.emit("connected");
1887
2054
  },
1888
2055
  onDisconnect: () => {
2056
+ debugLog("stream-source", "Disconnected", {
2057
+ aborted: abortController.signal.aborted
2058
+ });
1889
2059
  this.emit("disconnected");
1890
2060
  }
1891
2061
  }
1892
2062
  );
1893
- this.promise = promise;
1894
- return promise;
2063
+ this.promise = promise.catch((error) => {
2064
+ debugLog("stream-source", "Stream initialization failed", {
2065
+ aborted: abortController.signal.aborted
2066
+ });
2067
+ throw error;
2068
+ });
2069
+ return this.promise;
1895
2070
  } catch (error) {
2071
+ debugLog("stream-source", "Stream initialization failed", {
2072
+ aborted: abortController.signal.aborted
2073
+ });
1896
2074
  this.promise = void 0;
1897
2075
  this.abortController = void 0;
1898
2076
  throw error;
@@ -1902,29 +2080,15 @@ var StreamSource = class extends TypedEmitter {
1902
2080
  * Stop the stream connection.
1903
2081
  */
1904
2082
  stop() {
2083
+ debugLog("stream-source", "Stopping stream connection", {
2084
+ active: this.abortController !== void 0
2085
+ });
1905
2086
  this.abortController?.abort();
1906
2087
  this.abortController = void 0;
1907
2088
  this.promise = void 0;
1908
2089
  }
1909
2090
  };
1910
2091
 
1911
- // src/controller/tagged-data.ts
1912
- function tagData(data, origin) {
1913
- return Object.assign(data, { _origin: origin });
1914
- }
1915
- function originToMetricsSource(origin) {
1916
- switch (origin) {
1917
- case "stream":
1918
- case "poll":
1919
- case "provided":
1920
- return "in-memory";
1921
- case "fetched":
1922
- return "remote";
1923
- case "bundled":
1924
- return "embedded";
1925
- }
1926
- }
1927
-
1928
2092
  // src/controller/index.ts
1929
2093
  function parseConfigUpdatedAt(value) {
1930
2094
  if (typeof value === "number") return value;
@@ -1949,6 +2113,7 @@ var Controller = class {
1949
2113
  streamSource;
1950
2114
  pollingSource;
1951
2115
  bundledSource;
2116
+ headerSource;
1952
2117
  // Usage tracking
1953
2118
  usageTracker;
1954
2119
  isFirstGetData = true;
@@ -1968,6 +2133,7 @@ var Controller = class {
1968
2133
  auth: this.options.auth,
1969
2134
  readBundledDefinitions
1970
2135
  });
2136
+ this.headerSource = new HeaderSource(this.options);
1971
2137
  this.wireSourceEvents();
1972
2138
  if (this.options.datafile) {
1973
2139
  this.data = tagData(this.options.datafile, "provided");
@@ -2003,6 +2169,11 @@ var Controller = class {
2003
2169
  onPollError = (error) => {
2004
2170
  console.error("@vercel/flags-core: Poll failed:", error);
2005
2171
  };
2172
+ onFetchedData = (data) => {
2173
+ if (this.isNewerData(data)) {
2174
+ this.data = tagData(data, "fetched");
2175
+ }
2176
+ };
2006
2177
  // ---------------------------------------------------------------------------
2007
2178
  // Source event wiring
2008
2179
  // ---------------------------------------------------------------------------
@@ -2013,6 +2184,7 @@ var Controller = class {
2013
2184
  this.streamSource.on("disconnected", this.onStreamDisconnected);
2014
2185
  this.pollingSource.on("data", this.onPollData);
2015
2186
  this.pollingSource.on("error", this.onPollError);
2187
+ this.headerSource.on("data", this.onFetchedData);
2016
2188
  }
2017
2189
  unwireSourceEvents() {
2018
2190
  this.streamSource.off("data", this.onStreamData);
@@ -2021,11 +2193,18 @@ var Controller = class {
2021
2193
  this.streamSource.off("disconnected", this.onStreamDisconnected);
2022
2194
  this.pollingSource.off("data", this.onPollData);
2023
2195
  this.pollingSource.off("error", this.onPollError);
2196
+ this.headerSource.off("data", this.onFetchedData);
2024
2197
  }
2025
2198
  // ---------------------------------------------------------------------------
2026
2199
  // State machine
2027
2200
  // ---------------------------------------------------------------------------
2028
2201
  transition(to) {
2202
+ debugLog("controller", "State changed", {
2203
+ from: this.state,
2204
+ to,
2205
+ projectId: this.data?.projectId,
2206
+ origin: this.data?._origin
2207
+ });
2029
2208
  this.state = to;
2030
2209
  }
2031
2210
  get isConnected() {
@@ -2038,6 +2217,8 @@ var Controller = class {
2038
2217
  return "streaming";
2039
2218
  case "polling":
2040
2219
  return "polling";
2220
+ case "vercel":
2221
+ return "vercel";
2041
2222
  default:
2042
2223
  return "offline";
2043
2224
  }
@@ -2054,6 +2235,14 @@ var Controller = class {
2054
2235
  * Offline mode (neither): datafile → bundled → one-time fetch
2055
2236
  */
2056
2237
  async initialize() {
2238
+ debugLog("controller", "Initializing", {
2239
+ buildStep: this.options.buildStep,
2240
+ streamEnabled: this.options.stream.enabled,
2241
+ pollingEnabled: this.options.polling.enabled,
2242
+ hasData: this.data !== void 0,
2243
+ projectId: this.data?.projectId,
2244
+ origin: this.data?._origin
2245
+ });
2057
2246
  if (this.options.buildStep) {
2058
2247
  this.transition("build:loading");
2059
2248
  await this.initializeForBuildStep();
@@ -2073,7 +2262,9 @@ var Controller = class {
2073
2262
  }
2074
2263
  }
2075
2264
  if (this.data) {
2076
- if (this.options.stream.enabled) {
2265
+ if (this.headerSource.isAvailable(this.data.projectId)) {
2266
+ this.transition("vercel");
2267
+ } else if (this.options.stream.enabled) {
2077
2268
  this.transition("initializing:stream");
2078
2269
  await this.tryInitializeStream();
2079
2270
  } else if (this.options.polling.enabled) {
@@ -2084,6 +2275,9 @@ var Controller = class {
2084
2275
  }
2085
2276
  return;
2086
2277
  }
2278
+ debugLog("controller", "Header mode unavailable", {
2279
+ reason: "no-definitions"
2280
+ });
2087
2281
  if (this.options.stream.enabled) {
2088
2282
  this.transition("initializing:stream");
2089
2283
  const streamSuccess = await this.tryInitializeStream();
@@ -2112,6 +2306,15 @@ var Controller = class {
2112
2306
  const [result, cacheStatus] = await this.resolveData();
2113
2307
  const readMs = Date.now() - startTime;
2114
2308
  const source = originToMetricsSource(result._origin);
2309
+ debugLog("controller", "Read resolved", {
2310
+ projectId: result.projectId,
2311
+ mode: this.mode,
2312
+ source,
2313
+ origin: result._origin,
2314
+ cacheStatus,
2315
+ configUpdatedAt: parseConfigUpdatedAt(result.configUpdatedAt),
2316
+ revision: result.revision
2317
+ });
2115
2318
  this.trackRead(startTime, cacheHadDefinitions, isFirstRead, source);
2116
2319
  if (this.dataViewSource !== result) {
2117
2320
  const { _origin, ...rest } = result;
@@ -2136,6 +2339,7 @@ var Controller = class {
2136
2339
  this.unwireSourceEvents();
2137
2340
  this.streamSource.stop();
2138
2341
  this.pollingSource.stop();
2342
+ this.headerSource.stop();
2139
2343
  this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
2140
2344
  this.transition("shutdown");
2141
2345
  await this.usageTracker.shutdown();
@@ -2179,6 +2383,15 @@ var Controller = class {
2179
2383
  }
2180
2384
  }
2181
2385
  const source = originToMetricsSource(result._origin);
2386
+ debugLog("controller", "Datafile resolved", {
2387
+ projectId: result.projectId,
2388
+ mode: this.mode,
2389
+ source,
2390
+ origin: result._origin,
2391
+ cacheStatus,
2392
+ configUpdatedAt: parseConfigUpdatedAt(result.configUpdatedAt),
2393
+ revision: result.revision
2394
+ });
2182
2395
  if (this.dataViewSource !== result) {
2183
2396
  const { _origin, ...rest } = result;
2184
2397
  this.dataViewBase = rest;
@@ -2217,6 +2430,12 @@ var Controller = class {
2217
2430
  return this.resolveDataForBuildStep();
2218
2431
  }
2219
2432
  if (this.data) {
2433
+ if (this.mode === "vercel") {
2434
+ const result = await this.headerSource.read(this.data);
2435
+ if (result) {
2436
+ return result;
2437
+ }
2438
+ }
2220
2439
  const cacheStatus = this.isConnected ? "HIT" : "STALE";
2221
2440
  return [this.data, cacheStatus];
2222
2441
  }
@@ -2255,6 +2474,14 @@ var Controller = class {
2255
2474
  ]);
2256
2475
  clearTimeout(timeoutId);
2257
2476
  if (result === "timeout") {
2477
+ debugLog(
2478
+ "controller",
2479
+ "Stream initialization timed out; using fallback",
2480
+ {
2481
+ timeoutMs: this.options.stream.initTimeoutMs,
2482
+ origin: this.data?._origin
2483
+ }
2484
+ );
2258
2485
  console.warn(
2259
2486
  "@vercel/flags-core: Stream initialization timeout, falling back while continuing to connect in the background"
2260
2487
  );
@@ -2673,4 +2900,4 @@ export {
2673
2900
  resetDefaultFlagsClient,
2674
2901
  createClient
2675
2902
  };
2676
- //# sourceMappingURL=chunk-5BWCBI7I.js.map
2903
+ //# sourceMappingURL=chunk-FEALIMCH.js.map