@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.
@@ -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