@peerbit/shared-log 13.1.17 → 13.2.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 (59) hide show
  1. package/dist/benchmark/index.js +91 -29
  2. package/dist/benchmark/index.js.map +1 -1
  3. package/dist/benchmark/native-graph.js +120 -10
  4. package/dist/benchmark/native-graph.js.map +1 -1
  5. package/dist/benchmark/network-preset-e2e.d.ts +2 -0
  6. package/dist/benchmark/network-preset-e2e.d.ts.map +1 -0
  7. package/dist/benchmark/network-preset-e2e.js +497 -0
  8. package/dist/benchmark/network-preset-e2e.js.map +1 -0
  9. package/dist/benchmark/receive-prune.d.ts +2 -0
  10. package/dist/benchmark/receive-prune.d.ts.map +1 -0
  11. package/dist/benchmark/receive-prune.js +816 -0
  12. package/dist/benchmark/receive-prune.js.map +1 -0
  13. package/dist/benchmark/sync-catchup.d.ts +1 -2
  14. package/dist/benchmark/sync-catchup.d.ts.map +1 -1
  15. package/dist/benchmark/sync-catchup.js +175 -7
  16. package/dist/benchmark/sync-catchup.js.map +1 -1
  17. package/dist/src/checked-prune.d.ts +3 -0
  18. package/dist/src/checked-prune.d.ts.map +1 -1
  19. package/dist/src/checked-prune.js +24 -0
  20. package/dist/src/checked-prune.js.map +1 -1
  21. package/dist/src/exchange-heads.d.ts +238 -1
  22. package/dist/src/exchange-heads.d.ts.map +1 -1
  23. package/dist/src/exchange-heads.js +1512 -36
  24. package/dist/src/exchange-heads.js.map +1 -1
  25. package/dist/src/index.d.ts +276 -5
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/index.js +7358 -666
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/pid.d.ts.map +1 -1
  30. package/dist/src/pid.js +25 -9
  31. package/dist/src/pid.js.map +1 -1
  32. package/dist/src/ranges.d.ts +11 -2
  33. package/dist/src/ranges.d.ts.map +1 -1
  34. package/dist/src/ranges.js +74 -30
  35. package/dist/src/ranges.js.map +1 -1
  36. package/dist/src/sync/index.d.ts +77 -2
  37. package/dist/src/sync/index.d.ts.map +1 -1
  38. package/dist/src/sync/rateless-iblt.d.ts +11 -5
  39. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  40. package/dist/src/sync/rateless-iblt.js +123 -60
  41. package/dist/src/sync/rateless-iblt.js.map +1 -1
  42. package/dist/src/sync/simple.d.ts +56 -4
  43. package/dist/src/sync/simple.d.ts.map +1 -1
  44. package/dist/src/sync/simple.js +495 -63
  45. package/dist/src/sync/simple.js.map +1 -1
  46. package/dist/src/utils.d.ts +2 -1
  47. package/dist/src/utils.d.ts.map +1 -1
  48. package/dist/src/utils.js +49 -2
  49. package/dist/src/utils.js.map +1 -1
  50. package/package.json +27 -17
  51. package/src/checked-prune.ts +27 -0
  52. package/src/exchange-heads.ts +2018 -41
  53. package/src/index.ts +13114 -3167
  54. package/src/pid.ts +24 -13
  55. package/src/ranges.ts +90 -40
  56. package/src/sync/index.ts +115 -2
  57. package/src/sync/rateless-iblt.ts +154 -75
  58. package/src/sync/simple.ts +590 -95
  59. package/src/utils.ts +73 -3
