@peerbit/shared-log 12.3.5-1929680 → 12.3.5-42e98ce

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
@@ -2707,8 +2707,14 @@ export const toRebalance = <R extends "u32" | "u64">(
2707
2707
  | ReplicationChanges<ReplicationRangeIndexable<R>>[],
2708
2708
  index: Index<EntryReplicated<R>>,
2709
2709
  rebalanceHistory: Cache<string>,
2710
+ options?: { forceFresh?: boolean },
2710
2711
  ): AsyncIterable<EntryReplicated<R>> => {
2711
- const change = mergeReplicationChanges(changeOrChanges, rebalanceHistory);
2712
+ const change = options?.forceFresh
2713
+ ? (Array.isArray(changeOrChanges[0])
2714
+ ? (changeOrChanges as ReplicationChanges<ReplicationRangeIndexable<R>>[])
2715
+ .flat()
2716
+ : (changeOrChanges as ReplicationChanges<ReplicationRangeIndexable<R>>))
2717
+ : mergeReplicationChanges(changeOrChanges, rebalanceHistory);
2712
2718
  return {
2713
2719
  [Symbol.asyncIterator]: async function* () {
2714
2720
  const iterator = index.iterate({
package/src/sync/index.ts CHANGED
@@ -24,6 +24,30 @@ export type SyncOptions<R extends "u32" | "u64"> = {
24
24
  * high-priority entries using the simple synchronizer.
25
25
  */
26
26
  maxSimpleEntries?: number;
27
+
28
+ /**
29
+ * Maximum number of hash strings in one simple sync message.
30
+ */
31
+ maxSimpleHashesPerMessage?: number;
32
+
33
+ /**
34
+ * Maximum number of coordinates in one simple sync coordinate message.
35
+ */
36
+ maxSimpleCoordinatesPerMessage?: number;
37
+
38
+ /**
39
+ * Maximum number of hashes tracked per convergent repair session target.
40
+ * Large sessions still dispatch all entries, but only this many are tracked
41
+ * for deterministic completion metadata.
42
+ */
43
+ maxConvergentTrackedHashes?: number;
44
+
45
+ /**
46
+ * Maximum number of candidate entries buffered per target before the
47
+ * background repair sweep dispatches a maybe-sync batch.
48
+ * Larger values reduce orchestration overhead but increase per-target memory.
49
+ */
50
+ repairSweepTargetBufferSize?: number;
27
51
  };
28
52
 
29
53
  export type SynchronizerComponents<R extends "u32" | "u64"> = {
@@ -41,7 +65,35 @@ export type SynchronizerConstructor<R extends "u32" | "u64"> = new (
41
65
 
42
66
  export type SyncableKey = string | bigint; // hash or coordinate
43
67
 
68
+ export type RepairSessionMode = "best-effort" | "convergent";
69
+
70
+ export type RepairSessionResult = {
71
+ target: string;
72
+ requested: number;
73
+ resolved: number;
74
+ unresolved: string[];
75
+ attempts: number;
76
+ durationMs: number;
77
+ completed: boolean;
78
+ requestedTotal?: number;
79
+ truncated?: boolean;
80
+ };
81
+
82
+ export type RepairSession = {
83
+ id: string;
84
+ done: Promise<RepairSessionResult[]>;
85
+ cancel: () => void;
86
+ };
87
+
44
88
  export interface Syncronizer<R extends "u32" | "u64"> {
89
+ startRepairSession(properties: {
90
+ entries: Map<string, EntryReplicated<R>>;
91
+ targets: string[];
92
+ mode?: RepairSessionMode;
93
+ timeoutMs?: number;
94
+ retryIntervalsMs?: number[];
95
+ }): RepairSession;
96
+
45
97
  onMaybeMissingEntries(properties: {
46
98
  entries: Map<string, EntryReplicated<R>>;
47
99
  targets: string[];
@@ -59,7 +111,7 @@ export interface Syncronizer<R extends "u32" | "u64"> {
59
111
 
60
112
  onEntryAdded(entry: Entry<any>): void;
61
113
  onEntryRemoved(hash: string): void;
62
- onPeerDisconnected(key: PublicSignKey): void;
114
+ onPeerDisconnected(key: PublicSignKey | string): void;
63
115
 
64
116
  open(): Promise<void> | void;
65
117
  close(): Promise<void> | void;
@@ -21,6 +21,9 @@ import { type EntryWithRefs } from "../exchange-heads.js";
21
21
  import { TransportMessage } from "../message.js";
22
22
  import { type EntryReplicated } from "../ranges.js";
23
23
  import type {
24
+ RepairSession,
25
+ RepairSessionMode,
26
+ RepairSessionResult,
24
27
  SyncableKey,
25
28
  SynchronizerComponents,
26
29
  Syncronizer,
@@ -55,6 +58,10 @@ const getSyncIdString = (message: { syncId: Uint8Array }) => {
55
58
  return toBase64(message.syncId);
56
59
  };
57
60
 
61
+ const DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS = 30_000;
62
+ const DEFAULT_CONVERGENT_RETRY_INTERVALS_MS = [0, 1_000, 3_000, 7_000];
63
+ const DEFAULT_MAX_CONVERGENT_TRACKED_HASHES = 4_096;
64
+
58
65
  @variant([3, 0])
59
66
  export class StartSync extends TransportMessage {
60
67
  @field({ type: Uint8Array })
@@ -233,6 +240,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
233
240
  implements Syncronizer<D>
234
241
  {
235
242
  simple: SimpleSyncronizer<D>;
243
+ private repairSessionCounter: number;
236
244
 
237
245
  startedOrCompletedSynchronizations: Cache<string>;
238
246
  private localRangeEncoderCacheVersion = 0;
@@ -270,11 +278,179 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
270
278
 
271
279
  constructor(readonly properties: SynchronizerComponents<D>) {
272
280
  this.simple = new SimpleSyncronizer(properties);
281
+ this.repairSessionCounter = 0;
273
282
  this.outgoingSyncProcesses = new Map();
274
283
  this.ingoingSyncProcesses = new Map();
275
284
  this.startedOrCompletedSynchronizations = new Cache({ max: 1e4 });
276
285
  }
277
286
 
287
+ private get maxConvergentTrackedHashes() {
288
+ const value = this.properties.sync?.maxConvergentTrackedHashes;
289
+ return value && Number.isFinite(value) && value > 0
290
+ ? Math.floor(value)
291
+ : DEFAULT_MAX_CONVERGENT_TRACKED_HASHES;
292
+ }
293
+
294
+ private normalizeRetryIntervals(retryIntervalsMs?: number[]): number[] {
295
+ if (!retryIntervalsMs || retryIntervalsMs.length === 0) {
296
+ return [...DEFAULT_CONVERGENT_RETRY_INTERVALS_MS];
297
+ }
298
+
299
+ return [...retryIntervalsMs]
300
+ .map((x) => Math.max(0, Math.floor(x)))
301
+ .filter((x, i, arr) => arr.indexOf(x) === i)
302
+ .sort((a, b) => a - b);
303
+ }
304
+
305
+ private getPrioritizedEntries(entries: Map<string, EntryReplicated<D>>) {
306
+ const priorityFn = this.properties.sync?.priority;
307
+ if (!priorityFn) {
308
+ return [...entries.values()];
309
+ }
310
+
311
+ let index = 0;
312
+ const scored: { entry: EntryReplicated<D>; index: number; priority: number }[] =
313
+ [];
314
+ for (const entry of entries.values()) {
315
+ const priorityValue = priorityFn(entry);
316
+ scored.push({
317
+ entry,
318
+ index,
319
+ priority: Number.isFinite(priorityValue) ? priorityValue : 0,
320
+ });
321
+ index += 1;
322
+ }
323
+ scored.sort((a, b) => b.priority - a.priority || a.index - b.index);
324
+ return scored.map((x) => x.entry);
325
+ }
326
+
327
+ startRepairSession(properties: {
328
+ entries: Map<string, EntryReplicated<D>>;
329
+ targets: string[];
330
+ mode?: RepairSessionMode;
331
+ timeoutMs?: number;
332
+ retryIntervalsMs?: number[];
333
+ }): RepairSession {
334
+ const mode = properties.mode ?? "best-effort";
335
+ const targets = [...new Set(properties.targets)];
336
+ const timeoutMs = Math.max(
337
+ 1,
338
+ Math.floor(properties.timeoutMs ?? DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS),
339
+ );
340
+ const retryIntervalsMs = this.normalizeRetryIntervals(properties.retryIntervalsMs);
341
+ const trackedLimit = this.maxConvergentTrackedHashes;
342
+ const requestedHashes = [...properties.entries.keys()];
343
+ const requestedHashesTracked = requestedHashes.slice(0, trackedLimit);
344
+ const truncated = requestedHashesTracked.length < requestedHashes.length;
345
+
346
+ if (mode === "convergent") {
347
+ if (properties.entries.size <= trackedLimit) {
348
+ return this.simple.startRepairSession({
349
+ ...properties,
350
+ mode: "convergent",
351
+ timeoutMs,
352
+ retryIntervalsMs,
353
+ });
354
+ }
355
+
356
+ const id = `rateless-repair-${++this.repairSessionCounter}`;
357
+ const startedAt = Date.now();
358
+ const prioritized = this.getPrioritizedEntries(properties.entries);
359
+ const trackedEntries = new Map<string, EntryReplicated<D>>();
360
+ for (const entry of prioritized.slice(0, trackedLimit)) {
361
+ trackedEntries.set(entry.hash, entry);
362
+ }
363
+
364
+ let cancelled = false;
365
+ const trackedSession = this.simple.startRepairSession({
366
+ entries: trackedEntries,
367
+ targets,
368
+ mode: "convergent",
369
+ timeoutMs,
370
+ retryIntervalsMs,
371
+ });
372
+
373
+ const runDispatchSchedule = async () => {
374
+ let previousDelay = 0;
375
+ for (const delayMs of retryIntervalsMs) {
376
+ if (cancelled) {
377
+ return;
378
+ }
379
+ const elapsed = Date.now() - startedAt;
380
+ if (elapsed >= timeoutMs) {
381
+ return;
382
+ }
383
+ const waitMs = Math.max(0, delayMs - previousDelay);
384
+ previousDelay = delayMs;
385
+ if (waitMs > 0) {
386
+ await new Promise<void>((resolve) => {
387
+ const timer = setTimeout(resolve, waitMs);
388
+ timer.unref?.();
389
+ });
390
+ }
391
+ if (cancelled) {
392
+ return;
393
+ }
394
+ try {
395
+ await this.onMaybeMissingEntries({
396
+ entries: properties.entries,
397
+ targets,
398
+ });
399
+ } catch {
400
+ // Best-effort schedule: tracked session timeout/result decides completion.
401
+ }
402
+ }
403
+ };
404
+
405
+ const done = (async (): Promise<RepairSessionResult[]> => {
406
+ await runDispatchSchedule();
407
+ const trackedResults = await trackedSession.done;
408
+ return trackedResults.map((result) => ({
409
+ ...result,
410
+ requestedTotal: requestedHashes.length,
411
+ truncated: true,
412
+ }));
413
+ })();
414
+
415
+ return {
416
+ id,
417
+ done,
418
+ cancel: () => {
419
+ cancelled = true;
420
+ trackedSession.cancel();
421
+ },
422
+ };
423
+ }
424
+
425
+ const id = `rateless-repair-${++this.repairSessionCounter}`;
426
+ const startedAt = Date.now();
427
+ const done = (async (): Promise<RepairSessionResult[]> => {
428
+ await this.onMaybeMissingEntries({
429
+ entries: properties.entries,
430
+ targets,
431
+ });
432
+ const durationMs = Date.now() - startedAt;
433
+ return targets.map((target) => ({
434
+ target,
435
+ requested: requestedHashesTracked.length,
436
+ resolved: 0,
437
+ unresolved: [...requestedHashesTracked],
438
+ attempts: 1,
439
+ durationMs,
440
+ completed: false,
441
+ requestedTotal: requestedHashes.length,
442
+ truncated,
443
+ }));
444
+ })();
445
+ return {
446
+ id,
447
+ done,
448
+ cancel: () => {
449
+ // no-op: best-effort dispatch does not maintain cancelable session state
450
+ },
451
+ };
452
+ }
453
+
278
454
  private clearLocalRangeEncoderCache() {
279
455
  for (const [, cached] of this.localRangeEncoderCache) {
280
456
  cached.encoder.free();
@@ -825,7 +1001,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
825
1001
  return this.simple.onEntryRemoved(hash);
826
1002
  }
827
1003
 
828
- onPeerDisconnected(key: PublicSignKey) {
1004
+ onPeerDisconnected(key: PublicSignKey | string) {
829
1005
  return this.simple.onPeerDisconnected(key);
830
1006
  }
831
1007