@vercel/flags-core 1.6.0 → 1.7.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @vercel/flags-core
2
2
 
3
+ ## 1.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#427](https://github.com/vercel/flags/pull/427) [`50b1aa4`](https://github.com/vercel/flags/commit/50b1aa4f9614b8e78f76cc9e3ae539e58a46fa7e) Thanks [@dferber90](https://github.com/dferber90)! - Align rollout, split, and segment split user assignment onto a single hash bucketing scheme.
8
+
9
+ Previously splits and rollouts bucketed users with opposite conventions, so switching a flag between a split and a rollout (or locking in a rollout as a split) could reassign users even when the effective distribution was unchanged. All three now derive their cut points from one shared boundary function over the full hash space, so a rollout at a given percentage is identical to the equivalent split.
10
+
11
+ Split assignments are effectively unchanged. Rollouts and segment splits are re-bucketed once with this release; after that, converting a flag between outcome types never reassigns anyone.
12
+
3
13
  ## 1.6.0
4
14
 
5
15
  ### Minor Changes
@@ -32,16 +32,25 @@ var Packed;
32
32
 
33
33
  // src/evaluate.ts
34
34
  var MAX_REGEX_INPUT_LENGTH = 1e4;
35
- var UINT32_MAX = 4294967295;
36
- var scaledWeightsCache = /* @__PURE__ */ new WeakMap();
35
+ var HASH_SPACE = 2 ** 32;
36
+ var PROMILLE_SCALE = 1e5;
37
+ function boundaryFor(numerator, denominator) {
38
+ return Math.floor(numerator / denominator * HASH_SPACE);
39
+ }
40
+ var splitBoundariesCache = /* @__PURE__ */ new WeakMap();
37
41
  var compiledRegexCache = /* @__PURE__ */ new WeakMap();
38
- function getScaledWeights(outcome) {
39
- const cached = scaledWeightsCache.get(outcome);
42
+ function getSplitBoundaries(outcome) {
43
+ const cached = splitBoundariesCache.get(outcome);
40
44
  if (cached) return cached;
41
45
  const total = sum(outcome.weights);
42
- const scaled = outcome.weights.map((w) => w / total * UINT32_MAX);
43
- scaledWeightsCache.set(outcome, scaled);
44
- return scaled;
46
+ const boundaries = [];
47
+ let cumulative = 0;
48
+ for (const weight of outcome.weights) {
49
+ cumulative += weight;
50
+ boundaries.push(boundaryFor(cumulative, total));
51
+ }
52
+ splitBoundariesCache.set(outcome, boundaries);
53
+ return boundaries;
45
54
  }
46
55
  function getCompiledRegex(rhs) {
47
56
  const cached = compiledRegexCache.get(rhs);
@@ -276,11 +285,10 @@ function handleSegmentOutcome(params, outcome) {
276
285
  case "split": {
277
286
  const lhs = access(outcome.base, params);
278
287
  if (typeof lhs !== "string") return false;
279
- const maxValue = 1e5;
280
288
  if (outcome.passPromille <= 0) return false;
281
- if (outcome.passPromille >= maxValue) return true;
282
- const value = hashInput(lhs, params.definition.seed) % maxValue;
283
- return value < outcome.passPromille;
289
+ if (outcome.passPromille >= PROMILLE_SCALE) return true;
290
+ const bucket = hashInput(lhs, params.definition.seed);
291
+ return bucket < boundaryFor(outcome.passPromille, PROMILLE_SCALE);
284
292
  }
285
293
  default: {
286
294
  const { type } = outcome;
@@ -325,12 +333,18 @@ function handleOutcome(params, outcome) {
325
333
  outcomeType: "split" /* SPLIT */
326
334
  };
327
335
  }
328
- const value = hashInput(lhs, params.definition.seed);
329
- const scaledWeights = getScaledWeights(outcome);
330
- const variantIndex = findWeightedIndex(scaledWeights, value, UINT32_MAX);
331
- const variant = variantIndex === -1 ? defaultOutcome : getVariant(params.definition, variantIndex);
336
+ const bucket = hashInput(lhs, params.definition.seed);
337
+ const boundaries = getSplitBoundaries(outcome);
338
+ for (let index = 0; index < boundaries.length; index++) {
339
+ if (bucket < boundaries[index]) {
340
+ return {
341
+ ...getVariant(params.definition, index),
342
+ outcomeType: "split" /* SPLIT */
343
+ };
344
+ }
345
+ }
332
346
  return {
333
- ...variant,
347
+ ...defaultOutcome,
334
348
  outcomeType: "split" /* SPLIT */
335
349
  };
336
350
  }
@@ -366,7 +380,7 @@ function handleOutcome(params, outcome) {
366
380
  break;
367
381
  }
368
382
  }
369
- if (exhausted) currentPromille = 1e5;
383
+ if (exhausted) currentPromille = PROMILLE_SCALE;
370
384
  if (currentPromille <= 0) {
371
385
  return {
372
386
  ...rollFromVariant,
@@ -377,17 +391,26 @@ function handleOutcome(params, outcome) {
377
391
  params.definition,
378
392
  outcome.rollToVariant
379
393
  );
380
- if (currentPromille >= 1e5) {
394
+ if (currentPromille >= PROMILLE_SCALE) {
381
395
  return {
382
396
  ...rollToVariant,
383
397
  outcomeType: "rollout" /* ROLLOUT */
384
398
  };
385
399
  }
386
- const value = hashInput(lhs, params.definition.seed);
387
- const threshold = currentPromille / 1e5 * UINT32_MAX;
388
- const variant = value < threshold ? rollToVariant : rollFromVariant;
400
+ const bucket = hashInput(lhs, params.definition.seed);
401
+ if (outcome.rollToVariant < outcome.rollFromVariant) {
402
+ const rollToBoundary = boundaryFor(currentPromille, PROMILLE_SCALE);
403
+ return {
404
+ ...bucket < rollToBoundary ? rollToVariant : rollFromVariant,
405
+ outcomeType: "rollout" /* ROLLOUT */
406
+ };
407
+ }
408
+ const rollFromBoundary = boundaryFor(
409
+ PROMILLE_SCALE - currentPromille,
410
+ PROMILLE_SCALE
411
+ );
389
412
  return {
390
- ...variant,
413
+ ...bucket < rollFromBoundary ? rollFromVariant : rollToVariant,
391
414
  outcomeType: "rollout" /* ROLLOUT */
392
415
  };
393
416
  }
@@ -468,18 +491,9 @@ function bulkEvaluate(flags, shared) {
468
491
  }
469
492
  return results;
470
493
  }
471
- function findWeightedIndex(weights, value, maxValue) {
472
- if (value < 0 || value >= maxValue) return -1;
473
- let sum2 = 0;
474
- for (let i = 0; i < weights.length; i++) {
475
- sum2 += weights[i];
476
- if (value < sum2) return i;
477
- }
478
- return -1;
479
- }
480
494
 
481
495
  // package.json
482
- var version = "1.6.0";
496
+ var version = "1.7.0";
483
497
 
484
498
  // src/lib/report-value.ts
485
499
  function internalReportValue(key, value, data) {
@@ -2490,4 +2504,4 @@ export {
2490
2504
  resetDefaultFlagsClient,
2491
2505
  createClient
2492
2506
  };
2493
- //# sourceMappingURL=chunk-L653ZWQA.js.map
2507
+ //# sourceMappingURL=chunk-3QHXSGEW.js.map