@peerbit/shared-log 10.0.6 → 10.1.0

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/src/ranges.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { deserialize, field, serialize, variant, vec } from "@dao-xyz/borsh";
2
+ import type { Cache } from "@peerbit/cache";
2
3
  import {
3
4
  PublicSignKey,
4
5
  equals,
@@ -24,17 +25,18 @@ import {
24
25
  Sort,
25
26
  SortDirection,
26
27
  StringMatch,
28
+ iteratorInSeries,
27
29
  /* iteratorInSeries, */
28
30
  } from "@peerbit/indexer-interface";
29
31
  import { id } from "@peerbit/indexer-interface";
30
32
  import { Meta, ShallowMeta } from "@peerbit/log";
33
+ import { debounceAccumulator } from "./debounce.js";
31
34
  import {
32
35
  MAX_U32,
33
36
  MAX_U64,
34
37
  type NumberFromType,
35
38
  type Numbers,
36
39
  } from "./integers.js";
37
- import { type ReplicationChanges } from "./replication-domain.js";
38
40
 
39
41
  export enum ReplicationIntent {
40
42
  NonStrict = 0, // indicates that the segment will be replicated and nearby data might be replicated as well
@@ -268,7 +270,7 @@ export class ReplicationRangeMessageU32 extends ReplicationRangeMessage<"u32"> {
268
270
  id: this.id,
269
271
  publicKeyHash: key.hashcode(),
270
272
  offset: this.offset,
271
- length: this.factor,
273
+ width: this.factor,
272
274
  timestamp: this.timestamp,
273
275
  mode: this.mode,
274
276
  });
@@ -323,7 +325,7 @@ export class ReplicationRangeMessageU64 extends ReplicationRangeMessage<"u64"> {
323
325
  id: this.id,
324
326
  publicKeyHash: key.hashcode(),
325
327
  offset: this.offset,
326
- length: this.factor,
328
+ width: this.factor,
327
329
  timestamp: this.timestamp,
328
330
  mode: this.mode,
329
331
  });
