@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 +8 -0
- package/README.md +50 -5
- package/dist/{chunk-5BWCBI7I.js → chunk-FEALIMCH.js} +276 -49
- package/dist/chunk-FEALIMCH.js.map +1 -0
- package/dist/{chunk-CVDAHZHK.cjs → chunk-OXUU7M2A.cjs} +295 -68
- package/dist/chunk-OXUU7M2A.cjs.map +1 -0
- package/dist/{chunk-DYSF37WV.js → chunk-XFCAUSUZ.js} +276 -49
- package/dist/chunk-XFCAUSUZ.js.map +1 -0
- package/dist/{chunk-CKL562EU.cjs → chunk-ZUO5MVUD.cjs} +295 -68
- package/dist/chunk-ZUO5MVUD.cjs.map +1 -0
- package/dist/index.default.cjs +2 -2
- package/dist/index.default.d.cts +9 -2
- package/dist/index.default.d.ts +9 -2
- package/dist/index.default.js +1 -1
- package/dist/index.next-js.cjs +2 -2
- package/dist/index.next-js.d.cts +9 -2
- package/dist/index.next-js.d.ts +9 -2
- package/dist/index.next-js.js +1 -1
- package/dist/openfeature.default.cjs +2 -2
- package/dist/openfeature.default.d.cts +1 -1
- package/dist/openfeature.default.d.ts +1 -1
- package/dist/openfeature.default.js +1 -1
- package/dist/openfeature.next-js.cjs +2 -2
- package/dist/openfeature.next-js.d.cts +1 -1
- package/dist/openfeature.next-js.d.ts +1 -1
- package/dist/openfeature.next-js.js +1 -1
- package/dist/{types-CIONStRG.d.cts → types-w8u8i22D.d.cts} +15 -5
- package/dist/{types-CIONStRG.d.ts → types-w8u8i22D.d.ts} +15 -5
- package/package.json +1 -1
- package/dist/chunk-5BWCBI7I.js.map +0 -1
- package/dist/chunk-CKL562EU.cjs.map +0 -1
- package/dist/chunk-CVDAHZHK.cjs.map +0 -1
- package/dist/chunk-DYSF37WV.js.map +0 -1
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
|
|
19
|
-
|
|
20
|
-
await client.initialize();
|
|
23
|
+
const app = express();
|
|
24
|
+
const client = createClient(); // Uses Vercel OIDC; does not initialize yet.
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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-
|
|
2903
|
+
//# sourceMappingURL=chunk-FEALIMCH.js.map
|