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