@peerbit/shared-log 13.2.11 → 13.2.12
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/ranges.d.ts +12 -1
- package/dist/src/ranges.d.ts.map +1 -1
- package/dist/src/ranges.js +472 -110
- package/dist/src/ranges.js.map +1 -1
- package/package.json +10 -10
- package/src/ranges.ts +648 -127
package/dist/src/ranges.js
CHANGED
|
@@ -137,7 +137,8 @@ let EntryReplicatedU32 = (() => {
|
|
|
137
137
|
if (!properties.meta && !properties.metaBytes) {
|
|
138
138
|
throw new Error("Expected meta or metaBytes");
|
|
139
139
|
}
|
|
140
|
-
if (!properties.meta &&
|
|
140
|
+
if (!properties.meta &&
|
|
141
|
+
(properties.gid == null || properties.wallTime == null)) {
|
|
141
142
|
throw new Error("Expected gid and wallTime with metaBytes");
|
|
142
143
|
}
|
|
143
144
|
this.coordinates = properties.coordinates;
|
|
@@ -228,7 +229,8 @@ let EntryReplicatedU64 = (() => {
|
|
|
228
229
|
if (!properties.meta && !properties.metaBytes) {
|
|
229
230
|
throw new Error("Expected meta or metaBytes");
|
|
230
231
|
}
|
|
231
|
-
if (!properties.meta &&
|
|
232
|
+
if (!properties.meta &&
|
|
233
|
+
(properties.gid == null || properties.wallTime == null)) {
|
|
232
234
|
throw new Error("Expected gid and wallTime with metaBytes");
|
|
233
235
|
}
|
|
234
236
|
this.coordinates = properties.coordinates;
|
|
@@ -835,6 +837,7 @@ let ReplicationRangeIndexableU32 = (() => {
|
|
|
835
837
|
}
|
|
836
838
|
equalRange(other) {
|
|
837
839
|
return (this.hash === other.hash &&
|
|
840
|
+
this.mode === other.mode &&
|
|
838
841
|
this.start1 === other.start1 &&
|
|
839
842
|
this.end1 === other.end1 &&
|
|
840
843
|
this.start2 === other.start2 &&
|
|
@@ -1010,6 +1013,7 @@ let ReplicationRangeIndexableU64 = (() => {
|
|
|
1010
1013
|
}
|
|
1011
1014
|
equalRange(other) {
|
|
1012
1015
|
return (this.hash === other.hash &&
|
|
1016
|
+
this.mode === other.mode &&
|
|
1013
1017
|
this.start1 === other.start1 &&
|
|
1014
1018
|
this.end1 === other.end1 &&
|
|
1015
1019
|
this.start2 === other.start2 &&
|
|
@@ -1982,25 +1986,38 @@ export const matchEntriesInRangeQuery = (range) => {
|
|
|
1982
1986
|
};
|
|
1983
1987
|
export const createAssignedRangesQuery = (changes, options) => {
|
|
1984
1988
|
let ors = [];
|
|
1985
|
-
let
|
|
1989
|
+
let includeBoundary = changes.length === 0 && options?.includeBoundaryWhenEmpty !== false;
|
|
1986
1990
|
// TODO what if the ranges are many many?
|
|
1987
1991
|
for (const change of changes) {
|
|
1992
|
+
if (change.rebalanceBoundaryOnly) {
|
|
1993
|
+
includeBoundary = true;
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1988
1996
|
const matchRange = matchEntriesInRangeQuery(change.range);
|
|
1989
1997
|
ors.push(matchRange);
|
|
1990
1998
|
if (change.range.mode === ReplicationIntent.NonStrict) {
|
|
1991
|
-
|
|
1999
|
+
includeBoundary = true;
|
|
1992
2000
|
}
|
|
1993
2001
|
}
|
|
1994
2002
|
// entry is assigned to a range boundary, meaning it is due to be inspected
|
|
1995
|
-
if (!options?.strict) {
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
}));
|
|
2001
|
-
}
|
|
2003
|
+
if (!options?.strict && includeBoundary) {
|
|
2004
|
+
ors.push(new BoolQuery({
|
|
2005
|
+
key: "assignedToRangeBoundary",
|
|
2006
|
+
value: true,
|
|
2007
|
+
}));
|
|
2002
2008
|
}
|
|
2003
2009
|
// entry is not sufficiently replicated, and we are to still keep it
|
|
2010
|
+
if (ors.length === 0) {
|
|
2011
|
+
// The index query language rejects Or([]). Use an explicit contradiction
|
|
2012
|
+
// for a non-empty source batch that mergeReplicationChanges proved is a no-op.
|
|
2013
|
+
// Keep the exported helper's historical Or return shape for source consumers.
|
|
2014
|
+
return new Or([
|
|
2015
|
+
new And([
|
|
2016
|
+
new BoolQuery({ key: "assignedToRangeBoundary", value: true }),
|
|
2017
|
+
new BoolQuery({ key: "assignedToRangeBoundary", value: false }),
|
|
2018
|
+
]),
|
|
2019
|
+
]);
|
|
2020
|
+
}
|
|
2004
2021
|
return new Or(ors);
|
|
2005
2022
|
};
|
|
2006
2023
|
export const debounceAggregationChanges = (fn, delay) => {
|
|
@@ -2017,16 +2034,26 @@ export const debounceAggregationChanges = (fn, delay) => {
|
|
|
2017
2034
|
// updates produce a `replaced` + `added` pair; collapsing by id would drop the
|
|
2018
2035
|
// "removed" portion and prevent correct rebalancing/pruning.
|
|
2019
2036
|
//
|
|
2020
|
-
// Preserve each distinct
|
|
2037
|
+
// Preserve each distinct retired range as well. Adaptive replication can
|
|
2021
2038
|
// shrink a segment several times before the debounced rebalance runs; if we
|
|
2022
2039
|
// keep only the newest `replaced` range, entries from earlier wider ranges are
|
|
2023
|
-
// never revisited and can stay resident after they become prunable.
|
|
2024
|
-
|
|
2040
|
+
// never revisited and can stay resident after they become prunable. Full reset
|
|
2041
|
+
// announcements have the same requirement: a queued A -> B -> C chain emits
|
|
2042
|
+
// removed(A), added(B), removed(B), added(C), so collapsing removals by id
|
|
2043
|
+
// would omit A-only entries.
|
|
2044
|
+
// Segment ids are peer-supplied and only identify state within an owner, so
|
|
2045
|
+
// live additions must include the owner hash to avoid cross-peer collisions.
|
|
2046
|
+
const key = change.type === "replaced" || change.type === "removed"
|
|
2025
2047
|
? `${change.type}:${change.range.idString}:${change.range.rangeHash}`
|
|
2026
|
-
: `${change.type}:${change.range.idString}`;
|
|
2048
|
+
: `${change.type}:${change.range.hash}:${change.range.idString}`;
|
|
2027
2049
|
const prev = aggregated.get(key);
|
|
2028
2050
|
if (prev) {
|
|
2029
|
-
|
|
2051
|
+
// Replication ingress rejects strictly stale observations, while equal
|
|
2052
|
+
// timestamps are valid and ordered by arrival. Replace equal/newer keyed
|
|
2053
|
+
// values and move them to the Map tail so a rapid A -> B -> C update
|
|
2054
|
+
// emits replaced(A), replaced(B), added(C) in observed order.
|
|
2055
|
+
if (prev.range.timestamp <= change.range.timestamp) {
|
|
2056
|
+
aggregated.delete(key);
|
|
2030
2057
|
aggregated.set(key, change);
|
|
2031
2058
|
}
|
|
2032
2059
|
}
|
|
@@ -2043,124 +2070,459 @@ export const debounceAggregationChanges = (fn, delay) => {
|
|
|
2043
2070
|
};
|
|
2044
2071
|
}, delay);
|
|
2045
2072
|
};
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2073
|
+
const flattenReplicationChanges = (changesOrChangesArr) => {
|
|
2074
|
+
const first = changesOrChangesArr[0];
|
|
2075
|
+
return Array.isArray(first)
|
|
2076
|
+
? changesOrChangesArr.flat()
|
|
2077
|
+
: [
|
|
2078
|
+
...changesOrChangesArr,
|
|
2079
|
+
];
|
|
2080
|
+
};
|
|
2081
|
+
const diffReplicationProfiles = (before, after) => {
|
|
2082
|
+
const events = new Map();
|
|
2083
|
+
const updateEvent = (point, property, delta) => {
|
|
2084
|
+
let event = events.get(point);
|
|
2085
|
+
if (!event) {
|
|
2086
|
+
event = {
|
|
2087
|
+
before: 0,
|
|
2088
|
+
beforeNonStrict: 0,
|
|
2089
|
+
after: 0,
|
|
2090
|
+
afterNonStrict: 0,
|
|
2091
|
+
};
|
|
2092
|
+
events.set(point, event);
|
|
2093
|
+
}
|
|
2094
|
+
event[property] += delta;
|
|
2095
|
+
};
|
|
2096
|
+
const addRange = (range, isBefore) => {
|
|
2097
|
+
const coverageProperty = isBefore ? "before" : "after";
|
|
2098
|
+
const nonStrictProperty = isBefore ? "beforeNonStrict" : "afterNonStrict";
|
|
2099
|
+
for (const [start, end] of toSegmentsBigInt(range)) {
|
|
2100
|
+
if (start >= end) {
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
updateEvent(start, coverageProperty, 1);
|
|
2104
|
+
updateEvent(end, coverageProperty, -1);
|
|
2105
|
+
if (range.mode === ReplicationIntent.NonStrict) {
|
|
2106
|
+
updateEvent(start, nonStrictProperty, 1);
|
|
2107
|
+
updateEvent(end, nonStrictProperty, -1);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2111
|
+
for (const range of before) {
|
|
2112
|
+
addRange(range, true);
|
|
2051
2113
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2114
|
+
for (const range of after) {
|
|
2115
|
+
addRange(range, false);
|
|
2054
2116
|
}
|
|
2055
|
-
|
|
2056
|
-
const
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2117
|
+
const points = [...events.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
2118
|
+
const geometry = [];
|
|
2119
|
+
let boundary = false;
|
|
2120
|
+
let beforeCoverage = 0;
|
|
2121
|
+
let beforeNonStrict = 0;
|
|
2122
|
+
let afterCoverage = 0;
|
|
2123
|
+
let afterNonStrict = 0;
|
|
2124
|
+
for (let i = 0; i + 1 < points.length; i++) {
|
|
2125
|
+
const point = points[i];
|
|
2126
|
+
const event = events.get(point);
|
|
2127
|
+
beforeCoverage += event.before;
|
|
2128
|
+
beforeNonStrict += event.beforeNonStrict;
|
|
2129
|
+
afterCoverage += event.after;
|
|
2130
|
+
afterNonStrict += event.afterNonStrict;
|
|
2131
|
+
const next = points[i + 1];
|
|
2132
|
+
if (point >= next) {
|
|
2133
|
+
continue;
|
|
2061
2134
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2135
|
+
if (beforeCoverage > 0 !== afterCoverage > 0) {
|
|
2136
|
+
geometry.push([point, next]);
|
|
2137
|
+
}
|
|
2138
|
+
if (beforeNonStrict > 0 !== afterNonStrict > 0) {
|
|
2139
|
+
boundary = true;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return { geometry: mergeBigIntSegments(geometry), boundary };
|
|
2143
|
+
};
|
|
2144
|
+
const buildStrictQueryRanges = (segments, template) => {
|
|
2145
|
+
const merged = mergeBigIntSegments(segments
|
|
2146
|
+
.filter(([start, end]) => start < end)
|
|
2147
|
+
.map(([start, end]) => [start, end]));
|
|
2148
|
+
if (merged.length === 0) {
|
|
2149
|
+
return [];
|
|
2150
|
+
}
|
|
2151
|
+
const max = isU32Range(template) ? BigInt(MAX_U32) : MAX_U64;
|
|
2152
|
+
const arcs = [];
|
|
2153
|
+
if (merged.length > 1 &&
|
|
2154
|
+
merged[0][0] === 0n &&
|
|
2155
|
+
merged[merged.length - 1][1] === max) {
|
|
2156
|
+
arcs.push({
|
|
2157
|
+
first: merged[merged.length - 1],
|
|
2158
|
+
second: merged[0],
|
|
2159
|
+
});
|
|
2160
|
+
for (let i = 1; i + 1 < merged.length; i++) {
|
|
2161
|
+
arcs.push({ first: merged[i] });
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
else {
|
|
2165
|
+
for (const segment of merged) {
|
|
2166
|
+
arcs.push({ first: segment });
|
|
2064
2167
|
}
|
|
2065
2168
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2169
|
+
const proto = Object.getPrototypeOf(template);
|
|
2170
|
+
return arcs.map(({ first, second }) => {
|
|
2171
|
+
const [start1, end1] = toOriginalType(first, template);
|
|
2172
|
+
const [start2, end2] = second
|
|
2173
|
+
? toOriginalType(second, template)
|
|
2174
|
+
: [start1, end1];
|
|
2175
|
+
const widthBigInt = first[1] - first[0] + (second ? second[1] - second[0] : 0n);
|
|
2176
|
+
const width = (isU32Range(template) ? Number(widthBigInt) : widthBigInt);
|
|
2177
|
+
return Object.assign(Object.create(proto), {
|
|
2178
|
+
...template,
|
|
2179
|
+
start1,
|
|
2180
|
+
end1,
|
|
2181
|
+
start2,
|
|
2182
|
+
end2,
|
|
2183
|
+
width,
|
|
2184
|
+
mode: ReplicationIntent.Strict,
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
};
|
|
2188
|
+
const replicationRangeGeometryKey = (range) => toSegmentsBigInt(range)
|
|
2189
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
2190
|
+
.map(([start, end]) => `${start}:${end}`)
|
|
2191
|
+
.join("|");
|
|
2192
|
+
// SQLite's query builder produces a left-deep OR expression. Keep a wide safety
|
|
2193
|
+
// margin below SQLite's default expression-depth limit (1000), including the
|
|
2194
|
+
// nested comparisons needed for wrapped ranges.
|
|
2195
|
+
const MAX_REBALANCE_QUERY_RANGES = 128;
|
|
2196
|
+
const createRebalanceQueryPlan = (changes, includeBoundaryWhenEmpty) => {
|
|
2197
|
+
const boundarySource = changes.find((change) => change.rebalanceBoundaryOnly ||
|
|
2198
|
+
change.range.mode === ReplicationIntent.NonStrict);
|
|
2199
|
+
const boundaryChanges = boundarySource || includeBoundaryWhenEmpty
|
|
2200
|
+
? boundarySource
|
|
2201
|
+
? [{ ...boundarySource, rebalanceBoundaryOnly: true }]
|
|
2202
|
+
: []
|
|
2203
|
+
: undefined;
|
|
2204
|
+
const geometryChanges = changes.filter((change) => !change.rebalanceBoundaryOnly);
|
|
2205
|
+
if (geometryChanges.length === 0) {
|
|
2206
|
+
return { boundaryChanges, geometryBatches: [], ownedIntervals: [] };
|
|
2207
|
+
}
|
|
2208
|
+
const template = geometryChanges[geometryChanges.length - 1];
|
|
2209
|
+
const exactSegments = mergeBigIntSegments(geometryChanges
|
|
2210
|
+
.flatMap((change) => toSegmentsBigInt(change.range))
|
|
2211
|
+
.filter(([start, end]) => start < end)
|
|
2212
|
+
.map(([start, end]) => [start, end]));
|
|
2213
|
+
const exactRanges = buildStrictQueryRanges(exactSegments, template.range);
|
|
2214
|
+
const exactChanges = exactRanges.map((range) => ({
|
|
2215
|
+
range,
|
|
2216
|
+
type: "replaced",
|
|
2217
|
+
timestamp: template.timestamp,
|
|
2218
|
+
}));
|
|
2219
|
+
const geometryBatches = [];
|
|
2220
|
+
for (let offset = 0; offset < exactChanges.length; offset += MAX_REBALANCE_QUERY_RANGES) {
|
|
2221
|
+
geometryBatches.push(exactChanges.slice(offset, offset + MAX_REBALANCE_QUERY_RANGES));
|
|
2222
|
+
}
|
|
2223
|
+
// A row can contain coordinates in several query batches. Assign every exact
|
|
2224
|
+
// linear interval to its batch up front so candidates can be de-duplicated with
|
|
2225
|
+
// O(component-count) plan memory instead of an unbounded set of emitted hashes.
|
|
2226
|
+
// The two linear pieces of a wrapped seam range inherit the same batch tag.
|
|
2227
|
+
const ownedIntervals = exactRanges
|
|
2228
|
+
.flatMap((range, rangeIndex) => toSegmentsBigInt(range)
|
|
2229
|
+
.filter(([start, end]) => start < end)
|
|
2230
|
+
.map(([start, end]) => ({
|
|
2231
|
+
start,
|
|
2232
|
+
end,
|
|
2233
|
+
batch: Math.floor(rangeIndex / MAX_REBALANCE_QUERY_RANGES),
|
|
2234
|
+
})))
|
|
2235
|
+
.sort((a, b) => a.start < b.start
|
|
2236
|
+
? -1
|
|
2237
|
+
: a.start > b.start
|
|
2238
|
+
? 1
|
|
2239
|
+
: a.end < b.end
|
|
2071
2240
|
? -1
|
|
2072
|
-
: a.
|
|
2241
|
+
: a.end > b.end
|
|
2073
2242
|
? 1
|
|
2074
2243
|
: 0);
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2244
|
+
for (let i = 1; i < ownedIntervals.length; i++) {
|
|
2245
|
+
if (ownedIntervals[i - 1].end > ownedIntervals[i].start) {
|
|
2246
|
+
throw new Error("Rebalance query intervals must not overlap");
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return { boundaryChanges, geometryBatches, ownedIntervals };
|
|
2250
|
+
};
|
|
2251
|
+
const findOwningRebalanceBatch = (coordinates, intervals) => {
|
|
2252
|
+
let minimum;
|
|
2253
|
+
for (const value of coordinates) {
|
|
2254
|
+
const coordinate = typeof value === "number" ? BigInt(value) : value;
|
|
2255
|
+
let low = 0;
|
|
2256
|
+
let high = intervals.length - 1;
|
|
2257
|
+
while (low <= high) {
|
|
2258
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
2259
|
+
const interval = intervals[middle];
|
|
2260
|
+
if (coordinate < interval.start) {
|
|
2261
|
+
high = middle - 1;
|
|
2262
|
+
}
|
|
2263
|
+
else if (coordinate >= interval.end) {
|
|
2264
|
+
low = middle + 1;
|
|
2265
|
+
}
|
|
2266
|
+
else {
|
|
2267
|
+
minimum =
|
|
2268
|
+
minimum == null ? interval.batch : Math.min(minimum, interval.batch);
|
|
2269
|
+
break;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
if (minimum === 0) {
|
|
2273
|
+
break;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
return minimum;
|
|
2277
|
+
};
|
|
2278
|
+
const rotateRebalanceHistory = (changes, rebalanceHistory) => {
|
|
2279
|
+
// Only original source changes may enter history, and observed order defines
|
|
2280
|
+
// the terminal state: add -> remove ends absent; remove -> add ends present.
|
|
2281
|
+
for (const change of changes) {
|
|
2282
|
+
if (change.type === "added") {
|
|
2283
|
+
rebalanceHistory.add(change.range.rangeHash);
|
|
2284
|
+
}
|
|
2285
|
+
else {
|
|
2286
|
+
rebalanceHistory.del(change.range.rangeHash);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
export const mergeReplicationChanges = (changesOrChangesArr, rebalanceHistory, options) => {
|
|
2291
|
+
// Work from an immutable snapshot. In particular, synthetic difference ranges
|
|
2292
|
+
// must never become input events or rebalance-history entries.
|
|
2293
|
+
const changes = flattenReplicationChanges(changesOrChangesArr);
|
|
2294
|
+
const originals = changes.map((change, index) => ({
|
|
2295
|
+
change,
|
|
2296
|
+
index,
|
|
2297
|
+
wasInHistory: rebalanceHistory.has(change.range.rangeHash),
|
|
2298
|
+
}));
|
|
2299
|
+
if (originals.length === 0) {
|
|
2300
|
+
return [];
|
|
2301
|
+
}
|
|
2302
|
+
const consumed = new Set();
|
|
2303
|
+
const fragmentGroups = new Map();
|
|
2304
|
+
let boundarySource;
|
|
2305
|
+
const addFragments = (owner, segments, template) => {
|
|
2306
|
+
if (segments.length === 0) {
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
const existing = fragmentGroups.get(owner);
|
|
2310
|
+
if (existing) {
|
|
2311
|
+
existing.segments.push(...segments);
|
|
2312
|
+
if (existing.template.change.range.timestamp <
|
|
2313
|
+
template.change.range.timestamp) {
|
|
2314
|
+
existing.template = template;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
else {
|
|
2318
|
+
fragmentGroups.set(owner, {
|
|
2319
|
+
segments: [...segments],
|
|
2320
|
+
template,
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
const requireBoundary = (source) => {
|
|
2325
|
+
if (!boundarySource) {
|
|
2326
|
+
boundarySource = source;
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
// Replacement provenance is exact: a segment id describes a sequence of real
|
|
2330
|
+
// states. Compare each adjacent state once, then union the resulting query arcs.
|
|
2331
|
+
const replacementGroups = new Map();
|
|
2332
|
+
for (const original of originals) {
|
|
2333
|
+
if (original.change.type !== "replaced" &&
|
|
2334
|
+
(original.change.type !== "added" || original.change.matured)) {
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
const key = `${original.change.range.hash}:${original.change.range.idString}`;
|
|
2338
|
+
const group = replacementGroups.get(key);
|
|
2339
|
+
if (group) {
|
|
2340
|
+
group.push(original);
|
|
2341
|
+
}
|
|
2342
|
+
else {
|
|
2343
|
+
replacementGroups.set(key, [original]);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
for (const statesUnsorted of replacementGroups.values()) {
|
|
2347
|
+
if (!statesUnsorted.some((state) => state.change.type === "replaced") ||
|
|
2348
|
+
!statesUnsorted.some((state) => state.change.type === "added")) {
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
// Range timestamps express role age and may intentionally stay fixed across
|
|
2352
|
+
// adaptive geometry updates. The debounced batch order is the authoritative
|
|
2353
|
+
// state-transition order.
|
|
2354
|
+
const states = [...statesUnsorted].sort((a, b) => a.index - b.index);
|
|
2355
|
+
if (states[states.length - 1].change.type !== "added") {
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
// The overlap between adjacent replacement states is safe to omit only if
|
|
2359
|
+
// the first state was successfully processed before this batch. A replacement
|
|
2360
|
+
// event describes retired geometry, not proof that its overlap was queried.
|
|
2361
|
+
// Without that proof, leave the complete real states in the result so their
|
|
2362
|
+
// union (including the overlap) is inspected.
|
|
2363
|
+
if (!states[0].wasInHistory) {
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
for (let i = 0; i + 1 < states.length; i++) {
|
|
2367
|
+
const previous = states[i];
|
|
2368
|
+
const next = states[i + 1];
|
|
2369
|
+
const difference = diffReplicationProfiles([previous.change.range], [next.change.range]);
|
|
2370
|
+
addFragments(previous.change.range.hash, difference.geometry, next);
|
|
2371
|
+
if (difference.boundary) {
|
|
2372
|
+
requireBoundary(next);
|
|
2129
2373
|
}
|
|
2130
|
-
|
|
2374
|
+
}
|
|
2375
|
+
for (const state of states) {
|
|
2376
|
+
consumed.add(state.index);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
// Plain reset-style removals/additions have no id provenance. Only removals
|
|
2380
|
+
// whose old state was actually processed may cancel new owner state. Compare
|
|
2381
|
+
// their aggregate owner profiles; leave all unrelated/unmatched changes whole.
|
|
2382
|
+
const plainGroups = new Map();
|
|
2383
|
+
for (const original of originals) {
|
|
2384
|
+
if (consumed.has(original.index) ||
|
|
2385
|
+
(original.change.type !== "removed" && original.change.type !== "added")) {
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
const group = plainGroups.get(original.change.range.hash);
|
|
2389
|
+
if (group) {
|
|
2390
|
+
group.push(original);
|
|
2131
2391
|
}
|
|
2132
2392
|
else {
|
|
2133
|
-
|
|
2134
|
-
|
|
2393
|
+
plainGroups.set(original.change.range.hash, [original]);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
for (const ownerChanges of plainGroups.values()) {
|
|
2397
|
+
const removed = ownerChanges.filter((original) => original.change.type === "removed" && original.wasInHistory);
|
|
2398
|
+
const added = ownerChanges.filter((original) => original.change.type === "added" && !original.change.matured);
|
|
2399
|
+
if (removed.length === 0 || added.length === 0) {
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
const difference = diffReplicationProfiles(removed.map((original) => original.change.range), added.map((original) => original.change.range));
|
|
2403
|
+
const newestAdded = added.reduce((newest, current) => newest.change.range.timestamp >= current.change.range.timestamp
|
|
2404
|
+
? newest
|
|
2405
|
+
: current);
|
|
2406
|
+
addFragments(newestAdded.change.range.hash, difference.geometry, newestAdded);
|
|
2407
|
+
if (difference.boundary) {
|
|
2408
|
+
requireBoundary(newestAdded);
|
|
2409
|
+
}
|
|
2410
|
+
for (const original of [...removed, ...added]) {
|
|
2411
|
+
consumed.add(original.index);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
const results = originals
|
|
2415
|
+
.filter((original) => !consumed.has(original.index))
|
|
2416
|
+
.map((original) => original.change);
|
|
2417
|
+
for (const { segments, template } of fragmentGroups.values()) {
|
|
2418
|
+
for (const range of buildStrictQueryRanges(segments, template.change.range)) {
|
|
2419
|
+
results.push({
|
|
2420
|
+
range,
|
|
2421
|
+
type: "replaced",
|
|
2422
|
+
timestamp: template.change.timestamp,
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
// Query type and owner do not affect range matching. Remove exact geometry
|
|
2427
|
+
// duplicates while preferring a non-strict original when it carries boundary
|
|
2428
|
+
// semantics of its own.
|
|
2429
|
+
const deduplicated = [];
|
|
2430
|
+
const positions = new Map();
|
|
2431
|
+
for (const result of results) {
|
|
2432
|
+
if (result.rebalanceBoundaryOnly) {
|
|
2433
|
+
continue;
|
|
2135
2434
|
}
|
|
2435
|
+
const key = replicationRangeGeometryKey(result.range);
|
|
2436
|
+
const previousPosition = positions.get(key);
|
|
2437
|
+
if (previousPosition == null) {
|
|
2438
|
+
positions.set(key, deduplicated.length);
|
|
2439
|
+
deduplicated.push(result);
|
|
2440
|
+
}
|
|
2441
|
+
else if (deduplicated[previousPosition].range.mode === ReplicationIntent.Strict &&
|
|
2442
|
+
result.range.mode === ReplicationIntent.NonStrict) {
|
|
2443
|
+
deduplicated[previousPosition] = result;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
if (boundarySource) {
|
|
2447
|
+
deduplicated.push({
|
|
2448
|
+
...boundarySource.change,
|
|
2449
|
+
rebalanceBoundaryOnly: true,
|
|
2450
|
+
});
|
|
2136
2451
|
}
|
|
2137
|
-
|
|
2452
|
+
if (options?.updateHistory !== false) {
|
|
2453
|
+
rotateRebalanceHistory(changes, rebalanceHistory);
|
|
2454
|
+
}
|
|
2455
|
+
return deduplicated;
|
|
2138
2456
|
};
|
|
2139
2457
|
const REBALANCE_ITERATOR_BATCH_SIZE = 1048;
|
|
2140
2458
|
export const toRebalance = (changeOrChanges, index, rebalanceHistory, options) => {
|
|
2141
|
-
const
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2459
|
+
const sourceChanges = flattenReplicationChanges(changeOrChanges);
|
|
2460
|
+
const queryChanges = options?.forceFresh
|
|
2461
|
+
? sourceChanges
|
|
2462
|
+
: mergeReplicationChanges(sourceChanges, rebalanceHistory, {
|
|
2463
|
+
updateHistory: false,
|
|
2464
|
+
});
|
|
2465
|
+
const plan = createRebalanceQueryPlan(queryChanges, sourceChanges.length === 0);
|
|
2146
2466
|
return {
|
|
2147
2467
|
[Symbol.asyncIterator]: async function* () {
|
|
2148
|
-
|
|
2149
|
-
query: createAssignedRangesQuery(change),
|
|
2150
|
-
});
|
|
2468
|
+
let completed = false;
|
|
2151
2469
|
try {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2470
|
+
const passes = [];
|
|
2471
|
+
if (plan.boundaryChanges) {
|
|
2472
|
+
passes.push({ changes: plan.boundaryChanges });
|
|
2473
|
+
}
|
|
2474
|
+
for (let batch = 0; batch < plan.geometryBatches.length; batch++) {
|
|
2475
|
+
passes.push({
|
|
2476
|
+
changes: plan.geometryBatches[batch],
|
|
2477
|
+
geometryBatch: batch,
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
for (const pass of passes) {
|
|
2481
|
+
const isGeometry = pass.geometryBatch != null;
|
|
2482
|
+
const iterator = index.iterate({
|
|
2483
|
+
query: createAssignedRangesQuery(pass.changes, {
|
|
2484
|
+
strict: isGeometry,
|
|
2485
|
+
includeBoundaryWhenEmpty: !isGeometry,
|
|
2486
|
+
}),
|
|
2487
|
+
});
|
|
2488
|
+
let passCompleted = false;
|
|
2489
|
+
try {
|
|
2490
|
+
while (iterator.done() !== true) {
|
|
2491
|
+
const entries = await iterator.next(REBALANCE_ITERATOR_BATCH_SIZE);
|
|
2492
|
+
if (entries.length === 0) {
|
|
2493
|
+
break;
|
|
2494
|
+
}
|
|
2495
|
+
for (const entry of entries) {
|
|
2496
|
+
if (isGeometry) {
|
|
2497
|
+
if (plan.boundaryChanges &&
|
|
2498
|
+
entry.value.assignedToRangeBoundary) {
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
if (findOwningRebalanceBatch(entry.value.coordinates, plan.ownedIntervals) !== pass.geometryBatch) {
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
yield entry.value;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
passCompleted = iterator.done() === true;
|
|
2509
|
+
}
|
|
2510
|
+
finally {
|
|
2511
|
+
await iterator.close();
|
|
2156
2512
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2513
|
+
if (!passCompleted) {
|
|
2514
|
+
return;
|
|
2159
2515
|
}
|
|
2160
2516
|
}
|
|
2517
|
+
completed = true;
|
|
2161
2518
|
}
|
|
2162
2519
|
finally {
|
|
2163
|
-
|
|
2520
|
+
// An early consumer return, iterator error, incomplete empty batch, or
|
|
2521
|
+
// close failure in any sequential pass must leave history untouched so the
|
|
2522
|
+
// next event retries the complete query. forceFresh preserves its bypass.
|
|
2523
|
+
if (completed && !options?.forceFresh) {
|
|
2524
|
+
rotateRebalanceHistory(sourceChanges, rebalanceHistory);
|
|
2525
|
+
}
|
|
2164
2526
|
}
|
|
2165
2527
|
},
|
|
2166
2528
|
};
|