@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/index.ts CHANGED
@@ -63,6 +63,7 @@ import {
63
63
  } from "./exchange-heads.js";
64
64
  import {
65
65
  MAX_U32,
66
+ MAX_U64,
66
67
  type NumberFromType,
67
68
  type Numbers,
68
69
  bytesToNumber,
@@ -75,6 +76,8 @@ import {
75
76
  type EntryReplicated,
76
77
  EntryReplicatedU32,
77
78
  EntryReplicatedU64,
79
+ type ReplicationChange,
80
+ type ReplicationChanges,
78
81
  ReplicationIntent,
79
82
  type ReplicationRangeIndexable,
80
83
  ReplicationRangeIndexableU32,
@@ -82,9 +85,11 @@ import {
82
85
  ReplicationRangeMessage,
83
86
  SyncStatus,
84
87
  appromixateCoverage,
88
+ countCoveringRangesSameOwner,
89
+ debounceAggregationChanges,
90
+ getAllMergeCandiates,
85
91
  getCoverSet,
86
92
  getSamples,
87
- iHaveCoveringRange,
88
93
  isMatured,
89
94
  isReplicationRangeMessage,
90
95
  mergeRanges,
@@ -102,11 +107,8 @@ import {
102
107
  } from "./replication-domain-time.js";
103
108
  import {
104
109
  type ExtractDomainArgs,
105
- type ReplicationChange,
106
- type ReplicationChanges,
107
110
  type ReplicationDomain,
108
- debounceAggregationChanges,
109
- mergeReplicationChanges,
111
+ type ReplicationDomainConstructor,
110
112
  } from "./replication-domain.js";
111
113
  import {
112
114
  AbsoluteReplicas,
@@ -143,6 +145,7 @@ export {
143
145
  EntryReplicatedU32,
144
146
  EntryReplicatedU64,
145
147
  };
148
+ export { MAX_U32, MAX_U64, type NumberFromType };
146
149
  export const logger = loggerFn({ module: "shared-log" });
147
150
 
148
151
  const getLatestEntry = (
@@ -183,7 +186,6 @@ export type FixedReplicationOptions = {
183
186
  factor: number | bigint | "all" | "right";
184
187
  strict?: boolean; // if true, only this range will be replicated
185
188
  offset?: number | bigint;
186
- syncStatus?: SyncStatus;
187
189
  };
188
190
 
189
191
  export type ReplicationOptions<R extends "u32" | "u64" = any> =
@@ -191,7 +193,8 @@ export type ReplicationOptions<R extends "u32" | "u64" = any> =
191
193
  | FixedReplicationOptions
192
194
  | FixedReplicationOptions[]
193
195
  | number
194
- | boolean;
196
+ | boolean
197
+ | "resume";
195
198
 
196
199
  const isAdaptiveReplicatorOption = (
197
200
  options: ReplicationOptions<any>,
@@ -224,6 +227,10 @@ const isReplicationOptionsDependentOnPreviousState = (
224
227
  return true;
225
228
  }
226
229
 
230
+ if (options === "resume") {
231
+ return true;
232
+ }
233
+
227
234
  if (options == null) {
228
235
  // when not providing options, we assume previous behaviour
229
236
  return true;
@@ -249,7 +256,7 @@ interface IndexableDomain<R extends "u32" | "u64"> {
249
256
  properties: {
250
257
  id?: Uint8Array;
251
258
  offset: NumberFromType<R>;
252
- length: NumberFromType<R>;
259
+ width: NumberFromType<R>;
253
260
  mode?: ReplicationIntent;
254
261
  timestamp?: bigint;
255
262
  } & ({ publicKeyHash: string } | { publicKey: PublicSignKey }),
@@ -299,7 +306,7 @@ export type SharedLogOptions<
299
306
  waitForPruneDelay?: number;
300
307
  distributionDebounceTime?: number;
301
308
  compatibility?: number;
302
- domain?: D;
309
+ domain?: ReplicationDomainConstructor<D>;
303
310
  };
304
311
 
305
312
  export const DEFAULT_MIN_REPLICAS = 2;
@@ -374,6 +381,8 @@ export class SharedLog<
374
381
  private _replicationRangeIndex!: Index<ReplicationRangeIndexable<R>>;
375
382
  private _entryCoordinatesIndex!: Index<EntryReplicated<R>>;
376
383
  private coordinateToHash!: Cache<string>;
384
+ private recentlyRebalanced!: Cache<string>;
385
+
377
386
  private uniqueReplicators!: Set<string>;
378
387
 
379
388
  /* private _totalParticipation!: number; */
@@ -459,7 +468,7 @@ export class SharedLog<
459
468
  private _requestIPruneResponseReplicatorSet!: Map<string, Set<string>>; // tracks entry hash to peer hash
460
469
 
461
470
  private replicationChangeDebounceFn!: ReturnType<
462
- typeof debounceAggregationChanges
471
+ typeof debounceAggregationChanges<ReplicationRangeIndexable<R>>
463
472
  >;
464
473
 
465
474
  // regular distribution checks
@@ -557,13 +566,11 @@ export class SharedLog<
557
566
  {
558
567
  reset,
559
568
  checkDuplicates,
560
- syncStatus,
561
569
  announce,
562
570
  mergeSegments,
563
571
  }: {
564
572
  reset?: boolean;
565
573
  checkDuplicates?: boolean;
566
- syncStatus?: SyncStatus;
567
574
  mergeSegments?: boolean;
568
575
  announce?: (
569
576
  msg: AddedReplicationSegmentMessage | AllReplicatingSegmentsMessage,
@@ -573,8 +580,11 @@ export class SharedLog<
573
580
  let offsetWasProvided = false;
574
581
  if (isUnreplicationOptions(options)) {
575
582
  await this.unreplicate();
583
+ } else if (options === "resume") {
584
+ // don't do anything
576
585
  } else {
577
- let ranges: ReplicationRangeIndexable<any>[] = [];
586
+ let rangesToReplicate: ReplicationRangeIndexable<R>[] = [];
587
+ let rangesToUnreplicate: ReplicationRangeIndexable<R>[] = [];
578
588
 
579
589
  if (options == null) {
580
590
  options = {};
@@ -595,11 +605,11 @@ export class SharedLog<
595
605
  // not allowed
596
606
  return;
597
607
  }
598
- ranges = [maybeRange];
608
+ rangesToReplicate = [maybeRange];
599
609
 
600
610
  offsetWasProvided = true;
601
611
  } else if (isReplicationRangeMessage(options)) {
602
- ranges = [
612
+ rangesToReplicate = [
603
613
  options.toReplicationRangeIndexable(this.node.identity.publicKey),
604
614
  ];
605
615
 
@@ -650,60 +660,62 @@ export class SharedLog<
650
660
  let factorDenormalized = !normalized
651
661
  ? factor
652
662
  : this.indexableDomain.numbers.denormalize(factor as number);
653
- ranges.push(
663
+ rangesToReplicate.push(
654
664
  new this.indexableDomain.constructorRange({
655
665
  id: rangeArg.id,
656
666
  // @ts-ignore
657
667
  offset: offset,
658
668
  // @ts-ignore
659
- length: (factor === "all"
669
+ width: (factor === "all"
660
670
  ? fullWidth
661
671
  : factor === "right"
662
672
  ? // @ts-ignore
663
673
  fullWidth - offset
664
674
  : factorDenormalized) as NumberFromType<R>,
665
- /* typeof factor === "number"
666
- ? factor
667
- : factor === "all"
668
- ? width
669
- // @ts-ignore
670
- : width - offset, */
671
675
  publicKeyHash: this.node.identity.publicKey.hashcode(),
672
676
  mode: rangeArg.strict
673
677
  ? ReplicationIntent.Strict
674
678
  : ReplicationIntent.NonStrict, // automatic means that this range might be reused later for dynamic replication behaviour
675
679
  timestamp: timestamp ?? BigInt(+new Date()),
676
680
  }),
677
- /* new ReplicationRangeIndexable({
678
- id: rangeArg.id,
679
- normalized,
680
- offset: offset,
681
- length:
682
- typeof factor === "number"
683
- ? factor
684
- : factor === "all"
685
- ? width
686
- // @ts-ignore
687
- : width - offset,
688
- publicKeyHash: this.node.identity.publicKey.hashcode(),
689
- mode: rangeArg.strict
690
- ? ReplicationIntent.Strict
691
- : ReplicationIntent.NonStrict, // automatic means that this range might be reused later for dynamic replication behaviour
692
- timestamp: BigInt(+new Date()),
693
- }), */
694
681
  );
695
682
  }
696
683
 
697
- if (mergeSegments && ranges.length > 1) {
698
- const mergedSegment = mergeRanges(
699
- ranges,
700
- this.indexableDomain.numbers,
701
- );
702
- ranges = [mergedSegment];
684
+ if (mergeSegments) {
685
+ let range =
686
+ rangesToReplicate.length > 1
687
+ ? mergeRanges(rangesToReplicate, this.indexableDomain.numbers)
688
+ : rangesToReplicate[0];
689
+
690
+ // also merge segments that are already in the index
691
+ if (this.domain.canMerge) {
692
+ const mergable = await getAllMergeCandiates(
693
+ this.replicationIndex,
694
+ range,
695
+ this.indexableDomain.numbers,
696
+ );
697
+ const mergeableFiltered: ReplicationRangeIndexable<R>[] = [range];
698
+
699
+ for (const mergeCandidate of mergable) {
700
+ if (this.domain.canMerge(mergeCandidate, range)) {
701
+ mergeableFiltered.push(mergeCandidate);
702
+ if (mergeCandidate.idString !== range.idString) {
703
+ rangesToUnreplicate.push(mergeCandidate);
704
+ }
705
+ }
706
+ }
707
+ if (mergeableFiltered.length > 1) {
708
+ range = mergeRanges(
709
+ mergeableFiltered,
710
+ this.indexableDomain.numbers,
711
+ );
712
+ }
713
+ }
714
+ rangesToReplicate = [range];
703
715
  }
704
716
  }
705
717
 
706
- for (const range of ranges) {
718
+ for (const range of rangesToReplicate) {
707
719
  this.oldestOpenTime = Math.min(
708
720
  Number(range.timestamp),
709
721
  this.oldestOpenTime,
@@ -717,14 +729,31 @@ export class SharedLog<
717
729
  // but ({ replicate: 0.5, offset: 0.5 }) means that we want to add a range
718
730
  // TODO make behaviour more clear
719
731
  }
720
- await this.startAnnounceReplicating(ranges, {
732
+ if (rangesToUnreplicate.length > 0) {
733
+ await this.removeReplicationRanges(
734
+ rangesToUnreplicate,
735
+ this.node.identity.publicKey,
736
+ );
737
+ }
738
+
739
+ await this.startAnnounceReplicating(rangesToReplicate, {
721
740
  reset: resetRanges ?? false,
722
741
  checkDuplicates,
723
742
  announce,
724
- syncStatus,
725
743
  });
726
744
 
727
- return ranges;
745
+ if (rangesToUnreplicate.length > 0) {
746
+ await this.rpc.send(
747
+ new StoppedReplicating({
748
+ segmentIds: rangesToUnreplicate.map((x) => x.id),
749
+ }),
750
+ {
751
+ priority: 1,
752
+ },
753
+ );
754
+ }
755
+
756
+ return rangesToReplicate;
728
757
  }
729
758
  }
730
759
 
@@ -773,7 +802,6 @@ export class SharedLog<
773
802
  | ReplicationRangeMessage<any>[]
774
803
  | ReplicationOptions<R>
775
804
  | undefined = undefined;
776
- let syncStatus = SyncStatus.Unsynced;
777
805
 
778
806
  if (rangeOrEntry instanceof ReplicationRangeMessage) {
779
807
  range = rangeOrEntry;
@@ -783,7 +811,6 @@ export class SharedLog<
783
811
  offset: await this.domain.fromEntry(rangeOrEntry),
784
812
  normalized: false,
785
813
  };
786
- syncStatus = SyncStatus.Synced; /// we already have the entries
787
814
  } else if (Array.isArray(rangeOrEntry)) {
788
815
  let ranges: (ReplicationRangeMessage<any> | FixedReplicationOptions)[] =
789
816
  [];
@@ -793,9 +820,8 @@ export class SharedLog<
793
820
  factor: 1,
794
821
  offset: await this.domain.fromEntry(entry),
795
822
  normalized: false,
823
+ strict: true,
796
824
  });
797
-
798
- syncStatus = SyncStatus.Synced; /// we already have the entries
799
825
  } else {
800
826
  ranges.push(entry);
801
827
  }
@@ -805,18 +831,34 @@ export class SharedLog<
805
831
  range = rangeOrEntry ?? true;
806
832
  }
807
833
 
808
- return this._replicate(range, { ...options, syncStatus });
834
+ return this._replicate(range, options);
809
835
  }
810
836
 
811
- async unreplicate(rangeOrEntry?: Entry<T> | ReplicationRangeMessage<R>) {
812
- let range: FixedReplicationOptions;
837
+ async unreplicate(rangeOrEntry?: Entry<T> | { id: Uint8Array }[]) {
838
+ let segmentIds: Uint8Array<ArrayBufferLike>[];
813
839
  if (rangeOrEntry instanceof Entry) {
814
- range = {
840
+ let range: FixedReplicationOptions = {
815
841
  factor: 1,
816
842
  offset: await this.domain.fromEntry(rangeOrEntry),
817
843
  };
818
- } else if (rangeOrEntry instanceof ReplicationRangeMessage) {
819
- range = rangeOrEntry;
844
+ const indexed = this.replicationIndex.iterate({
845
+ query: {
846
+ width: 1,
847
+ start1: range.offset /* ,
848
+ hash: this.node.identity.publicKey.hashcode(), */,
849
+ },
850
+ });
851
+ segmentIds = (await indexed.all()).map((x) => x.id.key as Uint8Array);
852
+ if (segmentIds.length === 0) {
853
+ logger.warn("No segment found to unreplicate");
854
+ return;
855
+ }
856
+ } else if (Array.isArray(rangeOrEntry)) {
857
+ segmentIds = rangeOrEntry.map((x) => x.id);
858
+ if (segmentIds.length === 0) {
859
+ logger.warn("No segment found to unreplicate");
860
+ return;
861
+ }
820
862
  } else {
821
863
  this._isReplicating = false;
822
864
  this._isAdaptiveReplicating = false;
@@ -830,15 +872,14 @@ export class SharedLog<
830
872
  throw new Error("Unsupported when adaptive replicating");
831
873
  }
832
874
 
833
- const indexed = this.replicationIndex.iterate({
834
- query: {
835
- width: 1,
836
- start1: range.offset,
837
- },
838
- });
839
-
840
- const segmentIds = (await indexed.all()).map((x) => x.id.key as Uint8Array);
841
- await this.removeReplicationRange(segmentIds, this.node.identity.publicKey);
875
+ const rangesToRemove = await this.resolveReplicationRangesFromIdsAndKey(
876
+ segmentIds,
877
+ this.node.identity.publicKey,
878
+ );
879
+ await this.removeReplicationRanges(
880
+ rangesToRemove,
881
+ this.node.identity.publicKey,
882
+ );
842
883
  await this.rpc.send(new StoppedReplicating({ segmentIds }), {
843
884
  priority: 1,
844
885
  });
@@ -881,10 +922,12 @@ export class SharedLog<
881
922
  }
882
923
  }
883
924
 
925
+ const timestamp = BigInt(+new Date());
884
926
  for (const x of deleted) {
885
927
  this.replicationChangeDebounceFn.add({
886
928
  range: x.value,
887
929
  type: "removed",
930
+ timestamp,
888
931
  });
889
932
  }
890
933
 
@@ -917,7 +960,10 @@ export class SharedLog<
917
960
  : +new Date();
918
961
  }
919
962
 
920
- private async removeReplicationRange(ids: Uint8Array[], from: PublicSignKey) {
963
+ private async resolveReplicationRangesFromIdsAndKey(
964
+ ids: Uint8Array[],
965
+ from: PublicSignKey,
966
+ ) {
921
967
  let idMatcher = new Or(
922
968
  ids.map((x) => new ByteMatchQuery({ key: "id", value: x })),
923
969
  );
@@ -929,10 +975,17 @@ export class SharedLog<
929
975
  });
930
976
 
931
977
  let query = new And([idMatcher, identityMatcher]);
932
-
978
+ return (await this.replicationIndex.iterate({ query }).all()).map(
979
+ (x) => x.value,
980
+ );
981
+ }
982
+ private async removeReplicationRanges(
983
+ ranges: ReplicationRangeIndexable<R>[],
984
+ from: PublicSignKey,
985
+ ) {
933
986
  const pendingMaturity = this.pendingMaturity.get(from.hashcode());
934
987
  if (pendingMaturity) {
935
- for (const id of ids) {
988
+ for (const id of ranges) {
936
989
  const info = pendingMaturity.get(id.toString());
937
990
  if (info) {
938
991
  clearTimeout(info.timeout);
@@ -944,7 +997,11 @@ export class SharedLog<
944
997
  }
945
998
  }
946
999
 
947
- await this.replicationIndex.del({ query });
1000
+ await this.replicationIndex.del({
1001
+ query: new Or(
1002
+ ranges.map((x) => new ByteMatchQuery({ key: "id", value: x.id })),
1003
+ ),
1004
+ });
948
1005
 
949
1006
  const otherSegmentsIterator = this.replicationIndex.iterate(
950
1007
  { query: { hash: from.hashcode() } },
@@ -974,15 +1031,17 @@ export class SharedLog<
974
1031
  {
975
1032
  reset,
976
1033
  checkDuplicates,
977
- }: { reset?: boolean; checkDuplicates?: boolean } = {},
1034
+ timestamp: ts,
1035
+ }: { reset?: boolean; checkDuplicates?: boolean; timestamp?: number } = {},
978
1036
  ) {
979
1037
  if (this._isTrustedReplicator && !(await this._isTrustedReplicator(from))) {
980
1038
  return undefined;
981
1039
  }
982
1040
  let isNewReplicator = false;
1041
+ let timestamp = BigInt(ts ?? +new Date());
983
1042
 
984
- let diffs: ReplicationChanges;
985
- let deleted: ReplicationRangeIndexable<any>[] | undefined = undefined;
1043
+ let diffs: ReplicationChanges<ReplicationRangeIndexable<R>>;
1044
+ let deleted: ReplicationRangeIndexable<R>[] | undefined = undefined;
986
1045
  if (reset) {
987
1046
  deleted = (
988
1047
  await this.replicationIndex
@@ -998,10 +1057,10 @@ export class SharedLog<
998
1057
 
999
1058
  diffs = [
1000
1059
  ...deleted.map((x) => {
1001
- return { range: x, type: "removed" as const };
1060
+ return { range: x, type: "removed" as const, timestamp };
1002
1061
  }),
1003
1062
  ...ranges.map((x) => {
1004
- return { range: x, type: "added" as const };
1063
+ return { range: x, type: "added" as const, timestamp };
1005
1064
  }),
1006
1065
  ];
1007
1066
 
@@ -1031,7 +1090,9 @@ export class SharedLog<
1031
1090
 
1032
1091
  // TODO also deduplicate/de-overlap among the ranges that ought to be inserted?
1033
1092
  for (const range of ranges) {
1034
- if (!(await iHaveCoveringRange(this.replicationIndex, range))) {
1093
+ if (
1094
+ !(await countCoveringRangesSameOwner(this.replicationIndex, range))
1095
+ ) {
1035
1096
  deduplicated.push(range);
1036
1097
  }
1037
1098
  }
@@ -1042,19 +1103,31 @@ export class SharedLog<
1042
1103
  existingMap.set(result.value.idString, result.value);
1043
1104
  }
1044
1105
 
1045
- let changes: ReplicationChanges = ranges
1106
+ let changes: ReplicationChanges<ReplicationRangeIndexable<R>> = ranges
1046
1107
  .map((x) => {
1047
1108
  const prev = existingMap.get(x.idString);
1048
1109
  if (prev) {
1049
1110
  if (prev.equalRange(x)) {
1050
- return undefined;
1111
+ return [];
1051
1112
  }
1052
- return { range: x, prev, type: "updated" };
1113
+ return [
1114
+ {
1115
+ range: prev,
1116
+ timestamp: x.timestamp - 1n,
1117
+ prev,
1118
+ type: "replaced" as const,
1119
+ },
1120
+ {
1121
+ range: x,
1122
+ timestamp: x.timestamp,
1123
+ type: "added" as const,
1124
+ },
1125
+ ];
1053
1126
  } else {
1054
- return { range: x, type: "added" };
1127
+ return { range: x, timestamp: x.timestamp, type: "added" as const };
1055
1128
  }
1056
1129
  })
1057
- .filter((x) => x != null) as ReplicationChanges;
1130
+ .flat() as ReplicationChanges<ReplicationRangeIndexable<R>>;
1058
1131
  diffs = changes;
1059
1132
  }
1060
1133
 
@@ -1065,7 +1138,7 @@ export class SharedLog<
1065
1138
  let isAllMature = true;
1066
1139
 
1067
1140
  for (const diff of diffs) {
1068
- if (diff.type === "added" || diff.type === "updated") {
1141
+ if (diff.type === "added") {
1069
1142
  /* if (this.closed) {
1070
1143
  return;
1071
1144
  } */
@@ -1104,7 +1177,7 @@ export class SharedLog<
1104
1177
  }),
1105
1178
  );
1106
1179
 
1107
- this.replicationChangeDebounceFn.add(diff); // we need to call this here because the outcom of findLeaders will be different when some ranges become mature, i.e. some of data we own might be prunable!
1180
+ this.replicationChangeDebounceFn.add({ ...diff, matured: true }); // we need to call this here because the outcom of findLeaders will be different when some ranges become mature, i.e. some of data we own might be prunable!
1108
1181
  pendingRanges.delete(diff.range.idString);
1109
1182
  if (pendingRanges.size === 0) {
1110
1183
  this.pendingMaturity.delete(diff.range.hash);
@@ -1123,7 +1196,7 @@ export class SharedLog<
1123
1196
  });
1124
1197
  }
1125
1198
  }
1126
- } else {
1199
+ } else if (diff.type === "removed") {
1127
1200
  const pendingFromPeer = this.pendingMaturity.get(diff.range.hash);
1128
1201
  if (pendingFromPeer) {
1129
1202
  const prev = pendingFromPeer.get(diff.range.idString);
@@ -1136,6 +1209,7 @@ export class SharedLog<
1136
1209
  }
1137
1210
  }
1138
1211
  }
1212
+ // else replaced, do nothing
1139
1213
  }
1140
1214
 
1141
1215
  if (reset) {
@@ -1394,10 +1468,10 @@ export class SharedLog<
1394
1468
 
1395
1469
  // TODO types
1396
1470
  this.domain = options?.domain
1397
- ? (options.domain as any as D)
1471
+ ? (options.domain(this) as D)
1398
1472
  : (createReplicationDomainHash(
1399
1473
  options?.compatibility && options?.compatibility < 10 ? "u32" : "u64",
1400
- ) as D);
1474
+ )(this) as D);
1401
1475
  this.indexableDomain = createIndexableDomainFromResolution(
1402
1476
  this.domain.resolution,
1403
1477
  );
@@ -1406,6 +1480,7 @@ export class SharedLog<
1406
1480
  this._pendingIHave = new Map();
1407
1481
  this.latestReplicationInfoMessage = new Map();
1408
1482
  this.coordinateToHash = new Cache<string>({ max: 1e6, ttl: 1e4 });
1483
+ this.recentlyRebalanced = new Cache<string>({ max: 1e4, ttl: 1e5 });
1409
1484
 
1410
1485
  this.uniqueReplicators = new Set();
1411
1486
 
@@ -1476,7 +1551,9 @@ export class SharedLog<
1476
1551
  this._requestIPruneSent = new Map();
1477
1552
  this._requestIPruneResponseReplicatorSet = new Map();
1478
1553
 
1479
- this.replicationChangeDebounceFn = debounceAggregationChanges(
1554
+ this.replicationChangeDebounceFn = debounceAggregationChanges<
1555
+ ReplicationRangeIndexable<R>
1556
+ >(
1480
1557
  (change) =>
1481
1558
  this.onReplicationChange(change).then(() =>
1482
1559
  this.rebalanceParticipationDebounced?.(),
@@ -1800,17 +1877,18 @@ export class SharedLog<
1800
1877
  ) {
1801
1878
  let roleAge = options?.roleAge ?? (await this.getDefaultMinRoleAge());
1802
1879
  let eager = options?.eager ?? false;
1803
- const range = await this.domain.fromArgs(args, this);
1804
-
1880
+ const range = await this.domain.fromArgs(args);
1881
+
1882
+ const width =
1883
+ range.length ??
1884
+ (await minimumWidthToCover<R>(
1885
+ this.replicas.min.getValue(this),
1886
+ this.indexableDomain.numbers,
1887
+ ));
1805
1888
  const set = await getCoverSet<R>({
1806
1889
  peers: this.replicationIndex,
1807
1890
  start: range.offset,
1808
- widthToCoverScaled:
1809
- range.length ??
1810
- (await minimumWidthToCover<R>(
1811
- this.replicas.min.getValue(this),
1812
- this.indexableDomain.numbers,
1813
- )),
1891
+ widthToCoverScaled: width,
1814
1892
  roleAge,
1815
1893
  eager,
1816
1894
  numbers: this.indexableDomain.numbers,
@@ -1837,6 +1915,7 @@ export class SharedLog<
1837
1915
 
1838
1916
  this.distributeQueue?.clear();
1839
1917
  this.coordinateToHash.clear();
1918
+ this.recentlyRebalanced.clear();
1840
1919
  this.uniqueReplicators.clear();
1841
1920
 
1842
1921
  this._closeController.abort();
@@ -2378,7 +2457,11 @@ export class SharedLog<
2378
2457
  x.toReplicationRangeIndexable(context.from!),
2379
2458
  ),
2380
2459
  context.from!,
2381
- { reset, checkDuplicates: true },
2460
+ {
2461
+ reset,
2462
+ checkDuplicates: true,
2463
+ timestamp: Number(context.timestamp),
2464
+ },
2382
2465
  );
2383
2466
 
2384
2467
  /* await this._modifyReplicators(msg.role, context.from!); */
@@ -2403,7 +2486,19 @@ export class SharedLog<
2403
2486
  return;
2404
2487
  }
2405
2488
 
2406
- await this.removeReplicationRange(msg.segmentIds, context.from);
2489
+ const rangesToRemove = await this.resolveReplicationRangesFromIdsAndKey(
2490
+ msg.segmentIds,
2491
+ context.from,
2492
+ );
2493
+ await this.removeReplicationRanges(rangesToRemove, context.from);
2494
+ const timestamp = BigInt(+new Date());
2495
+ for (const range of rangesToRemove) {
2496
+ this.replicationChangeDebounceFn.add({
2497
+ range,
2498
+ type: "removed",
2499
+ timestamp,
2500
+ });
2501
+ }
2407
2502
  } else {
2408
2503
  throw new Error("Unexpected message");
2409
2504
  }
@@ -2563,6 +2658,7 @@ export class SharedLog<
2563
2658
  | boolean
2564
2659
  | {
2565
2660
  mergeSegments?: boolean;
2661
+ assumeSynced?: boolean;
2566
2662
  };
2567
2663
  },
2568
2664
  ): Promise<void> {
@@ -2605,11 +2701,20 @@ export class SharedLog<
2605
2701
 
2606
2702
  const persistCoordinate = async (entry: Entry<T>) => {
2607
2703
  const minReplicas = decodeReplicas(entry).getValue(this);
2608
- await this.findLeaders(
2704
+ const leaders = await this.findLeaders(
2609
2705
  await this.createCoordinates(entry, minReplicas),
2610
2706
  entry,
2611
2707
  { persist: {} },
2612
2708
  );
2709
+
2710
+ if (
2711
+ options?.replicate &&
2712
+ typeof options.replicate !== "boolean" &&
2713
+ options.replicate.assumeSynced
2714
+ ) {
2715
+ // make sure we dont start to initate syncing process outwards for this entry
2716
+ this.addPeersToGidPeerHistory(entry.meta.gid, leaders.keys());
2717
+ }
2613
2718
  };
2614
2719
  let entriesToPersist: Entry<T>[] = [];
2615
2720
  let joinOptions = {
@@ -3468,9 +3573,10 @@ export class SharedLog<
3468
3573
  this._gidPeersHistory.clear();
3469
3574
  }
3470
3575
 
3576
+ const timestamp = BigInt(+new Date());
3471
3577
  this.onReplicationChange(
3472
3578
  (await this.getAllReplicationSegments()).map((x) => {
3473
- return { range: x, type: "added" };
3579
+ return { range: x, type: "added", timestamp };
3474
3580
  }),
3475
3581
  );
3476
3582
  }
@@ -3480,7 +3586,9 @@ export class SharedLog<
3480
3586
  }
3481
3587
 
3482
3588
  async onReplicationChange(
3483
- changeOrChanges: ReplicationChanges | ReplicationChanges[],
3589
+ changeOrChanges:
3590
+ | ReplicationChanges<ReplicationRangeIndexable<R>>
3591
+ | ReplicationChanges<ReplicationRangeIndexable<R>>[],
3484
3592
  ) {
3485
3593
  /**
3486
3594
  * TODO use information of new joined/leaving peer to create a subset of heads
@@ -3493,7 +3601,6 @@ export class SharedLog<
3493
3601
 
3494
3602
  await this.log.trim();
3495
3603
 
3496
- const change = mergeReplicationChanges(changeOrChanges);
3497
3604
  const changed = false;
3498
3605
 
3499
3606
  try {
@@ -3503,13 +3610,13 @@ export class SharedLog<
3503
3610
  > = new Map();
3504
3611
 
3505
3612
  for await (const entryReplicated of toRebalance<R>(
3506
- change,
3613
+ changeOrChanges,
3507
3614
  this.entryCoordinatesIndex,
3615
+ this.recentlyRebalanced,
3508
3616
  )) {
3509
3617
  if (this.closed) {
3510
3618
  break;
3511
3619
  }
3512
-
3513
3620
  let oldPeersSet = this._gidPeersHistory.get(entryReplicated.gid);
3514
3621
  let isLeader = false;
3515
3622
 
@@ -3660,7 +3767,7 @@ export class SharedLog<
3660
3767
  // TODO can not reuse old range, since it will (potentially) affect the index because of sideeffects
3661
3768
  dynamicRange = new this.indexableDomain.constructorRange({
3662
3769
  offset: dynamicRange.start1,
3663
- length: this.indexableDomain.numbers.denormalize(newFactor),
3770
+ width: this.indexableDomain.numbers.denormalize(newFactor),
3664
3771
  publicKeyHash: dynamicRange.hash,
3665
3772
  id: dynamicRange.id,
3666
3773
  mode: dynamicRange.mode,
@@ -3729,7 +3836,7 @@ export class SharedLog<
3729
3836
  if (!range) {
3730
3837
  range = new this.indexableDomain.constructorRange({
3731
3838
  offset: this.getDynamicRangeOffset(),
3732
- length: this.indexableDomain.numbers.zero,
3839
+ width: this.indexableDomain.numbers.zero,
3733
3840
  publicKeyHash: this.node.identity.publicKey.hashcode(),
3734
3841
  mode: ReplicationIntent.NonStrict,
3735
3842
  timestamp: BigInt(+new Date()),