@vercel/flags-core 1.8.2 → 1.8.3

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,13 @@
1
1
  # @vercel/flags-core
2
2
 
3
+ ## 1.8.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#507](https://github.com/vercel/flags/pull/507) [`c43b9d9`](https://github.com/vercel/flags/commit/c43b9d9076009a6a70ca2de55020ebcb15b1a4cc) Thanks [@luismeyer](https://github.com/luismeyer)! - Retry transient datafile fetch failures across polling, build loading, and offline fallback reads. Fetches use up to three attempts within a shared ten-second deadline that includes authentication, backoff, and body parsing. Shutdown also cancels retries during polling initialization.
8
+
9
+ - [#533](https://github.com/vercel/flags/pull/533) [`7027cb2`](https://github.com/vercel/flags/commit/7027cb271183566ee224c6a96d36cb12ce7cf2fa) Thanks [@dferber90](https://github.com/dferber90)! - Strip all occurrences of the `g` and `y` flags from regex conditions so cached regular expressions produce consistent results across users and repeated evaluations, including when both flags are present.
10
+
3
11
  ## 1.8.2
4
12
 
5
13
  ### Patch Changes
@@ -73,7 +73,7 @@ function getSplitBoundaries(outcome) {
73
73
  function getCompiledRegex(rhs) {
74
74
  const cached = compiledRegexCache.get(rhs);
75
75
  if (cached) return cached;
76
- const compiled = new RegExp(rhs.pattern, rhs.flags.replace(/[gy]/, ""));
76
+ const compiled = new RegExp(rhs.pattern, rhs.flags.replace(/[gy]/g, ""));
77
77
  compiledRegexCache.set(rhs, compiled);
78
78
  return compiled;
79
79
  }
@@ -576,7 +576,7 @@ function bulkEvaluate(flags, shared) {
576
576
  }
577
577
 
578
578
  // package.json
579
- var version = "1.8.2";
579
+ var version = "1.8.3";
580
580
 
581
581
  // src/lib/report-value.ts
582
582
  function internalReportValue(key, value, data) {
@@ -1526,40 +1526,86 @@ var BundledSource = class {
1526
1526
 
1527
1527
  // src/controller/fetch-datafile.ts
1528
1528
  var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
1529
+ var DEFAULT_MAX_ATTEMPTS = 3;
1530
+ var DatafileHttpError = class extends Error {
1531
+ constructor(status, statusText) {
1532
+ super(`Failed to fetch data: ${statusText}`);
1533
+ this.status = status;
1534
+ }
1535
+ status;
1536
+ get retryable() {
1537
+ return this.status === 408 || this.status === 429 || this.status >= 500;
1538
+ }
1539
+ };
1529
1540
  async function fetchDatafile(options) {
1530
- const token = await options.auth.resolveToken();
1541
+ options.signal?.throwIfAborted();
1542
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
1543
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
1544
+ throw new Error("maxAttempts must be a positive integer");
1545
+ }
1531
1546
  const controller = new AbortController();
1547
+ const { signal } = controller;
1548
+ let rejectAbort;
1549
+ const aborted = new Promise((_, reject) => {
1550
+ rejectAbort = reject;
1551
+ });
1552
+ const onAbort = () => rejectAbort(signal.reason);
1553
+ signal.addEventListener("abort", onAbort, { once: true });
1554
+ const onExternalAbort = () => controller.abort(options.signal?.reason);
1555
+ options.signal?.addEventListener("abort", onExternalAbort, { once: true });
1532
1556
  const timeoutId = setTimeout(
1533
- () => controller.abort(),
1557
+ () => controller.abort(
1558
+ new Error("@vercel/flags-core: Datafile fetch deadline exceeded")
1559
+ ),
1534
1560
  DEFAULT_FETCH_TIMEOUT_MS
1535
1561
  );
1536
- const onExternalAbort = () => controller.abort();
1537
- if (options.signal) {
1538
- if (options.signal.aborted) {
1539
- clearTimeout(timeoutId);
1540
- throw new Error("Fetch aborted");
1541
- }
1542
- options.signal.addEventListener("abort", onExternalAbort, { once: true });
1543
- }
1544
- try {
1562
+ let delay;
1563
+ const fetchAttempt = async () => {
1564
+ const token = await options.auth.resolveToken();
1565
+ signal.throwIfAborted();
1545
1566
  const res = await options.fetch(`${options.host}/v1/datafile`, {
1546
1567
  headers: {
1547
1568
  Authorization: `Bearer ${token}`,
1548
1569
  "User-Agent": `VercelFlagsCore/${version}`,
1549
1570
  ...process.env.VERCEL_ENV ? { "X-Vercel-Env": process.env.VERCEL_ENV } : null
1550
1571
  },
1551
- signal: controller.signal
1572
+ signal
1552
1573
  });
1553
- clearTimeout(timeoutId);
1554
- options.signal?.removeEventListener("abort", onExternalAbort);
1574
+ signal.throwIfAborted();
1555
1575
  if (!res.ok) {
1556
- throw new Error(`Failed to fetch data: ${res.statusText}`);
1576
+ void res.body?.cancel().catch(() => {
1577
+ });
1578
+ throw new DatafileHttpError(res.status, res.statusText);
1557
1579
  }
1558
- return res.json();
1559
- } catch (error) {
1580
+ const data = await res.json();
1581
+ signal.throwIfAborted();
1582
+ return data;
1583
+ };
1584
+ try {
1585
+ for (let attempt = 0; ; attempt++) {
1586
+ if (attempt > 0) {
1587
+ await Promise.race([
1588
+ new Promise((resolve) => {
1589
+ delay = setTimeout(resolve, 100 * 2 ** (attempt - 1));
1590
+ }),
1591
+ aborted
1592
+ ]);
1593
+ }
1594
+ signal.throwIfAborted();
1595
+ try {
1596
+ return await Promise.race([fetchAttempt(), aborted]);
1597
+ } catch (error) {
1598
+ signal.throwIfAborted();
1599
+ if (attempt === maxAttempts - 1 || error instanceof DatafileHttpError && !error.retryable) {
1600
+ throw error instanceof Error ? error : new Error("Unknown fetch error");
1601
+ }
1602
+ }
1603
+ }
1604
+ } finally {
1560
1605
  clearTimeout(timeoutId);
1606
+ clearTimeout(delay);
1607
+ signal.removeEventListener("abort", onAbort);
1561
1608
  options.signal?.removeEventListener("abort", onExternalAbort);
1562
- throw error instanceof Error ? error : new Error("Unknown fetch error");
1563
1609
  }
1564
1610
  }
1565
1611
 
@@ -1655,13 +1701,16 @@ var PollingSource = class extends TypedEmitter {
1655
1701
  */
1656
1702
  async poll() {
1657
1703
  if (this.abortController?.signal.aborted) return;
1704
+ this.abortController ??= new AbortController();
1705
+ const controller = this.abortController;
1658
1706
  try {
1659
1707
  const data = await fetchDatafile({
1660
1708
  ...this.config,
1661
- signal: this.abortController?.signal
1709
+ signal: controller.signal
1662
1710
  });
1663
1711
  this.emit("data", data);
1664
1712
  } catch (error) {
1713
+ controller.signal.throwIfAborted();
1665
1714
  const err = error instanceof Error ? error : new Error("Unknown poll error");
1666
1715
  this.emit("error", err);
1667
1716
  }
@@ -1673,9 +1722,9 @@ var PollingSource = class extends TypedEmitter {
1673
1722
  */
1674
1723
  startInterval() {
1675
1724
  if (this.intervalId) return;
1676
- this.abortController = new AbortController();
1677
1725
  this.intervalId = setInterval(
1678
- () => void this.poll(),
1726
+ () => void this.poll().catch(() => {
1727
+ }),
1679
1728
  this.config.polling.intervalMs
1680
1729
  );
1681
1730
  }
@@ -2750,4 +2799,4 @@ export {
2750
2799
  resetDefaultFlagsClient,
2751
2800
  createClient
2752
2801
  };
2753
- //# sourceMappingURL=chunk-AMDRNXFQ.js.map
2802
+ //# sourceMappingURL=chunk-NPKDNJX6.js.map