@vercel/flags-core 1.0.1 → 1.1.0-3ca407e-20260306094417
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +2 -0
- package/dist/{chunk-5CDNQMQ6.js → chunk-MDVR4MXT.js} +990 -531
- package/dist/chunk-MDVR4MXT.js.map +1 -0
- package/dist/{chunk-BVKIEFFY.js → chunk-NEIY3ICC.js} +985 -527
- package/dist/chunk-NEIY3ICC.js.map +1 -0
- package/dist/chunk-OFRPUCDY.cjs +1976 -0
- package/dist/chunk-OFRPUCDY.cjs.map +1 -0
- package/dist/chunk-WRX7DC3S.cjs +1982 -0
- package/dist/chunk-WRX7DC3S.cjs.map +1 -0
- package/dist/index.default.cjs +22 -0
- package/dist/index.default.cjs.map +1 -0
- package/dist/index.default.d.cts +244 -0
- package/dist/index.default.d.ts +78 -83
- package/dist/index.default.js +4 -3
- package/dist/index.next-js.cjs +22 -0
- package/dist/index.next-js.cjs.map +1 -0
- package/dist/index.next-js.d.cts +231 -0
- package/dist/index.next-js.d.ts +78 -83
- package/dist/index.next-js.js +4 -3
- package/dist/openfeature.default.cjs +157 -0
- package/dist/openfeature.default.cjs.map +1 -0
- package/dist/openfeature.default.d.cts +48 -0
- package/dist/openfeature.default.d.ts +1 -1
- package/dist/openfeature.default.js +1 -1
- package/dist/openfeature.next-js.cjs +157 -0
- package/dist/openfeature.next-js.cjs.map +1 -0
- package/dist/openfeature.next-js.d.cts +49 -0
- package/dist/openfeature.next-js.d.ts +1 -1
- package/dist/openfeature.next-js.js +1 -1
- package/dist/{types-508sZTBu.d.ts → types-DtnecWgl.d.cts} +27 -5
- package/dist/types-DtnecWgl.d.ts +527 -0
- package/package.json +24 -11
- package/dist/chunk-5CDNQMQ6.js.map +0 -1
- package/dist/chunk-BVKIEFFY.js.map +0 -1
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// src/index.next-js.ts
|
|
2
2
|
import { cacheLife } from "next/cache";
|
|
3
3
|
|
|
4
|
-
// src/client-map.ts
|
|
5
|
-
var clientMap = /* @__PURE__ */ new Map();
|
|
6
|
-
|
|
7
4
|
// src/evaluate.ts
|
|
8
5
|
import { xxHash32 as hashInput } from "js-xxhash";
|
|
9
6
|
|
|
@@ -33,12 +30,11 @@ var Packed;
|
|
|
33
30
|
})(AccessorType = Packed2.AccessorType || (Packed2.AccessorType = {}));
|
|
34
31
|
})(Packed || (Packed = {}));
|
|
35
32
|
|
|
36
|
-
// src/
|
|
33
|
+
// src/evaluate.ts
|
|
34
|
+
var MAX_REGEX_INPUT_LENGTH = 1e4;
|
|
37
35
|
function exhaustivenessCheck(_) {
|
|
38
36
|
throw new Error("Exhaustiveness check failed");
|
|
39
37
|
}
|
|
40
|
-
|
|
41
|
-
// src/evaluate.ts
|
|
42
38
|
function getProperty(obj, pathArray) {
|
|
43
39
|
return pathArray.reduce((acc, key) => {
|
|
44
40
|
if (acc && key in acc) {
|
|
@@ -62,11 +58,33 @@ function isNumber(input) {
|
|
|
62
58
|
function isArray(input) {
|
|
63
59
|
return Array.isArray(input);
|
|
64
60
|
}
|
|
61
|
+
function lower(input) {
|
|
62
|
+
if (typeof input === "string") return input.toLowerCase();
|
|
63
|
+
if (Array.isArray(input)) return input.map(lower);
|
|
64
|
+
return input;
|
|
65
|
+
}
|
|
66
|
+
var IGNORE_CASE_COMPARATORS = /* @__PURE__ */ new Set([
|
|
67
|
+
"eq" /* EQ */,
|
|
68
|
+
"!eq" /* NOT_EQ */,
|
|
69
|
+
"oneOf" /* ONE_OF */,
|
|
70
|
+
"!oneOf" /* NOT_ONE_OF */,
|
|
71
|
+
"containsAllOf" /* CONTAINS_ALL_OF */,
|
|
72
|
+
"containsAnyOf" /* CONTAINS_ANY_OF */,
|
|
73
|
+
"containsNoneOf" /* CONTAINS_NONE_OF */,
|
|
74
|
+
"startsWith" /* STARTS_WITH */,
|
|
75
|
+
"!startsWith" /* NOT_STARTS_WITH */,
|
|
76
|
+
"endsWith" /* ENDS_WITH */,
|
|
77
|
+
"!endsWith" /* NOT_ENDS_WITH */,
|
|
78
|
+
"contains" /* CONTAINS */,
|
|
79
|
+
"!contains" /* NOT_CONTAINS */
|
|
80
|
+
]);
|
|
65
81
|
function matchTargetList(targets, params) {
|
|
66
|
-
for (const
|
|
67
|
-
|
|
82
|
+
for (const kind in targets) {
|
|
83
|
+
const attributes = targets[kind];
|
|
84
|
+
for (const attribute in attributes) {
|
|
68
85
|
const entity = access([kind, attribute], params);
|
|
69
|
-
if (isString(entity) &&
|
|
86
|
+
if (isString(entity) && attributes[attribute].includes(entity))
|
|
87
|
+
return true;
|
|
70
88
|
}
|
|
71
89
|
}
|
|
72
90
|
return false;
|
|
@@ -107,7 +125,7 @@ function matchSegmentCondition(cmp, rhs, params) {
|
|
|
107
125
|
return segmentIds.every((segmentId) => {
|
|
108
126
|
const segment = params.segments?.[segmentId];
|
|
109
127
|
if (!segment) return false;
|
|
110
|
-
return matchSegment(segment, params);
|
|
128
|
+
return !matchSegment(segment, params);
|
|
111
129
|
});
|
|
112
130
|
}
|
|
113
131
|
default:
|
|
@@ -116,11 +134,15 @@ function matchSegmentCondition(cmp, rhs, params) {
|
|
|
116
134
|
}
|
|
117
135
|
function matchConditions(conditions, params) {
|
|
118
136
|
return conditions.every((condition) => {
|
|
119
|
-
const [lhsAccessor, cmpKey,
|
|
137
|
+
const [lhsAccessor, cmpKey, rawRhs, options] = condition;
|
|
138
|
+
const hasIgnoreCaseFlag = typeof options === "string" && options.includes("i");
|
|
139
|
+
const hasIgnoreCaseOption = typeof options === "object" && options !== null && options.i === true;
|
|
140
|
+
const ignoreCase = IGNORE_CASE_COMPARATORS.has(cmpKey) && (hasIgnoreCaseFlag || hasIgnoreCaseOption);
|
|
120
141
|
if (lhsAccessor === Packed.AccessorType.SEGMENT) {
|
|
121
|
-
return
|
|
142
|
+
return rawRhs && matchSegmentCondition(cmpKey, rawRhs, params);
|
|
122
143
|
}
|
|
123
|
-
const lhs = access(lhsAccessor, params);
|
|
144
|
+
const lhs = ignoreCase ? lower(access(lhsAccessor, params)) : access(lhsAccessor, params);
|
|
145
|
+
const rhs = ignoreCase ? lower(rawRhs) : rawRhs;
|
|
124
146
|
try {
|
|
125
147
|
switch (cmpKey) {
|
|
126
148
|
case "eq" /* EQ */:
|
|
@@ -174,6 +196,10 @@ function matchConditions(conditions, params) {
|
|
|
174
196
|
return isString(lhs) && isString(rhs) && lhs.endsWith(rhs);
|
|
175
197
|
case "!endsWith" /* NOT_ENDS_WITH */:
|
|
176
198
|
return isString(lhs) && isString(rhs) && !lhs.endsWith(rhs);
|
|
199
|
+
case "contains" /* CONTAINS */:
|
|
200
|
+
return isString(lhs) && isString(rhs) && lhs.includes(rhs);
|
|
201
|
+
case "!contains" /* NOT_CONTAINS */:
|
|
202
|
+
return isString(lhs) && isString(rhs) && !lhs.includes(rhs);
|
|
177
203
|
case "ex" /* EXISTS */:
|
|
178
204
|
return lhs !== void 0 && lhs !== null;
|
|
179
205
|
case "!ex" /* NOT_EXISTS */:
|
|
@@ -191,12 +217,12 @@ function matchConditions(conditions, params) {
|
|
|
191
217
|
if (lhs === null || lhs === void 0) return false;
|
|
192
218
|
return (isNumber(rhs) || isString(rhs)) && lhs <= rhs;
|
|
193
219
|
case "regex" /* REGEX */:
|
|
194
|
-
if (isString(lhs) && typeof rhs === "object" && !Array.isArray(rhs) && rhs?.type === "regex") {
|
|
220
|
+
if (isString(lhs) && lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === "object" && !Array.isArray(rhs) && rhs?.type === "regex") {
|
|
195
221
|
return new RegExp(rhs.pattern, rhs.flags).test(lhs);
|
|
196
222
|
}
|
|
197
223
|
return false;
|
|
198
224
|
case "!regex" /* NOT_REGEX */:
|
|
199
|
-
if (isString(lhs) && typeof rhs === "object" && !Array.isArray(rhs) && rhs?.type === "regex") {
|
|
225
|
+
if (isString(lhs) && lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === "object" && !Array.isArray(rhs) && rhs?.type === "regex") {
|
|
200
226
|
return !new RegExp(rhs.pattern, rhs.flags).test(lhs);
|
|
201
227
|
}
|
|
202
228
|
return false;
|
|
@@ -287,7 +313,7 @@ function handleOutcome(params, outcome) {
|
|
|
287
313
|
}
|
|
288
314
|
}
|
|
289
315
|
}
|
|
290
|
-
function evaluate(params) {
|
|
316
|
+
function evaluate(params, _visited) {
|
|
291
317
|
const envConfig = params.definition.environments[params.environment];
|
|
292
318
|
if (typeof envConfig === "number") {
|
|
293
319
|
return Object.assign(handleOutcome(params, envConfig), {
|
|
@@ -308,7 +334,16 @@ function evaluate(params) {
|
|
|
308
334
|
`Could not find envConfig for "${envConfig.reuse}" when reusing`
|
|
309
335
|
);
|
|
310
336
|
}
|
|
311
|
-
|
|
337
|
+
const visited = _visited ?? /* @__PURE__ */ new Set();
|
|
338
|
+
if (visited.has(envConfig.reuse)) {
|
|
339
|
+
return {
|
|
340
|
+
reason: "error" /* ERROR */,
|
|
341
|
+
errorMessage: `Circular environment reuse detected: "${envConfig.reuse}"`,
|
|
342
|
+
value: params.defaultValue
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
visited.add(params.environment);
|
|
346
|
+
return evaluate({ ...params, environment: envConfig.reuse }, visited);
|
|
312
347
|
}
|
|
313
348
|
if (envConfig.targets) {
|
|
314
349
|
const matchedIndex = envConfig.targets.findIndex(
|
|
@@ -341,7 +376,7 @@ function findWeightedIndex(weights, value, maxValue) {
|
|
|
341
376
|
}
|
|
342
377
|
|
|
343
378
|
// package.json
|
|
344
|
-
var version = "1.0
|
|
379
|
+
var version = "1.1.0-3ca407e-20260306094417";
|
|
345
380
|
|
|
346
381
|
// src/lib/report-value.ts
|
|
347
382
|
function internalReportValue(key, value, data) {
|
|
@@ -353,37 +388,67 @@ function internalReportValue(key, value, data) {
|
|
|
353
388
|
});
|
|
354
389
|
}
|
|
355
390
|
|
|
356
|
-
// src/
|
|
391
|
+
// src/controller-fns.ts
|
|
392
|
+
var controllerInstanceMap = /* @__PURE__ */ new Map();
|
|
393
|
+
function getInstance(id) {
|
|
394
|
+
const instance = controllerInstanceMap.get(id);
|
|
395
|
+
if (!instance) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`@vercel/flags-core: Client instance ${id} not found. It may have been shut down.`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return instance;
|
|
401
|
+
}
|
|
357
402
|
function initialize(id) {
|
|
358
|
-
return
|
|
403
|
+
return getInstance(id).controller.initialize();
|
|
359
404
|
}
|
|
360
405
|
function shutdown(id) {
|
|
361
|
-
return
|
|
406
|
+
return getInstance(id).controller.shutdown();
|
|
362
407
|
}
|
|
363
408
|
function getDatafile(id) {
|
|
364
|
-
return
|
|
409
|
+
return getInstance(id).controller.getDatafile();
|
|
365
410
|
}
|
|
366
411
|
function getFallbackDatafile(id) {
|
|
367
|
-
const ds =
|
|
412
|
+
const ds = getInstance(id).controller;
|
|
368
413
|
if (ds.getFallbackDatafile) return ds.getFallbackDatafile();
|
|
369
414
|
throw new Error("flags: This data source does not support fallbacks");
|
|
370
415
|
}
|
|
371
416
|
async function evaluate2(id, flagKey, defaultValue, entities) {
|
|
372
|
-
const
|
|
373
|
-
|
|
417
|
+
const controller = getInstance(id).controller;
|
|
418
|
+
let datafile;
|
|
419
|
+
try {
|
|
420
|
+
datafile = await controller.read();
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (defaultValue !== void 0) {
|
|
423
|
+
return {
|
|
424
|
+
value: defaultValue,
|
|
425
|
+
reason: "error" /* ERROR */,
|
|
426
|
+
errorMessage: error instanceof Error ? error.message : "Failed to read datafile"
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
374
431
|
const flagDefinition = datafile.definitions[flagKey];
|
|
375
432
|
if (flagDefinition === void 0) {
|
|
433
|
+
if (datafile.projectId) {
|
|
434
|
+
internalReportValue(flagKey, defaultValue, {
|
|
435
|
+
originProjectId: datafile.projectId,
|
|
436
|
+
originProvider: "vercel",
|
|
437
|
+
reason: "error" /* ERROR */
|
|
438
|
+
});
|
|
439
|
+
}
|
|
376
440
|
return {
|
|
377
441
|
value: defaultValue,
|
|
378
442
|
reason: "error" /* ERROR */,
|
|
379
443
|
errorCode: "FLAG_NOT_FOUND" /* FLAG_NOT_FOUND */,
|
|
380
|
-
errorMessage:
|
|
444
|
+
errorMessage: `@vercel/flags-core: Definition not found for flag "${flagKey}"`,
|
|
381
445
|
metrics: {
|
|
382
446
|
evaluationMs: 0,
|
|
383
447
|
readMs: datafile.metrics.readMs,
|
|
384
448
|
source: datafile.metrics.source,
|
|
385
449
|
cacheStatus: datafile.metrics.cacheStatus,
|
|
386
|
-
connectionState: datafile.metrics.connectionState
|
|
450
|
+
connectionState: datafile.metrics.connectionState,
|
|
451
|
+
mode: datafile.metrics.mode
|
|
387
452
|
}
|
|
388
453
|
};
|
|
389
454
|
}
|
|
@@ -410,7 +475,8 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
|
|
|
410
475
|
readMs: datafile.metrics.readMs,
|
|
411
476
|
source: datafile.metrics.source,
|
|
412
477
|
cacheStatus: datafile.metrics.cacheStatus,
|
|
413
|
-
connectionState: datafile.metrics.connectionState
|
|
478
|
+
connectionState: datafile.metrics.connectionState,
|
|
479
|
+
mode: datafile.metrics.mode
|
|
414
480
|
}
|
|
415
481
|
});
|
|
416
482
|
}
|
|
@@ -428,18 +494,22 @@ async function performInitialize(instance, initFn) {
|
|
|
428
494
|
}
|
|
429
495
|
function createCreateRawClient(fns) {
|
|
430
496
|
return function createRawClient({
|
|
431
|
-
|
|
497
|
+
controller,
|
|
432
498
|
origin
|
|
433
499
|
}) {
|
|
434
500
|
const id = idCount++;
|
|
435
|
-
|
|
501
|
+
controllerInstanceMap.set(id, {
|
|
502
|
+
controller,
|
|
503
|
+
initialized: false,
|
|
504
|
+
initPromise: null
|
|
505
|
+
});
|
|
436
506
|
const api = {
|
|
437
507
|
origin,
|
|
438
508
|
initialize: async () => {
|
|
439
|
-
let instance =
|
|
509
|
+
let instance = controllerInstanceMap.get(id);
|
|
440
510
|
if (!instance) {
|
|
441
|
-
instance = {
|
|
442
|
-
|
|
511
|
+
instance = { controller, initialized: false, initPromise: null };
|
|
512
|
+
controllerInstanceMap.set(id, instance);
|
|
443
513
|
}
|
|
444
514
|
if (instance.initialized) return;
|
|
445
515
|
if (!instance.initPromise) {
|
|
@@ -452,15 +522,29 @@ function createCreateRawClient(fns) {
|
|
|
452
522
|
},
|
|
453
523
|
shutdown: async () => {
|
|
454
524
|
await fns.shutdown(id);
|
|
455
|
-
|
|
525
|
+
controllerInstanceMap.delete(id);
|
|
526
|
+
},
|
|
527
|
+
getDatafile: async () => {
|
|
528
|
+
const instance = controllerInstanceMap.get(id);
|
|
529
|
+
if (instance?.initPromise) {
|
|
530
|
+
try {
|
|
531
|
+
await instance.initPromise;
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return fns.getDatafile(id);
|
|
456
536
|
},
|
|
457
|
-
getDatafile: () => fns.getDatafile(id),
|
|
458
537
|
getFallbackDatafile: () => {
|
|
459
538
|
return fns.getFallbackDatafile(id);
|
|
460
539
|
},
|
|
461
540
|
evaluate: async (flagKey, defaultValue, entities) => {
|
|
462
|
-
const instance =
|
|
463
|
-
if (!instance?.initialized)
|
|
541
|
+
const instance = controllerInstanceMap.get(id);
|
|
542
|
+
if (!instance?.initialized) {
|
|
543
|
+
try {
|
|
544
|
+
await api.initialize();
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
464
548
|
return fns.evaluate(id, flagKey, defaultValue, entities);
|
|
465
549
|
}
|
|
466
550
|
};
|
|
@@ -468,20 +552,6 @@ function createCreateRawClient(fns) {
|
|
|
468
552
|
};
|
|
469
553
|
}
|
|
470
554
|
|
|
471
|
-
// src/errors.ts
|
|
472
|
-
var FallbackNotFoundError = class extends Error {
|
|
473
|
-
constructor() {
|
|
474
|
-
super("@vercel/flags-core: Bundled definitions file not found.");
|
|
475
|
-
this.name = "FallbackNotFoundError";
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
var FallbackEntryNotFoundError = class extends Error {
|
|
479
|
-
constructor() {
|
|
480
|
-
super("@vercel/flags-core: No bundled definitions found for SDK key.");
|
|
481
|
-
this.name = "FallbackEntryNotFoundError";
|
|
482
|
-
}
|
|
483
|
-
};
|
|
484
|
-
|
|
485
555
|
// src/utils/read-bundled-definitions.ts
|
|
486
556
|
var sdkKeyHashCache = /* @__PURE__ */ new Map();
|
|
487
557
|
async function computeHash(sdkKey) {
|
|
@@ -526,11 +596,6 @@ async function readBundledDefinitions(sdkKey) {
|
|
|
526
596
|
return { definitions: null, state: "missing-entry" };
|
|
527
597
|
}
|
|
528
598
|
|
|
529
|
-
// src/utils/sleep.ts
|
|
530
|
-
function sleep(ms) {
|
|
531
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
532
|
-
}
|
|
533
|
-
|
|
534
599
|
// src/utils/usage-tracker.ts
|
|
535
600
|
import { waitUntil } from "@vercel/functions";
|
|
536
601
|
var RESOLVED_VOID = Promise.resolve();
|
|
@@ -539,9 +604,9 @@ var debugLog = (...args) => {
|
|
|
539
604
|
if (!isDebugMode) return;
|
|
540
605
|
console.log(...args);
|
|
541
606
|
};
|
|
607
|
+
var MAX_RETRIES = 3;
|
|
542
608
|
var MAX_BATCH_SIZE = 50;
|
|
543
609
|
var MAX_BATCH_WAIT_MS = 5e3;
|
|
544
|
-
var trackedRequests = /* @__PURE__ */ new WeakSet();
|
|
545
610
|
var SYMBOL_FOR_REQ_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context");
|
|
546
611
|
var fromSymbol = globalThis;
|
|
547
612
|
function getRequestContext() {
|
|
@@ -559,24 +624,30 @@ function getRequestContext() {
|
|
|
559
624
|
}
|
|
560
625
|
}
|
|
561
626
|
var UsageTracker = class {
|
|
562
|
-
|
|
563
|
-
|
|
627
|
+
flushCounter = 0;
|
|
628
|
+
options;
|
|
629
|
+
trackedRequests = /* @__PURE__ */ new WeakSet();
|
|
564
630
|
batcher = {
|
|
565
631
|
events: [],
|
|
566
632
|
resolveWait: null,
|
|
567
633
|
pending: null
|
|
568
634
|
};
|
|
569
635
|
constructor(options) {
|
|
570
|
-
this.
|
|
571
|
-
this.host = options.host;
|
|
636
|
+
this.options = options;
|
|
572
637
|
}
|
|
573
638
|
/**
|
|
574
639
|
* Triggers an immediate flush of any pending events.
|
|
575
640
|
* Returns a promise that resolves when the flush completes.
|
|
576
641
|
*/
|
|
577
642
|
flush() {
|
|
578
|
-
this.batcher.
|
|
579
|
-
|
|
643
|
+
if (this.batcher.pending) {
|
|
644
|
+
this.batcher.resolveWait?.();
|
|
645
|
+
return this.batcher.pending;
|
|
646
|
+
}
|
|
647
|
+
if (this.batcher.events.length > 0) {
|
|
648
|
+
return this.flushEvents();
|
|
649
|
+
}
|
|
650
|
+
return RESOLVED_VOID;
|
|
580
651
|
}
|
|
581
652
|
/**
|
|
582
653
|
* Tracks a config read event. Deduplicates by request context.
|
|
@@ -585,8 +656,8 @@ var UsageTracker = class {
|
|
|
585
656
|
try {
|
|
586
657
|
const { ctx, headers } = getRequestContext();
|
|
587
658
|
if (ctx) {
|
|
588
|
-
if (trackedRequests.has(ctx)) return;
|
|
589
|
-
trackedRequests.add(ctx);
|
|
659
|
+
if (this.trackedRequests.has(ctx)) return;
|
|
660
|
+
this.trackedRequests.add(ctx);
|
|
590
661
|
}
|
|
591
662
|
const event = {
|
|
592
663
|
type: "FLAGS_CONFIG_READ",
|
|
@@ -605,6 +676,9 @@ var UsageTracker = class {
|
|
|
605
676
|
if (options.cacheStatus !== void 0) {
|
|
606
677
|
event.payload.cacheStatus = options.cacheStatus;
|
|
607
678
|
}
|
|
679
|
+
if (options.cacheAction !== void 0) {
|
|
680
|
+
event.payload.cacheAction = options.cacheAction;
|
|
681
|
+
}
|
|
608
682
|
if (options.cacheIsFirstRead !== void 0) {
|
|
609
683
|
event.payload.cacheIsFirstRead = options.cacheIsFirstRead;
|
|
610
684
|
}
|
|
@@ -617,6 +691,16 @@ var UsageTracker = class {
|
|
|
617
691
|
if (options.configUpdatedAt !== void 0) {
|
|
618
692
|
event.payload.configUpdatedAt = options.configUpdatedAt;
|
|
619
693
|
}
|
|
694
|
+
if (options.mode !== void 0) {
|
|
695
|
+
event.payload.mode = options.mode;
|
|
696
|
+
}
|
|
697
|
+
if (options.revision !== void 0) {
|
|
698
|
+
event.payload.revision = String(options.revision);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const environment = process.env.VERCEL_ENV || process.env.NODE_ENV || void 0;
|
|
702
|
+
if (environment) {
|
|
703
|
+
event.payload.environment = environment;
|
|
620
704
|
}
|
|
621
705
|
this.batcher.events.push(event);
|
|
622
706
|
this.scheduleFlush();
|
|
@@ -637,7 +721,10 @@ var UsageTracker = class {
|
|
|
637
721
|
if (timeout) clearTimeout(timeout);
|
|
638
722
|
await this.flushEvents();
|
|
639
723
|
})();
|
|
640
|
-
|
|
724
|
+
try {
|
|
725
|
+
waitUntil(pending);
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
641
728
|
this.batcher.pending = pending;
|
|
642
729
|
}
|
|
643
730
|
if (this.batcher.events.length >= MAX_BATCH_SIZE) {
|
|
@@ -648,41 +735,301 @@ var UsageTracker = class {
|
|
|
648
735
|
if (this.batcher.events.length === 0) return;
|
|
649
736
|
const eventsToSend = this.batcher.events;
|
|
650
737
|
this.batcher.events = [];
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
738
|
+
const flushId = ++this.flushCounter;
|
|
739
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
740
|
+
try {
|
|
741
|
+
const response = await this.options.fetch(
|
|
742
|
+
`${this.options.host}/v1/ingest`,
|
|
743
|
+
{
|
|
744
|
+
method: "POST",
|
|
745
|
+
headers: {
|
|
746
|
+
"Content-Type": "application/json",
|
|
747
|
+
Authorization: `Bearer ${this.options.sdkKey}`,
|
|
748
|
+
"User-Agent": `VercelFlagsCore/${version}`,
|
|
749
|
+
...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
|
|
750
|
+
},
|
|
751
|
+
body: JSON.stringify(eventsToSend)
|
|
752
|
+
}
|
|
753
|
+
);
|
|
666
754
|
debugLog(
|
|
667
|
-
|
|
668
|
-
|
|
755
|
+
`@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
|
|
756
|
+
);
|
|
757
|
+
if (response.ok) {
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
throw new Error(
|
|
761
|
+
`Ingest endpoint responded with status ${response.status} for ${eventsToSend.length} events on request ${response.headers.get("x-vercel-id")}.
|
|
762
|
+
Response body: ${await response.text().catch(() => null)}`
|
|
763
|
+
);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
console.error(
|
|
766
|
+
`@vercel/flags-core: Error sending events (attempt=${attempt}/${MAX_RETRIES} flushId=${flushId}):`,
|
|
767
|
+
error
|
|
768
|
+
);
|
|
769
|
+
if (attempt < MAX_RETRIES) {
|
|
770
|
+
await new Promise((res) => setTimeout(res, attempt * 100));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// src/errors.ts
|
|
778
|
+
var FallbackNotFoundError = class extends Error {
|
|
779
|
+
constructor() {
|
|
780
|
+
super("@vercel/flags-core: Bundled definitions file not found.");
|
|
781
|
+
this.name = "FallbackNotFoundError";
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
var FallbackEntryNotFoundError = class extends Error {
|
|
785
|
+
constructor() {
|
|
786
|
+
super("@vercel/flags-core: No bundled definitions found for SDK key.");
|
|
787
|
+
this.name = "FallbackEntryNotFoundError";
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// src/controller/bundled-source.ts
|
|
792
|
+
var BundledSource = class {
|
|
793
|
+
promise;
|
|
794
|
+
options;
|
|
795
|
+
constructor(options) {
|
|
796
|
+
this.options = options;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Load bundled definitions.
|
|
800
|
+
* Throws if bundled definitions are not available.
|
|
801
|
+
*/
|
|
802
|
+
async load() {
|
|
803
|
+
const result = await this.getResult();
|
|
804
|
+
if (result.state === "ok" && result.definitions) {
|
|
805
|
+
return result.definitions;
|
|
806
|
+
}
|
|
807
|
+
throw new Error(
|
|
808
|
+
"@vercel/flags-core: No flag definitions available. Bundled definitions not found."
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Get the raw BundledDefinitions (for getFallbackDatafile).
|
|
813
|
+
* Throws typed errors if not available.
|
|
814
|
+
*/
|
|
815
|
+
async getRaw() {
|
|
816
|
+
const result = await this.getResult();
|
|
817
|
+
switch (result.state) {
|
|
818
|
+
case "ok":
|
|
819
|
+
return result.definitions;
|
|
820
|
+
case "missing-file":
|
|
821
|
+
throw new FallbackNotFoundError();
|
|
822
|
+
case "missing-entry":
|
|
823
|
+
throw new FallbackEntryNotFoundError();
|
|
824
|
+
case "unexpected-error":
|
|
825
|
+
throw new Error(
|
|
826
|
+
"@vercel/flags-core: Failed to read bundled definitions: " + String(result.error)
|
|
669
827
|
);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Check if bundled definitions loaded successfully (without throwing).
|
|
832
|
+
*/
|
|
833
|
+
async tryLoad() {
|
|
834
|
+
const result = await this.getResult();
|
|
835
|
+
if (result?.state === "ok" && result.definitions) {
|
|
836
|
+
return result.definitions;
|
|
837
|
+
}
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
getResult() {
|
|
841
|
+
if (!this.promise) {
|
|
842
|
+
this.promise = this.options.readBundledDefinitions(this.options.sdkKey);
|
|
843
|
+
}
|
|
844
|
+
return this.promise;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// src/controller/fetch-datafile.ts
|
|
849
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
850
|
+
async function fetchDatafile(options) {
|
|
851
|
+
const controller = new AbortController();
|
|
852
|
+
const timeoutId = setTimeout(
|
|
853
|
+
() => controller.abort(),
|
|
854
|
+
DEFAULT_FETCH_TIMEOUT_MS
|
|
855
|
+
);
|
|
856
|
+
const onExternalAbort = () => controller.abort();
|
|
857
|
+
if (options.signal) {
|
|
858
|
+
if (options.signal.aborted) {
|
|
859
|
+
clearTimeout(timeoutId);
|
|
860
|
+
throw new Error("Fetch aborted");
|
|
861
|
+
}
|
|
862
|
+
options.signal.addEventListener("abort", onExternalAbort, { once: true });
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
const res = await options.fetch(`${options.host}/v1/datafile`, {
|
|
866
|
+
headers: {
|
|
867
|
+
Authorization: `Bearer ${options.sdkKey}`,
|
|
868
|
+
"User-Agent": `VercelFlagsCore/${version}`
|
|
869
|
+
},
|
|
870
|
+
signal: controller.signal
|
|
871
|
+
});
|
|
872
|
+
clearTimeout(timeoutId);
|
|
873
|
+
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
874
|
+
if (!res.ok) {
|
|
875
|
+
throw new Error(`Failed to fetch data: ${res.statusText}`);
|
|
876
|
+
}
|
|
877
|
+
return res.json();
|
|
878
|
+
} catch (error) {
|
|
879
|
+
clearTimeout(timeoutId);
|
|
880
|
+
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
881
|
+
throw error instanceof Error ? error : new Error("Unknown fetch error");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/controller/normalized-options.ts
|
|
886
|
+
var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
|
|
887
|
+
var DEFAULT_POLLING_INTERVAL_MS = 3e4;
|
|
888
|
+
var MIN_POLLING_INTERVAL_MS = 3e4;
|
|
889
|
+
var DEFAULT_POLLING_INIT_TIMEOUT_MS = 3e3;
|
|
890
|
+
function normalizeOptions(options) {
|
|
891
|
+
const autoDetectedBuildStep = process.env.CI === "1" || process.env.NEXT_PHASE === "phase-production-build";
|
|
892
|
+
const buildStep = options.buildStep ?? autoDetectedBuildStep;
|
|
893
|
+
let stream;
|
|
894
|
+
if (options.stream === void 0 || options.stream === true) {
|
|
895
|
+
stream = { enabled: true, initTimeoutMs: DEFAULT_STREAM_INIT_TIMEOUT_MS };
|
|
896
|
+
} else if (options.stream === false) {
|
|
897
|
+
stream = { enabled: false, initTimeoutMs: 0 };
|
|
898
|
+
} else {
|
|
899
|
+
stream = { enabled: true, initTimeoutMs: options.stream.initTimeoutMs };
|
|
900
|
+
}
|
|
901
|
+
let polling;
|
|
902
|
+
if (options.polling === void 0 || options.polling === true) {
|
|
903
|
+
polling = {
|
|
904
|
+
enabled: true,
|
|
905
|
+
intervalMs: DEFAULT_POLLING_INTERVAL_MS,
|
|
906
|
+
initTimeoutMs: DEFAULT_POLLING_INIT_TIMEOUT_MS
|
|
907
|
+
};
|
|
908
|
+
} else if (options.polling === false) {
|
|
909
|
+
polling = { enabled: false, intervalMs: 0, initTimeoutMs: 0 };
|
|
910
|
+
} else {
|
|
911
|
+
if (options.polling.intervalMs < MIN_POLLING_INTERVAL_MS) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`@vercel/flags-core: Polling interval must be at least ${MIN_POLLING_INTERVAL_MS}ms, got ${options.polling.intervalMs}ms.`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
polling = {
|
|
917
|
+
enabled: true,
|
|
918
|
+
intervalMs: options.polling.intervalMs,
|
|
919
|
+
initTimeoutMs: options.polling.initTimeoutMs
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
sdkKey: options.sdkKey,
|
|
924
|
+
datafile: options.datafile,
|
|
925
|
+
stream,
|
|
926
|
+
polling,
|
|
927
|
+
buildStep,
|
|
928
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
929
|
+
host: "https://flags.vercel.com"
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/controller/typed-emitter.ts
|
|
934
|
+
var TypedEmitter = class {
|
|
935
|
+
handlers = /* @__PURE__ */ new Map();
|
|
936
|
+
on(event, handler) {
|
|
937
|
+
let set = this.handlers.get(event);
|
|
938
|
+
if (!set) {
|
|
939
|
+
set = /* @__PURE__ */ new Set();
|
|
940
|
+
this.handlers.set(event, set);
|
|
941
|
+
}
|
|
942
|
+
set.add(handler);
|
|
943
|
+
}
|
|
944
|
+
off(event, handler) {
|
|
945
|
+
this.handlers.get(event)?.delete(handler);
|
|
946
|
+
}
|
|
947
|
+
emit(event, ...args) {
|
|
948
|
+
const set = this.handlers.get(event);
|
|
949
|
+
if (set) {
|
|
950
|
+
for (const handler of set) {
|
|
951
|
+
handler(...args);
|
|
670
952
|
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// src/controller/polling-source.ts
|
|
958
|
+
var PollingSource = class extends TypedEmitter {
|
|
959
|
+
config;
|
|
960
|
+
intervalId;
|
|
961
|
+
abortController;
|
|
962
|
+
constructor(config) {
|
|
963
|
+
super();
|
|
964
|
+
this.config = config;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Perform a single poll request.
|
|
968
|
+
* Emits 'data' on success, 'error' on failure.
|
|
969
|
+
*/
|
|
970
|
+
async poll() {
|
|
971
|
+
if (this.abortController?.signal.aborted) return;
|
|
972
|
+
try {
|
|
973
|
+
const data = await fetchDatafile({
|
|
974
|
+
...this.config,
|
|
975
|
+
signal: this.abortController?.signal
|
|
976
|
+
});
|
|
977
|
+
this.emit("data", data);
|
|
671
978
|
} catch (error) {
|
|
672
|
-
|
|
979
|
+
const err = error instanceof Error ? error : new Error("Unknown poll error");
|
|
980
|
+
this.emit("error", err);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Start interval-based polling.
|
|
985
|
+
* Polls at the configured interval. Does not perform an initial poll —
|
|
986
|
+
* callers should call poll() first if an immediate poll is needed.
|
|
987
|
+
*/
|
|
988
|
+
startInterval() {
|
|
989
|
+
if (this.intervalId) return;
|
|
990
|
+
this.abortController = new AbortController();
|
|
991
|
+
this.intervalId = setInterval(
|
|
992
|
+
() => void this.poll(),
|
|
993
|
+
this.config.polling.intervalMs
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Stop interval-based polling.
|
|
998
|
+
*/
|
|
999
|
+
stop() {
|
|
1000
|
+
if (this.intervalId) {
|
|
1001
|
+
clearInterval(this.intervalId);
|
|
1002
|
+
this.intervalId = void 0;
|
|
673
1003
|
}
|
|
1004
|
+
this.abortController?.abort();
|
|
1005
|
+
this.abortController = void 0;
|
|
674
1006
|
}
|
|
675
1007
|
};
|
|
676
1008
|
|
|
677
|
-
// src/
|
|
1009
|
+
// src/utils/sleep.ts
|
|
1010
|
+
function sleep(ms) {
|
|
1011
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/controller/stream-connection.ts
|
|
678
1015
|
var MAX_RETRY_COUNT = 15;
|
|
679
|
-
var
|
|
680
|
-
var
|
|
1016
|
+
var BASE_RETRY_DELAY_MS = 1e3;
|
|
1017
|
+
var MAX_RETRY_DELAY_MS = 6e4;
|
|
1018
|
+
var PING_TIMEOUT_MS = 9e4;
|
|
681
1019
|
function backoff(retryCount) {
|
|
682
1020
|
if (retryCount === 1) return 0;
|
|
683
|
-
const delay = Math.min(
|
|
1021
|
+
const delay = Math.min(
|
|
1022
|
+
BASE_RETRY_DELAY_MS * 2 ** (retryCount - 2),
|
|
1023
|
+
MAX_RETRY_DELAY_MS
|
|
1024
|
+
);
|
|
684
1025
|
return delay + Math.random() * 1e3;
|
|
685
1026
|
}
|
|
1027
|
+
var UnauthorizedError = class extends Error {
|
|
1028
|
+
constructor() {
|
|
1029
|
+
super("stream: unauthorized (401)");
|
|
1030
|
+
this.name = "UnauthorizedError";
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
686
1033
|
async function connectStream(config, callbacks) {
|
|
687
1034
|
const {
|
|
688
1035
|
host,
|
|
@@ -690,8 +1037,9 @@ async function connectStream(config, callbacks) {
|
|
|
690
1037
|
abortController,
|
|
691
1038
|
fetch: fetchFn = globalThis.fetch
|
|
692
1039
|
} = config;
|
|
693
|
-
const {
|
|
1040
|
+
const { onDatafile, onPrimed, onDisconnect } = callbacks;
|
|
694
1041
|
let retryCount = 0;
|
|
1042
|
+
let lastAttemptTime = 0;
|
|
695
1043
|
let resolveInit;
|
|
696
1044
|
let rejectInit;
|
|
697
1045
|
const initPromise = new Promise((resolve, reject) => {
|
|
@@ -703,183 +1051,267 @@ async function connectStream(config, callbacks) {
|
|
|
703
1051
|
while (!abortController.signal.aborted) {
|
|
704
1052
|
if (retryCount > MAX_RETRY_COUNT) {
|
|
705
1053
|
console.error("@vercel/flags-core: Max retry count exceeded");
|
|
1054
|
+
if (!initialDataReceived) {
|
|
1055
|
+
rejectInit(
|
|
1056
|
+
new Error("stream: max retry count exceeded before receiving data")
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
706
1059
|
abortController.abort();
|
|
707
1060
|
break;
|
|
708
1061
|
}
|
|
1062
|
+
const connectionAbort = new AbortController();
|
|
1063
|
+
const onMainAbort = () => connectionAbort.abort();
|
|
1064
|
+
abortController.signal.addEventListener("abort", onMainAbort, {
|
|
1065
|
+
once: true
|
|
1066
|
+
});
|
|
1067
|
+
let pingTimeoutId;
|
|
1068
|
+
let responseBody;
|
|
1069
|
+
const resetPingTimeout = () => {
|
|
1070
|
+
if (pingTimeoutId !== void 0) clearTimeout(pingTimeoutId);
|
|
1071
|
+
if (!initialDataReceived) return;
|
|
1072
|
+
pingTimeoutId = setTimeout(() => {
|
|
1073
|
+
responseBody?.cancel().catch(() => {
|
|
1074
|
+
});
|
|
1075
|
+
connectionAbort.abort();
|
|
1076
|
+
}, PING_TIMEOUT_MS);
|
|
1077
|
+
};
|
|
709
1078
|
try {
|
|
1079
|
+
lastAttemptTime = Date.now();
|
|
1080
|
+
const headers = {
|
|
1081
|
+
Authorization: `Bearer ${sdkKey}`,
|
|
1082
|
+
"User-Agent": `VercelFlagsCore/${version}`,
|
|
1083
|
+
"X-Retry-Attempt": String(retryCount)
|
|
1084
|
+
};
|
|
1085
|
+
const revision = config.revision?.();
|
|
1086
|
+
if (revision !== void 0) {
|
|
1087
|
+
headers["X-Revision"] = String(revision);
|
|
1088
|
+
}
|
|
710
1089
|
const response = await fetchFn(`${host}/v1/stream`, {
|
|
711
|
-
headers
|
|
712
|
-
|
|
713
|
-
"User-Agent": `VercelFlagsCore/${version}`,
|
|
714
|
-
"X-Retry-Attempt": String(retryCount)
|
|
715
|
-
},
|
|
716
|
-
signal: abortController.signal
|
|
1090
|
+
headers,
|
|
1091
|
+
signal: connectionAbort.signal
|
|
717
1092
|
});
|
|
718
1093
|
if (!response.ok) {
|
|
719
1094
|
if (response.status === 401) {
|
|
1095
|
+
if (!initialDataReceived) {
|
|
1096
|
+
rejectInit(new UnauthorizedError());
|
|
1097
|
+
}
|
|
720
1098
|
abortController.abort();
|
|
1099
|
+
break;
|
|
721
1100
|
}
|
|
722
1101
|
throw new Error(`stream was not ok: ${response.status}`);
|
|
723
1102
|
}
|
|
724
1103
|
if (!response.body) {
|
|
725
1104
|
throw new Error("stream body was not present");
|
|
726
1105
|
}
|
|
1106
|
+
responseBody = response.body;
|
|
1107
|
+
const reader = response.body.getReader();
|
|
727
1108
|
const decoder = new TextDecoder();
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1109
|
+
const bufferChunks = [];
|
|
1110
|
+
const onConnectionAbort = () => {
|
|
1111
|
+
reader.cancel().catch(() => {
|
|
1112
|
+
});
|
|
1113
|
+
};
|
|
1114
|
+
connectionAbort.signal.addEventListener("abort", onConnectionAbort, {
|
|
1115
|
+
once: true
|
|
1116
|
+
});
|
|
1117
|
+
try {
|
|
1118
|
+
while (true) {
|
|
1119
|
+
const { done, value: chunk } = await reader.read();
|
|
1120
|
+
if (done || abortController.signal.aborted) break;
|
|
1121
|
+
bufferChunks.push(decoder.decode(chunk, { stream: true }));
|
|
1122
|
+
const combined = bufferChunks.join("");
|
|
1123
|
+
bufferChunks.length = 0;
|
|
1124
|
+
const lines = combined.split("\n");
|
|
1125
|
+
bufferChunks.push(lines.pop());
|
|
1126
|
+
for (const line of lines) {
|
|
1127
|
+
if (line === "") continue;
|
|
1128
|
+
let message;
|
|
1129
|
+
try {
|
|
1130
|
+
message = JSON.parse(line);
|
|
1131
|
+
} catch {
|
|
1132
|
+
console.warn(
|
|
1133
|
+
"@vercel/flags-core: Failed to parse stream message, skipping"
|
|
1134
|
+
);
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (message.type === "datafile") {
|
|
1138
|
+
onDatafile(message.data);
|
|
1139
|
+
retryCount = 0;
|
|
1140
|
+
if (!initialDataReceived) {
|
|
1141
|
+
initialDataReceived = true;
|
|
1142
|
+
resolveInit();
|
|
1143
|
+
}
|
|
1144
|
+
resetPingTimeout();
|
|
1145
|
+
}
|
|
1146
|
+
if (message.type === "primed") {
|
|
1147
|
+
onPrimed?.(message);
|
|
1148
|
+
retryCount = 0;
|
|
1149
|
+
if (!initialDataReceived) {
|
|
1150
|
+
initialDataReceived = true;
|
|
1151
|
+
resolveInit();
|
|
1152
|
+
}
|
|
1153
|
+
resetPingTimeout();
|
|
1154
|
+
}
|
|
1155
|
+
if (message.type === "ping" && initialDataReceived) {
|
|
1156
|
+
retryCount = 0;
|
|
1157
|
+
resetPingTimeout();
|
|
751
1158
|
}
|
|
752
1159
|
}
|
|
753
1160
|
}
|
|
1161
|
+
} finally {
|
|
1162
|
+
connectionAbort.signal.removeEventListener(
|
|
1163
|
+
"abort",
|
|
1164
|
+
onConnectionAbort
|
|
1165
|
+
);
|
|
754
1166
|
}
|
|
1167
|
+
clearTimeout(pingTimeoutId);
|
|
1168
|
+
abortController.signal.removeEventListener("abort", onMainAbort);
|
|
755
1169
|
if (!abortController.signal.aborted) {
|
|
756
1170
|
onDisconnect?.();
|
|
757
1171
|
retryCount++;
|
|
758
|
-
|
|
1172
|
+
const elapsed = Date.now() - lastAttemptTime;
|
|
1173
|
+
const minGap = Math.max(0, BASE_RETRY_DELAY_MS - elapsed);
|
|
1174
|
+
await sleep(Math.max(backoff(retryCount), minGap));
|
|
759
1175
|
continue;
|
|
760
1176
|
}
|
|
761
1177
|
} catch (error) {
|
|
1178
|
+
clearTimeout(pingTimeoutId);
|
|
1179
|
+
abortController.signal.removeEventListener("abort", onMainAbort);
|
|
762
1180
|
if (abortController.signal.aborted) {
|
|
763
1181
|
break;
|
|
764
1182
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if (!initialDataReceived) {
|
|
768
|
-
rejectInit(error);
|
|
769
|
-
break;
|
|
1183
|
+
if (!connectionAbort.signal.aborted) {
|
|
1184
|
+
console.error("@vercel/flags-core: Stream error", error);
|
|
770
1185
|
}
|
|
1186
|
+
onDisconnect?.();
|
|
771
1187
|
retryCount++;
|
|
772
|
-
|
|
1188
|
+
const elapsed = Date.now() - lastAttemptTime;
|
|
1189
|
+
const minGap = Math.max(0, BASE_RETRY_DELAY_MS - elapsed);
|
|
1190
|
+
await sleep(Math.max(backoff(retryCount), minGap));
|
|
773
1191
|
}
|
|
774
1192
|
}
|
|
1193
|
+
if (!initialDataReceived) {
|
|
1194
|
+
rejectInit(new Error("stream: aborted before receiving data"));
|
|
1195
|
+
}
|
|
775
1196
|
})();
|
|
776
1197
|
return initPromise;
|
|
777
1198
|
}
|
|
778
1199
|
|
|
779
|
-
// src/
|
|
780
|
-
var
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const buildStep = options.buildStep ?? autoDetectedBuildStep;
|
|
790
|
-
let stream;
|
|
791
|
-
if (options.stream === void 0 || options.stream === true) {
|
|
792
|
-
stream = { enabled: true, initTimeoutMs: DEFAULT_STREAM_INIT_TIMEOUT_MS };
|
|
793
|
-
} else if (options.stream === false) {
|
|
794
|
-
stream = { enabled: false, initTimeoutMs: 0 };
|
|
795
|
-
} else {
|
|
796
|
-
stream = { enabled: true, initTimeoutMs: options.stream.initTimeoutMs };
|
|
797
|
-
}
|
|
798
|
-
let polling;
|
|
799
|
-
if (options.polling === void 0 || options.polling === true) {
|
|
800
|
-
polling = {
|
|
801
|
-
enabled: true,
|
|
802
|
-
intervalMs: DEFAULT_POLLING_INTERVAL_MS,
|
|
803
|
-
initTimeoutMs: DEFAULT_POLLING_INIT_TIMEOUT_MS
|
|
804
|
-
};
|
|
805
|
-
} else if (options.polling === false) {
|
|
806
|
-
polling = { enabled: false, intervalMs: 0, initTimeoutMs: 0 };
|
|
807
|
-
} else {
|
|
808
|
-
polling = {
|
|
809
|
-
enabled: true,
|
|
810
|
-
intervalMs: options.polling.intervalMs,
|
|
811
|
-
initTimeoutMs: options.polling.initTimeoutMs
|
|
812
|
-
};
|
|
1200
|
+
// src/controller/stream-source.ts
|
|
1201
|
+
var StreamSource = class extends TypedEmitter {
|
|
1202
|
+
options;
|
|
1203
|
+
revision;
|
|
1204
|
+
abortController;
|
|
1205
|
+
promise;
|
|
1206
|
+
constructor(options, revision) {
|
|
1207
|
+
super();
|
|
1208
|
+
this.options = options;
|
|
1209
|
+
this.revision = revision;
|
|
813
1210
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1211
|
+
/**
|
|
1212
|
+
* Start the stream connection.
|
|
1213
|
+
* Returns a promise that resolves when the first datafile or primed message arrives.
|
|
1214
|
+
* If already started, returns the existing promise.
|
|
1215
|
+
*/
|
|
1216
|
+
start() {
|
|
1217
|
+
if (this.promise) return this.promise;
|
|
1218
|
+
const abortController = new AbortController();
|
|
1219
|
+
this.abortController = abortController;
|
|
1220
|
+
abortController.signal.addEventListener(
|
|
1221
|
+
"abort",
|
|
1222
|
+
() => {
|
|
1223
|
+
if (this.abortController === abortController) {
|
|
1224
|
+
this.promise = void 0;
|
|
1225
|
+
this.abortController = void 0;
|
|
1226
|
+
}
|
|
1227
|
+
},
|
|
1228
|
+
{ once: true }
|
|
830
1229
|
);
|
|
831
|
-
let shouldRetry = true;
|
|
832
1230
|
try {
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1231
|
+
const promise = connectStream(
|
|
1232
|
+
{
|
|
1233
|
+
host: this.options.host,
|
|
1234
|
+
sdkKey: this.options.sdkKey,
|
|
1235
|
+
abortController,
|
|
1236
|
+
fetch: this.options.fetch,
|
|
1237
|
+
revision: this.revision
|
|
837
1238
|
},
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1239
|
+
{
|
|
1240
|
+
onDatafile: (newData) => {
|
|
1241
|
+
this.emit("data", newData);
|
|
1242
|
+
this.emit("connected");
|
|
1243
|
+
},
|
|
1244
|
+
onPrimed: (message) => {
|
|
1245
|
+
this.emit("primed", message);
|
|
1246
|
+
this.emit("connected");
|
|
1247
|
+
},
|
|
1248
|
+
onDisconnect: () => {
|
|
1249
|
+
this.emit("disconnected");
|
|
1250
|
+
}
|
|
844
1251
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
return
|
|
1252
|
+
);
|
|
1253
|
+
this.promise = promise;
|
|
1254
|
+
return promise;
|
|
848
1255
|
} catch (error) {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (attempt < MAX_FETCH_RETRIES - 1) {
|
|
853
|
-
const delay = FETCH_RETRY_BASE_DELAY_MS * 2 ** attempt + Math.random() * 500;
|
|
854
|
-
await sleep(delay);
|
|
855
|
-
}
|
|
1256
|
+
this.promise = void 0;
|
|
1257
|
+
this.abortController = void 0;
|
|
1258
|
+
throw error;
|
|
856
1259
|
}
|
|
857
1260
|
}
|
|
858
|
-
|
|
1261
|
+
/**
|
|
1262
|
+
* Stop the stream connection.
|
|
1263
|
+
*/
|
|
1264
|
+
stop() {
|
|
1265
|
+
this.abortController?.abort();
|
|
1266
|
+
this.abortController = void 0;
|
|
1267
|
+
this.promise = void 0;
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
// src/controller/tagged-data.ts
|
|
1272
|
+
function tagData(data, origin) {
|
|
1273
|
+
return Object.assign(data, { _origin: origin });
|
|
1274
|
+
}
|
|
1275
|
+
function originToMetricsSource(origin) {
|
|
1276
|
+
switch (origin) {
|
|
1277
|
+
case "stream":
|
|
1278
|
+
case "poll":
|
|
1279
|
+
case "provided":
|
|
1280
|
+
return "in-memory";
|
|
1281
|
+
case "fetched":
|
|
1282
|
+
return "remote";
|
|
1283
|
+
case "bundled":
|
|
1284
|
+
return "embedded";
|
|
1285
|
+
}
|
|
859
1286
|
}
|
|
860
|
-
|
|
1287
|
+
|
|
1288
|
+
// src/controller/index.ts
|
|
1289
|
+
function parseConfigUpdatedAt(value) {
|
|
1290
|
+
if (typeof value === "number") return value;
|
|
1291
|
+
if (typeof value === "string") {
|
|
1292
|
+
const parsed = Number(value);
|
|
1293
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1294
|
+
}
|
|
1295
|
+
return void 0;
|
|
1296
|
+
}
|
|
1297
|
+
var Controller = class {
|
|
861
1298
|
options;
|
|
862
|
-
|
|
863
|
-
|
|
1299
|
+
// State machine
|
|
1300
|
+
state = "idle";
|
|
1301
|
+
// Data state — tagged with origin
|
|
864
1302
|
data;
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
isStreamConnected = false;
|
|
870
|
-
hasWarnedAboutStaleData = false;
|
|
871
|
-
// Polling state
|
|
872
|
-
pollingIntervalId;
|
|
873
|
-
pollingAbortController;
|
|
874
|
-
// Initialization state — suppresses onDisconnect from starting polling
|
|
875
|
-
// while initialize() is still running its own fallback chain
|
|
876
|
-
isInitializing = false;
|
|
1303
|
+
// Sources (I/O delegates)
|
|
1304
|
+
streamSource;
|
|
1305
|
+
pollingSource;
|
|
1306
|
+
bundledSource;
|
|
877
1307
|
// Usage tracking
|
|
878
1308
|
usageTracker;
|
|
879
1309
|
isFirstGetData = true;
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1310
|
+
// Build-step deduplication
|
|
1311
|
+
buildDataPromise = null;
|
|
1312
|
+
buildReadTracked = false;
|
|
1313
|
+
// Suppresses usage tracking when the SDK key is unauthorized
|
|
1314
|
+
unauthorized = false;
|
|
883
1315
|
constructor(options) {
|
|
884
1316
|
if (!options.sdkKey || typeof options.sdkKey !== "string" || !options.sdkKey.startsWith("vf_")) {
|
|
885
1317
|
throw new Error(
|
|
@@ -887,16 +1319,88 @@ var FlagNetworkDataSource = class _FlagNetworkDataSource {
|
|
|
887
1319
|
);
|
|
888
1320
|
}
|
|
889
1321
|
this.options = normalizeOptions(options);
|
|
890
|
-
this.
|
|
891
|
-
this.options
|
|
1322
|
+
this.streamSource = new StreamSource(
|
|
1323
|
+
this.options,
|
|
1324
|
+
() => this.data?.revision
|
|
892
1325
|
);
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
this.usageTracker = new UsageTracker({
|
|
1326
|
+
this.pollingSource = new PollingSource(this.options);
|
|
1327
|
+
this.bundledSource = new BundledSource({
|
|
897
1328
|
sdkKey: this.options.sdkKey,
|
|
898
|
-
|
|
1329
|
+
readBundledDefinitions
|
|
899
1330
|
});
|
|
1331
|
+
this.wireSourceEvents();
|
|
1332
|
+
if (this.options.datafile) {
|
|
1333
|
+
this.data = tagData(this.options.datafile, "provided");
|
|
1334
|
+
}
|
|
1335
|
+
this.usageTracker = new UsageTracker(this.options);
|
|
1336
|
+
}
|
|
1337
|
+
// Source event handlers (stored for cleanup)
|
|
1338
|
+
onStreamData = (data) => {
|
|
1339
|
+
if (this.isNewerData(data)) {
|
|
1340
|
+
this.data = tagData(data, "stream");
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
onStreamPrimed = () => {
|
|
1344
|
+
if (this.state === "degraded" || this.state === "initializing:stream") {
|
|
1345
|
+
this.transition("streaming");
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
onStreamConnected = () => {
|
|
1349
|
+
if (this.state === "degraded" || this.state === "initializing:stream") {
|
|
1350
|
+
this.transition("streaming");
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
onStreamDisconnected = () => {
|
|
1354
|
+
if (this.state === "streaming") {
|
|
1355
|
+
this.transition("degraded");
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
onPollData = (data) => {
|
|
1359
|
+
if (this.isNewerData(data)) {
|
|
1360
|
+
this.data = tagData(data, "poll");
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
onPollError = (error) => {
|
|
1364
|
+
console.error("@vercel/flags-core: Poll failed:", error);
|
|
1365
|
+
};
|
|
1366
|
+
// ---------------------------------------------------------------------------
|
|
1367
|
+
// Source event wiring
|
|
1368
|
+
// ---------------------------------------------------------------------------
|
|
1369
|
+
wireSourceEvents() {
|
|
1370
|
+
this.streamSource.on("data", this.onStreamData);
|
|
1371
|
+
this.streamSource.on("primed", this.onStreamPrimed);
|
|
1372
|
+
this.streamSource.on("connected", this.onStreamConnected);
|
|
1373
|
+
this.streamSource.on("disconnected", this.onStreamDisconnected);
|
|
1374
|
+
this.pollingSource.on("data", this.onPollData);
|
|
1375
|
+
this.pollingSource.on("error", this.onPollError);
|
|
1376
|
+
}
|
|
1377
|
+
unwireSourceEvents() {
|
|
1378
|
+
this.streamSource.off("data", this.onStreamData);
|
|
1379
|
+
this.streamSource.off("primed", this.onStreamPrimed);
|
|
1380
|
+
this.streamSource.off("connected", this.onStreamConnected);
|
|
1381
|
+
this.streamSource.off("disconnected", this.onStreamDisconnected);
|
|
1382
|
+
this.pollingSource.off("data", this.onPollData);
|
|
1383
|
+
this.pollingSource.off("error", this.onPollError);
|
|
1384
|
+
}
|
|
1385
|
+
// ---------------------------------------------------------------------------
|
|
1386
|
+
// State machine
|
|
1387
|
+
// ---------------------------------------------------------------------------
|
|
1388
|
+
transition(to) {
|
|
1389
|
+
this.state = to;
|
|
1390
|
+
}
|
|
1391
|
+
get isConnected() {
|
|
1392
|
+
return this.state === "streaming";
|
|
1393
|
+
}
|
|
1394
|
+
get mode() {
|
|
1395
|
+
if (this.options.buildStep) return "build";
|
|
1396
|
+
switch (this.state) {
|
|
1397
|
+
case "streaming":
|
|
1398
|
+
return "streaming";
|
|
1399
|
+
case "polling":
|
|
1400
|
+
return "polling";
|
|
1401
|
+
default:
|
|
1402
|
+
return "offline";
|
|
1403
|
+
}
|
|
900
1404
|
}
|
|
901
1405
|
// ---------------------------------------------------------------------------
|
|
902
1406
|
// Public API (DataSource interface)
|
|
@@ -904,155 +1408,187 @@ var FlagNetworkDataSource = class _FlagNetworkDataSource {
|
|
|
904
1408
|
/**
|
|
905
1409
|
* Initializes the data source.
|
|
906
1410
|
*
|
|
907
|
-
* Build step: datafile → bundled → fetch
|
|
908
|
-
*
|
|
1411
|
+
* Build step: datafile → bundled → one-time fetch
|
|
1412
|
+
* Streaming mode: stream → datafile → bundled
|
|
1413
|
+
* Polling mode (no stream): poll → datafile → bundled
|
|
1414
|
+
* Offline mode (neither): datafile → bundled → one-time fetch
|
|
909
1415
|
*/
|
|
910
1416
|
async initialize() {
|
|
911
1417
|
if (this.options.buildStep) {
|
|
1418
|
+
this.transition("build:loading");
|
|
912
1419
|
await this.initializeForBuildStep();
|
|
1420
|
+
this.transition("build:ready");
|
|
913
1421
|
return;
|
|
914
1422
|
}
|
|
915
1423
|
if (!this.data && this.options.datafile) {
|
|
916
|
-
this.data = this.options.datafile;
|
|
1424
|
+
this.data = tagData(this.options.datafile, "provided");
|
|
1425
|
+
}
|
|
1426
|
+
if (!this.data) {
|
|
1427
|
+
try {
|
|
1428
|
+
const bundled = await this.bundledSource.tryLoad();
|
|
1429
|
+
if (bundled) {
|
|
1430
|
+
this.data = tagData(bundled, "bundled");
|
|
1431
|
+
}
|
|
1432
|
+
} catch {
|
|
1433
|
+
}
|
|
917
1434
|
}
|
|
918
1435
|
if (this.data) {
|
|
919
|
-
this.
|
|
1436
|
+
if (this.options.stream.enabled) {
|
|
1437
|
+
this.transition("initializing:stream");
|
|
1438
|
+
await this.tryInitializeStream();
|
|
1439
|
+
} else if (this.options.polling.enabled) {
|
|
1440
|
+
this.transition("initializing:polling");
|
|
1441
|
+
await this.tryInitializePolling();
|
|
1442
|
+
} else {
|
|
1443
|
+
this.transition("degraded");
|
|
1444
|
+
}
|
|
920
1445
|
return;
|
|
921
1446
|
}
|
|
922
|
-
this.
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1447
|
+
if (this.options.stream.enabled) {
|
|
1448
|
+
this.transition("initializing:stream");
|
|
1449
|
+
const streamSuccess = await this.tryInitializeStream();
|
|
1450
|
+
if (streamSuccess) {
|
|
1451
|
+
this.transition("streaming");
|
|
1452
|
+
return;
|
|
927
1453
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1454
|
+
} else if (this.options.polling.enabled) {
|
|
1455
|
+
this.transition("initializing:polling");
|
|
1456
|
+
const pollingSuccess = await this.tryInitializePolling();
|
|
1457
|
+
if (pollingSuccess) {
|
|
1458
|
+
this.transition("polling");
|
|
1459
|
+
return;
|
|
931
1460
|
}
|
|
932
|
-
if (this.data) return;
|
|
933
|
-
await this.initializeFromBundled();
|
|
934
|
-
} finally {
|
|
935
|
-
this.isInitializing = false;
|
|
936
1461
|
}
|
|
1462
|
+
await this.initializeFromFallbacks();
|
|
937
1463
|
}
|
|
938
1464
|
/**
|
|
939
1465
|
* Reads the current datafile with metrics.
|
|
940
1466
|
*/
|
|
941
1467
|
async read() {
|
|
942
1468
|
const startTime = Date.now();
|
|
943
|
-
const
|
|
944
|
-
const cacheHadDefinitions = cachedData !== void 0;
|
|
1469
|
+
const cacheHadDefinitions = this.data !== void 0;
|
|
945
1470
|
const isFirstRead = this.isFirstGetData;
|
|
946
1471
|
this.isFirstGetData = false;
|
|
947
|
-
|
|
948
|
-
let source;
|
|
949
|
-
let cacheStatus;
|
|
950
|
-
if (this.options.buildStep) {
|
|
951
|
-
[result, source, cacheStatus] = await this.getDataForBuildStep();
|
|
952
|
-
} else if (cachedData) {
|
|
953
|
-
[result, source, cacheStatus] = this.getDataFromCache(cachedData);
|
|
954
|
-
} else {
|
|
955
|
-
[result, source, cacheStatus] = await this.getDataWithFallbacks();
|
|
956
|
-
}
|
|
1472
|
+
const [result, cacheStatus] = await this.resolveData();
|
|
957
1473
|
const readMs = Date.now() - startTime;
|
|
1474
|
+
const { _origin, ...data } = result;
|
|
1475
|
+
const source = originToMetricsSource(_origin);
|
|
958
1476
|
this.trackRead(startTime, cacheHadDefinitions, isFirstRead, source);
|
|
959
|
-
return
|
|
1477
|
+
return {
|
|
1478
|
+
...data,
|
|
960
1479
|
metrics: {
|
|
961
1480
|
readMs,
|
|
962
1481
|
source,
|
|
963
1482
|
cacheStatus,
|
|
964
|
-
connectionState: this.
|
|
1483
|
+
connectionState: this.isConnected ? "connected" : "disconnected",
|
|
1484
|
+
mode: this.mode
|
|
965
1485
|
}
|
|
966
|
-
}
|
|
1486
|
+
};
|
|
967
1487
|
}
|
|
968
1488
|
/**
|
|
969
1489
|
* Shuts down the data source and releases resources.
|
|
970
1490
|
*/
|
|
971
1491
|
async shutdown() {
|
|
972
|
-
this.
|
|
973
|
-
this.
|
|
974
|
-
this.
|
|
975
|
-
this.
|
|
976
|
-
this.
|
|
977
|
-
this.hasWarnedAboutStaleData = false;
|
|
1492
|
+
this.unwireSourceEvents();
|
|
1493
|
+
this.streamSource.stop();
|
|
1494
|
+
this.pollingSource.stop();
|
|
1495
|
+
this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
|
|
1496
|
+
this.transition("shutdown");
|
|
978
1497
|
await this.usageTracker.flush();
|
|
979
1498
|
}
|
|
980
1499
|
/**
|
|
981
1500
|
* Returns the datafile with metrics.
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
* This method never opens a streaming connection, but will read from
|
|
986
|
-
* the stream if it is already open. Otherwise it fetches over the network.
|
|
1501
|
+
* Uses in-memory data if available, otherwise falls back to bundled,
|
|
1502
|
+
* then to a one-time fetch if called without prior initialization.
|
|
987
1503
|
*/
|
|
988
1504
|
async getDatafile() {
|
|
989
1505
|
const startTime = Date.now();
|
|
1506
|
+
this.isFirstGetData = false;
|
|
990
1507
|
let result;
|
|
991
|
-
let source;
|
|
992
1508
|
let cacheStatus;
|
|
993
1509
|
if (this.options.buildStep) {
|
|
994
|
-
[result,
|
|
995
|
-
} else if (this.
|
|
996
|
-
|
|
1510
|
+
[result, cacheStatus] = await this.resolveDataForBuildStep();
|
|
1511
|
+
} else if (this.data) {
|
|
1512
|
+
cacheStatus = this.isConnected ? "HIT" : "STALE";
|
|
1513
|
+
result = this.data;
|
|
997
1514
|
} else {
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
this.
|
|
1001
|
-
this.
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1515
|
+
const bundled = await this.bundledSource.tryLoad();
|
|
1516
|
+
if (bundled) {
|
|
1517
|
+
this.data = tagData(bundled, "bundled");
|
|
1518
|
+
result = this.data;
|
|
1519
|
+
cacheStatus = "MISS";
|
|
1520
|
+
} else {
|
|
1521
|
+
try {
|
|
1522
|
+
const fetched = await fetchDatafile({
|
|
1523
|
+
host: this.options.host,
|
|
1524
|
+
sdkKey: this.options.sdkKey,
|
|
1525
|
+
fetch: this.options.fetch
|
|
1526
|
+
});
|
|
1527
|
+
this.data = tagData(fetched, "fetched");
|
|
1528
|
+
result = this.data;
|
|
1529
|
+
cacheStatus = "MISS";
|
|
1530
|
+
} catch {
|
|
1531
|
+
throw new Error(
|
|
1532
|
+
"@vercel/flags-core: No flag definitions available. Initialize the client or provide a datafile."
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1005
1535
|
}
|
|
1006
|
-
[result, source, cacheStatus] = [this.data ?? fetched, "remote", "MISS"];
|
|
1007
1536
|
}
|
|
1008
|
-
|
|
1537
|
+
const source = originToMetricsSource(result._origin);
|
|
1538
|
+
return {
|
|
1539
|
+
...result,
|
|
1009
1540
|
metrics: {
|
|
1010
1541
|
readMs: Date.now() - startTime,
|
|
1011
1542
|
source,
|
|
1012
1543
|
cacheStatus,
|
|
1013
|
-
connectionState: this.
|
|
1544
|
+
connectionState: this.isConnected ? "connected" : "disconnected",
|
|
1545
|
+
mode: this.mode
|
|
1014
1546
|
}
|
|
1015
|
-
}
|
|
1547
|
+
};
|
|
1016
1548
|
}
|
|
1017
1549
|
/**
|
|
1018
1550
|
* Returns the bundled fallback datafile.
|
|
1019
1551
|
*/
|
|
1020
1552
|
async getFallbackDatafile() {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1553
|
+
return this.bundledSource.getRaw();
|
|
1554
|
+
}
|
|
1555
|
+
// ---------------------------------------------------------------------------
|
|
1556
|
+
// Data resolution (shared by read() and getDatafile())
|
|
1557
|
+
// ---------------------------------------------------------------------------
|
|
1558
|
+
/**
|
|
1559
|
+
* Resolves the current data, using the appropriate strategy for the
|
|
1560
|
+
* current mode. Returns tagged data and cache status.
|
|
1561
|
+
*
|
|
1562
|
+
* Build step: cached → bundled → one-time fetch
|
|
1563
|
+
* Runtime with cache: return cached data
|
|
1564
|
+
* Runtime without cache: stream/poll → datafile → bundled → fetch → throw
|
|
1565
|
+
*/
|
|
1566
|
+
async resolveData() {
|
|
1567
|
+
if (this.options.buildStep) {
|
|
1568
|
+
return this.resolveDataForBuildStep();
|
|
1027
1569
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
case "missing-file":
|
|
1032
|
-
throw new FallbackNotFoundError();
|
|
1033
|
-
case "missing-entry":
|
|
1034
|
-
throw new FallbackEntryNotFoundError();
|
|
1035
|
-
case "unexpected-error":
|
|
1036
|
-
throw new Error(
|
|
1037
|
-
"@vercel/flags-core: Failed to read bundled definitions: " + String(bundledResult.error)
|
|
1038
|
-
);
|
|
1570
|
+
if (this.data) {
|
|
1571
|
+
const cacheStatus = this.isConnected ? "HIT" : "STALE";
|
|
1572
|
+
return [this.data, cacheStatus];
|
|
1039
1573
|
}
|
|
1574
|
+
return this.resolveDataWithFallbacks();
|
|
1040
1575
|
}
|
|
1041
1576
|
// ---------------------------------------------------------------------------
|
|
1042
|
-
// Stream
|
|
1577
|
+
// Stream initialization
|
|
1043
1578
|
// ---------------------------------------------------------------------------
|
|
1044
1579
|
/**
|
|
1045
1580
|
* Attempts to initialize via stream with timeout.
|
|
1046
1581
|
* Returns true if stream connected successfully within timeout.
|
|
1047
1582
|
*/
|
|
1048
1583
|
async tryInitializeStream() {
|
|
1049
|
-
let streamPromise;
|
|
1050
1584
|
if (this.options.stream.initTimeoutMs <= 0) {
|
|
1051
1585
|
try {
|
|
1052
|
-
|
|
1053
|
-
await streamPromise;
|
|
1586
|
+
await this.streamSource.start();
|
|
1054
1587
|
return true;
|
|
1055
|
-
} catch {
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
if (error instanceof UnauthorizedError) {
|
|
1590
|
+
this.unauthorized = true;
|
|
1591
|
+
}
|
|
1056
1592
|
return false;
|
|
1057
1593
|
}
|
|
1058
1594
|
}
|
|
@@ -1064,87 +1600,44 @@ var FlagNetworkDataSource = class _FlagNetworkDataSource {
|
|
|
1064
1600
|
);
|
|
1065
1601
|
});
|
|
1066
1602
|
try {
|
|
1067
|
-
|
|
1068
|
-
|
|
1603
|
+
const result = await Promise.race([
|
|
1604
|
+
this.streamSource.start(),
|
|
1605
|
+
timeoutPromise
|
|
1606
|
+
]);
|
|
1069
1607
|
clearTimeout(timeoutId);
|
|
1070
1608
|
if (result === "timeout") {
|
|
1071
1609
|
console.warn(
|
|
1072
1610
|
"@vercel/flags-core: Stream initialization timeout, falling back"
|
|
1073
1611
|
);
|
|
1612
|
+
void this.streamSource.start().catch(() => {
|
|
1613
|
+
});
|
|
1074
1614
|
return false;
|
|
1075
1615
|
}
|
|
1076
1616
|
return true;
|
|
1077
|
-
} catch {
|
|
1617
|
+
} catch (error) {
|
|
1078
1618
|
clearTimeout(timeoutId);
|
|
1619
|
+
if (error instanceof Error && error.message.includes("401")) {
|
|
1620
|
+
this.unauthorized = true;
|
|
1621
|
+
}
|
|
1079
1622
|
return false;
|
|
1080
1623
|
}
|
|
1081
1624
|
}
|
|
1082
|
-
/**
|
|
1083
|
-
* Starts the stream connection with callbacks for data and disconnect.
|
|
1084
|
-
*/
|
|
1085
|
-
startStream() {
|
|
1086
|
-
if (this.streamPromise) return this.streamPromise;
|
|
1087
|
-
this.streamAbortController = new AbortController();
|
|
1088
|
-
this.isStreamConnected = false;
|
|
1089
|
-
this.hasWarnedAboutStaleData = false;
|
|
1090
|
-
try {
|
|
1091
|
-
const streamPromise = connectStream(
|
|
1092
|
-
{
|
|
1093
|
-
host: this.host,
|
|
1094
|
-
sdkKey: this.options.sdkKey,
|
|
1095
|
-
abortController: this.streamAbortController,
|
|
1096
|
-
fetch: this.options.fetch
|
|
1097
|
-
},
|
|
1098
|
-
{
|
|
1099
|
-
onMessage: (newData) => {
|
|
1100
|
-
if (this.isNewerData(newData)) {
|
|
1101
|
-
this.data = newData;
|
|
1102
|
-
}
|
|
1103
|
-
this.isStreamConnected = true;
|
|
1104
|
-
this.hasWarnedAboutStaleData = false;
|
|
1105
|
-
if (this.pollingIntervalId) {
|
|
1106
|
-
this.stopPolling();
|
|
1107
|
-
}
|
|
1108
|
-
},
|
|
1109
|
-
onDisconnect: () => {
|
|
1110
|
-
this.isStreamConnected = false;
|
|
1111
|
-
if (this.options.polling.enabled && !this.pollingIntervalId && !this.isInitializing) {
|
|
1112
|
-
this.startPolling();
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
);
|
|
1117
|
-
this.streamPromise = streamPromise;
|
|
1118
|
-
return streamPromise;
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
this.streamPromise = void 0;
|
|
1121
|
-
this.streamAbortController = void 0;
|
|
1122
|
-
throw error;
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
/**
|
|
1126
|
-
* Stops the stream connection.
|
|
1127
|
-
*/
|
|
1128
|
-
stopStream() {
|
|
1129
|
-
this.streamAbortController?.abort();
|
|
1130
|
-
this.streamAbortController = void 0;
|
|
1131
|
-
this.streamPromise = void 0;
|
|
1132
|
-
}
|
|
1133
1625
|
// ---------------------------------------------------------------------------
|
|
1134
|
-
// Polling
|
|
1626
|
+
// Polling initialization
|
|
1135
1627
|
// ---------------------------------------------------------------------------
|
|
1136
1628
|
/**
|
|
1137
1629
|
* Attempts to initialize via polling with timeout.
|
|
1138
1630
|
* Returns true if first poll succeeded within timeout.
|
|
1631
|
+
*
|
|
1632
|
+
* Only used when streaming is disabled and polling is the primary source.
|
|
1139
1633
|
*/
|
|
1140
1634
|
async tryInitializePolling() {
|
|
1141
|
-
|
|
1142
|
-
const pollPromise = this.performPoll();
|
|
1635
|
+
const pollPromise = this.pollingSource.poll();
|
|
1143
1636
|
if (this.options.polling.initTimeoutMs <= 0) {
|
|
1144
1637
|
try {
|
|
1145
1638
|
await pollPromise;
|
|
1146
1639
|
if (this.data) {
|
|
1147
|
-
this.
|
|
1640
|
+
this.pollingSource.startInterval();
|
|
1148
1641
|
return true;
|
|
1149
1642
|
}
|
|
1150
1643
|
return false;
|
|
@@ -1169,7 +1662,7 @@ var FlagNetworkDataSource = class _FlagNetworkDataSource {
|
|
|
1169
1662
|
return false;
|
|
1170
1663
|
}
|
|
1171
1664
|
if (this.data) {
|
|
1172
|
-
this.
|
|
1665
|
+
this.pollingSource.startInterval();
|
|
1173
1666
|
return true;
|
|
1174
1667
|
}
|
|
1175
1668
|
return false;
|
|
@@ -1178,68 +1671,6 @@ var FlagNetworkDataSource = class _FlagNetworkDataSource {
|
|
|
1178
1671
|
return false;
|
|
1179
1672
|
}
|
|
1180
1673
|
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Starts polling (initial poll + interval).
|
|
1183
|
-
*/
|
|
1184
|
-
startPolling() {
|
|
1185
|
-
if (this.pollingIntervalId) return;
|
|
1186
|
-
this.pollingAbortController = new AbortController();
|
|
1187
|
-
void this.performPoll();
|
|
1188
|
-
this.startPollingInterval();
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Starts the polling interval (without initial poll).
|
|
1192
|
-
*/
|
|
1193
|
-
startPollingInterval() {
|
|
1194
|
-
if (this.pollingIntervalId) return;
|
|
1195
|
-
this.pollingIntervalId = setInterval(
|
|
1196
|
-
() => void this.performPoll(),
|
|
1197
|
-
this.options.polling.intervalMs
|
|
1198
|
-
);
|
|
1199
|
-
}
|
|
1200
|
-
/**
|
|
1201
|
-
* Stops polling.
|
|
1202
|
-
*/
|
|
1203
|
-
stopPolling() {
|
|
1204
|
-
if (this.pollingIntervalId) {
|
|
1205
|
-
clearInterval(this.pollingIntervalId);
|
|
1206
|
-
this.pollingIntervalId = void 0;
|
|
1207
|
-
}
|
|
1208
|
-
this.pollingAbortController?.abort();
|
|
1209
|
-
this.pollingAbortController = void 0;
|
|
1210
|
-
}
|
|
1211
|
-
/**
|
|
1212
|
-
* Performs a single poll request.
|
|
1213
|
-
*/
|
|
1214
|
-
async performPoll() {
|
|
1215
|
-
if (this.pollingAbortController?.signal.aborted) return;
|
|
1216
|
-
try {
|
|
1217
|
-
const data = await fetchDatafile(
|
|
1218
|
-
this.host,
|
|
1219
|
-
this.options.sdkKey,
|
|
1220
|
-
this.options.fetch
|
|
1221
|
-
);
|
|
1222
|
-
if (this.isNewerData(data)) {
|
|
1223
|
-
this.data = data;
|
|
1224
|
-
}
|
|
1225
|
-
} catch (error) {
|
|
1226
|
-
console.error("@vercel/flags-core: Poll failed:", error);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
// ---------------------------------------------------------------------------
|
|
1230
|
-
// Background updates
|
|
1231
|
-
// ---------------------------------------------------------------------------
|
|
1232
|
-
/**
|
|
1233
|
-
* Starts background updates (stream or polling) without blocking.
|
|
1234
|
-
* Used when we already have data from provided datafile.
|
|
1235
|
-
*/
|
|
1236
|
-
startBackgroundUpdates() {
|
|
1237
|
-
if (this.options.stream.enabled) {
|
|
1238
|
-
void this.startStream();
|
|
1239
|
-
} else if (this.options.polling.enabled) {
|
|
1240
|
-
this.startPolling();
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
1674
|
// ---------------------------------------------------------------------------
|
|
1244
1675
|
// Build step helpers
|
|
1245
1676
|
// ---------------------------------------------------------------------------
|
|
@@ -1248,116 +1679,139 @@ var FlagNetworkDataSource = class _FlagNetworkDataSource {
|
|
|
1248
1679
|
*/
|
|
1249
1680
|
async initializeForBuildStep() {
|
|
1250
1681
|
if (this.data) return;
|
|
1251
|
-
if (this.
|
|
1252
|
-
|
|
1253
|
-
if (bundledResult?.state === "ok" && bundledResult.definitions) {
|
|
1254
|
-
this.data = bundledResult.definitions;
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1682
|
+
if (!this.buildDataPromise) {
|
|
1683
|
+
this.buildDataPromise = this.loadBuildData();
|
|
1257
1684
|
}
|
|
1258
|
-
this.data = await
|
|
1259
|
-
this.host,
|
|
1260
|
-
this.options.sdkKey,
|
|
1261
|
-
this.options.fetch
|
|
1262
|
-
);
|
|
1685
|
+
this.data = await this.buildDataPromise;
|
|
1263
1686
|
}
|
|
1264
1687
|
/**
|
|
1265
1688
|
* Retrieves data during build steps.
|
|
1689
|
+
* Concurrent callers share a single load promise. The first caller to
|
|
1690
|
+
* populate `this.data` gets cacheStatus MISS; subsequent callers get HIT.
|
|
1266
1691
|
*/
|
|
1267
|
-
async
|
|
1692
|
+
async resolveDataForBuildStep() {
|
|
1268
1693
|
if (this.data) {
|
|
1269
|
-
return [this.data, "
|
|
1694
|
+
return [this.data, "HIT"];
|
|
1270
1695
|
}
|
|
1271
|
-
if (this.
|
|
1272
|
-
|
|
1273
|
-
if (bundledResult?.state === "ok" && bundledResult.definitions) {
|
|
1274
|
-
this.data = bundledResult.definitions;
|
|
1275
|
-
return [this.data, "embedded", "MISS"];
|
|
1276
|
-
}
|
|
1696
|
+
if (!this.buildDataPromise) {
|
|
1697
|
+
this.buildDataPromise = this.loadBuildData();
|
|
1277
1698
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
this.
|
|
1281
|
-
|
|
1699
|
+
const data = await this.buildDataPromise;
|
|
1700
|
+
if (!this.data) {
|
|
1701
|
+
this.data = data;
|
|
1702
|
+
return [data, "MISS"];
|
|
1703
|
+
}
|
|
1704
|
+
return [this.data, "HIT"];
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Loads data for a build step: bundled → one-time fetch.
|
|
1708
|
+
*/
|
|
1709
|
+
async loadBuildData() {
|
|
1710
|
+
const bundled = await this.bundledSource.tryLoad();
|
|
1711
|
+
if (bundled) return tagData(bundled, "bundled");
|
|
1712
|
+
try {
|
|
1713
|
+
const fetched = await fetchDatafile({
|
|
1714
|
+
host: this.options.host,
|
|
1715
|
+
sdkKey: this.options.sdkKey,
|
|
1716
|
+
fetch: this.options.fetch
|
|
1717
|
+
});
|
|
1718
|
+
return tagData(fetched, "fetched");
|
|
1719
|
+
} catch {
|
|
1720
|
+
}
|
|
1721
|
+
throw new Error(
|
|
1722
|
+
"@vercel/flags-core: No flag definitions available during build. Provide a datafile or bundled definitions."
|
|
1282
1723
|
);
|
|
1283
|
-
return [this.data, "remote", "MISS"];
|
|
1284
1724
|
}
|
|
1285
1725
|
// ---------------------------------------------------------------------------
|
|
1286
|
-
//
|
|
1726
|
+
// Fallback helpers
|
|
1287
1727
|
// ---------------------------------------------------------------------------
|
|
1288
1728
|
/**
|
|
1289
|
-
*
|
|
1729
|
+
* Shared fallback chain used by both initialize() and resolveData().
|
|
1290
1730
|
*/
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
this.
|
|
1294
|
-
|
|
1295
|
-
|
|
1731
|
+
async initializeFromFallbacks() {
|
|
1732
|
+
this.transition("initializing:fallback");
|
|
1733
|
+
if (this.data) {
|
|
1734
|
+
this.transition("degraded");
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
const bundled = await this.bundledSource.tryLoad();
|
|
1738
|
+
if (bundled) {
|
|
1739
|
+
this.data = tagData(bundled, "bundled");
|
|
1740
|
+
this.transition("degraded");
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (!this.options.stream.enabled && !this.options.polling.enabled) {
|
|
1744
|
+
try {
|
|
1745
|
+
const fetched = await fetchDatafile({
|
|
1746
|
+
host: this.options.host,
|
|
1747
|
+
sdkKey: this.options.sdkKey,
|
|
1748
|
+
fetch: this.options.fetch
|
|
1749
|
+
});
|
|
1750
|
+
this.data = tagData(fetched, "fetched");
|
|
1751
|
+
this.transition("degraded");
|
|
1752
|
+
return;
|
|
1753
|
+
} catch {
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
throw new Error(
|
|
1757
|
+
"@vercel/flags-core: No flag definitions available. Bundled definitions not found."
|
|
1758
|
+
);
|
|
1296
1759
|
}
|
|
1297
1760
|
/**
|
|
1298
|
-
* Retrieves data using the fallback chain.
|
|
1761
|
+
* Retrieves data using the fallback chain (called when no cached data exists).
|
|
1762
|
+
* Streaming mode: stream → datafile → bundled.
|
|
1763
|
+
* Polling mode: poll → datafile → bundled.
|
|
1764
|
+
* Offline mode: datafile → bundled → one-time fetch.
|
|
1299
1765
|
*/
|
|
1300
|
-
async
|
|
1766
|
+
async resolveDataWithFallbacks() {
|
|
1301
1767
|
if (this.options.stream.enabled) {
|
|
1768
|
+
this.transition("initializing:stream");
|
|
1302
1769
|
const streamSuccess = await this.tryInitializeStream();
|
|
1303
1770
|
if (streamSuccess && this.data) {
|
|
1304
|
-
|
|
1771
|
+
this.transition("streaming");
|
|
1772
|
+
return [this.data, "MISS"];
|
|
1305
1773
|
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1774
|
+
} else if (this.options.polling.enabled) {
|
|
1775
|
+
this.transition("initializing:polling");
|
|
1308
1776
|
const pollingSuccess = await this.tryInitializePolling();
|
|
1309
1777
|
if (pollingSuccess && this.data) {
|
|
1310
|
-
|
|
1778
|
+
this.transition("polling");
|
|
1779
|
+
return [this.data, "MISS"];
|
|
1311
1780
|
}
|
|
1312
1781
|
}
|
|
1782
|
+
this.transition("initializing:fallback");
|
|
1313
1783
|
if (this.options.datafile) {
|
|
1314
|
-
this.data = this.options.datafile;
|
|
1315
|
-
|
|
1784
|
+
this.data = tagData(this.options.datafile, "provided");
|
|
1785
|
+
this.transition("degraded");
|
|
1786
|
+
return [this.data, "STALE"];
|
|
1316
1787
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
this.data = bundledResult.definitions;
|
|
1324
|
-
return [this.data, "embedded", "STALE"];
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
throw new Error(
|
|
1328
|
-
"@vercel/flags-core: No flag definitions available. Ensure streaming/polling is enabled or provide a datafile."
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
/**
|
|
1332
|
-
* Initializes from bundled definitions.
|
|
1333
|
-
*/
|
|
1334
|
-
async initializeFromBundled() {
|
|
1335
|
-
if (!this.bundledDefinitionsPromise) {
|
|
1336
|
-
throw new Error(
|
|
1337
|
-
"@vercel/flags-core: No flag definitions available. Ensure streaming/polling is enabled or provide a datafile."
|
|
1338
|
-
);
|
|
1788
|
+
const bundled = await this.bundledSource.tryLoad();
|
|
1789
|
+
if (bundled) {
|
|
1790
|
+
console.warn("@vercel/flags-core: Using bundled definitions as fallback");
|
|
1791
|
+
this.data = tagData(bundled, "bundled");
|
|
1792
|
+
this.transition("degraded");
|
|
1793
|
+
return [this.data, "STALE"];
|
|
1339
1794
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1795
|
+
if (!this.options.stream.enabled && !this.options.polling.enabled) {
|
|
1796
|
+
try {
|
|
1797
|
+
const fetched = await fetchDatafile({
|
|
1798
|
+
host: this.options.host,
|
|
1799
|
+
sdkKey: this.options.sdkKey,
|
|
1800
|
+
fetch: this.options.fetch
|
|
1801
|
+
});
|
|
1802
|
+
this.data = tagData(fetched, "fetched");
|
|
1803
|
+
this.transition("degraded");
|
|
1804
|
+
return [this.data, "MISS"];
|
|
1805
|
+
} catch {
|
|
1806
|
+
}
|
|
1344
1807
|
}
|
|
1345
1808
|
throw new Error(
|
|
1346
|
-
"@vercel/flags-core: No flag definitions available.
|
|
1809
|
+
"@vercel/flags-core: No flag definitions available. Provide a datafile or bundled definitions."
|
|
1347
1810
|
);
|
|
1348
1811
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
*/
|
|
1353
|
-
static parseConfigUpdatedAt(value) {
|
|
1354
|
-
if (typeof value === "number") return value;
|
|
1355
|
-
if (typeof value === "string") {
|
|
1356
|
-
const parsed = Number(value);
|
|
1357
|
-
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1358
|
-
}
|
|
1359
|
-
return void 0;
|
|
1360
|
-
}
|
|
1812
|
+
// ---------------------------------------------------------------------------
|
|
1813
|
+
// Data comparison
|
|
1814
|
+
// ---------------------------------------------------------------------------
|
|
1361
1815
|
/**
|
|
1362
1816
|
* Checks if the incoming data is newer than the current in-memory data.
|
|
1363
1817
|
* Returns true if the update should proceed, false if it should be skipped.
|
|
@@ -1367,50 +1821,47 @@ var FlagNetworkDataSource = class _FlagNetworkDataSource {
|
|
|
1367
1821
|
* - The current data has no configUpdatedAt
|
|
1368
1822
|
* - The incoming data has no configUpdatedAt
|
|
1369
1823
|
*
|
|
1370
|
-
* Skips the update only when both have configUpdatedAt and incoming is
|
|
1824
|
+
* Skips the update only when both have configUpdatedAt and incoming is not newer.
|
|
1371
1825
|
*/
|
|
1372
1826
|
isNewerData(incoming) {
|
|
1373
1827
|
if (!this.data) return true;
|
|
1374
|
-
const currentTs =
|
|
1375
|
-
|
|
1376
|
-
);
|
|
1377
|
-
const incomingTs = _FlagNetworkDataSource.parseConfigUpdatedAt(
|
|
1378
|
-
incoming.configUpdatedAt
|
|
1379
|
-
);
|
|
1828
|
+
const currentTs = parseConfigUpdatedAt(this.data.configUpdatedAt);
|
|
1829
|
+
const incomingTs = parseConfigUpdatedAt(incoming.configUpdatedAt);
|
|
1380
1830
|
if (currentTs === void 0 || incomingTs === void 0) {
|
|
1381
1831
|
return true;
|
|
1382
1832
|
}
|
|
1383
|
-
return incomingTs
|
|
1384
|
-
}
|
|
1385
|
-
/**
|
|
1386
|
-
* Logs a warning if returning cached data while stream is disconnected.
|
|
1387
|
-
*/
|
|
1388
|
-
warnIfDisconnected() {
|
|
1389
|
-
if (!this.isStreamConnected && !this.hasWarnedAboutStaleData) {
|
|
1390
|
-
this.hasWarnedAboutStaleData = true;
|
|
1391
|
-
console.warn(
|
|
1392
|
-
"@vercel/flags-core: Returning in-memory flag definitions while stream is disconnected. Data may be stale."
|
|
1393
|
-
);
|
|
1394
|
-
}
|
|
1833
|
+
return incomingTs > currentTs;
|
|
1395
1834
|
}
|
|
1396
1835
|
// ---------------------------------------------------------------------------
|
|
1397
1836
|
// Usage tracking
|
|
1398
1837
|
// ---------------------------------------------------------------------------
|
|
1399
1838
|
/**
|
|
1400
1839
|
* Tracks a read operation for usage analytics.
|
|
1840
|
+
* During build steps, only the first read is tracked.
|
|
1401
1841
|
*/
|
|
1402
1842
|
trackRead(startTime, cacheHadDefinitions, isFirstRead, source) {
|
|
1843
|
+
if (this.unauthorized) return;
|
|
1844
|
+
if (this.options.buildStep && this.buildReadTracked) return;
|
|
1845
|
+
if (this.options.buildStep) this.buildReadTracked = true;
|
|
1403
1846
|
const configOrigin = source === "embedded" ? "embedded" : "in-memory";
|
|
1847
|
+
const cacheAction = this.state === "streaming" ? "FOLLOWING" : this.state === "polling" ? "REFRESHING" : "NONE";
|
|
1848
|
+
const mode = this.mode;
|
|
1404
1849
|
const trackOptions = {
|
|
1405
1850
|
configOrigin,
|
|
1406
1851
|
cacheStatus: cacheHadDefinitions ? "HIT" : "MISS",
|
|
1852
|
+
cacheAction,
|
|
1407
1853
|
cacheIsBlocking: !cacheHadDefinitions,
|
|
1408
|
-
duration: Date.now() - startTime
|
|
1854
|
+
duration: Date.now() - startTime,
|
|
1855
|
+
mode: mode === "streaming" ? "stream" : mode === "polling" ? "poll" : mode
|
|
1409
1856
|
};
|
|
1410
1857
|
const configUpdatedAt = this.data?.configUpdatedAt;
|
|
1411
1858
|
if (typeof configUpdatedAt === "number") {
|
|
1412
1859
|
trackOptions.configUpdatedAt = configUpdatedAt;
|
|
1413
1860
|
}
|
|
1861
|
+
const revision = this.data?.revision;
|
|
1862
|
+
if (typeof revision === "number") {
|
|
1863
|
+
trackOptions.revision = revision;
|
|
1864
|
+
}
|
|
1414
1865
|
if (isFirstRead) {
|
|
1415
1866
|
trackOptions.cacheIsFirstRead = true;
|
|
1416
1867
|
}
|
|
@@ -1434,16 +1885,23 @@ function parseSdkKeyFromFlagsConnectionString(text) {
|
|
|
1434
1885
|
function make(createRawClient) {
|
|
1435
1886
|
let _defaultFlagsClient = null;
|
|
1436
1887
|
function createClient2(sdkKeyOrConnectionString, options) {
|
|
1437
|
-
if (!sdkKeyOrConnectionString)
|
|
1888
|
+
if (!sdkKeyOrConnectionString)
|
|
1889
|
+
throw new Error("@vercel/flags-core: Missing sdkKey");
|
|
1890
|
+
if (typeof sdkKeyOrConnectionString !== "string")
|
|
1891
|
+
throw new Error(
|
|
1892
|
+
`@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`
|
|
1893
|
+
);
|
|
1438
1894
|
const sdkKey = parseSdkKeyFromFlagsConnectionString(
|
|
1439
1895
|
sdkKeyOrConnectionString
|
|
1440
1896
|
);
|
|
1441
1897
|
if (!sdkKey) {
|
|
1442
|
-
throw new Error(
|
|
1898
|
+
throw new Error(
|
|
1899
|
+
"@vercel/flags-core: Missing sdkKey in connection string"
|
|
1900
|
+
);
|
|
1443
1901
|
}
|
|
1444
|
-
const
|
|
1902
|
+
const controller = new Controller({ sdkKey, ...options });
|
|
1445
1903
|
return createRawClient({
|
|
1446
|
-
|
|
1904
|
+
controller,
|
|
1447
1905
|
origin: { provider: "vercel", sdkKey }
|
|
1448
1906
|
});
|
|
1449
1907
|
}
|
|
@@ -1458,7 +1916,7 @@ function make(createRawClient) {
|
|
|
1458
1916
|
}
|
|
1459
1917
|
const sdkKey = parseSdkKeyFromFlagsConnectionString(process.env.FLAGS);
|
|
1460
1918
|
if (!sdkKey) {
|
|
1461
|
-
throw new Error("flags: Missing sdkKey");
|
|
1919
|
+
throw new Error("@vercel/flags-core: Missing sdkKey");
|
|
1462
1920
|
}
|
|
1463
1921
|
_defaultFlagsClient = createClient2(sdkKey);
|
|
1464
1922
|
}
|
|
@@ -1516,9 +1974,9 @@ export {
|
|
|
1516
1974
|
evaluate,
|
|
1517
1975
|
FallbackNotFoundError,
|
|
1518
1976
|
FallbackEntryNotFoundError,
|
|
1519
|
-
|
|
1977
|
+
Controller,
|
|
1520
1978
|
flagsClient,
|
|
1521
1979
|
resetDefaultFlagsClient,
|
|
1522
1980
|
createClient
|
|
1523
1981
|
};
|
|
1524
|
-
//# sourceMappingURL=chunk-
|
|
1982
|
+
//# sourceMappingURL=chunk-NEIY3ICC.js.map
|