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