@@ -410,6 +412,246 @@ export interface ReplicationRangeIndexable<R extends "u32" | "u64"> {
410
412
  contains(point: NumberFromType<R>): boolean;
411
413
  equalRange(other: ReplicationRangeIndexable<R>): boolean;
412
414
  overlaps(other: ReplicationRangeIndexable<R>): boolean;
415
+ toString(): string;
416
+ get rangeHash(): string;
417
+ }
418
+
419
+ export type NumericType = "u32" | "u64";
420
+
421
+ /**
422
+ * Convert a GeneralRange<N> into one or two `[bigint, bigint]` segments.
423
+ * - If it’s not wrapped, there’s one segment: [start1, end1).
424
+ * - If it’s wrapped, there’s two: [start1, end1) and [start2, end2).
425
+ *
426
+ * We always do the conversion to bigints internally.
427
+ */
428
+ export function toSegmentsBigInt<N extends NumericType>(
429
+ range: ReplicationRangeIndexable<N>,
430
+ ): Array<[bigint, bigint]> {
431
+ // Safely convert the numeric fields to bigint
432
+ const s1: bigint =
433
+ typeof range.start1 === "number" ? BigInt(range.start1) : range.start1;
434
+ const e1: bigint =
435
+ typeof range.end1 === "number" ? BigInt(range.end1) : range.end1;
436
+ const s2: bigint =
437
+ typeof range.start2 === "number" ? BigInt(range.start2) : range.start2;
438
+ const e2: bigint =
439
+ typeof range.end2 === "number" ? BigInt(range.end2) : range.end2;
440
+
441
+ const segments: Array<[bigint, bigint]> = [];
442
+
443
+ segments.push([s1, e1]);
444
+
445
+ if (s2 !== s1 && s2 !== e2) {
446
+ segments.push([s2, e2]);
447
+ }
448
+
449
+ return segments;
450
+ }
451
+
452
+ /**
453
+ * Build an array of new GeneralRange<N> objects from leftover `[bigint, bigint]` segments.
454
+ * We split them in pairs, each range can hold up to two segments:
455
+ *
456
+ * - [seg1Start, seg1End)
457
+ * - [seg2Start, seg2End) (if available)
458
+ *
459
+ * We convert bigints back to the correct numeric type, if needed.
460
+ */
461
+ function buildRangesFromBigIntSegments<N extends NumericType>(
462
+ segments: Array<[bigint, bigint]>,
463
+ templateRange: ReplicationRangeIndexable<N>,
464
+ ): Array<ReplicationRangeIndexable<N>> {
465
+ // Sort by start
466
+ segments.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
467
+
468
+ const result: Array<ReplicationRangeIndexable<N>> = [];
469
+ let i = 0;
470
+ const proto = Object.getPrototypeOf(templateRange);
471
+
472
+ while (i < segments.length) {
473
+ const seg1 = segments[i];
474
+ i++;
475
+ let seg2: [bigint, bigint] | null = null;
476
+ if (i < segments.length) {
477
+ seg2 = segments[i];
478
+ i++;
479
+ }
480
+
481
+ // Convert back to the original numeric type
482
+ const [s1, e1] = toOriginalType<N>(seg1, templateRange);
483
+ const [s2, e2] = seg2
484
+ ? toOriginalType<N>(seg2, templateRange)
485
+ : ([s1, e1] as [NumberFromType<N>, NumberFromType<N>]);
486
+
487
+ // Build a new range object. You can clone or replicate metadata as needed.
488
+ const newRange = Object.assign(Object.create(proto), {
489
+ ...templateRange,
490
+ start1: s1,
491
+ end1: e1,
492
+ start2: s2,
493
+ end2: e2,
494
+ });
495
+
496
+ result.push(newRange);
497
+ }
498
+ return result;
499
+ }
500
+
501
+ /**
502
+ * Subtract one bigint segment [bStart, bEnd) from [aStart, aEnd).
503
+ * Returns 0..2 leftover segments in bigint form.
504
+ */
505
+
506
+ function subtractBigIntSegment(
507
+ aStart: bigint,
508
+ aEnd: bigint,
509
+ bStart: bigint,
510
+ bEnd: bigint,
511
+ ): Array<[bigint, bigint]> {
512
+ const result: Array<[bigint, bigint]> = [];
513
+
514
+ // No overlap
515
+ if (bEnd <= aStart || bStart >= aEnd) {
516
+ result.push([aStart, aEnd]);
517
+ return result;
518
+ }
519
+
520
+ // Fully contained
521
+ if (bStart <= aStart && bEnd >= aEnd) {
522
+ return [];
523
+ }
524
+
525
+ // Partial overlaps
526
+ if (bStart > aStart) {
527
+ result.push([aStart, bStart]);
528
+ }
529
+ if (bEnd < aEnd) {
530
+ result.push([bEnd, aEnd]);
531
+ }
532
+
533
+ return result;
534
+ }
535
+
536
+ /**
537
+ * Helper: convert `[bigint, bigint]` to `[number, number]` if N is "u32",
538
+ * or keep as `[bigint, bigint]` if N is "u64".
539
+ */
540
+ function toOriginalType<N extends NumericType>(
541
+ segment: [bigint, bigint],
542
+ templateRange: ReplicationRangeIndexable<N>,
543
+ ): [NumberFromType<N>, NumberFromType<N>] {
544
+ const [start, end] = segment;
545
+ if (isU32Range(templateRange)) {
546
+ // Convert back to number
547
+ return [Number(start), Number(end)] as any;
548
+ } else {
549
+ // Keep as bigint
550
+ return [start, end] as any;
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Merge any adjacent or overlapping `[bigint, bigint]` segments.
556
+ * E.g. [10,20) and [20,25) => [10,25)
557
+ */
558
+ export function mergeBigIntSegments(
559
+ segments: Array<[bigint, bigint]>,
560
+ ): Array<[bigint, bigint]> {
561
+ if (segments.length < 2) return segments;
562
+
563
+ // Sort by start
564
+ segments.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
565
+
566
+ const merged: Array<[bigint, bigint]> = [];
567
+ let current = segments[0];
568
+
569
+ for (let i = 1; i < segments.length; i++) {
570
+ const next = segments[i];
571
+ // If current overlaps or touches next
572
+ if (current[1] >= next[0]) {
573
+ // Merge
574
+ current = [current[0], current[1] > next[1] ? current[1] : next[1]];
575
+ } else {
576
+ merged.push(current);
577
+ current = next;
578
+ }
579
+ }
580
+ merged.push(current);
581
+
582
+ return merged;
583
+ }
584
+
585
+ /**
586
+ * Figure out if a given range is "u32" or "u64".
587
+ * You might also store this in the object itself if you prefer.
588
+ */
589
+ function isU32Range<N extends NumericType>(
590
+ range: ReplicationRangeIndexable<N>,
591
+ ): boolean {
592
+ // If you store a separate `type: "u32" | "u64"` in the range, you can just check that:
593
+ // return range.type === "u32";
594
+ // or we do a hack by checking the type of start1, e.g.:
595
+
596
+ // If "start1" is a number (not a bigint), we treat it as u32
597
+ return typeof range.start1 === "number";
598
+ }
599
+
600
+ export function symmetricDifferenceRanges<N extends NumericType>(
601
+ rangeA: ReplicationRangeIndexable<N>,
602
+ rangeB: ReplicationRangeIndexable<N>,
603
+ ): {
604
+ rangesFromA: Array<ReplicationRangeIndexable<N>>;
605
+ rangesFromB: Array<ReplicationRangeIndexable<N>>;
606
+ } {
607
+ const segmentsA = toSegmentsBigInt(rangeA);
608
+ const segmentsB = toSegmentsBigInt(rangeB);
609
+
610
+ const resultSegmentsA: Array<[bigint, bigint]> = [];
611
+ const resultSegmentsB: Array<[bigint, bigint]> = [];
612
+
613
+ // Compute symmetric difference for A
614
+ for (const [aStart, aEnd] of segmentsA) {
615
+ let leftover = [[aStart, aEnd]] as Array<[bigint, bigint]>;
616
+ for (const [bStart, bEnd] of segmentsB) {
617
+ const newLeftover = [];
618
+ for (const [start, end] of leftover) {
619
+ newLeftover.push(...subtractBigIntSegment(start, end, bStart, bEnd));
620
+ }
621
+ leftover = newLeftover;
622
+ }
623
+ resultSegmentsA.push(...leftover);
624
+ }
625
+
626
+ // Compute symmetric difference for B
627
+ for (const [bStart, bEnd] of segmentsB) {
628
+ let leftover = [[bStart, bEnd]] as Array<[bigint, bigint]>;
629
+ for (const [aStart, aEnd] of segmentsA) {
630
+ const newLeftover = [];
631
+ for (const [start, end] of leftover) {
632
+ newLeftover.push(...subtractBigIntSegment(start, end, aStart, aEnd));
633
+ }
634
+ leftover = newLeftover;
635
+ }
636
+ resultSegmentsB.push(...leftover);
637
+ }
638
+
639
+ // Remove zero-length or invalid segments
640
+ const validSegmentsA = resultSegmentsA.filter(([start, end]) => start < end);
641
+ const validSegmentsB = resultSegmentsB.filter(([start, end]) => start < end);
642
+
643
+ // Merge and deduplicate segments
644
+ const mergedSegmentsA = mergeBigIntSegments(validSegmentsA);
645
+ const mergedSegmentsB = mergeBigIntSegments(validSegmentsB);
646
+
647
+ // Build ranges
648
+ const rangesFromA = buildRangesFromBigIntSegments(mergedSegmentsA, rangeA);
649
+ const rangesFromB = buildRangesFromBigIntSegments(mergedSegmentsB, rangeB);
650
+
651
+ return {
652
+ rangesFromA,
653
+ rangesFromB,
654
+ };
413
655
  }
414
656
 
415
657
  export class ReplicationRangeIndexableU32
@@ -446,7 +688,7 @@ export class ReplicationRangeIndexableU32
446
688
  properties: {
447
689
  id?: Uint8Array;
448
690
  offset: number;
449
- length: number;
691
+ width: number;
450
692
  mode?: ReplicationIntent;
451
693
  timestamp?: bigint;
452
694
  } & ({ publicKeyHash: string } | { publicKey: PublicSignKey }),
@@ -455,16 +697,16 @@ export class ReplicationRangeIndexableU32
455
697
  this.hash =
456
698
  (properties as { publicKeyHash: string }).publicKeyHash ||
457
699
  (properties as { publicKey: PublicSignKey }).publicKey.hashcode();
458
- this.transform({ length: properties.length, offset: properties.offset });
700
+ this.transform({ width: properties.width, offset: properties.offset });
459
701
 
460
702
  this.mode = properties.mode ?? ReplicationIntent.NonStrict;
461
703
  this.timestamp = properties.timestamp || BigInt(0);
462
704
  }
463
705
 
464
- private transform(properties: { offset: number; length: number }) {
706
+ private transform(properties: { offset: number; width: number }) {
465
707
  const ranges = getSegmentsFromOffsetAndRange(
466
708
  properties.offset,
467
- properties.length,
709
+ properties.width,
468
710
  0,
469
711
  MAX_U32,
470
712
  );
@@ -494,6 +736,11 @@ export class ReplicationRangeIndexableU32
494
736
  return toBase64(this.id);
495
737
  }
496
738
 
739
+ get rangeHash() {
740
+ const ser = serialize(this);
741
+ return sha256Base64Sync(ser);
742
+ }
743
+
497
744
  contains(point: number) {
498
745
  return (
499
746
  (point >= this.start1 && point < this.end1) ||
@@ -554,6 +801,7 @@ export class ReplicationRangeIndexableU32
554
801
 
555
802
  equalRange(other: ReplicationRangeIndexableU32) {
556
803
  return (
804
+ this.hash === other.hash &&
557
805
  this.start1 === other.start1 &&
558
806
  this.end1 === other.end1 &&
559
807
  this.start2 === other.start2 &&
@@ -588,7 +836,7 @@ export class ReplicationRangeIndexableU64
588
836
  id: Uint8Array;
589
837
 
590
838
  @field({ type: "string" })
591
- hash: string;
839
+ hash: string; // publickey hash
592
840
 
593
841
  @field({ type: "u64" })
594
842
  timestamp: bigint;
@@ -615,7 +863,7 @@ export class ReplicationRangeIndexableU64
615
863
  properties: {
616
864
  id?: Uint8Array;
617
865
  offset: bigint | number;
618
- length: bigint | number;
866
+ width: bigint | number;
619
867
  mode?: ReplicationIntent;
620
868
  timestamp?: bigint;
621
869
  } & ({ publicKeyHash: string } | { publicKey: PublicSignKey }),
@@ -624,7 +872,7 @@ export class ReplicationRangeIndexableU64
624
872
  this.hash =
625
873
  (properties as { publicKeyHash: string }).publicKeyHash ||
626
874
  (properties as { publicKey: PublicSignKey }).publicKey.hashcode();
627
- this.transform({ length: properties.length, offset: properties.offset });
875
+ this.transform({ width: properties.width, offset: properties.offset });
628
876
 
629
877
  this.mode = properties.mode ?? ReplicationIntent.NonStrict;
630
878
  this.timestamp = properties.timestamp || BigInt(0);
@@ -632,11 +880,11 @@ export class ReplicationRangeIndexableU64
632
880
 
633
881
  private transform(properties: {
634
882
  offset: bigint | number;
635
- length: bigint | number;
883
+ width: bigint | number;
636
884
  }) {
637
885
  const ranges = getSegmentsFromOffsetAndRange(
638
886
  BigInt(properties.offset),
639
- BigInt(properties.length),
887
+ BigInt(properties.width),
640
888
  0n,
641
889
  MAX_U64,
642
890
  );
@@ -673,6 +921,11 @@ export class ReplicationRangeIndexableU64
673
921
  );
674
922
  }
675
923
 
924
+ get rangeHash() {
925
+ const ser = serialize(this);
926
+ return sha256Base64Sync(ser);
927
+ }
928
+
676
929
  overlaps(other: ReplicationRangeIndexableU64, checkOther = true): boolean {
677
930
  if (
678
931
  this.contains(other.start1) ||
@@ -726,6 +979,7 @@ export class ReplicationRangeIndexableU64
726
979
 
727
980
  equalRange(other: ReplicationRangeIndexableU64) {
728
981
  return (
982
+ this.hash === other.hash &&
729
983
  this.start1 === other.start1 &&
730
984
  this.end1 === other.end1 &&
731
985
  this.start2 === other.start2 &&
@@ -770,22 +1024,24 @@ export const mergeRanges = <R extends "u32" | "u64">(
770
1024
  throw new Error("Segments have different publicKeyHash");
771
1025
  }
772
1026
 
773
- // only allow merging segments with length 1 (trivial)
774
- const sameLength = segments.every((x) => x.width === 1 || x.width === 1n);
775
- if (!sameLength) {
776
- throw new Error(
777
- "Segments have different length, only merging of segments length 1 is supported",
778
- );
779
- }
780
-
781
1027
  const sorted = segments.sort((a, b) => Number(a.start1 - b.start1));
782
1028
 
783
- let calculateLargeGap = (): [NumberFromType<R>, number] => {
1029
+ let calculateLargeGap = (): [
1030
+ NumberFromType<R>,
1031
+ number,
1032
+ ReplicationIntent,
1033
+ ] => {
784
1034
  let last = sorted[sorted.length - 1];
785
1035
  let largestArc = numbers.zero;
786
1036
  let largestArcIndex = -1;
1037
+ let mode = ReplicationIntent.NonStrict;
787
1038
  for (let i = 0; i < sorted.length; i++) {
788
1039
  const current = sorted[i];
1040
+
1041
+ if (current.mode === ReplicationIntent.Strict) {
1042
+ mode = ReplicationIntent.Strict;
1043
+ }
1044
+
789
1045
  if (current.start1 !== last.start1) {
790
1046
  let arc = numbers.zero;
791
1047
  if (current.start1 < last.end2) {
@@ -804,22 +1060,32 @@ export const mergeRanges = <R extends "u32" | "u64">(
804
1060
  last = current;
805
1061
  }
806
1062
 
807
- return [largestArc, largestArcIndex];
1063
+ return [largestArc, largestArcIndex, mode];
808
1064
  };
809
- const [largestArc, largestArcIndex] = calculateLargeGap();
1065
+ const [largestArc, largestArcIndex, mode] = calculateLargeGap();
810
1066
 
811
1067
  let totalLengthFinal: number = numbers.maxValue - largestArc;
812
1068
 
1069
+ const proto = segments[0].constructor;
1070
+
813
1071
  if (largestArcIndex === -1) {
1072
+ if (mode !== segments[0].mode) {
1073
+ return new (proto as any)({
1074
+ width: segments[0].width,
1075
+ offset: segments[0].start1,
1076
+ publicKeyHash: segments[0].hash,
1077
+ mode,
1078
+ });
1079
+ }
814
1080
  return segments[0]; // all ranges are the same
815
1081
  }
816
1082
  // use segments[0] constructor to create a new object
817
1083
 
818
- const proto = segments[0].constructor;
819
1084
  return new (proto as any)({
820
- length: totalLengthFinal,
1085
+ width: totalLengthFinal,
821
1086
  offset: segments[largestArcIndex].start1,
822
1087
  publicKeyHash: segments[0].hash,
1088
+ mode,
823
1089
  });
824
1090
  };
825
1091
 
@@ -1054,12 +1320,17 @@ const getClosest = <S extends Shape | undefined, R extends "u32" | "u64">(
1054
1320
  direction: "above" | "below",
1055
1321
  rects: Index<ReplicationRangeIndexable<R>>,
1056
1322
  point: NumberFromType<R>,
1057
- roleAgeLimit: number,
1058
- matured: boolean,
1059
- now: number,
1060
1323
  includeStrict: boolean,
1061
1324
  numbers: Numbers<R>,
1062
- options?: { shape?: S },
1325
+ options?: {
1326
+ shape?: S;
1327
+ hash?: string;
1328
+ time?: {
1329
+ roleAgeLimit: number;
1330
+ matured: boolean;
1331
+ now: number;
1332
+ };
1333
+ },
1063
1334
  ): IndexIterator<ReplicationRangeIndexable<R>, S> => {
1064
1335
  const createQueries = (p: NumberFromType<R>, equality: boolean) => {
1065
1336
  let queries: Query[];
@@ -1070,11 +1341,6 @@ const getClosest = <S extends Shape | undefined, R extends "u32" | "u64">(
1070
1341
  compare: equality ? Compare.LessOrEqual : Compare.Less,
1071
1342
  value: p,
1072
1343
  }),
1073
- new IntegerCompare({
1074
- key: "timestamp",
1075
- compare: matured ? Compare.LessOrEqual : Compare.GreaterOrEqual,
1076
- value: BigInt(now - roleAgeLimit),
1077
- }),
1078
1344
  ];
1079
1345
  } else {
1080
1346
  queries = [
@@ -1083,12 +1349,19 @@ const getClosest = <S extends Shape | undefined, R extends "u32" | "u64">(
1083
1349
  compare: equality ? Compare.GreaterOrEqual : Compare.Greater,
1084
1350
  value: p,
1085
1351
  }),
1352
+ ];
1353
+ }
1354
+
1355
+ if (options?.time) {
1356
+ queries.push(
1086
1357
  new IntegerCompare({
1087
1358
  key: "timestamp",
1088
- compare: matured ? Compare.LessOrEqual : Compare.GreaterOrEqual,
1089
- value: BigInt(now - roleAgeLimit),
1359
+ compare: options?.time?.matured
1360
+ ? Compare.LessOrEqual
1361
+ : Compare.GreaterOrEqual,
1362
+ value: BigInt(options.time.now - options.time.roleAgeLimit),
1090
1363
  }),
1091
- ];
1364
+ );
1092
1365
  }
1093
1366
 
1094
1367
  queries.push(
@@ -1104,6 +1377,9 @@ const getClosest = <S extends Shape | undefined, R extends "u32" | "u64">(
1104
1377
  }),
1105
1378
  );
1106
1379
  }
1380
+ if (options?.hash) {
1381
+ queries.push(new StringMatch({ key: "hash", value: options.hash }));
1382
+ }
1107
1383
  return queries;
1108
1384
  };
1109
1385
 
@@ -1210,7 +1486,7 @@ export const getCoveringRangeQuery = (range: {
1210
1486
  ]),
1211
1487
  ];
1212
1488
  };
1213
- export const iHaveCoveringRange = async <R extends "u32" | "u64">(
1489
+ export const countCoveringRangesSameOwner = async <R extends "u32" | "u64">(
1214
1490
  rects: Index<ReplicationRangeIndexable<R>>,
1215
1491
  range: ReplicationRangeIndexable<R>,
1216
1492
  ) => {
@@ -1234,6 +1510,35 @@ export const iHaveCoveringRange = async <R extends "u32" | "u64">(
1234
1510
  );
1235
1511
  };
1236
1512
 
1513
+ export const getCoveringRangesSameOwner = <R extends "u32" | "u64">(
1514
+ rects: Index<ReplicationRangeIndexable<R>>,
1515
+ range: {
1516
+ start1: number | bigint;
1517
+ end1: number | bigint;
1518
+ start2: number | bigint;
1519
+ end2: number | bigint;
1520
+ hash: string;
1521
+ id: Uint8Array;
1522
+ },
1523
+ ) => {
1524
+ return rects.iterate({
1525
+ query: [
1526
+ ...getCoveringRangeQuery(range),
1527
+ new StringMatch({
1528
+ key: "hash",
1529
+ value: range.hash,
1530
+ }),
1531
+ // assume that we are looking for other ranges, not want to update an existing one
1532
+ new Not(
1533
+ new ByteMatchQuery({
1534
+ key: "id",
1535
+ value: range.id,
1536
+ }),
1537
+ ),
1538
+ ],
1539
+ });
1540
+ };
1541
+
1237
1542
  // TODO
1238
1543
  export function getDistance(
1239
1544
  from: any,
@@ -1397,26 +1702,29 @@ const joinIterator = <S extends Shape | undefined, R extends "u32" | "u64">(
1397
1702
  };
1398
1703
  };
1399
1704
 
1400
- const getClosestAround = <
1705
+ const getClosestAroundOrContaining = <
1401
1706
  S extends (Shape & { timestamp: true }) | undefined,
1402
1707
  R extends "u32" | "u64",
1403
1708
  >(
1404
1709
  peers: Index<ReplicationRangeIndexable<R>>,
1405
1710
  point: NumberFromType<R>,
1406
- roleAge: number,
1407
- now: number,
1408
1711
  includeStrictBelow: boolean,
1409
1712
  includeStrictAbove: boolean,
1410
1713
  numbers: Numbers<R>,
1411
- options?: { shape?: S },
1714
+ options?: {
1715
+ shape?: S;
1716
+ hash?: string;
1717
+ time?: {
1718
+ roleAgeLimit: number;
1719
+ matured: boolean;
1720
+ now: number;
1721
+ };
1722
+ },
1412
1723
  ) => {
1413
1724
  const closestBelow = getClosest<S, R>(
1414
1725
  "below",
1415
1726
  peers,
1416
1727
  point,
1417
- roleAge,
1418
- true,
1419
- now,
1420
1728
  includeStrictBelow,
1421
1729
  numbers,
1422
1730
  options,
@@ -1425,35 +1733,92 @@ const getClosestAround = <
1425
1733
  "above",
1426
1734
  peers,
1427
1735
  point,
1428
- roleAge,
1429
- true,
1430
- now,
1431
1736
  includeStrictAbove,
1432
1737
  numbers,
1433
1738
  options,
1434
1739
  );
1435
- /* const containing = iterateRangesContainingPoint<S, R>(
1436
- peers,
1437
- point,
1438
- {
1439
- time: {
1440
- roleAgeLimit: roleAge,
1441
- matured: true,
1442
- now,
1443
- }
1444
- }
1445
- );
1740
+ const containing = iterateRangesContainingPoint<S, R>(peers, point, options);
1446
1741
 
1447
1742
  return iteratorInSeries(
1448
1743
  containing,
1449
1744
  joinIterator<S, R>([closestBelow, closestAbove], point, "closest", numbers),
1450
- ); */
1451
- return joinIterator<S, R>(
1452
- [closestBelow, closestAbove],
1453
- point,
1454
- "closest",
1745
+ );
1746
+ };
1747
+
1748
+ export const getAdjecentSameOwner = async <R extends "u32" | "u64">(
1749
+ peers: Index<ReplicationRangeIndexable<R>>,
1750
+ range: {
1751
+ idString?: string;
1752
+ start1: NumberFromType<R>;
1753
+ end2: NumberFromType<R>;
1754
+ hash: string;
1755
+ },
1756
+ numbers: Numbers<R>,
1757
+ ): Promise<{
1758
+ below?: ReplicationRangeIndexable<R>;
1759
+ above?: ReplicationRangeIndexable<R>;
1760
+ }> => {
1761
+ const closestBelowIterator = getClosest<undefined, R>(
1762
+ "below",
1763
+ peers,
1764
+ range.start1,
1765
+ true,
1766
+ numbers,
1767
+ {
1768
+ hash: range.hash,
1769
+ },
1770
+ );
1771
+ const closestBelow = await closestBelowIterator.next(1);
1772
+ closestBelowIterator.close();
1773
+ const closestAboveIterator = getClosest<undefined, R>(
1774
+ "above",
1775
+ peers,
1776
+ range.end2,
1777
+ true,
1455
1778
  numbers,
1779
+ {
1780
+ hash: range.hash,
1781
+ },
1456
1782
  );
1783
+ const closestAbove = await closestAboveIterator.next(1);
1784
+ closestAboveIterator.close();
1785
+ return {
1786
+ below:
1787
+ range.idString === closestBelow[0]?.value.idString
1788
+ ? undefined
1789
+ : closestBelow[0]?.value,
1790
+ above:
1791
+ closestBelow[0]?.id.primitive === closestAbove[0]?.id.primitive ||
1792
+ range.idString === closestBelow[0]?.value.idString
1793
+ ? undefined
1794
+ : closestAbove[0]?.value,
1795
+ };
1796
+ };
1797
+
1798
+ export const getAllMergeCandiates = async <R extends "u32" | "u64">(
1799
+ peers: Index<ReplicationRangeIndexable<R>>,
1800
+ range: {
1801
+ idString?: string;
1802
+ start1: NumberFromType<R>;
1803
+ start2: NumberFromType<R>;
1804
+ end1: NumberFromType<R>;
1805
+ end2: NumberFromType<R>;
1806
+ hash: string;
1807
+ id: Uint8Array;
1808
+ },
1809
+ numbers: Numbers<R>,
1810
+ ): Promise<ReplicationRangeIndexable<R>[]> => {
1811
+ const adjacent = await getAdjecentSameOwner(peers, range, numbers);
1812
+ const covering = await getCoveringRangesSameOwner(peers, range).all();
1813
+
1814
+ let ret: ReplicationRangeIndexable<R>[] = [];
1815
+ if (adjacent.below) {
1816
+ ret.push(adjacent.below);
1817
+ }
1818
+ if (adjacent.above) {
1819
+ ret.push(adjacent.above);
1820
+ }
1821
+ return [...ret, ...covering.map((x) => x.value)];
1457
1822
  };
1458
1823
 
1459
1824
  export const isMatured = (
@@ -1542,27 +1907,31 @@ const collectClosestAround = async <R extends "u32" | "u64">(
1542
1907
  "below",
1543
1908
  peers,
1544
1909
  point,
1545
- 0,
1546
- true,
1547
- now,
1548
1910
  false,
1549
1911
  numbers,
1912
+ /* {
1913
+ time: {
1914
+ matured: true,
1915
+ roleAgeLimit: 0,
1916
+ now
1917
+ }
1918
+ } */
1550
1919
  );
1551
1920
  const closestAbove = getClosest<undefined, R>(
1552
1921
  "above",
1553
1922
  peers,
1554
1923
  point,
1555
- 0,
1556
- true,
1557
- now,
1558
1924
  false,
1559
1925
  numbers,
1926
+ /* {
1927
+ time: {
1928
+ matured: true,
1929
+ roleAgeLimit: 0,
1930
+ now
1931
+ }
1932
+ } */
1560
1933
  );
1561
- /* const containingIterator = iterateRangesContainingPoint<undefined, R>(
1562
- peers,
1563
- point,
1564
- );
1565
- */
1934
+
1566
1935
  const aroundIterator = joinIterator<undefined, R>(
1567
1936
  [/* containingIterator, */ closestBelow, closestAbove],
1568
1937
  point,
@@ -1714,7 +2083,13 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
1714
2083
  const { startNode, startLocation, endLocation } = await getStartAndEnd<
1715
2084
  undefined,
1716
2085
  R
1717
- >(peers, start, widthToCoverScaled, roleAge, now, properties.numbers);
2086
+ >(peers, start, widthToCoverScaled, properties.numbers, {
2087
+ time: {
2088
+ roleAgeLimit: roleAge,
2089
+ now,
2090
+ matured: true,
2091
+ },
2092
+ });
1718
2093
 
1719
2094
  let ret = new Set<string>();
1720
2095
 
@@ -1778,16 +2153,13 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
1778
2153
  ) => {
1779
2154
  // if not get closest from above
1780
2155
  let next = await fetchOne<undefined, R>(
1781
- getClosest(
1782
- "above",
1783
- peers,
1784
- nextLocation,
1785
- roleAge,
1786
- true,
1787
- now,
1788
- true,
1789
- properties.numbers,
1790
- ),
2156
+ getClosest("above", peers, nextLocation, true, properties.numbers, {
2157
+ time: {
2158
+ matured: true,
2159
+ roleAgeLimit: roleAge,
2160
+ now,
2161
+ },
2162
+ }),
1791
2163
  );
1792
2164
  return next;
1793
2165
  };
@@ -1967,31 +2339,197 @@ export const matchEntriesInRangeQuery = (range: {
1967
2339
  ];
1968
2340
  return new Or(ors);
1969
2341
  };
2342
+
2343
+ export type ReplicationChanges<
2344
+ T extends ReplicationRangeIndexable<any> = ReplicationRangeIndexable<any>,
2345
+ > = ReplicationChange<T>[];
2346
+ export type ReplicationChange<
2347
+ T extends ReplicationRangeIndexable<any> = ReplicationRangeIndexable<any>,
2348
+ > = (
2349
+ | {
2350
+ type: "added";
2351
+ range: T;
2352
+ matured?: boolean;
2353
+ }
2354
+ | {
2355
+ type: "removed";
2356
+ range: T;
2357
+ }
2358
+ | {
2359
+ type: "replaced";
2360
+ range: T;
2361
+ }
2362
+ ) & { timestamp: bigint };
2363
+
2364
+ export const debounceAggregationChanges = <
2365
+ T extends ReplicationRangeIndexable<any>,
2366
+ >(
2367
+ fn: (changeOrChanges: ReplicationChange<T>[]) => void,
2368
+ delay: number,
2369
+ ) => {
2370
+ return debounceAccumulator(
2371
+ (result) => {
2372
+ return fn([...result.values()]);
2373
+ },
2374
+ () => {
2375
+ let aggregated: Map<string, ReplicationChange<T>> = new Map();
2376
+ return {
2377
+ add: (change: ReplicationChange<T>) => {
2378
+ const prev = aggregated.get(change.range.idString);
2379
+ if (prev) {
2380
+ if (prev.range.timestamp < change.range.timestamp) {
2381
+ aggregated.set(change.range.idString, change);
2382
+ }
2383
+ } else {
2384
+ aggregated.set(change.range.idString, change);
2385
+ }
2386
+ },
2387
+ delete: (key: string) => {
2388
+ aggregated.delete(key);
2389
+ },
2390
+ size: () => aggregated.size,
2391
+ value: aggregated,
2392
+ };
2393
+ },
2394
+ delay,
2395
+ );
2396
+ };
2397
+
2398
+ export const mergeReplicationChanges = <R extends NumericType>(
2399
+ changesOrChangesArr:
2400
+ | ReplicationChanges<ReplicationRangeIndexable<R>>
2401
+ | ReplicationChanges<ReplicationRangeIndexable<R>>[],
2402
+ rebalanceHistory: Cache<string>,
2403
+ ): ReplicationChange<ReplicationRangeIndexable<R>>[] => {
2404
+ let first = changesOrChangesArr[0];
2405
+ let changes: ReplicationChange<ReplicationRangeIndexable<R>>[];
2406
+ if (!Array.isArray(first)) {
2407
+ changes = changesOrChangesArr as ReplicationChange<
2408
+ ReplicationRangeIndexable<R>
2409
+ >[];
2410
+ } else {
2411
+ changes = changesOrChangesArr.flat() as ReplicationChange<
2412
+ ReplicationRangeIndexable<R>
2413
+ >[];
2414
+ }
2415
+
2416
+ // group by hash so we can cancel out changes
2417
+ const grouped = new Map<
2418
+ string,
2419
+ ReplicationChange<ReplicationRangeIndexable<R>>[]
2420
+ >();
2421
+ for (const change of changes) {
2422
+ const prev = grouped.get(change.range.hash);
2423
+ if (prev) {
2424
+ prev.push(change);
2425
+ } else {
2426
+ grouped.set(change.range.hash, [change]);
2427
+ }
2428
+ }
2429
+
2430
+ let all: ReplicationChange<ReplicationRangeIndexable<R>>[] = [];
2431
+ for (const [_k, v] of grouped) {
2432
+ if (v.length > 1) {
2433
+ // sort by timestamp so newest is last
2434
+ v.sort((a, b) =>
2435
+ a.range.timestamp < b.range.timestamp
2436
+ ? -1
2437
+ : a.range.timestamp > b.range.timestamp
2438
+ ? 1
2439
+ : 0,
2440
+ );
2441
+
2442
+ let results: ReplicationChange<ReplicationRangeIndexable<R>>[] = [];
2443
+ let consumed: Set<number> = new Set();
2444
+ for (let i = 0; i < v.length; i++) {
2445
+ // if segment is removed and we have previously processed it
2446
+ // then go over each overlapping added segment add remove the removal,
2447
+ // equivalent is that this would represent (1 - 1 + 1) = 1
2448
+ if (v[i].type === "removed" || v[i].type === "replaced") {
2449
+ if (rebalanceHistory.has(v[i].range.rangeHash)) {
2450
+ let vStart = v.length;
2451
+ for (let j = i + 1; j < vStart; j++) {
2452
+ const newer = v[j];
2453
+ if (newer.type === "added" && !newer.matured) {
2454
+ const {
2455
+ rangesFromA: updatedRemoved,
2456
+ rangesFromB: updatedNewer,
2457
+ } = symmetricDifferenceRanges(v[i].range, newer.range);
2458
+ for (const diff of updatedRemoved) {
2459
+ results.push({
2460
+ range: diff,
2461
+ type: "removed" as const,
2462
+ timestamp: v[i].timestamp,
2463
+ });
2464
+ }
2465
+ for (const diff of updatedNewer) {
2466
+ v.push({
2467
+ range: diff,
2468
+ type: "added" as const,
2469
+ timestamp: newer.timestamp,
2470
+ });
2471
+ }
2472
+ consumed.add(j);
2473
+ }
2474
+ }
2475
+ rebalanceHistory.del(v[i].range.rangeHash);
2476
+ } else {
2477
+ results.push(v[i]);
2478
+ }
2479
+ } else if (v[i].type === "added") {
2480
+ // TODO should the below clause be used?
2481
+ // after testing it seems that certain changes are not propagating as expected using this
2482
+ /* if (rebalanceHistory.has(v[i].range.rangeHash)) {
2483
+ continue;
2484
+ } */
2485
+
2486
+ rebalanceHistory.add(v[i].range.rangeHash);
2487
+ if (!consumed.has(i)) {
2488
+ results.push(v[i]);
2489
+ }
2490
+ } else {
2491
+ results.push(v[i]);
2492
+ }
2493
+ }
2494
+
2495
+ all.push(...results);
2496
+ } else {
2497
+ rebalanceHistory.add(v[0].range.rangeHash);
2498
+ all.push(v[0]);
2499
+ }
2500
+ }
2501
+ return all;
2502
+ };
2503
+
1970
2504
  export const toRebalance = <R extends "u32" | "u64">(
1971
- changes: ReplicationChanges,
2505
+ changeOrChanges:
2506
+ | ReplicationChanges<ReplicationRangeIndexable<R>>
2507
+ | ReplicationChanges<ReplicationRangeIndexable<R>>[],
1972
2508
  index: Index<EntryReplicated<R>>,
2509
+ rebalanceHistory: Cache<string>,
1973
2510
  ): AsyncIterable<EntryReplicated<R>> => {
2511
+ const change = mergeReplicationChanges(changeOrChanges, rebalanceHistory);
2512
+
1974
2513
  const assignedRangesQuery = (changes: ReplicationChanges) => {
1975
2514
  let ors: Query[] = [];
2515
+ let onlyStrict = true;
1976
2516
  for (const change of changes) {
1977
2517
  const matchRange = matchEntriesInRangeQuery(change.range);
1978
- if (change.type === "updated") {
1979
- // assuming a range is to be removed, is this entry still enoughly replicated
1980
- const prevMatchRange = matchEntriesInRangeQuery(change.prev);
1981
- ors.push(prevMatchRange);
1982
- ors.push(matchRange);
1983
- } else {
1984
- ors.push(matchRange);
2518
+ ors.push(matchRange);
2519
+ if (change.range.mode === ReplicationIntent.NonStrict) {
2520
+ onlyStrict = false;
1985
2521
  }
1986
2522
  }
1987
2523
 
1988
2524
  // entry is assigned to a range boundary, meaning it is due to be inspected
1989
- ors.push(
1990
- new BoolQuery({
1991
- key: "assignedToRangeBoundary",
1992
- value: true,
1993
- }),
1994
- );
2525
+ if (!onlyStrict || changes.length === 0) {
2526
+ ors.push(
2527
+ new BoolQuery({
2528
+ key: "assignedToRangeBoundary",
2529
+ value: true,
2530
+ }),
2531
+ );
2532
+ }
1995
2533
 
1996
2534
  // entry is not sufficiently replicated, and we are to still keep it
1997
2535
  return new Or(ors);
@@ -1999,7 +2537,7 @@ export const toRebalance = <R extends "u32" | "u64">(
1999
2537
  return {
2000
2538
  [Symbol.asyncIterator]: async function* () {
2001
2539
  const iterator = index.iterate({
2002
- query: assignedRangesQuery(changes),
2540
+ query: assignedRangesQuery(change),
2003
2541
  });
2004
2542
 
2005
2543
  while (iterator.done() !== true) {
@@ -2024,11 +2562,14 @@ export const fetchOneFromPublicKey = async <
2024
2562
  >(
2025
2563
  publicKey: PublicSignKey,
2026
2564
  index: Index<ReplicationRangeIndexable<R>>,
2027
- roleAge: number,
2028
- now: number,
2029
2565
  numbers: Numbers<R>,
2030
2566
  options?: {
2031
- shape: S;
2567
+ shape?: S;
2568
+ time?: {
2569
+ roleAgeLimit: number;
2570
+ matured: boolean;
2571
+ now: number;
2572
+ };
2032
2573
  },
2033
2574
  ) => {
2034
2575
  let iterator = index.iterate<S>(
@@ -2041,13 +2582,14 @@ export const fetchOneFromPublicKey = async <
2041
2582
  await iterator.close();
2042
2583
  let node = result[0]?.value;
2043
2584
  if (node) {
2044
- if (!isMatured(node, now, roleAge)) {
2585
+ if (
2586
+ options?.time &&
2587
+ !isMatured(node, options.time.now, options.time.roleAgeLimit)
2588
+ ) {
2045
2589
  const matured = await fetchOne(
2046
- getClosestAround<S, R>(
2590
+ getClosestAroundOrContaining<S, R>(
2047
2591
  index,
2048
2592
  node.start1,
2049
- roleAge,
2050
- now,
2051
2593
  false,
2052
2594
  false,
2053
2595
  numbers,
@@ -2069,10 +2611,15 @@ export const getStartAndEnd = async <
2069
2611
  peers: Index<ReplicationRangeIndexable<R>>,
2070
2612
  start: NumberFromType<R> | PublicSignKey | undefined | undefined,
2071
2613
  widthToCoverScaled: NumberFromType<R>,
2072
- roleAge: number,
2073
- now: number,
2074
2614
  numbers: Numbers<R>,
2075
- options?: { shape: S },
2615
+ options?: {
2616
+ shape?: S;
2617
+ time?: {
2618
+ roleAgeLimit: number;
2619
+ matured: boolean;
2620
+ now: number;
2621
+ };
2622
+ },
2076
2623
  ): Promise<{
2077
2624
  startNode: ReturnTypeFromShape<ReplicationRangeIndexable<R>, S> | undefined;
2078
2625
  startLocation: NumberFromType<R>;
@@ -2089,8 +2636,6 @@ export const getStartAndEnd = async <
2089
2636
  startNode = await fetchOneClosest<S, R>(
2090
2637
  peers,
2091
2638
  startLocation,
2092
- roleAge,
2093
- now,
2094
2639
  false,
2095
2640
  true,
2096
2641
  numbers,
@@ -2100,14 +2645,7 @@ export const getStartAndEnd = async <
2100
2645
 
2101
2646
  if (start instanceof PublicSignKey) {
2102
2647
  // start at our node (local first)
2103
- startNode = await fetchOneFromPublicKey(
2104
- start,
2105
- peers,
2106
- roleAge,
2107
- now,
2108
- numbers,
2109
- options,
2110
- );
2648
+ startNode = await fetchOneFromPublicKey(start, peers, numbers, options);
2111
2649
  if (!startNode) {
2112
2650
  // fetch randomly
2113
2651
  await nodeFromPoint();
@@ -2168,19 +2706,23 @@ export const fetchOneClosest = <
2168
2706
  >(
2169
2707
  peers: Index<ReplicationRangeIndexable<R>>,
2170
2708
  point: NumberFromType<R>,
2171
- roleAge: number,
2172
- now: number,
2173
2709
  includeStrictBelow: boolean,
2174
2710
  includeStrictAbove: boolean,
2175
2711
  numbers: Numbers<R>,
2176
- options?: { shape?: S },
2712
+ options?: {
2713
+ shape?: S;
2714
+ time?: {
2715
+ roleAgeLimit: number;
2716
+ matured: boolean;
2717
+ now: number;
2718
+ };
2719
+ },
2177
2720
  ) => {
2178
2721
  return fetchOne<S, R>(
2179
- getClosestAround<S, R>(
2722
+ getClosestAroundOrContaining<S, R>(
2180
2723
  peers,
2181
2724
  point,
2182
- roleAge,
2183
- now,
2725
+
2184
2726
  includeStrictBelow,
2185
2727
  includeStrictAbove,
2186
2728
  numbers,