@@ -16,13 +16,18 @@ import {
16
16
  import {
17
17
  EntryWithRefs,
18
18
  createExchangeHeadsMessages,
19
+ createRawExchangeHeadsMessages,
19
20
  } from "../exchange-heads.js";
20
21
  import { TransportMessage } from "../message.js";
21
22
  import type { EntryReplicated } from "../ranges.js";
22
23
  import type {
24
+ HashSymbolResolver,
25
+ HashSymbolHashListResolver,
26
+ RawExchangeHeadsSender,
23
27
  RepairSession,
24
28
  RepairSessionMode,
25
29
  RepairSessionResult,
30
+ SyncEntryCoordinates,
26
31
  SyncOptions,
27
32
  SyncableKey,
28
33
  Syncronizer,
@@ -73,14 +78,76 @@ export class ConfirmEntriesMessage extends TransportMessage {
73
78
  }
74
79
  }
75
80
 
81
+ export const SIMPLE_SYNC_RAW_EXCHANGE_HEADS_CAPABILITY = 1;
82
+
83
+ @variant([0, 8])
84
+ export class ResponseMaybeSyncCapabilities extends TransportMessage {
85
+ @field({ type: vec("string") })
86
+ hashes: string[];
87
+
88
+ @field({ type: "u32" })
89
+ capabilities: number;
90
+
91
+ constructor(props: { hashes: string[]; capabilities?: number }) {
92
+ super();
93
+ this.hashes = props.hashes;
94
+ this.capabilities =
95
+ props.capabilities ?? SIMPLE_SYNC_RAW_EXCHANGE_HEADS_CAPABILITY;
96
+ }
97
+ }
98
+
99
+ @variant([0, 9])
100
+ export class RequestMaybeSyncCoordinateCapabilities extends TransportMessage {
101
+ @field({ type: vec("u64") })
102
+ hashNumbers: bigint[];
103
+
104
+ @field({ type: "u32" })
105
+ capabilities: number;
106
+
107
+ constructor(props: { hashNumbers: bigint[]; capabilities?: number }) {
108
+ super();
109
+ this.hashNumbers = props.hashNumbers;
110
+ this.capabilities =
111
+ props.capabilities ?? SIMPLE_SYNC_RAW_EXCHANGE_HEADS_CAPABILITY;
112
+ }
113
+ }
114
+
115
+ const canReceiveRawExchangeHeads = (
116
+ message:
117
+ | ResponseMaybeSync
118
+ | ResponseMaybeSyncCapabilities
119
+ | RequestMaybeSyncCoordinate
120
+ | RequestMaybeSyncCoordinateCapabilities,
121
+ ) =>
122
+ (message instanceof ResponseMaybeSyncCapabilities ||
123
+ message instanceof RequestMaybeSyncCoordinateCapabilities) &&
124
+ (message.capabilities & SIMPLE_SYNC_RAW_EXCHANGE_HEADS_CAPABILITY) !== 0;
125
+
126
+ type KnownSyncKeys = {
127
+ keys: Set<SyncableKey>;
128
+ checkedCoordinates: boolean;
129
+ checkedHashes: boolean;
130
+ };
131
+
76
132
  const getHashesFromSymbols = async (
77
133
  symbols: bigint[],
78
134
  entryIndex: Index<EntryReplicated<any>, any>,
79
135
  coordinateToHash: Cache<string>,
80
- ) => {
136
+ resolveHashesForSymbols?: HashSymbolResolver,
137
+ resolveHashListForSymbols?: HashSymbolHashListResolver,
138
+ ): Promise<Set<string> | string[]> => {
81
139
  let queries: IntegerCompare[] = [];
82
140
  let batchSize = 128; // TODO arg
83
141
  let results = new Set<string>();
142
+ let missingSymbols: bigint[] = [];
143
+ const addMissingUnlessCached = (symbol: bigint) => {
144
+ const fromCache = coordinateToHash.get(symbol);
145
+ if (fromCache) {
146
+ results.add(fromCache);
147
+ return;
148
+ }
149
+ missingSymbols.push(symbol);
150
+ };
84
151
  const handleBatch = async (end = false) => {
85
152
  if (queries.length >= batchSize || (end && queries.length > 0)) {
86
153
  const entries = await entryIndex
@@ -97,16 +164,63 @@ const getHashesFromSymbols = async (
97
164
  }
98
165
  }
99
166
  };
100
- for (let i = 0; i < symbols.length; i++) {
101
- const fromCache = coordinateToHash.get(symbols[i]);
102
- if (fromCache) {
103
- results.add(fromCache);
104
- continue;
167
+
168
+ if (resolveHashListForSymbols) {
169
+ const resolvedHashes = await resolveHashListForSymbols(symbols);
170
+ if (resolvedHashes) {
171
+ const resolvedHashList = Array.isArray(resolvedHashes)
172
+ ? resolvedHashes
173
+ : [...resolvedHashes];
174
+ let mergedHashes: Set<string> | undefined;
175
+ for (const symbol of symbols) {
176
+ const fromCache = coordinateToHash.get(symbol);
177
+ if (fromCache) {
178
+ mergedHashes ??= new Set(resolvedHashList);
179
+ mergedHashes.add(fromCache);
180
+ }
181
+ }
182
+ return mergedHashes ?? resolvedHashList;
105
183
  }
184
+ }
185
+
186
+ if (resolveHashesForSymbols) {
187
+ const resolved = await resolveHashesForSymbols(symbols);
188
+ if (resolved) {
189
+ for (const symbol of symbols) {
190
+ const hashes = resolved.get(symbol);
191
+ if (!hashes) {
192
+ addMissingUnlessCached(symbol);
193
+ continue;
194
+ }
195
+ let singleHash: string | undefined;
196
+ let count = 0;
197
+ for (const hash of hashes) {
198
+ results.add(hash);
199
+ singleHash = hash;
200
+ count += 1;
201
+ }
202
+ if (count === 0) {
203
+ addMissingUnlessCached(symbol);
204
+ } else if (count === 1) {
205
+ coordinateToHash.add(symbol, singleHash!);
206
+ }
207
+ }
208
+ } else {
209
+ for (const symbol of symbols) {
210
+ addMissingUnlessCached(symbol);
211
+ }
212
+ }
213
+ } else {
214
+ for (const symbol of symbols) {
215
+ addMissingUnlessCached(symbol);
216
+ }
217
+ }
218
+
219
+ for (const symbol of missingSymbols) {
106
220
  const matchQuery = new IntegerCompare({
107
221
  key: "hashNumber",
108
222
  compare: Compare.Equal,
109
- value: symbols[i],
223
+ value: symbol,
110
224
  });
111
225
 
112
226
  queries.push(matchQuery);
@@ -117,6 +231,9 @@ const getHashesFromSymbols = async (
117
231
  return results;
118
232
  };
119
233
 
234
+ const hashLookupResultSize = (hashes: Set<string> | string[]) =>
235
+ Array.isArray(hashes) ? hashes.length : hashes.size;
236
+
120
237
  const DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS = 30_000;
121
238
  const DEFAULT_CONVERGENT_RETRY_INTERVALS_MS = [0, 1_000, 3_000, 7_000];
122
239
  const DEFAULT_BEST_EFFORT_RETRY_INTERVALS_MS = [0];
@@ -131,6 +248,8 @@ export const SYNC_MESSAGE_PRIORITY = CONVERGENCE_MESSAGE_PRIORITY;
131
248
  // pubsub stream warmup). Keep it coarse-grained so we do not hammer the network under
132
249
  // large historical backfills.
133
250
  const SIMPLE_SYNC_RETRY_AFTER_MS = 10_000;
251
+ const EXCHANGE_HEAD_RESPONSE_DEDUPE_TTL_MS = SIMPLE_SYNC_RETRY_AFTER_MS - 1_000;
252
+ const RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS = 30_000;
134
253
 
135
254
  const createDeferred = <T>() => {
136
255
  let resolve!: (value: T | PromiseLike<T>) => void;
@@ -176,7 +295,16 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
176
295
  log: Log<any>;
177
296
  entryIndex: Index<EntryReplicated<R>, any>;
178
297
  coordinateToHash: Cache<string>;
298
+ private resolveHashesForSymbols?: HashSymbolResolver;
299
+ private resolveHashListForSymbols?: HashSymbolHashListResolver;
179
300
  private syncOptions?: SyncOptions<R>;
301
+ private isEntryRecentlyKnownByPeer?: (
302
+ hash: string,
303
+ peer: string,
304
+ maxAgeMs: number,
305
+ ) => boolean;
306
+ private sendRawExchangeHeads?: RawExchangeHeadsSender;
307
+ private recentlySentExchangeHeads: Map<string, Map<string, number>>;
180
308
  private repairSessionCounter: number;
181
309
  private repairSessions: Map<string, RepairSessionState>;
182
310
 
@@ -190,7 +318,15 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
190
318
  entryIndex: Index<EntryReplicated<R>, any>;
191
319
  log: Log<any>;
192
320
  coordinateToHash: Cache<string>;
321
+ resolveHashesForSymbols?: HashSymbolResolver;
322
+ resolveHashListForSymbols?: HashSymbolHashListResolver;
193
323
  sync?: SyncOptions<R>;
324
+ isEntryRecentlyKnownByPeer?: (
325
+ hash: string,
326
+ peer: string,
327
+ maxAgeMs: number,
328
+ ) => boolean;
329
+ sendRawExchangeHeads?: RawExchangeHeadsSender;
194
330
  }) {
195
331
  this.syncInFlightQueue = new Map();
196
332
  this.syncInFlightQueueInverted = new Map();
@@ -199,13 +335,18 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
199
335
  this.log = properties.log;
200
336
  this.entryIndex = properties.entryIndex;
201
337
  this.coordinateToHash = properties.coordinateToHash;
338
+ this.resolveHashesForSymbols = properties.resolveHashesForSymbols;
339
+ this.resolveHashListForSymbols = properties.resolveHashListForSymbols;
202
340
  this.syncOptions = properties.sync;
341
+ this.isEntryRecentlyKnownByPeer = properties.isEntryRecentlyKnownByPeer;
342
+ this.sendRawExchangeHeads = properties.sendRawExchangeHeads;
343
+ this.recentlySentExchangeHeads = new Map();
203
344
  this.repairSessionCounter = 0;
204
345
  this.repairSessions = new Map();
205
346
  }
206
347
 
207
348
  private getPrioritizedHashes(
208
- entries: Map<string, EntryReplicated<R>>,
349
+ entries: Map<string, SyncEntryCoordinates<R>>,
209
350
  ): string[] {
210
351
  const priorityFn = this.syncOptions?.priority;
211
352
  if (!priorityFn) {
@@ -215,7 +356,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
215
356
  let index = 0;
216
357
  const scored: { hash: string; index: number; priority: number }[] = [];
217
358
  for (const [hash, entry] of entries) {
218
- const priorityValue = priorityFn(entry);
359
+ const priorityValue = priorityFn(entry as EntryReplicated<R>);
219
360
  scored.push({
220
361
  hash,
221
362
  index,
@@ -277,6 +418,47 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
277
418
  return out;
278
419
  }
279
420
 
421
+ private filterRecentlySentExchangeHeads(
422
+ hashes: Iterable<string>,
423
+ peer: PublicSignKey,
424
+ ): string[] {
425
+ const peerHash = peer.hashcode();
426
+ const now = Date.now();
427
+ let recentlySent = this.recentlySentExchangeHeads.get(peerHash);
428
+ if (!recentlySent) {
429
+ recentlySent = new Map();
430
+ this.recentlySentExchangeHeads.set(peerHash, recentlySent);
431
+ }
432
+ for (const [hash, timestamp] of recentlySent) {
433
+ if (now - timestamp > EXCHANGE_HEAD_RESPONSE_DEDUPE_TTL_MS) {
434
+ recentlySent.delete(hash);
435
+ }
436
+ }
437
+ const out: string[] = [];
438
+ const seen = new Set<string>();
439
+ for (const hash of hashes) {
440
+ if (seen.has(hash)) {
441
+ continue;
442
+ }
443
+ seen.add(hash);
444
+ if (recentlySent.has(hash)) {
445
+ continue;
446
+ }
447
+ if (
448
+ this.isEntryRecentlyKnownByPeer?.(
449
+ hash,
450
+ peerHash,
451
+ RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS,
452
+ )
453
+ ) {
454
+ continue;
455
+ }
456
+ recentlySent.set(hash, now);
457
+ out.push(hash);
458
+ }
459
+ return out;
460
+ }
461
+
280
462
  private isRepairSessionComplete(session: RepairSessionState): boolean {
281
463
  for (const state of session.targets.values()) {
282
464
  if (state.unresolved.size > 0) {
@@ -328,12 +510,26 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
328
510
  return;
329
511
  }
330
512
  for (const state of session.targets.values()) {
331
- for (const hash of [...state.unresolved]) {
332
- if (await this.log.has(hash)) {
333
- state.unresolved.delete(hash);
334
- }
513
+ const resolved =
514
+ typeof this.log.hasMany === "function"
515
+ ? await this.log.hasMany(state.unresolved)
516
+ : await this.getExistingRepairHashes(state.unresolved);
517
+ for (const hash of resolved) {
518
+ state.unresolved.delete(hash);
519
+ }
520
+ }
521
+ }
522
+
523
+ private async getExistingRepairHashes(
524
+ hashes: Iterable<string>,
525
+ ): Promise<Set<string>> {
526
+ const resolved = new Set<string>();
527
+ for (const hash of hashes) {
528
+ if (await this.log.has(hash)) {
529
+ resolved.add(hash);
335
530
  }
336
531
  }
532
+ return resolved;
337
533
  }
338
534
 
339
535
  private markRepairSessionResolvedHashes(hashes: string[]): void {
@@ -352,6 +548,20 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
352
548
  }
353
549
  }
354
550
 
551
+ private markRepairSessionResolvedHash(hash: string): void {
552
+ if (this.repairSessions.size === 0) {
553
+ return;
554
+ }
555
+ for (const [sessionId, session] of this.repairSessions) {
556
+ for (const state of session.targets.values()) {
557
+ state.unresolved.delete(hash);
558
+ }
559
+ if (this.isRepairSessionComplete(session)) {
560
+ this.finalizeRepairSession(sessionId, true);
561
+ }
562
+ }
563
+ }
564
+
355
565
  private async runRepairSession(sessionId: string): Promise<void> {
356
566
  const session = this.repairSessions.get(sessionId);
357
567
  if (!session) {
@@ -454,7 +664,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
454
664
  }
455
665
 
456
666
  startRepairSession(properties: {
457
- entries: Map<string, EntryReplicated<R>>;
667
+ entries: Map<string, SyncEntryCoordinates<R>>;
458
668
  targets: string[];
459
669
  mode?: RepairSessionMode;
460
670
  timeoutMs?: number;
@@ -550,12 +760,22 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
550
760
  }
551
761
 
552
762
  async onMaybeMissingEntries(properties: {
553
- entries: Map<string, EntryReplicated<R>>;
763
+ entries: Map<string, SyncEntryCoordinates<R>>;
764
+ targets: string[];
765
+ }): Promise<void> {
766
+ await this.onMaybeMissingHashes({
767
+ hashes: this.getPrioritizedHashes(properties.entries),
768
+ targets: properties.targets,
769
+ });
770
+ }
771
+
772
+ async onMaybeMissingHashes(properties: {
773
+ hashes: Iterable<string>;
554
774
  targets: string[];
555
775
  }): Promise<void> {
556
776
  const profile = this.syncOptions?.profile;
557
777
  const startedAt = syncProfileStart(profile);
558
- const hashes = this.getPrioritizedHashes(properties.entries);
778
+ const hashes = [...properties.hashes];
559
779
  const chunks = this.chunk(hashes, this.maxHashesPerMessage);
560
780
  try {
561
781
  await chunks.reduce(
@@ -583,6 +803,42 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
583
803
  }
584
804
  }
585
805
 
806
+ /**
807
+ * Ship exchange heads to one peer: the fused native raw path when the
808
+ * peer advertised raw capability and the shared log provided a fused
809
+ * sender, otherwise the TS message path (raw or plain by capability).
810
+ * Returns the number of messages sent and whether the fused path ran.
811
+ */
812
+ private async shipExchangeHeads(
813
+ hashes: string[],
814
+ to: PublicSignKey,
815
+ canReceiveRaw: boolean,
816
+ ): Promise<{ messages: number; fused: boolean }> {
817
+ if (canReceiveRaw && this.sendRawExchangeHeads) {
818
+ const sentMessages = await this.sendRawExchangeHeads(hashes, [
819
+ to.hashcode(),
820
+ ]);
821
+ if (sentMessages !== undefined) {
822
+ return { messages: sentMessages, fused: true };
823
+ }
824
+ }
825
+ let messages = 0;
826
+ const messageGenerator = canReceiveRaw
827
+ ? createRawExchangeHeadsMessages(
828
+ this.log,
829
+ hashes,
830
+ this.syncOptions?.profile,
831
+ )
832
+ : createExchangeHeadsMessages(this.log, hashes);
833
+ for await (const message of messageGenerator) {
834
+ messages += 1;
835
+ await this.rpc.send(message, {
836
+ mode: new SilentDelivery({ to: [to], redundancy: 1 }),
837
+ });
838
+ }
839
+ return { messages, fused: false };
840
+ }
841
+
586
842
  async onMessage(
587
843
  msg: TransportMessage,
588
844
  context: RequestContext,
@@ -591,72 +847,76 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
591
847
  if (msg instanceof RequestMaybeSync) {
592
848
  await this.queueSync(msg.hashes, from);
593
849
  return true;
594
- } else if (msg instanceof ResponseMaybeSync) {
850
+ } else if (
851
+ msg instanceof ResponseMaybeSync ||
852
+ msg instanceof ResponseMaybeSyncCapabilities
853
+ ) {
595
854
  // TODO perhaps send less messages to more receivers for performance reasons?
596
855
  // TODO wait for previous send to target before trying to send more?
597
856
 
598
857
  const profile = this.syncOptions?.profile;
599
858
  const startedAt = syncProfileStart(profile);
859
+ const hashes = this.filterRecentlySentExchangeHeads(msg.hashes, from);
600
860
  let messages = 0;
861
+ let fused = false;
601
862
  try {
602
- for await (const message of createExchangeHeadsMessages(
603
- this.log,
604
- msg.hashes,
605
- )) {
606
- messages += 1;
607
- await this.rpc.send(message, {
608
- mode: new SilentDelivery({ to: [context.from!], redundancy: 1 }),
609
- });
610
- }
863
+ ({ messages, fused } = await this.shipExchangeHeads(
864
+ hashes,
865
+ context.from!,
866
+ canReceiveRawExchangeHeads(msg),
867
+ ));
611
868
  } finally {
612
869
  if (profile) {
613
870
  emitSyncProfileDuration(profile, startedAt, {
614
871
  name: "simple.exchangeHeads",
615
- entries: msg.hashes.length,
872
+ entries: hashes.length,
616
873
  messages,
617
874
  targets: 1,
618
- details: { source: "responseMaybeSync" },
875
+ details: { source: "responseMaybeSync", fused },
619
876
  });
620
877
  }
621
878
  }
622
879
  return true;
623
- } else if (msg instanceof RequestMaybeSyncCoordinate) {
880
+ } else if (
881
+ msg instanceof RequestMaybeSyncCoordinate ||
882
+ msg instanceof RequestMaybeSyncCoordinateCapabilities
883
+ ) {
624
884
  const profile = this.syncOptions?.profile;
625
885
  const lookupStartedAt = syncProfileStart(profile);
626
886
  const hashes = await getHashesFromSymbols(
627
887
  msg.hashNumbers,
628
888
  this.entryIndex,
629
889
  this.coordinateToHash,
890
+ this.resolveHashesForSymbols,
891
+ this.resolveHashListForSymbols,
630
892
  );
631
893
  if (profile) {
632
894
  emitSyncProfileDuration(profile, lookupStartedAt, {
633
895
  name: "simple.coordinateLookup",
634
- entries: hashes.size,
896
+ entries: hashLookupResultSize(hashes),
635
897
  symbols: msg.hashNumbers.length,
636
898
  });
637
899
  }
638
900
 
639
901
  const exchangeStartedAt = syncProfileStart(profile);
902
+ const hashesToSend = this.filterRecentlySentExchangeHeads(hashes, from);
640
903
  let messages = 0;
904
+ let fused = false;
641
905
  try {
642
- for await (const message of createExchangeHeadsMessages(
643
- this.log,
644
- hashes,
645
- )) {
646
- messages += 1;
647
- await this.rpc.send(message, {
648
- mode: new SilentDelivery({ to: [context.from!], redundancy: 1 }),
649
- // dont set priority 1 here because this will block other messages that should higher priority
650
- });
651
- }
906
+ // dont set priority 1 here because this will block other messages that should higher priority
907
+ ({ messages, fused } = await this.shipExchangeHeads(
908
+ hashesToSend,
909
+ context.from!,
910
+ canReceiveRawExchangeHeads(msg),
911
+ ));
652
912
  } finally {
653
913
  if (profile) {
654
914
  emitSyncProfileDuration(profile, exchangeStartedAt, {
655
915
  name: "simple.exchangeHeads",
656
- entries: hashes.size,
916
+ entries: hashesToSend.length,
657
917
  messages,
658
918
  targets: 1,
659
- details: { source: "requestMaybeSyncCoordinate" },
919
+ details: { source: "requestMaybeSyncCoordinate", fused },
660
920
  });
661
921
  }
662
922
  }
@@ -671,15 +931,21 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
671
931
  entries: EntryWithRefs<any>[];
672
932
  from: PublicSignKey;
673
933
  }): Promise<void> | void {
674
- const resolvedHashes: string[] = [];
675
- for (const entry of properties.entries) {
676
- resolvedHashes.push(entry.entry.hash);
677
- this.clearSyncInFlightForPeer(
678
- properties.from.hashcode(),
679
- entry.entry.hash,
680
- );
681
- }
682
- this.markRepairSessionResolvedHashes(resolvedHashes);
934
+ return this.onReceivedEntryHashes({
935
+ hashes: properties.entries.map((entry) => entry.entry.hash),
936
+ from: properties.from,
937
+ });
938
+ }
939
+
940
+ onReceivedEntryHashes(properties: {
941
+ hashes: string[];
942
+ from: PublicSignKey;
943
+ }): Promise<void> | void {
944
+ this.clearSyncInFlightForPeerHashes(
945
+ properties.from.hashcode(),
946
+ properties.hashes,
947
+ );
948
+ this.markRepairSessionResolvedHashes(properties.hashes);
683
949
  }
684
950
 
685
951
  async queueSync(
@@ -690,39 +956,101 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
690
956
  const requestHashes: SyncableKey[] = [];
691
957
  const profile = this.syncOptions?.profile;
692
958
  const startedAt = syncProfileStart(profile);
959
+ const resolveKnownStartedAt = syncProfileStart(profile);
960
+ const knownKeys =
961
+ options?.skipCheck === true
962
+ ? undefined
963
+ : await this.resolveKnownSyncKeys(keys);
964
+ if (profile) {
965
+ emitSyncProfileDuration(profile, resolveKnownStartedAt, {
966
+ name: "simple.queueSync.resolveKnown",
967
+ entries: keys.length,
968
+ count: knownKeys?.keys.size ?? 0,
969
+ details: {
970
+ checkedCoordinates: knownKeys?.checkedCoordinates === true,
971
+ checkedHashes: knownKeys?.checkedHashes === true,
972
+ skipCheck: options?.skipCheck === true,
973
+ },
974
+ });
975
+ }
976
+ const fromHash = from.hashcode();
977
+ let queuedHashAliases: Map<string, SyncableKey> | undefined;
978
+ const getQueuedSyncKeyForBatch = (key: SyncableKey) => {
979
+ if (this.syncInFlightQueue.has(key)) {
980
+ return key;
981
+ }
982
+ if (typeof key === "string") {
983
+ if (!queuedHashAliases) {
984
+ queuedHashAliases = new Map();
985
+ for (const queuedKey of this.syncInFlightQueue.keys()) {
986
+ if (typeof queuedKey !== "bigint") {
987
+ continue;
988
+ }
989
+ const hash = this.coordinateToHash.get(queuedKey);
990
+ if (hash) {
991
+ queuedHashAliases.set(hash, queuedKey);
992
+ }
993
+ }
994
+ }
995
+ return queuedHashAliases.get(key);
996
+ }
997
+ const hash = this.coordinateToHash.get(key);
998
+ return hash && this.syncInFlightQueue.has(hash) ? hash : undefined;
999
+ };
693
1000
 
694
1001
  try {
1002
+ const loopStartedAt = syncProfileStart(profile);
695
1003
  for (const key of keys) {
696
- const coordinateOrHash = this.getQueuedSyncKey(key) ?? key;
1004
+ const coordinateOrHash = getQueuedSyncKeyForBatch(key) ?? key;
697
1005
  const inFlight = this.syncInFlightQueue.get(coordinateOrHash);
698
1006
  if (inFlight) {
699
- if (!inFlight.find((x) => x.hashcode() === from.hashcode())) {
1007
+ if (!inFlight.find((x) => x.hashcode() === fromHash)) {
700
1008
  inFlight.push(from);
701
- let inverted = this.syncInFlightQueueInverted.get(from.hashcode());
1009
+ let inverted = this.syncInFlightQueueInverted.get(fromHash);
702
1010
  if (!inverted) {
703
1011
  inverted = new Set();
704
- this.syncInFlightQueueInverted.set(from.hashcode(), inverted);
1012
+ this.syncInFlightQueueInverted.set(fromHash, inverted);
705
1013
  }
706
1014
  inverted.add(coordinateOrHash);
707
1015
  }
708
1016
  } else if (
709
1017
  options?.skipCheck ||
710
- !(await this.checkHasCoordinateOrHash(coordinateOrHash))
1018
+ !(await this.checkHasCoordinateOrHash(
1019
+ coordinateOrHash,
1020
+ knownKeys,
1021
+ ))
711
1022
  ) {
712
1023
  // Track the initial sender so we can retry if the first request is lost.
713
1024
  this.syncInFlightQueue.set(coordinateOrHash, [from]);
714
- let inverted = this.syncInFlightQueueInverted.get(from.hashcode());
1025
+ let inverted = this.syncInFlightQueueInverted.get(fromHash);
715
1026
  if (!inverted) {
716
1027
  inverted = new Set();
717
- this.syncInFlightQueueInverted.set(from.hashcode(), inverted);
1028
+ this.syncInFlightQueueInverted.set(fromHash, inverted);
718
1029
  }
719
1030
  inverted.add(coordinateOrHash);
720
1031
  requestHashes.push(coordinateOrHash); // request immediately (first time we have seen this hash)
1032
+ if (
1033
+ queuedHashAliases &&
1034
+ typeof coordinateOrHash === "bigint"
1035
+ ) {
1036
+ const hash = this.coordinateToHash.get(coordinateOrHash);
1037
+ if (hash) {
1038
+ queuedHashAliases.set(hash, coordinateOrHash);
1039
+ }
1040
+ }
721
1041
  }
722
1042
  }
1043
+ if (profile) {
1044
+ emitSyncProfileDuration(profile, loopStartedAt, {
1045
+ name: "simple.queueSync.plan",
1046
+ entries: keys.length,
1047
+ count: requestHashes.length,
1048
+ targets: 1,
1049
+ });
1050
+ }
723
1051
 
724
1052
  requestHashes.length > 0 &&
725
- (await this.requestSync(requestHashes, [from!.hashcode()]));
1053
+ (await this.requestSync(requestHashes, [fromHash]));
726
1054
  } finally {
727
1055
  if (profile) {
728
1056
  emitSyncProfileDuration(profile, startedAt, {
@@ -774,32 +1102,41 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
774
1102
  coordinateHashCount = coordinateHashes.length;
775
1103
  stringHashCount = stringHashes.length;
776
1104
 
777
- if (coordinateHashes.length > 0) {
778
- const chunks = this.chunk(
779
- coordinateHashes,
780
- this.maxCoordinatesPerMessage,
781
- );
782
- coordinateMessages = chunks.length;
783
- for (const chunk of chunks) {
784
- await this.rpc.send(
785
- new RequestMaybeSyncCoordinate({ hashNumbers: chunk }),
786
- {
787
- mode: new SilentDelivery({ to, redundancy: 1 }),
788
- priority: SYNC_MESSAGE_PRIORITY,
789
- },
1105
+ if (coordinateHashes.length > 0) {
1106
+ const chunks = this.chunk(
1107
+ coordinateHashes,
1108
+ this.maxCoordinatesPerMessage,
790
1109
  );
1110
+ coordinateMessages = chunks.length;
1111
+ for (const chunk of chunks) {
1112
+ await this.rpc.send(
1113
+ this.syncOptions?.rawExchangeHeads
1114
+ ? new RequestMaybeSyncCoordinateCapabilities({
1115
+ hashNumbers: chunk,
1116
+ })
1117
+ : new RequestMaybeSyncCoordinate({ hashNumbers: chunk }),
1118
+ {
1119
+ mode: new SilentDelivery({ to, redundancy: 1 }),
1120
+ priority: SYNC_MESSAGE_PRIORITY,
1121
+ },
1122
+ );
1123
+ }
791
1124
  }
792
- }
793
- if (stringHashes.length > 0) {
794
- const chunks = this.chunk(stringHashes, this.maxHashesPerMessage);
795
- stringMessages = chunks.length;
796
- for (const chunk of chunks) {
797
- await this.rpc.send(new ResponseMaybeSync({ hashes: chunk }), {
798
- mode: new SilentDelivery({ to, redundancy: 1 }),
799
- priority: SYNC_MESSAGE_PRIORITY,
800
- });
1125
+ if (stringHashes.length > 0) {
1126
+ const chunks = this.chunk(stringHashes, this.maxHashesPerMessage);
1127
+ stringMessages = chunks.length;
1128
+ for (const chunk of chunks) {
1129
+ await this.rpc.send(
1130
+ this.syncOptions?.rawExchangeHeads
1131
+ ? new ResponseMaybeSyncCapabilities({ hashes: chunk })
1132
+ : new ResponseMaybeSync({ hashes: chunk }),
1133
+ {
1134
+ mode: new SilentDelivery({ to, redundancy: 1 }),
1135
+ priority: SYNC_MESSAGE_PRIORITY,
1136
+ },
1137
+ );
1138
+ }
801
1139
  }
802
- }
803
1140
  } finally {
804
1141
  if (profile) {
805
1142
  emitSyncProfileDuration(profile, startedAt, {
@@ -815,7 +1152,63 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
815
1152
  }
816
1153
  }
817
1154
  }
818
- private async checkHasCoordinateOrHash(key: string | bigint) {
1155
+ private async resolveKnownSyncKeys(
1156
+ keys: SyncableKey[],
1157
+ ): Promise<KnownSyncKeys | undefined> {
1158
+ const hashes: string[] = [];
1159
+ const coordinates: bigint[] = [];
1160
+ for (const key of keys) {
1161
+ if (typeof key === "bigint") {
1162
+ coordinates.push(key);
1163
+ } else {
1164
+ hashes.push(key);
1165
+ }
1166
+ }
1167
+ const known: KnownSyncKeys = {
1168
+ keys: new Set(),
1169
+ checkedCoordinates: false,
1170
+ checkedHashes: false,
1171
+ };
1172
+ if (hashes.length > 0) {
1173
+ for (const hash of await this.log.hasMany(hashes)) {
1174
+ known.keys.add(hash);
1175
+ }
1176
+ known.checkedHashes = true;
1177
+ }
1178
+ if (coordinates.length > 0 && this.resolveHashesForSymbols) {
1179
+ const resolved = await this.resolveHashesForSymbols(coordinates);
1180
+ if (resolved) {
1181
+ for (const coordinate of coordinates) {
1182
+ const hashes = resolved.get(coordinate);
1183
+ if (!hashes) {
1184
+ continue;
1185
+ }
1186
+ for (const _hash of hashes) {
1187
+ known.keys.add(coordinate);
1188
+ break;
1189
+ }
1190
+ }
1191
+ known.checkedCoordinates = true;
1192
+ }
1193
+ }
1194
+ return known.checkedCoordinates || known.checkedHashes ? known : undefined;
1195
+ }
1196
+
1197
+ private async checkHasCoordinateOrHash(
1198
+ key: string | bigint,
1199
+ knownKeys?: KnownSyncKeys,
1200
+ ) {
1201
+ if (knownKeys) {
1202
+ if (knownKeys.keys.has(key)) {
1203
+ return true;
1204
+ }
1205
+ if (typeof key === "bigint" && knownKeys.checkedCoordinates) {
1206
+ return false;
1207
+ }
1208
+ if (typeof key === "string" && knownKeys.checkedHashes) {
1209
+ return false;
1210
+ }
1211
+ }
819
1212
  return typeof key === "bigint"
820
1213
  ? (await this.entryIndex.count({ query: { hashNumber: key } })) > 0
821
1214
  : this.log.has(key);
@@ -899,20 +1292,58 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
899
1292
  this.syncInFlightQueue.clear();
900
1293
  this.syncInFlightQueueInverted.clear();
901
1294
  this.syncInFlight.clear();
1295
+ this.recentlySentExchangeHeads.clear();
902
1296
  for (const sessionId of [...this.repairSessions.keys()]) {
903
1297
  this.finalizeRepairSession(sessionId, false);
904
1298
  }
905
1299
  clearTimeout(this.syncMoreInterval);
906
1300
  }
907
1301
  onEntryAdded(entry: Entry<any>): void {
908
- this.clearSyncProcess(entry.hash);
909
- this.markRepairSessionResolvedHashes([entry.hash]);
1302
+ this.onEntryAddedHash(entry.hash);
1303
+ }
1304
+
1305
+ onEntryAddedHashes(hashes: string[]): void {
1306
+ if (hashes.length === 0 || !this.hasEntryAddedState()) {
1307
+ return;
1308
+ }
1309
+ this.clearSyncProcesses(hashes);
1310
+ this.markRepairSessionResolvedHashes(hashes);
1311
+ }
1312
+
1313
+ onEntryAddedHash(hash: string): void {
1314
+ if (!this.hasEntryAddedState()) {
1315
+ return;
1316
+ }
1317
+ this.clearSyncProcess(hash);
1318
+ this.markRepairSessionResolvedHash(hash);
910
1319
  }
911
1320
 
912
1321
  onEntryRemoved(hash: string): void {
1322
+ if (!this.hasSyncProcessState()) {
1323
+ return;
1324
+ }
913
1325
  return this.clearSyncProcess(hash);
914
1326
  }
915
1327
 
1328
+ onEntryRemovedHashes(hashes: string[]): void {
1329
+ if (hashes.length === 0 || !this.hasSyncProcessState()) {
1330
+ return;
1331
+ }
1332
+ return this.clearSyncProcesses(hashes);
1333
+ }
1334
+
1335
+ private hasEntryAddedState(): boolean {
1336
+ return this.hasSyncProcessState() || this.repairSessions.size > 0;
1337
+ }
1338
+
1339
+ private hasSyncProcessState(): boolean {
1340
+ return (
1341
+ this.syncInFlightQueue.size > 0 ||
1342
+ this.syncInFlightQueueInverted.size > 0 ||
1343
+ this.syncInFlight.size > 0
1344
+ );
1345
+ }
1346
+
916
1347
  private clearSyncProcessKey(key: SyncableKey) {
917
1348
  const inflight = this.syncInFlightQueue.get(key);
918
1349
  if (inflight) {
@@ -941,17 +1372,29 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
941
1372
  }
942
1373
  }
943
1374
 
944
- private getKnownAliases(hash: string): SyncableKey[] {
945
- const aliases = new Set<SyncableKey>([hash]);
946
- for (const key of [
947
- ...this.syncInFlightQueue.keys(),
948
- ...[...this.syncInFlight.values()].flatMap((map) => [...map.keys()]),
949
- ]) {
1375
+ private forEachKnownAlias(
1376
+ hash: string,
1377
+ callback: (key: SyncableKey) => void,
1378
+ ): void {
1379
+ callback(hash);
1380
+ if (this.syncInFlightQueue.size === 0 && this.syncInFlight.size === 0) {
1381
+ return;
1382
+ }
1383
+ for (const key of this.syncInFlightQueue.keys()) {
950
1384
  if (typeof key === "bigint" && this.coordinateToHash.get(key) === hash) {
951
- aliases.add(key);
1385
+ callback(key);
1386
+ }
1387
+ }
1388
+ for (const map of this.syncInFlight.values()) {
1389
+ for (const key of map.keys()) {
1390
+ if (
1391
+ typeof key === "bigint" &&
1392
+ this.coordinateToHash.get(key) === hash
1393
+ ) {
1394
+ callback(key);
1395
+ }
952
1396
  }
953
1397
  }
954
- return [...aliases];
955
1398
  }
956
1399
 
957
1400
  private clearSyncInFlightForPeer(publicKeyHash: string, hash: string) {
@@ -959,7 +1402,32 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
959
1402
  if (!map) {
960
1403
  return;
961
1404
  }
962
- for (const key of this.getKnownAliases(hash)) {
1405
+ this.forEachKnownAlias(hash, (key) => map.delete(key));
1406
+ if (map.size === 0) {
1407
+ this.syncInFlight.delete(publicKeyHash);
1408
+ }
1409
+ }
1410
+
1411
+ private clearSyncInFlightForPeerHashes(
1412
+ publicKeyHash: string,
1413
+ hashes: string[],
1414
+ ) {
1415
+ const map = this.syncInFlight.get(publicKeyHash);
1416
+ if (!map || hashes.length === 0) {
1417
+ return;
1418
+ }
1419
+ const keys = new Set<SyncableKey>(hashes);
1420
+ const hashSet = new Set(hashes);
1421
+ for (const key of map.keys()) {
1422
+ if (typeof key !== "bigint") {
1423
+ continue;
1424
+ }
1425
+ const hash = this.coordinateToHash.get(key);
1426
+ if (hash != null && hashSet.has(hash)) {
1427
+ keys.add(key);
1428
+ }
1429
+ }
1430
+ for (const key of keys) {
963
1431
  map.delete(key);
964
1432
  }
965
1433
  if (map.size === 0) {
@@ -968,7 +1436,33 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
968
1436
  }
969
1437
 
970
1438
  private clearSyncProcess(hash: string) {
971
- for (const key of this.getKnownAliases(hash)) {
1439
+ this.forEachKnownAlias(hash, (key) => this.clearSyncProcessKey(key));
1440
+ }
1441
+
1442
+ private clearSyncProcesses(hashes: string[]) {
1443
+ if (hashes.length === 0) {
1444
+ return;
1445
+ }
1446
+ const keys = new Set<SyncableKey>(hashes);
1447
+ const hashSet = new Set(hashes);
1448
+ const maybeAddAlias = (key: SyncableKey) => {
1449
+ if (typeof key !== "bigint") {
1450
+ return;
1451
+ }
1452
+ const hash = this.coordinateToHash.get(key);
1453
+ if (hash != null && hashSet.has(hash)) {
1454
+ keys.add(key);
1455
+ }
1456
+ };
1457
+ for (const key of this.syncInFlightQueue.keys()) {
1458
+ maybeAddAlias(key);
1459
+ }
1460
+ for (const map of this.syncInFlight.values()) {
1461
+ for (const key of map.keys()) {
1462
+ maybeAddAlias(key);
1463
+ }
1464
+ }
1465
+ for (const key of keys) {
972
1466
  this.clearSyncProcessKey(key);
973
1467
  }
974
1468
  }
@@ -979,6 +1473,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
979
1473
  }
980
1474
  private clearSyncProcessPublicKeyHash(publicKeyHash: string) {
981
1475
  this.syncInFlight.delete(publicKeyHash);
1476
+ this.recentlySentExchangeHeads.delete(publicKeyHash);
982
1477
  const map = this.syncInFlightQueueInverted.get(publicKeyHash);
983
1478
  if (map) {
984
1479
  for (const hash of map) {