@peerbit/shared-log 10.0.6 → 10.1.0-7d319f1

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,110 +1733,104 @@ 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",
1455
- numbers,
1456
1745
  );
1457
1746
  };
1458
1747
 
1459
- export const isMatured = (
1460
- segment: { timestamp: bigint },
1461
- now: number,
1462
- minAge: number,
1463
- ) => {
1464
- return now - Number(segment.timestamp) >= minAge;
1465
- };
1466
- /*
1467
-
1468
- const collectNodesAroundPoint = async <R extends "u32" | "u64">(
1469
- roleAge: number,
1748
+ export const getAdjecentSameOwner = async <R extends "u32" | "u64">(
1470
1749
  peers: Index<ReplicationRangeIndexable<R>>,
1471
- collector: (
1472
- rect: { hash: string },
1473
- matured: boolean,
1474
- intersecting: boolean,
1475
- ) => void,
1476
- point: NumberFromType<R>,
1477
- now: number,
1750
+ range: {
1751
+ idString?: string;
1752
+ start1: NumberFromType<R>;
1753
+ end2: NumberFromType<R>;
1754
+ hash: string;
1755
+ },
1478
1756
  numbers: Numbers<R>,
1479
- done: () => boolean = () => true,
1480
- ) => {
1481
- const containing = iterateRangesContainingPoint<
1482
- { timestamp: true, hash: true },
1483
- R
1484
- >(peers, point, 0, true, now, { shape: { timestamp: true, hash: true } as const });
1485
- const allContaining = await containing.all();
1486
- for (const rect of allContaining) {
1487
- collector(rect.value, isMatured(rect.value, now, roleAge), true);
1488
- }
1489
-
1490
- if (done()) {
1491
- return;
1492
- }
1493
-
1494
- const closestBelow = getClosest<undefined, R>(
1757
+ ): Promise<{
1758
+ below?: ReplicationRangeIndexable<R>;
1759
+ above?: ReplicationRangeIndexable<R>;
1760
+ }> => {
1761
+ const closestBelowIterator = getClosest<undefined, R>(
1495
1762
  "below",
1496
1763
  peers,
1497
- point,
1498
- 0,
1764
+ range.start1,
1499
1765
  true,
1500
- now,
1501
- false,
1502
- numbers
1766
+ numbers,
1767
+ {
1768
+ hash: range.hash,
1769
+ },
1503
1770
  );
1504
- const closestAbove = getClosest<undefined, R>(
1771
+ const closestBelow = await closestBelowIterator.next(1);
1772
+ closestBelowIterator.close();
1773
+ const closestAboveIterator = getClosest<undefined, R>(
1505
1774
  "above",
1506
1775
  peers,
1507
- point,
1508
- 0,
1776
+ range.end2,
1509
1777
  true,
1510
- now,
1511
- false,
1512
- numbers
1513
- );
1514
- const aroundIterator = joinIterator<undefined, R>(
1515
- [closestBelow, closestAbove],
1516
- point,
1517
- "closest",
1518
1778
  numbers,
1779
+ {
1780
+ hash: range.hash,
1781
+ },
1519
1782
  );
1520
- while (aroundIterator.done() !== true && done() !== true) {
1521
- const res = await aroundIterator.next(1);
1522
- for (const rect of res) {
1523
- collector(rect.value, isMatured(rect.value, now, roleAge), false);
1524
- if (done()) {
1525
- return;
1526
- }
1527
- }
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<MapIterator<ReplicationRangeIndexable<R>>> => {
1811
+ const adjacent = await getAdjecentSameOwner(peers, range, numbers);
1812
+ const covering = await getCoveringRangesSameOwner(peers, range).all();
1813
+
1814
+ let ret: Map<string, ReplicationRangeIndexable<R>> = new Map();
1815
+ if (adjacent.below) {
1816
+ ret.set(adjacent.below.idString, adjacent.below);
1528
1817
  }
1818
+ if (adjacent.above) {
1819
+ ret.set(adjacent.above.idString, adjacent.above);
1820
+ }
1821
+ for (const range of covering) {
1822
+ ret.set(range.value.idString, range.value);
1823
+ }
1824
+ return ret.values();
1529
1825
  };
1530
- */
1531
1826
 
