@peerbit/shared-log 12.2.0 → 12.3.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.
Files changed (40) hide show
  1. package/dist/benchmark/pid-convergence.d.ts +2 -0
  2. package/dist/benchmark/pid-convergence.d.ts.map +1 -0
  3. package/dist/benchmark/pid-convergence.js +138 -0
  4. package/dist/benchmark/pid-convergence.js.map +1 -0
  5. package/dist/benchmark/rateless-iblt-sender-startsync.d.ts +2 -0
  6. package/dist/benchmark/rateless-iblt-sender-startsync.d.ts.map +1 -0
  7. package/dist/benchmark/rateless-iblt-sender-startsync.js +104 -0
  8. package/dist/benchmark/rateless-iblt-sender-startsync.js.map +1 -0
  9. package/dist/benchmark/rateless-iblt-startsync-cache.d.ts +2 -0
  10. package/dist/benchmark/rateless-iblt-startsync-cache.d.ts.map +1 -0
  11. package/dist/benchmark/rateless-iblt-startsync-cache.js +112 -0
  12. package/dist/benchmark/rateless-iblt-startsync-cache.js.map +1 -0
  13. package/dist/benchmark/sync-catchup.d.ts +3 -0
  14. package/dist/benchmark/sync-catchup.d.ts.map +1 -0
  15. package/dist/benchmark/sync-catchup.js +109 -0
  16. package/dist/benchmark/sync-catchup.js.map +1 -0
  17. package/dist/src/index.d.ts +16 -3
  18. package/dist/src/index.d.ts.map +1 -1
  19. package/dist/src/index.js +259 -82
  20. package/dist/src/index.js.map +1 -1
  21. package/dist/src/ranges.d.ts +1 -0
  22. package/dist/src/ranges.d.ts.map +1 -1
  23. package/dist/src/ranges.js +48 -18
  24. package/dist/src/ranges.js.map +1 -1
  25. package/dist/src/sync/index.d.ts +14 -0
  26. package/dist/src/sync/index.d.ts.map +1 -1
  27. package/dist/src/sync/rateless-iblt.d.ts +14 -22
  28. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  29. package/dist/src/sync/rateless-iblt.js +137 -22
  30. package/dist/src/sync/rateless-iblt.js.map +1 -1
  31. package/dist/src/sync/simple.d.ts +3 -1
  32. package/dist/src/sync/simple.d.ts.map +1 -1
  33. package/dist/src/sync/simple.js +23 -1
  34. package/dist/src/sync/simple.js.map +1 -1
  35. package/package.json +12 -12
  36. package/src/index.ts +333 -126
  37. package/src/ranges.ts +97 -65
  38. package/src/sync/index.ts +19 -0
  39. package/src/sync/rateless-iblt.ts +187 -41
  40. package/src/sync/simple.ts +25 -2
