@peerbit/shared-log 16.0.5 → 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.
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +12 -9
- package/dist/src/index.js.map +1 -1
- package/dist/src/ranges.d.ts +50 -1
- package/dist/src/ranges.d.ts.map +1 -1
- package/dist/src/ranges.js +273 -112
- package/dist/src/ranges.js.map +1 -1
- package/package.json +13 -13
- package/src/index.ts +34 -9
- package/src/ranges.ts +436 -165
package/dist/src/ranges.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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:
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1597
|
-
if (
|
|
1598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
1661
|
+
done: () => queues.every((queue) => queue.elements.length === 0) &&
|
|
1662
|
+
iterators.every((x) => x.done() === true),
|
|
1618
1663
|
close: async () => {
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
-
|
|
1626
|
-
|
|
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
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
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 ||
|
|
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 (
|
|
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
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2463
|
-
|
|
2464
|
-
:
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
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
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
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 (!
|
|
2516
|
-
|
|
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
|
|
2526
|
-
|
|
2686
|
+
if (completed) {
|
|
2687
|
+
applyRebalanceHistoryMutations(plan.historyMutations, rebalanceHistory);
|
|
2527
2688
|
}
|
|
2528
2689
|
}
|
|
2529
2690
|
},
|