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