@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.
- package/CHANGELOG.md +28 -0
- package/README.md +184 -0
- package/dist/{chunk-TDZBUVBG.js → chunk-D4522FGO.js} +1295 -367
- package/dist/chunk-D4522FGO.js.map +1 -0
- package/dist/{chunk-PGKH4XQ7.cjs → chunk-ITB5XQD3.cjs} +1335 -407
- package/dist/chunk-ITB5XQD3.cjs.map +1 -0
- package/dist/{chunk-SLNLPFKU.js → chunk-SYXANNZ4.js} +1295 -367
- package/dist/chunk-SYXANNZ4.js.map +1 -0
- package/dist/{chunk-W742GGZU.cjs → chunk-W6776MKF.cjs} +1336 -408
- package/dist/chunk-W6776MKF.cjs.map +1 -0
- package/dist/index.default.cjs +2 -2
- package/dist/index.default.d.cts +104 -39
- package/dist/index.default.d.ts +104 -39
- package/dist/index.default.js +1 -1
- package/dist/index.next-js.cjs +2 -2
- package/dist/index.next-js.d.cts +104 -39
- package/dist/index.next-js.d.ts +104 -39
- 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-1OK52NNi.d.cts → types-DpVlT3fI.d.cts} +7 -1
- package/dist/{types-1OK52NNi.d.ts → types-DpVlT3fI.d.ts} +7 -1
- package/package.json +1 -1
- package/dist/chunk-PGKH4XQ7.cjs.map +0 -1
- package/dist/chunk-SLNLPFKU.js.map +0 -1
- package/dist/chunk-TDZBUVBG.js.map +0 -1
- 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.
|
|
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
|
|
1032
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
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)
|
|
1062
|
-
|
|
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 =
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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',
|
|
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 (!
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
1661
|
-
_optionalChain([options, 'access',
|
|
1662
|
-
const timeoutId = setTimeout(
|
|
1663
|
-
() =>
|
|
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
|
-
|
|
1667
|
-
);
|
|
2072
|
+
);
|
|
2073
|
+
}, DEFAULT_FETCH_TIMEOUT_MS);
|
|
1668
2074
|
let delay;
|
|
1669
|
-
const fetchAttempt = async () => {
|
|
1670
|
-
|
|
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',
|
|
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',
|
|
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 = (
|
|
1776
|
-
|
|
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
|
-
},
|
|
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
|
-
*
|
|
2350
|
+
* Updates the cache on success; emits 'error' and rejects on failure.
|
|
1815
2351
|
*/
|
|
1816
2352
|
async poll() {
|
|
1817
|
-
if (
|
|
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
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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',
|
|
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
|
|
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
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
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',
|
|
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
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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 ||
|
|
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 (
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2861
|
+
var UNKNOWN_FRESHNESS = {
|
|
2862
|
+
assess: () => ({ status: "unknown" })
|
|
2863
|
+
};
|
|
2864
|
+
function isUnauthorizedError(error) {
|
|
2865
|
+
if (error instanceof UnauthorizedError) {
|
|
2866
|
+
return true;
|
|
2169
2867
|
}
|
|
2170
|
-
|
|
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 = (
|
|
2873
|
+
var Controller = (_class10 = class {
|
|
2173
2874
|
|
|
2174
2875
|
// State machine
|
|
2175
|
-
|
|
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
|
|
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
|
-
|
|
2182
|
-
|
|
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
|
-
|
|
2894
|
+
__init22() {this.isFirstGetData = true}
|
|
2190
2895
|
// Build-step deduplication
|
|
2191
|
-
|
|
2192
|
-
|
|
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
|
-
|
|
2196
|
-
constructor(options) {;
|
|
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
|
-
() =>
|
|
2922
|
+
() => this.cache.revision
|
|
2201
2923
|
);
|
|
2202
|
-
this.pollingSource = new PollingSource(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2217
|
-
this.data = tagData(data, "stream");
|
|
2218
|
-
}
|
|
2962
|
+
this.cache.updateFromSource(data, "stream");
|
|
2219
2963
|
}}
|
|
2220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2237
|
-
this.
|
|
2238
|
-
|
|
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
|
-
|
|
2999
|
+
__init32() {this.onSourceError = (error) => {
|
|
2243
3000
|
this.noteUnauthorized(error);
|
|
2244
|
-
|
|
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.
|
|
2255
|
-
this.
|
|
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.
|
|
2263
|
-
this.
|
|
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
|
-
|
|
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 (
|
|
2304
|
-
this.
|
|
3096
|
+
if (this.isShutdown) {
|
|
3097
|
+
this.wireSourceEvents();
|
|
3098
|
+
this.transition("idle", "restart-after-shutdown");
|
|
2305
3099
|
}
|
|
2306
|
-
if (!this.
|
|
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
|
-
|
|
2311
|
-
}
|
|
2312
|
-
} catch (e13) {
|
|
3106
|
+
if (bundled) this.cache.seed(tagData({ ...bundled }, "bundled"));
|
|
3107
|
+
} catch (e15) {
|
|
2313
3108
|
}
|
|
2314
3109
|
}
|
|
2315
|
-
if (this.
|
|
2316
|
-
|
|
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.
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
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.
|
|
3135
|
+
const cacheHadDefinitions = this.cache.hasData;
|
|
2350
3136
|
const isFirstRead = this.isFirstGetData;
|
|
2351
3137
|
this.isFirstGetData = false;
|
|
2352
|
-
|
|
2353
|
-
const
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
this.
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
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.
|
|
2380
|
-
this.
|
|
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
|
-
*
|
|
2386
|
-
*
|
|
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
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
3419
|
+
return "connected";
|
|
2478
3420
|
} catch (error) {
|
|
2479
3421
|
this.noteUnauthorized(error);
|
|
2480
|
-
return
|
|
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
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
3446
|
+
if (this.streamSource.active) {
|
|
3447
|
+
void this.streamSource.start().catch(() => {
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
return "connecting";
|
|
2503
3451
|
}
|
|
2504
|
-
return
|
|
3452
|
+
return "connected";
|
|
2505
3453
|
} catch (error) {
|
|
2506
3454
|
clearTimeout(timeoutId);
|
|
2507
3455
|
this.noteUnauthorized(error);
|
|
2508
|
-
return
|
|
3456
|
+
return "failed";
|
|
2509
3457
|
}
|
|
2510
3458
|
}
|
|
2511
3459
|
// ---------------------------------------------------------------------------
|
|
2512
3460
|
// Polling initialization
|
|
2513
3461
|
// ---------------------------------------------------------------------------
|
|
2514
3462
|
/**
|
|
2515
|
-
*
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
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
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
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
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
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
|
|
2543
|
-
|
|
2544
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
3522
|
+
if (this.cache.hasData) return;
|
|
2573
3523
|
if (!this.buildDataPromise) {
|
|
2574
3524
|
this.buildDataPromise = this.loadBuildData();
|
|
2575
3525
|
}
|
|
2576
|
-
this.
|
|
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
|
|
3531
|
+
* populate the cache gets cacheStatus MISS; subsequent callers get HIT.
|
|
2582
3532
|
*/
|
|
2583
3533
|
async resolveDataForBuildStep() {
|
|
2584
|
-
|
|
2585
|
-
|
|
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
|
-
|
|
2592
|
-
|
|
2593
|
-
return [
|
|
3542
|
+
const loaded = this.cache.read();
|
|
3543
|
+
if (loaded) {
|
|
3544
|
+
return [loaded, "HIT"];
|
|
2594
3545
|
}
|
|
2595
|
-
return [this.data, "
|
|
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
|
|
2605
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2638
|
-
|
|
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 (
|
|
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
|
|
2663
|
-
*
|
|
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
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
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
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
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
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
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
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
this.
|
|
2704
|
-
|
|
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,
|
|
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 =
|
|
3686
|
+
const configUpdatedAt = datafile.configUpdatedAt;
|
|
2759
3687
|
if (typeof configUpdatedAt === "number") {
|
|
2760
3688
|
trackOptions.configUpdatedAt = configUpdatedAt;
|
|
2761
3689
|
}
|
|
2762
|
-
const 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
|
-
},
|
|
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 (
|
|
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-
|
|
3810
|
+
//# sourceMappingURL=chunk-W6776MKF.cjs.map
|