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