@vercel/flags-core 0.1.8 → 1.0.0
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 +11 -0
- package/README.md +40 -0
- package/dist/chunk-7BUTND2Q.js +1496 -0
- package/dist/chunk-7BUTND2Q.js.map +1 -0
- package/dist/chunk-UQEFJL4F.js +1503 -0
- package/dist/chunk-UQEFJL4F.js.map +1 -0
- package/dist/index.default.d.ts +248 -0
- package/dist/index.default.js +21 -0
- package/dist/index.next-js.d.ts +235 -0
- package/dist/index.next-js.js +21 -0
- package/dist/index.next-js.js.map +1 -0
- package/dist/openfeature.default.d.ts +48 -0
- package/dist/openfeature.default.js +157 -0
- package/dist/openfeature.default.js.map +1 -0
- package/dist/openfeature.next-js.d.ts +49 -0
- package/dist/openfeature.next-js.js +157 -0
- package/dist/openfeature.next-js.js.map +1 -0
- package/dist/{client-BxFTPk0J.d.cts → types-508sZTBu.d.ts} +137 -58
- package/package.json +31 -19
- package/dist/chunk-4IFGPGNY.js +0 -566
- package/dist/chunk-4IFGPGNY.js.map +0 -1
- package/dist/chunk-6ZAELH3K.cjs +0 -566
- package/dist/chunk-6ZAELH3K.cjs.map +0 -1
- package/dist/client-BxFTPk0J.d.ts +0 -426
- package/dist/index.cjs +0 -27
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -79
- package/dist/index.d.ts +0 -79
- package/dist/index.js +0 -27
- package/dist/openfeature.cjs +0 -151
- package/dist/openfeature.cjs.map +0 -1
- package/dist/openfeature.d.cts +0 -24
- package/dist/openfeature.d.ts +0 -24
- package/dist/openfeature.js +0 -151
- package/dist/openfeature.js.map +0 -1
- /package/dist/{index.js.map → index.default.js.map} +0 -0
|
@@ -0,0 +1,1496 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/client-fns.ts
|
|
8
|
+
var client_fns_exports = {};
|
|
9
|
+
__export(client_fns_exports, {
|
|
10
|
+
evaluate: () => evaluate2,
|
|
11
|
+
getDatafile: () => getDatafile,
|
|
12
|
+
getFallbackDatafile: () => getFallbackDatafile,
|
|
13
|
+
initialize: () => initialize,
|
|
14
|
+
shutdown: () => shutdown
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// src/client-map.ts
|
|
18
|
+
var clientMap = /* @__PURE__ */ new Map();
|
|
19
|
+
|
|
20
|
+
// src/evaluate.ts
|
|
21
|
+
import { xxHash32 as hashInput } from "js-xxhash";
|
|
22
|
+
|
|
23
|
+
// src/types.ts
|
|
24
|
+
var ResolutionReason = /* @__PURE__ */ ((ResolutionReason2) => {
|
|
25
|
+
ResolutionReason2["PAUSED"] = "paused";
|
|
26
|
+
ResolutionReason2["TARGET_MATCH"] = "target_match";
|
|
27
|
+
ResolutionReason2["RULE_MATCH"] = "rule_match";
|
|
28
|
+
ResolutionReason2["FALLTHROUGH"] = "fallthrough";
|
|
29
|
+
ResolutionReason2["ERROR"] = "error";
|
|
30
|
+
return ResolutionReason2;
|
|
31
|
+
})(ResolutionReason || {});
|
|
32
|
+
var Original;
|
|
33
|
+
((Original2) => {
|
|
34
|
+
let AccessorType;
|
|
35
|
+
((AccessorType2) => {
|
|
36
|
+
AccessorType2["SEGMENT"] = "segment";
|
|
37
|
+
AccessorType2["ENTITY"] = "entity";
|
|
38
|
+
})(AccessorType = Original2.AccessorType || (Original2.AccessorType = {}));
|
|
39
|
+
})(Original || (Original = {}));
|
|
40
|
+
var Packed;
|
|
41
|
+
((Packed2) => {
|
|
42
|
+
let AccessorType;
|
|
43
|
+
((AccessorType2) => {
|
|
44
|
+
AccessorType2["SEGMENT"] = "segment";
|
|
45
|
+
AccessorType2["ENTITY"] = "entity";
|
|
46
|
+
})(AccessorType = Packed2.AccessorType || (Packed2.AccessorType = {}));
|
|
47
|
+
})(Packed || (Packed = {}));
|
|
48
|
+
|
|
49
|
+
// src/utils.ts
|
|
50
|
+
function exhaustivenessCheck(_) {
|
|
51
|
+
throw new Error("Exhaustiveness check failed");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/evaluate.ts
|
|
55
|
+
function getProperty(obj, pathArray) {
|
|
56
|
+
return pathArray.reduce((acc, key) => {
|
|
57
|
+
if (acc && key in acc) {
|
|
58
|
+
return acc[key];
|
|
59
|
+
}
|
|
60
|
+
return void 0;
|
|
61
|
+
}, obj);
|
|
62
|
+
}
|
|
63
|
+
function access(lhs, params) {
|
|
64
|
+
if (Array.isArray(lhs)) return getProperty(params.entities, lhs);
|
|
65
|
+
if (lhs === Packed.AccessorType.SEGMENT)
|
|
66
|
+
throw new Error("Unexpected segment");
|
|
67
|
+
throw new Error("Accessor not implemented");
|
|
68
|
+
}
|
|
69
|
+
function isString(input) {
|
|
70
|
+
return typeof input === "string";
|
|
71
|
+
}
|
|
72
|
+
function isNumber(input) {
|
|
73
|
+
return typeof input === "number";
|
|
74
|
+
}
|
|
75
|
+
function isArray(input) {
|
|
76
|
+
return Array.isArray(input);
|
|
77
|
+
}
|
|
78
|
+
function matchTargetList(targets, params) {
|
|
79
|
+
for (const [kind, attributes] of Object.entries(targets)) {
|
|
80
|
+
for (const [attribute, values] of Object.entries(attributes)) {
|
|
81
|
+
const entity = access([kind, attribute], params);
|
|
82
|
+
if (isString(entity) && values.includes(entity)) return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
function matchSegment(segment, params) {
|
|
88
|
+
if (segment.include && matchTargetList(segment.include, params)) return true;
|
|
89
|
+
if (segment.exclude && matchTargetList(segment.exclude, params)) return false;
|
|
90
|
+
if (!segment.rules?.length) return false;
|
|
91
|
+
const firstMatchingRule = segment.rules.find(
|
|
92
|
+
(rule) => matchConditions(rule.conditions, params)
|
|
93
|
+
);
|
|
94
|
+
if (!firstMatchingRule) return false;
|
|
95
|
+
return handleSegmentOutcome(params, firstMatchingRule.outcome);
|
|
96
|
+
}
|
|
97
|
+
function matchSegmentCondition(cmp, rhs, params) {
|
|
98
|
+
switch (cmp) {
|
|
99
|
+
case "eq" /* EQ */: {
|
|
100
|
+
const segment = params.segments?.[rhs];
|
|
101
|
+
if (!segment) return false;
|
|
102
|
+
return matchSegment(segment, params);
|
|
103
|
+
}
|
|
104
|
+
case "!eq" /* NOT_EQ */: {
|
|
105
|
+
const segment = params.segments?.[rhs];
|
|
106
|
+
if (!segment) return false;
|
|
107
|
+
return !matchSegment(segment, params);
|
|
108
|
+
}
|
|
109
|
+
case "oneOf" /* ONE_OF */: {
|
|
110
|
+
if (!isArray(rhs)) return false;
|
|
111
|
+
const segmentIds = rhs;
|
|
112
|
+
return segmentIds.some((segmentId) => {
|
|
113
|
+
const segment = params.segments?.[segmentId];
|
|
114
|
+
if (!segment) return false;
|
|
115
|
+
return matchSegment(segment, params);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
case "!oneOf" /* NOT_ONE_OF */: {
|
|
119
|
+
const segmentIds = rhs;
|
|
120
|
+
return segmentIds.every((segmentId) => {
|
|
121
|
+
const segment = params.segments?.[segmentId];
|
|
122
|
+
if (!segment) return false;
|
|
123
|
+
return matchSegment(segment, params);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
throw new Error(`Comparator ${cmp} not implemented for segment`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function matchConditions(conditions, params) {
|
|
131
|
+
return conditions.every((condition) => {
|
|
132
|
+
const [lhsAccessor, cmpKey, rhs] = condition;
|
|
133
|
+
if (lhsAccessor === Packed.AccessorType.SEGMENT) {
|
|
134
|
+
return rhs && matchSegmentCondition(cmpKey, rhs, params);
|
|
135
|
+
}
|
|
136
|
+
const lhs = access(lhsAccessor, params);
|
|
137
|
+
try {
|
|
138
|
+
switch (cmpKey) {
|
|
139
|
+
case "eq" /* EQ */:
|
|
140
|
+
return lhs === rhs;
|
|
141
|
+
case "!eq" /* NOT_EQ */:
|
|
142
|
+
return lhs !== rhs;
|
|
143
|
+
case "oneOf" /* ONE_OF */:
|
|
144
|
+
return isArray(rhs) && rhs.includes(lhs);
|
|
145
|
+
case "!oneOf" /* NOT_ONE_OF */:
|
|
146
|
+
return isArray(rhs) && typeof lhs !== "undefined" && !rhs.includes(lhs);
|
|
147
|
+
case "containsAllOf" /* CONTAINS_ALL_OF */: {
|
|
148
|
+
if (!Array.isArray(rhs) || !Array.isArray(lhs)) return false;
|
|
149
|
+
const lhsSet = new Set(lhs.filter(isString));
|
|
150
|
+
if (lhsSet.size === lhs.length) {
|
|
151
|
+
return rhs.filter(isString).every((item) => lhsSet.has(item));
|
|
152
|
+
}
|
|
153
|
+
return rhs.every((item) => lhs.includes(item));
|
|
154
|
+
}
|
|
155
|
+
case "containsAnyOf" /* CONTAINS_ANY_OF */: {
|
|
156
|
+
if (!Array.isArray(rhs) || !Array.isArray(lhs)) return false;
|
|
157
|
+
const rhsSet = new Set(rhs.filter(isString));
|
|
158
|
+
return lhs.some(
|
|
159
|
+
rhsSet.size === rhs.length ? (
|
|
160
|
+
// try to use a set if the rhs is a list of strings - O(1)
|
|
161
|
+
(item) => rhsSet.has(item)
|
|
162
|
+
) : (
|
|
163
|
+
// otherwise we need to iterate over the values - O(n)
|
|
164
|
+
(item) => rhs.includes(item)
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
case "containsNoneOf" /* CONTAINS_NONE_OF */: {
|
|
169
|
+
if (!Array.isArray(rhs)) return false;
|
|
170
|
+
if (!Array.isArray(lhs)) return true;
|
|
171
|
+
const rhsSet = new Set(rhs.filter(isString));
|
|
172
|
+
return lhs.every(
|
|
173
|
+
rhsSet.size === rhs.length ? (
|
|
174
|
+
// try to use a set if the rhs is a list of strings - O(1)
|
|
175
|
+
(item) => !rhsSet.has(item)
|
|
176
|
+
) : (
|
|
177
|
+
// otherwise we need to iterate over the values - O(n)
|
|
178
|
+
(item) => !rhs.includes(item)
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
case "startsWith" /* STARTS_WITH */:
|
|
183
|
+
return isString(lhs) && isString(rhs) && lhs.startsWith(rhs);
|
|
184
|
+
case "!startsWith" /* NOT_STARTS_WITH */:
|
|
185
|
+
return isString(lhs) && isString(rhs) && !lhs.startsWith(rhs);
|
|
186
|
+
case "endsWith" /* ENDS_WITH */:
|
|
187
|
+
return isString(lhs) && isString(rhs) && lhs.endsWith(rhs);
|
|
188
|
+
case "!endsWith" /* NOT_ENDS_WITH */:
|
|
189
|
+
return isString(lhs) && isString(rhs) && !lhs.endsWith(rhs);
|
|
190
|
+
case "ex" /* EXISTS */:
|
|
191
|
+
return lhs !== void 0 && lhs !== null;
|
|
192
|
+
case "!ex" /* NOT_EXISTS */:
|
|
193
|
+
return lhs === void 0 || lhs === null;
|
|
194
|
+
case "gt" /* GT */:
|
|
195
|
+
if (lhs === null || lhs === void 0) return false;
|
|
196
|
+
return (isNumber(rhs) || isString(rhs)) && lhs > rhs;
|
|
197
|
+
case "gte" /* GTE */:
|
|
198
|
+
if (lhs === null || lhs === void 0) return false;
|
|
199
|
+
return (isNumber(rhs) || isString(rhs)) && lhs >= rhs;
|
|
200
|
+
case "lt" /* LT */:
|
|
201
|
+
if (lhs === null || lhs === void 0) return false;
|
|
202
|
+
return (isNumber(rhs) || isString(rhs)) && lhs < rhs;
|
|
203
|
+
case "lte" /* LTE */:
|
|
204
|
+
if (lhs === null || lhs === void 0) return false;
|
|
205
|
+
return (isNumber(rhs) || isString(rhs)) && lhs <= rhs;
|
|
206
|
+
case "regex" /* REGEX */:
|
|
207
|
+
if (isString(lhs) && typeof rhs === "object" && !Array.isArray(rhs) && rhs?.type === "regex") {
|
|
208
|
+
return new RegExp(rhs.pattern, rhs.flags).test(lhs);
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
case "!regex" /* NOT_REGEX */:
|
|
212
|
+
if (isString(lhs) && typeof rhs === "object" && !Array.isArray(rhs) && rhs?.type === "regex") {
|
|
213
|
+
return !new RegExp(rhs.pattern, rhs.flags).test(lhs);
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
case "before" /* BEFORE */: {
|
|
217
|
+
if (!isString(lhs) || !isString(rhs)) return false;
|
|
218
|
+
const a = new Date(lhs);
|
|
219
|
+
const b = new Date(rhs);
|
|
220
|
+
return a.getTime() < b.getTime();
|
|
221
|
+
}
|
|
222
|
+
case "after" /* AFTER */: {
|
|
223
|
+
if (!isString(lhs) || !isString(rhs)) return false;
|
|
224
|
+
const a = new Date(lhs);
|
|
225
|
+
const b = new Date(rhs);
|
|
226
|
+
return a.getTime() > b.getTime();
|
|
227
|
+
}
|
|
228
|
+
default: {
|
|
229
|
+
const _x = cmpKey;
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error("flags: Error matching conditions", error);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function sum(list) {
|
|
240
|
+
return list.reduce((acc, n) => acc + n, 0);
|
|
241
|
+
}
|
|
242
|
+
function handleSegmentOutcome(params, outcome) {
|
|
243
|
+
if (outcome === 1) return true;
|
|
244
|
+
switch (outcome.type) {
|
|
245
|
+
case "split": {
|
|
246
|
+
const lhs = access(outcome.base, params);
|
|
247
|
+
if (typeof lhs !== "string") return false;
|
|
248
|
+
const maxValue = 1e5;
|
|
249
|
+
if (outcome.passPromille <= 0) return false;
|
|
250
|
+
if (outcome.passPromille >= maxValue) return true;
|
|
251
|
+
const value = hashInput(lhs, params.definition.seed) % maxValue;
|
|
252
|
+
return value < outcome.passPromille;
|
|
253
|
+
}
|
|
254
|
+
default: {
|
|
255
|
+
const { type } = outcome;
|
|
256
|
+
exhaustivenessCheck(type);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function getVariant(variants, index) {
|
|
261
|
+
if (index < 0 || index >= variants.length) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`@vercel/flags-core: Invalid variant index ${index}, variants length is ${variants.length}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return variants[index];
|
|
267
|
+
}
|
|
268
|
+
function handleOutcome(params, outcome) {
|
|
269
|
+
if (typeof outcome === "number") {
|
|
270
|
+
return {
|
|
271
|
+
value: getVariant(params.definition.variants, outcome),
|
|
272
|
+
outcomeType: "value" /* VALUE */
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
switch (outcome.type) {
|
|
276
|
+
case "split": {
|
|
277
|
+
const lhs = access(outcome.base, params);
|
|
278
|
+
const defaultOutcome = getVariant(
|
|
279
|
+
params.definition.variants,
|
|
280
|
+
outcome.defaultVariant
|
|
281
|
+
);
|
|
282
|
+
if (typeof lhs !== "string") {
|
|
283
|
+
return { value: defaultOutcome, outcomeType: "split" /* SPLIT */ };
|
|
284
|
+
}
|
|
285
|
+
const maxValue = 4294967295;
|
|
286
|
+
const value = hashInput(lhs, params.definition.seed);
|
|
287
|
+
const sumOfWeights = sum(outcome.weights);
|
|
288
|
+
const scaledWeights = outcome.weights.map(
|
|
289
|
+
(weight) => weight / sumOfWeights * maxValue
|
|
290
|
+
);
|
|
291
|
+
const variantIndex = findWeightedIndex(scaledWeights, value, maxValue);
|
|
292
|
+
return {
|
|
293
|
+
value: variantIndex === -1 ? defaultOutcome : getVariant(params.definition.variants, variantIndex),
|
|
294
|
+
outcomeType: "split" /* SPLIT */
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
default: {
|
|
298
|
+
const { type } = outcome;
|
|
299
|
+
exhaustivenessCheck(type);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function evaluate(params) {
|
|
304
|
+
const envConfig = params.definition.environments[params.environment];
|
|
305
|
+
if (typeof envConfig === "number") {
|
|
306
|
+
return Object.assign(handleOutcome(params, envConfig), {
|
|
307
|
+
reason: "paused" /* PAUSED */
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (!envConfig) {
|
|
311
|
+
return {
|
|
312
|
+
reason: "error" /* ERROR */,
|
|
313
|
+
errorMessage: `Could not find envConfig for "${params.environment}"`,
|
|
314
|
+
value: params.defaultValue
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if ("reuse" in envConfig) {
|
|
318
|
+
const reuseEnvConfig = params.definition.environments[envConfig.reuse];
|
|
319
|
+
if (reuseEnvConfig === void 0) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Could not find envConfig for "${envConfig.reuse}" when reusing`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return evaluate({ ...params, environment: envConfig.reuse });
|
|
325
|
+
}
|
|
326
|
+
if (envConfig.targets) {
|
|
327
|
+
const matchedIndex = envConfig.targets.findIndex(
|
|
328
|
+
(targetList) => matchTargetList(targetList, params)
|
|
329
|
+
);
|
|
330
|
+
if (matchedIndex > -1) {
|
|
331
|
+
return Object.assign(handleOutcome(params, matchedIndex), {
|
|
332
|
+
reason: "target_match" /* TARGET_MATCH */
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const firstMatchingRule = envConfig.rules ? envConfig.rules.find((rule) => matchConditions(rule.conditions, params)) : void 0;
|
|
337
|
+
if (firstMatchingRule) {
|
|
338
|
+
return Object.assign(handleOutcome(params, firstMatchingRule.outcome), {
|
|
339
|
+
reason: "rule_match" /* RULE_MATCH */
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return Object.assign(handleOutcome(params, envConfig.fallthrough), {
|
|
343
|
+
reason: "fallthrough" /* FALLTHROUGH */
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function findWeightedIndex(weights, value, maxValue) {
|
|
347
|
+
if (value < 0 || value >= maxValue) return -1;
|
|
348
|
+
let sum2 = 0;
|
|
349
|
+
for (let i = 0; i < weights.length; i++) {
|
|
350
|
+
sum2 += weights[i];
|
|
351
|
+
if (value < sum2) return i;
|
|
352
|
+
}
|
|
353
|
+
return -1;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// package.json
|
|
357
|
+
var version = "1.0.0";
|
|
358
|
+
|
|
359
|
+
// src/lib/report-value.ts
|
|
360
|
+
function internalReportValue(key, value, data) {
|
|
361
|
+
const symbol = /* @__PURE__ */ Symbol.for("@vercel/request-context");
|
|
362
|
+
const ctx = Reflect.get(globalThis, symbol)?.get();
|
|
363
|
+
ctx?.flags?.reportValue(key, value, {
|
|
364
|
+
sdkVersion: version,
|
|
365
|
+
...data
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/client-fns.ts
|
|
370
|
+
function initialize(id) {
|
|
371
|
+
return clientMap.get(id).dataSource.initialize();
|
|
372
|
+
}
|
|
373
|
+
function shutdown(id) {
|
|
374
|
+
return clientMap.get(id).dataSource.shutdown();
|
|
375
|
+
}
|
|
376
|
+
function getDatafile(id) {
|
|
377
|
+
return clientMap.get(id).dataSource.getDatafile();
|
|
378
|
+
}
|
|
379
|
+
function getFallbackDatafile(id) {
|
|
380
|
+
const ds = clientMap.get(id).dataSource;
|
|
381
|
+
if (ds.getFallbackDatafile) return ds.getFallbackDatafile();
|
|
382
|
+
throw new Error("flags: This data source does not support fallbacks");
|
|
383
|
+
}
|
|
384
|
+
async function evaluate2(id, flagKey, defaultValue, entities) {
|
|
385
|
+
const ds = clientMap.get(id).dataSource;
|
|
386
|
+
const datafile = await ds.read();
|
|
387
|
+
const flagDefinition = datafile.definitions[flagKey];
|
|
388
|
+
if (flagDefinition === void 0) {
|
|
389
|
+
return {
|
|
390
|
+
value: defaultValue,
|
|
391
|
+
reason: "error" /* ERROR */,
|
|
392
|
+
errorCode: "FLAG_NOT_FOUND" /* FLAG_NOT_FOUND */,
|
|
393
|
+
errorMessage: `Definition not found for flag "${flagKey}"`,
|
|
394
|
+
metrics: {
|
|
395
|
+
evaluationMs: 0,
|
|
396
|
+
readMs: datafile.metrics.readMs,
|
|
397
|
+
source: datafile.metrics.source,
|
|
398
|
+
cacheStatus: datafile.metrics.cacheStatus,
|
|
399
|
+
connectionState: datafile.metrics.connectionState
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const evalStartTime = Date.now();
|
|
404
|
+
const result = evaluate({
|
|
405
|
+
defaultValue,
|
|
406
|
+
definition: flagDefinition,
|
|
407
|
+
environment: datafile.environment,
|
|
408
|
+
entities: entities ?? {},
|
|
409
|
+
segments: datafile.segments
|
|
410
|
+
});
|
|
411
|
+
const evaluationDurationMs = Date.now() - evalStartTime;
|
|
412
|
+
if (datafile.projectId) {
|
|
413
|
+
internalReportValue(flagKey, result.value, {
|
|
414
|
+
originProjectId: datafile.projectId,
|
|
415
|
+
originProvider: "vercel",
|
|
416
|
+
reason: result.reason,
|
|
417
|
+
outcomeType: result.reason !== "error" /* ERROR */ ? result.outcomeType : void 0
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return Object.assign(result, {
|
|
421
|
+
metrics: {
|
|
422
|
+
evaluationMs: evaluationDurationMs,
|
|
423
|
+
readMs: datafile.metrics.readMs,
|
|
424
|
+
source: datafile.metrics.source,
|
|
425
|
+
cacheStatus: datafile.metrics.cacheStatus,
|
|
426
|
+
connectionState: datafile.metrics.connectionState
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/create-raw-client.ts
|
|
432
|
+
var idCount = 0;
|
|
433
|
+
function createCreateRawClient(fns) {
|
|
434
|
+
return function createRawClient({
|
|
435
|
+
dataSource,
|
|
436
|
+
origin
|
|
437
|
+
}) {
|
|
438
|
+
const id = idCount++;
|
|
439
|
+
clientMap.set(id, { dataSource, initialized: false });
|
|
440
|
+
const api = {
|
|
441
|
+
origin,
|
|
442
|
+
initialize: async () => {
|
|
443
|
+
let instance = clientMap.get(id);
|
|
444
|
+
if (!instance) {
|
|
445
|
+
instance = { dataSource, initialized: false };
|
|
446
|
+
clientMap.set(id, instance);
|
|
447
|
+
}
|
|
448
|
+
if (instance.initialized) return;
|
|
449
|
+
const promise = fns.initialize(id);
|
|
450
|
+
await promise;
|
|
451
|
+
instance.initialized = true;
|
|
452
|
+
return promise;
|
|
453
|
+
},
|
|
454
|
+
shutdown: async () => {
|
|
455
|
+
await fns.shutdown(id);
|
|
456
|
+
clientMap.delete(id);
|
|
457
|
+
},
|
|
458
|
+
getDatafile: () => fns.getDatafile(id),
|
|
459
|
+
getFallbackDatafile: () => {
|
|
460
|
+
return fns.getFallbackDatafile(id);
|
|
461
|
+
},
|
|
462
|
+
evaluate: async (flagKey, defaultValue, entities) => {
|
|
463
|
+
const instance = clientMap.get(id);
|
|
464
|
+
if (!instance?.initialized) await api.initialize();
|
|
465
|
+
return fns.evaluate(id, flagKey, defaultValue, entities);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
return api;
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/errors.ts
|
|
473
|
+
var FallbackNotFoundError = class extends Error {
|
|
474
|
+
constructor() {
|
|
475
|
+
super("@vercel/flags-core: Bundled definitions file not found.");
|
|
476
|
+
this.name = "FallbackNotFoundError";
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
var FallbackEntryNotFoundError = class extends Error {
|
|
480
|
+
constructor() {
|
|
481
|
+
super("@vercel/flags-core: No bundled definitions found for SDK key.");
|
|
482
|
+
this.name = "FallbackEntryNotFoundError";
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// src/utils/read-bundled-definitions.ts
|
|
487
|
+
var sdkKeyHashCache = /* @__PURE__ */ new Map();
|
|
488
|
+
async function computeHash(sdkKey) {
|
|
489
|
+
const hashBuffer = await crypto.subtle.digest(
|
|
490
|
+
"SHA-256",
|
|
491
|
+
new TextEncoder().encode(sdkKey)
|
|
492
|
+
);
|
|
493
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
494
|
+
return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
495
|
+
}
|
|
496
|
+
function hashSdkKey(sdkKey) {
|
|
497
|
+
const cached = sdkKeyHashCache.get(sdkKey);
|
|
498
|
+
if (cached) return cached;
|
|
499
|
+
const promise = computeHash(sdkKey);
|
|
500
|
+
sdkKeyHashCache.set(sdkKey, promise);
|
|
501
|
+
return promise;
|
|
502
|
+
}
|
|
503
|
+
async function readBundledDefinitions(sdkKey) {
|
|
504
|
+
let get;
|
|
505
|
+
try {
|
|
506
|
+
const module = await import(
|
|
507
|
+
/* turbopackOptional: true */
|
|
508
|
+
// @ts-expect-error this only exists at build time
|
|
509
|
+
"@vercel/flags-definitions"
|
|
510
|
+
);
|
|
511
|
+
get = module.get;
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (error && typeof error === "object" && "code" in error && error.code === "MODULE_NOT_FOUND") {
|
|
514
|
+
return { definitions: null, state: "missing-file" };
|
|
515
|
+
}
|
|
516
|
+
return { definitions: null, state: "unexpected-error", error };
|
|
517
|
+
}
|
|
518
|
+
const entry = get(sdkKey);
|
|
519
|
+
if (entry) return { definitions: entry, state: "ok" };
|
|
520
|
+
try {
|
|
521
|
+
const hashedKey = await hashSdkKey(sdkKey);
|
|
522
|
+
const hashedEntry = get(hashedKey);
|
|
523
|
+
if (hashedEntry) return { definitions: hashedEntry, state: "ok" };
|
|
524
|
+
} catch (error) {
|
|
525
|
+
return { definitions: null, state: "unexpected-error", error };
|
|
526
|
+
}
|
|
527
|
+
return { definitions: null, state: "missing-entry" };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/utils/sleep.ts
|
|
531
|
+
function sleep(ms) {
|
|
532
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/utils/usage-tracker.ts
|
|
536
|
+
import { waitUntil } from "@vercel/functions";
|
|
537
|
+
var RESOLVED_VOID = Promise.resolve();
|
|
538
|
+
var isDebugMode = process.env.DEBUG?.includes("@vercel/flags-core");
|
|
539
|
+
var debugLog = (...args) => {
|
|
540
|
+
if (!isDebugMode) return;
|
|
541
|
+
console.log(...args);
|
|
542
|
+
};
|
|
543
|
+
var MAX_BATCH_SIZE = 50;
|
|
544
|
+
var MAX_BATCH_WAIT_MS = 5e3;
|
|
545
|
+
var trackedRequests = /* @__PURE__ */ new WeakSet();
|
|
546
|
+
var SYMBOL_FOR_REQ_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context");
|
|
547
|
+
var fromSymbol = globalThis;
|
|
548
|
+
function getRequestContext() {
|
|
549
|
+
try {
|
|
550
|
+
const ctx = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.();
|
|
551
|
+
if (ctx && Object.hasOwn(ctx, "headers")) {
|
|
552
|
+
return {
|
|
553
|
+
ctx,
|
|
554
|
+
headers: ctx.headers
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return { ctx, headers: void 0 };
|
|
558
|
+
} catch {
|
|
559
|
+
return { ctx: void 0, headers: void 0 };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
var UsageTracker = class {
|
|
563
|
+
sdkKey;
|
|
564
|
+
host;
|
|
565
|
+
batcher = {
|
|
566
|
+
events: [],
|
|
567
|
+
resolveWait: null,
|
|
568
|
+
pending: null
|
|
569
|
+
};
|
|
570
|
+
constructor(options) {
|
|
571
|
+
this.sdkKey = options.sdkKey;
|
|
572
|
+
this.host = options.host;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Triggers an immediate flush of any pending events.
|
|
576
|
+
* Returns a promise that resolves when the flush completes.
|
|
577
|
+
*/
|
|
578
|
+
flush() {
|
|
579
|
+
this.batcher.resolveWait?.();
|
|
580
|
+
return this.batcher.pending ?? RESOLVED_VOID;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Tracks a config read event. Deduplicates by request context.
|
|
584
|
+
*/
|
|
585
|
+
trackRead(options) {
|
|
586
|
+
try {
|
|
587
|
+
const { ctx, headers } = getRequestContext();
|
|
588
|
+
if (ctx) {
|
|
589
|
+
if (trackedRequests.has(ctx)) return;
|
|
590
|
+
trackedRequests.add(ctx);
|
|
591
|
+
}
|
|
592
|
+
const event = {
|
|
593
|
+
type: "FLAGS_CONFIG_READ",
|
|
594
|
+
ts: Date.now(),
|
|
595
|
+
payload: {
|
|
596
|
+
deploymentId: process.env.VERCEL_DEPLOYMENT_ID,
|
|
597
|
+
region: process.env.VERCEL_REGION
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
if (headers) {
|
|
601
|
+
event.payload.vercelRequestId = headers["x-vercel-id"] ?? void 0;
|
|
602
|
+
event.payload.invocationHost = headers.host ?? void 0;
|
|
603
|
+
}
|
|
604
|
+
if (options) {
|
|
605
|
+
event.payload.configOrigin = options.configOrigin;
|
|
606
|
+
if (options.cacheStatus !== void 0) {
|
|
607
|
+
event.payload.cacheStatus = options.cacheStatus;
|
|
608
|
+
}
|
|
609
|
+
if (options.cacheIsFirstRead !== void 0) {
|
|
610
|
+
event.payload.cacheIsFirstRead = options.cacheIsFirstRead;
|
|
611
|
+
}
|
|
612
|
+
if (options.cacheIsBlocking !== void 0) {
|
|
613
|
+
event.payload.cacheIsBlocking = options.cacheIsBlocking;
|
|
614
|
+
}
|
|
615
|
+
if (options.duration !== void 0) {
|
|
616
|
+
event.payload.duration = options.duration;
|
|
617
|
+
}
|
|
618
|
+
if (options.configUpdatedAt !== void 0) {
|
|
619
|
+
event.payload.configUpdatedAt = options.configUpdatedAt;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
this.batcher.events.push(event);
|
|
623
|
+
this.scheduleFlush();
|
|
624
|
+
} catch (error) {
|
|
625
|
+
console.error("@vercel/flags-core: Failed to record event:", error);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
scheduleFlush() {
|
|
629
|
+
if (!this.batcher.pending) {
|
|
630
|
+
let timeout = null;
|
|
631
|
+
const pending = (async () => {
|
|
632
|
+
await new Promise((res) => {
|
|
633
|
+
this.batcher.resolveWait = res;
|
|
634
|
+
timeout = setTimeout(res, MAX_BATCH_WAIT_MS);
|
|
635
|
+
});
|
|
636
|
+
this.batcher.pending = null;
|
|
637
|
+
this.batcher.resolveWait = null;
|
|
638
|
+
if (timeout) clearTimeout(timeout);
|
|
639
|
+
await this.flushEvents();
|
|
640
|
+
})();
|
|
641
|
+
waitUntil(pending);
|
|
642
|
+
this.batcher.pending = pending;
|
|
643
|
+
}
|
|
644
|
+
if (this.batcher.events.length >= MAX_BATCH_SIZE) {
|
|
645
|
+
this.batcher.resolveWait?.();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async flushEvents() {
|
|
649
|
+
if (this.batcher.events.length === 0) return;
|
|
650
|
+
const eventsToSend = this.batcher.events;
|
|
651
|
+
this.batcher.events = [];
|
|
652
|
+
try {
|
|
653
|
+
const response = await fetch(`${this.host}/v1/ingest`, {
|
|
654
|
+
method: "POST",
|
|
655
|
+
headers: {
|
|
656
|
+
"Content-Type": "application/json",
|
|
657
|
+
Authorization: `Bearer ${this.sdkKey}`,
|
|
658
|
+
"User-Agent": `VercelFlagsCore/${version}`,
|
|
659
|
+
...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
|
|
660
|
+
},
|
|
661
|
+
body: JSON.stringify(eventsToSend)
|
|
662
|
+
});
|
|
663
|
+
debugLog(
|
|
664
|
+
`@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
|
|
665
|
+
);
|
|
666
|
+
if (!response.ok) {
|
|
667
|
+
debugLog(
|
|
668
|
+
"@vercel/flags-core: Failed to send events:",
|
|
669
|
+
response.statusText
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
} catch (error) {
|
|
673
|
+
debugLog("@vercel/flags-core: Error sending events:", error);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// src/data-source/stream-connection.ts
|
|
679
|
+
var MAX_RETRY_COUNT = 15;
|
|
680
|
+
var BASE_DELAY_MS = 1e3;
|
|
681
|
+
var MAX_DELAY_MS = 6e4;
|
|
682
|
+
function backoff(retryCount) {
|
|
683
|
+
if (retryCount === 1) return 0;
|
|
684
|
+
const delay = Math.min(BASE_DELAY_MS * 2 ** (retryCount - 2), MAX_DELAY_MS);
|
|
685
|
+
return delay + Math.random() * 1e3;
|
|
686
|
+
}
|
|
687
|
+
async function connectStream(config, callbacks) {
|
|
688
|
+
const {
|
|
689
|
+
host,
|
|
690
|
+
sdkKey,
|
|
691
|
+
abortController,
|
|
692
|
+
fetch: fetchFn = globalThis.fetch
|
|
693
|
+
} = config;
|
|
694
|
+
const { onMessage, onDisconnect } = callbacks;
|
|
695
|
+
let retryCount = 0;
|
|
696
|
+
let resolveInit;
|
|
697
|
+
let rejectInit;
|
|
698
|
+
const initPromise = new Promise((resolve, reject) => {
|
|
699
|
+
resolveInit = resolve;
|
|
700
|
+
rejectInit = reject;
|
|
701
|
+
});
|
|
702
|
+
void (async () => {
|
|
703
|
+
let initialDataReceived = false;
|
|
704
|
+
while (!abortController.signal.aborted) {
|
|
705
|
+
if (retryCount > MAX_RETRY_COUNT) {
|
|
706
|
+
console.error("@vercel/flags-core: Max retry count exceeded");
|
|
707
|
+
abortController.abort();
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
const response = await fetchFn(`${host}/v1/stream`, {
|
|
712
|
+
headers: {
|
|
713
|
+
Authorization: `Bearer ${sdkKey}`,
|
|
714
|
+
"User-Agent": `VercelFlagsCore/${version}`,
|
|
715
|
+
"X-Retry-Attempt": String(retryCount)
|
|
716
|
+
},
|
|
717
|
+
signal: abortController.signal
|
|
718
|
+
});
|
|
719
|
+
if (!response.ok) {
|
|
720
|
+
if (response.status === 401) {
|
|
721
|
+
abortController.abort();
|
|
722
|
+
}
|
|
723
|
+
throw new Error(`stream was not ok: ${response.status}`);
|
|
724
|
+
}
|
|
725
|
+
if (!response.body) {
|
|
726
|
+
throw new Error("stream body was not present");
|
|
727
|
+
}
|
|
728
|
+
const decoder = new TextDecoder();
|
|
729
|
+
let buffer = "";
|
|
730
|
+
for await (const chunk of response.body) {
|
|
731
|
+
if (abortController.signal.aborted) break;
|
|
732
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
733
|
+
const lines = buffer.split("\n");
|
|
734
|
+
buffer = lines.pop();
|
|
735
|
+
for (const line of lines) {
|
|
736
|
+
if (line === "") continue;
|
|
737
|
+
let message;
|
|
738
|
+
try {
|
|
739
|
+
message = JSON.parse(line);
|
|
740
|
+
} catch {
|
|
741
|
+
console.warn(
|
|
742
|
+
"@vercel/flags-core: Failed to parse stream message, skipping"
|
|
743
|
+
);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (message.type === "datafile") {
|
|
747
|
+
onMessage(message.data);
|
|
748
|
+
retryCount = 0;
|
|
749
|
+
if (!initialDataReceived) {
|
|
750
|
+
initialDataReceived = true;
|
|
751
|
+
resolveInit();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (!abortController.signal.aborted) {
|
|
757
|
+
onDisconnect?.();
|
|
758
|
+
retryCount++;
|
|
759
|
+
await sleep(backoff(retryCount));
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (abortController.signal.aborted) {
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
console.error("@vercel/flags-core: Stream error", error);
|
|
767
|
+
onDisconnect?.();
|
|
768
|
+
if (!initialDataReceived) {
|
|
769
|
+
rejectInit(error);
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
retryCount++;
|
|
773
|
+
await sleep(backoff(retryCount));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
})();
|
|
777
|
+
return initPromise;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/data-source/flag-network-data-source.ts
|
|
781
|
+
var FLAGS_HOST = "https://flags.vercel.com";
|
|
782
|
+
var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
|
|
783
|
+
var DEFAULT_POLLING_INTERVAL_MS = 3e4;
|
|
784
|
+
var DEFAULT_POLLING_INIT_TIMEOUT_MS = 3e3;
|
|
785
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
786
|
+
var MAX_FETCH_RETRIES = 3;
|
|
787
|
+
var FETCH_RETRY_BASE_DELAY_MS = 500;
|
|
788
|
+
function normalizeOptions(options) {
|
|
789
|
+
const autoDetectedBuildStep = process.env.CI === "1" || process.env.NEXT_PHASE === "phase-production-build";
|
|
790
|
+
const buildStep = options.buildStep ?? autoDetectedBuildStep;
|
|
791
|
+
let stream;
|
|
792
|
+
if (options.stream === void 0 || options.stream === true) {
|
|
793
|
+
stream = { enabled: true, initTimeoutMs: DEFAULT_STREAM_INIT_TIMEOUT_MS };
|
|
794
|
+
} else if (options.stream === false) {
|
|
795
|
+
stream = { enabled: false, initTimeoutMs: 0 };
|
|
796
|
+
} else {
|
|
797
|
+
stream = { enabled: true, initTimeoutMs: options.stream.initTimeoutMs };
|
|
798
|
+
}
|
|
799
|
+
let polling;
|
|
800
|
+
if (options.polling === void 0 || options.polling === true) {
|
|
801
|
+
polling = {
|
|
802
|
+
enabled: true,
|
|
803
|
+
intervalMs: DEFAULT_POLLING_INTERVAL_MS,
|
|
804
|
+
initTimeoutMs: DEFAULT_POLLING_INIT_TIMEOUT_MS
|
|
805
|
+
};
|
|
806
|
+
} else if (options.polling === false) {
|
|
807
|
+
polling = { enabled: false, intervalMs: 0, initTimeoutMs: 0 };
|
|
808
|
+
} else {
|
|
809
|
+
polling = {
|
|
810
|
+
enabled: true,
|
|
811
|
+
intervalMs: options.polling.intervalMs,
|
|
812
|
+
initTimeoutMs: options.polling.initTimeoutMs
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
sdkKey: options.sdkKey,
|
|
817
|
+
datafile: options.datafile,
|
|
818
|
+
stream,
|
|
819
|
+
polling,
|
|
820
|
+
buildStep,
|
|
821
|
+
fetch: options.fetch ?? globalThis.fetch
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
async function fetchDatafile(host, sdkKey, fetchFn) {
|
|
825
|
+
let lastError;
|
|
826
|
+
for (let attempt = 0; attempt < MAX_FETCH_RETRIES; attempt++) {
|
|
827
|
+
const controller = new AbortController();
|
|
828
|
+
const timeoutId = setTimeout(
|
|
829
|
+
() => controller.abort(),
|
|
830
|
+
DEFAULT_FETCH_TIMEOUT_MS
|
|
831
|
+
);
|
|
832
|
+
let shouldRetry = true;
|
|
833
|
+
try {
|
|
834
|
+
const res = await fetchFn(`${host}/v1/datafile`, {
|
|
835
|
+
headers: {
|
|
836
|
+
Authorization: `Bearer ${sdkKey}`,
|
|
837
|
+
"User-Agent": `VercelFlagsCore/${version}`
|
|
838
|
+
},
|
|
839
|
+
signal: controller.signal
|
|
840
|
+
});
|
|
841
|
+
clearTimeout(timeoutId);
|
|
842
|
+
if (!res.ok) {
|
|
843
|
+
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
|
|
844
|
+
shouldRetry = false;
|
|
845
|
+
}
|
|
846
|
+
throw new Error(`Failed to fetch data: ${res.statusText}`);
|
|
847
|
+
}
|
|
848
|
+
return res.json();
|
|
849
|
+
} catch (error) {
|
|
850
|
+
clearTimeout(timeoutId);
|
|
851
|
+
lastError = error instanceof Error ? error : new Error("Unknown fetch error");
|
|
852
|
+
if (!shouldRetry) throw lastError;
|
|
853
|
+
if (attempt < MAX_FETCH_RETRIES - 1) {
|
|
854
|
+
const delay = FETCH_RETRY_BASE_DELAY_MS * 2 ** attempt + Math.random() * 500;
|
|
855
|
+
await sleep(delay);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
throw lastError ?? new Error("Failed to fetch data after retries");
|
|
860
|
+
}
|
|
861
|
+
var FlagNetworkDataSource = class _FlagNetworkDataSource {
|
|
862
|
+
options;
|
|
863
|
+
host = FLAGS_HOST;
|
|
864
|
+
// Data state
|
|
865
|
+
data;
|
|
866
|
+
bundledDefinitionsPromise;
|
|
867
|
+
// Stream state
|
|
868
|
+
streamAbortController;
|
|
869
|
+
streamPromise;
|
|
870
|
+
isStreamConnected = false;
|
|
871
|
+
hasWarnedAboutStaleData = false;
|
|
872
|
+
// Polling state
|
|
873
|
+
pollingIntervalId;
|
|
874
|
+
pollingAbortController;
|
|
875
|
+
// Usage tracking
|
|
876
|
+
usageTracker;
|
|
877
|
+
isFirstGetData = true;
|
|
878
|
+
/**
|
|
879
|
+
* Creates a new FlagNetworkDataSource instance.
|
|
880
|
+
*/
|
|
881
|
+
constructor(options) {
|
|
882
|
+
if (!options.sdkKey || typeof options.sdkKey !== "string" || !options.sdkKey.startsWith("vf_")) {
|
|
883
|
+
throw new Error(
|
|
884
|
+
'@vercel/flags-core: SDK key must be a string starting with "vf_"'
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
this.options = normalizeOptions(options);
|
|
888
|
+
this.bundledDefinitionsPromise = readBundledDefinitions(
|
|
889
|
+
this.options.sdkKey
|
|
890
|
+
);
|
|
891
|
+
if (this.options.datafile) {
|
|
892
|
+
this.data = this.options.datafile;
|
|
893
|
+
}
|
|
894
|
+
this.usageTracker = new UsageTracker({
|
|
895
|
+
sdkKey: this.options.sdkKey,
|
|
896
|
+
host: this.host
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
// ---------------------------------------------------------------------------
|
|
900
|
+
// Public API (DataSource interface)
|
|
901
|
+
// ---------------------------------------------------------------------------
|
|
902
|
+
/**
|
|
903
|
+
* Initializes the data source.
|
|
904
|
+
*
|
|
905
|
+
* Build step: datafile → bundled → fetch
|
|
906
|
+
* Runtime: stream → poll → datafile → bundled
|
|
907
|
+
*/
|
|
908
|
+
async initialize() {
|
|
909
|
+
if (this.options.buildStep) {
|
|
910
|
+
await this.initializeForBuildStep();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (!this.data && this.options.datafile) {
|
|
914
|
+
this.data = this.options.datafile;
|
|
915
|
+
}
|
|
916
|
+
if (this.data) {
|
|
917
|
+
this.startBackgroundUpdates();
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (this.options.stream.enabled) {
|
|
921
|
+
const streamSuccess = await this.tryInitializeStream();
|
|
922
|
+
if (streamSuccess) return;
|
|
923
|
+
}
|
|
924
|
+
if (this.options.polling.enabled) {
|
|
925
|
+
const pollingSuccess = await this.tryInitializePolling();
|
|
926
|
+
if (pollingSuccess) return;
|
|
927
|
+
}
|
|
928
|
+
if (this.data) return;
|
|
929
|
+
await this.initializeFromBundled();
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Reads the current datafile with metrics.
|
|
933
|
+
*/
|
|
934
|
+
async read() {
|
|
935
|
+
const startTime = Date.now();
|
|
936
|
+
const cachedData = this.data;
|
|
937
|
+
const cacheHadDefinitions = cachedData !== void 0;
|
|
938
|
+
const isFirstRead = this.isFirstGetData;
|
|
939
|
+
this.isFirstGetData = false;
|
|
940
|
+
let result;
|
|
941
|
+
let source;
|
|
942
|
+
let cacheStatus;
|
|
943
|
+
if (this.options.buildStep) {
|
|
944
|
+
[result, source, cacheStatus] = await this.getDataForBuildStep();
|
|
945
|
+
} else if (cachedData) {
|
|
946
|
+
[result, source, cacheStatus] = this.getDataFromCache(cachedData);
|
|
947
|
+
} else {
|
|
948
|
+
[result, source, cacheStatus] = await this.getDataWithFallbacks();
|
|
949
|
+
}
|
|
950
|
+
const readMs = Date.now() - startTime;
|
|
951
|
+
this.trackRead(startTime, cacheHadDefinitions, isFirstRead, source);
|
|
952
|
+
return Object.assign(result, {
|
|
953
|
+
metrics: {
|
|
954
|
+
readMs,
|
|
955
|
+
source,
|
|
956
|
+
cacheStatus,
|
|
957
|
+
connectionState: this.isStreamConnected ? "connected" : "disconnected"
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Shuts down the data source and releases resources.
|
|
963
|
+
*/
|
|
964
|
+
async shutdown() {
|
|
965
|
+
this.stopStream();
|
|
966
|
+
this.stopPolling();
|
|
967
|
+
this.data = this.options.datafile;
|
|
968
|
+
this.isStreamConnected = false;
|
|
969
|
+
this.hasWarnedAboutStaleData = false;
|
|
970
|
+
await this.usageTracker.flush();
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Returns the datafile with metrics.
|
|
974
|
+
*
|
|
975
|
+
* During builds this will read from the bundled file if available.
|
|
976
|
+
*
|
|
977
|
+
* This method never opens a streaming connection, but will read from
|
|
978
|
+
* the stream if it is already open. Otherwise it fetches over the network.
|
|
979
|
+
*/
|
|
980
|
+
async getDatafile() {
|
|
981
|
+
const startTime = Date.now();
|
|
982
|
+
let result;
|
|
983
|
+
let source;
|
|
984
|
+
let cacheStatus;
|
|
985
|
+
if (this.options.buildStep) {
|
|
986
|
+
[result, source, cacheStatus] = await this.getDataForBuildStep();
|
|
987
|
+
} else if (this.isStreamConnected && this.data) {
|
|
988
|
+
[result, source, cacheStatus] = this.getDataFromCache();
|
|
989
|
+
} else {
|
|
990
|
+
const fetched = await fetchDatafile(
|
|
991
|
+
this.host,
|
|
992
|
+
this.options.sdkKey,
|
|
993
|
+
this.options.fetch
|
|
994
|
+
);
|
|
995
|
+
if (this.isNewerData(fetched)) {
|
|
996
|
+
this.data = fetched;
|
|
997
|
+
}
|
|
998
|
+
[result, source, cacheStatus] = [this.data ?? fetched, "remote", "MISS"];
|
|
999
|
+
}
|
|
1000
|
+
return Object.assign(result, {
|
|
1001
|
+
metrics: {
|
|
1002
|
+
readMs: Date.now() - startTime,
|
|
1003
|
+
source,
|
|
1004
|
+
cacheStatus,
|
|
1005
|
+
connectionState: this.isStreamConnected ? "connected" : "disconnected"
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Returns the bundled fallback datafile.
|
|
1011
|
+
*/
|
|
1012
|
+
async getFallbackDatafile() {
|
|
1013
|
+
if (!this.bundledDefinitionsPromise) {
|
|
1014
|
+
throw new FallbackNotFoundError();
|
|
1015
|
+
}
|
|
1016
|
+
const bundledResult = await this.bundledDefinitionsPromise;
|
|
1017
|
+
if (!bundledResult) {
|
|
1018
|
+
throw new FallbackNotFoundError();
|
|
1019
|
+
}
|
|
1020
|
+
switch (bundledResult.state) {
|
|
1021
|
+
case "ok":
|
|
1022
|
+
return bundledResult.definitions;
|
|
1023
|
+
case "missing-file":
|
|
1024
|
+
throw new FallbackNotFoundError();
|
|
1025
|
+
case "missing-entry":
|
|
1026
|
+
throw new FallbackEntryNotFoundError();
|
|
1027
|
+
case "unexpected-error":
|
|
1028
|
+
throw new Error(
|
|
1029
|
+
"@vercel/flags-core: Failed to read bundled definitions: " + String(bundledResult.error)
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
// ---------------------------------------------------------------------------
|
|
1034
|
+
// Stream management
|
|
1035
|
+
// ---------------------------------------------------------------------------
|
|
1036
|
+
/**
|
|
1037
|
+
* Attempts to initialize via stream with timeout.
|
|
1038
|
+
* Returns true if stream connected successfully within timeout.
|
|
1039
|
+
*/
|
|
1040
|
+
async tryInitializeStream() {
|
|
1041
|
+
let streamPromise;
|
|
1042
|
+
if (this.options.stream.initTimeoutMs <= 0) {
|
|
1043
|
+
try {
|
|
1044
|
+
streamPromise = this.startStream();
|
|
1045
|
+
await streamPromise;
|
|
1046
|
+
return true;
|
|
1047
|
+
} catch {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
let timeoutId;
|
|
1052
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1053
|
+
timeoutId = setTimeout(
|
|
1054
|
+
() => resolve("timeout"),
|
|
1055
|
+
this.options.stream.initTimeoutMs
|
|
1056
|
+
);
|
|
1057
|
+
});
|
|
1058
|
+
try {
|
|
1059
|
+
streamPromise = this.startStream();
|
|
1060
|
+
const result = await Promise.race([streamPromise, timeoutPromise]);
|
|
1061
|
+
clearTimeout(timeoutId);
|
|
1062
|
+
if (result === "timeout") {
|
|
1063
|
+
console.warn(
|
|
1064
|
+
"@vercel/flags-core: Stream initialization timeout, falling back"
|
|
1065
|
+
);
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
return true;
|
|
1069
|
+
} catch {
|
|
1070
|
+
clearTimeout(timeoutId);
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Starts the stream connection with callbacks for data and disconnect.
|
|
1076
|
+
*/
|
|
1077
|
+
startStream() {
|
|
1078
|
+
if (this.streamPromise) return this.streamPromise;
|
|
1079
|
+
this.streamAbortController = new AbortController();
|
|
1080
|
+
this.isStreamConnected = false;
|
|
1081
|
+
this.hasWarnedAboutStaleData = false;
|
|
1082
|
+
try {
|
|
1083
|
+
const streamPromise = connectStream(
|
|
1084
|
+
{
|
|
1085
|
+
host: this.host,
|
|
1086
|
+
sdkKey: this.options.sdkKey,
|
|
1087
|
+
abortController: this.streamAbortController,
|
|
1088
|
+
fetch: this.options.fetch
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
onMessage: (newData) => {
|
|
1092
|
+
if (this.isNewerData(newData)) {
|
|
1093
|
+
this.data = newData;
|
|
1094
|
+
}
|
|
1095
|
+
this.isStreamConnected = true;
|
|
1096
|
+
this.hasWarnedAboutStaleData = false;
|
|
1097
|
+
if (this.pollingIntervalId) {
|
|
1098
|
+
this.stopPolling();
|
|
1099
|
+
}
|
|
1100
|
+
},
|
|
1101
|
+
onDisconnect: () => {
|
|
1102
|
+
this.isStreamConnected = false;
|
|
1103
|
+
if (this.options.polling.enabled && !this.pollingIntervalId) {
|
|
1104
|
+
this.startPolling();
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
);
|
|
1109
|
+
this.streamPromise = streamPromise;
|
|
1110
|
+
return streamPromise;
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
this.streamPromise = void 0;
|
|
1113
|
+
this.streamAbortController = void 0;
|
|
1114
|
+
throw error;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Stops the stream connection.
|
|
1119
|
+
*/
|
|
1120
|
+
stopStream() {
|
|
1121
|
+
this.streamAbortController?.abort();
|
|
1122
|
+
this.streamAbortController = void 0;
|
|
1123
|
+
this.streamPromise = void 0;
|
|
1124
|
+
}
|
|
1125
|
+
// ---------------------------------------------------------------------------
|
|
1126
|
+
// Polling management
|
|
1127
|
+
// ---------------------------------------------------------------------------
|
|
1128
|
+
/**
|
|
1129
|
+
* Attempts to initialize via polling with timeout.
|
|
1130
|
+
* Returns true if first poll succeeded within timeout.
|
|
1131
|
+
*/
|
|
1132
|
+
async tryInitializePolling() {
|
|
1133
|
+
this.pollingAbortController = new AbortController();
|
|
1134
|
+
const pollPromise = this.performPoll();
|
|
1135
|
+
if (this.options.polling.initTimeoutMs <= 0) {
|
|
1136
|
+
try {
|
|
1137
|
+
await pollPromise;
|
|
1138
|
+
if (this.data) {
|
|
1139
|
+
this.startPollingInterval();
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
return false;
|
|
1143
|
+
} catch {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
let timeoutId;
|
|
1148
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1149
|
+
timeoutId = setTimeout(
|
|
1150
|
+
() => resolve("timeout"),
|
|
1151
|
+
this.options.polling.initTimeoutMs
|
|
1152
|
+
);
|
|
1153
|
+
});
|
|
1154
|
+
try {
|
|
1155
|
+
const result = await Promise.race([pollPromise, timeoutPromise]);
|
|
1156
|
+
clearTimeout(timeoutId);
|
|
1157
|
+
if (result === "timeout") {
|
|
1158
|
+
console.warn(
|
|
1159
|
+
"@vercel/flags-core: Polling initialization timeout, falling back"
|
|
1160
|
+
);
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
if (this.data) {
|
|
1164
|
+
this.startPollingInterval();
|
|
1165
|
+
return true;
|
|
1166
|
+
}
|
|
1167
|
+
return false;
|
|
1168
|
+
} catch {
|
|
1169
|
+
clearTimeout(timeoutId);
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Starts polling (initial poll + interval).
|
|
1175
|
+
*/
|
|
1176
|
+
startPolling() {
|
|
1177
|
+
if (this.pollingIntervalId) return;
|
|
1178
|
+
this.pollingAbortController = new AbortController();
|
|
1179
|
+
void this.performPoll();
|
|
1180
|
+
this.startPollingInterval();
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Starts the polling interval (without initial poll).
|
|
1184
|
+
*/
|
|
1185
|
+
startPollingInterval() {
|
|
1186
|
+
if (this.pollingIntervalId) return;
|
|
1187
|
+
this.pollingIntervalId = setInterval(
|
|
1188
|
+
() => void this.performPoll(),
|
|
1189
|
+
this.options.polling.intervalMs
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Stops polling.
|
|
1194
|
+
*/
|
|
1195
|
+
stopPolling() {
|
|
1196
|
+
if (this.pollingIntervalId) {
|
|
1197
|
+
clearInterval(this.pollingIntervalId);
|
|
1198
|
+
this.pollingIntervalId = void 0;
|
|
1199
|
+
}
|
|
1200
|
+
this.pollingAbortController?.abort();
|
|
1201
|
+
this.pollingAbortController = void 0;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Performs a single poll request.
|
|
1205
|
+
*/
|
|
1206
|
+
async performPoll() {
|
|
1207
|
+
if (this.pollingAbortController?.signal.aborted) return;
|
|
1208
|
+
try {
|
|
1209
|
+
const data = await fetchDatafile(
|
|
1210
|
+
this.host,
|
|
1211
|
+
this.options.sdkKey,
|
|
1212
|
+
this.options.fetch
|
|
1213
|
+
);
|
|
1214
|
+
if (this.isNewerData(data)) {
|
|
1215
|
+
this.data = data;
|
|
1216
|
+
}
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
console.error("@vercel/flags-core: Poll failed:", error);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
// ---------------------------------------------------------------------------
|
|
1222
|
+
// Background updates
|
|
1223
|
+
// ---------------------------------------------------------------------------
|
|
1224
|
+
/**
|
|
1225
|
+
* Starts background updates (stream or polling) without blocking.
|
|
1226
|
+
* Used when we already have data from provided datafile.
|
|
1227
|
+
*/
|
|
1228
|
+
startBackgroundUpdates() {
|
|
1229
|
+
if (this.options.stream.enabled) {
|
|
1230
|
+
void this.startStream();
|
|
1231
|
+
} else if (this.options.polling.enabled) {
|
|
1232
|
+
this.startPolling();
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
// ---------------------------------------------------------------------------
|
|
1236
|
+
// Build step helpers
|
|
1237
|
+
// ---------------------------------------------------------------------------
|
|
1238
|
+
/**
|
|
1239
|
+
* Initializes data for build step environments.
|
|
1240
|
+
*/
|
|
1241
|
+
async initializeForBuildStep() {
|
|
1242
|
+
if (this.data) return;
|
|
1243
|
+
if (this.bundledDefinitionsPromise) {
|
|
1244
|
+
const bundledResult = await this.bundledDefinitionsPromise;
|
|
1245
|
+
if (bundledResult?.state === "ok" && bundledResult.definitions) {
|
|
1246
|
+
this.data = bundledResult.definitions;
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
this.data = await fetchDatafile(
|
|
1251
|
+
this.host,
|
|
1252
|
+
this.options.sdkKey,
|
|
1253
|
+
this.options.fetch
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Retrieves data during build steps.
|
|
1258
|
+
*/
|
|
1259
|
+
async getDataForBuildStep() {
|
|
1260
|
+
if (this.data) {
|
|
1261
|
+
return [this.data, "in-memory", "HIT"];
|
|
1262
|
+
}
|
|
1263
|
+
if (this.bundledDefinitionsPromise) {
|
|
1264
|
+
const bundledResult = await this.bundledDefinitionsPromise;
|
|
1265
|
+
if (bundledResult?.state === "ok" && bundledResult.definitions) {
|
|
1266
|
+
this.data = bundledResult.definitions;
|
|
1267
|
+
return [this.data, "embedded", "MISS"];
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
this.data = await fetchDatafile(
|
|
1271
|
+
this.host,
|
|
1272
|
+
this.options.sdkKey,
|
|
1273
|
+
this.options.fetch
|
|
1274
|
+
);
|
|
1275
|
+
return [this.data, "remote", "MISS"];
|
|
1276
|
+
}
|
|
1277
|
+
// ---------------------------------------------------------------------------
|
|
1278
|
+
// Runtime helpers
|
|
1279
|
+
// ---------------------------------------------------------------------------
|
|
1280
|
+
/**
|
|
1281
|
+
* Returns data from the in-memory cache.
|
|
1282
|
+
*/
|
|
1283
|
+
getDataFromCache(cachedData) {
|
|
1284
|
+
const data = cachedData ?? this.data;
|
|
1285
|
+
this.warnIfDisconnected();
|
|
1286
|
+
const cacheStatus = this.isStreamConnected ? "HIT" : "STALE";
|
|
1287
|
+
return [data, "in-memory", cacheStatus];
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Retrieves data using the fallback chain.
|
|
1291
|
+
*/
|
|
1292
|
+
async getDataWithFallbacks() {
|
|
1293
|
+
if (this.options.stream.enabled) {
|
|
1294
|
+
const streamSuccess = await this.tryInitializeStream();
|
|
1295
|
+
if (streamSuccess && this.data) {
|
|
1296
|
+
return [this.data, "in-memory", "MISS"];
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (this.options.polling.enabled) {
|
|
1300
|
+
const pollingSuccess = await this.tryInitializePolling();
|
|
1301
|
+
if (pollingSuccess && this.data) {
|
|
1302
|
+
return [this.data, "remote", "MISS"];
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (this.options.datafile) {
|
|
1306
|
+
this.data = this.options.datafile;
|
|
1307
|
+
return [this.data, "in-memory", "STALE"];
|
|
1308
|
+
}
|
|
1309
|
+
if (this.bundledDefinitionsPromise) {
|
|
1310
|
+
const bundledResult = await this.bundledDefinitionsPromise;
|
|
1311
|
+
if (bundledResult?.state === "ok" && bundledResult.definitions) {
|
|
1312
|
+
console.warn(
|
|
1313
|
+
"@vercel/flags-core: Using bundled definitions as fallback"
|
|
1314
|
+
);
|
|
1315
|
+
this.data = bundledResult.definitions;
|
|
1316
|
+
return [this.data, "embedded", "STALE"];
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
throw new Error(
|
|
1320
|
+
"@vercel/flags-core: No flag definitions available. Ensure streaming/polling is enabled or provide a datafile."
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Initializes from bundled definitions.
|
|
1325
|
+
*/
|
|
1326
|
+
async initializeFromBundled() {
|
|
1327
|
+
if (!this.bundledDefinitionsPromise) {
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
"@vercel/flags-core: No flag definitions available. Ensure streaming/polling is enabled or provide a datafile."
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
const bundledResult = await this.bundledDefinitionsPromise;
|
|
1333
|
+
if (bundledResult?.state === "ok" && bundledResult.definitions) {
|
|
1334
|
+
this.data = bundledResult.definitions;
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
"@vercel/flags-core: No flag definitions available. Bundled definitions not found."
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Parses a configUpdatedAt value (number or string) into a numeric timestamp.
|
|
1343
|
+
* Returns undefined if the value is missing or cannot be parsed.
|
|
1344
|
+
*/
|
|
1345
|
+
static parseConfigUpdatedAt(value) {
|
|
1346
|
+
if (typeof value === "number") return value;
|
|
1347
|
+
if (typeof value === "string") {
|
|
1348
|
+
const parsed = Number(value);
|
|
1349
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1350
|
+
}
|
|
1351
|
+
return void 0;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Checks if the incoming data is newer than the current in-memory data.
|
|
1355
|
+
* Returns true if the update should proceed, false if it should be skipped.
|
|
1356
|
+
*
|
|
1357
|
+
* Always accepts the update if:
|
|
1358
|
+
* - There is no current data
|
|
1359
|
+
* - The current data has no configUpdatedAt
|
|
1360
|
+
* - The incoming data has no configUpdatedAt
|
|
1361
|
+
*
|
|
1362
|
+
* Skips the update only when both have configUpdatedAt and incoming is older.
|
|
1363
|
+
*/
|
|
1364
|
+
isNewerData(incoming) {
|
|
1365
|
+
if (!this.data) return true;
|
|
1366
|
+
const currentTs = _FlagNetworkDataSource.parseConfigUpdatedAt(
|
|
1367
|
+
this.data.configUpdatedAt
|
|
1368
|
+
);
|
|
1369
|
+
const incomingTs = _FlagNetworkDataSource.parseConfigUpdatedAt(
|
|
1370
|
+
incoming.configUpdatedAt
|
|
1371
|
+
);
|
|
1372
|
+
if (currentTs === void 0 || incomingTs === void 0) {
|
|
1373
|
+
return true;
|
|
1374
|
+
}
|
|
1375
|
+
return incomingTs >= currentTs;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Logs a warning if returning cached data while stream is disconnected.
|
|
1379
|
+
*/
|
|
1380
|
+
warnIfDisconnected() {
|
|
1381
|
+
if (!this.isStreamConnected && !this.hasWarnedAboutStaleData) {
|
|
1382
|
+
this.hasWarnedAboutStaleData = true;
|
|
1383
|
+
console.warn(
|
|
1384
|
+
"@vercel/flags-core: Returning in-memory flag definitions while stream is disconnected. Data may be stale."
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
// ---------------------------------------------------------------------------
|
|
1389
|
+
// Usage tracking
|
|
1390
|
+
// ---------------------------------------------------------------------------
|
|
1391
|
+
/**
|
|
1392
|
+
* Tracks a read operation for usage analytics.
|
|
1393
|
+
*/
|
|
1394
|
+
trackRead(startTime, cacheHadDefinitions, isFirstRead, source) {
|
|
1395
|
+
const configOrigin = source === "embedded" ? "embedded" : "in-memory";
|
|
1396
|
+
const trackOptions = {
|
|
1397
|
+
configOrigin,
|
|
1398
|
+
cacheStatus: cacheHadDefinitions ? "HIT" : "MISS",
|
|
1399
|
+
cacheIsBlocking: !cacheHadDefinitions,
|
|
1400
|
+
duration: Date.now() - startTime
|
|
1401
|
+
};
|
|
1402
|
+
const configUpdatedAt = this.data?.configUpdatedAt;
|
|
1403
|
+
if (typeof configUpdatedAt === "number") {
|
|
1404
|
+
trackOptions.configUpdatedAt = configUpdatedAt;
|
|
1405
|
+
}
|
|
1406
|
+
if (isFirstRead) {
|
|
1407
|
+
trackOptions.cacheIsFirstRead = true;
|
|
1408
|
+
}
|
|
1409
|
+
this.usageTracker.trackRead(trackOptions);
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
// src/utils/sdk-keys.ts
|
|
1414
|
+
function parseSdkKeyFromFlagsConnectionString(text) {
|
|
1415
|
+
if (text.startsWith("vf_")) return text;
|
|
1416
|
+
try {
|
|
1417
|
+
if (!text.startsWith("flags:")) return null;
|
|
1418
|
+
const params = new URLSearchParams(text.slice(6));
|
|
1419
|
+
return params.get("sdkKey");
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/index.make.ts
|
|
1426
|
+
function make(createRawClient) {
|
|
1427
|
+
let _defaultFlagsClient = null;
|
|
1428
|
+
function createClient2(sdkKeyOrConnectionString, options) {
|
|
1429
|
+
if (!sdkKeyOrConnectionString) throw new Error("flags: Missing sdkKey");
|
|
1430
|
+
const sdkKey = parseSdkKeyFromFlagsConnectionString(
|
|
1431
|
+
sdkKeyOrConnectionString
|
|
1432
|
+
);
|
|
1433
|
+
if (!sdkKey) {
|
|
1434
|
+
throw new Error("flags: Missing sdkKey in connection string");
|
|
1435
|
+
}
|
|
1436
|
+
const dataSource = new FlagNetworkDataSource({ sdkKey, ...options });
|
|
1437
|
+
return createRawClient({
|
|
1438
|
+
dataSource,
|
|
1439
|
+
origin: { provider: "vercel", sdkKey }
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
function resetDefaultFlagsClient2() {
|
|
1443
|
+
_defaultFlagsClient = null;
|
|
1444
|
+
}
|
|
1445
|
+
const flagsClient2 = new Proxy({}, {
|
|
1446
|
+
get(_, prop) {
|
|
1447
|
+
if (!_defaultFlagsClient) {
|
|
1448
|
+
if (!process.env.FLAGS) {
|
|
1449
|
+
throw new Error("flags: Missing environment variable FLAGS");
|
|
1450
|
+
}
|
|
1451
|
+
const sdkKey = parseSdkKeyFromFlagsConnectionString(process.env.FLAGS);
|
|
1452
|
+
if (!sdkKey) {
|
|
1453
|
+
throw new Error("flags: Missing sdkKey");
|
|
1454
|
+
}
|
|
1455
|
+
_defaultFlagsClient = createClient2(sdkKey);
|
|
1456
|
+
}
|
|
1457
|
+
return _defaultFlagsClient[prop];
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
return {
|
|
1461
|
+
flagsClient: flagsClient2,
|
|
1462
|
+
resetDefaultFlagsClient: resetDefaultFlagsClient2,
|
|
1463
|
+
createClient: createClient2
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// src/index.default.ts
|
|
1468
|
+
var {
|
|
1469
|
+
/**
|
|
1470
|
+
* A lazily-initialized default flags client.
|
|
1471
|
+
*
|
|
1472
|
+
* - relies on process.env.FLAGS
|
|
1473
|
+
* - does not use process.env.EDGE_CONFIG
|
|
1474
|
+
*/
|
|
1475
|
+
flagsClient,
|
|
1476
|
+
/**
|
|
1477
|
+
* For testing purposes
|
|
1478
|
+
*/
|
|
1479
|
+
resetDefaultFlagsClient,
|
|
1480
|
+
/**
|
|
1481
|
+
* Create a flags client based on an SDK Key
|
|
1482
|
+
*/
|
|
1483
|
+
createClient
|
|
1484
|
+
} = make(createCreateRawClient(client_fns_exports));
|
|
1485
|
+
|
|
1486
|
+
export {
|
|
1487
|
+
ResolutionReason,
|
|
1488
|
+
evaluate,
|
|
1489
|
+
FallbackNotFoundError,
|
|
1490
|
+
FallbackEntryNotFoundError,
|
|
1491
|
+
FlagNetworkDataSource,
|
|
1492
|
+
flagsClient,
|
|
1493
|
+
resetDefaultFlagsClient,
|
|
1494
|
+
createClient
|
|
1495
|
+
};
|
|
1496
|
+
//# sourceMappingURL=chunk-7BUTND2Q.js.map
|