@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.
Files changed (37) hide show
  1. package/dist/benchmark/sync-batch-sweep.d.ts +2 -0
  2. package/dist/benchmark/sync-batch-sweep.d.ts.map +1 -0
  3. package/dist/benchmark/sync-batch-sweep.js +305 -0
  4. package/dist/benchmark/sync-batch-sweep.js.map +1 -0
  5. package/dist/src/fanout-envelope.d.ts +18 -0
  6. package/dist/src/fanout-envelope.d.ts.map +1 -0
  7. package/dist/src/fanout-envelope.js +85 -0
  8. package/dist/src/fanout-envelope.js.map +1 -0
  9. package/dist/src/index.d.ts +55 -6
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +1595 -339
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/pid.d.ts.map +1 -1
  14. package/dist/src/pid.js +21 -5
  15. package/dist/src/pid.js.map +1 -1
  16. package/dist/src/ranges.d.ts +3 -1
  17. package/dist/src/ranges.d.ts.map +1 -1
  18. package/dist/src/ranges.js +14 -5
  19. package/dist/src/ranges.js.map +1 -1
  20. package/dist/src/sync/index.d.ts +45 -1
  21. package/dist/src/sync/index.d.ts.map +1 -1
  22. package/dist/src/sync/rateless-iblt.d.ts +13 -2
  23. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  24. package/dist/src/sync/rateless-iblt.js +194 -3
  25. package/dist/src/sync/rateless-iblt.js.map +1 -1
  26. package/dist/src/sync/simple.d.ts +24 -3
  27. package/dist/src/sync/simple.d.ts.map +1 -1
  28. package/dist/src/sync/simple.js +330 -32
  29. package/dist/src/sync/simple.js.map +1 -1
  30. package/package.json +16 -16
  31. package/src/fanout-envelope.ts +27 -0
  32. package/src/index.ts +2162 -691
  33. package/src/pid.ts +22 -4
  34. package/src/ranges.ts +14 -4
  35. package/src/sync/index.ts +53 -1
  36. package/src/sync/rateless-iblt.ts +237 -4
  37. package/src/sync/simple.ts +427 -41
@@ -16,7 +16,14 @@ 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 { SyncOptions, SyncableKey, Syncronizer } from "./index.js";
19
+ import type {
20
+ RepairSession,
21
+ RepairSessionMode,
22
+ RepairSessionResult,
23
+ SyncOptions,
24
+ SyncableKey,
25
+ Syncronizer,
26
+ } from "./index.js";
20
27
 
21
28
  @variant([0, 1])
