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