@peerbit/shared-log 12.3.5-3f16953 → 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/dist/benchmark/sync-batch-sweep.d.ts +2 -0
- package/dist/benchmark/sync-batch-sweep.d.ts.map +1 -0
- package/dist/benchmark/sync-batch-sweep.js +305 -0
- package/dist/benchmark/sync-batch-sweep.js.map +1 -0
- package/dist/src/index.d.ts +14 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +445 -67
- package/dist/src/index.js.map +1 -1
- package/dist/src/ranges.d.ts +3 -1
- package/dist/src/ranges.d.ts.map +1 -1
- package/dist/src/ranges.js +7 -2
- package/dist/src/ranges.js.map +1 -1
- package/dist/src/sync/index.d.ts +45 -1
- package/dist/src/sync/index.d.ts.map +1 -1
- package/dist/src/sync/rateless-iblt.d.ts +13 -2
- package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
- package/dist/src/sync/rateless-iblt.js +150 -0
- package/dist/src/sync/rateless-iblt.js.map +1 -1
- package/dist/src/sync/simple.d.ts +24 -3
- package/dist/src/sync/simple.d.ts.map +1 -1
- package/dist/src/sync/simple.js +330 -32
- package/dist/src/sync/simple.js.map +1 -1
- package/package.json +18 -18
- package/src/index.ts +581 -136
- package/src/ranges.ts +7 -1
- package/src/sync/index.ts +53 -1
- package/src/sync/rateless-iblt.ts +177 -1
- package/src/sync/simple.ts +427 -41
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 =
|
|
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
|
|