@peerbit/shared-log 12.3.5 → 13.0.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.
- 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/fanout-envelope.d.ts +18 -0
- package/dist/src/fanout-envelope.d.ts.map +1 -0
- package/dist/src/fanout-envelope.js +85 -0
- package/dist/src/fanout-envelope.js.map +1 -0
- package/dist/src/index.d.ts +55 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1595 -339
- package/dist/src/index.js.map +1 -1
- package/dist/src/pid.d.ts.map +1 -1
- package/dist/src/pid.js +21 -5
- package/dist/src/pid.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 +14 -5
- 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 +194 -3
- 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 +16 -16
- package/src/fanout-envelope.ts +27 -0
- package/src/index.ts +2162 -691
- package/src/pid.ts +22 -4
- package/src/ranges.ts +14 -4
- package/src/sync/index.ts +53 -1
- package/src/sync/rateless-iblt.ts +237 -4
- package/src/sync/simple.ts +427 -41
package/src/pid.ts
CHANGED
|
@@ -63,16 +63,34 @@ export class PIDReplicationController {
|
|
|
63
63
|
errorCoverageUnmodified;
|
|
64
64
|
|
|
65
65
|
const errorFromEven = 1 / peerCount - currentFactor;
|
|
66
|
+
// When the network is under-covered (`totalFactor < 1`) balancing "down" (negative
|
|
67
|
+
// error) can further reduce coverage and force constrained peers (memory/CPU limited)
|
|
68
|
+
// to take boundary assignments that exceed their budgets.
|
|
69
|
+
//
|
|
70
|
+
// Use a soft clamp: only suppress negative balance strongly when the coverage deficit
|
|
71
|
+
// is material. This avoids oscillations around `totalFactor ~= 1`.
|
|
72
|
+
const coverageDeficit = Math.max(0, errorCoverageUnmodified); // ~= max(0, 1 - totalFactor)
|
|
73
|
+
const negativeBalanceScale =
|
|
74
|
+
coverageDeficit <= 0
|
|
75
|
+
? 1
|
|
76
|
+
: 1 - Math.min(1, coverageDeficit / 0.1); // full clamp at 10% deficit
|
|
77
|
+
const errorFromEvenForBalance =
|
|
78
|
+
errorFromEven >= 0 ? errorFromEven : errorFromEven * negativeBalanceScale;
|
|
66
79
|
|
|
67
80
|
const balanceErrorScaler = this.maxMemoryLimit
|
|
68
81
|
? Math.abs(errorMemory)
|
|
69
82
|
: 1 - Math.abs(errorCoverage);
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
// Balance should be symmetric (allow negative error) so a peer can *reduce*
|
|
85
|
+
// participation when peerCount increases. Otherwise early joiners can get
|
|
86
|
+
// "stuck" over-replicating even after new peers join (no memory/CPU limits).
|
|
87
|
+
const errorBalance = this.maxMemoryLimit
|
|
88
|
+
? // Only balance when we have spare memory headroom. When memory is
|
|
89
|
+
// constrained (`errorMemory < 0`) the memory term will dominate anyway.
|
|
90
|
+
errorMemory > 0
|
|
91
|
+
? balanceErrorScaler * errorFromEvenForBalance
|
|
74
92
|
: 0
|
|
75
|
-
:
|
|
93
|
+
: balanceErrorScaler * errorFromEvenForBalance;
|
|
76
94
|
|
|
77
95
|
const errorCPU = peerCount > 1 ? -1 * (cpuUsage || 0) : 0;
|
|
78
96
|
|
package/src/ranges.ts
CHANGED
|
@@ -2565,13 +2565,17 @@ export const debounceAggregationChanges = <
|
|
|
2565
2565
|
let aggregated: Map<string, ReplicationChange<T>> = new Map();
|
|
2566
2566
|
return {
|
|
2567
2567
|
add: (change: ReplicationChange<T>) => {
|
|
2568
|
-
|
|
2568
|
+
// Keep different change types for the same segment id. In particular, range
|
|
2569
|
+
// updates produce a `replaced` + `added` pair; collapsing by id would drop the
|
|
2570
|
+
// "removed" portion and prevent correct rebalancing/pruning.
|
|
2571
|
+
const key = `${change.type}:${change.range.idString}`;
|
|
2572
|
+
const prev = aggregated.get(key);
|
|
2569
2573
|
if (prev) {
|
|
2570
2574
|
if (prev.range.timestamp < change.range.timestamp) {
|
|
2571
|
-
aggregated.set(
|
|
2575
|
+
aggregated.set(key, change);
|
|
2572
2576
|
}
|
|
2573
2577
|
} else {
|
|
2574
|
-
aggregated.set(
|
|
2578
|
+
aggregated.set(key, change);
|
|
2575
2579
|
}
|
|
2576
2580
|
},
|
|
2577
2581
|
delete: (key: string) => {
|
|
@@ -2703,8 +2707,14 @@ export const toRebalance = <R extends "u32" | "u64">(
|
|
|
2703
2707
|
| ReplicationChanges<ReplicationRangeIndexable<R>>[],
|
|
2704
2708
|
index: Index<EntryReplicated<R>>,
|
|
2705
2709
|
rebalanceHistory: Cache<string>,
|
|
2710
|
+
options?: { forceFresh?: boolean },
|
|
2706
2711
|
): AsyncIterable<EntryReplicated<R>> => {
|
|
2707
|
-
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);
|
|
2708
2718
|
return {
|
|
2709
2719
|
[Symbol.asyncIterator]: async function* () {
|
|
2710
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;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { field, variant, vec } from "@dao-xyz/borsh";
|
|
2
2
|
import { Cache } from "@peerbit/cache";
|
|
3
3
|
import { type PublicSignKey, randomBytes, toBase64 } from "@peerbit/crypto";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
And,
|
|
6
|
+
type Index,
|
|
7
|
+
IntegerCompare,
|
|
8
|
+
Or,
|
|
9
|
+
type Query,
|
|
10
|
+
} from "@peerbit/indexer-interface";
|
|
5
11
|
import type { Entry, Log } from "@peerbit/log";
|
|
6
12
|
import { logger as loggerFn } from "@peerbit/logger";
|
|
7
13
|
import {
|
|
@@ -13,8 +19,11 @@ import type { RPC, RequestContext } from "@peerbit/rpc";
|
|
|
13
19
|
import { SilentDelivery } from "@peerbit/stream-interface";
|
|
14
20
|
import { type EntryWithRefs } from "../exchange-heads.js";
|
|
15
21
|
import { TransportMessage } from "../message.js";
|
|
16
|
-
import { type EntryReplicated
|
|
22
|
+
import { type EntryReplicated } from "../ranges.js";
|
|
17
23
|
import type {
|
|
24
|
+
RepairSession,
|
|
25
|
+
RepairSessionMode,
|
|
26
|
+
RepairSessionResult,
|
|
18
27
|
SyncableKey,
|
|
19
28
|
SynchronizerComponents,
|
|
20
29
|
Syncronizer,
|
|
@@ -49,6 +58,10 @@ const getSyncIdString = (message: { syncId: Uint8Array }) => {
|
|
|
49
58
|
return toBase64(message.syncId);
|
|
50
59
|
};
|
|
51
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
|
+
|
|
52
65
|
@variant([3, 0])
|
|
53
66
|
export class StartSync extends TransportMessage {
|
|
54
67
|
@field({ type: Uint8Array })
|
|
@@ -131,6 +144,50 @@ export interface SSymbol {
|
|
|
131
144
|
symbol: bigint;
|
|
132
145
|
}
|
|
133
146
|
|
|
147
|
+
const matchEntriesByHashNumberInRangeQuery = (range: {
|
|
148
|
+
start1: number | bigint;
|
|
149
|
+
end1: number | bigint;
|
|
150
|
+
start2: number | bigint;
|
|
151
|
+
end2: number | bigint;
|
|
152
|
+
}): Query => {
|
|
153
|
+
const c1 = new And([
|
|
154
|
+
new IntegerCompare({
|
|
155
|
+
key: "hashNumber",
|
|
156
|
+
compare: "gte",
|
|
157
|
+
value: range.start1,
|
|
158
|
+
}),
|
|
159
|
+
new IntegerCompare({
|
|
160
|
+
key: "hashNumber",
|
|
161
|
+
compare: "lt",
|
|
162
|
+
value: range.end1,
|
|
163
|
+
}),
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
// if range2 has length 0 or range 2 is equal to range 1 only make one query
|
|
167
|
+
if (
|
|
168
|
+
range.start2 === range.end2 ||
|
|
169
|
+
(range.start1 === range.start2 && range.end1 === range.end2)
|
|
170
|
+
) {
|
|
171
|
+
return c1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return new Or([
|
|
175
|
+
c1,
|
|
176
|
+
new And([
|
|
177
|
+
new IntegerCompare({
|
|
178
|
+
key: "hashNumber",
|
|
179
|
+
compare: "gte",
|
|
180
|
+
value: range.start2,
|
|
181
|
+
}),
|
|
182
|
+
new IntegerCompare({
|
|
183
|
+
key: "hashNumber",
|
|
184
|
+
compare: "lt",
|
|
185
|
+
value: range.end2,
|
|
186
|
+
}),
|
|
187
|
+
]),
|
|
188
|
+
]);
|
|
189
|
+
};
|
|
190
|
+
|
|
134
191
|
const buildEncoderOrDecoderFromRange = async <
|
|
135
192
|
T extends "encoder" | "decoder",
|
|
136
193
|
E = T extends "encoder" ? EncoderWrapper : DecoderWrapper,
|
|
@@ -152,7 +209,8 @@ const buildEncoderOrDecoderFromRange = async <
|
|
|
152
209
|
const entries = await entryIndex
|
|
153
210
|
.iterate(
|
|
154
211
|
{
|
|
155
|
-
|
|
212
|
+
// Range sync for IBLT is done in hashNumber space.
|
|
213
|
+
query: matchEntriesByHashNumberInRangeQuery({
|
|
156
214
|
end1: ranges.end1,
|
|
157
215
|
start1: ranges.start1,
|
|
158
216
|
end2: ranges.end2,
|
|
@@ -182,6 +240,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
|
|
|
182
240
|
implements Syncronizer<D>
|
|
183
241
|
{
|
|
184
242
|
simple: SimpleSyncronizer<D>;
|
|
243
|
+
private repairSessionCounter: number;
|
|
185
244
|
|
|
186
245
|
startedOrCompletedSynchronizations: Cache<string>;
|
|
187
246
|
private localRangeEncoderCacheVersion = 0;
|
|
@@ -219,11 +278,179 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
|
|
|
219
278
|
|
|
220
279
|
constructor(readonly properties: SynchronizerComponents<D>) {
|
|
221
280
|
this.simple = new SimpleSyncronizer(properties);
|
|
281
|
+
this.repairSessionCounter = 0;
|
|
222
282
|
this.outgoingSyncProcesses = new Map();
|
|
223
283
|
this.ingoingSyncProcesses = new Map();
|
|
224
284
|
this.startedOrCompletedSynchronizations = new Cache({ max: 1e4 });
|
|
225
285
|
}
|
|
226
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
|
+
|
|
227
454
|
private clearLocalRangeEncoderCache() {
|
|
228
455
|
for (const [, cached] of this.localRangeEncoderCache) {
|
|
229
456
|
cached.encoder.free();
|
|
@@ -313,6 +540,8 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
|
|
|
313
540
|
entries: Map<string, EntryReplicated<D>>;
|
|
314
541
|
targets: string[];
|
|
315
542
|
}): Promise<void> {
|
|
543
|
+
// NOTE: this method is best-effort dispatch, not a per-hash convergence API.
|
|
544
|
+
// It may require follow-up repair rounds under churn/loss to fully close all gaps.
|
|
316
545
|
// Strategy:
|
|
317
546
|
// - For small sets, prefer the simple synchronizer to reduce complexity and avoid
|
|
318
547
|
// IBLT overhead on tiny batches.
|
|
@@ -478,9 +707,13 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
|
|
|
478
707
|
}
|
|
479
708
|
}
|
|
480
709
|
|
|
710
|
+
// For smaller sets, the original `sqrt(n)` heuristic can occasionally under-provision
|
|
711
|
+
// low-degree symbols early, causing an unnecessary `MoreSymbols` round-trip. Use a
|
|
712
|
+
// small floor to make small-delta syncs more reliable without affecting large-n behavior.
|
|
481
713
|
let initialSymbols = Math.round(
|
|
482
714
|
Math.sqrt(allCoordinatesToSyncWithIblt.length),
|
|
483
715
|
); // TODO choose better
|
|
716
|
+
initialSymbols = Math.max(64, initialSymbols);
|
|
484
717
|
for (let i = 0; i < initialSymbols; i++) {
|
|
485
718
|
startSync.symbols.push(
|
|
486
719
|
new SymbolSerialized(encoder.produce_next_coded_symbol()),
|
|
@@ -770,7 +1003,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
|
|
|
770
1003
|
return this.simple.onEntryRemoved(hash);
|
|
771
1004
|
}
|
|
772
1005
|
|
|
773
|
-
onPeerDisconnected(key: PublicSignKey) {
|
|
1006
|
+
onPeerDisconnected(key: PublicSignKey | string) {
|
|
774
1007
|
return this.simple.onPeerDisconnected(key);
|
|
775
1008
|
}
|
|
776
1009
|
|