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