1827
+ export const isMatured = (
1828
+ segment: { timestamp: bigint },
1829
+ now: number,
1830
+ minAge: number,
1831
+ ) => {
1832
+ return now - Number(segment.timestamp) >= minAge;
1833
+ };
1532
1834
  const collectClosestAround = async <R extends "u32" | "u64">(
1533
1835
  roleAge: number,
1534
1836
  peers: Index<ReplicationRangeIndexable<R>>,
@@ -1542,9 +1844,6 @@ const collectClosestAround = async <R extends "u32" | "u64">(
1542
1844
  "below",
1543
1845
  peers,
1544
1846
  point,
1545
- 0,
1546
- true,
1547
- now,
1548
1847
  false,
1549
1848
  numbers,
1550
1849
  );
@@ -1552,17 +1851,10 @@ const collectClosestAround = async <R extends "u32" | "u64">(
1552
1851
  "above",
1553
1852
  peers,
1554
1853
  point,
1555
- 0,
1556
- true,
1557
- now,
1558
1854
  false,
1559
1855
  numbers,
1560
1856
  );
1561
- /* const containingIterator = iterateRangesContainingPoint<undefined, R>(
1562
- peers,
1563
- point,
1564
- );
1565
- */
1857
+
1566
1858
  const aroundIterator = joinIterator<undefined, R>(
1567
1859
  [/* containingIterator, */ closestBelow, closestAbove],
1568
1860
  point,
@@ -1714,7 +2006,13 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
1714
2006
  const { startNode, startLocation, endLocation } = await getStartAndEnd<
1715
2007
  undefined,
1716
2008
  R
1717
- >(peers, start, widthToCoverScaled, roleAge, now, properties.numbers);
2009
+ >(peers, start, widthToCoverScaled, properties.numbers, {
2010
+ time: {
2011
+ roleAgeLimit: roleAge,
2012
+ now,
2013
+ matured: true,
2014
+ },
2015
+ });
1718
2016
 
1719
2017
  let ret = new Set<string>();
1720
2018
 
@@ -1778,16 +2076,13 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
1778
2076
  ) => {
1779
2077
  // if not get closest from above
1780
2078
  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
- ),
2079
+ getClosest("above", peers, nextLocation, true, properties.numbers, {
2080
+ time: {
2081
+ matured: true,
2082
+ roleAgeLimit: roleAge,
2083
+ now,
2084
+ },
2085
+ }),
1791
2086
  );
1792
2087
  return next;
1793
2088
  };
@@ -1911,21 +2206,6 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
1911
2206
  start instanceof PublicSignKey && ret.add(start.hashcode());
1912
2207
  return ret;
1913
2208
  };
1914
- /* export const getReplicationDiff = (changes: ReplicationChange) => {
1915
- // reduce the change set to only regions that are changed for each peer
1916
- // i.e. subtract removed regions from added regions, and vice versa
1917
- const result = new Map<string, { range: ReplicationRangeIndexable, added: boolean }[]>();
1918
-
1919
- for (const addedChange of changes.added ?? []) {
1920
- let prev = result.get(addedChange.hash) ?? [];
1921
- for (const [_hash, ranges] of result.entries()) {
1922
- for (const r of ranges) {
1923
-
1924
- }
1925
- }
1926
- }
1927
- }
1928
- */
1929
2209
 
