@peerbit/shared-log 16.0.6 → 16.0.7

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.
@@ -1373,13 +1373,13 @@ export const calculateCoverage = async (properties) => {
1373
1373
  return minCoverage === Infinity || minCoverage <= 0 ? 0 : minCoverage;
1374
1374
  };
1375
1375
  const getClosest = (direction, rects, point, includeStrict, numbers, options) => {
1376
- const createQueries = (p, equality) => {
1376
+ const createQueries = (p, phase) => {
1377
1377
  let queries;
1378
1378
  if (direction === "below") {
1379
1379
  queries = [
1380
1380
  new IntegerCompare({
1381
1381
  key: "end2",
1382
- compare: equality ? Compare.LessOrEqual : Compare.Less,
1382
+ compare: phase === "primary" ? Compare.LessOrEqual : Compare.Greater,
1383
1383
  value: p,
1384
1384
  }),
1385
1385
  ];
@@ -1388,7 +1388,7 @@ const getClosest = (direction, rects, point, includeStrict, numbers, options) =>
1388
1388
  queries = [
1389
1389
  new IntegerCompare({
1390
1390
  key: "start1",
1391
- compare: equality ? Compare.GreaterOrEqual : Compare.Greater,
1391
+ compare: phase === "primary" ? Compare.GreaterOrEqual : Compare.Less,
1392
1392
  value: p,
1393
1393
  }),
1394
1394
  ];
@@ -1413,12 +1413,18 @@ const getClosest = (direction, rects, point, includeStrict, numbers, options) =>
1413
1413
  if (options?.hash) {
1414
1414
  queries.push(new StringMatch({ key: "hash", value: options.hash }));
1415
1415
  }
1416
+ if (options?.excludeId) {
1417
+ queries.push(new Not(new ByteMatchQuery({
1418
+ key: "id",
1419
+ value: options.excludeId,
1420
+ })));
1421
+ }
1416
1422
  return queries;
1417
1423
  };
1418
1424
  const sortByOldest = new Sort({ key: "timestamp", direction: "asc" });
1419
1425
  const sortByHash = new Sort({ key: "hash", direction: "asc" }); // when breaking even
1420
1426
  const iterator = rects.iterate({
1421
- query: createQueries(point, false),
1427
+ query: createQueries(point, "primary"),
1422
1428
  sort: [
1423
1429
  direction === "below"
1424
1430
  ? new Sort({ key: ["end2"], direction: "desc" })
@@ -1428,7 +1434,7 @@ const getClosest = (direction, rects, point, includeStrict, numbers, options) =>
1428
1434
  ],
1429
1435
  }, options);
1430
1436
  const iteratorWrapped = rects.iterate({
1431
- query: createQueries(direction === "below" ? numbers.maxValue : numbers.zero, true),
1437
+ query: createQueries(point, "wrapped"),
1432
1438
  sort: [
1433
1439
  direction === "below"
1434
1440
  ? new Sort({ key: ["end2"], direction: "desc" })
@@ -1531,10 +1537,10 @@ export const getCoveringRangesSameOwner = (rects, range) => {
1531
1537
  export function getDistance(from, to, direction, end) {
1532
1538
  const abs = (value) => value < 0 ? -value : value;
1533
1539
  const diff = (a, b) => abs(a - b);
1540
+ if (from === to) {
1541
+ return typeof from === "number" ? 0 : 0n;
1542
+ }
1534
1543
  if (direction === "closest") {
1535
- if (from === to) {
1536
- return typeof from === "number" ? 0 : 0n; // returns 0 of the correct type
1537
- }
1538
1544
  return diff(from, to) < diff(end, diff(from, to))
1539
1545
  ? diff(from, to)
1540
1546
  : diff(end, diff(from, to));
@@ -1553,9 +1559,43 @@ export function getDistance(from, to, direction, end) {
1553
1559
  }
1554
1560
  throw new Error("Invalid direction");
1555
1561
  }
1556
- const joinIterator = (iterators, point, direction, numbers) => {
1562
+ /** @internal */
1563
+ export const joinIterator = (iterators, point, direction, numbers) => {
1564
+ const compareQueuedResults = (left, right) => {
1565
+ if (left.dist < right.dist)
1566
+ return -1;
1567
+ if (left.dist > right.dist)
1568
+ return 1;
1569
+ // Match the native closest-range total order. The shaped iterator users do
1570
+ // not always project every tie field, so only compare a field when both
1571
+ // results carry it; getSamples uses complete ranges and therefore always
1572
+ // follows distance, timestamp, owner hash, then range id.
1573
+ const leftValue = left.result.value;
1574
+ const rightValue = right.result.value;
1575
+ if (leftValue.timestamp != null && rightValue.timestamp != null) {
1576
+ if (leftValue.timestamp < rightValue.timestamp)
1577
+ return -1;
1578
+ if (leftValue.timestamp > rightValue.timestamp)
1579
+ return 1;
1580
+ }
1581
+ if (leftValue.hash != null && rightValue.hash != null) {
1582
+ if (leftValue.hash < rightValue.hash)
1583
+ return -1;
1584
+ if (leftValue.hash > rightValue.hash)
1585
+ return 1;
1586
+ }
1587
+ const leftId = leftValue.id == null ? undefined : toBase64(leftValue.id);
1588
+ const rightId = rightValue.id == null ? undefined : toBase64(rightValue.id);
1589
+ if (leftId != null && rightId != null) {
1590
+ if (leftId < rightId)
1591
+ return -1;
1592
+ if (leftId > rightId)
1593
+ return 1;
1594
+ }
1595
+ return 0;
1596
+ };
1557
1597
  let queues = [];
1558
- return {
1598
+ const joined = {
1559
1599
  next: async (count) => {
1560
1600
  let results = [];
1561
1601
  for (let i = 0; i < iterators.length; i++) {
@@ -1589,13 +1629,14 @@ const joinIterator = (iterators, point, direction, numbers) => {
1589
1629
  // pull the 'count' the closest element from one of the queue
1590
1630
  for (let i = 0; i < count; i++) {
1591
1631
  let closestQueue = -1;
1592
- let closestDist = Number.MAX_VALUE;
1632
+ let selected;
1593
1633
  for (let j = 0; j < queues.length; j++) {
1594
1634
  let queue = queues[j];
1595
1635
  if (queue && queue.elements.length > 0) {
1596
- let closest = queue.elements[0];
1597
- if (closest.dist < closestDist) {
1598
- closestDist = closest.dist;
1636
+ const candidate = queue.elements[0];
1637
+ if (candidate &&
1638
+ (!selected || compareQueuedResults(candidate, selected) < 0)) {
1639
+ selected = candidate;
1599
1640
  closestQueue = j;
1600
1641
  }
1601
1642
  }
@@ -1603,7 +1644,7 @@ const joinIterator = (iterators, point, direction, numbers) => {
1603
1644
  if (closestQueue === -1) {
1604
1645
  break;
1605
1646
  }
1606
- let closest = queues[closestQueue]?.elements.shift();
1647
+ const closest = queues[closestQueue]?.elements.shift();
1607
1648
  if (closest) {
1608
1649
  results.push(closest.result);
1609
1650
  }
@@ -1612,23 +1653,33 @@ const joinIterator = (iterators, point, direction, numbers) => {
1612
1653
  },
1613
1654
  pending: async () => {
1614
1655
  let allPending = await Promise.all(iterators.map((x) => x.pending()));
1615
- return allPending.reduce((acc, x) => acc + x, 0);
1656
+ // A child can be exhausted while this join still owns prefetched rows.
1657
+ // Counting local queues makes the same contract compose through nested joins.
1658
+ const buffered = queues.reduce((acc, queue) => acc + queue.elements.length, 0);
1659
+ return buffered + allPending.reduce((acc, x) => acc + x, 0);
1616
1660
  },
1617
- done: () => iterators.every((x) => x.done() === true),
1661
+ done: () => queues.every((queue) => queue.elements.length === 0) &&
1662
+ iterators.every((x) => x.done() === true),
1618
1663
  close: async () => {
1619
- for (const iterator of iterators) {
1620
- await iterator.close();
1664
+ queues.length = 0;
1665
+ const settled = await Promise.allSettled(iterators.map(async (iterator) => iterator.close()));
1666
+ const errors = settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1667
+ if (errors.length === 1) {
1668
+ throw errors[0];
1669
+ }
1670
+ if (errors.length > 1) {
1671
+ throw new AggregateError(errors, "Failed to close joined iterators");
1621
1672
  }
1622
1673
  },
1623
1674
  all: async () => {
1624
1675
  let results = [];
1625
- for (const iterator of iterators) {
1626
- let res = await iterator.all();
1627
- results.push(...res);
1676
+ while (!joined.done()) {
1677
+ results.push(...(await joined.next(100)));
1628
1678
  }
1629
1679
  return results;
1630
1680
  },
1631
1681
  };
1682
+ return joined;
1632
1683
  };
1633
1684
  const getClosestAroundOrContaining = (peers, point, includeStrictBelow, includeStrictAbove, numbers, options) => {
1634
1685
  const closestBelow = getClosest("below", peers, point, includeStrictBelow, numbers, options);
@@ -1639,23 +1690,24 @@ const getClosestAroundOrContaining = (peers, point, includeStrictBelow, includeS
1639
1690
  export const getAdjecentSameOwner = async (peers, range, numbers) => {
1640
1691
  const closestBelowIterator = getClosest("below", peers, range.start1, true, numbers, {
1641
1692
  hash: range.hash,
1693
+ excludeId: range.id,
1642
1694
  });
1643
1695
  const closestBelow = await closestBelowIterator.next(1);
1644
1696
  closestBelowIterator.close();
1645
1697
  const closestAboveIterator = getClosest("above", peers, range.end2, true, numbers, {
1646
1698
  hash: range.hash,
1699
+ excludeId: range.id,
1647
1700
  });
1648
1701
  const closestAbove = await closestAboveIterator.next(1);
1649
1702
  closestAboveIterator.close();
1650
- return {
1651
- below: range.idString === closestBelow[0]?.value.idString
1652
- ? undefined
1653
- : closestBelow[0]?.value,
1654
- above: closestBelow[0]?.id.primitive === closestAbove[0]?.id.primitive ||
1655
- range.idString === closestBelow[0]?.value.idString
1656
- ? undefined
1657
- : closestAbove[0]?.value,
1658
- };
1703
+ const below = range.idString === closestBelow[0]?.value.idString
1704
+ ? undefined
1705
+ : closestBelow[0]?.value;
1706
+ const above = range.idString === closestAbove[0]?.value.idString ||
1707
+ below?.idString === closestAbove[0]?.value.idString
1708
+ ? undefined
1709
+ : closestAbove[0]?.value;
1710
+ return { below, above };
1659
1711
  };
1660
1712
  export const getAllMergeCandiates = async (peers, range, numbers) => {
1661
1713
  const adjacent = await getAdjecentSameOwner(peers, range, numbers);
@@ -1706,7 +1758,7 @@ export const getSamples = async (cursor, peers, roleAge, numbers, options) => {
1706
1758
  return new Map();
1707
1759
  }
1708
1760
  const now = +new Date();
1709
- let matured = 0;
1761
+ const matureOwners = new Set();
1710
1762
  let uniqueVisited = new Set();
1711
1763
  const peerFilter = options?.peerFilter;
1712
1764
  for (let i = 0; i < cursor.length; i++) {
@@ -1718,10 +1770,10 @@ export const getSamples = async (cursor, peers, roleAge, numbers, options) => {
1718
1770
  }
1719
1771
  uniqueVisited.add(rect.value.hash);
1720
1772
  let prev = leaders.get(rect.value.hash);
1773
+ if (isMatured(rect.value, now, roleAge)) {
1774
+ matureOwners.add(rect.value.hash);
1775
+ }
1721
1776
  if (!prev) {
1722
- if (isMatured(rect.value, now, roleAge)) {
1723
- matured++;
1724
- }
1725
1777
  leaders.set(rect.value.hash, { intersecting: true });
1726
1778
  }
1727
1779
  else {
@@ -1734,7 +1786,7 @@ export const getSamples = async (cursor, peers, roleAge, numbers, options) => {
1734
1786
  break; // nothing more to find
1735
1787
  }
1736
1788
  }
1737
- if (options?.onlyIntersecting || matured > i) {
1789
+ if (options?.onlyIntersecting || matureOwners.size > i) {
1738
1790
  continue;
1739
1791
  }
1740
1792
  let foundOneUniqueMatured = false;
@@ -1745,11 +1797,11 @@ export const getSamples = async (cursor, peers, roleAge, numbers, options) => {
1745
1797
  uniqueVisited.add(rect.hash);
1746
1798
  const prev = leaders.get(rect.hash);
1747
1799
  if (m) {
1800
+ matureOwners.add(rect.hash);
1748
1801
  if (!prev) {
1749
- matured++;
1750
1802
  leaders.set(rect.hash, { intersecting: false });
1751
1803
  }
1752
- if (matured > i) {
1804
+ if (matureOwners.size > i) {
1753
1805
  foundOneUniqueMatured = true;
1754
1806
  }
1755
1807
  }
@@ -2195,44 +2247,78 @@ const replicationRangeGeometryKey = (range) => toSegmentsBigInt(range)
2195
2247
  // margin below SQLite's default expression-depth limit (1000), including the
2196
2248
  // nested comparisons needed for wrapped ranges.
2197
2249
  const MAX_REBALANCE_QUERY_RANGES = 128;
2198
- const createRebalanceQueryPlan = (changes, includeBoundaryWhenEmpty) => {
2199
- const boundarySource = changes.find((change) => change.rebalanceBoundaryOnly ||
2200
- change.range.mode === ReplicationIntent.NonStrict);
2201
- const boundaryChanges = boundarySource || includeBoundaryWhenEmpty
2202
- ? boundarySource
2203
- ? [{ ...boundarySource, rebalanceBoundaryOnly: true }]
2204
- : []
2205
- : undefined;
2250
+ const MAX_REBALANCE_TASKS_PER_PAGE = 8;
2251
+ const buildStrictQueryRangeDTOs = (segments, template) => {
2252
+ const merged = mergeBigIntSegments(segments
2253
+ .filter(([start, end]) => start < end)
2254
+ .map(([start, end]) => [start, end]));
2255
+ if (merged.length === 0) {
2256
+ return [];
2257
+ }
2258
+ const max = isU32Range(template) ? BigInt(MAX_U32) : MAX_U64;
2259
+ const arcs = [];
2260
+ if (merged.length > 1 &&
2261
+ merged[0][0] === 0n &&
2262
+ merged[merged.length - 1][1] === max) {
2263
+ // Preserve the historical seam-first order. Apart from compatibility, this
2264
+ // keeps the two linear pieces of a wrapped query range in one task.
2265
+ arcs.push({
2266
+ first: merged[merged.length - 1],
2267
+ second: merged[0],
2268
+ });
2269
+ for (let i = 1; i + 1 < merged.length; i++) {
2270
+ arcs.push({ first: merged[i] });
2271
+ }
2272
+ }
2273
+ else {
2274
+ for (const segment of merged) {
2275
+ arcs.push({ first: segment });
2276
+ }
2277
+ }
2278
+ return arcs.map(({ first, second }) => {
2279
+ const [start1, end1] = toOriginalType(first, template);
2280
+ const [start2, end2] = second
2281
+ ? toOriginalType(second, template)
2282
+ : [start1, end1];
2283
+ return {
2284
+ start1,
2285
+ end1,
2286
+ start2,
2287
+ end2,
2288
+ mode: ReplicationIntent.Strict,
2289
+ };
2290
+ });
2291
+ };
2292
+ const createRebalanceQueryGeometryPlan = (changes, includeBoundaryWhenEmpty) => {
2293
+ const boundary = includeBoundaryWhenEmpty ||
2294
+ changes.some((change) => change.rebalanceBoundaryOnly ||
2295
+ change.range.mode === ReplicationIntent.NonStrict);
2206
2296
  const geometryChanges = changes.filter((change) => !change.rebalanceBoundaryOnly);
2207
2297
  if (geometryChanges.length === 0) {
2208
- return { boundaryChanges, geometryBatches: [], ownedIntervals: [] };
2298
+ return {
2299
+ boundary,
2300
+ geometryRanges: [],
2301
+ ownedIntervals: [],
2302
+ taskCount: boundary ? 1 : 0,
2303
+ };
2209
2304
  }
2210
2305
  const template = geometryChanges[geometryChanges.length - 1];
2211
2306
  const exactSegments = mergeBigIntSegments(geometryChanges
2212
2307
  .flatMap((change) => toSegmentsBigInt(change.range))
2213
2308
  .filter(([start, end]) => start < end)
2214
2309
  .map(([start, end]) => [start, end]));
2215
- const exactRanges = buildStrictQueryRanges(exactSegments, template.range);
2216
- const exactChanges = exactRanges.map((range) => ({
2217
- range,
2218
- type: "replaced",
2219
- timestamp: template.timestamp,
2220
- }));
2221
- const geometryBatches = [];
2222
- for (let offset = 0; offset < exactChanges.length; offset += MAX_REBALANCE_QUERY_RANGES) {
2223
- geometryBatches.push(exactChanges.slice(offset, offset + MAX_REBALANCE_QUERY_RANGES));
2224
- }
2310
+ const geometryRanges = buildStrictQueryRangeDTOs(exactSegments, template.range);
2225
2311
  // A row can contain coordinates in several query batches. Assign every exact
2226
2312
  // linear interval to its batch up front so candidates can be de-duplicated with
2227
2313
  // O(component-count) plan memory instead of an unbounded set of emitted hashes.
2228
2314
  // The two linear pieces of a wrapped seam range inherit the same batch tag.
2229
- const ownedIntervals = exactRanges
2315
+ const ownedIntervals = geometryRanges
2230
2316
  .flatMap((range, rangeIndex) => toSegmentsBigInt(range)
2231
2317
  .filter(([start, end]) => start < end)
2232
2318
  .map(([start, end]) => ({
2233
2319
  start,
2234
2320
  end,
2235
- batch: Math.floor(rangeIndex / MAX_REBALANCE_QUERY_RANGES),
2321
+ geometryTask: Math.floor(rangeIndex / MAX_REBALANCE_QUERY_RANGES),
2236
2322
  })))
2237
2323
  .sort((a, b) => a.start < b.start
2238
2324
  ? -1
@@ -2248,9 +2334,47 @@ const createRebalanceQueryPlan = (changes, includeBoundaryWhenEmpty) => {
2248
2334
  throw new Error("Rebalance query intervals must not overlap");
2249
2335
  }
2250
2336
  }
2251
- return { boundaryChanges, geometryBatches, ownedIntervals };
2337
+ return {
2338
+ boundary,
2339
+ geometryRanges,
2340
+ ownedIntervals,
2341
+ taskCount: (boundary ? 1 : 0) +
2342
+ Math.ceil(geometryRanges.length / MAX_REBALANCE_QUERY_RANGES),
2343
+ };
2252
2344
  };
2253
- const findOwningRebalanceBatch = (coordinates, intervals) => {
2345
+ /**
2346
+ * Bounds control-plane query construction only. A task can still match an
2347
+ * arbitrary number of local rows, so executors must apply their own durable
2348
+ * row/byte/time budgets while scanning it.
2349
+ */
2350
+ export const getRebalanceScanTaskPage = (plan, cursor) => {
2351
+ const start = cursor?.nextTask ?? 0;
2352
+ if (!Number.isSafeInteger(start) || start < 0 || start > plan.taskCount) {
2353
+ throw new Error("Invalid rebalance task cursor");
2354
+ }
2355
+ const end = Math.min(start + MAX_REBALANCE_TASKS_PER_PAGE, plan.taskCount);
2356
+ const boundaryOffset = plan.boundary ? 1 : 0;
2357
+ const tasks = [];
2358
+ for (let ordinal = start; ordinal < end; ordinal++) {
2359
+ if (plan.boundary && ordinal === 0) {
2360
+ tasks.push({ kind: "boundary", ordinal });
2361
+ continue;
2362
+ }
2363
+ const geometryTask = ordinal - boundaryOffset;
2364
+ const rangeOffset = geometryTask * MAX_REBALANCE_QUERY_RANGES;
2365
+ tasks.push({
2366
+ kind: "geometry",
2367
+ ordinal,
2368
+ geometryTask,
2369
+ ranges: plan.geometryRanges.slice(rangeOffset, rangeOffset + MAX_REBALANCE_QUERY_RANGES),
2370
+ });
2371
+ }
2372
+ return {
2373
+ tasks,
2374
+ next: end < plan.taskCount ? { nextTask: end } : undefined,
2375
+ };
2376
+ };
2377
+ export const findOwningRebalanceTask = (coordinates, intervals) => {
2254
2378
  let minimum;
2255
2379
  for (const value of coordinates) {
2256
2380
  const coordinate = typeof value === "number" ? BigInt(value) : value;
@@ -2267,7 +2391,9 @@ const findOwningRebalanceBatch = (coordinates, intervals) => {
2267
2391
  }
2268
2392
  else {
2269
2393
  minimum =
2270
- minimum == null ? interval.batch : Math.min(minimum, interval.batch);
2394
+ minimum == null
2395
+ ? interval.geometryTask
2396
+ : Math.min(minimum, interval.geometryTask);
2271
2397
  break;
2272
2398
  }
2273
2399
  }
@@ -2289,7 +2415,13 @@ const rotateRebalanceHistory = (changes, rebalanceHistory) => {
2289
2415
  }
2290
2416
  }
2291
2417
  };
2292
- export const mergeReplicationChanges = (changesOrChangesArr, rebalanceHistory, options) => {
2418
+ const snapshotRebalanceHistory = (rebalanceHistory) => {
2419
+ // Expiration is a cache concern, not a planner concern. Resolve it once and
2420
+ // give the pure planner a stable view for the complete plan.
2421
+ rebalanceHistory.trim();
2422
+ return new Set([...rebalanceHistory.map.keys()].filter((key) => typeof key === "string"));
2423
+ };
2424
+ const mergeReplicationChangesFromHistory = (changesOrChangesArr, rebalanceHistory) => {
2293
2425
  // Work from an immutable snapshot. In particular, synthetic difference ranges
2294
2426
  // must never become input events or rebalance-history entries.
2295
2427
  const changes = flattenReplicationChanges(changesOrChangesArr);
@@ -2451,70 +2583,99 @@ export const mergeReplicationChanges = (changesOrChangesArr, rebalanceHistory, o
2451
2583
  rebalanceBoundaryOnly: true,
2452
2584
  });
2453
2585
  }
2586
+ return deduplicated;
2587
+ };
2588
+ export const mergeReplicationChanges = (changesOrChangesArr, rebalanceHistory, options) => {
2589
+ const changes = flattenReplicationChanges(changesOrChangesArr);
2590
+ const merged = mergeReplicationChangesFromHistory(changes, snapshotRebalanceHistory(rebalanceHistory));
2454
2591
  if (options?.updateHistory !== false) {
2455
2592
  rotateRebalanceHistory(changes, rebalanceHistory);
2456
2593
  }
2457
- return deduplicated;
2594
+ return merged;
2595
+ };
2596
+ export const createRebalanceScanPlan = (input) => {
2597
+ const sourceChanges = [...input.changes];
2598
+ const queryChanges = input.forceFresh
2599
+ ? sourceChanges
2600
+ : mergeReplicationChangesFromHistory(sourceChanges, input.history);
2601
+ const geometry = createRebalanceQueryGeometryPlan(queryChanges, sourceChanges.length === 0);
2602
+ return {
2603
+ ...geometry,
2604
+ historyMutations: input.forceFresh
2605
+ ? []
2606
+ : sourceChanges.map((change) => ({
2607
+ rangeHash: change.range.rangeHash,
2608
+ present: change.type === "added",
2609
+ })),
2610
+ };
2611
+ };
2612
+ const applyRebalanceHistoryMutations = (mutations, rebalanceHistory) => {
2613
+ for (const mutation of mutations) {
2614
+ if (mutation.present) {
2615
+ rebalanceHistory.add(mutation.rangeHash);
2616
+ }
2617
+ else {
2618
+ rebalanceHistory.del(mutation.rangeHash);
2619
+ }
2620
+ }
2458
2621
  };
2459
2622
  const REBALANCE_ITERATOR_BATCH_SIZE = 1048;
2460
2623
  export const toRebalance = (changeOrChanges, index, rebalanceHistory, options) => {
2461
2624
  const sourceChanges = flattenReplicationChanges(changeOrChanges);
2462
- const queryChanges = options?.forceFresh
2463
- ? sourceChanges
2464
- : mergeReplicationChanges(sourceChanges, rebalanceHistory, {
2465
- updateHistory: false,
2466
- });
2467
- const plan = createRebalanceQueryPlan(queryChanges, sourceChanges.length === 0);
2625
+ const plan = createRebalanceScanPlan({
2626
+ changes: sourceChanges,
2627
+ history: options?.forceFresh
2628
+ ? new Set()
2629
+ : snapshotRebalanceHistory(rebalanceHistory),
2630
+ forceFresh: options?.forceFresh,
2631
+ });
2468
2632
  return {
2469
2633
  [Symbol.asyncIterator]: async function* () {
2470
2634
  let completed = false;
2471
2635
  try {
2472
- const passes = [];
2473
- if (plan.boundaryChanges) {
2474
- passes.push({ changes: plan.boundaryChanges });
2475
- }
2476
- for (let batch = 0; batch < plan.geometryBatches.length; batch++) {
2477
- passes.push({
2478
- changes: plan.geometryBatches[batch],
2479
- geometryBatch: batch,
2480
- });
2481
- }
2482
- for (const pass of passes) {
2483
- const isGeometry = pass.geometryBatch != null;
2484
- const iterator = index.iterate({
2485
- query: createAssignedRangesQuery(pass.changes, {
2486
- strict: isGeometry,
2487
- includeBoundaryWhenEmpty: !isGeometry,
2488
- }),
2489
- });
2490
- let passCompleted = false;
2491
- try {
2492
- while (iterator.done() !== true) {
2493
- const entries = await iterator.next(REBALANCE_ITERATOR_BATCH_SIZE);
2494
- if (entries.length === 0) {
2495
- break;
2496
- }
2497
- for (const entry of entries) {
2498
- if (isGeometry) {
2499
- if (plan.boundaryChanges &&
2500
- entry.value.assignedToRangeBoundary) {
2501
- continue;
2502
- }
2503
- if (findOwningRebalanceBatch(entry.value.coordinates, plan.ownedIntervals) !== pass.geometryBatch) {
2504
- continue;
2636
+ let cursor;
2637
+ for (;;) {
2638
+ const page = getRebalanceScanTaskPage(plan, cursor);
2639
+ for (const task of page.tasks) {
2640
+ const isGeometry = task.kind === "geometry";
2641
+ const iterator = index.iterate({
2642
+ query: createAssignedRangesQuery(isGeometry ? task.ranges.map((range) => ({ range })) : [], {
2643
+ strict: isGeometry,
2644
+ includeBoundaryWhenEmpty: !isGeometry,
2645
+ }),
2646
+ });
2647
+ let taskCompleted = false;
2648
+ try {
2649
+ while (iterator.done() !== true) {
2650
+ const entries = await iterator.next(REBALANCE_ITERATOR_BATCH_SIZE);
2651
+ if (entries.length === 0) {
2652
+ break;
2653
+ }
2654
+ for (const entry of entries) {
2655
+ if (isGeometry) {
2656
+ if (plan.boundary && entry.value.assignedToRangeBoundary) {
2657
+ continue;
2658
+ }
2659
+ if (findOwningRebalanceTask(entry.value.coordinates, plan.ownedIntervals) !== task.geometryTask) {
2660
+ continue;
2661
+ }
2505
2662
  }
2663
+ yield entry.value;
2506
2664
  }
2507
- yield entry.value;
2508
2665
  }
2666
+ taskCompleted = iterator.done() === true;
2667
+ }
2668
+ finally {
2669
+ await iterator.close();
2670
+ }
2671
+ if (!taskCompleted) {
2672
+ return;
2509
2673
  }
2510
- passCompleted = iterator.done() === true;
2511
- }
2512
- finally {
2513
- await iterator.close();
2514
2674
  }
2515
- if (!passCompleted) {
2516
- return;
2675
+ if (!page.next) {
2676
+ break;
2517
2677
  }
2678
+ cursor = page.next;
2518
2679
  }
2519
2680
  completed = true;
2520
2681
  }
@@ -2522,8 +2683,8 @@ export const toRebalance = (changeOrChanges, index, rebalanceHistory, options) =
2522
2683
  // An early consumer return, iterator error, incomplete empty batch, or
2523
2684
  // close failure in any sequential pass must leave history untouched so the
2524
2685
  // next event retries the complete query. forceFresh preserves its bypass.
2525
- if (completed && !options?.forceFresh) {
2526
- rotateRebalanceHistory(sourceChanges, rebalanceHistory);
2686
+ if (completed) {
2687
+ applyRebalanceHistoryMutations(plan.historyMutations, rebalanceHistory);
2527
2688
  }
2528
2689
  }
2529
2690
  },