@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/src/ranges.ts
CHANGED
|
@@ -476,7 +476,10 @@ export type NumericType = "u32" | "u64";
|
|
|
476
476
|
* We always do the conversion to bigints internally.
|
|
477
477
|
*/
|
|
478
478
|
export function toSegmentsBigInt<N extends NumericType>(
|
|
479
|
-
range:
|
|
479
|
+
range: Pick<
|
|
480
|
+
ReplicationRangeIndexable<N>,
|
|
481
|
+
"start1" | "end1" | "start2" | "end2"
|
|
482
|
+
>,
|
|
480
483
|
): Array<[bigint, bigint]> {
|
|
481
484
|
// Safely convert the numeric fields to bigint
|
|
482
485
|
const s1: bigint =
|
|
@@ -1553,6 +1556,7 @@ const getClosest = <S extends Shape | undefined, R extends "u32" | "u64">(
|
|
|
1553
1556
|
options?: {
|
|
1554
1557
|
shape?: S;
|
|
1555
1558
|
hash?: string;
|
|
1559
|
+
excludeId?: Uint8Array;
|
|
1556
1560
|
time?: {
|
|
1557
1561
|
roleAgeLimit: number;
|
|
1558
1562
|
matured: boolean;
|
|
@@ -1560,13 +1564,16 @@ const getClosest = <S extends Shape | undefined, R extends "u32" | "u64">(
|
|
|
1560
1564
|
};
|
|
1561
1565
|
},
|
|
1562
1566
|
): IndexIterator<ReplicationRangeIndexable<R>, S> => {
|
|
1563
|
-
const createQueries = (
|
|
1567
|
+
const createQueries = (
|
|
1568
|
+
p: NumberFromType<R>,
|
|
1569
|
+
phase: "primary" | "wrapped",
|
|
1570
|
+
) => {
|
|
1564
1571
|
let queries: Query[];
|
|
1565
1572
|
if (direction === "below") {
|
|
1566
1573
|
queries = [
|
|
1567
1574
|
new IntegerCompare({
|
|
1568
1575
|
key: "end2",
|
|
1569
|
-
compare:
|
|
1576
|
+
compare: phase === "primary" ? Compare.LessOrEqual : Compare.Greater,
|
|
1570
1577
|
value: p,
|
|
1571
1578
|
}),
|
|
1572
1579
|
];
|
|
@@ -1574,7 +1581,7 @@ const getClosest = <S extends Shape | undefined, R extends "u32" | "u64">(
|
|
|
1574
1581
|
queries = [
|
|
1575
1582
|
new IntegerCompare({
|
|
1576
1583
|
key: "start1",
|
|
1577
|
-
compare:
|
|
1584
|
+
compare: phase === "primary" ? Compare.GreaterOrEqual : Compare.Less,
|
|
1578
1585
|
value: p,
|
|
1579
1586
|
}),
|
|
1580
1587
|
];
|
|
@@ -1608,6 +1615,16 @@ const getClosest = <S extends Shape | undefined, R extends "u32" | "u64">(
|
|
|
1608
1615
|
if (options?.hash) {
|
|
1609
1616
|
queries.push(new StringMatch({ key: "hash", value: options.hash }));
|
|
1610
1617
|
}
|
|
1618
|
+
if (options?.excludeId) {
|
|
1619
|
+
queries.push(
|
|
1620
|
+
new Not(
|
|
1621
|
+
new ByteMatchQuery({
|
|
1622
|
+
key: "id",
|
|
1623
|
+
value: options.excludeId,
|
|
1624
|
+
}),
|
|
1625
|
+
),
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1611
1628
|
return queries;
|
|
1612
1629
|
};
|
|
1613
1630
|
|
|
@@ -1616,7 +1633,7 @@ const getClosest = <S extends Shape | undefined, R extends "u32" | "u64">(
|
|
|
1616
1633
|
|
|
1617
1634
|
const iterator = rects.iterate(
|
|
1618
1635
|
{
|
|
1619
|
-
query: createQueries(point,
|
|
1636
|
+
query: createQueries(point, "primary"),
|
|
1620
1637
|
sort: [
|
|
1621
1638
|
direction === "below"
|
|
1622
1639
|
? new Sort({ key: ["end2"], direction: "desc" })
|
|
@@ -1630,10 +1647,7 @@ const getClosest = <S extends Shape | undefined, R extends "u32" | "u64">(
|
|
|
1630
1647
|
|
|
1631
1648
|
const iteratorWrapped = rects.iterate(
|
|
1632
1649
|
{
|
|
1633
|
-
query: createQueries(
|
|
1634
|
-
direction === "below" ? numbers.maxValue : numbers.zero,
|
|
1635
|
-
true,
|
|
1636
|
-
),
|
|
1650
|
+
query: createQueries(point, "wrapped"),
|
|
1637
1651
|
sort: [
|
|
1638
1652
|
direction === "below"
|
|
1639
1653
|
? new Sort({ key: ["end2"], direction: "desc" })
|
|
@@ -1778,10 +1792,11 @@ export function getDistance(
|
|
|
1778
1792
|
value < 0 ? -value : value;
|
|
1779
1793
|
const diff = <T extends number | bigint>(a: T, b: T): T => abs(a - b) as T;
|
|
1780
1794
|
|
|
1795
|
+
if (from === to) {
|
|
1796
|
+
return typeof from === "number" ? 0 : 0n;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1781
1799
|
if (direction === "closest") {
|
|
1782
|
-
if (from === to) {
|
|
1783
|
-
return typeof from === "number" ? 0 : 0n; // returns 0 of the correct type
|
|
1784
|
-
}
|
|
1785
1800
|
return diff(from, to) < diff(end, diff(from, to))
|
|
1786
1801
|
? diff(from, to)
|
|
1787
1802
|
: diff(end, diff(from, to));
|
|
@@ -1804,22 +1819,57 @@ export function getDistance(
|
|
|
1804
1819
|
throw new Error("Invalid direction");
|
|
1805
1820
|
}
|
|
1806
1821
|
|
|
1807
|
-
|
|
1822
|
+
/** @internal */
|
|
1823
|
+
export const joinIterator = <
|
|
1824
|
+
S extends Shape | undefined,
|
|
1825
|
+
R extends "u32" | "u64",
|
|
1826
|
+
>(
|
|
1808
1827
|
iterators: IndexIterator<ReplicationRangeIndexable<R>, S>[],
|
|
1809
1828
|
point: NumberFromType<R>,
|
|
1810
1829
|
direction: "above" | "below" | "closest",
|
|
1811
1830
|
numbers: Numbers<R>,
|
|
1812
1831
|
): IndexIterator<ReplicationRangeIndexable<R>, S> => {
|
|
1832
|
+
type QueuedResult = {
|
|
1833
|
+
result: IndexedResult<ReturnTypeFromShape<ReplicationRangeIndexable<R>, S>>;
|
|
1834
|
+
dist: NumberFromType<R>;
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
const compareQueuedResults = (left: QueuedResult, right: QueuedResult) => {
|
|
1838
|
+
if (left.dist < right.dist) return -1;
|
|
1839
|
+
if (left.dist > right.dist) return 1;
|
|
1840
|
+
|
|
1841
|
+
// Match the native closest-range total order. The shaped iterator users do
|
|
1842
|
+
// not always project every tie field, so only compare a field when both
|
|
1843
|
+
// results carry it; getSamples uses complete ranges and therefore always
|
|
1844
|
+
// follows distance, timestamp, owner hash, then range id.
|
|
1845
|
+
const leftValue = left.result.value as Partial<
|
|
1846
|
+
ReplicationRangeIndexable<R>
|
|
1847
|
+
>;
|
|
1848
|
+
const rightValue = right.result.value as Partial<
|
|
1849
|
+
ReplicationRangeIndexable<R>
|
|
1850
|
+
>;
|
|
1851
|
+
if (leftValue.timestamp != null && rightValue.timestamp != null) {
|
|
1852
|
+
if (leftValue.timestamp < rightValue.timestamp) return -1;
|
|
1853
|
+
if (leftValue.timestamp > rightValue.timestamp) return 1;
|
|
1854
|
+
}
|
|
1855
|
+
if (leftValue.hash != null && rightValue.hash != null) {
|
|
1856
|
+
if (leftValue.hash < rightValue.hash) return -1;
|
|
1857
|
+
if (leftValue.hash > rightValue.hash) return 1;
|
|
1858
|
+
}
|
|
1859
|
+
const leftId = leftValue.id == null ? undefined : toBase64(leftValue.id);
|
|
1860
|
+
const rightId = rightValue.id == null ? undefined : toBase64(rightValue.id);
|
|
1861
|
+
if (leftId != null && rightId != null) {
|
|
1862
|
+
if (leftId < rightId) return -1;
|
|
1863
|
+
if (leftId > rightId) return 1;
|
|
1864
|
+
}
|
|
1865
|
+
return 0;
|
|
1866
|
+
};
|
|
1867
|
+
|
|
1813
1868
|
let queues: {
|
|
1814
|
-
elements:
|
|
1815
|
-
result: IndexedResult<
|
|
1816
|
-
ReturnTypeFromShape<ReplicationRangeIndexable<R>, S>
|
|
1817
|
-
>;
|
|
1818
|
-
dist: NumberFromType<R>;
|
|
1819
|
-
}[];
|
|
1869
|
+
elements: QueuedResult[];
|
|
1820
1870
|
}[] = [];
|
|
1821
1871
|
|
|
1822
|
-
|
|
1872
|
+
const joined: IndexIterator<ReplicationRangeIndexable<R>, S> = {
|
|
1823
1873
|
next: async (
|
|
1824
1874
|
count: number,
|
|
1825
1875
|
): Promise<
|
|
@@ -1884,13 +1934,16 @@ const joinIterator = <S extends Shape | undefined, R extends "u32" | "u64">(
|
|
|
1884
1934
|
|
|
1885
1935
|
for (let i = 0; i < count; i++) {
|
|
1886
1936
|
let closestQueue = -1;
|
|
1887
|
-
let
|
|
1937
|
+
let selected: QueuedResult | undefined;
|
|
1888
1938
|
for (let j = 0; j < queues.length; j++) {
|
|
1889
1939
|
let queue = queues[j];
|
|
1890
1940
|
if (queue && queue.elements.length > 0) {
|
|
1891
|
-
|
|
1892
|
-
if (
|
|
1893
|
-
|
|
1941
|
+
const candidate = queue.elements[0];
|
|
1942
|
+
if (
|
|
1943
|
+
candidate &&
|
|
1944
|
+
(!selected || compareQueuedResults(candidate, selected) < 0)
|
|
1945
|
+
) {
|
|
1946
|
+
selected = candidate;
|
|
1894
1947
|
closestQueue = j;
|
|
1895
1948
|
}
|
|
1896
1949
|
}
|
|
@@ -1900,7 +1953,7 @@ const joinIterator = <S extends Shape | undefined, R extends "u32" | "u64">(
|
|
|
1900
1953
|
break;
|
|
1901
1954
|
}
|
|
1902
1955
|
|
|
1903
|
-
|
|
1956
|
+
const closest = queues[closestQueue]?.elements.shift();
|
|
1904
1957
|
if (closest) {
|
|
1905
1958
|
results.push(closest.result);
|
|
1906
1959
|
}
|
|
@@ -1909,25 +1962,43 @@ const joinIterator = <S extends Shape | undefined, R extends "u32" | "u64">(
|
|
|
1909
1962
|
},
|
|
1910
1963
|
pending: async () => {
|
|
1911
1964
|
let allPending = await Promise.all(iterators.map((x) => x.pending()));
|
|
1912
|
-
|
|
1965
|
+
// A child can be exhausted while this join still owns prefetched rows.
|
|
1966
|
+
// Counting local queues makes the same contract compose through nested joins.
|
|
1967
|
+
const buffered = queues.reduce(
|
|
1968
|
+
(acc, queue) => acc + queue.elements.length,
|
|
1969
|
+
0,
|
|
1970
|
+
);
|
|
1971
|
+
return buffered + allPending.reduce((acc, x) => acc + x, 0);
|
|
1913
1972
|
},
|
|
1914
|
-
done: () =>
|
|
1973
|
+
done: () =>
|
|
1974
|
+
queues.every((queue) => queue.elements.length === 0) &&
|
|
1975
|
+
iterators.every((x) => x.done() === true),
|
|
1915
1976
|
close: async () => {
|
|
1916
|
-
|
|
1917
|
-
|
|
1977
|
+
queues.length = 0;
|
|
1978
|
+
const settled = await Promise.allSettled(
|
|
1979
|
+
iterators.map(async (iterator) => iterator.close()),
|
|
1980
|
+
);
|
|
1981
|
+
const errors = settled.flatMap((result) =>
|
|
1982
|
+
result.status === "rejected" ? [result.reason] : [],
|
|
1983
|
+
);
|
|
1984
|
+
if (errors.length === 1) {
|
|
1985
|
+
throw errors[0];
|
|
1986
|
+
}
|
|
1987
|
+
if (errors.length > 1) {
|
|
1988
|
+
throw new AggregateError(errors, "Failed to close joined iterators");
|
|
1918
1989
|
}
|
|
1919
1990
|
},
|
|
1920
1991
|
all: async () => {
|
|
1921
1992
|
let results: IndexedResult<
|
|
1922
1993
|
ReturnTypeFromShape<ReplicationRangeIndexable<R>, S>
|
|
1923
1994
|
>[] = [];
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
results.push(...res);
|
|
1995
|
+
while (!joined.done()) {
|
|
1996
|
+
results.push(...(await joined.next(100)));
|
|
1927
1997
|
}
|
|
1928
1998
|
return results;
|
|
1929
1999
|
},
|
|
1930
2000
|
};
|
|
2001
|
+
return joined;
|
|
1931
2002
|
};
|
|
1932
2003
|
|
|
1933
2004
|
const getClosestAroundOrContaining = <
|
|
@@ -1976,6 +2047,7 @@ const getClosestAroundOrContaining = <
|
|
|
1976
2047
|
export const getAdjecentSameOwner = async <R extends "u32" | "u64">(
|
|
1977
2048
|
peers: Index<ReplicationRangeIndexable<R>>,
|
|
1978
2049
|
range: {
|
|
2050
|
+
id?: Uint8Array;
|
|
1979
2051
|
idString?: string;
|
|
1980
2052
|
start1: NumberFromType<R>;
|
|
1981
2053
|
end2: NumberFromType<R>;
|
|
@@ -1994,6 +2066,7 @@ export const getAdjecentSameOwner = async <R extends "u32" | "u64">(
|
|
|
1994
2066
|
numbers,
|
|
1995
2067
|
{
|
|
1996
2068
|
hash: range.hash,
|
|
2069
|
+
excludeId: range.id,
|
|
1997
2070
|
},
|
|
1998
2071
|
);
|
|
1999
2072
|
const closestBelow = await closestBelowIterator.next(1);
|
|
@@ -2006,21 +2079,21 @@ export const getAdjecentSameOwner = async <R extends "u32" | "u64">(
|
|
|
2006
2079
|
numbers,
|
|
2007
2080
|
{
|
|
2008
2081
|
hash: range.hash,
|
|
2082
|
+
excludeId: range.id,
|
|
2009
2083
|
},
|
|
2010
2084
|
);
|
|
2011
2085
|
const closestAbove = await closestAboveIterator.next(1);
|
|
2012
2086
|
closestAboveIterator.close();
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
};
|
|
2087
|
+
const below =
|
|
2088
|
+
range.idString === closestBelow[0]?.value.idString
|
|
2089
|
+
? undefined
|
|
2090
|
+
: closestBelow[0]?.value;
|
|
2091
|
+
const above =
|
|
2092
|
+
range.idString === closestAbove[0]?.value.idString ||
|
|
2093
|
+
below?.idString === closestAbove[0]?.value.idString
|
|
2094
|
+
? undefined
|
|
2095
|
+
: closestAbove[0]?.value;
|
|
2096
|
+
return { below, above };
|
|
2024
2097
|
};
|
|
2025
2098
|
|
|
2026
2099
|
export const getAllMergeCandiates = async <R extends "u32" | "u64">(
|
|
@@ -2128,7 +2201,7 @@ export const getSamples = async <R extends "u32" | "u64">(
|
|
|
2128
2201
|
}
|
|
2129
2202
|
|
|
2130
2203
|
const now = +new Date();
|
|
2131
|
-
|
|
2204
|
+
const matureOwners = new Set<string>();
|
|
2132
2205
|
|
|
2133
2206
|
let uniqueVisited = new Set<string>();
|
|
2134
2207
|
const peerFilter = options?.peerFilter;
|
|
@@ -2146,10 +2219,10 @@ export const getSamples = async <R extends "u32" | "u64">(
|
|
|
2146
2219
|
}
|
|
2147
2220
|
uniqueVisited.add(rect.value.hash);
|
|
2148
2221
|
let prev = leaders.get(rect.value.hash);
|
|
2222
|
+
if (isMatured(rect.value, now, roleAge)) {
|
|
2223
|
+
matureOwners.add(rect.value.hash);
|
|
2224
|
+
}
|
|
2149
2225
|
if (!prev) {
|
|
2150
|
-
if (isMatured(rect.value, now, roleAge)) {
|
|
2151
|
-
matured++;
|
|
2152
|
-
}
|
|
2153
2226
|
leaders.set(rect.value.hash, { intersecting: true });
|
|
2154
2227
|
} else {
|
|
2155
2228
|
prev.intersecting = true;
|
|
@@ -2165,7 +2238,7 @@ export const getSamples = async <R extends "u32" | "u64">(
|
|
|
2165
2238
|
}
|
|
2166
2239
|
}
|
|
2167
2240
|
|
|
2168
|
-
if (options?.onlyIntersecting ||
|
|
2241
|
+
if (options?.onlyIntersecting || matureOwners.size > i) {
|
|
2169
2242
|
continue;
|
|
2170
2243
|
}
|
|
2171
2244
|
|
|
@@ -2180,11 +2253,11 @@ export const getSamples = async <R extends "u32" | "u64">(
|
|
|
2180
2253
|
uniqueVisited.add(rect.hash);
|
|
2181
2254
|
const prev = leaders.get(rect.hash);
|
|
2182
2255
|
if (m) {
|
|
2256
|
+
matureOwners.add(rect.hash);
|
|
2183
2257
|
if (!prev) {
|
|
2184
|
-
matured++;
|
|
2185
2258
|
leaders.set(rect.hash, { intersecting: false });
|
|
2186
2259
|
}
|
|
2187
|
-
if (
|
|
2260
|
+
if (matureOwners.size > i) {
|
|
2188
2261
|
foundOneUniqueMatured = true;
|
|
2189
2262
|
}
|
|
2190
2263
|
}
|
|
@@ -2864,39 +2937,142 @@ const replicationRangeGeometryKey = <R extends NumericType>(
|
|
|
2864
2937
|
// margin below SQLite's default expression-depth limit (1000), including the
|
|
2865
2938
|
// nested comparisons needed for wrapped ranges.
|
|
2866
2939
|
const MAX_REBALANCE_QUERY_RANGES = 128;
|
|
2940
|
+
const MAX_REBALANCE_TASKS_PER_PAGE = 8;
|
|
2867
2941
|
|
|
2868
|
-
type
|
|
2942
|
+
export type RebalanceQueryRange<R extends NumericType> = Readonly<{
|
|
2943
|
+
start1: NumberFromType<R>;
|
|
2944
|
+
end1: NumberFromType<R>;
|
|
2945
|
+
start2: NumberFromType<R>;
|
|
2946
|
+
end2: NumberFromType<R>;
|
|
2947
|
+
mode: ReplicationIntent.Strict;
|
|
2948
|
+
}>;
|
|
2949
|
+
|
|
2950
|
+
export type RebalanceOwnedInterval = Readonly<{
|
|
2869
2951
|
start: bigint;
|
|
2870
2952
|
end: bigint;
|
|
2871
|
-
|
|
2872
|
-
}
|
|
2953
|
+
geometryTask: number;
|
|
2954
|
+
}>;
|
|
2955
|
+
|
|
2956
|
+
export type RebalanceHistoryMutation = Readonly<{
|
|
2957
|
+
rangeHash: string;
|
|
2958
|
+
present: boolean;
|
|
2959
|
+
}>;
|
|
2960
|
+
|
|
2961
|
+
/**
|
|
2962
|
+
* Complete control-plane query geometry. Its size is proportional to the input
|
|
2963
|
+
* range metadata; use task pages to bound query construction, not plan memory.
|
|
2964
|
+
* @internal
|
|
2965
|
+
*/
|
|
2966
|
+
export type RebalanceScanPlan<R extends NumericType> = Readonly<{
|
|
2967
|
+
boundary: boolean;
|
|
2968
|
+
geometryRanges: readonly RebalanceQueryRange<R>[];
|
|
2969
|
+
ownedIntervals: readonly RebalanceOwnedInterval[];
|
|
2970
|
+
taskCount: number;
|
|
2971
|
+
historyMutations: readonly RebalanceHistoryMutation[];
|
|
2972
|
+
}>;
|
|
2973
|
+
|
|
2974
|
+
export type RebalanceScanTask<R extends NumericType> =
|
|
2975
|
+
| Readonly<{
|
|
2976
|
+
kind: "boundary";
|
|
2977
|
+
ordinal: number;
|
|
2978
|
+
}>
|
|
2979
|
+
| Readonly<{
|
|
2980
|
+
kind: "geometry";
|
|
2981
|
+
ordinal: number;
|
|
2982
|
+
geometryTask: number;
|
|
2983
|
+
ranges: readonly RebalanceQueryRange<R>[];
|
|
2984
|
+
}>;
|
|
2873
2985
|
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2986
|
+
/**
|
|
2987
|
+
* Ephemeral offset within one immutable plan. It is not a restart cursor: any
|
|
2988
|
+
* durable executor must bind progress to an exact plan and placement view.
|
|
2989
|
+
*/
|
|
2990
|
+
export type RebalanceTaskCursor = Readonly<{ nextTask: number }>;
|
|
2991
|
+
|
|
2992
|
+
export type RebalanceTaskPage<R extends NumericType> = Readonly<{
|
|
2993
|
+
tasks: readonly RebalanceScanTask<R>[];
|
|
2994
|
+
next?: RebalanceTaskCursor;
|
|
2995
|
+
}>;
|
|
2996
|
+
|
|
2997
|
+
type RebalanceQueryGeometryPlan<R extends NumericType> = Pick<
|
|
2998
|
+
RebalanceScanPlan<R>,
|
|
2999
|
+
"boundary" | "geometryRanges" | "ownedIntervals" | "taskCount"
|
|
3000
|
+
>;
|
|
3001
|
+
|
|
3002
|
+
const buildStrictQueryRangeDTOs = <R extends NumericType>(
|
|
3003
|
+
segments: Array<[bigint, bigint]>,
|
|
3004
|
+
template: ReplicationRangeIndexable<R>,
|
|
3005
|
+
): RebalanceQueryRange<R>[] => {
|
|
3006
|
+
const merged = mergeBigIntSegments(
|
|
3007
|
+
segments
|
|
3008
|
+
.filter(([start, end]) => start < end)
|
|
3009
|
+
.map(([start, end]) => [start, end]),
|
|
3010
|
+
);
|
|
3011
|
+
if (merged.length === 0) {
|
|
3012
|
+
return [];
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
const max = isU32Range(template) ? BigInt(MAX_U32) : MAX_U64;
|
|
3016
|
+
const arcs: Array<{
|
|
3017
|
+
first: [bigint, bigint];
|
|
3018
|
+
second?: [bigint, bigint];
|
|
3019
|
+
}> = [];
|
|
3020
|
+
if (
|
|
3021
|
+
merged.length > 1 &&
|
|
3022
|
+
merged[0][0] === 0n &&
|
|
3023
|
+
merged[merged.length - 1][1] === max
|
|
3024
|
+
) {
|
|
3025
|
+
// Preserve the historical seam-first order. Apart from compatibility, this
|
|
3026
|
+
// keeps the two linear pieces of a wrapped query range in one task.
|
|
3027
|
+
arcs.push({
|
|
3028
|
+
first: merged[merged.length - 1],
|
|
3029
|
+
second: merged[0],
|
|
3030
|
+
});
|
|
3031
|
+
for (let i = 1; i + 1 < merged.length; i++) {
|
|
3032
|
+
arcs.push({ first: merged[i] });
|
|
3033
|
+
}
|
|
3034
|
+
} else {
|
|
3035
|
+
for (const segment of merged) {
|
|
3036
|
+
arcs.push({ first: segment });
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
return arcs.map(({ first, second }) => {
|
|
3041
|
+
const [start1, end1] = toOriginalType(first, template);
|
|
3042
|
+
const [start2, end2] = second
|
|
3043
|
+
? toOriginalType(second, template)
|
|
3044
|
+
: ([start1, end1] as [NumberFromType<R>, NumberFromType<R>]);
|
|
3045
|
+
return {
|
|
3046
|
+
start1,
|
|
3047
|
+
end1,
|
|
3048
|
+
start2,
|
|
3049
|
+
end2,
|
|
3050
|
+
mode: ReplicationIntent.Strict,
|
|
3051
|
+
};
|
|
3052
|
+
});
|
|
2878
3053
|
};
|
|
2879
3054
|
|
|
2880
|
-
const
|
|
2881
|
-
changes: ReplicationChange<ReplicationRangeIndexable<R>>[],
|
|
3055
|
+
const createRebalanceQueryGeometryPlan = <R extends NumericType>(
|
|
3056
|
+
changes: readonly ReplicationChange<ReplicationRangeIndexable<R>>[],
|
|
2882
3057
|
includeBoundaryWhenEmpty: boolean,
|
|
2883
|
-
):
|
|
2884
|
-
const
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
change
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
? boundarySource
|
|
2892
|
-
? [{ ...boundarySource, rebalanceBoundaryOnly: true }]
|
|
2893
|
-
: []
|
|
2894
|
-
: undefined;
|
|
3058
|
+
): RebalanceQueryGeometryPlan<R> => {
|
|
3059
|
+
const boundary =
|
|
3060
|
+
includeBoundaryWhenEmpty ||
|
|
3061
|
+
changes.some(
|
|
3062
|
+
(change) =>
|
|
3063
|
+
change.rebalanceBoundaryOnly ||
|
|
3064
|
+
change.range.mode === ReplicationIntent.NonStrict,
|
|
3065
|
+
);
|
|
2895
3066
|
const geometryChanges = changes.filter(
|
|
2896
3067
|
(change) => !change.rebalanceBoundaryOnly,
|
|
2897
3068
|
);
|
|
2898
3069
|
if (geometryChanges.length === 0) {
|
|
2899
|
-
return {
|
|
3070
|
+
return {
|
|
3071
|
+
boundary,
|
|
3072
|
+
geometryRanges: [],
|
|
3073
|
+
ownedIntervals: [],
|
|
3074
|
+
taskCount: boundary ? 1 : 0,
|
|
3075
|
+
};
|
|
2900
3076
|
}
|
|
2901
3077
|
|
|
2902
3078
|
const template = geometryChanges[geometryChanges.length - 1];
|
|
@@ -2906,37 +3082,23 @@ const createRebalanceQueryPlan = <R extends NumericType>(
|
|
|
2906
3082
|
.filter(([start, end]) => start < end)
|
|
2907
3083
|
.map(([start, end]) => [start, end]),
|
|
2908
3084
|
);
|
|
2909
|
-
const
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
type: "replaced",
|
|
2914
|
-
timestamp: template.timestamp,
|
|
2915
|
-
}));
|
|
2916
|
-
const geometryBatches: ReplicationChange<ReplicationRangeIndexable<R>>[][] =
|
|
2917
|
-
[];
|
|
2918
|
-
for (
|
|
2919
|
-
let offset = 0;
|
|
2920
|
-
offset < exactChanges.length;
|
|
2921
|
-
offset += MAX_REBALANCE_QUERY_RANGES
|
|
2922
|
-
) {
|
|
2923
|
-
geometryBatches.push(
|
|
2924
|
-
exactChanges.slice(offset, offset + MAX_REBALANCE_QUERY_RANGES),
|
|
2925
|
-
);
|
|
2926
|
-
}
|
|
3085
|
+
const geometryRanges = buildStrictQueryRangeDTOs(
|
|
3086
|
+
exactSegments,
|
|
3087
|
+
template.range,
|
|
3088
|
+
);
|
|
2927
3089
|
|
|
2928
3090
|
// A row can contain coordinates in several query batches. Assign every exact
|
|
2929
3091
|
// linear interval to its batch up front so candidates can be de-duplicated with
|
|
2930
3092
|
// O(component-count) plan memory instead of an unbounded set of emitted hashes.
|
|
2931
3093
|
// The two linear pieces of a wrapped seam range inherit the same batch tag.
|
|
2932
|
-
const ownedIntervals =
|
|
3094
|
+
const ownedIntervals = geometryRanges
|
|
2933
3095
|
.flatMap((range, rangeIndex) =>
|
|
2934
3096
|
toSegmentsBigInt(range)
|
|
2935
3097
|
.filter(([start, end]) => start < end)
|
|
2936
3098
|
.map(([start, end]) => ({
|
|
2937
3099
|
start,
|
|
2938
3100
|
end,
|
|
2939
|
-
|
|
3101
|
+
geometryTask: Math.floor(rangeIndex / MAX_REBALANCE_QUERY_RANGES),
|
|
2940
3102
|
})),
|
|
2941
3103
|
)
|
|
2942
3104
|
.sort((a, b) =>
|
|
@@ -2956,12 +3118,60 @@ const createRebalanceQueryPlan = <R extends NumericType>(
|
|
|
2956
3118
|
}
|
|
2957
3119
|
}
|
|
2958
3120
|
|
|
2959
|
-
return {
|
|
3121
|
+
return {
|
|
3122
|
+
boundary,
|
|
3123
|
+
geometryRanges,
|
|
3124
|
+
ownedIntervals,
|
|
3125
|
+
taskCount:
|
|
3126
|
+
(boundary ? 1 : 0) +
|
|
3127
|
+
Math.ceil(geometryRanges.length / MAX_REBALANCE_QUERY_RANGES),
|
|
3128
|
+
};
|
|
2960
3129
|
};
|
|
2961
3130
|
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3131
|
+
/**
|
|
3132
|
+
* Bounds control-plane query construction only. A task can still match an
|
|
3133
|
+
* arbitrary number of local rows, so executors must apply their own durable
|
|
3134
|
+
* row/byte/time budgets while scanning it.
|
|
3135
|
+
*/
|
|
3136
|
+
export const getRebalanceScanTaskPage = <R extends NumericType>(
|
|
3137
|
+
plan: RebalanceScanPlan<R>,
|
|
3138
|
+
cursor?: RebalanceTaskCursor,
|
|
3139
|
+
): RebalanceTaskPage<R> => {
|
|
3140
|
+
const start = cursor?.nextTask ?? 0;
|
|
3141
|
+
if (!Number.isSafeInteger(start) || start < 0 || start > plan.taskCount) {
|
|
3142
|
+
throw new Error("Invalid rebalance task cursor");
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
const end = Math.min(start + MAX_REBALANCE_TASKS_PER_PAGE, plan.taskCount);
|
|
3146
|
+
const boundaryOffset = plan.boundary ? 1 : 0;
|
|
3147
|
+
const tasks: RebalanceScanTask<R>[] = [];
|
|
3148
|
+
for (let ordinal = start; ordinal < end; ordinal++) {
|
|
3149
|
+
if (plan.boundary && ordinal === 0) {
|
|
3150
|
+
tasks.push({ kind: "boundary", ordinal });
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
const geometryTask = ordinal - boundaryOffset;
|
|
3154
|
+
const rangeOffset = geometryTask * MAX_REBALANCE_QUERY_RANGES;
|
|
3155
|
+
tasks.push({
|
|
3156
|
+
kind: "geometry",
|
|
3157
|
+
ordinal,
|
|
3158
|
+
geometryTask,
|
|
3159
|
+
ranges: plan.geometryRanges.slice(
|
|
3160
|
+
rangeOffset,
|
|
3161
|
+
rangeOffset + MAX_REBALANCE_QUERY_RANGES,
|
|
3162
|
+
),
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
return {
|
|
3167
|
+
tasks,
|
|
3168
|
+
next: end < plan.taskCount ? { nextTask: end } : undefined,
|
|
3169
|
+
};
|
|
3170
|
+
};
|
|
3171
|
+
|
|
3172
|
+
export const findOwningRebalanceTask = (
|
|
3173
|
+
coordinates: readonly (number | bigint)[],
|
|
3174
|
+
intervals: readonly RebalanceOwnedInterval[],
|
|
2965
3175
|
): number | undefined => {
|
|
2966
3176
|
let minimum: number | undefined;
|
|
2967
3177
|
for (const value of coordinates) {
|
|
@@ -2977,7 +3187,9 @@ const findOwningRebalanceBatch = (
|
|
|
2977
3187
|
low = middle + 1;
|
|
2978
3188
|
} else {
|
|
2979
3189
|
minimum =
|
|
2980
|
-
minimum == null
|
|
3190
|
+
minimum == null
|
|
3191
|
+
? interval.geometryTask
|
|
3192
|
+
: Math.min(minimum, interval.geometryTask);
|
|
2981
3193
|
break;
|
|
2982
3194
|
}
|
|
2983
3195
|
}
|
|
@@ -2989,7 +3201,7 @@ const findOwningRebalanceBatch = (
|
|
|
2989
3201
|
};
|
|
2990
3202
|
|
|
2991
3203
|
const rotateRebalanceHistory = <R extends NumericType>(
|
|
2992
|
-
changes: ReplicationChange<ReplicationRangeIndexable<R>>[],
|
|
3204
|
+
changes: readonly ReplicationChange<ReplicationRangeIndexable<R>>[],
|
|
2993
3205
|
rebalanceHistory: Cache<string>,
|
|
2994
3206
|
) => {
|
|
2995
3207
|
// Only original source changes may enter history, and observed order defines
|
|
@@ -3003,12 +3215,24 @@ const rotateRebalanceHistory = <R extends NumericType>(
|
|
|
3003
3215
|
}
|
|
3004
3216
|
};
|
|
3005
3217
|
|
|
3006
|
-
|
|
3218
|
+
const snapshotRebalanceHistory = (
|
|
3219
|
+
rebalanceHistory: Cache<string>,
|
|
3220
|
+
): ReadonlySet<string> => {
|
|
3221
|
+
// Expiration is a cache concern, not a planner concern. Resolve it once and
|
|
3222
|
+
// give the pure planner a stable view for the complete plan.
|
|
3223
|
+
rebalanceHistory.trim();
|
|
3224
|
+
return new Set(
|
|
3225
|
+
[...rebalanceHistory.map.keys()].filter(
|
|
3226
|
+
(key): key is string => typeof key === "string",
|
|
3227
|
+
),
|
|
3228
|
+
);
|
|
3229
|
+
};
|
|
3230
|
+
|
|
3231
|
+
const mergeReplicationChangesFromHistory = <R extends NumericType>(
|
|
3007
3232
|
changesOrChangesArr:
|
|
3008
3233
|
| ReplicationChanges<ReplicationRangeIndexable<R>>
|
|
3009
3234
|
| ReplicationChanges<ReplicationRangeIndexable<R>>[],
|
|
3010
|
-
rebalanceHistory:
|
|
3011
|
-
options?: { updateHistory?: boolean },
|
|
3235
|
+
rebalanceHistory: ReadonlySet<string>,
|
|
3012
3236
|
): ReplicationChange<ReplicationRangeIndexable<R>>[] => {
|
|
3013
3237
|
// Work from an immutable snapshot. In particular, synthetic difference ranges
|
|
3014
3238
|
// must never become input events or rebalance-history entries.
|
|
@@ -3214,10 +3438,62 @@ export const mergeReplicationChanges = <R extends NumericType>(
|
|
|
3214
3438
|
rebalanceBoundaryOnly: true,
|
|
3215
3439
|
});
|
|
3216
3440
|
}
|
|
3441
|
+
return deduplicated;
|
|
3442
|
+
};
|
|
3443
|
+
|
|
3444
|
+
export const mergeReplicationChanges = <R extends NumericType>(
|
|
3445
|
+
changesOrChangesArr:
|
|
3446
|
+
| ReplicationChanges<ReplicationRangeIndexable<R>>
|
|
3447
|
+
| ReplicationChanges<ReplicationRangeIndexable<R>>[],
|
|
3448
|
+
rebalanceHistory: Cache<string>,
|
|
3449
|
+
options?: { updateHistory?: boolean },
|
|
3450
|
+
): ReplicationChange<ReplicationRangeIndexable<R>>[] => {
|
|
3451
|
+
const changes = flattenReplicationChanges(changesOrChangesArr);
|
|
3452
|
+
const merged = mergeReplicationChangesFromHistory(
|
|
3453
|
+
changes,
|
|
3454
|
+
snapshotRebalanceHistory(rebalanceHistory),
|
|
3455
|
+
);
|
|
3217
3456
|
if (options?.updateHistory !== false) {
|
|
3218
3457
|
rotateRebalanceHistory(changes, rebalanceHistory);
|
|
3219
3458
|
}
|
|
3220
|
-
return
|
|
3459
|
+
return merged;
|
|
3460
|
+
};
|
|
3461
|
+
|
|
3462
|
+
export const createRebalanceScanPlan = <R extends NumericType>(input: {
|
|
3463
|
+
changes: readonly ReplicationChange<ReplicationRangeIndexable<R>>[];
|
|
3464
|
+
history: ReadonlySet<string>;
|
|
3465
|
+
forceFresh?: boolean;
|
|
3466
|
+
}): RebalanceScanPlan<R> => {
|
|
3467
|
+
const sourceChanges = [...input.changes];
|
|
3468
|
+
const queryChanges = input.forceFresh
|
|
3469
|
+
? sourceChanges
|
|
3470
|
+
: mergeReplicationChangesFromHistory(sourceChanges, input.history);
|
|
3471
|
+
const geometry = createRebalanceQueryGeometryPlan(
|
|
3472
|
+
queryChanges,
|
|
3473
|
+
sourceChanges.length === 0,
|
|
3474
|
+
);
|
|
3475
|
+
return {
|
|
3476
|
+
...geometry,
|
|
3477
|
+
historyMutations: input.forceFresh
|
|
3478
|
+
? []
|
|
3479
|
+
: sourceChanges.map((change) => ({
|
|
3480
|
+
rangeHash: change.range.rangeHash,
|
|
3481
|
+
present: change.type === "added",
|
|
3482
|
+
})),
|
|
3483
|
+
};
|
|
3484
|
+
};
|
|
3485
|
+
|
|
3486
|
+
const applyRebalanceHistoryMutations = (
|
|
3487
|
+
mutations: readonly RebalanceHistoryMutation[],
|
|
3488
|
+
rebalanceHistory: Cache<string>,
|
|
3489
|
+
) => {
|
|
3490
|
+
for (const mutation of mutations) {
|
|
3491
|
+
if (mutation.present) {
|
|
3492
|
+
rebalanceHistory.add(mutation.rangeHash);
|
|
3493
|
+
} else {
|
|
3494
|
+
rebalanceHistory.del(mutation.rangeHash);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3221
3497
|
};
|
|
3222
3498
|
|
|
3223
3499
|
const REBALANCE_ITERATOR_BATCH_SIZE = 1048;
|
|
@@ -3231,85 +3507,80 @@ export const toRebalance = <R extends "u32" | "u64">(
|
|
|
3231
3507
|
options?: { forceFresh?: boolean },
|
|
3232
3508
|
): AsyncIterable<EntryReplicated<R>> => {
|
|
3233
3509
|
const sourceChanges = flattenReplicationChanges(changeOrChanges);
|
|
3234
|
-
const
|
|
3235
|
-
|
|
3236
|
-
:
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
sourceChanges.length === 0,
|
|
3242
|
-
);
|
|
3510
|
+
const plan = createRebalanceScanPlan({
|
|
3511
|
+
changes: sourceChanges,
|
|
3512
|
+
history: options?.forceFresh
|
|
3513
|
+
? new Set<string>()
|
|
3514
|
+
: snapshotRebalanceHistory(rebalanceHistory),
|
|
3515
|
+
forceFresh: options?.forceFresh,
|
|
3516
|
+
});
|
|
3243
3517
|
return {
|
|
3244
3518
|
[Symbol.asyncIterator]: async function* () {
|
|
3245
3519
|
let completed = false;
|
|
3246
3520
|
try {
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
) {
|
|
3284
|
-
continue;
|
|
3285
|
-
}
|
|
3286
|
-
if (
|
|
3287
|
-
findOwningRebalanceBatch(
|
|
3288
|
-
entry.value.coordinates,
|
|
3289
|
-
plan.ownedIntervals,
|
|
3290
|
-
) !== pass.geometryBatch
|
|
3291
|
-
) {
|
|
3292
|
-
continue;
|
|
3521
|
+
let cursor: RebalanceTaskCursor | undefined;
|
|
3522
|
+
for (;;) {
|
|
3523
|
+
const page = getRebalanceScanTaskPage(plan, cursor);
|
|
3524
|
+
for (const task of page.tasks) {
|
|
3525
|
+
const isGeometry = task.kind === "geometry";
|
|
3526
|
+
const iterator = index.iterate({
|
|
3527
|
+
query: createAssignedRangesQuery(
|
|
3528
|
+
isGeometry ? task.ranges.map((range) => ({ range })) : [],
|
|
3529
|
+
{
|
|
3530
|
+
strict: isGeometry,
|
|
3531
|
+
includeBoundaryWhenEmpty: !isGeometry,
|
|
3532
|
+
},
|
|
3533
|
+
),
|
|
3534
|
+
});
|
|
3535
|
+
let taskCompleted = false;
|
|
3536
|
+
try {
|
|
3537
|
+
while (iterator.done() !== true) {
|
|
3538
|
+
const entries = await iterator.next(
|
|
3539
|
+
REBALANCE_ITERATOR_BATCH_SIZE,
|
|
3540
|
+
);
|
|
3541
|
+
if (entries.length === 0) {
|
|
3542
|
+
break;
|
|
3543
|
+
}
|
|
3544
|
+
for (const entry of entries) {
|
|
3545
|
+
if (isGeometry) {
|
|
3546
|
+
if (plan.boundary && entry.value.assignedToRangeBoundary) {
|
|
3547
|
+
continue;
|
|
3548
|
+
}
|
|
3549
|
+
if (
|
|
3550
|
+
findOwningRebalanceTask(
|
|
3551
|
+
entry.value.coordinates,
|
|
3552
|
+
plan.ownedIntervals,
|
|
3553
|
+
) !== task.geometryTask
|
|
3554
|
+
) {
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3293
3557
|
}
|
|
3558
|
+
yield entry.value;
|
|
3294
3559
|
}
|
|
3295
|
-
yield entry.value;
|
|
3296
3560
|
}
|
|
3561
|
+
taskCompleted = iterator.done() === true;
|
|
3562
|
+
} finally {
|
|
3563
|
+
await iterator.close();
|
|
3564
|
+
}
|
|
3565
|
+
if (!taskCompleted) {
|
|
3566
|
+
return;
|
|
3297
3567
|
}
|
|
3298
|
-
passCompleted = iterator.done() === true;
|
|
3299
|
-
} finally {
|
|
3300
|
-
await iterator.close();
|
|
3301
3568
|
}
|
|
3302
|
-
if (!
|
|
3303
|
-
|
|
3569
|
+
if (!page.next) {
|
|
3570
|
+
break;
|
|
3304
3571
|
}
|
|
3572
|
+
cursor = page.next;
|
|
3305
3573
|
}
|
|
3306
3574
|
completed = true;
|
|
3307
3575
|
} finally {
|
|
3308
3576
|
// An early consumer return, iterator error, incomplete empty batch, or
|
|
3309
3577
|
// close failure in any sequential pass must leave history untouched so the
|
|
3310
3578
|
// next event retries the complete query. forceFresh preserves its bypass.
|
|
3311
|
-
if (completed
|
|
3312
|
-
|
|
3579
|
+
if (completed) {
|
|
3580
|
+
applyRebalanceHistoryMutations(
|
|
3581
|
+
plan.historyMutations,
|
|
3582
|
+
rebalanceHistory,
|
|
3583
|
+
);
|
|
3313
3584
|
}
|
|
3314
3585
|
}
|
|
3315
3586
|
},
|