@vercel/flags-core 1.8.1 → 1.9.0-004baf8-20260915082635
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 +6 -0
- package/README.md +26 -5
- package/dist/{chunk-DYSF37WV.js → chunk-KURHLZJF.js} +148 -44
- package/dist/chunk-KURHLZJF.js.map +1 -0
- package/dist/{chunk-CKL562EU.cjs → chunk-Q5SPNHKQ.cjs} +161 -57
- package/dist/chunk-Q5SPNHKQ.cjs.map +1 -0
- package/dist/{chunk-5BWCBI7I.js → chunk-T3VCPYWD.js} +148 -44
- package/dist/chunk-T3VCPYWD.js.map +1 -0
- package/dist/{chunk-CVDAHZHK.cjs → chunk-WM42VU6R.cjs} +161 -57
- package/dist/chunk-WM42VU6R.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,11 @@
|
|
|
1
1
|
# @vercel/flags-core
|
|
2
2
|
|
|
3
|
+
## 1.9.0-004baf8-20260915082635
|
|
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
|
+
|
|
3
9
|
## 1.8.1
|
|
4
10
|
|
|
5
11
|
### 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
|
|
@@ -558,7 +558,7 @@ function bulkEvaluate(flags, shared) {
|
|
|
558
558
|
}
|
|
559
559
|
|
|
560
560
|
// package.json
|
|
561
|
-
var version = "1.
|
|
561
|
+
var version = "1.9.0-004baf8-20260915082635";
|
|
562
562
|
|
|
563
563
|
// src/lib/report-value.ts
|
|
564
564
|
function internalReportValue(key, value, data) {
|
|
@@ -1479,6 +1479,131 @@ async function fetchDatafile(options) {
|
|
|
1479
1479
|
}
|
|
1480
1480
|
}
|
|
1481
1481
|
|
|
1482
|
+
// src/controller/header-source.ts
|
|
1483
|
+
import { waitUntil as waitUntil3 } from "@vercel/functions";
|
|
1484
|
+
|
|
1485
|
+
// src/controller/tagged-data.ts
|
|
1486
|
+
function tagData(data, origin) {
|
|
1487
|
+
return Object.assign(data, { _origin: origin });
|
|
1488
|
+
}
|
|
1489
|
+
function originToMetricsSource(origin) {
|
|
1490
|
+
switch (origin) {
|
|
1491
|
+
case "stream":
|
|
1492
|
+
case "poll":
|
|
1493
|
+
case "provided":
|
|
1494
|
+
return "in-memory";
|
|
1495
|
+
case "fetched":
|
|
1496
|
+
return "remote";
|
|
1497
|
+
case "bundled":
|
|
1498
|
+
return "embedded";
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// src/controller/typed-emitter.ts
|
|
1503
|
+
var TypedEmitter = class {
|
|
1504
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1505
|
+
on(event, handler) {
|
|
1506
|
+
let set = this.handlers.get(event);
|
|
1507
|
+
if (!set) {
|
|
1508
|
+
set = /* @__PURE__ */ new Set();
|
|
1509
|
+
this.handlers.set(event, set);
|
|
1510
|
+
}
|
|
1511
|
+
set.add(handler);
|
|
1512
|
+
}
|
|
1513
|
+
off(event, handler) {
|
|
1514
|
+
this.handlers.get(event)?.delete(handler);
|
|
1515
|
+
}
|
|
1516
|
+
emit(event, ...args) {
|
|
1517
|
+
const set = this.handlers.get(event);
|
|
1518
|
+
if (set) {
|
|
1519
|
+
for (const handler of set) {
|
|
1520
|
+
handler(...args);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
// src/controller/header-source.ts
|
|
1527
|
+
var HeaderSource = class extends TypedEmitter {
|
|
1528
|
+
options;
|
|
1529
|
+
abortController;
|
|
1530
|
+
promise;
|
|
1531
|
+
constructor(options) {
|
|
1532
|
+
super();
|
|
1533
|
+
this.options = options;
|
|
1534
|
+
}
|
|
1535
|
+
fetchDatafile() {
|
|
1536
|
+
if (this.promise) return this.promise;
|
|
1537
|
+
const abortController = new AbortController();
|
|
1538
|
+
this.abortController = abortController;
|
|
1539
|
+
this.promise = fetchDatafile({
|
|
1540
|
+
...this.options,
|
|
1541
|
+
signal: abortController.signal
|
|
1542
|
+
}).then((data) => {
|
|
1543
|
+
abortController.signal.throwIfAborted();
|
|
1544
|
+
this.emit("data", data);
|
|
1545
|
+
return data;
|
|
1546
|
+
}).finally(() => {
|
|
1547
|
+
if (this.abortController === abortController) {
|
|
1548
|
+
this.promise = void 0;
|
|
1549
|
+
this.abortController = void 0;
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
return this.promise;
|
|
1553
|
+
}
|
|
1554
|
+
getUpdatedAtHeader(projectId) {
|
|
1555
|
+
const ctx = getRequestContext();
|
|
1556
|
+
const header = ctx.headers?.["x-vercel-edge-config-versions"] ?? ctx.headers?.["edge-config-versions"];
|
|
1557
|
+
if (!header) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const prefix = `flags_${projectId}=`;
|
|
1561
|
+
const value = header.split(";").map((part) => part.trim()).find((part) => part.startsWith(prefix))?.slice(prefix.length);
|
|
1562
|
+
const timestamp = Number(value);
|
|
1563
|
+
return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : void 0;
|
|
1564
|
+
}
|
|
1565
|
+
async resolveData(currentData) {
|
|
1566
|
+
if (!currentData.configUpdatedAt) {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const updatedAtHeader = this.getUpdatedAtHeader(currentData.projectId);
|
|
1570
|
+
if (!updatedAtHeader) {
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const currentUpdatedAt = Number(currentData.configUpdatedAt);
|
|
1574
|
+
if (updatedAtHeader <= currentUpdatedAt) {
|
|
1575
|
+
return [currentData, "HIT"];
|
|
1576
|
+
}
|
|
1577
|
+
if (updatedAtHeader <= currentUpdatedAt + 1e4) {
|
|
1578
|
+
const pending = this.fetchDatafile();
|
|
1579
|
+
const signal = this.abortController?.signal;
|
|
1580
|
+
const background = pending.catch((error) => {
|
|
1581
|
+
if (!signal?.aborted) {
|
|
1582
|
+
console.error("@vercel/flags-core: Header refresh failed:", error);
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1585
|
+
waitUntil3(background);
|
|
1586
|
+
return [currentData, "STALE"];
|
|
1587
|
+
}
|
|
1588
|
+
const data = await this.fetchDatafile();
|
|
1589
|
+
return [tagData(data, "fetched"), "MISS"];
|
|
1590
|
+
}
|
|
1591
|
+
read(currentData) {
|
|
1592
|
+
return this.resolveData(currentData);
|
|
1593
|
+
}
|
|
1594
|
+
isAvailable(projectId) {
|
|
1595
|
+
return !!this.getUpdatedAtHeader(projectId);
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Abort the current header-driven fetch and discard its pending work.
|
|
1599
|
+
*/
|
|
1600
|
+
stop() {
|
|
1601
|
+
this.abortController?.abort();
|
|
1602
|
+
this.abortController = void 0;
|
|
1603
|
+
this.promise = void 0;
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1482
1607
|
// src/controller/normalized-options.ts
|
|
1483
1608
|
var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
|
|
1484
1609
|
var DEFAULT_POLLING_INTERVAL_MS = 3e4;
|
|
@@ -1530,30 +1655,6 @@ function normalizeOptions(options) {
|
|
|
1530
1655
|
};
|
|
1531
1656
|
}
|
|
1532
1657
|
|
|
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
1658
|
// src/controller/polling-source.ts
|
|
1558
1659
|
var PollingSource = class extends TypedEmitter {
|
|
1559
1660
|
config;
|
|
@@ -1893,23 +1994,6 @@ var StreamSource = class extends TypedEmitter {
|
|
|
1893
1994
|
}
|
|
1894
1995
|
};
|
|
1895
1996
|
|
|
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
1997
|
// src/controller/index.ts
|
|
1914
1998
|
function parseConfigUpdatedAt(value) {
|
|
1915
1999
|
if (typeof value === "number") return value;
|
|
@@ -1934,6 +2018,7 @@ var Controller = class {
|
|
|
1934
2018
|
streamSource;
|
|
1935
2019
|
pollingSource;
|
|
1936
2020
|
bundledSource;
|
|
2021
|
+
headerSource;
|
|
1937
2022
|
// Usage tracking
|
|
1938
2023
|
usageTracker;
|
|
1939
2024
|
isFirstGetData = true;
|
|
@@ -1953,6 +2038,7 @@ var Controller = class {
|
|
|
1953
2038
|
auth: this.options.auth,
|
|
1954
2039
|
readBundledDefinitions
|
|
1955
2040
|
});
|
|
2041
|
+
this.headerSource = new HeaderSource(this.options);
|
|
1956
2042
|
this.wireSourceEvents();
|
|
1957
2043
|
if (this.options.datafile) {
|
|
1958
2044
|
this.data = tagData(this.options.datafile, "provided");
|
|
@@ -1988,6 +2074,11 @@ var Controller = class {
|
|
|
1988
2074
|
onPollError = (error) => {
|
|
1989
2075
|
console.error("@vercel/flags-core: Poll failed:", error);
|
|
1990
2076
|
};
|
|
2077
|
+
onFetchedData = (data) => {
|
|
2078
|
+
if (this.isNewerData(data)) {
|
|
2079
|
+
this.data = tagData(data, "fetched");
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
1991
2082
|
// ---------------------------------------------------------------------------
|
|
1992
2083
|
// Source event wiring
|
|
1993
2084
|
// ---------------------------------------------------------------------------
|
|
@@ -1998,6 +2089,7 @@ var Controller = class {
|
|
|
1998
2089
|
this.streamSource.on("disconnected", this.onStreamDisconnected);
|
|
1999
2090
|
this.pollingSource.on("data", this.onPollData);
|
|
2000
2091
|
this.pollingSource.on("error", this.onPollError);
|
|
2092
|
+
this.headerSource.on("data", this.onFetchedData);
|
|
2001
2093
|
}
|
|
2002
2094
|
unwireSourceEvents() {
|
|
2003
2095
|
this.streamSource.off("data", this.onStreamData);
|
|
@@ -2006,6 +2098,7 @@ var Controller = class {
|
|
|
2006
2098
|
this.streamSource.off("disconnected", this.onStreamDisconnected);
|
|
2007
2099
|
this.pollingSource.off("data", this.onPollData);
|
|
2008
2100
|
this.pollingSource.off("error", this.onPollError);
|
|
2101
|
+
this.headerSource.off("data", this.onFetchedData);
|
|
2009
2102
|
}
|
|
2010
2103
|
// ---------------------------------------------------------------------------
|
|
2011
2104
|
// State machine
|
|
@@ -2023,6 +2116,8 @@ var Controller = class {
|
|
|
2023
2116
|
return "streaming";
|
|
2024
2117
|
case "polling":
|
|
2025
2118
|
return "polling";
|
|
2119
|
+
case "vercel":
|
|
2120
|
+
return "vercel";
|
|
2026
2121
|
default:
|
|
2027
2122
|
return "offline";
|
|
2028
2123
|
}
|
|
@@ -2058,7 +2153,9 @@ var Controller = class {
|
|
|
2058
2153
|
}
|
|
2059
2154
|
}
|
|
2060
2155
|
if (this.data) {
|
|
2061
|
-
if (this.
|
|
2156
|
+
if (this.headerSource.isAvailable(this.data.projectId)) {
|
|
2157
|
+
this.transition("vercel");
|
|
2158
|
+
} else if (this.options.stream.enabled) {
|
|
2062
2159
|
this.transition("initializing:stream");
|
|
2063
2160
|
await this.tryInitializeStream();
|
|
2064
2161
|
} else if (this.options.polling.enabled) {
|
|
@@ -2121,6 +2218,7 @@ var Controller = class {
|
|
|
2121
2218
|
this.unwireSourceEvents();
|
|
2122
2219
|
this.streamSource.stop();
|
|
2123
2220
|
this.pollingSource.stop();
|
|
2221
|
+
this.headerSource.stop();
|
|
2124
2222
|
this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
|
|
2125
2223
|
this.transition("shutdown");
|
|
2126
2224
|
await this.usageTracker.shutdown();
|
|
@@ -2202,6 +2300,12 @@ var Controller = class {
|
|
|
2202
2300
|
return this.resolveDataForBuildStep();
|
|
2203
2301
|
}
|
|
2204
2302
|
if (this.data) {
|
|
2303
|
+
if (this.mode === "vercel") {
|
|
2304
|
+
const result = await this.headerSource.read(this.data);
|
|
2305
|
+
if (result) {
|
|
2306
|
+
return result;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2205
2309
|
const cacheStatus = this.isConnected ? "HIT" : "STALE";
|
|
2206
2310
|
return [this.data, cacheStatus];
|
|
2207
2311
|
}
|
|
@@ -2683,4 +2787,4 @@ export {
|
|
|
2683
2787
|
resetDefaultFlagsClient,
|
|
2684
2788
|
createClient
|
|
2685
2789
|
};
|
|
2686
|
-
//# sourceMappingURL=chunk-
|
|
2790
|
+
//# sourceMappingURL=chunk-KURHLZJF.js.map
|