@peerbit/shared-log 13.2.10 → 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/index.d.ts +90 -9
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1643 -203
- package/dist/src/index.js.map +1 -1
- 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 +15 -15
- package/src/index.ts +2316 -281
- package/src/ranges.ts +648 -127
package/src/ranges.ts
CHANGED
|
@@ -141,7 +141,10 @@ export class EntryReplicatedU32 implements EntryReplicated<"u32"> {
|
|
|
141
141
|
if (!properties.meta && !properties.metaBytes) {
|
|
142
142
|
throw new Error("Expected meta or metaBytes");
|
|
143
143
|
}
|
|
144
|
-
if (
|
|
144
|
+
if (
|
|
145
|
+
!properties.meta &&
|
|
146
|
+
(properties.gid == null || properties.wallTime == null)
|
|
147
|
+
) {
|
|
145
148
|
throw new Error("Expected gid and wallTime with metaBytes");
|
|
146
149
|
}
|
|
147
150
|
this.coordinates = properties.coordinates;
|
|
@@ -211,7 +214,10 @@ export class EntryReplicatedU64 implements EntryReplicated<"u64"> {
|
|
|
211
214
|
if (!properties.meta && !properties.metaBytes) {
|
|
212
215
|
throw new Error("Expected meta or metaBytes");
|
|
213
216
|
}
|
|
214
|
-
if (
|
|
217
|
+
if (
|
|
218
|
+
!properties.meta &&
|
|
219
|
+
(properties.gid == null || properties.wallTime == null)
|
|
220
|
+
) {
|
|
215
221
|
throw new Error("Expected gid and wallTime with metaBytes");
|
|
216
222
|
}
|
|
217
223
|
this.coordinates = properties.coordinates;
|
|
@@ -841,6 +847,7 @@ export class ReplicationRangeIndexableU32
|
|
|
841
847
|
equalRange(other: ReplicationRangeIndexableU32) {
|
|
842
848
|
return (
|
|
843
849
|
this.hash === other.hash &&
|
|
850
|
+
this.mode === other.mode &&
|
|
844
851
|
this.start1 === other.start1 &&
|
|
845
852
|
this.end1 === other.end1 &&
|
|
846
853
|
this.start2 === other.start2 &&
|
|
@@ -1020,6 +1027,7 @@ export class ReplicationRangeIndexableU64
|
|
|
1020
1027
|
equalRange(other: ReplicationRangeIndexableU64) {
|
|
1021
1028
|
return (
|
|
1022
1029
|
this.hash === other.hash &&
|
|
1030
|
+
this.mode === other.mode &&
|
|
1023
1031
|
this.start1 === other.start1 &&
|
|
1024
1032
|
this.end1 === other.end1 &&
|
|
1025
1033
|
this.start2 === other.start2 &&
|
|
@@ -2538,33 +2546,49 @@ export const createAssignedRangesQuery = (
|
|
|
2538
2546
|
end2: number | bigint;
|
|
2539
2547
|
mode: ReplicationIntent;
|
|
2540
2548
|
};
|
|
2549
|
+
/** Internal query-only marker. This wrapper field is never serialized. */
|
|
2550
|
+
rebalanceBoundaryOnly?: boolean;
|
|
2541
2551
|
}[],
|
|
2542
|
-
options?: { strict?: boolean },
|
|
2552
|
+
options?: { strict?: boolean; includeBoundaryWhenEmpty?: boolean },
|
|
2543
2553
|
) => {
|
|
2544
2554
|
let ors: Query[] = [];
|
|
2545
|
-
let
|
|
2555
|
+
let includeBoundary =
|
|
2556
|
+
changes.length === 0 && options?.includeBoundaryWhenEmpty !== false;
|
|
2546
2557
|
// TODO what if the ranges are many many?
|
|
2547
2558
|
for (const change of changes) {
|
|
2559
|
+
if (change.rebalanceBoundaryOnly) {
|
|
2560
|
+
includeBoundary = true;
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2548
2563
|
const matchRange = matchEntriesInRangeQuery(change.range);
|
|
2549
2564
|
ors.push(matchRange);
|
|
2550
2565
|
if (change.range.mode === ReplicationIntent.NonStrict) {
|
|
2551
|
-
|
|
2566
|
+
includeBoundary = true;
|
|
2552
2567
|
}
|
|
2553
2568
|
}
|
|
2554
2569
|
|
|
2555
2570
|
// entry is assigned to a range boundary, meaning it is due to be inspected
|
|
2556
|
-
if (!options?.strict) {
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
);
|
|
2564
|
-
}
|
|
2571
|
+
if (!options?.strict && includeBoundary) {
|
|
2572
|
+
ors.push(
|
|
2573
|
+
new BoolQuery({
|
|
2574
|
+
key: "assignedToRangeBoundary",
|
|
2575
|
+
value: true,
|
|
2576
|
+
}),
|
|
2577
|
+
);
|
|
2565
2578
|
}
|
|
2566
2579
|
|
|
2567
2580
|
// entry is not sufficiently replicated, and we are to still keep it
|
|
2581
|
+
if (ors.length === 0) {
|
|
2582
|
+
// The index query language rejects Or([]). Use an explicit contradiction
|
|
2583
|
+
// for a non-empty source batch that mergeReplicationChanges proved is a no-op.
|
|
2584
|
+
// Keep the exported helper's historical Or return shape for source consumers.
|
|
2585
|
+
return new Or([
|
|
2586
|
+
new And([
|
|
2587
|
+
new BoolQuery({ key: "assignedToRangeBoundary", value: true }),
|
|
2588
|
+
new BoolQuery({ key: "assignedToRangeBoundary", value: false }),
|
|
2589
|
+
]),
|
|
2590
|
+
]);
|
|
2591
|
+
}
|
|
2568
2592
|
return new Or(ors);
|
|
2569
2593
|
};
|
|
2570
2594
|
|
|
@@ -2587,7 +2611,15 @@ export type ReplicationChange<
|
|
|
2587
2611
|
type: "replaced";
|
|
2588
2612
|
range: T;
|
|
2589
2613
|
}
|
|
2590
|
-
) & {
|
|
2614
|
+
) & {
|
|
2615
|
+
timestamp: bigint;
|
|
2616
|
+
/**
|
|
2617
|
+
* Internal, non-wire query intent emitted by mergeReplicationChanges.
|
|
2618
|
+
* When set, createAssignedRangesQuery performs only the global boundary
|
|
2619
|
+
* re-evaluation and deliberately ignores this wrapper's range geometry.
|
|
2620
|
+
*/
|
|
2621
|
+
rebalanceBoundaryOnly?: boolean;
|
|
2622
|
+
};
|
|
2591
2623
|
|
|
2592
2624
|
export const debounceAggregationChanges = <
|
|
2593
2625
|
T extends ReplicationRangeIndexable<any>,
|
|
@@ -2610,17 +2642,27 @@ export const debounceAggregationChanges = <
|
|
|
2610
2642
|
// updates produce a `replaced` + `added` pair; collapsing by id would drop the
|
|
2611
2643
|
// "removed" portion and prevent correct rebalancing/pruning.
|
|
2612
2644
|
//
|
|
2613
|
-
// Preserve each distinct
|
|
2645
|
+
// Preserve each distinct retired range as well. Adaptive replication can
|
|
2614
2646
|
// shrink a segment several times before the debounced rebalance runs; if we
|
|
2615
2647
|
// keep only the newest `replaced` range, entries from earlier wider ranges are
|
|
2616
|
-
// never revisited and can stay resident after they become prunable.
|
|
2648
|
+
// never revisited and can stay resident after they become prunable. Full reset
|
|
2649
|
+
// announcements have the same requirement: a queued A -> B -> C chain emits
|
|
2650
|
+
// removed(A), added(B), removed(B), added(C), so collapsing removals by id
|
|
2651
|
+
// would omit A-only entries.
|
|
2652
|
+
// Segment ids are peer-supplied and only identify state within an owner, so
|
|
2653
|
+
// live additions must include the owner hash to avoid cross-peer collisions.
|
|
2617
2654
|
const key =
|
|
2618
|
-
change.type === "replaced"
|
|
2655
|
+
change.type === "replaced" || change.type === "removed"
|
|
2619
2656
|
? `${change.type}:${change.range.idString}:${change.range.rangeHash}`
|
|
2620
|
-
: `${change.type}:${change.range.idString}`;
|
|
2657
|
+
: `${change.type}:${change.range.hash}:${change.range.idString}`;
|
|
2621
2658
|
const prev = aggregated.get(key);
|
|
2622
2659
|
if (prev) {
|
|
2623
|
-
|
|
2660
|
+
// Replication ingress rejects strictly stale observations, while equal
|
|
2661
|
+
// timestamps are valid and ordered by arrival. Replace equal/newer keyed
|
|
2662
|
+
// values and move them to the Map tail so a rapid A -> B -> C update
|
|
2663
|
+
// emits replaced(A), replaced(B), added(C) in observed order.
|
|
2664
|
+
if (prev.range.timestamp <= change.range.timestamp) {
|
|
2665
|
+
aggregated.delete(key);
|
|
2624
2666
|
aggregated.set(key, change);
|
|
2625
2667
|
}
|
|
2626
2668
|
} else {
|
|
@@ -2639,115 +2681,537 @@ export const debounceAggregationChanges = <
|
|
|
2639
2681
|
);
|
|
2640
2682
|
};
|
|
2641
2683
|
|
|
2684
|
+
type IndexedReplicationChange<R extends NumericType> = {
|
|
2685
|
+
change: ReplicationChange<ReplicationRangeIndexable<R>>;
|
|
2686
|
+
index: number;
|
|
2687
|
+
wasInHistory: boolean;
|
|
2688
|
+
};
|
|
2689
|
+
|
|
2690
|
+
const flattenReplicationChanges = <R extends NumericType>(
|
|
2691
|
+
changesOrChangesArr:
|
|
2692
|
+
| ReplicationChanges<ReplicationRangeIndexable<R>>
|
|
2693
|
+
| ReplicationChanges<ReplicationRangeIndexable<R>>[],
|
|
2694
|
+
): ReplicationChange<ReplicationRangeIndexable<R>>[] => {
|
|
2695
|
+
const first = changesOrChangesArr[0];
|
|
2696
|
+
return Array.isArray(first)
|
|
2697
|
+
? (
|
|
2698
|
+
changesOrChangesArr as ReplicationChanges<
|
|
2699
|
+
ReplicationRangeIndexable<R>
|
|
2700
|
+
>[]
|
|
2701
|
+
).flat()
|
|
2702
|
+
: [
|
|
2703
|
+
...(changesOrChangesArr as ReplicationChanges<
|
|
2704
|
+
ReplicationRangeIndexable<R>
|
|
2705
|
+
>),
|
|
2706
|
+
];
|
|
2707
|
+
};
|
|
2708
|
+
|
|
2709
|
+
type ReplicationProfileDelta = {
|
|
2710
|
+
before: number;
|
|
2711
|
+
beforeNonStrict: number;
|
|
2712
|
+
after: number;
|
|
2713
|
+
afterNonStrict: number;
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
const diffReplicationProfiles = <R extends NumericType>(
|
|
2717
|
+
before: ReplicationRangeIndexable<R>[],
|
|
2718
|
+
after: ReplicationRangeIndexable<R>[],
|
|
2719
|
+
): { geometry: Array<[bigint, bigint]>; boundary: boolean } => {
|
|
2720
|
+
const events = new Map<bigint, ReplicationProfileDelta>();
|
|
2721
|
+
const updateEvent = (
|
|
2722
|
+
point: bigint,
|
|
2723
|
+
property: keyof ReplicationProfileDelta,
|
|
2724
|
+
delta: number,
|
|
2725
|
+
) => {
|
|
2726
|
+
let event = events.get(point);
|
|
2727
|
+
if (!event) {
|
|
2728
|
+
event = {
|
|
2729
|
+
before: 0,
|
|
2730
|
+
beforeNonStrict: 0,
|
|
2731
|
+
after: 0,
|
|
2732
|
+
afterNonStrict: 0,
|
|
2733
|
+
};
|
|
2734
|
+
events.set(point, event);
|
|
2735
|
+
}
|
|
2736
|
+
event[property] += delta;
|
|
2737
|
+
};
|
|
2738
|
+
const addRange = (range: ReplicationRangeIndexable<R>, isBefore: boolean) => {
|
|
2739
|
+
const coverageProperty = isBefore ? "before" : "after";
|
|
2740
|
+
const nonStrictProperty = isBefore ? "beforeNonStrict" : "afterNonStrict";
|
|
2741
|
+
for (const [start, end] of toSegmentsBigInt(range)) {
|
|
2742
|
+
if (start >= end) {
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
updateEvent(start, coverageProperty, 1);
|
|
2746
|
+
updateEvent(end, coverageProperty, -1);
|
|
2747
|
+
if (range.mode === ReplicationIntent.NonStrict) {
|
|
2748
|
+
updateEvent(start, nonStrictProperty, 1);
|
|
2749
|
+
updateEvent(end, nonStrictProperty, -1);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
for (const range of before) {
|
|
2754
|
+
addRange(range, true);
|
|
2755
|
+
}
|
|
2756
|
+
for (const range of after) {
|
|
2757
|
+
addRange(range, false);
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
const points = [...events.keys()].sort((a, b) =>
|
|
2761
|
+
a < b ? -1 : a > b ? 1 : 0,
|
|
2762
|
+
);
|
|
2763
|
+
const geometry: Array<[bigint, bigint]> = [];
|
|
2764
|
+
let boundary = false;
|
|
2765
|
+
let beforeCoverage = 0;
|
|
2766
|
+
let beforeNonStrict = 0;
|
|
2767
|
+
let afterCoverage = 0;
|
|
2768
|
+
let afterNonStrict = 0;
|
|
2769
|
+
for (let i = 0; i + 1 < points.length; i++) {
|
|
2770
|
+
const point = points[i];
|
|
2771
|
+
const event = events.get(point)!;
|
|
2772
|
+
beforeCoverage += event.before;
|
|
2773
|
+
beforeNonStrict += event.beforeNonStrict;
|
|
2774
|
+
afterCoverage += event.after;
|
|
2775
|
+
afterNonStrict += event.afterNonStrict;
|
|
2776
|
+
const next = points[i + 1];
|
|
2777
|
+
if (point >= next) {
|
|
2778
|
+
continue;
|
|
2779
|
+
}
|
|
2780
|
+
if (beforeCoverage > 0 !== afterCoverage > 0) {
|
|
2781
|
+
geometry.push([point, next]);
|
|
2782
|
+
}
|
|
2783
|
+
if (beforeNonStrict > 0 !== afterNonStrict > 0) {
|
|
2784
|
+
boundary = true;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
return { geometry: mergeBigIntSegments(geometry), boundary };
|
|
2788
|
+
};
|
|
2789
|
+
|
|
2790
|
+
const buildStrictQueryRanges = <R extends NumericType>(
|
|
2791
|
+
segments: Array<[bigint, bigint]>,
|
|
2792
|
+
template: ReplicationRangeIndexable<R>,
|
|
2793
|
+
): ReplicationRangeIndexable<R>[] => {
|
|
2794
|
+
const merged = mergeBigIntSegments(
|
|
2795
|
+
segments
|
|
2796
|
+
.filter(([start, end]) => start < end)
|
|
2797
|
+
.map(([start, end]) => [start, end]),
|
|
2798
|
+
);
|
|
2799
|
+
if (merged.length === 0) {
|
|
2800
|
+
return [];
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
const max = isU32Range(template) ? BigInt(MAX_U32) : MAX_U64;
|
|
2804
|
+
const arcs: Array<{
|
|
2805
|
+
first: [bigint, bigint];
|
|
2806
|
+
second?: [bigint, bigint];
|
|
2807
|
+
}> = [];
|
|
2808
|
+
if (
|
|
2809
|
+
merged.length > 1 &&
|
|
2810
|
+
merged[0][0] === 0n &&
|
|
2811
|
+
merged[merged.length - 1][1] === max
|
|
2812
|
+
) {
|
|
2813
|
+
arcs.push({
|
|
2814
|
+
first: merged[merged.length - 1],
|
|
2815
|
+
second: merged[0],
|
|
2816
|
+
});
|
|
2817
|
+
for (let i = 1; i + 1 < merged.length; i++) {
|
|
2818
|
+
arcs.push({ first: merged[i] });
|
|
2819
|
+
}
|
|
2820
|
+
} else {
|
|
2821
|
+
for (const segment of merged) {
|
|
2822
|
+
arcs.push({ first: segment });
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
const proto = Object.getPrototypeOf(template);
|
|
2827
|
+
return arcs.map(({ first, second }) => {
|
|
2828
|
+
const [start1, end1] = toOriginalType(first, template);
|
|
2829
|
+
const [start2, end2] = second
|
|
2830
|
+
? toOriginalType(second, template)
|
|
2831
|
+
: ([start1, end1] as [NumberFromType<R>, NumberFromType<R>]);
|
|
2832
|
+
const widthBigInt =
|
|
2833
|
+
first[1] - first[0] + (second ? second[1] - second[0] : 0n);
|
|
2834
|
+
const width = (
|
|
2835
|
+
isU32Range(template) ? Number(widthBigInt) : widthBigInt
|
|
2836
|
+
) as NumberFromType<R>;
|
|
2837
|
+
return Object.assign(Object.create(proto), {
|
|
2838
|
+
...template,
|
|
2839
|
+
start1,
|
|
2840
|
+
end1,
|
|
2841
|
+
start2,
|
|
2842
|
+
end2,
|
|
2843
|
+
width,
|
|
2844
|
+
mode: ReplicationIntent.Strict,
|
|
2845
|
+
}) as ReplicationRangeIndexable<R>;
|
|
2846
|
+
});
|
|
2847
|
+
};
|
|
2848
|
+
|
|
2849
|
+
const replicationRangeGeometryKey = <R extends NumericType>(
|
|
2850
|
+
range: ReplicationRangeIndexable<R>,
|
|
2851
|
+
) =>
|
|
2852
|
+
toSegmentsBigInt(range)
|
|
2853
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
2854
|
+
.map(([start, end]) => `${start}:${end}`)
|
|
2855
|
+
.join("|");
|
|
2856
|
+
|
|
2857
|
+
// SQLite's query builder produces a left-deep OR expression. Keep a wide safety
|
|
2858
|
+
// margin below SQLite's default expression-depth limit (1000), including the
|
|
2859
|
+
// nested comparisons needed for wrapped ranges.
|
|
2860
|
+
const MAX_REBALANCE_QUERY_RANGES = 128;
|
|
2861
|
+
|
|
2862
|
+
type RebalanceOwnedInterval = {
|
|
2863
|
+
start: bigint;
|
|
2864
|
+
end: bigint;
|
|
2865
|
+
batch: number;
|
|
2866
|
+
};
|
|
2867
|
+
|
|
2868
|
+
type RebalanceQueryPlan<R extends NumericType> = {
|
|
2869
|
+
boundaryChanges?: ReplicationChange<ReplicationRangeIndexable<R>>[];
|
|
2870
|
+
geometryBatches: ReplicationChange<ReplicationRangeIndexable<R>>[][];
|
|
2871
|
+
ownedIntervals: RebalanceOwnedInterval[];
|
|
2872
|
+
};
|
|
2873
|
+
|
|
2874
|
+
const createRebalanceQueryPlan = <R extends NumericType>(
|
|
2875
|
+
changes: ReplicationChange<ReplicationRangeIndexable<R>>[],
|
|
2876
|
+
includeBoundaryWhenEmpty: boolean,
|
|
2877
|
+
): RebalanceQueryPlan<R> => {
|
|
2878
|
+
const boundarySource = changes.find(
|
|
2879
|
+
(change) =>
|
|
2880
|
+
change.rebalanceBoundaryOnly ||
|
|
2881
|
+
change.range.mode === ReplicationIntent.NonStrict,
|
|
2882
|
+
);
|
|
2883
|
+
const boundaryChanges =
|
|
2884
|
+
boundarySource || includeBoundaryWhenEmpty
|
|
2885
|
+
? boundarySource
|
|
2886
|
+
? [{ ...boundarySource, rebalanceBoundaryOnly: true }]
|
|
2887
|
+
: []
|
|
2888
|
+
: undefined;
|
|
2889
|
+
const geometryChanges = changes.filter(
|
|
2890
|
+
(change) => !change.rebalanceBoundaryOnly,
|
|
2891
|
+
);
|
|
2892
|
+
if (geometryChanges.length === 0) {
|
|
2893
|
+
return { boundaryChanges, geometryBatches: [], ownedIntervals: [] };
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
const template = geometryChanges[geometryChanges.length - 1];
|
|
2897
|
+
const exactSegments = mergeBigIntSegments(
|
|
2898
|
+
geometryChanges
|
|
2899
|
+
.flatMap((change) => toSegmentsBigInt(change.range))
|
|
2900
|
+
.filter(([start, end]) => start < end)
|
|
2901
|
+
.map(([start, end]) => [start, end]),
|
|
2902
|
+
);
|
|
2903
|
+
const exactRanges = buildStrictQueryRanges(exactSegments, template.range);
|
|
2904
|
+
const exactChanges: ReplicationChange<ReplicationRangeIndexable<R>>[] =
|
|
2905
|
+
exactRanges.map((range) => ({
|
|
2906
|
+
range,
|
|
2907
|
+
type: "replaced",
|
|
2908
|
+
timestamp: template.timestamp,
|
|
2909
|
+
}));
|
|
2910
|
+
const geometryBatches: ReplicationChange<ReplicationRangeIndexable<R>>[][] =
|
|
2911
|
+
[];
|
|
2912
|
+
for (
|
|
2913
|
+
let offset = 0;
|
|
2914
|
+
offset < exactChanges.length;
|
|
2915
|
+
offset += MAX_REBALANCE_QUERY_RANGES
|
|
2916
|
+
) {
|
|
2917
|
+
geometryBatches.push(
|
|
2918
|
+
exactChanges.slice(offset, offset + MAX_REBALANCE_QUERY_RANGES),
|
|
2919
|
+
);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
// A row can contain coordinates in several query batches. Assign every exact
|
|
2923
|
+
// linear interval to its batch up front so candidates can be de-duplicated with
|
|
2924
|
+
// O(component-count) plan memory instead of an unbounded set of emitted hashes.
|
|
2925
|
+
// The two linear pieces of a wrapped seam range inherit the same batch tag.
|
|
2926
|
+
const ownedIntervals = exactRanges
|
|
2927
|
+
.flatMap((range, rangeIndex) =>
|
|
2928
|
+
toSegmentsBigInt(range)
|
|
2929
|
+
.filter(([start, end]) => start < end)
|
|
2930
|
+
.map(([start, end]) => ({
|
|
2931
|
+
start,
|
|
2932
|
+
end,
|
|
2933
|
+
batch: Math.floor(rangeIndex / MAX_REBALANCE_QUERY_RANGES),
|
|
2934
|
+
})),
|
|
2935
|
+
)
|
|
2936
|
+
.sort((a, b) =>
|
|
2937
|
+
a.start < b.start
|
|
2938
|
+
? -1
|
|
2939
|
+
: a.start > b.start
|
|
2940
|
+
? 1
|
|
2941
|
+
: a.end < b.end
|
|
2942
|
+
? -1
|
|
2943
|
+
: a.end > b.end
|
|
2944
|
+
? 1
|
|
2945
|
+
: 0,
|
|
2946
|
+
);
|
|
2947
|
+
for (let i = 1; i < ownedIntervals.length; i++) {
|
|
2948
|
+
if (ownedIntervals[i - 1].end > ownedIntervals[i].start) {
|
|
2949
|
+
throw new Error("Rebalance query intervals must not overlap");
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
return { boundaryChanges, geometryBatches, ownedIntervals };
|
|
2954
|
+
};
|
|
2955
|
+
|
|
2956
|
+
const findOwningRebalanceBatch = (
|
|
2957
|
+
coordinates: Array<number | bigint>,
|
|
2958
|
+
intervals: RebalanceOwnedInterval[],
|
|
2959
|
+
): number | undefined => {
|
|
2960
|
+
let minimum: number | undefined;
|
|
2961
|
+
for (const value of coordinates) {
|
|
2962
|
+
const coordinate = typeof value === "number" ? BigInt(value) : value;
|
|
2963
|
+
let low = 0;
|
|
2964
|
+
let high = intervals.length - 1;
|
|
2965
|
+
while (low <= high) {
|
|
2966
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
2967
|
+
const interval = intervals[middle];
|
|
2968
|
+
if (coordinate < interval.start) {
|
|
2969
|
+
high = middle - 1;
|
|
2970
|
+
} else if (coordinate >= interval.end) {
|
|
2971
|
+
low = middle + 1;
|
|
2972
|
+
} else {
|
|
2973
|
+
minimum =
|
|
2974
|
+
minimum == null ? interval.batch : Math.min(minimum, interval.batch);
|
|
2975
|
+
break;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
if (minimum === 0) {
|
|
2979
|
+
break;
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
return minimum;
|
|
2983
|
+
};
|
|
2984
|
+
|
|
2985
|
+
const rotateRebalanceHistory = <R extends NumericType>(
|
|
2986
|
+
changes: ReplicationChange<ReplicationRangeIndexable<R>>[],
|
|
2987
|
+
rebalanceHistory: Cache<string>,
|
|
2988
|
+
) => {
|
|
2989
|
+
// Only original source changes may enter history, and observed order defines
|
|
2990
|
+
// the terminal state: add -> remove ends absent; remove -> add ends present.
|
|
2991
|
+
for (const change of changes) {
|
|
2992
|
+
if (change.type === "added") {
|
|
2993
|
+
rebalanceHistory.add(change.range.rangeHash);
|
|
2994
|
+
} else {
|
|
2995
|
+
rebalanceHistory.del(change.range.rangeHash);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2999
|
+
|
|
2642
3000
|
export const mergeReplicationChanges = <R extends NumericType>(
|
|
2643
3001
|
changesOrChangesArr:
|
|
2644
3002
|
| ReplicationChanges<ReplicationRangeIndexable<R>>
|
|
2645
3003
|
| ReplicationChanges<ReplicationRangeIndexable<R>>[],
|
|
2646
3004
|
rebalanceHistory: Cache<string>,
|
|
3005
|
+
options?: { updateHistory?: boolean },
|
|
2647
3006
|
): ReplicationChange<ReplicationRangeIndexable<R>>[] => {
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
3007
|
+
// Work from an immutable snapshot. In particular, synthetic difference ranges
|
|
3008
|
+
// must never become input events or rebalance-history entries.
|
|
3009
|
+
const changes = flattenReplicationChanges(changesOrChangesArr);
|
|
3010
|
+
const originals: IndexedReplicationChange<R>[] = changes.map(
|
|
3011
|
+
(change, index) => ({
|
|
3012
|
+
change,
|
|
3013
|
+
index,
|
|
3014
|
+
wasInHistory: rebalanceHistory.has(change.range.rangeHash),
|
|
3015
|
+
}),
|
|
3016
|
+
);
|
|
3017
|
+
if (originals.length === 0) {
|
|
3018
|
+
return [];
|
|
2658
3019
|
}
|
|
2659
3020
|
|
|
2660
|
-
|
|
2661
|
-
const
|
|
3021
|
+
const consumed = new Set<number>();
|
|
3022
|
+
const fragmentGroups = new Map<
|
|
2662
3023
|
string,
|
|
2663
|
-
|
|
3024
|
+
{
|
|
3025
|
+
segments: Array<[bigint, bigint]>;
|
|
3026
|
+
template: IndexedReplicationChange<R>;
|
|
3027
|
+
}
|
|
2664
3028
|
>();
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
3029
|
+
let boundarySource: IndexedReplicationChange<R> | undefined;
|
|
3030
|
+
const addFragments = (
|
|
3031
|
+
owner: string,
|
|
3032
|
+
segments: Array<[bigint, bigint]>,
|
|
3033
|
+
template: IndexedReplicationChange<R>,
|
|
3034
|
+
) => {
|
|
3035
|
+
if (segments.length === 0) {
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
const existing = fragmentGroups.get(owner);
|
|
3039
|
+
if (existing) {
|
|
3040
|
+
existing.segments.push(...segments);
|
|
3041
|
+
if (
|
|
3042
|
+
existing.template.change.range.timestamp <
|
|
3043
|
+
template.change.range.timestamp
|
|
3044
|
+
) {
|
|
3045
|
+
existing.template = template;
|
|
3046
|
+
}
|
|
2669
3047
|
} else {
|
|
2670
|
-
|
|
3048
|
+
fragmentGroups.set(owner, {
|
|
3049
|
+
segments: [...segments],
|
|
3050
|
+
template,
|
|
3051
|
+
});
|
|
2671
3052
|
}
|
|
2672
|
-
}
|
|
3053
|
+
};
|
|
3054
|
+
const requireBoundary = (source: IndexedReplicationChange<R>) => {
|
|
3055
|
+
if (!boundarySource) {
|
|
3056
|
+
boundarySource = source;
|
|
3057
|
+
}
|
|
3058
|
+
};
|
|
2673
3059
|
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
3060
|
+
// Replacement provenance is exact: a segment id describes a sequence of real
|
|
3061
|
+
// states. Compare each adjacent state once, then union the resulting query arcs.
|
|
3062
|
+
const replacementGroups = new Map<string, IndexedReplicationChange<R>[]>();
|
|
3063
|
+
for (const original of originals) {
|
|
3064
|
+
if (
|
|
3065
|
+
original.change.type !== "replaced" &&
|
|
3066
|
+
(original.change.type !== "added" || original.change.matured)
|
|
3067
|
+
) {
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
3070
|
+
const key = `${original.change.range.hash}:${original.change.range.idString}`;
|
|
3071
|
+
const group = replacementGroups.get(key);
|
|
3072
|
+
if (group) {
|
|
3073
|
+
group.push(original);
|
|
3074
|
+
} else {
|
|
3075
|
+
replacementGroups.set(key, [original]);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
for (const statesUnsorted of replacementGroups.values()) {
|
|
3079
|
+
if (
|
|
3080
|
+
!statesUnsorted.some((state) => state.change.type === "replaced") ||
|
|
3081
|
+
!statesUnsorted.some((state) => state.change.type === "added")
|
|
3082
|
+
) {
|
|
3083
|
+
continue;
|
|
3084
|
+
}
|
|
3085
|
+
// Range timestamps express role age and may intentionally stay fixed across
|
|
3086
|
+
// adaptive geometry updates. The debounced batch order is the authoritative
|
|
3087
|
+
// state-transition order.
|
|
3088
|
+
const states = [...statesUnsorted].sort((a, b) => a.index - b.index);
|
|
3089
|
+
if (states[states.length - 1].change.type !== "added") {
|
|
3090
|
+
continue;
|
|
3091
|
+
}
|
|
3092
|
+
// The overlap between adjacent replacement states is safe to omit only if
|
|
3093
|
+
// the first state was successfully processed before this batch. A replacement
|
|
3094
|
+
// event describes retired geometry, not proof that its overlap was queried.
|
|
3095
|
+
// Without that proof, leave the complete real states in the result so their
|
|
3096
|
+
// union (including the overlap) is inspected.
|
|
3097
|
+
if (!states[0].wasInHistory) {
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
for (let i = 0; i + 1 < states.length; i++) {
|
|
3101
|
+
const previous = states[i];
|
|
3102
|
+
const next = states[i + 1];
|
|
3103
|
+
const difference = diffReplicationProfiles(
|
|
3104
|
+
[previous.change.range],
|
|
3105
|
+
[next.change.range],
|
|
2684
3106
|
);
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
for (let i = 0; i < v.length; i++) {
|
|
2689
|
-
// If segment is removed and we have previously processed it then go over each
|
|
2690
|
-
// overlapping added segment and remove the overlap. Equivalent to: (1 - 1 + 1) = 1.
|
|
2691
|
-
if (v[i].type === "removed" || v[i].type === "replaced") {
|
|
2692
|
-
if (rebalanceHistory.has(v[i].range.rangeHash)) {
|
|
2693
|
-
let adjusted = false;
|
|
2694
|
-
const vStart = v.length;
|
|
2695
|
-
for (let j = i + 1; j < vStart; j++) {
|
|
2696
|
-
const newer = v[j];
|
|
2697
|
-
if (newer.type === "added" && !newer.matured) {
|
|
2698
|
-
adjusted = true;
|
|
2699
|
-
const {
|
|
2700
|
-
rangesFromA: updatedRemoved,
|
|
2701
|
-
rangesFromB: updatedNewer,
|
|
2702
|
-
} = symmetricDifferenceRanges(v[i].range, newer.range);
|
|
2703
|
-
|
|
2704
|
-
for (const diff of updatedRemoved) {
|
|
2705
|
-
results.push({
|
|
2706
|
-
range: diff,
|
|
2707
|
-
type: "removed" as const,
|
|
2708
|
-
timestamp: v[i].timestamp,
|
|
2709
|
-
});
|
|
2710
|
-
}
|
|
2711
|
-
for (const diff of updatedNewer) {
|
|
2712
|
-
v.push({
|
|
2713
|
-
range: diff,
|
|
2714
|
-
type: "added" as const,
|
|
2715
|
-
timestamp: newer.timestamp,
|
|
2716
|
-
});
|
|
2717
|
-
}
|
|
2718
|
-
consumed.add(j);
|
|
2719
|
-
}
|
|
2720
|
-
}
|
|
2721
|
-
rebalanceHistory.del(v[i].range.rangeHash);
|
|
2722
|
-
if (!adjusted) {
|
|
2723
|
-
results.push(v[i]);
|
|
2724
|
-
}
|
|
2725
|
-
} else {
|
|
2726
|
-
results.push(v[i]);
|
|
2727
|
-
}
|
|
2728
|
-
} else if (v[i].type === "added") {
|
|
2729
|
-
// TODO should the below clause be used?
|
|
2730
|
-
// after testing it seems that certain changes are not propagating as expected using this
|
|
2731
|
-
/* if (rebalanceHistory.has(v[i].range.rangeHash)) {
|
|
2732
|
-
continue;
|
|
2733
|
-
} */
|
|
2734
|
-
|
|
2735
|
-
rebalanceHistory.add(v[i].range.rangeHash);
|
|
2736
|
-
if (!consumed.has(i)) {
|
|
2737
|
-
results.push(v[i]);
|
|
2738
|
-
}
|
|
2739
|
-
} else {
|
|
2740
|
-
results.push(v[i]);
|
|
2741
|
-
}
|
|
3107
|
+
addFragments(previous.change.range.hash, difference.geometry, next);
|
|
3108
|
+
if (difference.boundary) {
|
|
3109
|
+
requireBoundary(next);
|
|
2742
3110
|
}
|
|
3111
|
+
}
|
|
3112
|
+
for (const state of states) {
|
|
3113
|
+
consumed.add(state.index);
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
2743
3116
|
|
|
2744
|
-
|
|
3117
|
+
// Plain reset-style removals/additions have no id provenance. Only removals
|
|
3118
|
+
// whose old state was actually processed may cancel new owner state. Compare
|
|
3119
|
+
// their aggregate owner profiles; leave all unrelated/unmatched changes whole.
|
|
3120
|
+
const plainGroups = new Map<string, IndexedReplicationChange<R>[]>();
|
|
3121
|
+
for (const original of originals) {
|
|
3122
|
+
if (
|
|
3123
|
+
consumed.has(original.index) ||
|
|
3124
|
+
(original.change.type !== "removed" && original.change.type !== "added")
|
|
3125
|
+
) {
|
|
3126
|
+
continue;
|
|
3127
|
+
}
|
|
3128
|
+
const group = plainGroups.get(original.change.range.hash);
|
|
3129
|
+
if (group) {
|
|
3130
|
+
group.push(original);
|
|
2745
3131
|
} else {
|
|
2746
|
-
|
|
2747
|
-
|
|
3132
|
+
plainGroups.set(original.change.range.hash, [original]);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
for (const ownerChanges of plainGroups.values()) {
|
|
3136
|
+
const removed = ownerChanges.filter(
|
|
3137
|
+
(original) => original.change.type === "removed" && original.wasInHistory,
|
|
3138
|
+
);
|
|
3139
|
+
const added = ownerChanges.filter(
|
|
3140
|
+
(original) =>
|
|
3141
|
+
original.change.type === "added" && !original.change.matured,
|
|
3142
|
+
);
|
|
3143
|
+
if (removed.length === 0 || added.length === 0) {
|
|
3144
|
+
continue;
|
|
3145
|
+
}
|
|
3146
|
+
const difference = diffReplicationProfiles(
|
|
3147
|
+
removed.map((original) => original.change.range),
|
|
3148
|
+
added.map((original) => original.change.range),
|
|
3149
|
+
);
|
|
3150
|
+
const newestAdded = added.reduce((newest, current) =>
|
|
3151
|
+
newest.change.range.timestamp >= current.change.range.timestamp
|
|
3152
|
+
? newest
|
|
3153
|
+
: current,
|
|
3154
|
+
);
|
|
3155
|
+
addFragments(
|
|
3156
|
+
newestAdded.change.range.hash,
|
|
3157
|
+
difference.geometry,
|
|
3158
|
+
newestAdded,
|
|
3159
|
+
);
|
|
3160
|
+
if (difference.boundary) {
|
|
3161
|
+
requireBoundary(newestAdded);
|
|
3162
|
+
}
|
|
3163
|
+
for (const original of [...removed, ...added]) {
|
|
3164
|
+
consumed.add(original.index);
|
|
2748
3165
|
}
|
|
2749
3166
|
}
|
|
2750
|
-
|
|
3167
|
+
|
|
3168
|
+
const results: ReplicationChange<ReplicationRangeIndexable<R>>[] = originals
|
|
3169
|
+
.filter((original) => !consumed.has(original.index))
|
|
3170
|
+
.map((original) => original.change);
|
|
3171
|
+
for (const { segments, template } of fragmentGroups.values()) {
|
|
3172
|
+
for (const range of buildStrictQueryRanges(
|
|
3173
|
+
segments,
|
|
3174
|
+
template.change.range,
|
|
3175
|
+
)) {
|
|
3176
|
+
results.push({
|
|
3177
|
+
range,
|
|
3178
|
+
type: "replaced",
|
|
3179
|
+
timestamp: template.change.timestamp,
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// Query type and owner do not affect range matching. Remove exact geometry
|
|
3185
|
+
// duplicates while preferring a non-strict original when it carries boundary
|
|
3186
|
+
// semantics of its own.
|
|
3187
|
+
const deduplicated: ReplicationChange<ReplicationRangeIndexable<R>>[] = [];
|
|
3188
|
+
const positions = new Map<string, number>();
|
|
3189
|
+
for (const result of results) {
|
|
3190
|
+
if (result.rebalanceBoundaryOnly) {
|
|
3191
|
+
continue;
|
|
3192
|
+
}
|
|
3193
|
+
const key = replicationRangeGeometryKey(result.range);
|
|
3194
|
+
const previousPosition = positions.get(key);
|
|
3195
|
+
if (previousPosition == null) {
|
|
3196
|
+
positions.set(key, deduplicated.length);
|
|
3197
|
+
deduplicated.push(result);
|
|
3198
|
+
} else if (
|
|
3199
|
+
deduplicated[previousPosition].range.mode === ReplicationIntent.Strict &&
|
|
3200
|
+
result.range.mode === ReplicationIntent.NonStrict
|
|
3201
|
+
) {
|
|
3202
|
+
deduplicated[previousPosition] = result;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
if (boundarySource) {
|
|
3206
|
+
deduplicated.push({
|
|
3207
|
+
...boundarySource.change,
|
|
3208
|
+
rebalanceBoundaryOnly: true,
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
if (options?.updateHistory !== false) {
|
|
3212
|
+
rotateRebalanceHistory(changes, rebalanceHistory);
|
|
3213
|
+
}
|
|
3214
|
+
return deduplicated;
|
|
2751
3215
|
};
|
|
2752
3216
|
|
|
2753
3217
|
const REBALANCE_ITERATOR_BATCH_SIZE = 1048;
|
|
@@ -2760,30 +3224,87 @@ export const toRebalance = <R extends "u32" | "u64">(
|
|
|
2760
3224
|
rebalanceHistory: Cache<string>,
|
|
2761
3225
|
options?: { forceFresh?: boolean },
|
|
2762
3226
|
): AsyncIterable<EntryReplicated<R>> => {
|
|
2763
|
-
const
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
3227
|
+
const sourceChanges = flattenReplicationChanges(changeOrChanges);
|
|
3228
|
+
const queryChanges = options?.forceFresh
|
|
3229
|
+
? sourceChanges
|
|
3230
|
+
: mergeReplicationChanges(sourceChanges, rebalanceHistory, {
|
|
3231
|
+
updateHistory: false,
|
|
3232
|
+
});
|
|
3233
|
+
const plan = createRebalanceQueryPlan(
|
|
3234
|
+
queryChanges,
|
|
3235
|
+
sourceChanges.length === 0,
|
|
3236
|
+
);
|
|
2770
3237
|
return {
|
|
2771
3238
|
[Symbol.asyncIterator]: async function* () {
|
|
2772
|
-
|
|
2773
|
-
query: createAssignedRangesQuery(change),
|
|
2774
|
-
});
|
|
3239
|
+
let completed = false;
|
|
2775
3240
|
try {
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
3241
|
+
const passes: Array<{
|
|
3242
|
+
changes: ReplicationChange<ReplicationRangeIndexable<R>>[];
|
|
3243
|
+
geometryBatch?: number;
|
|
3244
|
+
}> = [];
|
|
3245
|
+
if (plan.boundaryChanges) {
|
|
3246
|
+
passes.push({ changes: plan.boundaryChanges });
|
|
3247
|
+
}
|
|
3248
|
+
for (let batch = 0; batch < plan.geometryBatches.length; batch++) {
|
|
3249
|
+
passes.push({
|
|
3250
|
+
changes: plan.geometryBatches[batch],
|
|
3251
|
+
geometryBatch: batch,
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
for (const pass of passes) {
|
|
3256
|
+
const isGeometry = pass.geometryBatch != null;
|
|
3257
|
+
const iterator = index.iterate({
|
|
3258
|
+
query: createAssignedRangesQuery(pass.changes, {
|
|
3259
|
+
strict: isGeometry,
|
|
3260
|
+
includeBoundaryWhenEmpty: !isGeometry,
|
|
3261
|
+
}),
|
|
3262
|
+
});
|
|
3263
|
+
let passCompleted = false;
|
|
3264
|
+
try {
|
|
3265
|
+
while (iterator.done() !== true) {
|
|
3266
|
+
const entries = await iterator.next(
|
|
3267
|
+
REBALANCE_ITERATOR_BATCH_SIZE,
|
|
3268
|
+
);
|
|
3269
|
+
if (entries.length === 0) {
|
|
3270
|
+
break;
|
|
3271
|
+
}
|
|
3272
|
+
for (const entry of entries) {
|
|
3273
|
+
if (isGeometry) {
|
|
3274
|
+
if (
|
|
3275
|
+
plan.boundaryChanges &&
|
|
3276
|
+
entry.value.assignedToRangeBoundary
|
|
3277
|
+
) {
|
|
3278
|
+
continue;
|
|
3279
|
+
}
|
|
3280
|
+
if (
|
|
3281
|
+
findOwningRebalanceBatch(
|
|
3282
|
+
entry.value.coordinates,
|
|
3283
|
+
plan.ownedIntervals,
|
|
3284
|
+
) !== pass.geometryBatch
|
|
3285
|
+
) {
|
|
3286
|
+
continue;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
yield entry.value;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
passCompleted = iterator.done() === true;
|
|
3293
|
+
} finally {
|
|
3294
|
+
await iterator.close();
|
|
2780
3295
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
3296
|
+
if (!passCompleted) {
|
|
3297
|
+
return;
|
|
2783
3298
|
}
|
|
2784
3299
|
}
|
|
3300
|
+
completed = true;
|
|
2785
3301
|
} finally {
|
|
2786
|
-
|
|
3302
|
+
// An early consumer return, iterator error, incomplete empty batch, or
|
|
3303
|
+
// close failure in any sequential pass must leave history untouched so the
|
|
3304
|
+
// next event retries the complete query. forceFresh preserves its bypass.
|
|
3305
|
+
if (completed && !options?.forceFresh) {
|
|
3306
|
+
rotateRebalanceHistory(sourceChanges, rebalanceHistory);
|
|
3307
|
+
}
|
|
2787
3308
|
}
|
|
2788
3309
|
},
|
|
2789
3310
|
};
|