22
29
  export class RequestMaybeSync extends TransportMessage {
@@ -95,6 +102,44 @@ const getHashesFromSymbols = async (
95
102
  return results;
96
103
  };
97
104
 
105
+ const DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS = 30_000;
106
+ const DEFAULT_CONVERGENT_RETRY_INTERVALS_MS = [0, 1_000, 3_000, 7_000];
107
+ const DEFAULT_BEST_EFFORT_RETRY_INTERVALS_MS = [0];
108
+ const SESSION_POLL_INTERVAL_MS = 100;
109
+ const DEFAULT_MAX_HASHES_PER_MESSAGE = 1_024;
110
+ const DEFAULT_MAX_COORDINATES_PER_MESSAGE = 1_024;
111
+ const DEFAULT_MAX_CONVERGENT_TRACKED_HASHES = 4_096;
112
+
113
+ const createDeferred = <T>() => {
114
+ let resolve!: (value: T | PromiseLike<T>) => void;
115
+ let reject!: (reason?: unknown) => void;
116
+ const promise = new Promise<T>((res, rej) => {
117
+ resolve = res;
118
+ reject = rej;
119
+ });
120
+ return { promise, resolve, reject };
121
+ };
122
+
123
+ type RepairSessionTargetState = {
124
+ unresolved: Set<string>;
125
+ requestedCount: number;
126
+ requestedTotalCount: number;
127
+ attempts: number;
128
+ };
129
+
130
+ type RepairSessionState = {
131
+ id: string;
132
+ mode: RepairSessionMode;
133
+ startedAt: number;
134
+ timeoutMs: number;
135
+ retryIntervalsMs: number[];
136
+ targets: Map<string, RepairSessionTargetState>;
137
+ truncated: boolean;
138
+ deferred: ReturnType<typeof createDeferred<RepairSessionResult[]>>;
139
+ cancelled: boolean;
140
+ timer?: ReturnType<typeof setTimeout>;
141
+ };
142
+
98
143
  export class SimpleSyncronizer<R extends "u32" | "u64">
99
144
  implements Syncronizer<R>
100
145
  {
@@ -110,6 +155,8 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
110
155
  entryIndex: Index<EntryReplicated<R>, any>;
111
156
  coordinateToHash: Cache<string>;
112
157
  private syncOptions?: SyncOptions<R>;
158
+ private repairSessionCounter: number;
159
+ private repairSessions: Map<string, RepairSessionState>;
113
160
 
114
161
  // Syncing and dedeplucation work
115
162
  syncMoreInterval?: ReturnType<typeof setTimeout>;
@@ -131,36 +178,354 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
131
178
  this.entryIndex = properties.entryIndex;
132
179
  this.coordinateToHash = properties.coordinateToHash;
133
180
  this.syncOptions = properties.sync;
181
+ this.repairSessionCounter = 0;
182
+ this.repairSessions = new Map();
134
183
  }
135
184
 
136
- onMaybeMissingEntries(properties: {
137
- entries: Map<string, EntryReplicated<R>>;
138
- targets: string[];
139
- }): Promise<void> {
140
- let hashes: string[];
185
+ private getPrioritizedHashes(
186
+ entries: Map<string, EntryReplicated<R>>,
187
+ ): string[] {
141
188
  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,
189
+ if (!priorityFn) {
190
+ return [...entries.keys()];
191
+ }
192
+
193
+ let index = 0;
194
+ const scored: { hash: string; index: number; priority: number }[] = [];
195
+ for (const [hash, entry] of entries) {
196
+ const priorityValue = priorityFn(entry);
197
+ scored.push({
198
+ hash,
199
+ index,
200
+ priority: Number.isFinite(priorityValue) ? priorityValue : 0,
201
+ });
202
+ index += 1;
203
+ }
204
+ scored.sort((a, b) => b.priority - a.priority || a.index - b.index);
205
+ return scored.map((x) => x.hash);
206
+ }
207
+
208
+ private normalizeRetryIntervals(
209
+ mode: RepairSessionMode,
210
+ retryIntervalsMs?: number[],
211
+ ): number[] {
212
+ const defaults =
213
+ mode === "convergent"
214
+ ? DEFAULT_CONVERGENT_RETRY_INTERVALS_MS
215
+ : DEFAULT_BEST_EFFORT_RETRY_INTERVALS_MS;
216
+ if (!retryIntervalsMs || retryIntervalsMs.length === 0) {
217
+ return [...defaults];
218
+ }
219
+
220
+ return [...retryIntervalsMs]
221
+ .map((x) => Math.max(0, Math.floor(x)))
222
+ .filter((x, i, arr) => arr.indexOf(x) === i)
223
+ .sort((a, b) => a - b);
224
+ }
225
+
226
+ private get maxHashesPerMessage() {
227
+ const value = this.syncOptions?.maxSimpleHashesPerMessage;
228
+ return value && Number.isFinite(value) && value > 0
229
+ ? Math.floor(value)
230
+ : DEFAULT_MAX_HASHES_PER_MESSAGE;
231
+ }
232
+
233
+ private get maxCoordinatesPerMessage() {
234
+ const value = this.syncOptions?.maxSimpleCoordinatesPerMessage;
235
+ return value && Number.isFinite(value) && value > 0
236
+ ? Math.floor(value)
237
+ : DEFAULT_MAX_COORDINATES_PER_MESSAGE;
238
+ }
239
+
240
+ private get maxConvergentTrackedHashes() {
241
+ const value = this.syncOptions?.maxConvergentTrackedHashes;
242
+ return value && Number.isFinite(value) && value > 0
243
+ ? Math.floor(value)
244
+ : DEFAULT_MAX_CONVERGENT_TRACKED_HASHES;
245
+ }
246
+
247
+ private chunk<T>(values: T[], size: number): T[][] {
248
+ if (values.length === 0) {
249
+ return [];
250
+ }
251
+ const out: T[][] = [];
252
+ for (let i = 0; i < values.length; i += size) {
253
+ out.push(values.slice(i, i + size));
254
+ }
255
+ return out;
256
+ }
257
+
258
+ private isRepairSessionComplete(session: RepairSessionState): boolean {
259
+ for (const state of session.targets.values()) {
260
+ if (state.unresolved.size > 0) {
261
+ return false;
262
+ }
263
+ }
264
+ return true;
265
+ }
266
+
267
+ private buildRepairSessionResult(
268
+ session: RepairSessionState,
269
+ completed: boolean,
270
+ ): RepairSessionResult[] {
271
+ const durationMs = Date.now() - session.startedAt;
272
+ const out: RepairSessionResult[] = [];
273
+ for (const [target, state] of session.targets) {
274
+ const unresolved = [...state.unresolved];
275
+ out.push({
276
+ target,
277
+ requested: state.requestedCount,
278
+ resolved: state.requestedCount - unresolved.length,
279
+ unresolved,
280
+ attempts: state.attempts,
281
+ durationMs,
282
+ completed,
283
+ requestedTotal: state.requestedTotalCount,
284
+ truncated: session.truncated,
285
+ });
286
+ }
287
+ return out;
288
+ }
289
+
290
+ private finalizeRepairSession(sessionId: string, completed: boolean): void {
291
+ const session = this.repairSessions.get(sessionId);
292
+ if (!session) {
293
+ return;
294
+ }
295
+ this.repairSessions.delete(sessionId);
296
+ session.cancelled = true;
297
+ if (session.timer) {
298
+ clearTimeout(session.timer);
299
+ }
300
+ session.deferred.resolve(this.buildRepairSessionResult(session, completed));
301
+ }
302
+
303
+ private async refreshRepairSessionState(sessionId: string): Promise<void> {
304
+ const session = this.repairSessions.get(sessionId);
305
+ if (!session) {
306
+ return;
307
+ }
308
+ for (const state of session.targets.values()) {
309
+ for (const hash of [...state.unresolved]) {
310
+ if (await this.log.has(hash)) {
311
+ state.unresolved.delete(hash);
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ private markRepairSessionResolvedHashes(hashes: string[]): void {
318
+ if (hashes.length === 0 || this.repairSessions.size === 0) {
319
+ return;
320
+ }
321
+ for (const [sessionId, session] of this.repairSessions) {
322
+ for (const state of session.targets.values()) {
323
+ for (const hash of hashes) {
324
+ state.unresolved.delete(hash);
325
+ }
326
+ }
327
+ if (this.isRepairSessionComplete(session)) {
328
+ this.finalizeRepairSession(sessionId, true);
329
+ }
330
+ }
331
+ }
332
+
333
+ private async runRepairSession(sessionId: string): Promise<void> {
334
+ const session = this.repairSessions.get(sessionId);
335
+ if (!session) {
336
+ return;
337
+ }
338
+
339
+ let previousDelay = 0;
340
+ for (const delayMs of session.retryIntervalsMs) {
341
+ if (!this.repairSessions.has(sessionId) || this.closed) {
342
+ return;
343
+ }
344
+
345
+ const waitMs = Math.max(0, delayMs - previousDelay);
346
+ previousDelay = delayMs;
347
+ if (waitMs > 0) {
348
+ await new Promise<void>((resolve) => {
349
+ const timer = setTimeout(resolve, waitMs);
350
+ timer.unref?.();
151
351
  });
152
- index += 1;
153
352
  }
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()];
353
+ if (!this.repairSessions.has(sessionId) || this.closed) {
354
+ return;
355
+ }
356
+
357
+ await this.refreshRepairSessionState(sessionId);
358
+ const current = this.repairSessions.get(sessionId);
359
+ if (!current) {
360
+ return;
361
+ }
362
+ if (this.isRepairSessionComplete(current)) {
363
+ this.finalizeRepairSession(sessionId, true);
364
+ return;
365
+ }
366
+
367
+ for (const [target, state] of current.targets) {
368
+ if (state.unresolved.size === 0) {
369
+ continue;
370
+ }
371
+ state.attempts += 1;
372
+ try {
373
+ await this.requestSync([...state.unresolved], [target]);
374
+ } catch {
375
+ // Best-effort: keep unresolved and let retries/timeout determine outcome.
376
+ }
377
+ }
378
+
379
+ await this.refreshRepairSessionState(sessionId);
380
+ const afterSend = this.repairSessions.get(sessionId);
381
+ if (!afterSend) {
382
+ return;
383
+ }
384
+ if (this.isRepairSessionComplete(afterSend)) {
385
+ this.finalizeRepairSession(sessionId, true);
386
+ return;
387
+ }
388
+
389
+ if (afterSend.mode === "best-effort") {
390
+ this.finalizeRepairSession(sessionId, false);
391
+ return;
392
+ }
393
+ }
394
+
395
+ for (;;) {
396
+ if (!this.repairSessions.has(sessionId) || this.closed) {
397
+ return;
398
+ }
399
+ await this.refreshRepairSessionState(sessionId);
400
+ const current = this.repairSessions.get(sessionId);
401
+ if (!current) {
402
+ return;
403
+ }
404
+ if (this.isRepairSessionComplete(current)) {
405
+ this.finalizeRepairSession(sessionId, true);
406
+ return;
407
+ }
408
+ await new Promise<void>((resolve) => {
409
+ const timer = setTimeout(resolve, SESSION_POLL_INTERVAL_MS);
410
+ timer.unref?.();
411
+ });
412
+ }
413
+ }
414
+
415
+ startRepairSession(properties: {
416
+ entries: Map<string, EntryReplicated<R>>;
417
+ targets: string[];
418
+ mode?: RepairSessionMode;
419
+ timeoutMs?: number;
420
+ retryIntervalsMs?: number[];
421
+ }): RepairSession {
422
+ const mode = properties.mode ?? "best-effort";
423
+ const startedAt = Date.now();
424
+ const timeoutMs = Math.max(
425
+ 1,
426
+ Math.floor(
427
+ properties.timeoutMs ??
428
+ (mode === "convergent"
429
+ ? DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS
430
+ : DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS),
431
+ ),
432
+ );
433
+ const retryIntervalsMs = this.normalizeRetryIntervals(
434
+ mode,
435
+ properties.retryIntervalsMs,
436
+ );
437
+ const allHashes = this.getPrioritizedHashes(properties.entries);
438
+ const trackedHashes =
439
+ mode === "convergent" && allHashes.length > this.maxConvergentTrackedHashes
440
+ ? allHashes.slice(0, this.maxConvergentTrackedHashes)
441
+ : allHashes;
442
+ const truncated = trackedHashes.length < allHashes.length;
443
+ const targets = [...new Set(properties.targets)];
444
+ const id = `repair-${++this.repairSessionCounter}`;
445
+ const deferred = createDeferred<RepairSessionResult[]>();
446
+
447
+ const targetStates = new Map<string, RepairSessionTargetState>();
448
+ for (const target of targets) {
449
+ targetStates.set(target, {
450
+ unresolved: new Set(trackedHashes),
451
+ requestedCount: trackedHashes.length,
452
+ requestedTotalCount: allHashes.length,
453
+ attempts: 0,
454
+ });
158
455
  }
159
456
 
160
- return this.rpc.send(new RequestMaybeSync({ hashes }), {
161
- priority: 1,
162
- mode: new SilentDelivery({ to: properties.targets, redundancy: 1 }),
457
+ const session: RepairSessionState = {
458
+ id,
459
+ mode,
460
+ startedAt,
461
+ timeoutMs,
462
+ retryIntervalsMs,
463
+ targets: targetStates,
464
+ truncated,
465
+ deferred,
466
+ cancelled: false,
467
+ };
468
+
469
+ if (allHashes.length === 0 || targets.length === 0) {
470
+ deferred.resolve(this.buildRepairSessionResult(session, true));
471
+ return {
472
+ id,
473
+ done: deferred.promise,
474
+ cancel: () => {
475
+ // no-op
476
+ },
477
+ };
478
+ }
479
+
480
+ // For capped convergent sessions, still dispatch the full set once so large
481
+ // repairs are not limited to tracked hashes.
482
+ if (mode === "convergent" && truncated) {
483
+ void this.onMaybeMissingEntries({
484
+ entries: properties.entries,
485
+ targets,
486
+ }).catch(() => {
487
+ // Best-effort: retries on tracked hashes continue via runRepairSession.
488
+ });
489
+ }
490
+
491
+ session.timer = setTimeout(() => {
492
+ this.finalizeRepairSession(id, false);
493
+ }, timeoutMs);
494
+ session.timer.unref?.();
495
+
496
+ this.repairSessions.set(id, session);
497
+ void this.runRepairSession(id).catch(() => {
498
+ this.finalizeRepairSession(id, false);
163
499
  });
500
+
501
+ return {
502
+ id,
503
+ done: deferred.promise,
504
+ cancel: () => {
505
+ this.finalizeRepairSession(id, false);
506
+ },
507
+ };
508
+ }
509
+
510
+ onMaybeMissingEntries(properties: {
511
+ entries: Map<string, EntryReplicated<R>>;
512
+ targets: string[];
513
+ }): Promise<void> {
514
+ const hashes = this.getPrioritizedHashes(properties.entries);
515
+ const chunks = this.chunk(hashes, this.maxHashesPerMessage);
516
+ return chunks.reduce(
517
+ (promise, chunk) =>
518
+ promise.then(() =>
519
+ this.rpc.send(new RequestMaybeSync({ hashes: chunk }), {
520
+ priority: 1,
521
+ mode: new SilentDelivery({
522
+ to: properties.targets,
523
+ redundancy: 1,
524
+ }),
525
+ }),
526
+ ),
527
+ Promise.resolve(),
528
+ );
164
529
  }
165
530
 
166
531
  async onMessage(
@@ -210,7 +575,9 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
210
575
  entries: EntryWithRefs<any>[];
211
576
  from: PublicSignKey;
212
577
  }): Promise<void> | void {
578
+ const resolvedHashes: string[] = [];
213
579
  for (const entry of properties.entries) {
580
+ resolvedHashes.push(entry.entry.hash);
214
581
  const set = this.syncInFlight.get(properties.from.hashcode());
215
582
  if (set) {
216
583
  set.delete(entry.entry.hash);
@@ -219,6 +586,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
219
586
  }
220
587
  }
221
588
  }
589
+ this.markRepairSessionResolvedHashes(resolvedHashes);
222
590
  }
223
591
 
224
592
  async queueSync(
@@ -276,16 +644,29 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
276
644
  }
277
645
 
278
646
  const isBigInt = typeof hashes[0] === "bigint";
279
-
280
- await this.rpc.send(
281
- isBigInt
282
- ? new RequestMaybeSyncCoordinate({ hashNumbers: hashes as bigint[] })
283
- : new ResponseMaybeSync({ hashes: hashes as string[] }),
284
- {
285
- mode: new SilentDelivery({ to, redundancy: 1 }),
286
- priority: 1,
287
- },
288
- );
647
+ if (isBigInt) {
648
+ const chunks = this.chunk(
649
+ hashes as bigint[],
650
+ this.maxCoordinatesPerMessage,
651
+ );
652
+ for (const chunk of chunks) {
653
+ await this.rpc.send(
654
+ new RequestMaybeSyncCoordinate({ hashNumbers: chunk }),
655
+ {
656
+ mode: new SilentDelivery({ to, redundancy: 1 }),
657
+ priority: 1,
658
+ },
659
+ );
660
+ }
661
+ } else {
662
+ const chunks = this.chunk(hashes as string[], this.maxHashesPerMessage);
663
+ for (const chunk of chunks) {
664
+ await this.rpc.send(new ResponseMaybeSync({ hashes: chunk }), {
665
+ mode: new SilentDelivery({ to, redundancy: 1 }),
666
+ priority: 1,
667
+ });
668
+ }
669
+ }
289
670
  }
290
671
  private async checkHasCoordinateOrHash(key: string | bigint) {
291
672
  return typeof key === "bigint"
@@ -366,10 +747,14 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
366
747
  this.syncInFlightQueue.clear();
367
748
  this.syncInFlightQueueInverted.clear();
368
749
  this.syncInFlight.clear();
750
+ for (const sessionId of [...this.repairSessions.keys()]) {
751
+ this.finalizeRepairSession(sessionId, false);
752
+ }
369
753
  clearTimeout(this.syncMoreInterval);
370
754
  }
371
755
  onEntryAdded(entry: Entry<any>): void {
372
- return this.clearSyncProcess(entry.hash);
756
+ this.clearSyncProcess(entry.hash);
757
+ this.markRepairSessionResolvedHashes([entry.hash]);
373
758
  }
374
759
 
375
760
  onEntryRemoved(hash: string): void {
@@ -393,17 +778,18 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
393
778
  }
394
779
  }
395
780
 
396
- onPeerDisconnected(key: PublicSignKey): Promise<void> | void {
397
- return this.clearSyncProcessPublicKey(key);
781
+ onPeerDisconnected(key: PublicSignKey | string): Promise<void> | void {
782
+ const publicKeyHash = typeof key === "string" ? key : key.hashcode();
783
+ return this.clearSyncProcessPublicKeyHash(publicKeyHash);
398
784
  }
399
- private clearSyncProcessPublicKey(publicKey: PublicSignKey) {
400
- this.syncInFlight.delete(publicKey.hashcode());
401
- const map = this.syncInFlightQueueInverted.get(publicKey.hashcode());
785
+ private clearSyncProcessPublicKeyHash(publicKeyHash: string) {
786
+ this.syncInFlight.delete(publicKeyHash);
787
+ const map = this.syncInFlightQueueInverted.get(publicKeyHash);
402
788
  if (map) {
403
789
  for (const hash of map) {
404
790
  const arr = this.syncInFlightQueue.get(hash);
405
791
  if (arr) {
406
- const filtered = arr.filter((x) => !x.equals(publicKey));
792
+ const filtered = arr.filter((x) => x.hashcode() !== publicKeyHash);
407
793
  if (filtered.length > 0) {
408
794
  this.syncInFlightQueue.set(hash, filtered);
409
795
  } else {
@@ -411,7 +797,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
411
797
  }
412
798
  }
413
799
  }
414
- this.syncInFlightQueueInverted.delete(publicKey.hashcode());
800
+ this.syncInFlightQueueInverted.delete(publicKeyHash);
415
801
  }
416
802
  }
417
803