package/src/ranges.ts CHANGED
@@ -2064,6 +2064,7 @@ export const getSamples = async <R extends "u32" | "u64">(
2064
2064
  options?: {
2065
2065
  onlyIntersecting?: boolean;
2066
2066
  uniqueReplicators?: Set<string>;
2067
+ peerFilter?: Set<string>;
2067
2068
  },
2068
2069
  ): Promise<Map<string, { intersecting: boolean }>> => {
2069
2070
  const leaders: Map<string, { intersecting: boolean }> = new Map();
@@ -2075,6 +2076,7 @@ export const getSamples = async <R extends "u32" | "u64">(
2075
2076
  let matured = 0;
2076
2077
 
2077
2078
  let uniqueVisited = new Set<string>();
2079
+ const peerFilter = options?.peerFilter;
2078
2080
  for (let i = 0; i < cursor.length; i++) {
2079
2081
  let point = cursor[i];
2080
2082
 
@@ -2084,6 +2086,9 @@ export const getSamples = async <R extends "u32" | "u64">(
2084
2086
  );
2085
2087
 
2086
2088
  for (const rect of allContaining) {
2089
+ if (peerFilter && !peerFilter.has(rect.value.hash)) {
2090
+ continue;
2091
+ }
2087
2092
  uniqueVisited.add(rect.value.hash);
2088
2093
  let prev = leaders.get(rect.value.hash);
2089
2094
  if (!prev) {
@@ -2096,7 +2101,7 @@ export const getSamples = async <R extends "u32" | "u64">(
2096
2101
  }
2097
2102
  }
2098
2103
 
2099
- if (options?.uniqueReplicators) {
2104
+ if (options?.uniqueReplicators && options.uniqueReplicators.size > 0) {
2100
2105
  if (
2101
2106
  options.uniqueReplicators.size === leaders.size ||
2102
2107
  options.uniqueReplicators.size === uniqueVisited.size
@@ -2114,6 +2119,9 @@ export const getSamples = async <R extends "u32" | "u64">(
2114
2119
  roleAge,
2115
2120
  peers,
2116
2121
  (rect, m) => {
2122
+ if (peerFilter && !peerFilter.has(rect.hash)) {
2123
+ return;
2124
+ }
2117
2125
  uniqueVisited.add(rect.hash);
2118
2126
  const prev = leaders.get(rect.hash);
2119
2127
  if (m) {
@@ -2234,7 +2242,7 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
2234
2242
  nextLocation: NumberFromType<R>,
2235
2243
  roleAge: number,
2236
2244
  ) => {
2237
- let next = await fetchOne(
2245
+ const next = await fetchOne(
2238
2246
  iterateRangesContainingPoint<undefined, R>(peers, nextLocation, {
2239
2247
  sort: [new Sort({ key: "end2", direction: SortDirection.DESC })],
2240
2248
  time: {
@@ -2243,31 +2251,31 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
2243
2251
  now,
2244
2252
  },
2245
2253
  }),
2246
- ); // get entersecting sort by largest end2
2254
+ ); // get intersecting sort by largest end2
2247
2255
  return next;
2248
2256
  };
2249
2257
 
2250
- const resolveNextAbove = async (
2251
- nextLocation: NumberFromType<R>,
2252
- roleAge: number,
2253
- ) => {
2254
- // if not get closest from above
2255
- let next = await fetchOne<undefined, R>(
2256
- getClosest("above", peers, nextLocation, true, properties.numbers, {
2257
- time: {
2258
- matured: true,
2259
- roleAgeLimit: roleAge,
2260
- now,
2261
- },
2262
- }),
2263
- );
2264
- return next;
2265
- };
2258
+ const resolveNextAbove = async (
2259
+ nextLocation: NumberFromType<R>,
2260
+ roleAge: number,
2261
+ ) => {
2262
+ // if not get closest from above
2263
+ const next = await fetchOne<undefined, R>(
2264
+ getClosest("above", peers, nextLocation, true, properties.numbers, {
2265
+ time: {
2266
+ matured: true,
2267
+ roleAgeLimit: roleAge,
2268
+ now,
2269
+ },
2270
+ }),
2271
+ );
2272
+ return next;
2273
+ };
2266
2274
 
2267
2275
  const resolveNext = async (
2268
2276
  nextLocation: NumberFromType<R>,
2269
2277
  roleAge: number,
2270
- ): Promise<[ReplicationRangeIndexable<R>, boolean]> => {
2278
+ ): Promise<[ReplicationRangeIndexable<R> | undefined, boolean]> => {
2271
2279
  const containing = await resolveNextContaining(nextLocation, roleAge);
2272
2280
  if (containing) {
2273
2281
  return [containing, true];
@@ -2288,12 +2296,25 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
2288
2296
  from: NumberFromType<R>,
2289
2297
  ) => {
2290
2298
  const toEnd2 = properties.numbers.increment(to.end2); // TODO investigate why this is needed
2291
- if (toEnd2 < from || to.wrapped) {
2299
+ if (toEnd2 < from) {
2292
2300
  wrappedOnce = true;
2293
2301
  // @ts-ignore
2294
2302
  coveredLength += properties.numbers.maxValue - from;
2295
2303
  // @ts-ignore
2296
2304
  coveredLength += toEnd2;
2305
+ } else if (to.wrapped) {
2306
+ // When the range is wrapped and `from` is in the second segment (near zero),
2307
+ // the distance to `end2` does not wrap. Otherwise we must wrap to reach `end2`.
2308
+ if (from < to.end2) {
2309
+ // @ts-ignore
2310
+ coveredLength += toEnd2 - from;
2311
+ } else {
2312
+ wrappedOnce = true;
2313
+ // @ts-ignore
2314
+ coveredLength += properties.numbers.maxValue - from;
2315
+ // @ts-ignore
2316
+ coveredLength += toEnd2;
2317
+ }
2297
2318
  } else {
2298
2319
  // @ts-ignore
2299
2320
  coveredLength += to.end1 - from;
@@ -2314,6 +2335,7 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
2314
2335
  (coveredLength <= properties.numbers.maxValue || !wrappedOnce) // eslint-disable-line no-unmodified-loop-condition
2315
2336
  ) {
2316
2337
  let distanceBefore = coveredLength;
2338
+ const nextLocationBefore = nextLocation;
2317
2339
 
2318
2340
  let nextCandidate = await resolveNext(nextLocation, roleAge);
2319
2341
  let matured = true;
@@ -2343,55 +2365,53 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
2343
2365
  let last = current;
2344
2366
  current = nextCandidate[0];
2345
2367
 
2346
- let isLast =
2347
- distanceBefore < widthToCoverScaled &&
2348
- coveredLength >= widthToCoverScaled;
2368
+ const isLast =
2369
+ distanceBefore < widthToCoverScaled && coveredLength >= widthToCoverScaled;
2370
+
2371
+ const lastDistanceToEndLocation = properties.numbers.min(
2372
+ getDistance(
2373
+ last.start1,
2374
+ endLocation,
2375
+ "closest",
2376
+ properties.numbers.maxValue,
2377
+ ),
2378
+ getDistance(
2379
+ last.end2,
2380
+ endLocation,
2381
+ "closest",
2382
+ properties.numbers.maxValue,
2383
+ ),
2384
+ );
2385
+
2386
+ const currentDistanceToEndLocation = properties.numbers.min(
2387
+ getDistance(
2388
+ current.start1,
2389
+ endLocation,
2390
+ "closest",
2391
+ properties.numbers.maxValue,
2392
+ ),
2393
+ getDistance(
2394
+ current.end2,
2395
+ endLocation,
2396
+ "closest",
2397
+ properties.numbers.maxValue,
2398
+ ),
2399
+ );
2349
2400
 
2350
2401
  if (
2351
2402
  !isLast ||
2352
2403
  nextCandidate[1] ||
2353
- properties.numbers.min(
2354
- getDistance(
2355
- last.start1,
2356
- endLocation,
2357
- "closest",
2358
- properties.numbers.maxValue,
2359
- ),
2360
- getDistance(
2361
- last.end2,
2362
- endLocation,
2363
- "closest",
2364
- properties.numbers.maxValue,
2365
- ),
2366
- ) >
2367
- properties.numbers.min(
2368
- getDistance(
2369
- current.start1,
2370
- endLocation,
2371
- "closest",
2372
- properties.numbers.maxValue,
2373
- ),
2374
- getDistance(
2375
- current.end2,
2376
- endLocation,
2377
- "closest",
2378
- properties.numbers.maxValue,
2379
- ),
2380
- )
2404
+ lastDistanceToEndLocation >= currentDistanceToEndLocation
2381
2405
  ) {
2382
2406
  ret.add(current.hash);
2383
2407
  }
2384
2408
 
2385
- if (isLast && !nextCandidate[1] /* || equals(endRect.id, current.id) */) {
2386
- break;
2387
- }
2388
-
2389
2409
  if (matured) {
2390
2410
  maturedCoveredLength = coveredLength;
2391
2411
  }
2392
2412
 
2393
2413
  let startForNext = extraDistanceForNext
2394
- ? properties.numbers.increment(current.end2)
2414
+ ? properties.numbers.increment(nextLocation)
2395
2415
  : current.end2;
2396
2416
  nextLocation = endIsWrapped
2397
2417
  ? wrappedOnce
@@ -2399,6 +2419,15 @@ export const getCoverSet = async <R extends "u32" | "u64">(properties: {
2399
2419
  : startForNext
2400
2420
  : properties.numbers.min(startForNext, endLocation);
2401
2421
 
2422
+ // Safety: ensure we always make progress to avoid infinite loops (can happen when
2423
+ // the chosen range is the same and `nextLocation` doesn't advance).
2424
+ if (
2425
+ nextLocation === nextLocationBefore &&
2426
+ coveredLength === distanceBefore
2427
+ ) {
2428
+ break;
2429
+ }
2430
+
2402
2431
  if (
2403
2432
  (typeof nextLocation === "bigint" &&
2404
2433
  nextLocation === (endLocation as bigint)) ||
@@ -2603,19 +2632,19 @@ export const mergeReplicationChanges = <R extends NumericType>(
2603
2632
  let results: ReplicationChange<ReplicationRangeIndexable<R>>[] = [];
2604
2633
  let consumed: Set<number> = new Set();
2605
2634
  for (let i = 0; i < v.length; i++) {
2606
- // if segment is removed and we have previously processed it
2607
- // then go over each overlapping added segment add remove the removal,
2608
- // equivalent is that this would represent (1 - 1 + 1) = 1
2635
+ // If segment is removed and we have previously processed it then go over each
2636
+ // overlapping added segment and remove the overlap. Equivalent to: (1 - 1 + 1) = 1.
2609
2637
  if (v[i].type === "removed" || v[i].type === "replaced") {
2610
2638
  if (rebalanceHistory.has(v[i].range.rangeHash)) {
2611
- let vStart = v.length;
2639
+ let adjusted = false;
2640
+ const vStart = v.length;
2612
2641
  for (let j = i + 1; j < vStart; j++) {
2613
2642
  const newer = v[j];
2614
2643
  if (newer.type === "added" && !newer.matured) {
2615
- const {
2616
- rangesFromA: updatedRemoved,
2617
- rangesFromB: updatedNewer,
2618
- } = symmetricDifferenceRanges(v[i].range, newer.range);
2644
+ adjusted = true;
2645
+ const { rangesFromA: updatedRemoved, rangesFromB: updatedNewer } =
2646
+ symmetricDifferenceRanges(v[i].range, newer.range);
2647
+
2619
2648
  for (const diff of updatedRemoved) {
2620
2649
  results.push({
2621
2650
  range: diff,
@@ -2634,6 +2663,9 @@ export const mergeReplicationChanges = <R extends NumericType>(
2634
2663
  }
2635
2664
  }
2636
2665
  rebalanceHistory.del(v[i].range.rangeHash);
2666
+ if (!adjusted) {
2667
+ results.push(v[i]);
2668
+ }
2637
2669
  } else {
2638
2670
  results.push(v[i]);
2639
2671
  }
package/src/sync/index.ts CHANGED
@@ -8,6 +8,24 @@ import type { Numbers } from "../integers.js";
8
8
  import type { TransportMessage } from "../message.js";
9
9
  import type { EntryReplicated, ReplicationRangeIndexable } from "../ranges.js";
10
10
 
11
+ export type SyncPriorityFn<R extends "u32" | "u64"> = (
12
+ entry: EntryReplicated<R>,
13
+ ) => number;
14
+
15
+ export type SyncOptions<R extends "u32" | "u64"> = {
16
+ /**
17
+ * Higher numbers are synced first.
18
+ * The callback should be fast and side-effect free.
19
+ */
20
+ priority?: SyncPriorityFn<R>;
21
+
22
+ /**
23
+ * When using rateless IBLT sync, optionally pre-sync up to this many
24
+ * high-priority entries using the simple synchronizer.
25
+ */
26
+ maxSimpleEntries?: number;
27
+ };
28
+
11
29
  export type SynchronizerComponents<R extends "u32" | "u64"> = {
12
30
  rpc: RPC<TransportMessage, TransportMessage>;
13
31
  rangeIndex: Index<ReplicationRangeIndexable<R>, any>;
@@ -15,6 +33,7 @@ export type SynchronizerComponents<R extends "u32" | "u64"> = {
15
33
  log: Log<any>;
16
34
  coordinateToHash: Cache<string>;
17
35
  numbers: Numbers<R>;
36
+ sync?: SyncOptions<R>;
18
37
  };
19
38
  export type SynchronizerConstructor<R extends "u32" | "u64"> = new (
20
39
  properties: SynchronizerComponents<R>,
@@ -12,14 +12,16 @@ import {
12
12
  import type { RPC, RequestContext } from "@peerbit/rpc";
13
13
  import { SilentDelivery } from "@peerbit/stream-interface";
14
14
  import { type EntryWithRefs } from "../exchange-heads.js";
15
- import { type Numbers } from "../integers.js";
16
15
  import { TransportMessage } from "../message.js";
17
16
  import {
18
17
  type EntryReplicated,
19
- type ReplicationRangeIndexable,
20
18
  matchEntriesInRangeQuery,
21
19
  } from "../ranges.js";
22
- import type { SyncableKey, Syncronizer } from "./index.js";
20
+ import type {
21
+ SyncableKey,
22
+ SynchronizerComponents,
23
+ Syncronizer,
24
+ } from "./index.js";
23
25
  import { SimpleSyncronizer } from "./simple.js";
24
26
 
25
27
  export const logger = loggerFn("peerbit:shared-log:rateless");
@@ -185,6 +187,13 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
185
187
  simple: SimpleSyncronizer<D>;
186
188
 
187
189
  startedOrCompletedSynchronizations: Cache<string>;
190
+ private localRangeEncoderCacheVersion = 0;
191
+ private localRangeEncoderCache: Map<
192
+ string,
193
+ { encoder: EncoderWrapper; version: number; lastUsed: number }
194
+ > = new Map();
195
+ private localRangeEncoderCacheMax = 2;
196
+
188
197
  ingoingSyncProcesses: Map<
189
198
  string,
190
199
  {
@@ -212,14 +221,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
212
221
  >;
213
222
 
214
223
  constructor(
215
- readonly properties: {
216
- rpc: RPC<TransportMessage, TransportMessage>;
217
- rangeIndex: Index<ReplicationRangeIndexable<D>, any>;
218
- entryIndex: Index<EntryReplicated<D>, any>;
219
- log: Log<any>;
220
- coordinateToHash: Cache<string>;
221
- numbers: Numbers<D>;
222
- },
224
+ readonly properties: SynchronizerComponents<D>,
223
225
  ) {
224
226
  this.simple = new SimpleSyncronizer(properties);
225
227
  this.outgoingSyncProcesses = new Map();
@@ -227,6 +229,91 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
227
229
  this.startedOrCompletedSynchronizations = new Cache({ max: 1e4 });
228
230
  }
229
231
 
232
+ private clearLocalRangeEncoderCache() {
233
+ for (const [, cached] of this.localRangeEncoderCache) {
234
+ cached.encoder.free();
235
+ }
236
+ this.localRangeEncoderCache.clear();
237
+ }
238
+
239
+ private invalidateLocalRangeEncoderCache() {
240
+ this.localRangeEncoderCacheVersion += 1;
241
+ this.clearLocalRangeEncoderCache();
242
+ }
243
+
244
+ private localRangeEncoderCacheKey(ranges: {
245
+ start1: NumberOrBigint;
246
+ end1: NumberOrBigint;
247
+ start2: NumberOrBigint;
248
+ end2: NumberOrBigint;
249
+ }) {
250
+ return `${String(ranges.start1)}:${String(ranges.end1)}:${String(
251
+ ranges.start2,
252
+ )}:${String(ranges.end2)}`;
253
+ }
254
+
255
+ private decoderFromCachedEncoder(encoder: EncoderWrapper): DecoderWrapper {
256
+ const clone = encoder.clone();
257
+ const decoder = clone.to_decoder();
258
+ clone.free();
259
+ return decoder;
260
+ }
261
+
262
+ private async getLocalDecoderForRange(ranges: {
263
+ start1: NumberOrBigint;
264
+ end1: NumberOrBigint;
265
+ start2: NumberOrBigint;
266
+ end2: NumberOrBigint;
267
+ }): Promise<DecoderWrapper | false> {
268
+ const key = this.localRangeEncoderCacheKey(ranges);
269
+ const cached = this.localRangeEncoderCache.get(key);
270
+ if (cached && cached.version === this.localRangeEncoderCacheVersion) {
271
+ cached.lastUsed = Date.now();
272
+ return this.decoderFromCachedEncoder(cached.encoder);
273
+ }
274
+
275
+ const encoder = (await buildEncoderOrDecoderFromRange(
276
+ ranges,
277
+ this.properties.entryIndex,
278
+ "encoder",
279
+ )) as EncoderWrapper | false;
280
+ if (!encoder) {
281
+ return false;
282
+ }
283
+
284
+ const now = Date.now();
285
+ const existing = this.localRangeEncoderCache.get(key);
286
+ if (existing) {
287
+ existing.encoder.free();
288
+ }
289
+ this.localRangeEncoderCache.set(key, {
290
+ encoder,
291
+ version: this.localRangeEncoderCacheVersion,
292
+ lastUsed: now,
293
+ });
294
+
295
+ while (this.localRangeEncoderCache.size > this.localRangeEncoderCacheMax) {
296
+ let oldestKey: string | undefined;
297
+ let oldestUsed = Number.POSITIVE_INFINITY;
298
+ for (const [candidateKey, value] of this.localRangeEncoderCache) {
299
+ if (value.lastUsed < oldestUsed) {
300
+ oldestUsed = value.lastUsed;
301
+ oldestKey = candidateKey;
302
+ }
303
+ }
304
+ if (!oldestKey) {
305
+ break;
306
+ }
307
+ const victim = this.localRangeEncoderCache.get(oldestKey);
308
+ if (victim) {
309
+ victim.encoder.free();
310
+ }
311
+ this.localRangeEncoderCache.delete(oldestKey);
312
+ }
313
+
314
+ return this.decoderFromCachedEncoder(encoder);
315
+ }
316
+
230
317
  async onMaybeMissingEntries(properties: {
231
318
  entries: Map<string, EntryReplicated<D>>;
232
319
  targets: string[];
@@ -238,7 +325,6 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
238
325
  // such as those assigned to range boundaries.
239
326
 
240
327
  let entriesToSyncNaively: Map<string, EntryReplicated<D>> = new Map();
241
- let allCoordinatesToSyncWithIblt: bigint[] = [];
242
328
  let minSyncIbltSize = 333; // TODO: make configurable
243
329
  let maxSyncWithSimpleMethod = 1e3;
244
330
 
@@ -251,15 +337,59 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
251
337
  return;
252
338
  }
253
339
 
254
- await ribltReady;
255
-
256
- // Mixed strategy for larger batches
340
+ const nonBoundaryEntries: EntryReplicated<D>[] = [];
257
341
  for (const entry of properties.entries.values()) {
258
342
  if (entry.assignedToRangeBoundary) {
259
343
  entriesToSyncNaively.set(entry.hash, entry);
260
344
  } else {
261
- allCoordinatesToSyncWithIblt.push(coerceBigInt(entry.hashNumber));
345
+ nonBoundaryEntries.push(entry);
346
+ }
347
+ }
348
+
349
+ const priorityFn = this.properties.sync?.priority;
350
+ const maxSimpleEntries = this.properties.sync?.maxSimpleEntries;
351
+ const maxAdditionalNaive =
352
+ priorityFn &&
353
+ typeof maxSimpleEntries === "number" &&
354
+ Number.isFinite(maxSimpleEntries) &&
355
+ maxSimpleEntries > 0
356
+ ? Math.max(
357
+ 0,
358
+ Math.min(
359
+ Math.floor(maxSimpleEntries),
360
+ maxSyncWithSimpleMethod - entriesToSyncNaively.size,
361
+ ),
362
+ )
363
+ : 0;
364
+
365
+ if (priorityFn && maxAdditionalNaive > 0 && nonBoundaryEntries.length > 0) {
366
+ let index = 0;
367
+ const scored: {
368
+ entry: EntryReplicated<D>;
369
+ index: number;
370
+ priority: number;
371
+ }[] = [];
372
+ for (const entry of nonBoundaryEntries) {
373
+ const priorityValue = priorityFn(entry);
374
+ scored.push({
375
+ entry,
376
+ index,
377
+ priority: Number.isFinite(priorityValue) ? priorityValue : 0,
378
+ });
379
+ index += 1;
380
+ }
381
+ scored.sort((a, b) => b.priority - a.priority || a.index - b.index);
382
+ for (const { entry } of scored.slice(0, maxAdditionalNaive)) {
383
+ entriesToSyncNaively.set(entry.hash, entry);
384
+ }
385
+ }
386
+
387
+ let allCoordinatesToSyncWithIblt: bigint[] = [];
388
+ for (const entry of nonBoundaryEntries) {
389
+ if (entriesToSyncNaively.has(entry.hash)) {
390
+ continue;
262
391
  }
392
+ allCoordinatesToSyncWithIblt.push(coerceBigInt(entry.hashNumber));
263
393
  }
264
394
 
265
395
  if (entriesToSyncNaively.size > 0) {
@@ -275,31 +405,44 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
275
405
  entriesToSyncNaively.size > maxSyncWithSimpleMethod
276
406
  ) {
277
407
  // Fallback: if nothing left for IBLT (or simple set is too large), include all in IBLT
278
- allCoordinatesToSyncWithIblt = Array.from(
279
- properties.entries.values(),
280
- ).map((x) => coerceBigInt(x.hashNumber));
408
+ allCoordinatesToSyncWithIblt = [];
409
+ for (const entry of properties.entries.values()) {
410
+ allCoordinatesToSyncWithIblt.push(coerceBigInt(entry.hashNumber));
411
+ }
281
412
  }
282
413
 
283
414
  if (allCoordinatesToSyncWithIblt.length === 0) {
284
415
  return;
285
416
  }
286
417
 
287
- const sortedEntries = allCoordinatesToSyncWithIblt.sort((a, b) => {
288
- if (a > b) {
289
- return 1;
290
- } else if (a < b) {
291
- return -1;
292
- } else {
293
- return 0;
418
+ await ribltReady;
419
+
420
+ let sortedEntries: bigint[] | BigUint64Array;
421
+ if (typeof BigUint64Array !== "undefined") {
422
+ const typed = new BigUint64Array(allCoordinatesToSyncWithIblt.length);
423
+ for (let i = 0; i < allCoordinatesToSyncWithIblt.length; i++) {
424
+ typed[i] = allCoordinatesToSyncWithIblt[i];
294
425
  }
295
- });
426
+ typed.sort();
427
+ sortedEntries = typed;
428
+ } else {
429
+ sortedEntries = allCoordinatesToSyncWithIblt.sort((a, b) => {
430
+ if (a > b) {
431
+ return 1;
432
+ } else if (a < b) {
433
+ return -1;
434
+ } else {
435
+ return 0;
436
+ }
437
+ });
438
+ }
296
439
 
297
440
  // assume sorted, and find the largest gap
298
441
  let largestGap = 0n;
299
442
  let largestGapIndex = 0;
300
- for (let i = 0; i < sortedEntries.length - 1; i++) {
443
+ for (let i = 0; i < sortedEntries.length; i++) {
301
444
  const current = sortedEntries[i];
302
- const next = sortedEntries[i + 1];
445
+ const next = sortedEntries[(i + 1) % sortedEntries.length];
303
446
  const gap =
304
447
  next >= current
305
448
  ? next - current
@@ -329,8 +472,12 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
329
472
 
330
473
  const startSync = new StartSync({ from: start, to: end, symbols: [] });
331
474
  const encoder = new EncoderWrapper();
332
- for (const entry of sortedEntries) {
333
- encoder.add_symbol(coerceBigInt(entry));
475
+ if (typeof BigUint64Array !== "undefined" && sortedEntries instanceof BigUint64Array) {
476
+ encoder.add_symbols(sortedEntries);
477
+ } else {
478
+ for (const entry of sortedEntries) {
479
+ encoder.add_symbol(coerceBigInt(entry));
480
+ }
334
481
  }
335
482
 
336
483
  let initialSymbols = Math.round(
@@ -406,16 +553,12 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
406
553
  this.startedOrCompletedSynchronizations.add(syncId);
407
554
 
408
555
  const wrapped = message.end < message.start;
409
- const decoder = await buildEncoderOrDecoderFromRange(
410
- {
411
- start1: message.start,
412
- end1: wrapped ? this.properties.numbers.maxValue : message.end,
413
- start2: 0n,
414
- end2: wrapped ? message.end : 0n,
415
- },
416
- this.properties.entryIndex,
417
- "decoder",
418
- );
556
+ const decoder = await this.getLocalDecoderForRange({
557
+ start1: message.start,
558
+ end1: wrapped ? this.properties.numbers.maxValue : message.end,
559
+ start2: 0n,
560
+ end2: wrapped ? message.end : 0n,
561
+ });
419
562
 
420
563
  if (!decoder) {
421
564
  await this.simple.rpc.send(
@@ -620,10 +763,12 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
620
763
  }
621
764
 
622
765
  onEntryAdded(entry: Entry<any>): void {
766
+ this.invalidateLocalRangeEncoderCache();
623
767
  return this.simple.onEntryAdded(entry);
624
768
  }
625
769
 
626
770
  onEntryRemoved(hash: string) {
771
+ this.invalidateLocalRangeEncoderCache();
627
772
  return this.simple.onEntryRemoved(hash);
628
773
  }
629
774
 
@@ -642,6 +787,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
642
787
  for (const [, obj] of this.outgoingSyncProcesses) {
643
788
  obj.free();
644
789
  }
790
+ this.clearLocalRangeEncoderCache();
645
791
  return this.simple.close();
646
792
  }
647
793
 
@@ -16,7 +16,7 @@ import {
16
16
  } from "../exchange-heads.js";
17
17
  import { TransportMessage } from "../message.js";
18
18
  import type { EntryReplicated } from "../ranges.js";
19
- import type { SyncableKey, Syncronizer } from "./index.js";
19
+ import type { SyncableKey, SyncOptions, Syncronizer } from "./index.js";
20
20
 
21
21
  @variant([0, 1])
22
22
  export class RequestMaybeSync extends TransportMessage {
@@ -109,6 +109,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
109
109
  log: Log<any>;
110
110
  entryIndex: Index<EntryReplicated<R>, any>;
111
111
  coordinateToHash: Cache<string>;
112
+ private syncOptions?: SyncOptions<R>;
112
113
 
113
114
  // Syncing and dedeplucation work
114
115
  syncMoreInterval?: ReturnType<typeof setTimeout>;
@@ -120,6 +121,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
120
121
  entryIndex: Index<EntryReplicated<R>, any>;
121
122
  log: Log<any>;
122
123
  coordinateToHash: Cache<string>;
124
+ sync?: SyncOptions<R>;
123
125
  }) {
124
126
  this.syncInFlightQueue = new Map();
125
127
  this.syncInFlightQueueInverted = new Map();
@@ -128,14 +130,35 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
128
130
  this.log = properties.log;
129
131
  this.entryIndex = properties.entryIndex;
130
132
  this.coordinateToHash = properties.coordinateToHash;
133
+ this.syncOptions = properties.sync;
131
134
  }
132
135
 
133
136
  onMaybeMissingEntries(properties: {
134
137
  entries: Map<string, EntryReplicated<R>>;
135
138
  targets: string[];
136
139
  }): Promise<void> {
140
+ let hashes: string[];
141
+ const priorityFn = this.syncOptions?.priority;
142
+ if (priorityFn) {
143
+ let index = 0;
144
+ const scored: { hash: string; index: number; priority: number }[] = [];
145
+ for (const [hash, entry] of properties.entries) {
146
+ const priorityValue = priorityFn(entry);
147
+ scored.push({
148
+ hash,
149
+ index,
150
+ priority: Number.isFinite(priorityValue) ? priorityValue : 0,
151
+ });
152
+ index += 1;
153
+ }
154
+ scored.sort((a, b) => b.priority - a.priority || a.index - b.index);
155
+ hashes = scored.map((x) => x.hash);
156
+ } else {
157
+ hashes = [...properties.entries.keys()];
158
+ }
159
+
137
160
  return this.rpc.send(
138
- new RequestMaybeSync({ hashes: [...properties.entries.keys()] }),
161
+ new RequestMaybeSync({ hashes }),
139
162
  {
140
163
  priority: 1,
141
164
  mode: new SilentDelivery({ to: properties.targets, redundancy: 1 }),