1930
2210
  export const matchEntriesInRangeQuery = (range: {
1931
2211
  start1: number | bigint;
@@ -1967,31 +2247,197 @@ export const matchEntriesInRangeQuery = (range: {
1967
2247
  ];
1968
2248
  return new Or(ors);
1969
2249
  };
2250
+
2251
+ export type ReplicationChanges<
2252
+ T extends ReplicationRangeIndexable<any> = ReplicationRangeIndexable<any>,
2253
+ > = ReplicationChange<T>[];
2254
+ export type ReplicationChange<
2255
+ T extends ReplicationRangeIndexable<any> = ReplicationRangeIndexable<any>,
2256
+ > = (
2257
+ | {
2258
+ type: "added";
2259
+ range: T;
2260
+ matured?: boolean;
2261
+ }
2262
+ | {
2263
+ type: "removed";
2264
+ range: T;
2265
+ }
2266
+ | {
2267
+ type: "replaced";
2268
+ range: T;
2269
+ }
2270
+ ) & { timestamp: bigint };
2271
+
2272
+ export const debounceAggregationChanges = <
2273
+ T extends ReplicationRangeIndexable<any>,
2274
+ >(
2275
+ fn: (changeOrChanges: ReplicationChange<T>[]) => void,
2276
+ delay: number,
2277
+ ) => {
2278
+ return debounceAccumulator(
2279
+ (result) => {
2280
+ return fn([...result.values()]);
2281
+ },
2282
+ () => {
2283
+ let aggregated: Map<string, ReplicationChange<T>> = new Map();
2284
+ return {
2285
+ add: (change: ReplicationChange<T>) => {
2286
+ const prev = aggregated.get(change.range.idString);
2287
+ if (prev) {
2288
+ if (prev.range.timestamp < change.range.timestamp) {
2289
+ aggregated.set(change.range.idString, change);
2290
+ }
2291
+ } else {
2292
+ aggregated.set(change.range.idString, change);
2293
+ }
2294
+ },
2295
+ delete: (key: string) => {
2296
+ aggregated.delete(key);
2297
+ },
2298
+ size: () => aggregated.size,
2299
+ value: aggregated,
2300
+ };
2301
+ },
2302
+ delay,
2303
+ );
2304
+ };
2305
+
2306
+ export const mergeReplicationChanges = <R extends NumericType>(
2307
+ changesOrChangesArr:
2308
+ | ReplicationChanges<ReplicationRangeIndexable<R>>
2309
+ | ReplicationChanges<ReplicationRangeIndexable<R>>[],
2310
+ rebalanceHistory: Cache<string>,
2311
+ ): ReplicationChange<ReplicationRangeIndexable<R>>[] => {
2312
+ let first = changesOrChangesArr[0];
2313
+ let changes: ReplicationChange<ReplicationRangeIndexable<R>>[];
2314
+ if (!Array.isArray(first)) {
2315
+ changes = changesOrChangesArr as ReplicationChange<
2316
+ ReplicationRangeIndexable<R>
2317
+ >[];
2318
+ } else {
2319
+ changes = changesOrChangesArr.flat() as ReplicationChange<
2320
+ ReplicationRangeIndexable<R>
2321
+ >[];
2322
+ }
2323
+
2324
+ // group by hash so we can cancel out changes
2325
+ const grouped = new Map<
2326
+ string,
2327
+ ReplicationChange<ReplicationRangeIndexable<R>>[]
2328
+ >();
2329
+ for (const change of changes) {
2330
+ const prev = grouped.get(change.range.hash);
2331
+ if (prev) {
2332
+ prev.push(change);
2333
+ } else {
2334
+ grouped.set(change.range.hash, [change]);
2335
+ }
2336
+ }
2337
+
2338
+ let all: ReplicationChange<ReplicationRangeIndexable<R>>[] = [];
2339
+ for (const [_k, v] of grouped) {
2340
+ if (v.length > 1) {
2341
+ // sort by timestamp so newest is last
2342
+ v.sort((a, b) =>
2343
+ a.range.timestamp < b.range.timestamp
2344
+ ? -1
2345
+ : a.range.timestamp > b.range.timestamp
2346
+ ? 1
2347
+ : 0,
2348
+ );
2349
+
2350
+ let results: ReplicationChange<ReplicationRangeIndexable<R>>[] = [];
2351
+ let consumed: Set<number> = new Set();
2352
+ for (let i = 0; i < v.length; i++) {
2353
+ // if segment is removed and we have previously processed it
2354
+ // then go over each overlapping added segment add remove the removal,
2355
+ // equivalent is that this would represent (1 - 1 + 1) = 1
2356
+ if (v[i].type === "removed" || v[i].type === "replaced") {
2357
+ if (rebalanceHistory.has(v[i].range.rangeHash)) {
2358
+ let vStart = v.length;
2359
+ for (let j = i + 1; j < vStart; j++) {
2360
+ const newer = v[j];
2361
+ if (newer.type === "added" && !newer.matured) {
2362
+ const {
2363
+ rangesFromA: updatedRemoved,
2364
+ rangesFromB: updatedNewer,
2365
+ } = symmetricDifferenceRanges(v[i].range, newer.range);
2366
+ for (const diff of updatedRemoved) {
2367
+ results.push({
2368
+ range: diff,
2369
+ type: "removed" as const,
2370
+ timestamp: v[i].timestamp,
2371
+ });
2372
+ }
2373
+ for (const diff of updatedNewer) {
2374
+ v.push({
2375
+ range: diff,
2376
+ type: "added" as const,
2377
+ timestamp: newer.timestamp,
2378
+ });
2379
+ }
2380
+ consumed.add(j);
2381
+ }
2382
+ }
2383
+ rebalanceHistory.del(v[i].range.rangeHash);
2384
+ } else {
2385
+ results.push(v[i]);
2386
+ }
2387
+ } else if (v[i].type === "added") {
2388
+ // TODO should the below clause be used?
2389
+ // after testing it seems that certain changes are not propagating as expected using this
2390
+ /* if (rebalanceHistory.has(v[i].range.rangeHash)) {
2391
+ continue;
2392
+ } */
2393
+
2394
+ rebalanceHistory.add(v[i].range.rangeHash);
2395
+ if (!consumed.has(i)) {
2396
+ results.push(v[i]);
2397
+ }
2398
+ } else {
2399
+ results.push(v[i]);
2400
+ }
2401
+ }
2402
+
2403
+ all.push(...results);
2404
+ } else {
2405
+ rebalanceHistory.add(v[0].range.rangeHash);
2406
+ all.push(v[0]);
2407
+ }
2408
+ }
2409
+ return all;
2410
+ };
2411
+
1970
2412
  export const toRebalance = <R extends "u32" | "u64">(
1971
- changes: ReplicationChanges,
2413
+ changeOrChanges:
2414
+ | ReplicationChanges<ReplicationRangeIndexable<R>>
2415
+ | ReplicationChanges<ReplicationRangeIndexable<R>>[],
1972
2416
  index: Index<EntryReplicated<R>>,
2417
+ rebalanceHistory: Cache<string>,
1973
2418
  ): AsyncIterable<EntryReplicated<R>> => {
2419
+ const change = mergeReplicationChanges(changeOrChanges, rebalanceHistory);
2420
+
1974
2421
  const assignedRangesQuery = (changes: ReplicationChanges) => {
1975
2422
  let ors: Query[] = [];
2423
+ let onlyStrict = true;
1976
2424
  for (const change of changes) {
1977
2425
  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);
2426
+ ors.push(matchRange);
2427
+ if (change.range.mode === ReplicationIntent.NonStrict) {
2428
+ onlyStrict = false;
1985
2429
  }
1986
2430
  }
1987
2431
 
1988
2432
  // 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
- );
2433
+ if (!onlyStrict || changes.length === 0) {
2434
+ ors.push(
2435
+ new BoolQuery({
2436
+ key: "assignedToRangeBoundary",
2437
+ value: true,
2438
+ }),
2439
+ );
2440
+ }
1995
2441
 
1996
2442
  // entry is not sufficiently replicated, and we are to still keep it
1997
2443
  return new Or(ors);
@@ -1999,7 +2445,7 @@ export const toRebalance = <R extends "u32" | "u64">(
1999
2445
  return {
2000
2446
  [Symbol.asyncIterator]: async function* () {
2001
2447
  const iterator = index.iterate({
2002
- query: assignedRangesQuery(changes),
2448
+ query: assignedRangesQuery(change),
2003
2449
  });
2004
2450
 
2005
2451
  while (iterator.done() !== true) {
@@ -2024,11 +2470,14 @@ export const fetchOneFromPublicKey = async <
2024
2470
  >(
2025
2471
  publicKey: PublicSignKey,
2026
2472
  index: Index<ReplicationRangeIndexable<R>>,
2027
- roleAge: number,
2028
- now: number,
2029
2473
  numbers: Numbers<R>,
2030
2474
  options?: {
2031
- shape: S;
2475
+ shape?: S;
2476
+ time?: {
2477
+ roleAgeLimit: number;
2478
+ matured: boolean;
2479
+ now: number;
2480
+ };
2032
2481
  },
2033
2482
  ) => {
2034
2483
  let iterator = index.iterate<S>(
@@ -2041,13 +2490,14 @@ export const fetchOneFromPublicKey = async <
2041
2490
  await iterator.close();
2042
2491
  let node = result[0]?.value;
2043
2492
  if (node) {
2044
- if (!isMatured(node, now, roleAge)) {
2493
+ if (
2494
+ options?.time &&
2495
+ !isMatured(node, options.time.now, options.time.roleAgeLimit)
2496
+ ) {
2045
2497
  const matured = await fetchOne(
2046
- getClosestAround<S, R>(
2498
+ getClosestAroundOrContaining<S, R>(
2047
2499
  index,
2048
2500
  node.start1,
2049
- roleAge,
2050
- now,
2051
2501
  false,
2052
2502
  false,
2053
2503
  numbers,
@@ -2069,10 +2519,15 @@ export const getStartAndEnd = async <
2069
2519
  peers: Index<ReplicationRangeIndexable<R>>,
2070
2520
  start: NumberFromType<R> | PublicSignKey | undefined | undefined,
2071
2521
  widthToCoverScaled: NumberFromType<R>,
2072
- roleAge: number,
2073
- now: number,
2074
2522
  numbers: Numbers<R>,
2075
- options?: { shape: S },
2523
+ options?: {
2524
+ shape?: S;
2525
+ time?: {
2526
+ roleAgeLimit: number;
2527
+ matured: boolean;
2528
+ now: number;
2529
+ };
2530
+ },
2076
2531
  ): Promise<{
2077
2532
  startNode: ReturnTypeFromShape<ReplicationRangeIndexable<R>, S> | undefined;
2078
2533
  startLocation: NumberFromType<R>;
@@ -2089,8 +2544,6 @@ export const getStartAndEnd = async <
2089
2544
  startNode = await fetchOneClosest<S, R>(
2090
2545
  peers,
2091
2546
  startLocation,
2092
- roleAge,
2093
- now,
2094
2547
  false,
2095
2548
  true,
2096
2549
  numbers,
@@ -2100,14 +2553,7 @@ export const getStartAndEnd = async <
2100
2553
 
2101
2554
  if (start instanceof PublicSignKey) {
2102
2555
  // start at our node (local first)
2103
- startNode = await fetchOneFromPublicKey(
2104
- start,
2105
- peers,
2106
- roleAge,
2107
- now,
2108
- numbers,
2109
- options,
2110
- );
2556
+ startNode = await fetchOneFromPublicKey(start, peers, numbers, options);
2111
2557
  if (!startNode) {
2112
2558
  // fetch randomly
2113
2559
  await nodeFromPoint();
@@ -2168,19 +2614,23 @@ export const fetchOneClosest = <
2168
2614
  >(
2169
2615
  peers: Index<ReplicationRangeIndexable<R>>,
2170
2616
  point: NumberFromType<R>,
2171
- roleAge: number,
2172
- now: number,
2173
2617
  includeStrictBelow: boolean,
2174
2618
  includeStrictAbove: boolean,
2175
2619
  numbers: Numbers<R>,
2176
- options?: { shape?: S },
2620
+ options?: {
2621
+ shape?: S;
2622
+ time?: {
2623
+ roleAgeLimit: number;
2624
+ matured: boolean;
2625
+ now: number;
2626
+ };
2627
+ },
2177
2628
  ) => {
2178
2629
  return fetchOne<S, R>(
2179
- getClosestAround<S, R>(
2630
+ getClosestAroundOrContaining<S, R>(
2180
2631
  peers,
2181
2632
  point,
2182
- roleAge,
2183
- now,
2633
+
2184
2634
  includeStrictBelow,
2185
2635
  includeStrictAbove,
2186
2636
  numbers,