@vercel/flags-core 1.0.1 → 1.1.0-3ca407e-20260306094417
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 +18 -0
- package/README.md +2 -0
- package/dist/{chunk-5CDNQMQ6.js → chunk-MDVR4MXT.js} +990 -531
- package/dist/chunk-MDVR4MXT.js.map +1 -0
- package/dist/{chunk-BVKIEFFY.js → chunk-NEIY3ICC.js} +985 -527
- package/dist/chunk-NEIY3ICC.js.map +1 -0
- package/dist/chunk-OFRPUCDY.cjs +1976 -0
- package/dist/chunk-OFRPUCDY.cjs.map +1 -0
- package/dist/chunk-WRX7DC3S.cjs +1982 -0
- package/dist/chunk-WRX7DC3S.cjs.map +1 -0
- package/dist/index.default.cjs +22 -0
- package/dist/index.default.cjs.map +1 -0
- package/dist/index.default.d.cts +244 -0
- package/dist/index.default.d.ts +78 -83
- package/dist/index.default.js +4 -3
- package/dist/index.next-js.cjs +22 -0
- package/dist/index.next-js.cjs.map +1 -0
- package/dist/index.next-js.d.cts +231 -0
- package/dist/index.next-js.d.ts +78 -83
- package/dist/index.next-js.js +4 -3
- package/dist/openfeature.default.cjs +157 -0
- package/dist/openfeature.default.cjs.map +1 -0
- package/dist/openfeature.default.d.cts +48 -0
- package/dist/openfeature.default.d.ts +1 -1
- package/dist/openfeature.default.js +1 -1
- package/dist/openfeature.next-js.cjs +157 -0
- package/dist/openfeature.next-js.cjs.map +1 -0
- package/dist/openfeature.next-js.d.cts +49 -0
- package/dist/openfeature.next-js.d.ts +1 -1
- package/dist/openfeature.next-js.js +1 -1
- package/dist/{types-508sZTBu.d.ts → types-DtnecWgl.d.cts} +27 -5
- package/dist/types-DtnecWgl.d.ts +527 -0
- package/package.json +24 -11
- package/dist/chunk-5CDNQMQ6.js.map +0 -1
- package/dist/chunk-BVKIEFFY.js.map +0 -1
|
@@ -0,0 +1,1982 @@
|
|
|
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;// src/index.next-js.ts
|
|
2
|
+
var _cache = require('next/cache');
|
|
3
|
+
|
|
4
|
+
// src/evaluate.ts
|
|
5
|
+
var _jsxxhash = require('js-xxhash');
|
|
6
|
+
|
|
7
|
+
// src/types.ts
|
|
8
|
+
var ResolutionReason = /* @__PURE__ */ ((ResolutionReason2) => {
|
|
9
|
+
ResolutionReason2["PAUSED"] = "paused";
|
|
10
|
+
ResolutionReason2["TARGET_MATCH"] = "target_match";
|
|
11
|
+
ResolutionReason2["RULE_MATCH"] = "rule_match";
|
|
12
|
+
ResolutionReason2["FALLTHROUGH"] = "fallthrough";
|
|
13
|
+
ResolutionReason2["ERROR"] = "error";
|
|
14
|
+
return ResolutionReason2;
|
|
15
|
+
})(ResolutionReason || {});
|
|
16
|
+
var Original;
|
|
17
|
+
((Original2) => {
|
|
18
|
+
let AccessorType;
|
|
19
|
+
((AccessorType2) => {
|
|
20
|
+
AccessorType2["SEGMENT"] = "segment";
|
|
21
|
+
AccessorType2["ENTITY"] = "entity";
|
|
22
|
+
})(AccessorType = Original2.AccessorType || (Original2.AccessorType = {}));
|
|
23
|
+
})(Original || (Original = {}));
|
|
24
|
+
var Packed;
|
|
25
|
+
((Packed2) => {
|
|
26
|
+
let AccessorType;
|
|
27
|
+
((AccessorType2) => {
|
|
28
|
+
AccessorType2["SEGMENT"] = "segment";
|
|
29
|
+
AccessorType2["ENTITY"] = "entity";
|
|
30
|
+
})(AccessorType = Packed2.AccessorType || (Packed2.AccessorType = {}));
|
|
31
|
+
})(Packed || (Packed = {}));
|
|
32
|
+
|
|
33
|
+
// src/evaluate.ts
|
|
34
|
+
var MAX_REGEX_INPUT_LENGTH = 1e4;
|
|
35
|
+
function exhaustivenessCheck(_) {
|
|
36
|
+
throw new Error("Exhaustiveness check failed");
|
|
37
|
+
}
|
|
38
|
+
function getProperty(obj, pathArray) {
|
|
39
|
+
return pathArray.reduce((acc, key) => {
|
|
40
|
+
if (acc && key in acc) {
|
|
41
|
+
return acc[key];
|
|
42
|
+
}
|
|
43
|
+
return void 0;
|
|
44
|
+
}, obj);
|
|
45
|
+
}
|
|
46
|
+
function access(lhs, params) {
|
|
47
|
+
if (Array.isArray(lhs)) return getProperty(params.entities, lhs);
|
|
48
|
+
if (lhs === Packed.AccessorType.SEGMENT)
|
|
49
|
+
throw new Error("Unexpected segment");
|
|
50
|
+
throw new Error("Accessor not implemented");
|
|
51
|
+
}
|
|
52
|
+
function isString(input) {
|
|
53
|
+
return typeof input === "string";
|
|
54
|
+
}
|
|
55
|
+
function isNumber(input) {
|
|
56
|
+
return typeof input === "number";
|
|
57
|
+
}
|
|
58
|
+
function isArray(input) {
|
|
59
|
+
return Array.isArray(input);
|
|
60
|
+
}
|
|
61
|
+
function lower(input) {
|
|
62
|
+
if (typeof input === "string") return input.toLowerCase();
|
|
63
|
+
if (Array.isArray(input)) return input.map(lower);
|
|
64
|
+
return input;
|
|
65
|
+
}
|
|
66
|
+
var IGNORE_CASE_COMPARATORS = /* @__PURE__ */ new Set([
|
|
67
|
+
"eq" /* EQ */,
|
|
68
|
+
"!eq" /* NOT_EQ */,
|
|
69
|
+
"oneOf" /* ONE_OF */,
|
|
70
|
+
"!oneOf" /* NOT_ONE_OF */,
|
|
71
|
+
"containsAllOf" /* CONTAINS_ALL_OF */,
|
|
72
|
+
"containsAnyOf" /* CONTAINS_ANY_OF */,
|
|
73
|
+
"containsNoneOf" /* CONTAINS_NONE_OF */,
|
|
74
|
+
"startsWith" /* STARTS_WITH */,
|
|
75
|
+
"!startsWith" /* NOT_STARTS_WITH */,
|
|
76
|
+
"endsWith" /* ENDS_WITH */,
|
|
77
|
+
"!endsWith" /* NOT_ENDS_WITH */,
|
|
78
|
+
"contains" /* CONTAINS */,
|
|
79
|
+
"!contains" /* NOT_CONTAINS */
|
|
80
|
+
]);
|
|
81
|
+
function matchTargetList(targets, params) {
|
|
82
|
+
for (const kind in targets) {
|
|
83
|
+
const attributes = targets[kind];
|
|
84
|
+
for (const attribute in attributes) {
|
|
85
|
+
const entity = access([kind, attribute], params);
|
|
86
|
+
if (isString(entity) && attributes[attribute].includes(entity))
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
function matchSegment(segment, params) {
|
|
93
|
+
if (segment.include && matchTargetList(segment.include, params)) return true;
|
|
94
|
+
if (segment.exclude && matchTargetList(segment.exclude, params)) return false;
|
|
95
|
+
if (!_optionalChain([segment, 'access', _2 => _2.rules, 'optionalAccess', _3 => _3.length])) return false;
|
|
96
|
+
const firstMatchingRule = segment.rules.find(
|
|
97
|
+
(rule) => matchConditions(rule.conditions, params)
|
|
98
|
+
);
|
|
99
|
+
if (!firstMatchingRule) return false;
|
|
100
|
+
return handleSegmentOutcome(params, firstMatchingRule.outcome);
|
|
101
|
+
}
|
|
102
|
+
function matchSegmentCondition(cmp, rhs, params) {
|
|
103
|
+
switch (cmp) {
|
|
104
|
+
case "eq" /* EQ */: {
|
|
105
|
+
const segment = _optionalChain([params, 'access', _4 => _4.segments, 'optionalAccess', _5 => _5[rhs]]);
|
|
106
|
+
if (!segment) return false;
|
|
107
|
+
return matchSegment(segment, params);
|
|
108
|
+
}
|
|
109
|
+
case "!eq" /* NOT_EQ */: {
|
|
110
|
+
const segment = _optionalChain([params, 'access', _6 => _6.segments, 'optionalAccess', _7 => _7[rhs]]);
|
|
111
|
+
if (!segment) return false;
|
|
112
|
+
return !matchSegment(segment, params);
|
|
113
|
+
}
|
|
114
|
+
case "oneOf" /* ONE_OF */: {
|
|
115
|
+
if (!isArray(rhs)) return false;
|
|
116
|
+
const segmentIds = rhs;
|
|
117
|
+
return segmentIds.some((segmentId) => {
|
|
118
|
+
const segment = _optionalChain([params, 'access', _8 => _8.segments, 'optionalAccess', _9 => _9[segmentId]]);
|
|
119
|
+
if (!segment) return false;
|
|
120
|
+
return matchSegment(segment, params);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
case "!oneOf" /* NOT_ONE_OF */: {
|
|
124
|
+
const segmentIds = rhs;
|
|
125
|
+
return segmentIds.every((segmentId) => {
|
|
126
|
+
const segment = _optionalChain([params, 'access', _10 => _10.segments, 'optionalAccess', _11 => _11[segmentId]]);
|
|
127
|
+
if (!segment) return false;
|
|
128
|
+
return !matchSegment(segment, params);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
default:
|
|
132
|
+
throw new Error(`Comparator ${cmp} not implemented for segment`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function matchConditions(conditions, params) {
|
|
136
|
+
return conditions.every((condition) => {
|
|
137
|
+
const [lhsAccessor, cmpKey, rawRhs, options] = condition;
|
|
138
|
+
const hasIgnoreCaseFlag = typeof options === "string" && options.includes("i");
|
|
139
|
+
const hasIgnoreCaseOption = typeof options === "object" && options !== null && options.i === true;
|
|
140
|
+
const ignoreCase = IGNORE_CASE_COMPARATORS.has(cmpKey) && (hasIgnoreCaseFlag || hasIgnoreCaseOption);
|
|
141
|
+
if (lhsAccessor === Packed.AccessorType.SEGMENT) {
|
|
142
|
+
return rawRhs && matchSegmentCondition(cmpKey, rawRhs, params);
|
|
143
|
+
}
|
|
144
|
+
const lhs = ignoreCase ? lower(access(lhsAccessor, params)) : access(lhsAccessor, params);
|
|
145
|
+
const rhs = ignoreCase ? lower(rawRhs) : rawRhs;
|
|
146
|
+
try {
|
|
147
|
+
switch (cmpKey) {
|
|
148
|
+
case "eq" /* EQ */:
|
|
149
|
+
return lhs === rhs;
|
|
150
|
+
case "!eq" /* NOT_EQ */:
|
|
151
|
+
return lhs !== rhs;
|
|
152
|
+
case "oneOf" /* ONE_OF */:
|
|
153
|
+
return isArray(rhs) && rhs.includes(lhs);
|
|
154
|
+
case "!oneOf" /* NOT_ONE_OF */:
|
|
155
|
+
return isArray(rhs) && typeof lhs !== "undefined" && !rhs.includes(lhs);
|
|
156
|
+
case "containsAllOf" /* CONTAINS_ALL_OF */: {
|
|
157
|
+
if (!Array.isArray(rhs) || !Array.isArray(lhs)) return false;
|
|
158
|
+
const lhsSet = new Set(lhs.filter(isString));
|
|
159
|
+
if (lhsSet.size === lhs.length) {
|
|
160
|
+
return rhs.filter(isString).every((item) => lhsSet.has(item));
|
|
161
|
+
}
|
|
162
|
+
return rhs.every((item) => lhs.includes(item));
|
|
163
|
+
}
|
|
164
|
+
case "containsAnyOf" /* CONTAINS_ANY_OF */: {
|
|
165
|
+
if (!Array.isArray(rhs) || !Array.isArray(lhs)) return false;
|
|
166
|
+
const rhsSet = new Set(rhs.filter(isString));
|
|
167
|
+
return lhs.some(
|
|
168
|
+
rhsSet.size === rhs.length ? (
|
|
169
|
+
// try to use a set if the rhs is a list of strings - O(1)
|
|
170
|
+
(item) => rhsSet.has(item)
|
|
171
|
+
) : (
|
|
172
|
+
// otherwise we need to iterate over the values - O(n)
|
|
173
|
+
(item) => rhs.includes(item)
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
case "containsNoneOf" /* CONTAINS_NONE_OF */: {
|
|
178
|
+
if (!Array.isArray(rhs)) return false;
|
|
179
|
+
if (!Array.isArray(lhs)) return true;
|
|
180
|
+
const rhsSet = new Set(rhs.filter(isString));
|
|
181
|
+
return lhs.every(
|
|
182
|
+
rhsSet.size === rhs.length ? (
|
|
183
|
+
// try to use a set if the rhs is a list of strings - O(1)
|
|
184
|
+
(item) => !rhsSet.has(item)
|
|
185
|
+
) : (
|
|
186
|
+
// otherwise we need to iterate over the values - O(n)
|
|
187
|
+
(item) => !rhs.includes(item)
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
case "startsWith" /* STARTS_WITH */:
|
|
192
|
+
return isString(lhs) && isString(rhs) && lhs.startsWith(rhs);
|
|
193
|
+
case "!startsWith" /* NOT_STARTS_WITH */:
|
|
194
|
+
return isString(lhs) && isString(rhs) && !lhs.startsWith(rhs);
|
|
195
|
+
case "endsWith" /* ENDS_WITH */:
|
|
196
|
+
return isString(lhs) && isString(rhs) && lhs.endsWith(rhs);
|
|
197
|
+
case "!endsWith" /* NOT_ENDS_WITH */:
|
|
198
|
+
return isString(lhs) && isString(rhs) && !lhs.endsWith(rhs);
|
|
199
|
+
case "contains" /* CONTAINS */:
|
|
200
|
+
return isString(lhs) && isString(rhs) && lhs.includes(rhs);
|
|
201
|
+
case "!contains" /* NOT_CONTAINS */:
|
|
202
|
+
return isString(lhs) && isString(rhs) && !lhs.includes(rhs);
|
|
203
|
+
case "ex" /* EXISTS */:
|
|
204
|
+
return lhs !== void 0 && lhs !== null;
|
|
205
|
+
case "!ex" /* NOT_EXISTS */:
|
|
206
|
+
return lhs === void 0 || lhs === null;
|
|
207
|
+
case "gt" /* GT */:
|
|
208
|
+
if (lhs === null || lhs === void 0) return false;
|
|
209
|
+
return (isNumber(rhs) || isString(rhs)) && lhs > rhs;
|
|
210
|
+
case "gte" /* GTE */:
|
|
211
|
+
if (lhs === null || lhs === void 0) return false;
|
|
212
|
+
return (isNumber(rhs) || isString(rhs)) && lhs >= rhs;
|
|
213
|
+
case "lt" /* LT */:
|
|
214
|
+
if (lhs === null || lhs === void 0) return false;
|
|
215
|
+
return (isNumber(rhs) || isString(rhs)) && lhs < rhs;
|
|
216
|
+
case "lte" /* LTE */:
|
|
217
|
+
if (lhs === null || lhs === void 0) return false;
|
|
218
|
+
return (isNumber(rhs) || isString(rhs)) && lhs <= rhs;
|
|
219
|
+
case "regex" /* REGEX */:
|
|
220
|
+
if (isString(lhs) && lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === "object" && !Array.isArray(rhs) && _optionalChain([rhs, 'optionalAccess', _12 => _12.type]) === "regex") {
|
|
221
|
+
return new RegExp(rhs.pattern, rhs.flags).test(lhs);
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
case "!regex" /* NOT_REGEX */:
|
|
225
|
+
if (isString(lhs) && lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === "object" && !Array.isArray(rhs) && _optionalChain([rhs, 'optionalAccess', _13 => _13.type]) === "regex") {
|
|
226
|
+
return !new RegExp(rhs.pattern, rhs.flags).test(lhs);
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
case "before" /* BEFORE */: {
|
|
230
|
+
if (!isString(lhs) || !isString(rhs)) return false;
|
|
231
|
+
const a = new Date(lhs);
|
|
232
|
+
const b = new Date(rhs);
|
|
233
|
+
return a.getTime() < b.getTime();
|
|
234
|
+
}
|
|
235
|
+
case "after" /* AFTER */: {
|
|
236
|
+
if (!isString(lhs) || !isString(rhs)) return false;
|
|
237
|
+
const a = new Date(lhs);
|
|
238
|
+
const b = new Date(rhs);
|
|
239
|
+
return a.getTime() > b.getTime();
|
|
240
|
+
}
|
|
241
|
+
default: {
|
|
242
|
+
const _x = cmpKey;
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error("flags: Error matching conditions", error);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function sum(list) {
|
|
253
|
+
return list.reduce((acc, n) => acc + n, 0);
|
|
254
|
+
}
|
|
255
|
+
function handleSegmentOutcome(params, outcome) {
|
|
256
|
+
if (outcome === 1) return true;
|
|
257
|
+
switch (outcome.type) {
|
|
258
|
+
case "split": {
|
|
259
|
+
const lhs = access(outcome.base, params);
|
|
260
|
+
if (typeof lhs !== "string") return false;
|
|
261
|
+
const maxValue = 1e5;
|
|
262
|
+
if (outcome.passPromille <= 0) return false;
|
|
263
|
+
if (outcome.passPromille >= maxValue) return true;
|
|
264
|
+
const value = _jsxxhash.xxHash32.call(void 0, lhs, params.definition.seed) % maxValue;
|
|
265
|
+
return value < outcome.passPromille;
|
|
266
|
+
}
|
|
267
|
+
default: {
|
|
268
|
+
const { type } = outcome;
|
|
269
|
+
exhaustivenessCheck(type);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function getVariant(variants, index) {
|
|
274
|
+
if (index < 0 || index >= variants.length) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`@vercel/flags-core: Invalid variant index ${index}, variants length is ${variants.length}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return variants[index];
|
|
280
|
+
}
|
|
281
|
+
function handleOutcome(params, outcome) {
|
|
282
|
+
if (typeof outcome === "number") {
|
|
283
|
+
return {
|
|
284
|
+
value: getVariant(params.definition.variants, outcome),
|
|
285
|
+
outcomeType: "value" /* VALUE */
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
switch (outcome.type) {
|
|
289
|
+
case "split": {
|
|
290
|
+
const lhs = access(outcome.base, params);
|
|
291
|
+
const defaultOutcome = getVariant(
|
|
292
|
+
params.definition.variants,
|
|
293
|
+
outcome.defaultVariant
|
|
294
|
+
);
|
|
295
|
+
if (typeof lhs !== "string") {
|
|
296
|
+
return { value: defaultOutcome, outcomeType: "split" /* SPLIT */ };
|
|
297
|
+
}
|
|
298
|
+
const maxValue = 4294967295;
|
|
299
|
+
const value = _jsxxhash.xxHash32.call(void 0, lhs, params.definition.seed);
|
|
300
|
+
const sumOfWeights = sum(outcome.weights);
|
|
301
|
+
const scaledWeights = outcome.weights.map(
|
|
302
|
+
(weight) => weight / sumOfWeights * maxValue
|
|
303
|
+
);
|
|
304
|
+
const variantIndex = findWeightedIndex(scaledWeights, value, maxValue);
|
|
305
|
+
return {
|
|
306
|
+
value: variantIndex === -1 ? defaultOutcome : getVariant(params.definition.variants, variantIndex),
|
|
307
|
+
outcomeType: "split" /* SPLIT */
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
default: {
|
|
311
|
+
const { type } = outcome;
|
|
312
|
+
exhaustivenessCheck(type);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function evaluate(params, _visited) {
|
|
317
|
+
const envConfig = params.definition.environments[params.environment];
|
|
318
|
+
if (typeof envConfig === "number") {
|
|
319
|
+
return Object.assign(handleOutcome(params, envConfig), {
|
|
320
|
+
reason: "paused" /* PAUSED */
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (!envConfig) {
|
|
324
|
+
return {
|
|
325
|
+
reason: "error" /* ERROR */,
|
|
326
|
+
errorMessage: `Could not find envConfig for "${params.environment}"`,
|
|
327
|
+
value: params.defaultValue
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if ("reuse" in envConfig) {
|
|
331
|
+
const reuseEnvConfig = params.definition.environments[envConfig.reuse];
|
|
332
|
+
if (reuseEnvConfig === void 0) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Could not find envConfig for "${envConfig.reuse}" when reusing`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const visited = _nullishCoalesce(_visited, () => ( /* @__PURE__ */ new Set()));
|
|
338
|
+
if (visited.has(envConfig.reuse)) {
|
|
339
|
+
return {
|
|
340
|
+
reason: "error" /* ERROR */,
|
|
341
|
+
errorMessage: `Circular environment reuse detected: "${envConfig.reuse}"`,
|
|
342
|
+
value: params.defaultValue
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
visited.add(params.environment);
|
|
346
|
+
return evaluate({ ...params, environment: envConfig.reuse }, visited);
|
|
347
|
+
}
|
|
348
|
+
if (envConfig.targets) {
|
|
349
|
+
const matchedIndex = envConfig.targets.findIndex(
|
|
350
|
+
(targetList) => matchTargetList(targetList, params)
|
|
351
|
+
);
|
|
352
|
+
if (matchedIndex > -1) {
|
|
353
|
+
return Object.assign(handleOutcome(params, matchedIndex), {
|
|
354
|
+
reason: "target_match" /* TARGET_MATCH */
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const firstMatchingRule = envConfig.rules ? envConfig.rules.find((rule) => matchConditions(rule.conditions, params)) : void 0;
|
|
359
|
+
if (firstMatchingRule) {
|
|
360
|
+
return Object.assign(handleOutcome(params, firstMatchingRule.outcome), {
|
|
361
|
+
reason: "rule_match" /* RULE_MATCH */
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return Object.assign(handleOutcome(params, envConfig.fallthrough), {
|
|
365
|
+
reason: "fallthrough" /* FALLTHROUGH */
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function findWeightedIndex(weights, value, maxValue) {
|
|
369
|
+
if (value < 0 || value >= maxValue) return -1;
|
|
370
|
+
let sum2 = 0;
|
|
371
|
+
for (let i = 0; i < weights.length; i++) {
|
|
372
|
+
sum2 += weights[i];
|
|
373
|
+
if (value < sum2) return i;
|
|
374
|
+
}
|
|
375
|
+
return -1;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// package.json
|
|
379
|
+
var version = "1.1.0-3ca407e-20260306094417";
|
|
380
|
+
|
|
381
|
+
// src/lib/report-value.ts
|
|
382
|
+
function internalReportValue(key, value, data) {
|
|
383
|
+
const symbol = /* @__PURE__ */ Symbol.for("@vercel/request-context");
|
|
384
|
+
const ctx = _optionalChain([Reflect, 'access', _14 => _14.get, 'call', _15 => _15(globalThis, symbol), 'optionalAccess', _16 => _16.get, 'call', _17 => _17()]);
|
|
385
|
+
_optionalChain([ctx, 'optionalAccess', _18 => _18.flags, 'optionalAccess', _19 => _19.reportValue, 'call', _20 => _20(key, value, {
|
|
386
|
+
sdkVersion: version,
|
|
387
|
+
...data
|
|
388
|
+
})]);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/controller-fns.ts
|
|
392
|
+
var controllerInstanceMap = /* @__PURE__ */ new Map();
|
|
393
|
+
function getInstance(id) {
|
|
394
|
+
const instance = controllerInstanceMap.get(id);
|
|
395
|
+
if (!instance) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`@vercel/flags-core: Client instance ${id} not found. It may have been shut down.`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return instance;
|
|
401
|
+
}
|
|
402
|
+
function initialize(id) {
|
|
403
|
+
return getInstance(id).controller.initialize();
|
|
404
|
+
}
|
|
405
|
+
function shutdown(id) {
|
|
406
|
+
return getInstance(id).controller.shutdown();
|
|
407
|
+
}
|
|
408
|
+
function getDatafile(id) {
|
|
409
|
+
return getInstance(id).controller.getDatafile();
|
|
410
|
+
}
|
|
411
|
+
function getFallbackDatafile(id) {
|
|
412
|
+
const ds = getInstance(id).controller;
|
|
413
|
+
if (ds.getFallbackDatafile) return ds.getFallbackDatafile();
|
|
414
|
+
throw new Error("flags: This data source does not support fallbacks");
|
|
415
|
+
}
|
|
416
|
+
async function evaluate2(id, flagKey, defaultValue, entities) {
|
|
417
|
+
const controller = getInstance(id).controller;
|
|
418
|
+
let datafile;
|
|
419
|
+
try {
|
|
420
|
+
datafile = await controller.read();
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (defaultValue !== void 0) {
|
|
423
|
+
return {
|
|
424
|
+
value: defaultValue,
|
|
425
|
+
reason: "error" /* ERROR */,
|
|
426
|
+
errorMessage: error instanceof Error ? error.message : "Failed to read datafile"
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
const flagDefinition = datafile.definitions[flagKey];
|
|
432
|
+
if (flagDefinition === void 0) {
|
|
433
|
+
if (datafile.projectId) {
|
|
434
|
+
internalReportValue(flagKey, defaultValue, {
|
|
435
|
+
originProjectId: datafile.projectId,
|
|
436
|
+
originProvider: "vercel",
|
|
437
|
+
reason: "error" /* ERROR */
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
value: defaultValue,
|
|
442
|
+
reason: "error" /* ERROR */,
|
|
443
|
+
errorCode: "FLAG_NOT_FOUND" /* FLAG_NOT_FOUND */,
|
|
444
|
+
errorMessage: `@vercel/flags-core: Definition not found for flag "${flagKey}"`,
|
|
445
|
+
metrics: {
|
|
446
|
+
evaluationMs: 0,
|
|
447
|
+
readMs: datafile.metrics.readMs,
|
|
448
|
+
source: datafile.metrics.source,
|
|
449
|
+
cacheStatus: datafile.metrics.cacheStatus,
|
|
450
|
+
connectionState: datafile.metrics.connectionState,
|
|
451
|
+
mode: datafile.metrics.mode
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
const evalStartTime = Date.now();
|
|
456
|
+
const result = evaluate({
|
|
457
|
+
defaultValue,
|
|
458
|
+
definition: flagDefinition,
|
|
459
|
+
environment: datafile.environment,
|
|
460
|
+
entities: _nullishCoalesce(entities, () => ( {})),
|
|
461
|
+
segments: datafile.segments
|
|
462
|
+
});
|
|
463
|
+
const evaluationDurationMs = Date.now() - evalStartTime;
|
|
464
|
+
if (datafile.projectId) {
|
|
465
|
+
internalReportValue(flagKey, result.value, {
|
|
466
|
+
originProjectId: datafile.projectId,
|
|
467
|
+
originProvider: "vercel",
|
|
468
|
+
reason: result.reason,
|
|
469
|
+
outcomeType: result.reason !== "error" /* ERROR */ ? result.outcomeType : void 0
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return Object.assign(result, {
|
|
473
|
+
metrics: {
|
|
474
|
+
evaluationMs: evaluationDurationMs,
|
|
475
|
+
readMs: datafile.metrics.readMs,
|
|
476
|
+
source: datafile.metrics.source,
|
|
477
|
+
cacheStatus: datafile.metrics.cacheStatus,
|
|
478
|
+
connectionState: datafile.metrics.connectionState,
|
|
479
|
+
mode: datafile.metrics.mode
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/create-raw-client.ts
|
|
485
|
+
var idCount = 0;
|
|
486
|
+
async function performInitialize(instance, initFn) {
|
|
487
|
+
try {
|
|
488
|
+
await initFn();
|
|
489
|
+
instance.initialized = true;
|
|
490
|
+
} catch (error) {
|
|
491
|
+
instance.initPromise = null;
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function createCreateRawClient(fns) {
|
|
496
|
+
return function createRawClient({
|
|
497
|
+
controller,
|
|
498
|
+
origin
|
|
499
|
+
}) {
|
|
500
|
+
const id = idCount++;
|
|
501
|
+
controllerInstanceMap.set(id, {
|
|
502
|
+
controller,
|
|
503
|
+
initialized: false,
|
|
504
|
+
initPromise: null
|
|
505
|
+
});
|
|
506
|
+
const api = {
|
|
507
|
+
origin,
|
|
508
|
+
initialize: async () => {
|
|
509
|
+
let instance = controllerInstanceMap.get(id);
|
|
510
|
+
if (!instance) {
|
|
511
|
+
instance = { controller, initialized: false, initPromise: null };
|
|
512
|
+
controllerInstanceMap.set(id, instance);
|
|
513
|
+
}
|
|
514
|
+
if (instance.initialized) return;
|
|
515
|
+
if (!instance.initPromise) {
|
|
516
|
+
instance.initPromise = performInitialize(
|
|
517
|
+
instance,
|
|
518
|
+
() => fns.initialize(id)
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return instance.initPromise;
|
|
522
|
+
},
|
|
523
|
+
shutdown: async () => {
|
|
524
|
+
await fns.shutdown(id);
|
|
525
|
+
controllerInstanceMap.delete(id);
|
|
526
|
+
},
|
|
527
|
+
getDatafile: async () => {
|
|
528
|
+
const instance = controllerInstanceMap.get(id);
|
|
529
|
+
if (_optionalChain([instance, 'optionalAccess', _21 => _21.initPromise])) {
|
|
530
|
+
try {
|
|
531
|
+
await instance.initPromise;
|
|
532
|
+
} catch (e) {
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return fns.getDatafile(id);
|
|
536
|
+
},
|
|
537
|
+
getFallbackDatafile: () => {
|
|
538
|
+
return fns.getFallbackDatafile(id);
|
|
539
|
+
},
|
|
540
|
+
evaluate: async (flagKey, defaultValue, entities) => {
|
|
541
|
+
const instance = controllerInstanceMap.get(id);
|
|
542
|
+
if (!_optionalChain([instance, 'optionalAccess', _22 => _22.initialized])) {
|
|
543
|
+
try {
|
|
544
|
+
await api.initialize();
|
|
545
|
+
} catch (e2) {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return fns.evaluate(id, flagKey, defaultValue, entities);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
return api;
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/utils/read-bundled-definitions.ts
|
|
556
|
+
var sdkKeyHashCache = /* @__PURE__ */ new Map();
|
|
557
|
+
async function computeHash(sdkKey) {
|
|
558
|
+
const hashBuffer = await crypto.subtle.digest(
|
|
559
|
+
"SHA-256",
|
|
560
|
+
new TextEncoder().encode(sdkKey)
|
|
561
|
+
);
|
|
562
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
563
|
+
return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
564
|
+
}
|
|
565
|
+
function hashSdkKey(sdkKey) {
|
|
566
|
+
const cached = sdkKeyHashCache.get(sdkKey);
|
|
567
|
+
if (cached) return cached;
|
|
568
|
+
const promise = computeHash(sdkKey);
|
|
569
|
+
sdkKeyHashCache.set(sdkKey, promise);
|
|
570
|
+
return promise;
|
|
571
|
+
}
|
|
572
|
+
async function readBundledDefinitions(sdkKey) {
|
|
573
|
+
let get;
|
|
574
|
+
try {
|
|
575
|
+
const module = await Promise.resolve().then(() => _interopRequireWildcard(require(
|
|
576
|
+
/* turbopackOptional: true */
|
|
577
|
+
// @ts-expect-error this only exists at build time
|
|
578
|
+
"@vercel/flags-definitions"
|
|
579
|
+
)));
|
|
580
|
+
get = module.get;
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (error && typeof error === "object" && "code" in error && error.code === "MODULE_NOT_FOUND") {
|
|
583
|
+
return { definitions: null, state: "missing-file" };
|
|
584
|
+
}
|
|
585
|
+
return { definitions: null, state: "unexpected-error", error };
|
|
586
|
+
}
|
|
587
|
+
const entry = get(sdkKey);
|
|
588
|
+
if (entry) return { definitions: entry, state: "ok" };
|
|
589
|
+
try {
|
|
590
|
+
const hashedKey = await hashSdkKey(sdkKey);
|
|
591
|
+
const hashedEntry = get(hashedKey);
|
|
592
|
+
if (hashedEntry) return { definitions: hashedEntry, state: "ok" };
|
|
593
|
+
} catch (error) {
|
|
594
|
+
return { definitions: null, state: "unexpected-error", error };
|
|
595
|
+
}
|
|
596
|
+
return { definitions: null, state: "missing-entry" };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/utils/usage-tracker.ts
|
|
600
|
+
var _functions = require('@vercel/functions');
|
|
601
|
+
var RESOLVED_VOID = Promise.resolve();
|
|
602
|
+
var isDebugMode = _optionalChain([process, 'access', _23 => _23.env, 'access', _24 => _24.DEBUG, 'optionalAccess', _25 => _25.includes, 'call', _26 => _26("@vercel/flags-core")]);
|
|
603
|
+
var debugLog = (...args) => {
|
|
604
|
+
if (!isDebugMode) return;
|
|
605
|
+
console.log(...args);
|
|
606
|
+
};
|
|
607
|
+
var MAX_RETRIES = 3;
|
|
608
|
+
var MAX_BATCH_SIZE = 50;
|
|
609
|
+
var MAX_BATCH_WAIT_MS = 5e3;
|
|
610
|
+
var SYMBOL_FOR_REQ_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context");
|
|
611
|
+
var fromSymbol = globalThis;
|
|
612
|
+
function getRequestContext() {
|
|
613
|
+
try {
|
|
614
|
+
const ctx = _optionalChain([fromSymbol, 'access', _27 => _27[SYMBOL_FOR_REQ_CONTEXT], 'optionalAccess', _28 => _28.get, 'optionalCall', _29 => _29()]);
|
|
615
|
+
if (ctx && Object.hasOwn(ctx, "headers")) {
|
|
616
|
+
return {
|
|
617
|
+
ctx,
|
|
618
|
+
headers: ctx.headers
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return { ctx, headers: void 0 };
|
|
622
|
+
} catch (e3) {
|
|
623
|
+
return { ctx: void 0, headers: void 0 };
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
var UsageTracker = (_class = class {
|
|
627
|
+
__init() {this.flushCounter = 0}
|
|
628
|
+
|
|
629
|
+
__init2() {this.trackedRequests = /* @__PURE__ */ new WeakSet()}
|
|
630
|
+
__init3() {this.batcher = {
|
|
631
|
+
events: [],
|
|
632
|
+
resolveWait: null,
|
|
633
|
+
pending: null
|
|
634
|
+
}}
|
|
635
|
+
constructor(options) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);
|
|
636
|
+
this.options = options;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Triggers an immediate flush of any pending events.
|
|
640
|
+
* Returns a promise that resolves when the flush completes.
|
|
641
|
+
*/
|
|
642
|
+
flush() {
|
|
643
|
+
if (this.batcher.pending) {
|
|
644
|
+
_optionalChain([this, 'access', _30 => _30.batcher, 'access', _31 => _31.resolveWait, 'optionalCall', _32 => _32()]);
|
|
645
|
+
return this.batcher.pending;
|
|
646
|
+
}
|
|
647
|
+
if (this.batcher.events.length > 0) {
|
|
648
|
+
return this.flushEvents();
|
|
649
|
+
}
|
|
650
|
+
return RESOLVED_VOID;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Tracks a config read event. Deduplicates by request context.
|
|
654
|
+
*/
|
|
655
|
+
trackRead(options) {
|
|
656
|
+
try {
|
|
657
|
+
const { ctx, headers } = getRequestContext();
|
|
658
|
+
if (ctx) {
|
|
659
|
+
if (this.trackedRequests.has(ctx)) return;
|
|
660
|
+
this.trackedRequests.add(ctx);
|
|
661
|
+
}
|
|
662
|
+
const event = {
|
|
663
|
+
type: "FLAGS_CONFIG_READ",
|
|
664
|
+
ts: Date.now(),
|
|
665
|
+
payload: {
|
|
666
|
+
deploymentId: process.env.VERCEL_DEPLOYMENT_ID,
|
|
667
|
+
region: process.env.VERCEL_REGION
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
if (headers) {
|
|
671
|
+
event.payload.vercelRequestId = _nullishCoalesce(headers["x-vercel-id"], () => ( void 0));
|
|
672
|
+
event.payload.invocationHost = _nullishCoalesce(headers.host, () => ( void 0));
|
|
673
|
+
}
|
|
674
|
+
if (options) {
|
|
675
|
+
event.payload.configOrigin = options.configOrigin;
|
|
676
|
+
if (options.cacheStatus !== void 0) {
|
|
677
|
+
event.payload.cacheStatus = options.cacheStatus;
|
|
678
|
+
}
|
|
679
|
+
if (options.cacheAction !== void 0) {
|
|
680
|
+
event.payload.cacheAction = options.cacheAction;
|
|
681
|
+
}
|
|
682
|
+
if (options.cacheIsFirstRead !== void 0) {
|
|
683
|
+
event.payload.cacheIsFirstRead = options.cacheIsFirstRead;
|
|
684
|
+
}
|
|
685
|
+
if (options.cacheIsBlocking !== void 0) {
|
|
686
|
+
event.payload.cacheIsBlocking = options.cacheIsBlocking;
|
|
687
|
+
}
|
|
688
|
+
if (options.duration !== void 0) {
|
|
689
|
+
event.payload.duration = options.duration;
|
|
690
|
+
}
|
|
691
|
+
if (options.configUpdatedAt !== void 0) {
|
|
692
|
+
event.payload.configUpdatedAt = options.configUpdatedAt;
|
|
693
|
+
}
|
|
694
|
+
if (options.mode !== void 0) {
|
|
695
|
+
event.payload.mode = options.mode;
|
|
696
|
+
}
|
|
697
|
+
if (options.revision !== void 0) {
|
|
698
|
+
event.payload.revision = String(options.revision);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const environment = process.env.VERCEL_ENV || process.env.NODE_ENV || void 0;
|
|
702
|
+
if (environment) {
|
|
703
|
+
event.payload.environment = environment;
|
|
704
|
+
}
|
|
705
|
+
this.batcher.events.push(event);
|
|
706
|
+
this.scheduleFlush();
|
|
707
|
+
} catch (error) {
|
|
708
|
+
console.error("@vercel/flags-core: Failed to record event:", error);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
scheduleFlush() {
|
|
712
|
+
if (!this.batcher.pending) {
|
|
713
|
+
let timeout = null;
|
|
714
|
+
const pending = (async () => {
|
|
715
|
+
await new Promise((res) => {
|
|
716
|
+
this.batcher.resolveWait = res;
|
|
717
|
+
timeout = setTimeout(res, MAX_BATCH_WAIT_MS);
|
|
718
|
+
});
|
|
719
|
+
this.batcher.pending = null;
|
|
720
|
+
this.batcher.resolveWait = null;
|
|
721
|
+
if (timeout) clearTimeout(timeout);
|
|
722
|
+
await this.flushEvents();
|
|
723
|
+
})();
|
|
724
|
+
try {
|
|
725
|
+
_functions.waitUntil.call(void 0, pending);
|
|
726
|
+
} catch (e4) {
|
|
727
|
+
}
|
|
728
|
+
this.batcher.pending = pending;
|
|
729
|
+
}
|
|
730
|
+
if (this.batcher.events.length >= MAX_BATCH_SIZE) {
|
|
731
|
+
_optionalChain([this, 'access', _33 => _33.batcher, 'access', _34 => _34.resolveWait, 'optionalCall', _35 => _35()]);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async flushEvents() {
|
|
735
|
+
if (this.batcher.events.length === 0) return;
|
|
736
|
+
const eventsToSend = this.batcher.events;
|
|
737
|
+
this.batcher.events = [];
|
|
738
|
+
const flushId = ++this.flushCounter;
|
|
739
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
740
|
+
try {
|
|
741
|
+
const response = await this.options.fetch(
|
|
742
|
+
`${this.options.host}/v1/ingest`,
|
|
743
|
+
{
|
|
744
|
+
method: "POST",
|
|
745
|
+
headers: {
|
|
746
|
+
"Content-Type": "application/json",
|
|
747
|
+
Authorization: `Bearer ${this.options.sdkKey}`,
|
|
748
|
+
"User-Agent": `VercelFlagsCore/${version}`,
|
|
749
|
+
...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
|
|
750
|
+
},
|
|
751
|
+
body: JSON.stringify(eventsToSend)
|
|
752
|
+
}
|
|
753
|
+
);
|
|
754
|
+
debugLog(
|
|
755
|
+
`@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
|
|
756
|
+
);
|
|
757
|
+
if (response.ok) {
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
throw new Error(
|
|
761
|
+
`Ingest endpoint responded with status ${response.status} for ${eventsToSend.length} events on request ${response.headers.get("x-vercel-id")}.
|
|
762
|
+
Response body: ${await response.text().catch(() => null)}`
|
|
763
|
+
);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
console.error(
|
|
766
|
+
`@vercel/flags-core: Error sending events (attempt=${attempt}/${MAX_RETRIES} flushId=${flushId}):`,
|
|
767
|
+
error
|
|
768
|
+
);
|
|
769
|
+
if (attempt < MAX_RETRIES) {
|
|
770
|
+
await new Promise((res) => setTimeout(res, attempt * 100));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}, _class);
|
|
776
|
+
|
|
777
|
+
// src/errors.ts
|
|
778
|
+
var FallbackNotFoundError = class extends Error {
|
|
779
|
+
constructor() {
|
|
780
|
+
super("@vercel/flags-core: Bundled definitions file not found.");
|
|
781
|
+
this.name = "FallbackNotFoundError";
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
var FallbackEntryNotFoundError = class extends Error {
|
|
785
|
+
constructor() {
|
|
786
|
+
super("@vercel/flags-core: No bundled definitions found for SDK key.");
|
|
787
|
+
this.name = "FallbackEntryNotFoundError";
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// src/controller/bundled-source.ts
|
|
792
|
+
var BundledSource = class {
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
constructor(options) {
|
|
796
|
+
this.options = options;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Load bundled definitions.
|
|
800
|
+
* Throws if bundled definitions are not available.
|
|
801
|
+
*/
|
|
802
|
+
async load() {
|
|
803
|
+
const result = await this.getResult();
|
|
804
|
+
if (result.state === "ok" && result.definitions) {
|
|
805
|
+
return result.definitions;
|
|
806
|
+
}
|
|
807
|
+
throw new Error(
|
|
808
|
+
"@vercel/flags-core: No flag definitions available. Bundled definitions not found."
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Get the raw BundledDefinitions (for getFallbackDatafile).
|
|
813
|
+
* Throws typed errors if not available.
|
|
814
|
+
*/
|
|
815
|
+
async getRaw() {
|
|
816
|
+
const result = await this.getResult();
|
|
817
|
+
switch (result.state) {
|
|
818
|
+
case "ok":
|
|
819
|
+
return result.definitions;
|
|
820
|
+
case "missing-file":
|
|
821
|
+
throw new FallbackNotFoundError();
|
|
822
|
+
case "missing-entry":
|
|
823
|
+
throw new FallbackEntryNotFoundError();
|
|
824
|
+
case "unexpected-error":
|
|
825
|
+
throw new Error(
|
|
826
|
+
"@vercel/flags-core: Failed to read bundled definitions: " + String(result.error)
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Check if bundled definitions loaded successfully (without throwing).
|
|
832
|
+
*/
|
|
833
|
+
async tryLoad() {
|
|
834
|
+
const result = await this.getResult();
|
|
835
|
+
if (_optionalChain([result, 'optionalAccess', _36 => _36.state]) === "ok" && result.definitions) {
|
|
836
|
+
return result.definitions;
|
|
837
|
+
}
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
getResult() {
|
|
841
|
+
if (!this.promise) {
|
|
842
|
+
this.promise = this.options.readBundledDefinitions(this.options.sdkKey);
|
|
843
|
+
}
|
|
844
|
+
return this.promise;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// src/controller/fetch-datafile.ts
|
|
849
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
850
|
+
async function fetchDatafile(options) {
|
|
851
|
+
const controller = new AbortController();
|
|
852
|
+
const timeoutId = setTimeout(
|
|
853
|
+
() => controller.abort(),
|
|
854
|
+
DEFAULT_FETCH_TIMEOUT_MS
|
|
855
|
+
);
|
|
856
|
+
const onExternalAbort = () => controller.abort();
|
|
857
|
+
if (options.signal) {
|
|
858
|
+
if (options.signal.aborted) {
|
|
859
|
+
clearTimeout(timeoutId);
|
|
860
|
+
throw new Error("Fetch aborted");
|
|
861
|
+
}
|
|
862
|
+
options.signal.addEventListener("abort", onExternalAbort, { once: true });
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
const res = await options.fetch(`${options.host}/v1/datafile`, {
|
|
866
|
+
headers: {
|
|
867
|
+
Authorization: `Bearer ${options.sdkKey}`,
|
|
868
|
+
"User-Agent": `VercelFlagsCore/${version}`
|
|
869
|
+
},
|
|
870
|
+
signal: controller.signal
|
|
871
|
+
});
|
|
872
|
+
clearTimeout(timeoutId);
|
|
873
|
+
_optionalChain([options, 'access', _37 => _37.signal, 'optionalAccess', _38 => _38.removeEventListener, 'call', _39 => _39("abort", onExternalAbort)]);
|
|
874
|
+
if (!res.ok) {
|
|
875
|
+
throw new Error(`Failed to fetch data: ${res.statusText}`);
|
|
876
|
+
}
|
|
877
|
+
return res.json();
|
|
878
|
+
} catch (error) {
|
|
879
|
+
clearTimeout(timeoutId);
|
|
880
|
+
_optionalChain([options, 'access', _40 => _40.signal, 'optionalAccess', _41 => _41.removeEventListener, 'call', _42 => _42("abort", onExternalAbort)]);
|
|
881
|
+
throw error instanceof Error ? error : new Error("Unknown fetch error");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/controller/normalized-options.ts
|
|
886
|
+
var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
|
|
887
|
+
var DEFAULT_POLLING_INTERVAL_MS = 3e4;
|
|
888
|
+
var MIN_POLLING_INTERVAL_MS = 3e4;
|
|
889
|
+
var DEFAULT_POLLING_INIT_TIMEOUT_MS = 3e3;
|
|
890
|
+
function normalizeOptions(options) {
|
|
891
|
+
const autoDetectedBuildStep = process.env.CI === "1" || process.env.NEXT_PHASE === "phase-production-build";
|
|
892
|
+
const buildStep = _nullishCoalesce(options.buildStep, () => ( autoDetectedBuildStep));
|
|
893
|
+
let stream;
|
|
894
|
+
if (options.stream === void 0 || options.stream === true) {
|
|
895
|
+
stream = { enabled: true, initTimeoutMs: DEFAULT_STREAM_INIT_TIMEOUT_MS };
|
|
896
|
+
} else if (options.stream === false) {
|
|
897
|
+
stream = { enabled: false, initTimeoutMs: 0 };
|
|
898
|
+
} else {
|
|
899
|
+
stream = { enabled: true, initTimeoutMs: options.stream.initTimeoutMs };
|
|
900
|
+
}
|
|
901
|
+
let polling;
|
|
902
|
+
if (options.polling === void 0 || options.polling === true) {
|
|
903
|
+
polling = {
|
|
904
|
+
enabled: true,
|
|
905
|
+
intervalMs: DEFAULT_POLLING_INTERVAL_MS,
|
|
906
|
+
initTimeoutMs: DEFAULT_POLLING_INIT_TIMEOUT_MS
|
|
907
|
+
};
|
|
908
|
+
} else if (options.polling === false) {
|
|
909
|
+
polling = { enabled: false, intervalMs: 0, initTimeoutMs: 0 };
|
|
910
|
+
} else {
|
|
911
|
+
if (options.polling.intervalMs < MIN_POLLING_INTERVAL_MS) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`@vercel/flags-core: Polling interval must be at least ${MIN_POLLING_INTERVAL_MS}ms, got ${options.polling.intervalMs}ms.`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
polling = {
|
|
917
|
+
enabled: true,
|
|
918
|
+
intervalMs: options.polling.intervalMs,
|
|
919
|
+
initTimeoutMs: options.polling.initTimeoutMs
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
sdkKey: options.sdkKey,
|
|
924
|
+
datafile: options.datafile,
|
|
925
|
+
stream,
|
|
926
|
+
polling,
|
|
927
|
+
buildStep,
|
|
928
|
+
fetch: _nullishCoalesce(options.fetch, () => ( globalThis.fetch)),
|
|
929
|
+
host: "https://flags.vercel.com"
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/controller/typed-emitter.ts
|
|
934
|
+
var TypedEmitter = (_class2 = class {constructor() { _class2.prototype.__init4.call(this); }
|
|
935
|
+
__init4() {this.handlers = /* @__PURE__ */ new Map()}
|
|
936
|
+
on(event, handler) {
|
|
937
|
+
let set = this.handlers.get(event);
|
|
938
|
+
if (!set) {
|
|
939
|
+
set = /* @__PURE__ */ new Set();
|
|
940
|
+
this.handlers.set(event, set);
|
|
941
|
+
}
|
|
942
|
+
set.add(handler);
|
|
943
|
+
}
|
|
944
|
+
off(event, handler) {
|
|
945
|
+
_optionalChain([this, 'access', _43 => _43.handlers, 'access', _44 => _44.get, 'call', _45 => _45(event), 'optionalAccess', _46 => _46.delete, 'call', _47 => _47(handler)]);
|
|
946
|
+
}
|
|
947
|
+
emit(event, ...args) {
|
|
948
|
+
const set = this.handlers.get(event);
|
|
949
|
+
if (set) {
|
|
950
|
+
for (const handler of set) {
|
|
951
|
+
handler(...args);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}, _class2);
|
|
956
|
+
|
|
957
|
+
// src/controller/polling-source.ts
|
|
958
|
+
var PollingSource = class extends TypedEmitter {
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
constructor(config) {
|
|
963
|
+
super();
|
|
964
|
+
this.config = config;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Perform a single poll request.
|
|
968
|
+
* Emits 'data' on success, 'error' on failure.
|
|
969
|
+
*/
|
|
970
|
+
async poll() {
|
|
971
|
+
if (_optionalChain([this, 'access', _48 => _48.abortController, 'optionalAccess', _49 => _49.signal, 'access', _50 => _50.aborted])) return;
|
|
972
|
+
try {
|
|
973
|
+
const data = await fetchDatafile({
|
|
974
|
+
...this.config,
|
|
975
|
+
signal: _optionalChain([this, 'access', _51 => _51.abortController, 'optionalAccess', _52 => _52.signal])
|
|
976
|
+
});
|
|
977
|
+
this.emit("data", data);
|
|
978
|
+
} catch (error) {
|
|
979
|
+
const err = error instanceof Error ? error : new Error("Unknown poll error");
|
|
980
|
+
this.emit("error", err);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Start interval-based polling.
|
|
985
|
+
* Polls at the configured interval. Does not perform an initial poll —
|
|
986
|
+
* callers should call poll() first if an immediate poll is needed.
|
|
987
|
+
*/
|
|
988
|
+
startInterval() {
|
|
989
|
+
if (this.intervalId) return;
|
|
990
|
+
this.abortController = new AbortController();
|
|
991
|
+
this.intervalId = setInterval(
|
|
992
|
+
() => void this.poll(),
|
|
993
|
+
this.config.polling.intervalMs
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Stop interval-based polling.
|
|
998
|
+
*/
|
|
999
|
+
stop() {
|
|
1000
|
+
if (this.intervalId) {
|
|
1001
|
+
clearInterval(this.intervalId);
|
|
1002
|
+
this.intervalId = void 0;
|
|
1003
|
+
}
|
|
1004
|
+
_optionalChain([this, 'access', _53 => _53.abortController, 'optionalAccess', _54 => _54.abort, 'call', _55 => _55()]);
|
|
1005
|
+
this.abortController = void 0;
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
// src/utils/sleep.ts
|
|
1010
|
+
function sleep(ms) {
|
|
1011
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/controller/stream-connection.ts
|
|
1015
|
+
var MAX_RETRY_COUNT = 15;
|
|
1016
|
+
var BASE_RETRY_DELAY_MS = 1e3;
|
|
1017
|
+
var MAX_RETRY_DELAY_MS = 6e4;
|
|
1018
|
+
var PING_TIMEOUT_MS = 9e4;
|
|
1019
|
+
function backoff(retryCount) {
|
|
1020
|
+
if (retryCount === 1) return 0;
|
|
1021
|
+
const delay = Math.min(
|
|
1022
|
+
BASE_RETRY_DELAY_MS * 2 ** (retryCount - 2),
|
|
1023
|
+
MAX_RETRY_DELAY_MS
|
|
1024
|
+
);
|
|
1025
|
+
return delay + Math.random() * 1e3;
|
|
1026
|
+
}
|
|
1027
|
+
var UnauthorizedError = class extends Error {
|
|
1028
|
+
constructor() {
|
|
1029
|
+
super("stream: unauthorized (401)");
|
|
1030
|
+
this.name = "UnauthorizedError";
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
async function connectStream(config, callbacks) {
|
|
1034
|
+
const {
|
|
1035
|
+
host,
|
|
1036
|
+
sdkKey,
|
|
1037
|
+
abortController,
|
|
1038
|
+
fetch: fetchFn = globalThis.fetch
|
|
1039
|
+
} = config;
|
|
1040
|
+
const { onDatafile, onPrimed, onDisconnect } = callbacks;
|
|
1041
|
+
let retryCount = 0;
|
|
1042
|
+
let lastAttemptTime = 0;
|
|
1043
|
+
let resolveInit;
|
|
1044
|
+
let rejectInit;
|
|
1045
|
+
const initPromise = new Promise((resolve, reject) => {
|
|
1046
|
+
resolveInit = resolve;
|
|
1047
|
+
rejectInit = reject;
|
|
1048
|
+
});
|
|
1049
|
+
void (async () => {
|
|
1050
|
+
let initialDataReceived = false;
|
|
1051
|
+
while (!abortController.signal.aborted) {
|
|
1052
|
+
if (retryCount > MAX_RETRY_COUNT) {
|
|
1053
|
+
console.error("@vercel/flags-core: Max retry count exceeded");
|
|
1054
|
+
if (!initialDataReceived) {
|
|
1055
|
+
rejectInit(
|
|
1056
|
+
new Error("stream: max retry count exceeded before receiving data")
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
abortController.abort();
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
const connectionAbort = new AbortController();
|
|
1063
|
+
const onMainAbort = () => connectionAbort.abort();
|
|
1064
|
+
abortController.signal.addEventListener("abort", onMainAbort, {
|
|
1065
|
+
once: true
|
|
1066
|
+
});
|
|
1067
|
+
let pingTimeoutId;
|
|
1068
|
+
let responseBody;
|
|
1069
|
+
const resetPingTimeout = () => {
|
|
1070
|
+
if (pingTimeoutId !== void 0) clearTimeout(pingTimeoutId);
|
|
1071
|
+
if (!initialDataReceived) return;
|
|
1072
|
+
pingTimeoutId = setTimeout(() => {
|
|
1073
|
+
_optionalChain([responseBody, 'optionalAccess', _56 => _56.cancel, 'call', _57 => _57(), 'access', _58 => _58.catch, 'call', _59 => _59(() => {
|
|
1074
|
+
})]);
|
|
1075
|
+
connectionAbort.abort();
|
|
1076
|
+
}, PING_TIMEOUT_MS);
|
|
1077
|
+
};
|
|
1078
|
+
try {
|
|
1079
|
+
lastAttemptTime = Date.now();
|
|
1080
|
+
const headers = {
|
|
1081
|
+
Authorization: `Bearer ${sdkKey}`,
|
|
1082
|
+
"User-Agent": `VercelFlagsCore/${version}`,
|
|
1083
|
+
"X-Retry-Attempt": String(retryCount)
|
|
1084
|
+
};
|
|
1085
|
+
const revision = _optionalChain([config, 'access', _60 => _60.revision, 'optionalCall', _61 => _61()]);
|
|
1086
|
+
if (revision !== void 0) {
|
|
1087
|
+
headers["X-Revision"] = String(revision);
|
|
1088
|
+
}
|
|
1089
|
+
const response = await fetchFn(`${host}/v1/stream`, {
|
|
1090
|
+
headers,
|
|
1091
|
+
signal: connectionAbort.signal
|
|
1092
|
+
});
|
|
1093
|
+
if (!response.ok) {
|
|
1094
|
+
if (response.status === 401) {
|
|
1095
|
+
if (!initialDataReceived) {
|
|
1096
|
+
rejectInit(new UnauthorizedError());
|
|
1097
|
+
}
|
|
1098
|
+
abortController.abort();
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
throw new Error(`stream was not ok: ${response.status}`);
|
|
1102
|
+
}
|
|
1103
|
+
if (!response.body) {
|
|
1104
|
+
throw new Error("stream body was not present");
|
|
1105
|
+
}
|
|
1106
|
+
responseBody = response.body;
|
|
1107
|
+
const reader = response.body.getReader();
|
|
1108
|
+
const decoder = new TextDecoder();
|
|
1109
|
+
const bufferChunks = [];
|
|
1110
|
+
const onConnectionAbort = () => {
|
|
1111
|
+
reader.cancel().catch(() => {
|
|
1112
|
+
});
|
|
1113
|
+
};
|
|
1114
|
+
connectionAbort.signal.addEventListener("abort", onConnectionAbort, {
|
|
1115
|
+
once: true
|
|
1116
|
+
});
|
|
1117
|
+
try {
|
|
1118
|
+
while (true) {
|
|
1119
|
+
const { done, value: chunk } = await reader.read();
|
|
1120
|
+
if (done || abortController.signal.aborted) break;
|
|
1121
|
+
bufferChunks.push(decoder.decode(chunk, { stream: true }));
|
|
1122
|
+
const combined = bufferChunks.join("");
|
|
1123
|
+
bufferChunks.length = 0;
|
|
1124
|
+
const lines = combined.split("\n");
|
|
1125
|
+
bufferChunks.push(lines.pop());
|
|
1126
|
+
for (const line of lines) {
|
|
1127
|
+
if (line === "") continue;
|
|
1128
|
+
let message;
|
|
1129
|
+
try {
|
|
1130
|
+
message = JSON.parse(line);
|
|
1131
|
+
} catch (e5) {
|
|
1132
|
+
console.warn(
|
|
1133
|
+
"@vercel/flags-core: Failed to parse stream message, skipping"
|
|
1134
|
+
);
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (message.type === "datafile") {
|
|
1138
|
+
onDatafile(message.data);
|
|
1139
|
+
retryCount = 0;
|
|
1140
|
+
if (!initialDataReceived) {
|
|
1141
|
+
initialDataReceived = true;
|
|
1142
|
+
resolveInit();
|
|
1143
|
+
}
|
|
1144
|
+
resetPingTimeout();
|
|
1145
|
+
}
|
|
1146
|
+
if (message.type === "primed") {
|
|
1147
|
+
_optionalChain([onPrimed, 'optionalCall', _62 => _62(message)]);
|
|
1148
|
+
retryCount = 0;
|
|
1149
|
+
if (!initialDataReceived) {
|
|
1150
|
+
initialDataReceived = true;
|
|
1151
|
+
resolveInit();
|
|
1152
|
+
}
|
|
1153
|
+
resetPingTimeout();
|
|
1154
|
+
}
|
|
1155
|
+
if (message.type === "ping" && initialDataReceived) {
|
|
1156
|
+
retryCount = 0;
|
|
1157
|
+
resetPingTimeout();
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
} finally {
|
|
1162
|
+
connectionAbort.signal.removeEventListener(
|
|
1163
|
+
"abort",
|
|
1164
|
+
onConnectionAbort
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
clearTimeout(pingTimeoutId);
|
|
1168
|
+
abortController.signal.removeEventListener("abort", onMainAbort);
|
|
1169
|
+
if (!abortController.signal.aborted) {
|
|
1170
|
+
_optionalChain([onDisconnect, 'optionalCall', _63 => _63()]);
|
|
1171
|
+
retryCount++;
|
|
1172
|
+
const elapsed = Date.now() - lastAttemptTime;
|
|
1173
|
+
const minGap = Math.max(0, BASE_RETRY_DELAY_MS - elapsed);
|
|
1174
|
+
await sleep(Math.max(backoff(retryCount), minGap));
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
clearTimeout(pingTimeoutId);
|
|
1179
|
+
abortController.signal.removeEventListener("abort", onMainAbort);
|
|
1180
|
+
if (abortController.signal.aborted) {
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
if (!connectionAbort.signal.aborted) {
|
|
1184
|
+
console.error("@vercel/flags-core: Stream error", error);
|
|
1185
|
+
}
|
|
1186
|
+
_optionalChain([onDisconnect, 'optionalCall', _64 => _64()]);
|
|
1187
|
+
retryCount++;
|
|
1188
|
+
const elapsed = Date.now() - lastAttemptTime;
|
|
1189
|
+
const minGap = Math.max(0, BASE_RETRY_DELAY_MS - elapsed);
|
|
1190
|
+
await sleep(Math.max(backoff(retryCount), minGap));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (!initialDataReceived) {
|
|
1194
|
+
rejectInit(new Error("stream: aborted before receiving data"));
|
|
1195
|
+
}
|
|
1196
|
+
})();
|
|
1197
|
+
return initPromise;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/controller/stream-source.ts
|
|
1201
|
+
var StreamSource = class extends TypedEmitter {
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
constructor(options, revision) {
|
|
1207
|
+
super();
|
|
1208
|
+
this.options = options;
|
|
1209
|
+
this.revision = revision;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Start the stream connection.
|
|
1213
|
+
* Returns a promise that resolves when the first datafile or primed message arrives.
|
|
1214
|
+
* If already started, returns the existing promise.
|
|
1215
|
+
*/
|
|
1216
|
+
start() {
|
|
1217
|
+
if (this.promise) return this.promise;
|
|
1218
|
+
const abortController = new AbortController();
|
|
1219
|
+
this.abortController = abortController;
|
|
1220
|
+
abortController.signal.addEventListener(
|
|
1221
|
+
"abort",
|
|
1222
|
+
() => {
|
|
1223
|
+
if (this.abortController === abortController) {
|
|
1224
|
+
this.promise = void 0;
|
|
1225
|
+
this.abortController = void 0;
|
|
1226
|
+
}
|
|
1227
|
+
},
|
|
1228
|
+
{ once: true }
|
|
1229
|
+
);
|
|
1230
|
+
try {
|
|
1231
|
+
const promise = connectStream(
|
|
1232
|
+
{
|
|
1233
|
+
host: this.options.host,
|
|
1234
|
+
sdkKey: this.options.sdkKey,
|
|
1235
|
+
abortController,
|
|
1236
|
+
fetch: this.options.fetch,
|
|
1237
|
+
revision: this.revision
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
onDatafile: (newData) => {
|
|
1241
|
+
this.emit("data", newData);
|
|
1242
|
+
this.emit("connected");
|
|
1243
|
+
},
|
|
1244
|
+
onPrimed: (message) => {
|
|
1245
|
+
this.emit("primed", message);
|
|
1246
|
+
this.emit("connected");
|
|
1247
|
+
},
|
|
1248
|
+
onDisconnect: () => {
|
|
1249
|
+
this.emit("disconnected");
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
);
|
|
1253
|
+
this.promise = promise;
|
|
1254
|
+
return promise;
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
this.promise = void 0;
|
|
1257
|
+
this.abortController = void 0;
|
|
1258
|
+
throw error;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Stop the stream connection.
|
|
1263
|
+
*/
|
|
1264
|
+
stop() {
|
|
1265
|
+
_optionalChain([this, 'access', _65 => _65.abortController, 'optionalAccess', _66 => _66.abort, 'call', _67 => _67()]);
|
|
1266
|
+
this.abortController = void 0;
|
|
1267
|
+
this.promise = void 0;
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
// src/controller/tagged-data.ts
|
|
1272
|
+
function tagData(data, origin) {
|
|
1273
|
+
return Object.assign(data, { _origin: origin });
|
|
1274
|
+
}
|
|
1275
|
+
function originToMetricsSource(origin) {
|
|
1276
|
+
switch (origin) {
|
|
1277
|
+
case "stream":
|
|
1278
|
+
case "poll":
|
|
1279
|
+
case "provided":
|
|
1280
|
+
return "in-memory";
|
|
1281
|
+
case "fetched":
|
|
1282
|
+
return "remote";
|
|
1283
|
+
case "bundled":
|
|
1284
|
+
return "embedded";
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// src/controller/index.ts
|
|
1289
|
+
function parseConfigUpdatedAt(value) {
|
|
1290
|
+
if (typeof value === "number") return value;
|
|
1291
|
+
if (typeof value === "string") {
|
|
1292
|
+
const parsed = Number(value);
|
|
1293
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1294
|
+
}
|
|
1295
|
+
return void 0;
|
|
1296
|
+
}
|
|
1297
|
+
var Controller = (_class3 = class {
|
|
1298
|
+
|
|
1299
|
+
// State machine
|
|
1300
|
+
__init5() {this.state = "idle"}
|
|
1301
|
+
// Data state — tagged with origin
|
|
1302
|
+
|
|
1303
|
+
// Sources (I/O delegates)
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
// Usage tracking
|
|
1308
|
+
|
|
1309
|
+
__init6() {this.isFirstGetData = true}
|
|
1310
|
+
// Build-step deduplication
|
|
1311
|
+
__init7() {this.buildDataPromise = null}
|
|
1312
|
+
__init8() {this.buildReadTracked = false}
|
|
1313
|
+
// Suppresses usage tracking when the SDK key is unauthorized
|
|
1314
|
+
__init9() {this.unauthorized = false}
|
|
1315
|
+
constructor(options) {;_class3.prototype.__init5.call(this);_class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);_class3.prototype.__init14.call(this);_class3.prototype.__init15.call(this);
|
|
1316
|
+
if (!options.sdkKey || typeof options.sdkKey !== "string" || !options.sdkKey.startsWith("vf_")) {
|
|
1317
|
+
throw new Error(
|
|
1318
|
+
'@vercel/flags-core: SDK key must be a string starting with "vf_"'
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
this.options = normalizeOptions(options);
|
|
1322
|
+
this.streamSource = new StreamSource(
|
|
1323
|
+
this.options,
|
|
1324
|
+
() => _optionalChain([this, 'access', _68 => _68.data, 'optionalAccess', _69 => _69.revision])
|
|
1325
|
+
);
|
|
1326
|
+
this.pollingSource = new PollingSource(this.options);
|
|
1327
|
+
this.bundledSource = new BundledSource({
|
|
1328
|
+
sdkKey: this.options.sdkKey,
|
|
1329
|
+
readBundledDefinitions
|
|
1330
|
+
});
|
|
1331
|
+
this.wireSourceEvents();
|
|
1332
|
+
if (this.options.datafile) {
|
|
1333
|
+
this.data = tagData(this.options.datafile, "provided");
|
|
1334
|
+
}
|
|
1335
|
+
this.usageTracker = new UsageTracker(this.options);
|
|
1336
|
+
}
|
|
1337
|
+
// Source event handlers (stored for cleanup)
|
|
1338
|
+
__init10() {this.onStreamData = (data) => {
|
|
1339
|
+
if (this.isNewerData(data)) {
|
|
1340
|
+
this.data = tagData(data, "stream");
|
|
1341
|
+
}
|
|
1342
|
+
}}
|
|
1343
|
+
__init11() {this.onStreamPrimed = () => {
|
|
1344
|
+
if (this.state === "degraded" || this.state === "initializing:stream") {
|
|
1345
|
+
this.transition("streaming");
|
|
1346
|
+
}
|
|
1347
|
+
}}
|
|
1348
|
+
__init12() {this.onStreamConnected = () => {
|
|
1349
|
+
if (this.state === "degraded" || this.state === "initializing:stream") {
|
|
1350
|
+
this.transition("streaming");
|
|
1351
|
+
}
|
|
1352
|
+
}}
|
|
1353
|
+
__init13() {this.onStreamDisconnected = () => {
|
|
1354
|
+
if (this.state === "streaming") {
|
|
1355
|
+
this.transition("degraded");
|
|
1356
|
+
}
|
|
1357
|
+
}}
|
|
1358
|
+
__init14() {this.onPollData = (data) => {
|
|
1359
|
+
if (this.isNewerData(data)) {
|
|
1360
|
+
this.data = tagData(data, "poll");
|
|
1361
|
+
}
|
|
1362
|
+
}}
|
|
1363
|
+
__init15() {this.onPollError = (error) => {
|
|
1364
|
+
console.error("@vercel/flags-core: Poll failed:", error);
|
|
1365
|
+
}}
|
|
1366
|
+
// ---------------------------------------------------------------------------
|
|
1367
|
+
// Source event wiring
|
|
1368
|
+
// ---------------------------------------------------------------------------
|
|
1369
|
+
wireSourceEvents() {
|
|
1370
|
+
this.streamSource.on("data", this.onStreamData);
|
|
1371
|
+
this.streamSource.on("primed", this.onStreamPrimed);
|
|
1372
|
+
this.streamSource.on("connected", this.onStreamConnected);
|
|
1373
|
+
this.streamSource.on("disconnected", this.onStreamDisconnected);
|
|
1374
|
+
this.pollingSource.on("data", this.onPollData);
|
|
1375
|
+
this.pollingSource.on("error", this.onPollError);
|
|
1376
|
+
}
|
|
1377
|
+
unwireSourceEvents() {
|
|
1378
|
+
this.streamSource.off("data", this.onStreamData);
|
|
1379
|
+
this.streamSource.off("primed", this.onStreamPrimed);
|
|
1380
|
+
this.streamSource.off("connected", this.onStreamConnected);
|
|
1381
|
+
this.streamSource.off("disconnected", this.onStreamDisconnected);
|
|
1382
|
+
this.pollingSource.off("data", this.onPollData);
|
|
1383
|
+
this.pollingSource.off("error", this.onPollError);
|
|
1384
|
+
}
|
|
1385
|
+
// ---------------------------------------------------------------------------
|
|
1386
|
+
// State machine
|
|
1387
|
+
// ---------------------------------------------------------------------------
|
|
1388
|
+
transition(to) {
|
|
1389
|
+
this.state = to;
|
|
1390
|
+
}
|
|
1391
|
+
get isConnected() {
|
|
1392
|
+
return this.state === "streaming";
|
|
1393
|
+
}
|
|
1394
|
+
get mode() {
|
|
1395
|
+
if (this.options.buildStep) return "build";
|
|
1396
|
+
switch (this.state) {
|
|
1397
|
+
case "streaming":
|
|
1398
|
+
return "streaming";
|
|
1399
|
+
case "polling":
|
|
1400
|
+
return "polling";
|
|
1401
|
+
default:
|
|
1402
|
+
return "offline";
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
// ---------------------------------------------------------------------------
|
|
1406
|
+
// Public API (DataSource interface)
|
|
1407
|
+
// ---------------------------------------------------------------------------
|
|
1408
|
+
/**
|
|
1409
|
+
* Initializes the data source.
|
|
1410
|
+
*
|
|
1411
|
+
* Build step: datafile → bundled → one-time fetch
|
|
1412
|
+
* Streaming mode: stream → datafile → bundled
|
|
1413
|
+
* Polling mode (no stream): poll → datafile → bundled
|
|
1414
|
+
* Offline mode (neither): datafile → bundled → one-time fetch
|
|
1415
|
+
*/
|
|
1416
|
+
async initialize() {
|
|
1417
|
+
if (this.options.buildStep) {
|
|
1418
|
+
this.transition("build:loading");
|
|
1419
|
+
await this.initializeForBuildStep();
|
|
1420
|
+
this.transition("build:ready");
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (!this.data && this.options.datafile) {
|
|
1424
|
+
this.data = tagData(this.options.datafile, "provided");
|
|
1425
|
+
}
|
|
1426
|
+
if (!this.data) {
|
|
1427
|
+
try {
|
|
1428
|
+
const bundled = await this.bundledSource.tryLoad();
|
|
1429
|
+
if (bundled) {
|
|
1430
|
+
this.data = tagData(bundled, "bundled");
|
|
1431
|
+
}
|
|
1432
|
+
} catch (e6) {
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (this.data) {
|
|
1436
|
+
if (this.options.stream.enabled) {
|
|
1437
|
+
this.transition("initializing:stream");
|
|
1438
|
+
await this.tryInitializeStream();
|
|
1439
|
+
} else if (this.options.polling.enabled) {
|
|
1440
|
+
this.transition("initializing:polling");
|
|
1441
|
+
await this.tryInitializePolling();
|
|
1442
|
+
} else {
|
|
1443
|
+
this.transition("degraded");
|
|
1444
|
+
}
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
if (this.options.stream.enabled) {
|
|
1448
|
+
this.transition("initializing:stream");
|
|
1449
|
+
const streamSuccess = await this.tryInitializeStream();
|
|
1450
|
+
if (streamSuccess) {
|
|
1451
|
+
this.transition("streaming");
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
} else if (this.options.polling.enabled) {
|
|
1455
|
+
this.transition("initializing:polling");
|
|
1456
|
+
const pollingSuccess = await this.tryInitializePolling();
|
|
1457
|
+
if (pollingSuccess) {
|
|
1458
|
+
this.transition("polling");
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
await this.initializeFromFallbacks();
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Reads the current datafile with metrics.
|
|
1466
|
+
*/
|
|
1467
|
+
async read() {
|
|
1468
|
+
const startTime = Date.now();
|
|
1469
|
+
const cacheHadDefinitions = this.data !== void 0;
|
|
1470
|
+
const isFirstRead = this.isFirstGetData;
|
|
1471
|
+
this.isFirstGetData = false;
|
|
1472
|
+
const [result, cacheStatus] = await this.resolveData();
|
|
1473
|
+
const readMs = Date.now() - startTime;
|
|
1474
|
+
const { _origin, ...data } = result;
|
|
1475
|
+
const source = originToMetricsSource(_origin);
|
|
1476
|
+
this.trackRead(startTime, cacheHadDefinitions, isFirstRead, source);
|
|
1477
|
+
return {
|
|
1478
|
+
...data,
|
|
1479
|
+
metrics: {
|
|
1480
|
+
readMs,
|
|
1481
|
+
source,
|
|
1482
|
+
cacheStatus,
|
|
1483
|
+
connectionState: this.isConnected ? "connected" : "disconnected",
|
|
1484
|
+
mode: this.mode
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Shuts down the data source and releases resources.
|
|
1490
|
+
*/
|
|
1491
|
+
async shutdown() {
|
|
1492
|
+
this.unwireSourceEvents();
|
|
1493
|
+
this.streamSource.stop();
|
|
1494
|
+
this.pollingSource.stop();
|
|
1495
|
+
this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
|
|
1496
|
+
this.transition("shutdown");
|
|
1497
|
+
await this.usageTracker.flush();
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Returns the datafile with metrics.
|
|
1501
|
+
* Uses in-memory data if available, otherwise falls back to bundled,
|
|
1502
|
+
* then to a one-time fetch if called without prior initialization.
|
|
1503
|
+
*/
|
|
1504
|
+
async getDatafile() {
|
|
1505
|
+
const startTime = Date.now();
|
|
1506
|
+
this.isFirstGetData = false;
|
|
1507
|
+
let result;
|
|
1508
|
+
let cacheStatus;
|
|
1509
|
+
if (this.options.buildStep) {
|
|
1510
|
+
[result, cacheStatus] = await this.resolveDataForBuildStep();
|
|
1511
|
+
} else if (this.data) {
|
|
1512
|
+
cacheStatus = this.isConnected ? "HIT" : "STALE";
|
|
1513
|
+
result = this.data;
|
|
1514
|
+
} else {
|
|
1515
|
+
const bundled = await this.bundledSource.tryLoad();
|
|
1516
|
+
if (bundled) {
|
|
1517
|
+
this.data = tagData(bundled, "bundled");
|
|
1518
|
+
result = this.data;
|
|
1519
|
+
cacheStatus = "MISS";
|
|
1520
|
+
} else {
|
|
1521
|
+
try {
|
|
1522
|
+
const fetched = await fetchDatafile({
|
|
1523
|
+
host: this.options.host,
|
|
1524
|
+
sdkKey: this.options.sdkKey,
|
|
1525
|
+
fetch: this.options.fetch
|
|
1526
|
+
});
|
|
1527
|
+
this.data = tagData(fetched, "fetched");
|
|
1528
|
+
result = this.data;
|
|
1529
|
+
cacheStatus = "MISS";
|
|
1530
|
+
} catch (e7) {
|
|
1531
|
+
throw new Error(
|
|
1532
|
+
"@vercel/flags-core: No flag definitions available. Initialize the client or provide a datafile."
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const source = originToMetricsSource(result._origin);
|
|
1538
|
+
return {
|
|
1539
|
+
...result,
|
|
1540
|
+
metrics: {
|
|
1541
|
+
readMs: Date.now() - startTime,
|
|
1542
|
+
source,
|
|
1543
|
+
cacheStatus,
|
|
1544
|
+
connectionState: this.isConnected ? "connected" : "disconnected",
|
|
1545
|
+
mode: this.mode
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Returns the bundled fallback datafile.
|
|
1551
|
+
*/
|
|
1552
|
+
async getFallbackDatafile() {
|
|
1553
|
+
return this.bundledSource.getRaw();
|
|
1554
|
+
}
|
|
1555
|
+
// ---------------------------------------------------------------------------
|
|
1556
|
+
// Data resolution (shared by read() and getDatafile())
|
|
1557
|
+
// ---------------------------------------------------------------------------
|
|
1558
|
+
/**
|
|
1559
|
+
* Resolves the current data, using the appropriate strategy for the
|
|
1560
|
+
* current mode. Returns tagged data and cache status.
|
|
1561
|
+
*
|
|
1562
|
+
* Build step: cached → bundled → one-time fetch
|
|
1563
|
+
* Runtime with cache: return cached data
|
|
1564
|
+
* Runtime without cache: stream/poll → datafile → bundled → fetch → throw
|
|
1565
|
+
*/
|
|
1566
|
+
async resolveData() {
|
|
1567
|
+
if (this.options.buildStep) {
|
|
1568
|
+
return this.resolveDataForBuildStep();
|
|
1569
|
+
}
|
|
1570
|
+
if (this.data) {
|
|
1571
|
+
const cacheStatus = this.isConnected ? "HIT" : "STALE";
|
|
1572
|
+
return [this.data, cacheStatus];
|
|
1573
|
+
}
|
|
1574
|
+
return this.resolveDataWithFallbacks();
|
|
1575
|
+
}
|
|
1576
|
+
// ---------------------------------------------------------------------------
|
|
1577
|
+
// Stream initialization
|
|
1578
|
+
// ---------------------------------------------------------------------------
|
|
1579
|
+
/**
|
|
1580
|
+
* Attempts to initialize via stream with timeout.
|
|
1581
|
+
* Returns true if stream connected successfully within timeout.
|
|
1582
|
+
*/
|
|
1583
|
+
async tryInitializeStream() {
|
|
1584
|
+
if (this.options.stream.initTimeoutMs <= 0) {
|
|
1585
|
+
try {
|
|
1586
|
+
await this.streamSource.start();
|
|
1587
|
+
return true;
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
if (error instanceof UnauthorizedError) {
|
|
1590
|
+
this.unauthorized = true;
|
|
1591
|
+
}
|
|
1592
|
+
return false;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
let timeoutId;
|
|
1596
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1597
|
+
timeoutId = setTimeout(
|
|
1598
|
+
() => resolve("timeout"),
|
|
1599
|
+
this.options.stream.initTimeoutMs
|
|
1600
|
+
);
|
|
1601
|
+
});
|
|
1602
|
+
try {
|
|
1603
|
+
const result = await Promise.race([
|
|
1604
|
+
this.streamSource.start(),
|
|
1605
|
+
timeoutPromise
|
|
1606
|
+
]);
|
|
1607
|
+
clearTimeout(timeoutId);
|
|
1608
|
+
if (result === "timeout") {
|
|
1609
|
+
console.warn(
|
|
1610
|
+
"@vercel/flags-core: Stream initialization timeout, falling back"
|
|
1611
|
+
);
|
|
1612
|
+
void this.streamSource.start().catch(() => {
|
|
1613
|
+
});
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
return true;
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
clearTimeout(timeoutId);
|
|
1619
|
+
if (error instanceof Error && error.message.includes("401")) {
|
|
1620
|
+
this.unauthorized = true;
|
|
1621
|
+
}
|
|
1622
|
+
return false;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
// ---------------------------------------------------------------------------
|
|
1626
|
+
// Polling initialization
|
|
1627
|
+
// ---------------------------------------------------------------------------
|
|
1628
|
+
/**
|
|
1629
|
+
* Attempts to initialize via polling with timeout.
|
|
1630
|
+
* Returns true if first poll succeeded within timeout.
|
|
1631
|
+
*
|
|
1632
|
+
* Only used when streaming is disabled and polling is the primary source.
|
|
1633
|
+
*/
|
|
1634
|
+
async tryInitializePolling() {
|
|
1635
|
+
const pollPromise = this.pollingSource.poll();
|
|
1636
|
+
if (this.options.polling.initTimeoutMs <= 0) {
|
|
1637
|
+
try {
|
|
1638
|
+
await pollPromise;
|
|
1639
|
+
if (this.data) {
|
|
1640
|
+
this.pollingSource.startInterval();
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1643
|
+
return false;
|
|
1644
|
+
} catch (e8) {
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
let timeoutId;
|
|
1649
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1650
|
+
timeoutId = setTimeout(
|
|
1651
|
+
() => resolve("timeout"),
|
|
1652
|
+
this.options.polling.initTimeoutMs
|
|
1653
|
+
);
|
|
1654
|
+
});
|
|
1655
|
+
try {
|
|
1656
|
+
const result = await Promise.race([pollPromise, timeoutPromise]);
|
|
1657
|
+
clearTimeout(timeoutId);
|
|
1658
|
+
if (result === "timeout") {
|
|
1659
|
+
console.warn(
|
|
1660
|
+
"@vercel/flags-core: Polling initialization timeout, falling back"
|
|
1661
|
+
);
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
if (this.data) {
|
|
1665
|
+
this.pollingSource.startInterval();
|
|
1666
|
+
return true;
|
|
1667
|
+
}
|
|
1668
|
+
return false;
|
|
1669
|
+
} catch (e9) {
|
|
1670
|
+
clearTimeout(timeoutId);
|
|
1671
|
+
return false;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
// ---------------------------------------------------------------------------
|
|
1675
|
+
// Build step helpers
|
|
1676
|
+
// ---------------------------------------------------------------------------
|
|
1677
|
+
/**
|
|
1678
|
+
* Initializes data for build step environments.
|
|
1679
|
+
*/
|
|
1680
|
+
async initializeForBuildStep() {
|
|
1681
|
+
if (this.data) return;
|
|
1682
|
+
if (!this.buildDataPromise) {
|
|
1683
|
+
this.buildDataPromise = this.loadBuildData();
|
|
1684
|
+
}
|
|
1685
|
+
this.data = await this.buildDataPromise;
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Retrieves data during build steps.
|
|
1689
|
+
* Concurrent callers share a single load promise. The first caller to
|
|
1690
|
+
* populate `this.data` gets cacheStatus MISS; subsequent callers get HIT.
|
|
1691
|
+
*/
|
|
1692
|
+
async resolveDataForBuildStep() {
|
|
1693
|
+
if (this.data) {
|
|
1694
|
+
return [this.data, "HIT"];
|
|
1695
|
+
}
|
|
1696
|
+
if (!this.buildDataPromise) {
|
|
1697
|
+
this.buildDataPromise = this.loadBuildData();
|
|
1698
|
+
}
|
|
1699
|
+
const data = await this.buildDataPromise;
|
|
1700
|
+
if (!this.data) {
|
|
1701
|
+
this.data = data;
|
|
1702
|
+
return [data, "MISS"];
|
|
1703
|
+
}
|
|
1704
|
+
return [this.data, "HIT"];
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Loads data for a build step: bundled → one-time fetch.
|
|
1708
|
+
*/
|
|
1709
|
+
async loadBuildData() {
|
|
1710
|
+
const bundled = await this.bundledSource.tryLoad();
|
|
1711
|
+
if (bundled) return tagData(bundled, "bundled");
|
|
1712
|
+
try {
|
|
1713
|
+
const fetched = await fetchDatafile({
|
|
1714
|
+
host: this.options.host,
|
|
1715
|
+
sdkKey: this.options.sdkKey,
|
|
1716
|
+
fetch: this.options.fetch
|
|
1717
|
+
});
|
|
1718
|
+
return tagData(fetched, "fetched");
|
|
1719
|
+
} catch (e10) {
|
|
1720
|
+
}
|
|
1721
|
+
throw new Error(
|
|
1722
|
+
"@vercel/flags-core: No flag definitions available during build. Provide a datafile or bundled definitions."
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
// ---------------------------------------------------------------------------
|
|
1726
|
+
// Fallback helpers
|
|
1727
|
+
// ---------------------------------------------------------------------------
|
|
1728
|
+
/**
|
|
1729
|
+
* Shared fallback chain used by both initialize() and resolveData().
|
|
1730
|
+
*/
|
|
1731
|
+
async initializeFromFallbacks() {
|
|
1732
|
+
this.transition("initializing:fallback");
|
|
1733
|
+
if (this.data) {
|
|
1734
|
+
this.transition("degraded");
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
const bundled = await this.bundledSource.tryLoad();
|
|
1738
|
+
if (bundled) {
|
|
1739
|
+
this.data = tagData(bundled, "bundled");
|
|
1740
|
+
this.transition("degraded");
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (!this.options.stream.enabled && !this.options.polling.enabled) {
|
|
1744
|
+
try {
|
|
1745
|
+
const fetched = await fetchDatafile({
|
|
1746
|
+
host: this.options.host,
|
|
1747
|
+
sdkKey: this.options.sdkKey,
|
|
1748
|
+
fetch: this.options.fetch
|
|
1749
|
+
});
|
|
1750
|
+
this.data = tagData(fetched, "fetched");
|
|
1751
|
+
this.transition("degraded");
|
|
1752
|
+
return;
|
|
1753
|
+
} catch (e11) {
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
throw new Error(
|
|
1757
|
+
"@vercel/flags-core: No flag definitions available. Bundled definitions not found."
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Retrieves data using the fallback chain (called when no cached data exists).
|
|
1762
|
+
* Streaming mode: stream → datafile → bundled.
|
|
1763
|
+
* Polling mode: poll → datafile → bundled.
|
|
1764
|
+
* Offline mode: datafile → bundled → one-time fetch.
|
|
1765
|
+
*/
|
|
1766
|
+
async resolveDataWithFallbacks() {
|
|
1767
|
+
if (this.options.stream.enabled) {
|
|
1768
|
+
this.transition("initializing:stream");
|
|
1769
|
+
const streamSuccess = await this.tryInitializeStream();
|
|
1770
|
+
if (streamSuccess && this.data) {
|
|
1771
|
+
this.transition("streaming");
|
|
1772
|
+
return [this.data, "MISS"];
|
|
1773
|
+
}
|
|
1774
|
+
} else if (this.options.polling.enabled) {
|
|
1775
|
+
this.transition("initializing:polling");
|
|
1776
|
+
const pollingSuccess = await this.tryInitializePolling();
|
|
1777
|
+
if (pollingSuccess && this.data) {
|
|
1778
|
+
this.transition("polling");
|
|
1779
|
+
return [this.data, "MISS"];
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
this.transition("initializing:fallback");
|
|
1783
|
+
if (this.options.datafile) {
|
|
1784
|
+
this.data = tagData(this.options.datafile, "provided");
|
|
1785
|
+
this.transition("degraded");
|
|
1786
|
+
return [this.data, "STALE"];
|
|
1787
|
+
}
|
|
1788
|
+
const bundled = await this.bundledSource.tryLoad();
|
|
1789
|
+
if (bundled) {
|
|
1790
|
+
console.warn("@vercel/flags-core: Using bundled definitions as fallback");
|
|
1791
|
+
this.data = tagData(bundled, "bundled");
|
|
1792
|
+
this.transition("degraded");
|
|
1793
|
+
return [this.data, "STALE"];
|
|
1794
|
+
}
|
|
1795
|
+
if (!this.options.stream.enabled && !this.options.polling.enabled) {
|
|
1796
|
+
try {
|
|
1797
|
+
const fetched = await fetchDatafile({
|
|
1798
|
+
host: this.options.host,
|
|
1799
|
+
sdkKey: this.options.sdkKey,
|
|
1800
|
+
fetch: this.options.fetch
|
|
1801
|
+
});
|
|
1802
|
+
this.data = tagData(fetched, "fetched");
|
|
1803
|
+
this.transition("degraded");
|
|
1804
|
+
return [this.data, "MISS"];
|
|
1805
|
+
} catch (e12) {
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
throw new Error(
|
|
1809
|
+
"@vercel/flags-core: No flag definitions available. Provide a datafile or bundled definitions."
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
// ---------------------------------------------------------------------------
|
|
1813
|
+
// Data comparison
|
|
1814
|
+
// ---------------------------------------------------------------------------
|
|
1815
|
+
/**
|
|
1816
|
+
* Checks if the incoming data is newer than the current in-memory data.
|
|
1817
|
+
* Returns true if the update should proceed, false if it should be skipped.
|
|
1818
|
+
*
|
|
1819
|
+
* Always accepts the update if:
|
|
1820
|
+
* - There is no current data
|
|
1821
|
+
* - The current data has no configUpdatedAt
|
|
1822
|
+
* - The incoming data has no configUpdatedAt
|
|
1823
|
+
*
|
|
1824
|
+
* Skips the update only when both have configUpdatedAt and incoming is not newer.
|
|
1825
|
+
*/
|
|
1826
|
+
isNewerData(incoming) {
|
|
1827
|
+
if (!this.data) return true;
|
|
1828
|
+
const currentTs = parseConfigUpdatedAt(this.data.configUpdatedAt);
|
|
1829
|
+
const incomingTs = parseConfigUpdatedAt(incoming.configUpdatedAt);
|
|
1830
|
+
if (currentTs === void 0 || incomingTs === void 0) {
|
|
1831
|
+
return true;
|
|
1832
|
+
}
|
|
1833
|
+
return incomingTs > currentTs;
|
|
1834
|
+
}
|
|
1835
|
+
// ---------------------------------------------------------------------------
|
|
1836
|
+
// Usage tracking
|
|
1837
|
+
// ---------------------------------------------------------------------------
|
|
1838
|
+
/**
|
|
1839
|
+
* Tracks a read operation for usage analytics.
|
|
1840
|
+
* During build steps, only the first read is tracked.
|
|
1841
|
+
*/
|
|
1842
|
+
trackRead(startTime, cacheHadDefinitions, isFirstRead, source) {
|
|
1843
|
+
if (this.unauthorized) return;
|
|
1844
|
+
if (this.options.buildStep && this.buildReadTracked) return;
|
|
1845
|
+
if (this.options.buildStep) this.buildReadTracked = true;
|
|
1846
|
+
const configOrigin = source === "embedded" ? "embedded" : "in-memory";
|
|
1847
|
+
const cacheAction = this.state === "streaming" ? "FOLLOWING" : this.state === "polling" ? "REFRESHING" : "NONE";
|
|
1848
|
+
const mode = this.mode;
|
|
1849
|
+
const trackOptions = {
|
|
1850
|
+
configOrigin,
|
|
1851
|
+
cacheStatus: cacheHadDefinitions ? "HIT" : "MISS",
|
|
1852
|
+
cacheAction,
|
|
1853
|
+
cacheIsBlocking: !cacheHadDefinitions,
|
|
1854
|
+
duration: Date.now() - startTime,
|
|
1855
|
+
mode: mode === "streaming" ? "stream" : mode === "polling" ? "poll" : mode
|
|
1856
|
+
};
|
|
1857
|
+
const configUpdatedAt = _optionalChain([this, 'access', _70 => _70.data, 'optionalAccess', _71 => _71.configUpdatedAt]);
|
|
1858
|
+
if (typeof configUpdatedAt === "number") {
|
|
1859
|
+
trackOptions.configUpdatedAt = configUpdatedAt;
|
|
1860
|
+
}
|
|
1861
|
+
const revision = _optionalChain([this, 'access', _72 => _72.data, 'optionalAccess', _73 => _73.revision]);
|
|
1862
|
+
if (typeof revision === "number") {
|
|
1863
|
+
trackOptions.revision = revision;
|
|
1864
|
+
}
|
|
1865
|
+
if (isFirstRead) {
|
|
1866
|
+
trackOptions.cacheIsFirstRead = true;
|
|
1867
|
+
}
|
|
1868
|
+
this.usageTracker.trackRead(trackOptions);
|
|
1869
|
+
}
|
|
1870
|
+
}, _class3);
|
|
1871
|
+
|
|
1872
|
+
// src/utils/sdk-keys.ts
|
|
1873
|
+
function parseSdkKeyFromFlagsConnectionString(text) {
|
|
1874
|
+
if (text.startsWith("vf_")) return text;
|
|
1875
|
+
try {
|
|
1876
|
+
if (!text.startsWith("flags:")) return null;
|
|
1877
|
+
const params = new URLSearchParams(text.slice(6));
|
|
1878
|
+
return params.get("sdkKey");
|
|
1879
|
+
} catch (e13) {
|
|
1880
|
+
}
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// src/index.make.ts
|
|
1885
|
+
function make(createRawClient) {
|
|
1886
|
+
let _defaultFlagsClient = null;
|
|
1887
|
+
function createClient2(sdkKeyOrConnectionString, options) {
|
|
1888
|
+
if (!sdkKeyOrConnectionString)
|
|
1889
|
+
throw new Error("@vercel/flags-core: Missing sdkKey");
|
|
1890
|
+
if (typeof sdkKeyOrConnectionString !== "string")
|
|
1891
|
+
throw new Error(
|
|
1892
|
+
`@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`
|
|
1893
|
+
);
|
|
1894
|
+
const sdkKey = parseSdkKeyFromFlagsConnectionString(
|
|
1895
|
+
sdkKeyOrConnectionString
|
|
1896
|
+
);
|
|
1897
|
+
if (!sdkKey) {
|
|
1898
|
+
throw new Error(
|
|
1899
|
+
"@vercel/flags-core: Missing sdkKey in connection string"
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
const controller = new Controller({ sdkKey, ...options });
|
|
1903
|
+
return createRawClient({
|
|
1904
|
+
controller,
|
|
1905
|
+
origin: { provider: "vercel", sdkKey }
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
function resetDefaultFlagsClient2() {
|
|
1909
|
+
_defaultFlagsClient = null;
|
|
1910
|
+
}
|
|
1911
|
+
const flagsClient2 = new Proxy({}, {
|
|
1912
|
+
get(_, prop) {
|
|
1913
|
+
if (!_defaultFlagsClient) {
|
|
1914
|
+
if (!process.env.FLAGS) {
|
|
1915
|
+
throw new Error("flags: Missing environment variable FLAGS");
|
|
1916
|
+
}
|
|
1917
|
+
const sdkKey = parseSdkKeyFromFlagsConnectionString(process.env.FLAGS);
|
|
1918
|
+
if (!sdkKey) {
|
|
1919
|
+
throw new Error("@vercel/flags-core: Missing sdkKey");
|
|
1920
|
+
}
|
|
1921
|
+
_defaultFlagsClient = createClient2(sdkKey);
|
|
1922
|
+
}
|
|
1923
|
+
return _defaultFlagsClient[prop];
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
return {
|
|
1927
|
+
flagsClient: flagsClient2,
|
|
1928
|
+
resetDefaultFlagsClient: resetDefaultFlagsClient2,
|
|
1929
|
+
createClient: createClient2
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// src/index.next-js.ts
|
|
1934
|
+
function setCacheLife() {
|
|
1935
|
+
try {
|
|
1936
|
+
_cache.cacheLife.call(void 0, { revalidate: 0, expire: 0 });
|
|
1937
|
+
_cache.cacheLife.call(void 0, { stale: 60 });
|
|
1938
|
+
} catch (e14) {
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
var cachedFns = {
|
|
1942
|
+
initialize: async (...args) => {
|
|
1943
|
+
"use cache";
|
|
1944
|
+
setCacheLife();
|
|
1945
|
+
return initialize(...args);
|
|
1946
|
+
},
|
|
1947
|
+
shutdown: async (...args) => {
|
|
1948
|
+
"use cache";
|
|
1949
|
+
setCacheLife();
|
|
1950
|
+
return shutdown(...args);
|
|
1951
|
+
},
|
|
1952
|
+
getDatafile: async (...args) => {
|
|
1953
|
+
"use cache";
|
|
1954
|
+
setCacheLife();
|
|
1955
|
+
return getDatafile(...args);
|
|
1956
|
+
},
|
|
1957
|
+
getFallbackDatafile: async (...args) => {
|
|
1958
|
+
"use cache";
|
|
1959
|
+
setCacheLife();
|
|
1960
|
+
return getFallbackDatafile(...args);
|
|
1961
|
+
},
|
|
1962
|
+
evaluate: async (...args) => {
|
|
1963
|
+
"use cache";
|
|
1964
|
+
setCacheLife();
|
|
1965
|
+
return evaluate2(...args);
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
var { flagsClient, resetDefaultFlagsClient, createClient } = make(
|
|
1969
|
+
createCreateRawClient(cachedFns)
|
|
1970
|
+
);
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
exports.ResolutionReason = ResolutionReason; exports.evaluate = evaluate; exports.FallbackNotFoundError = FallbackNotFoundError; exports.FallbackEntryNotFoundError = FallbackEntryNotFoundError; exports.Controller = Controller; exports.flagsClient = flagsClient; exports.resetDefaultFlagsClient = resetDefaultFlagsClient; exports.createClient = createClient;
|
|
1982
|
+
//# sourceMappingURL=chunk-WRX7DC3S.cjs.map
|