@vercel/flags-core 1.4.0 → 1.5.0-581a9c5-20260525125622
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 +39 -0
- package/dist/{chunk-OABKRAPP.js → chunk-IMGERCR6.js} +171 -18
- package/dist/chunk-IMGERCR6.js.map +1 -0
- package/dist/{chunk-WKSYRFUY.cjs → chunk-OC6E7QKL.cjs} +213 -60
- package/dist/chunk-OC6E7QKL.cjs.map +1 -0
- package/dist/{chunk-2F6JQ5SL.js → chunk-QYTLEVW5.js} +175 -18
- package/dist/chunk-QYTLEVW5.js.map +1 -0
- package/dist/{chunk-3Z4P7Q6O.cjs → chunk-XRXYFQ7B.cjs} +218 -61
- package/dist/chunk-XRXYFQ7B.cjs.map +1 -0
- package/dist/index.default.cjs +2 -2
- package/dist/index.default.d.cts +4 -2
- package/dist/index.default.d.ts +4 -2
- package/dist/index.default.js +1 -1
- package/dist/index.next-js.cjs +2 -2
- package/dist/index.next-js.d.cts +4 -2
- package/dist/index.next-js.d.ts +4 -2
- package/dist/index.next-js.js +1 -1
- 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-FKiSedQ9.d.cts → types-C70GeY3G.d.cts} +20 -0
- package/dist/{types-FKiSedQ9.d.ts → types-C70GeY3G.d.ts} +20 -0
- package/package.json +14 -4
- package/dist/chunk-2F6JQ5SL.js.map +0 -1
- package/dist/chunk-3Z4P7Q6O.cjs.map +0 -1
- package/dist/chunk-OABKRAPP.js.map +0 -1
- package/dist/chunk-WKSYRFUY.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @vercel/flags-core
|
|
2
2
|
|
|
3
|
+
## 1.5.0-581a9c5-20260525125622
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#385](https://github.com/vercel/flags/pull/385) [`5b2dc37`](https://github.com/vercel/flags/commit/5b2dc37041e24811f735beede41fc8c29f508cb7) Thanks [@dferber90](https://github.com/dferber90)! - Add `bulkEvaluate` method to `FlagsClient` for resolving multiple flags against shared entities in a single call.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const results = await client.bulkEvaluate(
|
|
11
|
+
[
|
|
12
|
+
{ key: "a", defaultValue: false },
|
|
13
|
+
{ key: "b", defaultValue: "off" },
|
|
14
|
+
],
|
|
15
|
+
entities
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
results.a; // EvaluationResult<boolean>
|
|
19
|
+
results.b; // EvaluationResult<string>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Avoids the per-flag overhead of separate `evaluate()` calls — the datafile is read once, entities are resolved once, and all flags share the same environment/segments lookup. Each entry in the returned record is a full `EvaluationResult` with `value`, `reason`, `outcomeType`, and `metrics`.
|
|
23
|
+
|
|
24
|
+
- [#371](https://github.com/vercel/flags/pull/371) [`bd4d01a`](https://github.com/vercel/flags/commit/bd4d01a9b2b5d70bf7ae62cda645d8cd7292ad83) Thanks [@vincent-derks](https://github.com/vincent-derks)! - Add jitter to ingest retries and the batch-flush window.
|
|
25
|
+
|
|
26
|
+
The usage tracker now uses AWS-style "Full Jitter" exponential backoff between
|
|
27
|
+
retry attempts (replacing the previous deterministic 100/200ms schedule) and
|
|
28
|
+
randomizes the 5s batch-flush window by ±20% to desynchronize concurrent
|
|
29
|
+
processes. When all retry attempts are exhausted the SDK now logs a structured
|
|
30
|
+
warning so consumers can alert on dropped batches.
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- [#382](https://github.com/vercel/flags/pull/382) [`4d90e91`](https://github.com/vercel/flags/commit/4d90e912a4d7c9d4ef986d5e8dc609c30b203242) Thanks [@dferber90](https://github.com/dferber90)! - Speed up flag evaluation on the hot path.
|
|
35
|
+
|
|
36
|
+
- `handleOutcome` no longer recomputes `scaledWeights` on every split-outcome evaluation; the per-outcome scaled weights are cached on first call.
|
|
37
|
+
- `matchConditions` no longer recompiles `RegExp` on every REGEX / NOT_REGEX condition; the compiled regex is cached on first call.
|
|
38
|
+
- `Controller.read()` and `getDatafile()` no longer re-destructure and re-spread the in-memory datafile on every call; the result is cached and rebuilt only when stream/poll replaces the underlying data.
|
|
39
|
+
|
|
40
|
+
In micro-benchmarks the pure `evaluate()` path is ~22% faster for split outcomes and ~32% faster for regex conditions. The full `client.evaluate()` path is 14–22% faster across all scenarios.
|
|
41
|
+
|
|
3
42
|
## 1.4.0
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
|
@@ -7,6 +7,7 @@ var __export = (target, all) => {
|
|
|
7
7
|
// src/controller-fns.ts
|
|
8
8
|
var controller_fns_exports = {};
|
|
9
9
|
__export(controller_fns_exports, {
|
|
10
|
+
bulkEvaluate: () => bulkEvaluate2,
|
|
10
11
|
controllerInstanceMap: () => controllerInstanceMap,
|
|
11
12
|
evaluate: () => evaluate2,
|
|
12
13
|
getDatafile: () => getDatafile,
|
|
@@ -46,6 +47,24 @@ var Packed;
|
|
|
46
47
|
|
|
47
48
|
// src/evaluate.ts
|
|
48
49
|
var MAX_REGEX_INPUT_LENGTH = 1e4;
|
|
50
|
+
var UINT32_MAX = 4294967295;
|
|
51
|
+
var SCALED_WEIGHTS = /* @__PURE__ */ Symbol("@vercel/flags-core:scaledWeights");
|
|
52
|
+
var COMPILED_REGEX = /* @__PURE__ */ Symbol("@vercel/flags-core:compiledRegex");
|
|
53
|
+
function getScaledWeights(outcome) {
|
|
54
|
+
const cached = outcome[SCALED_WEIGHTS];
|
|
55
|
+
if (cached) return cached;
|
|
56
|
+
const total = sum(outcome.weights);
|
|
57
|
+
const scaled = outcome.weights.map((w) => w / total * UINT32_MAX);
|
|
58
|
+
outcome[SCALED_WEIGHTS] = scaled;
|
|
59
|
+
return scaled;
|
|
60
|
+
}
|
|
61
|
+
function getCompiledRegex(rhs) {
|
|
62
|
+
const cached = rhs[COMPILED_REGEX];
|
|
63
|
+
if (cached) return cached;
|
|
64
|
+
const compiled = new RegExp(rhs.pattern, rhs.flags);
|
|
65
|
+
rhs[COMPILED_REGEX] = compiled;
|
|
66
|
+
return compiled;
|
|
67
|
+
}
|
|
49
68
|
function exhaustivenessCheck(_) {
|
|
50
69
|
throw new Error("Exhaustiveness check failed");
|
|
51
70
|
}
|
|
@@ -232,12 +251,12 @@ function matchConditions(conditions, params) {
|
|
|
232
251
|
return (isNumber(rhs) || isString(rhs)) && lhs <= rhs;
|
|
233
252
|
case "regex" /* REGEX */:
|
|
234
253
|
if (isString(lhs) && lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === "object" && !Array.isArray(rhs) && rhs?.type === "regex") {
|
|
235
|
-
return
|
|
254
|
+
return getCompiledRegex(rhs).test(lhs);
|
|
236
255
|
}
|
|
237
256
|
return false;
|
|
238
257
|
case "!regex" /* NOT_REGEX */:
|
|
239
258
|
if (isString(lhs) && lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === "object" && !Array.isArray(rhs) && rhs?.type === "regex") {
|
|
240
|
-
return !
|
|
259
|
+
return !getCompiledRegex(rhs).test(lhs);
|
|
241
260
|
}
|
|
242
261
|
return false;
|
|
243
262
|
case "before" /* BEFORE */: {
|
|
@@ -309,13 +328,9 @@ function handleOutcome(params, outcome) {
|
|
|
309
328
|
if (typeof lhs !== "string") {
|
|
310
329
|
return { value: defaultOutcome, outcomeType: "split" /* SPLIT */ };
|
|
311
330
|
}
|
|
312
|
-
const maxValue = 4294967295;
|
|
313
331
|
const value = hashInput(lhs, params.definition.seed);
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
(weight) => weight / sumOfWeights * maxValue
|
|
317
|
-
);
|
|
318
|
-
const variantIndex = findWeightedIndex(scaledWeights, value, maxValue);
|
|
332
|
+
const scaledWeights = getScaledWeights(outcome);
|
|
333
|
+
const variantIndex = findWeightedIndex(scaledWeights, value, UINT32_MAX);
|
|
319
334
|
return {
|
|
320
335
|
value: variantIndex === -1 ? defaultOutcome : getVariant(params.definition.variants, variantIndex),
|
|
321
336
|
outcomeType: "split" /* SPLIT */
|
|
@@ -371,9 +386,8 @@ function handleOutcome(params, outcome) {
|
|
|
371
386
|
outcomeType: "rollout" /* ROLLOUT */
|
|
372
387
|
};
|
|
373
388
|
}
|
|
374
|
-
const maxValue = 4294967295;
|
|
375
389
|
const value = hashInput(lhs, params.definition.seed);
|
|
376
|
-
const threshold = currentPromille / 1e5 *
|
|
390
|
+
const threshold = currentPromille / 1e5 * UINT32_MAX;
|
|
377
391
|
return {
|
|
378
392
|
value: value < threshold ? getVariant(params.definition.variants, outcome.rollToVariant) : getVariant(
|
|
379
393
|
params.definition.variants,
|
|
@@ -440,6 +454,23 @@ function evaluate(params, _visited) {
|
|
|
440
454
|
reason: "fallthrough" /* FALLTHROUGH */
|
|
441
455
|
});
|
|
442
456
|
}
|
|
457
|
+
function bulkEvaluate(flags, shared) {
|
|
458
|
+
const params = {
|
|
459
|
+
entities: shared.entities,
|
|
460
|
+
environment: shared.environment,
|
|
461
|
+
segments: shared.segments,
|
|
462
|
+
definition: void 0,
|
|
463
|
+
defaultValue: void 0
|
|
464
|
+
};
|
|
465
|
+
const results = {};
|
|
466
|
+
for (const key in flags) {
|
|
467
|
+
const flag = flags[key];
|
|
468
|
+
params.definition = flag.definition;
|
|
469
|
+
params.defaultValue = flag.defaultValue;
|
|
470
|
+
results[key] = evaluate(params);
|
|
471
|
+
}
|
|
472
|
+
return results;
|
|
473
|
+
}
|
|
443
474
|
function findWeightedIndex(weights, value, maxValue) {
|
|
444
475
|
if (value < 0 || value >= maxValue) return -1;
|
|
445
476
|
let sum2 = 0;
|
|
@@ -451,7 +482,7 @@ function findWeightedIndex(weights, value, maxValue) {
|
|
|
451
482
|
}
|
|
452
483
|
|
|
453
484
|
// package.json
|
|
454
|
-
var version = "1.
|
|
485
|
+
var version = "1.5.0-581a9c5-20260525125622";
|
|
455
486
|
|
|
456
487
|
// src/lib/report-value.ts
|
|
457
488
|
function internalReportValue(key, value, data) {
|
|
@@ -557,6 +588,78 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
|
|
|
557
588
|
}
|
|
558
589
|
});
|
|
559
590
|
}
|
|
591
|
+
async function bulkEvaluate2(id, flags, entities) {
|
|
592
|
+
const controller = getInstance(id).controller;
|
|
593
|
+
let datafile;
|
|
594
|
+
try {
|
|
595
|
+
datafile = await controller.read();
|
|
596
|
+
} catch (error) {
|
|
597
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to read datafile";
|
|
598
|
+
const results2 = {};
|
|
599
|
+
for (const flag of flags) {
|
|
600
|
+
results2[flag.key] = {
|
|
601
|
+
value: flag.defaultValue,
|
|
602
|
+
reason: "error" /* ERROR */,
|
|
603
|
+
errorMessage
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return results2;
|
|
607
|
+
}
|
|
608
|
+
const baseMetrics = {
|
|
609
|
+
readMs: datafile.metrics.readMs,
|
|
610
|
+
source: datafile.metrics.source,
|
|
611
|
+
cacheStatus: datafile.metrics.cacheStatus,
|
|
612
|
+
connectionState: datafile.metrics.connectionState,
|
|
613
|
+
mode: datafile.metrics.mode
|
|
614
|
+
};
|
|
615
|
+
const projectId = datafile.projectId;
|
|
616
|
+
const results = {};
|
|
617
|
+
const toEvaluate = {};
|
|
618
|
+
for (const flag of flags) {
|
|
619
|
+
const { key, defaultValue } = flag;
|
|
620
|
+
const flagDefinition = datafile.definitions[key];
|
|
621
|
+
if (flagDefinition === void 0) {
|
|
622
|
+
if (projectId) {
|
|
623
|
+
internalReportValue(key, defaultValue, {
|
|
624
|
+
originProjectId: projectId,
|
|
625
|
+
originProvider: "vercel",
|
|
626
|
+
reason: "error" /* ERROR */
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
results[key] = {
|
|
630
|
+
value: defaultValue,
|
|
631
|
+
reason: "error" /* ERROR */,
|
|
632
|
+
errorCode: "FLAG_NOT_FOUND" /* FLAG_NOT_FOUND */,
|
|
633
|
+
errorMessage: `@vercel/flags-core: Definition not found for flag "${key}"`,
|
|
634
|
+
metrics: { evaluationMs: 0, ...baseMetrics }
|
|
635
|
+
};
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
toEvaluate[key] = { definition: flagDefinition, defaultValue };
|
|
639
|
+
}
|
|
640
|
+
const evalStartTime = Date.now();
|
|
641
|
+
const evaluated = bulkEvaluate(toEvaluate, {
|
|
642
|
+
entities: entities ?? {},
|
|
643
|
+
environment: datafile.environment,
|
|
644
|
+
segments: datafile.segments
|
|
645
|
+
});
|
|
646
|
+
const evaluationDurationMs = Date.now() - evalStartTime;
|
|
647
|
+
for (const key in toEvaluate) {
|
|
648
|
+
const result = evaluated[key];
|
|
649
|
+
if (projectId) {
|
|
650
|
+
internalReportValue(key, result.value, {
|
|
651
|
+
originProjectId: projectId,
|
|
652
|
+
originProvider: "vercel",
|
|
653
|
+
reason: result.reason,
|
|
654
|
+
outcomeType: result.reason !== "error" /* ERROR */ ? result.outcomeType : void 0
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
results[key] = Object.assign(result, {
|
|
658
|
+
metrics: { evaluationMs: evaluationDurationMs, ...baseMetrics }
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
return results;
|
|
662
|
+
}
|
|
560
663
|
|
|
561
664
|
// src/create-raw-client.ts
|
|
562
665
|
var idCount = 0;
|
|
@@ -623,6 +726,16 @@ function createCreateRawClient(fns) {
|
|
|
623
726
|
}
|
|
624
727
|
}
|
|
625
728
|
return fns.evaluate(id, flagKey, defaultValue, entities);
|
|
729
|
+
},
|
|
730
|
+
bulkEvaluate: async (flags, entities) => {
|
|
731
|
+
const instance = controllerInstanceMap.get(id);
|
|
732
|
+
if (!instance?.initialized) {
|
|
733
|
+
try {
|
|
734
|
+
await api.initialize();
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return fns.bulkEvaluate(id, flags, entities);
|
|
626
739
|
}
|
|
627
740
|
};
|
|
628
741
|
return api;
|
|
@@ -677,6 +790,23 @@ async function readBundledDefinitions(sdkKey) {
|
|
|
677
790
|
|
|
678
791
|
// src/utils/usage-tracker.ts
|
|
679
792
|
import { waitUntil } from "@vercel/functions";
|
|
793
|
+
|
|
794
|
+
// src/utils/backoff.ts
|
|
795
|
+
var DEFAULT_BASE_MS = 250;
|
|
796
|
+
var DEFAULT_CAP_MS = 5e3;
|
|
797
|
+
function getRetryDelayMs(attempt, options = {}) {
|
|
798
|
+
const baseMs = options.baseMs ?? DEFAULT_BASE_MS;
|
|
799
|
+
const capMs = options.capMs ?? DEFAULT_CAP_MS;
|
|
800
|
+
const ceiling = Math.min(capMs, baseMs * 2 ** Math.max(0, attempt - 1));
|
|
801
|
+
return Math.floor(Math.random() * ceiling);
|
|
802
|
+
}
|
|
803
|
+
function getJitteredWaitMs(baseMs, ratio) {
|
|
804
|
+
const min = baseMs * (1 - ratio);
|
|
805
|
+
const span = baseMs * 2 * ratio;
|
|
806
|
+
return Math.floor(min + Math.random() * span);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/utils/usage-tracker.ts
|
|
680
810
|
var RESOLVED_VOID = Promise.resolve();
|
|
681
811
|
var isDebugMode = process.env.DEBUG?.includes("@vercel/flags-core");
|
|
682
812
|
var debugLog = (...args) => {
|
|
@@ -686,6 +816,7 @@ var debugLog = (...args) => {
|
|
|
686
816
|
var MAX_RETRIES = 3;
|
|
687
817
|
var MAX_BATCH_SIZE = 50;
|
|
688
818
|
var MAX_BATCH_WAIT_MS = 5e3;
|
|
819
|
+
var BATCH_WAIT_JITTER_RATIO = 0.2;
|
|
689
820
|
var SYMBOL_FOR_REQ_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context");
|
|
690
821
|
var fromSymbol = globalThis;
|
|
691
822
|
function getRequestContext() {
|
|
@@ -792,7 +923,10 @@ var UsageTracker = class {
|
|
|
792
923
|
const pending = (async () => {
|
|
793
924
|
await new Promise((res) => {
|
|
794
925
|
this.batcher.resolveWait = res;
|
|
795
|
-
timeout = setTimeout(
|
|
926
|
+
timeout = setTimeout(
|
|
927
|
+
res,
|
|
928
|
+
getJitteredWaitMs(MAX_BATCH_WAIT_MS, BATCH_WAIT_JITTER_RATIO)
|
|
929
|
+
);
|
|
796
930
|
});
|
|
797
931
|
this.batcher.pending = null;
|
|
798
932
|
this.batcher.resolveWait = null;
|
|
@@ -846,7 +980,12 @@ Response body: ${await response.text().catch(() => null)}`
|
|
|
846
980
|
error
|
|
847
981
|
);
|
|
848
982
|
if (attempt < MAX_RETRIES) {
|
|
849
|
-
|
|
983
|
+
const delayMs = getRetryDelayMs(attempt);
|
|
984
|
+
await new Promise((res) => setTimeout(res, delayMs));
|
|
985
|
+
} else {
|
|
986
|
+
console.error(
|
|
987
|
+
`@vercel/flags-core: Dropped ${eventsToSend.length} events after ${MAX_RETRIES} attempts (flushId=${flushId})`
|
|
988
|
+
);
|
|
850
989
|
}
|
|
851
990
|
}
|
|
852
991
|
}
|
|
@@ -1384,6 +1523,11 @@ var Controller = class {
|
|
|
1384
1523
|
state = "idle";
|
|
1385
1524
|
// Data state — tagged with origin
|
|
1386
1525
|
data;
|
|
1526
|
+
// Memoized data spread for read() / getDatafile().
|
|
1527
|
+
// Rebuilt only when `this.data` reference changes (e.g. on stream/poll update).
|
|
1528
|
+
// Holds the result of stripping `_origin`; metrics are appended per-call.
|
|
1529
|
+
dataViewSource = void 0;
|
|
1530
|
+
dataViewBase = void 0;
|
|
1387
1531
|
// Sources (I/O delegates)
|
|
1388
1532
|
streamSource;
|
|
1389
1533
|
pollingSource;
|
|
@@ -1555,11 +1699,15 @@ var Controller = class {
|
|
|
1555
1699
|
this.isFirstGetData = false;
|
|
1556
1700
|
const [result, cacheStatus] = await this.resolveData();
|
|
1557
1701
|
const readMs = Date.now() - startTime;
|
|
1558
|
-
const
|
|
1559
|
-
const source = originToMetricsSource(_origin);
|
|
1702
|
+
const source = originToMetricsSource(result._origin);
|
|
1560
1703
|
this.trackRead(startTime, cacheHadDefinitions, isFirstRead, source);
|
|
1704
|
+
if (this.dataViewSource !== result) {
|
|
1705
|
+
const { _origin, ...rest } = result;
|
|
1706
|
+
this.dataViewBase = rest;
|
|
1707
|
+
this.dataViewSource = result;
|
|
1708
|
+
}
|
|
1561
1709
|
return {
|
|
1562
|
-
...
|
|
1710
|
+
...this.dataViewBase,
|
|
1563
1711
|
metrics: {
|
|
1564
1712
|
readMs,
|
|
1565
1713
|
source,
|
|
@@ -1619,8 +1767,13 @@ var Controller = class {
|
|
|
1619
1767
|
}
|
|
1620
1768
|
}
|
|
1621
1769
|
const source = originToMetricsSource(result._origin);
|
|
1770
|
+
if (this.dataViewSource !== result) {
|
|
1771
|
+
const { _origin, ...rest } = result;
|
|
1772
|
+
this.dataViewBase = rest;
|
|
1773
|
+
this.dataViewSource = result;
|
|
1774
|
+
}
|
|
1622
1775
|
return {
|
|
1623
|
-
...
|
|
1776
|
+
...this.dataViewBase,
|
|
1624
1777
|
metrics: {
|
|
1625
1778
|
readMs: Date.now() - startTime,
|
|
1626
1779
|
source,
|
|
@@ -2045,4 +2198,4 @@ export {
|
|
|
2045
2198
|
resetDefaultFlagsClient,
|
|
2046
2199
|
createClient
|
|
2047
2200
|
};
|
|
2048
|
-
//# sourceMappingURL=chunk-
|
|
2201
|
+
//# sourceMappingURL=chunk-IMGERCR6.js.map
|