@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
@@ -37,7 +37,7 @@ import { Cache } from "@peerbit/cache";
37
37
  import { Compare, IntegerCompare, Or, } from "@peerbit/indexer-interface";
38
38
  import { Entry, Log } from "@peerbit/log";
39
39
  import { CONVERGENCE_MESSAGE_PRIORITY, SilentDelivery, } from "@peerbit/stream-interface";
40
- import { EntryWithRefs, createExchangeHeadsMessages, } from "../exchange-heads.js";
40
+ import { EntryWithRefs, createExchangeHeadsMessages, createRawExchangeHeadsMessages, } from "../exchange-heads.js";
41
41
  import { TransportMessage } from "../message.js";
42
42
  import { emitSyncProfileDuration, syncProfileStart } from "./profile.js";
43
43
  let RequestMaybeSync = (() => {
@@ -160,10 +160,99 @@ let ConfirmEntriesMessage = (() => {
160
160
  return ConfirmEntriesMessage = _classThis;
161
161
  })();
162
162
  export { ConfirmEntriesMessage };
163
- const getHashesFromSymbols = async (symbols, entryIndex, coordinateToHash) => {
163
+ export const SIMPLE_SYNC_RAW_EXCHANGE_HEADS_CAPABILITY = 1;
164
+ let ResponseMaybeSyncCapabilities = (() => {
165
+ let _classDecorators = [variant([0, 8])];
166
+ let _classDescriptor;
167
+ let _classExtraInitializers = [];
168
+ let _classThis;
169
+ let _classSuper = TransportMessage;
170
+ let _hashes_decorators;
171
+ let _hashes_initializers = [];
172
+ let _hashes_extraInitializers = [];
173
+ let _capabilities_decorators;
174
+ let _capabilities_initializers = [];
175
+ let _capabilities_extraInitializers = [];
176
+ var ResponseMaybeSyncCapabilities = class extends _classSuper {
177
+ static { _classThis = this; }
178
+ static {
179
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
180
+ _hashes_decorators = [field({ type: vec("string") })];
181
+ _capabilities_decorators = [field({ type: "u32" })];
182
+ __esDecorate(null, null, _hashes_decorators, { kind: "field", name: "hashes", static: false, private: false, access: { has: obj => "hashes" in obj, get: obj => obj.hashes, set: (obj, value) => { obj.hashes = value; } }, metadata: _metadata }, _hashes_initializers, _hashes_extraInitializers);
183
+ __esDecorate(null, null, _capabilities_decorators, { kind: "field", name: "capabilities", static: false, private: false, access: { has: obj => "capabilities" in obj, get: obj => obj.capabilities, set: (obj, value) => { obj.capabilities = value; } }, metadata: _metadata }, _capabilities_initializers, _capabilities_extraInitializers);
184
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
185
+ ResponseMaybeSyncCapabilities = _classThis = _classDescriptor.value;
186
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
187
+ __runInitializers(_classThis, _classExtraInitializers);
188
+ }
189
+ hashes = __runInitializers(this, _hashes_initializers, void 0);
190
+ capabilities = (__runInitializers(this, _hashes_extraInitializers), __runInitializers(this, _capabilities_initializers, void 0));
191
+ constructor(props) {
192
+ super();
193
+ __runInitializers(this, _capabilities_extraInitializers);
194
+ this.hashes = props.hashes;
195
+ this.capabilities =
196
+ props.capabilities ?? SIMPLE_SYNC_RAW_EXCHANGE_HEADS_CAPABILITY;
197
+ }
198
+ };
199
+ return ResponseMaybeSyncCapabilities = _classThis;
200
+ })();
201
+ export { ResponseMaybeSyncCapabilities };
202
+ let RequestMaybeSyncCoordinateCapabilities = (() => {
203
+ let _classDecorators = [variant([0, 9])];
204
+ let _classDescriptor;
205
+ let _classExtraInitializers = [];
206
+ let _classThis;
207
+ let _classSuper = TransportMessage;
208
+ let _hashNumbers_decorators;
209
+ let _hashNumbers_initializers = [];
210
+ let _hashNumbers_extraInitializers = [];
211
+ let _capabilities_decorators;
212
+ let _capabilities_initializers = [];
213
+ let _capabilities_extraInitializers = [];
214
+ var RequestMaybeSyncCoordinateCapabilities = class extends _classSuper {
215
+ static { _classThis = this; }
216
+ static {
217
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
218
+ _hashNumbers_decorators = [field({ type: vec("u64") })];
219
+ _capabilities_decorators = [field({ type: "u32" })];
220
+ __esDecorate(null, null, _hashNumbers_decorators, { kind: "field", name: "hashNumbers", static: false, private: false, access: { has: obj => "hashNumbers" in obj, get: obj => obj.hashNumbers, set: (obj, value) => { obj.hashNumbers = value; } }, metadata: _metadata }, _hashNumbers_initializers, _hashNumbers_extraInitializers);
221
+ __esDecorate(null, null, _capabilities_decorators, { kind: "field", name: "capabilities", static: false, private: false, access: { has: obj => "capabilities" in obj, get: obj => obj.capabilities, set: (obj, value) => { obj.capabilities = value; } }, metadata: _metadata }, _capabilities_initializers, _capabilities_extraInitializers);
222
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
223
+ RequestMaybeSyncCoordinateCapabilities = _classThis = _classDescriptor.value;
224
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
225
+ __runInitializers(_classThis, _classExtraInitializers);
226
+ }
227
+ hashNumbers = __runInitializers(this, _hashNumbers_initializers, void 0);
228
+ capabilities = (__runInitializers(this, _hashNumbers_extraInitializers), __runInitializers(this, _capabilities_initializers, void 0));
229
+ constructor(props) {
230
+ super();
231
+ __runInitializers(this, _capabilities_extraInitializers);
232
+ this.hashNumbers = props.hashNumbers;
233
+ this.capabilities =
234
+ props.capabilities ?? SIMPLE_SYNC_RAW_EXCHANGE_HEADS_CAPABILITY;
235
+ }
236
+ };
237
+ return RequestMaybeSyncCoordinateCapabilities = _classThis;
238
+ })();
239
+ export { RequestMaybeSyncCoordinateCapabilities };
240
+ const canReceiveRawExchangeHeads = (message) => (message instanceof ResponseMaybeSyncCapabilities ||
241
+ message instanceof RequestMaybeSyncCoordinateCapabilities) &&
242
+ (message.capabilities & SIMPLE_SYNC_RAW_EXCHANGE_HEADS_CAPABILITY) !== 0;
243
+ const getHashesFromSymbols = async (symbols, entryIndex, coordinateToHash, resolveHashesForSymbols, resolveHashListForSymbols) => {
164
244
  let queries = [];
165
245
  let batchSize = 128; // TODO arg
166
246
  let results = new Set();
247
+ let missingSymbols = [];
248
+ const addMissingUnlessCached = (symbol) => {
249
+ const fromCache = coordinateToHash.get(symbol);
250
+ if (fromCache) {
251
+ results.add(fromCache);
252
+ return;
253
+ }
254
+ missingSymbols.push(symbol);
255
+ };
167
256
  const handleBatch = async (end = false) => {
168
257
  if (queries.length >= batchSize || (end && queries.length > 0)) {
169
258
  const entries = await entryIndex
@@ -176,16 +265,63 @@ const getHashesFromSymbols = async (symbols, entryIndex, coordinateToHash) => {
176
265
  }
177
266
  }
178
267
  };
179
- for (let i = 0; i < symbols.length; i++) {
180
- const fromCache = coordinateToHash.get(symbols[i]);
181
- if (fromCache) {
182
- results.add(fromCache);
183
- continue;
268
+ if (resolveHashListForSymbols) {
269
+ const resolvedHashes = await resolveHashListForSymbols(symbols);
270
+ if (resolvedHashes) {
271
+ const resolvedHashList = Array.isArray(resolvedHashes)
272
+ ? resolvedHashes
273
+ : [...resolvedHashes];
274
+ let mergedHashes;
275
+ for (const symbol of symbols) {
276
+ const fromCache = coordinateToHash.get(symbol);
277
+ if (fromCache) {
278
+ mergedHashes ??= new Set(resolvedHashList);
279
+ mergedHashes.add(fromCache);
280
+ }
281
+ }
282
+ return mergedHashes ?? resolvedHashList;
283
+ }
284
+ }
285
+ if (resolveHashesForSymbols) {
286
+ const resolved = await resolveHashesForSymbols(symbols);
287
+ if (resolved) {
288
+ for (const symbol of symbols) {
289
+ const hashes = resolved.get(symbol);
290
+ if (!hashes) {
291
+ addMissingUnlessCached(symbol);
292
+ continue;
293
+ }
294
+ let singleHash;
295
+ let count = 0;
296
+ for (const hash of hashes) {
297
+ results.add(hash);
298
+ singleHash = hash;
299
+ count += 1;
300
+ }
301
+ if (count === 0) {
302
+ addMissingUnlessCached(symbol);
303
+ }
304
+ else if (count === 1) {
305
+ coordinateToHash.add(symbol, singleHash);
306
+ }
307
+ }
308
+ }
309
+ else {
310
+ for (const symbol of symbols) {
311
+ addMissingUnlessCached(symbol);
312
+ }
313
+ }
314
+ }
315
+ else {
316
+ for (const symbol of symbols) {
317
+ addMissingUnlessCached(symbol);
184
318
  }
319
+ }
320
+ for (const symbol of missingSymbols) {
185
321
  const matchQuery = new IntegerCompare({
186
322
  key: "hashNumber",
187
323
  compare: Compare.Equal,
188
- value: symbols[i],
324
+ value: symbol,
189
325
  });
190
326
  queries.push(matchQuery);
191
327
  await handleBatch();
@@ -193,6 +329,7 @@ const getHashesFromSymbols = async (symbols, entryIndex, coordinateToHash) => {
193
329
  await handleBatch(true);
194
330
  return results;
195
331
  };
332
+ const hashLookupResultSize = (hashes) => Array.isArray(hashes) ? hashes.length : hashes.size;
196
333
  const DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS = 30_000;
197
334
  const DEFAULT_CONVERGENT_RETRY_INTERVALS_MS = [0, 1_000, 3_000, 7_000];
198
335
  const DEFAULT_BEST_EFFORT_RETRY_INTERVALS_MS = [0];
@@ -207,6 +344,8 @@ export const SYNC_MESSAGE_PRIORITY = CONVERGENCE_MESSAGE_PRIORITY;
207
344
  // pubsub stream warmup). Keep it coarse-grained so we do not hammer the network under
208
345
  // large historical backfills.
209
346
  const SIMPLE_SYNC_RETRY_AFTER_MS = 10_000;
347
+ const EXCHANGE_HEAD_RESPONSE_DEDUPE_TTL_MS = SIMPLE_SYNC_RETRY_AFTER_MS - 1_000;
348
+ const RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS = 30_000;
210
349
  const createDeferred = () => {
211
350
  let resolve;
212
351
  let reject;
@@ -226,7 +365,12 @@ export class SimpleSyncronizer {
226
365
  log;
227
366
  entryIndex;
228
367
  coordinateToHash;
368
+ resolveHashesForSymbols;
369
+ resolveHashListForSymbols;
229
370
  syncOptions;
371
+ isEntryRecentlyKnownByPeer;
372
+ sendRawExchangeHeads;
373
+ recentlySentExchangeHeads;
230
374
  repairSessionCounter;
231
375
  repairSessions;
232
376
  // Syncing and dedeplucation work
@@ -240,7 +384,12 @@ export class SimpleSyncronizer {
240
384
  this.log = properties.log;
241
385
  this.entryIndex = properties.entryIndex;
242
386
  this.coordinateToHash = properties.coordinateToHash;
387
+ this.resolveHashesForSymbols = properties.resolveHashesForSymbols;
388
+ this.resolveHashListForSymbols = properties.resolveHashListForSymbols;
243
389
  this.syncOptions = properties.sync;
390
+ this.isEntryRecentlyKnownByPeer = properties.isEntryRecentlyKnownByPeer;
391
+ this.sendRawExchangeHeads = properties.sendRawExchangeHeads;
392
+ this.recentlySentExchangeHeads = new Map();
244
393
  this.repairSessionCounter = 0;
245
394
  this.repairSessions = new Map();
246
395
  }
@@ -303,6 +452,37 @@ export class SimpleSyncronizer {
303
452
  }
304
453
  return out;
305
454
  }
455
+ filterRecentlySentExchangeHeads(hashes, peer) {
456
+ const peerHash = peer.hashcode();
457
+ const now = Date.now();
458
+ let recentlySent = this.recentlySentExchangeHeads.get(peerHash);
459
+ if (!recentlySent) {
460
+ recentlySent = new Map();
461
+ this.recentlySentExchangeHeads.set(peerHash, recentlySent);
462
+ }
463
+ for (const [hash, timestamp] of recentlySent) {
464
+ if (now - timestamp > EXCHANGE_HEAD_RESPONSE_DEDUPE_TTL_MS) {
465
+ recentlySent.delete(hash);
466
+ }
467
+ }
468
+ const out = [];
469
+ const seen = new Set();
470
+ for (const hash of hashes) {
471
+ if (seen.has(hash)) {
472
+ continue;
473
+ }
474
+ seen.add(hash);
475
+ if (recentlySent.has(hash)) {
476
+ continue;
477
+ }
478
+ if (this.isEntryRecentlyKnownByPeer?.(hash, peerHash, RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS)) {
479
+ continue;
480
+ }
481
+ recentlySent.set(hash, now);
482
+ out.push(hash);
483
+ }
484
+ return out;
485
+ }
306
486
  isRepairSessionComplete(session) {
307
487
  for (const state of session.targets.values()) {
308
488
  if (state.unresolved.size > 0) {
@@ -348,12 +528,22 @@ export class SimpleSyncronizer {
348
528
  return;
349
529
  }
350
530
  for (const state of session.targets.values()) {
351
- for (const hash of [...state.unresolved]) {
352
- if (await this.log.has(hash)) {
353
- state.unresolved.delete(hash);
354
- }
531
+ const resolved = typeof this.log.hasMany === "function"
532
+ ? await this.log.hasMany(state.unresolved)
533
+ : await this.getExistingRepairHashes(state.unresolved);
534
+ for (const hash of resolved) {
535
+ state.unresolved.delete(hash);
536
+ }
537
+ }
538
+ }
539
+ async getExistingRepairHashes(hashes) {
540
+ const resolved = new Set();
541
+ for (const hash of hashes) {
542
+ if (await this.log.has(hash)) {
543
+ resolved.add(hash);
355
544
  }
356
545
  }
546
+ return resolved;
357
547
  }
358
548
  markRepairSessionResolvedHashes(hashes) {
359
549
  if (hashes.length === 0 || this.repairSessions.size === 0) {
@@ -370,6 +560,19 @@ export class SimpleSyncronizer {
370
560
  }
371
561
  }
372
562
  }
563
+ markRepairSessionResolvedHash(hash) {
564
+ if (this.repairSessions.size === 0) {
565
+ return;
566
+ }
567
+ for (const [sessionId, session] of this.repairSessions) {
568
+ for (const state of session.targets.values()) {
569
+ state.unresolved.delete(hash);
570
+ }
571
+ if (this.isRepairSessionComplete(session)) {
572
+ this.finalizeRepairSession(sessionId, true);
573
+ }
574
+ }
575
+ }
373
576
  async runRepairSession(sessionId) {
374
577
  const session = this.repairSessions.get(sessionId);
375
578
  if (!session) {
@@ -535,9 +738,15 @@ export class SimpleSyncronizer {
535
738
  };
536
739
  }
537
740
  async onMaybeMissingEntries(properties) {
741
+ await this.onMaybeMissingHashes({
742
+ hashes: this.getPrioritizedHashes(properties.entries),
743
+ targets: properties.targets,
744
+ });
745
+ }
746
+ async onMaybeMissingHashes(properties) {
538
747
  const profile = this.syncOptions?.profile;
539
748
  const startedAt = syncProfileStart(profile);
540
- const hashes = this.getPrioritizedHashes(properties.entries);
749
+ const hashes = [...properties.hashes];
541
750
  const chunks = this.chunk(hashes, this.maxHashesPerMessage);
542
751
  try {
543
752
  await chunks.reduce((promise, chunk) => promise.then(() => this.rpc.send(new RequestMaybeSync({ hashes: chunk }), {
@@ -559,69 +768,92 @@ export class SimpleSyncronizer {
559
768
  }
560
769
  }
561
770
  }
771
+ /**
772
+ * Ship exchange heads to one peer: the fused native raw path when the
773
+ * peer advertised raw capability and the shared log provided a fused
774
+ * sender, otherwise the TS message path (raw or plain by capability).
775
+ * Returns the number of messages sent and whether the fused path ran.
776
+ */
777
+ async shipExchangeHeads(hashes, to, canReceiveRaw) {
778
+ if (canReceiveRaw && this.sendRawExchangeHeads) {
779
+ const sentMessages = await this.sendRawExchangeHeads(hashes, [
780
+ to.hashcode(),
781
+ ]);
782
+ if (sentMessages !== undefined) {
783
+ return { messages: sentMessages, fused: true };
784
+ }
785
+ }
786
+ let messages = 0;
787
+ const messageGenerator = canReceiveRaw
788
+ ? createRawExchangeHeadsMessages(this.log, hashes, this.syncOptions?.profile)
789
+ : createExchangeHeadsMessages(this.log, hashes);
790
+ for await (const message of messageGenerator) {
791
+ messages += 1;
792
+ await this.rpc.send(message, {
793
+ mode: new SilentDelivery({ to: [to], redundancy: 1 }),
794
+ });
795
+ }
796
+ return { messages, fused: false };
797
+ }
562
798
  async onMessage(msg, context) {
563
799
  const from = context.from;
564
800
  if (msg instanceof RequestMaybeSync) {
565
801
  await this.queueSync(msg.hashes, from);
566
802
  return true;
567
803
  }
568
- else if (msg instanceof ResponseMaybeSync) {
804
+ else if (msg instanceof ResponseMaybeSync ||
805
+ msg instanceof ResponseMaybeSyncCapabilities) {
569
806
  // TODO perhaps send less messages to more receivers for performance reasons?
570
807
  // TODO wait for previous send to target before trying to send more?
571
808
  const profile = this.syncOptions?.profile;
572
809
  const startedAt = syncProfileStart(profile);
810
+ const hashes = this.filterRecentlySentExchangeHeads(msg.hashes, from);
573
811
  let messages = 0;
812
+ let fused = false;
574
813
  try {
575
- for await (const message of createExchangeHeadsMessages(this.log, msg.hashes)) {
576
- messages += 1;
577
- await this.rpc.send(message, {
578
- mode: new SilentDelivery({ to: [context.from], redundancy: 1 }),
579
- });
580
- }
814
+ ({ messages, fused } = await this.shipExchangeHeads(hashes, context.from, canReceiveRawExchangeHeads(msg)));
581
815
  }
582
816
  finally {
583
817
  if (profile) {
584
818
  emitSyncProfileDuration(profile, startedAt, {
585
819
  name: "simple.exchangeHeads",
586
- entries: msg.hashes.length,
820
+ entries: hashes.length,
587
821
  messages,
588
822
  targets: 1,
589
- details: { source: "responseMaybeSync" },
823
+ details: { source: "responseMaybeSync", fused },
590
824
  });
591
825
  }
592
826
  }
593
827
  return true;
594
828
  }
595
- else if (msg instanceof RequestMaybeSyncCoordinate) {
829
+ else if (msg instanceof RequestMaybeSyncCoordinate ||
830
+ msg instanceof RequestMaybeSyncCoordinateCapabilities) {
596
831
  const profile = this.syncOptions?.profile;
597
832
  const lookupStartedAt = syncProfileStart(profile);
598
- const hashes = await getHashesFromSymbols(msg.hashNumbers, this.entryIndex, this.coordinateToHash);
833
+ const hashes = await getHashesFromSymbols(msg.hashNumbers, this.entryIndex, this.coordinateToHash, this.resolveHashesForSymbols, this.resolveHashListForSymbols);
599
834
  if (profile) {
600
835
  emitSyncProfileDuration(profile, lookupStartedAt, {
601
836
  name: "simple.coordinateLookup",
602
- entries: hashes.size,
837
+ entries: hashLookupResultSize(hashes),
603
838
  symbols: msg.hashNumbers.length,
604
839
  });
605
840
  }
606
841
  const exchangeStartedAt = syncProfileStart(profile);
842
+ const hashesToSend = this.filterRecentlySentExchangeHeads(hashes, from);
607
843
  let messages = 0;
844
+ let fused = false;
608
845
  try {
609
- for await (const message of createExchangeHeadsMessages(this.log, hashes)) {
610
- messages += 1;
611
- await this.rpc.send(message, {
612
- mode: new SilentDelivery({ to: [context.from], redundancy: 1 }),
613
- // dont set priority 1 here because this will block other messages that should higher priority
614
- });
615
- }
846
+ // dont set priority 1 here because this will block other messages that should higher priority
847
+ ({ messages, fused } = await this.shipExchangeHeads(hashesToSend, context.from, canReceiveRawExchangeHeads(msg)));
616
848
  }
617
849
  finally {
618
850
  if (profile) {
619
851
  emitSyncProfileDuration(profile, exchangeStartedAt, {
620
852
  name: "simple.exchangeHeads",
621
- entries: hashes.size,
853
+ entries: hashesToSend.length,
622
854
  messages,
623
855
  targets: 1,
624
- details: { source: "requestMaybeSyncCoordinate" },
856
+ details: { source: "requestMaybeSyncCoordinate", fused },
625
857
  });
626
858
  }
627
859
  }
@@ -632,47 +864,105 @@ export class SimpleSyncronizer {
632
864
  }
633
865
  }
634
866
  onReceivedEntries(properties) {
635
- const resolvedHashes = [];
636
- for (const entry of properties.entries) {
637
- resolvedHashes.push(entry.entry.hash);
638
- this.clearSyncInFlightForPeer(properties.from.hashcode(), entry.entry.hash);
639
- }
640
- this.markRepairSessionResolvedHashes(resolvedHashes);
867
+ return this.onReceivedEntryHashes({
868
+ hashes: properties.entries.map((entry) => entry.entry.hash),
869
+ from: properties.from,
870
+ });
871
+ }
872
+ onReceivedEntryHashes(properties) {
873
+ this.clearSyncInFlightForPeerHashes(properties.from.hashcode(), properties.hashes);
874
+ this.markRepairSessionResolvedHashes(properties.hashes);
641
875
  }
642
876
  async queueSync(keys, from, options) {
643
877
  const requestHashes = [];
644
878
  const profile = this.syncOptions?.profile;
645
879
  const startedAt = syncProfileStart(profile);
880
+ const resolveKnownStartedAt = syncProfileStart(profile);
881
+ const knownKeys = options?.skipCheck === true
882
+ ? undefined
883
+ : await this.resolveKnownSyncKeys(keys);
884
+ if (profile) {
885
+ emitSyncProfileDuration(profile, resolveKnownStartedAt, {
886
+ name: "simple.queueSync.resolveKnown",
887
+ entries: keys.length,
888
+ count: knownKeys?.keys.size ?? 0,
889
+ details: {
890
+ checkedCoordinates: knownKeys?.checkedCoordinates === true,
891
+ checkedHashes: knownKeys?.checkedHashes === true,
892
+ skipCheck: options?.skipCheck === true,
893
+ },
894
+ });
895
+ }
896
+ const fromHash = from.hashcode();
897
+ let queuedHashAliases;
898
+ const getQueuedSyncKeyForBatch = (key) => {
899
+ if (this.syncInFlightQueue.has(key)) {
900
+ return key;
901
+ }
902
+ if (typeof key === "string") {
903
+ if (!queuedHashAliases) {
904
+ queuedHashAliases = new Map();
905
+ for (const queuedKey of this.syncInFlightQueue.keys()) {
906
+ if (typeof queuedKey !== "bigint") {
907
+ continue;
908
+ }
909
+ const hash = this.coordinateToHash.get(queuedKey);
910
+ if (hash) {
911
+ queuedHashAliases.set(hash, queuedKey);
912
+ }
913
+ }
914
+ }
915
+ return queuedHashAliases.get(key);
916
+ }
917
+ const hash = this.coordinateToHash.get(key);
918
+ return hash && this.syncInFlightQueue.has(hash) ? hash : undefined;
919
+ };
646
920
  try {
921
+ const loopStartedAt = syncProfileStart(profile);
647
922
  for (const key of keys) {
648
- const coordinateOrHash = this.getQueuedSyncKey(key) ?? key;
923
+ const coordinateOrHash = getQueuedSyncKeyForBatch(key) ?? key;
649
924
  const inFlight = this.syncInFlightQueue.get(coordinateOrHash);
650
925
  if (inFlight) {
651
- if (!inFlight.find((x) => x.hashcode() === from.hashcode())) {
926
+ if (!inFlight.find((x) => x.hashcode() === fromHash)) {
652
927
  inFlight.push(from);
653
- let inverted = this.syncInFlightQueueInverted.get(from.hashcode());
928
+ let inverted = this.syncInFlightQueueInverted.get(fromHash);
654
929
  if (!inverted) {
655
930
  inverted = new Set();
656
- this.syncInFlightQueueInverted.set(from.hashcode(), inverted);
931
+ this.syncInFlightQueueInverted.set(fromHash, inverted);
657
932
  }
658
933
  inverted.add(coordinateOrHash);
659
934
  }
660
935
  }
661
936
  else if (options?.skipCheck ||
662
- !(await this.checkHasCoordinateOrHash(coordinateOrHash))) {
937
+ !(await this.checkHasCoordinateOrHash(coordinateOrHash, knownKeys))) {
663
938
  // Track the initial sender so we can retry if the first request is lost.
664
939
  this.syncInFlightQueue.set(coordinateOrHash, [from]);
665
- let inverted = this.syncInFlightQueueInverted.get(from.hashcode());
940
+ let inverted = this.syncInFlightQueueInverted.get(fromHash);
666
941
  if (!inverted) {
667
942
  inverted = new Set();
668
- this.syncInFlightQueueInverted.set(from.hashcode(), inverted);
943
+ this.syncInFlightQueueInverted.set(fromHash, inverted);
669
944
  }
670
945
  inverted.add(coordinateOrHash);
671
946
  requestHashes.push(coordinateOrHash); // request immediately (first time we have seen this hash)
947
+ if (queuedHashAliases &&
948
+ typeof coordinateOrHash === "bigint") {
949
+ const hash = this.coordinateToHash.get(coordinateOrHash);
950
+ if (hash) {
951
+ queuedHashAliases.set(hash, coordinateOrHash);
952
+ }
953
+ }
672
954
  }
673
955
  }
956
+ if (profile) {
957
+ emitSyncProfileDuration(profile, loopStartedAt, {
958
+ name: "simple.queueSync.plan",
959
+ entries: keys.length,
960
+ count: requestHashes.length,
961
+ targets: 1,
962
+ });
963
+ }
674
964
  requestHashes.length > 0 &&
675
- (await this.requestSync(requestHashes, [from.hashcode()]));
965
+ (await this.requestSync(requestHashes, [fromHash]));
676
966
  }
677
967
  finally {
678
968
  if (profile) {
@@ -726,7 +1016,11 @@ export class SimpleSyncronizer {
726
1016
  const chunks = this.chunk(coordinateHashes, this.maxCoordinatesPerMessage);
727
1017
  coordinateMessages = chunks.length;
728
1018
  for (const chunk of chunks) {
729
- await this.rpc.send(new RequestMaybeSyncCoordinate({ hashNumbers: chunk }), {
1019
+ await this.rpc.send(this.syncOptions?.rawExchangeHeads
1020
+ ? new RequestMaybeSyncCoordinateCapabilities({
1021
+ hashNumbers: chunk,
1022
+ })
1023
+ : new RequestMaybeSyncCoordinate({ hashNumbers: chunk }), {
730
1024
  mode: new SilentDelivery({ to, redundancy: 1 }),
731
1025
  priority: SYNC_MESSAGE_PRIORITY,
732
1026
  });
@@ -736,7 +1030,9 @@ export class SimpleSyncronizer {
736
1030
  const chunks = this.chunk(stringHashes, this.maxHashesPerMessage);
737
1031
  stringMessages = chunks.length;
738
1032
  for (const chunk of chunks) {
739
- await this.rpc.send(new ResponseMaybeSync({ hashes: chunk }), {
1033
+ await this.rpc.send(this.syncOptions?.rawExchangeHeads
1034
+ ? new ResponseMaybeSyncCapabilities({ hashes: chunk })
1035
+ : new ResponseMaybeSync({ hashes: chunk }), {
740
1036
  mode: new SilentDelivery({ to, redundancy: 1 }),
741
1037
  priority: SYNC_MESSAGE_PRIORITY,
742
1038
  });
@@ -758,7 +1054,58 @@ export class SimpleSyncronizer {
758
1054
  }
759
1055
  }
760
1056
  }
761
- async checkHasCoordinateOrHash(key) {
1057
+ async resolveKnownSyncKeys(keys) {
1058
+ const hashes = [];
1059
+ const coordinates = [];
1060
+ for (const key of keys) {
1061
+ if (typeof key === "bigint") {
1062
+ coordinates.push(key);
1063
+ }
1064
+ else {
1065
+ hashes.push(key);
1066
+ }
1067
+ }
1068
+ const known = {
1069
+ keys: new Set(),
1070
+ checkedCoordinates: false,
1071
+ checkedHashes: false,
1072
+ };
1073
+ if (hashes.length > 0) {
1074
+ for (const hash of await this.log.hasMany(hashes)) {
1075
+ known.keys.add(hash);
1076
+ }
1077
+ known.checkedHashes = true;
1078
+ }
1079
+ if (coordinates.length > 0 && this.resolveHashesForSymbols) {
1080
+ const resolved = await this.resolveHashesForSymbols(coordinates);
1081
+ if (resolved) {
1082
+ for (const coordinate of coordinates) {
1083
+ const hashes = resolved.get(coordinate);
1084
+ if (!hashes) {
1085
+ continue;
1086
+ }
1087
+ for (const _hash of hashes) {
1088
+ known.keys.add(coordinate);
1089
+ break;
1090
+ }
1091
+ }
1092
+ known.checkedCoordinates = true;
1093
+ }
1094
+ }
1095
+ return known.checkedCoordinates || known.checkedHashes ? known : undefined;
1096
+ }
1097
+ async checkHasCoordinateOrHash(key, knownKeys) {
1098
+ if (knownKeys) {
1099
+ if (knownKeys.keys.has(key)) {
1100
+ return true;
1101
+ }
1102
+ if (typeof key === "bigint" && knownKeys.checkedCoordinates) {
1103
+ return false;
1104
+ }
1105
+ if (typeof key === "string" && knownKeys.checkedHashes) {
1106
+ return false;
1107
+ }
1108
+ }
762
1109
  return typeof key === "bigint"
763
1110
  ? (await this.entryIndex.count({ query: { hashNumber: key } })) > 0
764
1111
  : this.log.has(key);
@@ -834,18 +1181,49 @@ export class SimpleSyncronizer {
834
1181
  this.syncInFlightQueue.clear();
835
1182
  this.syncInFlightQueueInverted.clear();
836
1183
  this.syncInFlight.clear();
1184
+ this.recentlySentExchangeHeads.clear();
837
1185
  for (const sessionId of [...this.repairSessions.keys()]) {
838
1186
  this.finalizeRepairSession(sessionId, false);
839
1187
  }
840
1188
  clearTimeout(this.syncMoreInterval);
841
1189
  }
842
1190
  onEntryAdded(entry) {
843
- this.clearSyncProcess(entry.hash);
844
- this.markRepairSessionResolvedHashes([entry.hash]);
1191
+ this.onEntryAddedHash(entry.hash);
1192
+ }
1193
+ onEntryAddedHashes(hashes) {
1194
+ if (hashes.length === 0 || !this.hasEntryAddedState()) {
1195
+ return;
1196
+ }
1197
+ this.clearSyncProcesses(hashes);
1198
+ this.markRepairSessionResolvedHashes(hashes);
1199
+ }
1200
+ onEntryAddedHash(hash) {
1201
+ if (!this.hasEntryAddedState()) {
1202
+ return;
1203
+ }
1204
+ this.clearSyncProcess(hash);
1205
+ this.markRepairSessionResolvedHash(hash);
845
1206
  }
846
1207
  onEntryRemoved(hash) {
1208
+ if (!this.hasSyncProcessState()) {
1209
+ return;
1210
+ }
847
1211
  return this.clearSyncProcess(hash);
848
1212
  }
1213
+ onEntryRemovedHashes(hashes) {
1214
+ if (hashes.length === 0 || !this.hasSyncProcessState()) {
1215
+ return;
1216
+ }
1217
+ return this.clearSyncProcesses(hashes);
1218
+ }
1219
+ hasEntryAddedState() {
1220
+ return this.hasSyncProcessState() || this.repairSessions.size > 0;
1221
+ }
1222
+ hasSyncProcessState() {
1223
+ return (this.syncInFlightQueue.size > 0 ||
1224
+ this.syncInFlightQueueInverted.size > 0 ||
1225
+ this.syncInFlight.size > 0);
1226
+ }
849
1227
  clearSyncProcessKey(key) {
850
1228
  const inflight = this.syncInFlightQueue.get(key);
851
1229
  if (inflight) {
@@ -870,24 +1248,52 @@ export class SimpleSyncronizer {
870
1248
  }
871
1249
  }
872
1250
  }
873
- getKnownAliases(hash) {
874
- const aliases = new Set([hash]);
875
- for (const key of [
876
- ...this.syncInFlightQueue.keys(),
877
- ...[...this.syncInFlight.values()].flatMap((map) => [...map.keys()]),
878
- ]) {
1251
+ forEachKnownAlias(hash, callback) {
1252
+ callback(hash);
1253
+ if (this.syncInFlightQueue.size === 0 && this.syncInFlight.size === 0) {
1254
+ return;
1255
+ }
1256
+ for (const key of this.syncInFlightQueue.keys()) {
879
1257
  if (typeof key === "bigint" && this.coordinateToHash.get(key) === hash) {
880
- aliases.add(key);
1258
+ callback(key);
1259
+ }
1260
+ }
1261
+ for (const map of this.syncInFlight.values()) {
1262
+ for (const key of map.keys()) {
1263
+ if (typeof key === "bigint" &&
1264
+ this.coordinateToHash.get(key) === hash) {
1265
+ callback(key);
1266
+ }
881
1267
  }
882
1268
  }
883
- return [...aliases];
884
1269
  }
885
1270
  clearSyncInFlightForPeer(publicKeyHash, hash) {
886
1271
  const map = this.syncInFlight.get(publicKeyHash);
887
1272
  if (!map) {
888
1273
  return;
889
1274
  }
890
- for (const key of this.getKnownAliases(hash)) {
1275
+ this.forEachKnownAlias(hash, (key) => map.delete(key));
1276
+ if (map.size === 0) {
1277
+ this.syncInFlight.delete(publicKeyHash);
1278
+ }
1279
+ }
1280
+ clearSyncInFlightForPeerHashes(publicKeyHash, hashes) {
1281
+ const map = this.syncInFlight.get(publicKeyHash);
1282
+ if (!map || hashes.length === 0) {
1283
+ return;
1284
+ }
1285
+ const keys = new Set(hashes);
1286
+ const hashSet = new Set(hashes);
1287
+ for (const key of map.keys()) {
1288
+ if (typeof key !== "bigint") {
1289
+ continue;
1290
+ }
1291
+ const hash = this.coordinateToHash.get(key);
1292
+ if (hash != null && hashSet.has(hash)) {
1293
+ keys.add(key);
1294
+ }
1295
+ }
1296
+ for (const key of keys) {
891
1297
  map.delete(key);
892
1298
  }
893
1299
  if (map.size === 0) {
@@ -895,7 +1301,32 @@ export class SimpleSyncronizer {
895
1301
  }
896
1302
  }
897
1303
  clearSyncProcess(hash) {
898
- for (const key of this.getKnownAliases(hash)) {
1304
+ this.forEachKnownAlias(hash, (key) => this.clearSyncProcessKey(key));
1305
+ }
1306
+ clearSyncProcesses(hashes) {
1307
+ if (hashes.length === 0) {
1308
+ return;
1309
+ }
1310
+ const keys = new Set(hashes);
1311
+ const hashSet = new Set(hashes);
1312
+ const maybeAddAlias = (key) => {
1313
+ if (typeof key !== "bigint") {
1314
+ return;
1315
+ }
1316
+ const hash = this.coordinateToHash.get(key);
1317
+ if (hash != null && hashSet.has(hash)) {
1318
+ keys.add(key);
1319
+ }
1320
+ };
1321
+ for (const key of this.syncInFlightQueue.keys()) {
1322
+ maybeAddAlias(key);
1323
+ }
1324
+ for (const map of this.syncInFlight.values()) {
1325
+ for (const key of map.keys()) {
1326
+ maybeAddAlias(key);
1327
+ }
1328
+ }
1329
+ for (const key of keys) {
899
1330
  this.clearSyncProcessKey(key);
900
1331
  }
901
1332
  }
@@ -905,6 +1336,7 @@ export class SimpleSyncronizer {
905
1336
  }
906
1337
  clearSyncProcessPublicKeyHash(publicKeyHash) {
907
1338
  this.syncInFlight.delete(publicKeyHash);
1339
+ this.recentlySentExchangeHeads.delete(publicKeyHash);
908
1340
  const map = this.syncInFlightQueueInverted.get(publicKeyHash);
909
1341
  if (map) {
910
1342
  for (const hash of map) {