@peerbit/shared-log 13.2.11 → 13.2.13

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.
Files changed (36) hide show
  1. package/dist/benchmark/receive-prune.js +31 -29
  2. package/dist/benchmark/receive-prune.js.map +1 -1
  3. package/dist/src/checked-prune.d.ts +43 -10
  4. package/dist/src/checked-prune.d.ts.map +1 -1
  5. package/dist/src/checked-prune.js +257 -27
  6. package/dist/src/checked-prune.js.map +1 -1
  7. package/dist/src/exchange-heads.d.ts +26 -0
  8. package/dist/src/exchange-heads.d.ts.map +1 -1
  9. package/dist/src/exchange-heads.js +99 -0
  10. package/dist/src/exchange-heads.js.map +1 -1
  11. package/dist/src/index.d.ts +71 -14
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/index.js +3492 -1493
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/ranges.d.ts +12 -1
  16. package/dist/src/ranges.d.ts.map +1 -1
  17. package/dist/src/ranges.js +472 -110
  18. package/dist/src/ranges.js.map +1 -1
  19. package/dist/src/sync/index.d.ts +2 -0
  20. package/dist/src/sync/index.d.ts.map +1 -1
  21. package/dist/src/sync/rateless-iblt.d.ts +96 -20
  22. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  23. package/dist/src/sync/rateless-iblt.js +1616 -428
  24. package/dist/src/sync/rateless-iblt.js.map +1 -1
  25. package/dist/src/sync/simple.d.ts +176 -1
  26. package/dist/src/sync/simple.d.ts.map +1 -1
  27. package/dist/src/sync/simple.js +2919 -525
  28. package/dist/src/sync/simple.js.map +1 -1
  29. package/package.json +13 -13
  30. package/src/checked-prune.ts +338 -29
  31. package/src/exchange-heads.ts +52 -0
  32. package/src/index.ts +6370 -2792
  33. package/src/ranges.ts +648 -127
  34. package/src/sync/index.ts +2 -1
  35. package/src/sync/rateless-iblt.ts +2036 -498
  36. package/src/sync/simple.ts +3779 -634
@@ -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 && (properties.gid == null || properties.wallTime == null)) {
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 && (properties.gid == null || properties.wallTime == null)) {
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 onlyStrict = true;
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
- onlyStrict = false;
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
- if (!onlyStrict || changes.length === 0) {
1997
- ors.push(new BoolQuery({
1998
- key: "assignedToRangeBoundary",
1999
- value: true,
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 replaced range as well. Adaptive replication can
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
- const key = change.type === "replaced"
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
- if (prev.range.timestamp < change.range.timestamp) {
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
- export const mergeReplicationChanges = (changesOrChangesArr, rebalanceHistory) => {
2047
- let first = changesOrChangesArr[0];
2048
- let changes;
2049
- if (!Array.isArray(first)) {
2050
- changes = changesOrChangesArr;
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
- else {
2053
- changes = changesOrChangesArr.flat();
2114
+ for (const range of after) {
2115
+ addRange(range, false);
2054
2116
  }
2055
- // group by hash so we can cancel out changes
2056
- const grouped = new Map();
2057
- for (const change of changes) {
2058
- const prev = grouped.get(change.range.hash);
2059
- if (prev) {
2060
- prev.push(change);
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
- else {
2063
- grouped.set(change.range.hash, [change]);
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
- let all = [];
2067
- for (const [_k, v] of grouped) {
2068
- if (v.length > 1) {
2069
- // sort by timestamp so newest is last
2070
- v.sort((a, b) => a.range.timestamp < b.range.timestamp
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.range.timestamp > b.range.timestamp
2241
+ : a.end > b.end
2073
2242
  ? 1
2074
2243
  : 0);
2075
- let results = [];
2076
- let consumed = new Set();
2077
- for (let i = 0; i < v.length; i++) {
2078
- // If segment is removed and we have previously processed it then go over each
2079
- // overlapping added segment and remove the overlap. Equivalent to: (1 - 1 + 1) = 1.
2080
- if (v[i].type === "removed" || v[i].type === "replaced") {
2081
- if (rebalanceHistory.has(v[i].range.rangeHash)) {
2082
- let adjusted = false;
2083
- const vStart = v.length;
2084
- for (let j = i + 1; j < vStart; j++) {
2085
- const newer = v[j];
2086
- if (newer.type === "added" && !newer.matured) {
2087
- adjusted = true;
2088
- const { rangesFromA: updatedRemoved, rangesFromB: updatedNewer, } = symmetricDifferenceRanges(v[i].range, newer.range);
2089
- for (const diff of updatedRemoved) {
2090
- results.push({
2091
- range: diff,
2092
- type: "removed",
2093
- timestamp: v[i].timestamp,
2094
- });
2095
- }
2096
- for (const diff of updatedNewer) {
2097
- v.push({
2098
- range: diff,
2099
- type: "added",
2100
- timestamp: newer.timestamp,
2101
- });
2102
- }
2103
- consumed.add(j);
2104
- }
2105
- }
2106
- rebalanceHistory.del(v[i].range.rangeHash);
2107
- if (!adjusted) {
2108
- results.push(v[i]);
2109
- }
2110
- }
2111
- else {
2112
- results.push(v[i]);
2113
- }
2114
- }
2115
- else if (v[i].type === "added") {
2116
- // TODO should the below clause be used?
2117
- // after testing it seems that certain changes are not propagating as expected using this
2118
- /* if (rebalanceHistory.has(v[i].range.rangeHash)) {
2119
- continue;
2120
- } */
2121
- rebalanceHistory.add(v[i].range.rangeHash);
2122
- if (!consumed.has(i)) {
2123
- results.push(v[i]);
2124
- }
2125
- }
2126
- else {
2127
- results.push(v[i]);
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
- all.push(...results);
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
- rebalanceHistory.add(v[0].range.rangeHash);
2134
- all.push(v[0]);
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
- return all;
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 change = options?.forceFresh
2142
- ? Array.isArray(changeOrChanges[0])
2143
- ? changeOrChanges.flat()
2144
- : changeOrChanges
2145
- : mergeReplicationChanges(changeOrChanges, rebalanceHistory);
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
- const iterator = index.iterate({
2149
- query: createAssignedRangesQuery(change),
2150
- });
2468
+ let completed = false;
2151
2469
  try {
2152
- while (iterator.done() !== true) {
2153
- const entries = await iterator.next(REBALANCE_ITERATOR_BATCH_SIZE);
2154
- if (entries.length === 0) {
2155
- break;
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
- for (const entry of entries) {
2158
- yield entry.value;
2513
+ if (!passCompleted) {
2514
+ return;
2159
2515
  }
2160
2516
  }
2517
+ completed = true;
2161
2518
  }
2162
2519
  finally {
2163
- await iterator.close();
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
  };