@peerbit/shared-log 13.1.16 → 13.1.17

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 (39) hide show
  1. package/dist/benchmark/native-graph.d.ts +2 -0
  2. package/dist/benchmark/native-graph.d.ts.map +1 -0
  3. package/dist/benchmark/native-graph.js +249 -0
  4. package/dist/benchmark/native-graph.js.map +1 -0
  5. package/dist/benchmark/sync-batch-sweep.js +72 -24
  6. package/dist/benchmark/sync-batch-sweep.js.map +1 -1
  7. package/dist/benchmark/sync-phase-profile.d.ts +2 -0
  8. package/dist/benchmark/sync-phase-profile.d.ts.map +1 -0
  9. package/dist/benchmark/sync-phase-profile.js +303 -0
  10. package/dist/benchmark/sync-phase-profile.js.map +1 -0
  11. package/dist/src/checked-prune.d.ts +55 -0
  12. package/dist/src/checked-prune.d.ts.map +1 -0
  13. package/dist/src/checked-prune.js +244 -0
  14. package/dist/src/checked-prune.js.map +1 -0
  15. package/dist/src/index.d.ts +10 -9
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js +265 -164
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/sync/index.d.ts +9 -1
  20. package/dist/src/sync/index.d.ts.map +1 -1
  21. package/dist/src/sync/profile.d.ts +3 -0
  22. package/dist/src/sync/profile.d.ts.map +1 -0
  23. package/dist/src/sync/profile.js +2 -0
  24. package/dist/src/sync/profile.js.map +1 -0
  25. package/dist/src/sync/rateless-iblt.d.ts +25 -11
  26. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  27. package/dist/src/sync/rateless-iblt.js +597 -106
  28. package/dist/src/sync/rateless-iblt.js.map +1 -1
  29. package/dist/src/sync/simple.d.ts +5 -0
  30. package/dist/src/sync/simple.d.ts.map +1 -1
  31. package/dist/src/sync/simple.js +224 -74
  32. package/dist/src/sync/simple.js.map +1 -1
  33. package/package.json +17 -13
  34. package/src/checked-prune.ts +331 -0
  35. package/src/index.ts +429 -282
  36. package/src/sync/index.ts +11 -1
  37. package/src/sync/profile.ts +9 -0
  38. package/src/sync/rateless-iblt.ts +768 -128
  39. package/src/sync/simple.ts +256 -94
@@ -32,7 +32,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
32
32
  }
33
33
  return useValue ? value : void 0;
34
34
  };
35
- import { field, variant, vec } from "@dao-xyz/borsh";
35
+ import { field, variant, } from "@dao-xyz/borsh";
36
36
  import { Cache } from "@peerbit/cache";
37
37
  import { randomBytes, toBase64 } from "@peerbit/crypto";
38
38
  import { And, IntegerCompare, Or, } from "@peerbit/indexer-interface";
@@ -42,7 +42,8 @@ import { SilentDelivery } from "@peerbit/stream-interface";
42
42
  import {} from "../exchange-heads.js";
43
43
  import { TransportMessage } from "../message.js";
44
44
  import {} from "../ranges.js";
45
- import { SimpleSyncronizer } from "./simple.js";
45
+ import { emitSyncProfileDuration, emitSyncProfileEvent, syncProfileStart, } from "./profile.js";
46
+ import { SYNC_MESSAGE_PRIORITY, SimpleSyncronizer, } from "./simple.js";
46
47
  export const logger = loggerFn("peerbit:shared-log:rateless");
47
48
  const coerceBigInt = (value) => typeof value === "bigint" ? value : BigInt(value);
48
49
  let SymbolSerialized = (() => {
@@ -77,12 +78,285 @@ let SymbolSerialized = (() => {
77
78
  }
78
79
  };
79
80
  })();
81
+ const CODED_SYMBOL_WORDS = 3;
82
+ const CODED_SYMBOL_WORD_BYTES = 8;
83
+ const CODED_SYMBOL_BYTES = CODED_SYMBOL_WORDS * CODED_SYMBOL_WORD_BYTES;
84
+ const BIG_UINT64_ARRAY_IS_LITTLE_ENDIAN = typeof BigUint64Array !== "undefined" &&
85
+ new Uint8Array(new BigUint64Array([1n]).buffer)[0] === 1;
86
+ const assertValidFlatCodedSymbols = (flat) => {
87
+ if (flat.length % CODED_SYMBOL_WORDS !== 0) {
88
+ throw new Error("Invalid RIBLT coded symbol batch");
89
+ }
90
+ };
91
+ export class CodedSymbolBatch {
92
+ flat;
93
+ symbols;
94
+ constructor(properties) {
95
+ this.flat = properties.flat;
96
+ this.symbols = properties.symbols;
97
+ }
98
+ static from(symbols) {
99
+ if (symbols instanceof CodedSymbolBatch) {
100
+ return symbols;
101
+ }
102
+ if (typeof BigUint64Array !== "undefined" &&
103
+ symbols instanceof BigUint64Array) {
104
+ return CodedSymbolBatch.fromFlat(symbols);
105
+ }
106
+ return CodedSymbolBatch.fromSymbols(symbols);
107
+ }
108
+ static fromFlat(flat) {
109
+ assertValidFlatCodedSymbols(flat);
110
+ return new CodedSymbolBatch({ flat });
111
+ }
112
+ static fromSymbols(symbols) {
113
+ return new CodedSymbolBatch({ symbols });
114
+ }
115
+ get length() {
116
+ return this.flat
117
+ ? this.flat.length / CODED_SYMBOL_WORDS
118
+ : (this.symbols?.length ?? 0);
119
+ }
120
+ toFlat() {
121
+ if (this.flat) {
122
+ return this.flat;
123
+ }
124
+ const symbols = this.symbols ?? [];
125
+ const flat = new BigUint64Array(symbols.length * CODED_SYMBOL_WORDS);
126
+ for (let i = 0; i < symbols.length; i++) {
127
+ const offset = i * CODED_SYMBOL_WORDS;
128
+ const symbol = symbols[i];
129
+ flat[offset] = coerceBigInt(symbol.count);
130
+ flat[offset + 1] = coerceBigInt(symbol.hash);
131
+ flat[offset + 2] = coerceBigInt(symbol.symbol);
132
+ }
133
+ this.flat = flat;
134
+ return flat;
135
+ }
136
+ toSymbols() {
137
+ const symbols = [];
138
+ for (const symbol of this) {
139
+ symbols.push(symbol instanceof SymbolSerialized
140
+ ? symbol
141
+ : new SymbolSerialized({
142
+ count: symbol.count,
143
+ hash: symbol.hash,
144
+ symbol: symbol.symbol,
145
+ }));
146
+ }
147
+ return symbols;
148
+ }
149
+ *[Symbol.iterator]() {
150
+ if (this.symbols) {
151
+ yield* this.symbols;
152
+ return;
153
+ }
154
+ const flat = this.flat;
155
+ if (!flat) {
156
+ return;
157
+ }
158
+ for (let i = 0; i < flat.length; i += CODED_SYMBOL_WORDS) {
159
+ yield {
160
+ count: flat[i],
161
+ hash: flat[i + 1],
162
+ symbol: flat[i + 2],
163
+ };
164
+ }
165
+ }
166
+ }
167
+ const codedSymbolBatchField = {
168
+ serialize: (symbols, writer) => {
169
+ const batch = CodedSymbolBatch.from(symbols);
170
+ const length = batch.length;
171
+ writer.u32(length);
172
+ if (length === 0) {
173
+ return;
174
+ }
175
+ if (typeof BigUint64Array !== "undefined") {
176
+ const flat = batch.toFlat();
177
+ if (BIG_UINT64_ARRAY_IS_LITTLE_ENDIAN) {
178
+ writer.set(new Uint8Array(flat.buffer, flat.byteOffset, flat.byteLength));
179
+ return;
180
+ }
181
+ for (let i = 0; i < flat.length; i++) {
182
+ writer.u64(flat[i]);
183
+ }
184
+ return;
185
+ }
186
+ for (const symbol of batch) {
187
+ writer.u64(symbol.count);
188
+ writer.u64(symbol.hash);
189
+ writer.u64(symbol.symbol);
190
+ }
191
+ },
192
+ deserialize: (reader) => {
193
+ const length = reader.u32();
194
+ const wordLength = length * CODED_SYMBOL_WORDS;
195
+ const byteLength = length * CODED_SYMBOL_BYTES;
196
+ if (typeof BigUint64Array !== "undefined" &&
197
+ BIG_UINT64_ARRAY_IS_LITTLE_ENDIAN) {
198
+ if (reader._offset + byteLength > reader._buf.length) {
199
+ throw new Error("Invalid RIBLT coded symbol batch length");
200
+ }
201
+ const bytes = reader.buffer(byteLength);
202
+ const flat = new BigUint64Array(wordLength);
203
+ new Uint8Array(flat.buffer).set(bytes);
204
+ return CodedSymbolBatch.fromFlat(flat);
205
+ }
206
+ const symbols = [];
207
+ for (let i = 0; i < length; i++) {
208
+ symbols.push(new SymbolSerialized({
209
+ count: reader.u64(),
210
+ hash: reader.u64(),
211
+ symbol: reader.u64(),
212
+ }));
213
+ }
214
+ return CodedSymbolBatch.fromSymbols(symbols);
215
+ },
216
+ };
217
+ const addSymbolsToRiblt = (target, symbols) => {
218
+ if (typeof BigUint64Array !== "undefined" &&
219
+ typeof target.add_symbols === "function") {
220
+ target.add_symbols(symbols instanceof BigUint64Array
221
+ ? symbols
222
+ : BigUint64Array.from(symbols, coerceBigInt));
223
+ return;
224
+ }
225
+ for (const symbol of symbols) {
226
+ target.add_symbol(coerceBigInt(symbol));
227
+ }
228
+ };
229
+ const produceNextCodedSymbols = (encoder, count) => {
230
+ const produceBatch = encoder
231
+ .produce_next_coded_symbols;
232
+ if (typeof BigUint64Array !== "undefined" && produceBatch) {
233
+ return CodedSymbolBatch.fromFlat(produceBatch.call(encoder, count));
234
+ }
235
+ const symbols = [];
236
+ for (let i = 0; i < count; i++) {
237
+ symbols.push(new SymbolSerialized(encoder.produce_next_coded_symbol()));
238
+ }
239
+ return CodedSymbolBatch.fromSymbols(symbols);
240
+ };
241
+ const flatFromCodedSymbols = (symbols) => {
242
+ if (typeof BigUint64Array !== "undefined" &&
243
+ symbols instanceof BigUint64Array) {
244
+ assertValidFlatCodedSymbols(symbols);
245
+ return symbols;
246
+ }
247
+ return CodedSymbolBatch.from(symbols).toFlat();
248
+ };
249
+ const getRemoteSymbolValues = (decoder) => {
250
+ const getBatch = decoder.get_remote_symbol_values;
251
+ if (typeof BigUint64Array !== "undefined" && getBatch) {
252
+ return Array.from(getBatch.call(decoder));
253
+ }
254
+ const symbols = [];
255
+ for (const missingSymbol of decoder.get_remote_symbols()) {
256
+ symbols.push(coerceBigInt(missingSymbol));
257
+ }
258
+ return symbols;
259
+ };
260
+ const prepareStartSyncEncoder = (coordinates, maxValue, initialSymbolCount) => {
261
+ const encoder = new EncoderWrapper();
262
+ let complete = false;
263
+ try {
264
+ const prepareAndProduceNative = encoder
265
+ .add_symbols_sorted_find_range_and_produce;
266
+ if (typeof BigUint64Array !== "undefined" && prepareAndProduceNative) {
267
+ const prepared = prepareAndProduceNative.call(encoder, BigUint64Array.from(coordinates), coerceBigInt(maxValue), initialSymbolCount);
268
+ if (prepared.length < 2 ||
269
+ (prepared.length - 2) % CODED_SYMBOL_WORDS !== 0) {
270
+ throw new Error("Invalid RIBLT prepared encoder result");
271
+ }
272
+ complete = true;
273
+ return {
274
+ encoder,
275
+ start: prepared[0],
276
+ end: prepared[1],
277
+ initialSymbols: CodedSymbolBatch.fromFlat(prepared.subarray(2)),
278
+ };
279
+ }
280
+ const prepareNative = encoder
281
+ .add_symbols_sorted_and_find_range;
282
+ if (typeof BigUint64Array !== "undefined" && prepareNative) {
283
+ const range = prepareNative.call(encoder, BigUint64Array.from(coordinates), coerceBigInt(maxValue));
284
+ if (range.length !== 2) {
285
+ throw new Error("Invalid RIBLT range result");
286
+ }
287
+ complete = true;
288
+ return { encoder, start: range[0], end: range[1] };
289
+ }
290
+ let sortedEntries;
291
+ if (typeof BigUint64Array !== "undefined") {
292
+ const typed = new BigUint64Array(coordinates.length);
293
+ for (let i = 0; i < coordinates.length; i++) {
294
+ typed[i] = coordinates[i];
295
+ }
296
+ typed.sort();
297
+ sortedEntries = typed;
298
+ }
299
+ else {
300
+ sortedEntries = [...coordinates].sort((a, b) => {
301
+ if (a > b) {
302
+ return 1;
303
+ }
304
+ else if (a < b) {
305
+ return -1;
306
+ }
307
+ else {
308
+ return 0;
309
+ }
310
+ });
311
+ }
312
+ // assume sorted, and find the largest gap
313
+ let largestGap = 0n;
314
+ let largestGapIndex = 0;
315
+ for (let i = 0; i < sortedEntries.length; i++) {
316
+ const current = sortedEntries[i];
317
+ const next = sortedEntries[(i + 1) % sortedEntries.length];
318
+ const gap = next >= current
319
+ ? next - current
320
+ : coerceBigInt(maxValue) - current + next;
321
+ if (gap > largestGap) {
322
+ largestGap = gap;
323
+ largestGapIndex = i;
324
+ }
325
+ }
326
+ const smallestRangeStartIndex = (largestGapIndex + 1) % sortedEntries.length;
327
+ const smallestRangeEndIndex = largestGapIndex; /// === (smallRangeStartIndex + 1) % sortedEntries.length
328
+ let smallestRangeStart = sortedEntries[smallestRangeStartIndex];
329
+ let smallestRangeEnd = sortedEntries[smallestRangeEndIndex];
330
+ let start, end;
331
+ if (smallestRangeEnd === smallestRangeStart) {
332
+ start = smallestRangeEnd;
333
+ end = smallestRangeEnd + 1n;
334
+ if (end > maxValue) {
335
+ end = 0n;
336
+ }
337
+ }
338
+ else {
339
+ start = smallestRangeStart;
340
+ end = smallestRangeEnd;
341
+ }
342
+ addSymbolsToRiblt(encoder, sortedEntries);
343
+ complete = true;
344
+ return { encoder, start, end };
345
+ }
346
+ finally {
347
+ if (!complete) {
348
+ encoder.free();
349
+ }
350
+ }
351
+ };
80
352
  const getSyncIdString = (message) => {
81
353
  return toBase64(message.syncId);
82
354
  };
83
355
  const DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS = 30_000;
84
356
  const DEFAULT_CONVERGENT_RETRY_INTERVALS_MS = [0, 1_000, 3_000, 7_000];
85
357
  const DEFAULT_MAX_CONVERGENT_TRACKED_HASHES = 4_096;
358
+ const MIN_MORE_SYMBOLS_BATCH_SIZE = 64;
359
+ const MAX_MORE_SYMBOLS_BATCH_SIZE = 1_024;
86
360
  let StartSync = (() => {
87
361
  let _classDecorators = [variant([3, 0])];
88
362
  let _classDescriptor;
@@ -108,7 +382,7 @@ let StartSync = (() => {
108
382
  _syncId_decorators = [field({ type: Uint8Array })];
109
383
  _start_decorators = [field({ type: "u64" })];
110
384
  _end_decorators = [field({ type: "u64" })];
111
- _symbols_decorators = [field({ type: vec(SymbolSerialized) })];
385
+ _symbols_decorators = [field({ type: codedSymbolBatchField })];
112
386
  __esDecorate(null, null, _syncId_decorators, { kind: "field", name: "syncId", static: false, private: false, access: { has: obj => "syncId" in obj, get: obj => obj.syncId, set: (obj, value) => { obj.syncId = value; } }, metadata: _metadata }, _syncId_initializers, _syncId_extraInitializers);
113
387
  __esDecorate(null, null, _start_decorators, { kind: "field", name: "start", static: false, private: false, access: { has: obj => "start" in obj, get: obj => obj.start, set: (obj, value) => { obj.start = value; } }, metadata: _metadata }, _start_initializers, _start_extraInitializers);
114
388
  __esDecorate(null, null, _end_decorators, { kind: "field", name: "end", static: false, private: false, access: { has: obj => "end" in obj, get: obj => obj.end, set: (obj, value) => { obj.end = value; } }, metadata: _metadata }, _end_initializers, _end_extraInitializers);
@@ -128,7 +402,7 @@ let StartSync = (() => {
128
402
  this.syncId = randomBytes(32);
129
403
  this.start = coerceBigInt(props.from);
130
404
  this.end = coerceBigInt(props.to);
131
- this.symbols = props.symbols;
405
+ this.symbols = CodedSymbolBatch.from(props.symbols);
132
406
  }
133
407
  };
134
408
  return StartSync = _classThis;
@@ -155,7 +429,7 @@ let MoreSymbols = (() => {
155
429
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
156
430
  _syncId_decorators = [field({ type: Uint8Array })];
157
431
  _seqNo_decorators = [field({ type: "u64" })];
158
- _symbols_decorators = [field({ type: vec(SymbolSerialized) })];
432
+ _symbols_decorators = [field({ type: codedSymbolBatchField })];
159
433
  __esDecorate(null, null, _syncId_decorators, { kind: "field", name: "syncId", static: false, private: false, access: { has: obj => "syncId" in obj, get: obj => obj.syncId, set: (obj, value) => { obj.syncId = value; } }, metadata: _metadata }, _syncId_initializers, _syncId_extraInitializers);
160
434
  __esDecorate(null, null, _seqNo_decorators, { kind: "field", name: "seqNo", static: false, private: false, access: { has: obj => "seqNo" in obj, get: obj => obj.seqNo, set: (obj, value) => { obj.seqNo = value; } }, metadata: _metadata }, _seqNo_initializers, _seqNo_extraInitializers);
161
435
  __esDecorate(null, null, _symbols_decorators, { kind: "field", name: "symbols", static: false, private: false, access: { has: obj => "symbols" in obj, get: obj => obj.symbols, set: (obj, value) => { obj.symbols = value; } }, metadata: _metadata }, _symbols_initializers, _symbols_extraInitializers);
@@ -172,7 +446,7 @@ let MoreSymbols = (() => {
172
446
  __runInitializers(this, _symbols_extraInitializers);
173
447
  this.syncId = props.syncId;
174
448
  this.seqNo = props.lastSeqNo + 1n;
175
- this.symbols = props.symbols;
449
+ this.symbols = CodedSymbolBatch.from(props.symbols);
176
450
  }
177
451
  };
178
452
  return MoreSymbols = _classThis;
@@ -279,9 +553,10 @@ const matchEntriesByHashNumberInRangeQuery = (range) => {
279
553
  ]),
280
554
  ]);
281
555
  };
282
- const buildEncoderOrDecoderFromRange = async (ranges, entryIndex, type) => {
556
+ const buildEncoderOrDecoderFromRange = async (ranges, entryIndex, type, profile) => {
283
557
  await ribltReady;
284
558
  const encoder = type === "encoder" ? new EncoderWrapper() : new DecoderWrapper();
559
+ const rangeQueryStartedAt = syncProfileStart(profile);
285
560
  const entries = await entryIndex
286
561
  .iterate({
287
562
  // Range sync for IBLT is done in hashNumber space.
@@ -298,11 +573,37 @@ const buildEncoderOrDecoderFromRange = async (ranges, entryIndex, type) => {
298
573
  },
299
574
  })
300
575
  .all();
576
+ if (profile) {
577
+ emitSyncProfileDuration(profile, rangeQueryStartedAt, {
578
+ name: "rateless.rangeQuery",
579
+ entries: entries.length,
580
+ details: { type },
581
+ });
582
+ }
301
583
  if (entries.length === 0) {
302
584
  return false;
303
585
  }
304
- for (const entry of entries) {
305
- encoder.add_symbol(coerceBigInt(entry.value.hashNumber));
586
+ const addSymbolsStartedAt = syncProfileStart(profile);
587
+ if (typeof BigUint64Array !== "undefined" &&
588
+ typeof encoder.add_symbols === "function") {
589
+ const symbols = new BigUint64Array(entries.length);
590
+ for (let i = 0; i < entries.length; i++) {
591
+ symbols[i] = coerceBigInt(entries[i].value.hashNumber);
592
+ }
593
+ addSymbolsToRiblt(encoder, symbols);
594
+ }
595
+ else {
596
+ for (const entry of entries) {
597
+ encoder.add_symbol(coerceBigInt(entry.value.hashNumber));
598
+ }
599
+ }
600
+ if (profile) {
601
+ emitSyncProfileDuration(profile, addSymbolsStartedAt, {
602
+ name: "rateless.rangeAddSymbols",
603
+ entries: entries.length,
604
+ symbols: entries.length,
605
+ details: { type },
606
+ });
306
607
  }
307
608
  return encoder;
308
609
  };
@@ -489,14 +790,34 @@ export class RatelessIBLTSynchronizer {
489
790
  return decoder;
490
791
  }
491
792
  async getLocalDecoderForRange(ranges) {
793
+ const profile = this.properties.sync?.profile;
492
794
  const key = this.localRangeEncoderCacheKey(ranges);
493
795
  const cached = this.localRangeEncoderCache.get(key);
494
796
  if (cached && cached.version === this.localRangeEncoderCacheVersion) {
797
+ const startedAt = syncProfileStart(profile);
495
798
  cached.lastUsed = Date.now();
496
- return this.decoderFromCachedEncoder(cached.encoder);
799
+ try {
800
+ return this.decoderFromCachedEncoder(cached.encoder);
801
+ }
802
+ finally {
803
+ if (profile) {
804
+ emitSyncProfileDuration(profile, startedAt, {
805
+ name: "rateless.localDecoder",
806
+ cacheHit: true,
807
+ });
808
+ }
809
+ }
497
810
  }
498
- const encoder = (await buildEncoderOrDecoderFromRange(ranges, this.properties.entryIndex, "encoder"));
811
+ const startedAt = syncProfileStart(profile);
812
+ const encoder = (await buildEncoderOrDecoderFromRange(ranges, this.properties.entryIndex, "encoder", profile));
499
813
  if (!encoder) {
814
+ if (profile) {
815
+ emitSyncProfileDuration(profile, startedAt, {
816
+ name: "rateless.localDecoder",
817
+ cacheHit: false,
818
+ entries: 0,
819
+ });
820
+ }
500
821
  return false;
501
822
  }
502
823
  const now = Date.now();
@@ -527,9 +848,21 @@ export class RatelessIBLTSynchronizer {
527
848
  }
528
849
  this.localRangeEncoderCache.delete(oldestKey);
529
850
  }
530
- return this.decoderFromCachedEncoder(encoder);
851
+ try {
852
+ return this.decoderFromCachedEncoder(encoder);
853
+ }
854
+ finally {
855
+ if (profile) {
856
+ emitSyncProfileDuration(profile, startedAt, {
857
+ name: "rateless.localDecoder",
858
+ cacheHit: false,
859
+ });
860
+ }
861
+ }
531
862
  }
532
863
  async onMaybeMissingEntries(properties) {
864
+ const profile = this.properties.sync?.profile;
865
+ const startedAt = syncProfileStart(profile);
533
866
  // NOTE: this method is best-effort dispatch, not a per-hash convergence API.
534
867
  // It may require follow-up repair rounds under churn/loss to fully close all gaps.
535
868
  // Strategy:
@@ -542,12 +875,33 @@ export class RatelessIBLTSynchronizer {
542
875
  let maxSyncWithSimpleMethod = 1e3;
543
876
  // Small batch => use simple synchronizer entirely
544
877
  if (properties.entries.size <= minSyncIbltSize) {
545
- await this.simple.onMaybeMissingEntries({
546
- entries: properties.entries,
547
- targets: properties.targets,
548
- });
878
+ if (profile) {
879
+ emitSyncProfileEvent(profile, {
880
+ name: "rateless.dispatchMode",
881
+ entries: properties.entries.size,
882
+ targets: properties.targets.length,
883
+ details: { mode: "simple-small" },
884
+ });
885
+ }
886
+ try {
887
+ await this.simple.onMaybeMissingEntries({
888
+ entries: properties.entries,
889
+ targets: properties.targets,
890
+ });
891
+ }
892
+ finally {
893
+ if (profile) {
894
+ emitSyncProfileDuration(profile, startedAt, {
895
+ name: "rateless.onMaybeMissingEntries",
896
+ entries: properties.entries.size,
897
+ targets: properties.targets.length,
898
+ details: { mode: "simple-small" },
899
+ });
900
+ }
901
+ }
549
902
  return;
550
903
  }
904
+ const selectStartedAt = syncProfileStart(profile);
551
905
  const nonBoundaryEntries = [];
552
906
  for (const entry of properties.entries.values()) {
553
907
  if (entry.assignedToRangeBoundary) {
@@ -604,91 +958,84 @@ export class RatelessIBLTSynchronizer {
604
958
  allCoordinatesToSyncWithIblt.push(coerceBigInt(entry.hashNumber));
605
959
  }
606
960
  }
607
- if (allCoordinatesToSyncWithIblt.length === 0) {
608
- return;
609
- }
610
- await ribltReady;
611
- let sortedEntries;
612
- if (typeof BigUint64Array !== "undefined") {
613
- const typed = new BigUint64Array(allCoordinatesToSyncWithIblt.length);
614
- for (let i = 0; i < allCoordinatesToSyncWithIblt.length; i++) {
615
- typed[i] = allCoordinatesToSyncWithIblt[i];
616
- }
617
- typed.sort();
618
- sortedEntries = typed;
619
- }
620
- else {
621
- sortedEntries = allCoordinatesToSyncWithIblt.sort((a, b) => {
622
- if (a > b) {
623
- return 1;
624
- }
625
- else if (a < b) {
626
- return -1;
627
- }
628
- else {
629
- return 0;
630
- }
961
+ if (profile) {
962
+ emitSyncProfileDuration(profile, selectStartedAt, {
963
+ name: "rateless.selectEntries",
964
+ entries: properties.entries.size,
965
+ symbols: allCoordinatesToSyncWithIblt.length,
966
+ targets: properties.targets.length,
967
+ details: {
968
+ naiveEntries: entriesToSyncNaively.size,
969
+ priority: priorityFn != null,
970
+ },
631
971
  });
632
972
  }
633
- // assume sorted, and find the largest gap
634
- let largestGap = 0n;
635
- let largestGapIndex = 0;
636
- for (let i = 0; i < sortedEntries.length; i++) {
637
- const current = sortedEntries[i];
638
- const next = sortedEntries[(i + 1) % sortedEntries.length];
639
- const gap = next >= current
640
- ? next - current
641
- : coerceBigInt(this.properties.numbers.maxValue) - current + next;
642
- if (gap > largestGap) {
643
- largestGap = gap;
644
- largestGapIndex = i;
645
- }
646
- }
647
- const smallestRangeStartIndex = (largestGapIndex + 1) % sortedEntries.length;
648
- const smallestRangeEndIndex = largestGapIndex; /// === (smallRangeStartIndex + 1) % sortedEntries.length
649
- let smallestRangeStart = sortedEntries[smallestRangeStartIndex];
650
- let smallestRangeEnd = sortedEntries[smallestRangeEndIndex];
651
- let start, end;
652
- if (smallestRangeEnd === smallestRangeStart) {
653
- start = smallestRangeEnd;
654
- end = smallestRangeEnd + 1n;
655
- if (end > this.properties.numbers.maxValue) {
656
- end = 0n;
657
- }
658
- }
659
- else {
660
- start = smallestRangeStart;
661
- end = smallestRangeEnd;
662
- }
663
- const startSync = new StartSync({ from: start, to: end, symbols: [] });
664
- const encoder = new EncoderWrapper();
665
- if (typeof BigUint64Array !== "undefined" &&
666
- sortedEntries instanceof BigUint64Array) {
667
- encoder.add_symbols(sortedEntries);
668
- }
669
- else {
670
- for (const entry of sortedEntries) {
671
- encoder.add_symbol(coerceBigInt(entry));
973
+ if (allCoordinatesToSyncWithIblt.length === 0) {
974
+ if (profile) {
975
+ emitSyncProfileEvent(profile, {
976
+ name: "rateless.dispatchMode",
977
+ entries: properties.entries.size,
978
+ targets: properties.targets.length,
979
+ details: { mode: "simple-only" },
980
+ });
981
+ emitSyncProfileDuration(profile, startedAt, {
982
+ name: "rateless.onMaybeMissingEntries",
983
+ entries: properties.entries.size,
984
+ targets: properties.targets.length,
985
+ details: { mode: "simple-only" },
986
+ });
672
987
  }
988
+ return;
673
989
  }
990
+ await ribltReady;
674
991
  // For smaller sets, the original `sqrt(n)` heuristic can occasionally under-provision
675
992
  // low-degree symbols early, causing an unnecessary `MoreSymbols` round-trip. Use a
676
993
  // small floor to make small-delta syncs more reliable without affecting large-n behavior.
677
- let initialSymbols = Math.round(Math.sqrt(allCoordinatesToSyncWithIblt.length)); // TODO choose better
678
- initialSymbols = Math.max(64, initialSymbols);
679
- for (let i = 0; i < initialSymbols; i++) {
680
- startSync.symbols.push(new SymbolSerialized(encoder.produce_next_coded_symbol()));
994
+ let initialSymbolCount = Math.round(Math.sqrt(allCoordinatesToSyncWithIblt.length)); // TODO choose better
995
+ initialSymbolCount = Math.max(64, initialSymbolCount);
996
+ const prepareStartedAt = syncProfileStart(profile);
997
+ const { encoder, start, end, initialSymbols } = prepareStartSyncEncoder(allCoordinatesToSyncWithIblt, this.properties.numbers.maxValue, initialSymbolCount);
998
+ if (profile) {
999
+ emitSyncProfileDuration(profile, prepareStartedAt, {
1000
+ name: "rateless.prepareStartSyncEncoder",
1001
+ entries: allCoordinatesToSyncWithIblt.length,
1002
+ symbols: initialSymbols?.length,
1003
+ details: {
1004
+ initialSymbolCount,
1005
+ includesInitialSymbols: initialSymbols != null,
1006
+ },
1007
+ });
681
1008
  }
1009
+ let startSyncSymbols = initialSymbols;
1010
+ if (!startSyncSymbols) {
1011
+ const produceStartedAt = syncProfileStart(profile);
1012
+ startSyncSymbols = produceNextCodedSymbols(encoder, initialSymbolCount);
1013
+ if (profile) {
1014
+ emitSyncProfileDuration(profile, produceStartedAt, {
1015
+ name: "rateless.produceStartSyncSymbols",
1016
+ symbols: startSyncSymbols.length,
1017
+ });
1018
+ }
1019
+ }
1020
+ const startSync = new StartSync({
1021
+ from: start,
1022
+ to: end,
1023
+ symbols: startSyncSymbols,
1024
+ });
1025
+ const syncId = getSyncIdString(startSync);
682
1026
  const clear = () => {
683
1027
  encoder.free();
684
- clearTimeout(this.outgoingSyncProcesses.get(getSyncIdString(startSync))?.timeout);
685
- this.outgoingSyncProcesses.delete(getSyncIdString(startSync));
1028
+ clearTimeout(this.outgoingSyncProcesses.get(syncId)?.timeout);
1029
+ this.outgoingSyncProcesses.delete(syncId);
686
1030
  };
687
1031
  const createTimeout = () => {
688
1032
  return setTimeout(clear, 1e4); // TODO arg
689
1033
  };
690
1034
  let lastSeqNo = -1n;
691
- let nextBatch = 1e4;
1035
+ // Keep follow-up symbol payloads bounded. Each symbol is serialized as an
1036
+ // object with three bigint fields, so very large batches can dominate heap under
1037
+ // concurrent churn even though the native RIBLT encoder itself is compact.
1038
+ const nextBatch = Math.max(MIN_MORE_SYMBOLS_BATCH_SIZE, Math.min(MAX_MORE_SYMBOLS_BATCH_SIZE, Math.ceil(allCoordinatesToSyncWithIblt.length / 4)));
692
1039
  const obj = {
693
1040
  encoder,
694
1041
  timeout: createTimeout(),
@@ -701,26 +1048,73 @@ export class RatelessIBLTSynchronizer {
701
1048
  },
702
1049
  next: (properties) => {
703
1050
  if (properties.lastSeqNo <= lastSeqNo) {
704
- return [];
1051
+ return CodedSymbolBatch.fromSymbols([]);
705
1052
  }
706
1053
  lastSeqNo++;
707
1054
  obj.refresh(); // TODO use timestamp instead and collective pruning/refresh
708
- let result = [];
709
- for (let i = 0; i < nextBatch; i++) {
710
- result.push(encoder.produce_next_coded_symbol());
1055
+ const produceStartedAt = syncProfileStart(profile);
1056
+ const symbols = produceNextCodedSymbols(encoder, nextBatch);
1057
+ if (profile) {
1058
+ emitSyncProfileDuration(profile, produceStartedAt, {
1059
+ name: "rateless.produceMoreSymbols",
1060
+ syncId,
1061
+ symbols: symbols.length,
1062
+ });
711
1063
  }
712
- return result;
1064
+ return symbols;
713
1065
  },
714
1066
  free: clear,
715
1067
  outgoing: properties.entries,
716
1068
  };
717
- this.outgoingSyncProcesses.set(getSyncIdString(startSync), obj);
718
- this.simple.rpc.send(startSync, {
1069
+ this.outgoingSyncProcesses.set(syncId, obj);
1070
+ if (profile) {
1071
+ emitSyncProfileEvent(profile, {
1072
+ name: "rateless.dispatchMode",
1073
+ entries: properties.entries.size,
1074
+ symbols: startSyncSymbols.length,
1075
+ targets: properties.targets.length,
1076
+ syncId,
1077
+ details: { mode: "rateless" },
1078
+ });
1079
+ }
1080
+ const sendStartedAt = syncProfileStart(profile);
1081
+ const sendResult = this.simple.rpc.send(startSync, {
719
1082
  mode: new SilentDelivery({ to: properties.targets, redundancy: 1 }),
720
- priority: 1,
1083
+ priority: SYNC_MESSAGE_PRIORITY,
721
1084
  });
1085
+ if (profile) {
1086
+ void Promise.resolve(sendResult).then(() => emitSyncProfileDuration(profile, sendStartedAt, {
1087
+ name: "rateless.sendStartSync",
1088
+ messages: 1,
1089
+ symbols: startSyncSymbols.length,
1090
+ targets: properties.targets.length,
1091
+ syncId,
1092
+ }), () => emitSyncProfileDuration(profile, sendStartedAt, {
1093
+ name: "rateless.sendStartSync",
1094
+ messages: 1,
1095
+ symbols: startSyncSymbols.length,
1096
+ targets: properties.targets.length,
1097
+ syncId,
1098
+ details: { rejected: true },
1099
+ }));
1100
+ }
1101
+ if (profile) {
1102
+ emitSyncProfileDuration(profile, startedAt, {
1103
+ name: "rateless.onMaybeMissingEntries",
1104
+ entries: properties.entries.size,
1105
+ messages: 1,
1106
+ symbols: startSyncSymbols.length,
1107
+ targets: properties.targets.length,
1108
+ details: {
1109
+ mode: "rateless",
1110
+ ibltEntries: allCoordinatesToSyncWithIblt.length,
1111
+ naiveEntries: entriesToSyncNaively.size,
1112
+ },
1113
+ });
1114
+ }
722
1115
  }
723
1116
  async onMessage(message, context) {
1117
+ const profile = this.properties.sync?.profile;
724
1118
  if (message instanceof StartSync) {
725
1119
  const syncId = getSyncIdString(message);
726
1120
  if (this.ingoingSyncProcesses.has(syncId)) {
@@ -731,19 +1125,36 @@ export class RatelessIBLTSynchronizer {
731
1125
  }
732
1126
  this.startedOrCompletedSynchronizations.add(syncId);
733
1127
  const wrapped = message.end < message.start;
1128
+ const decoderStartedAt = syncProfileStart(profile);
734
1129
  const decoder = await this.getLocalDecoderForRange({
735
1130
  start1: message.start,
736
1131
  end1: wrapped ? this.properties.numbers.maxValue : message.end,
737
1132
  start2: 0n,
738
1133
  end2: wrapped ? message.end : 0n,
739
1134
  });
1135
+ if (profile) {
1136
+ emitSyncProfileDuration(profile, decoderStartedAt, {
1137
+ name: "rateless.getLocalDecoderForRange",
1138
+ syncId,
1139
+ details: { wrapped, found: decoder !== false },
1140
+ });
1141
+ }
740
1142
  if (!decoder) {
1143
+ const sendStartedAt = syncProfileStart(profile);
741
1144
  await this.simple.rpc.send(new RequestAll({
742
1145
  syncId: message.syncId,
743
1146
  }), {
744
1147
  mode: new SilentDelivery({ to: [context.from], redundancy: 1 }),
745
- priority: 1,
1148
+ priority: SYNC_MESSAGE_PRIORITY,
746
1149
  });
1150
+ if (profile) {
1151
+ emitSyncProfileDuration(profile, sendStartedAt, {
1152
+ name: "rateless.sendRequestAll",
1153
+ messages: 1,
1154
+ targets: 1,
1155
+ syncId,
1156
+ });
1157
+ }
747
1158
  return true;
748
1159
  }
749
1160
  const createTimeout = () => {
@@ -778,9 +1189,15 @@ export class RatelessIBLTSynchronizer {
778
1189
  if (!decoder.decoded()) {
779
1190
  return false;
780
1191
  }
781
- const allMissingSymbolsInRemote = [];
782
- for (const missingSymbol of decoder.get_remote_symbols()) {
783
- allMissingSymbolsInRemote.push(missingSymbol);
1192
+ const remoteStartedAt = syncProfileStart(profile);
1193
+ const allMissingSymbolsInRemote = getRemoteSymbolValues(decoder);
1194
+ if (profile) {
1195
+ emitSyncProfileDuration(profile, remoteStartedAt, {
1196
+ name: "rateless.remoteSymbols",
1197
+ entries: allMissingSymbolsInRemote.length,
1198
+ symbols: allMissingSymbolsInRemote.length,
1199
+ syncId,
1200
+ });
784
1201
  }
785
1202
  // The IBLT decoder is based on a local snapshot. Entries can arrive via
786
1203
  // overlapping repair before we issue the follow-up simple request, so
@@ -796,7 +1213,37 @@ export class RatelessIBLTSynchronizer {
796
1213
  break;
797
1214
  }
798
1215
  lastSeqNo = symbolMessage.seqNo;
799
- for (const symbol of symbolMessage.symbols) {
1216
+ const addBatchAndDecode = decoder
1217
+ .add_coded_symbols_and_try_decode;
1218
+ if (typeof BigUint64Array !== "undefined" && addBatchAndDecode) {
1219
+ const flatStartedAt = syncProfileStart(profile);
1220
+ const flatSymbols = flatFromCodedSymbols(symbolMessage.symbols);
1221
+ if (profile) {
1222
+ emitSyncProfileDuration(profile, flatStartedAt, {
1223
+ name: "rateless.symbolBatchToFlat",
1224
+ symbols: flatSymbols.length / CODED_SYMBOL_WORDS,
1225
+ syncId,
1226
+ });
1227
+ }
1228
+ const decodeStartedAt = syncProfileStart(profile);
1229
+ const decoded = addBatchAndDecode.call(decoder, flatSymbols);
1230
+ if (profile) {
1231
+ emitSyncProfileDuration(profile, decodeStartedAt, {
1232
+ name: "rateless.decodeBatch",
1233
+ symbols: flatSymbols.length / CODED_SYMBOL_WORDS,
1234
+ syncId,
1235
+ details: { decoded },
1236
+ });
1237
+ }
1238
+ if (decoded && finalizeIfDecoded()) {
1239
+ return true;
1240
+ }
1241
+ continue;
1242
+ }
1243
+ const decodeLoopStartedAt = syncProfileStart(profile);
1244
+ let symbolsProcessed = 0;
1245
+ for (const symbol of CodedSymbolBatch.from(symbolMessage.symbols)) {
1246
+ symbolsProcessed += 1;
800
1247
  const normalizedSymbol = symbol instanceof SymbolSerialized
801
1248
  ? symbol
802
1249
  : new SymbolSerialized({
@@ -820,6 +1267,13 @@ export class RatelessIBLTSynchronizer {
820
1267
  throw error;
821
1268
  }
822
1269
  }
1270
+ if (profile) {
1271
+ emitSyncProfileDuration(profile, decodeLoopStartedAt, {
1272
+ name: "rateless.decodeSymbolLoop",
1273
+ symbols: symbolsProcessed,
1274
+ syncId,
1275
+ });
1276
+ }
823
1277
  }
824
1278
  return false;
825
1279
  },
@@ -834,17 +1288,27 @@ export class RatelessIBLTSynchronizer {
834
1288
  return true;
835
1289
  }
836
1290
  // not done, request more symbols
1291
+ const sendStartedAt = syncProfileStart(profile);
837
1292
  await this.simple.rpc.send(new RequestMoreSymbols({
838
1293
  lastSeqNo: 0n,
839
1294
  syncId: message.syncId,
840
1295
  }), {
841
1296
  mode: new SilentDelivery({ to: [context.from], redundancy: 1 }),
842
- priority: 1,
1297
+ priority: SYNC_MESSAGE_PRIORITY,
843
1298
  });
1299
+ if (profile) {
1300
+ emitSyncProfileDuration(profile, sendStartedAt, {
1301
+ name: "rateless.sendRequestMoreSymbols",
1302
+ messages: 1,
1303
+ targets: 1,
1304
+ syncId,
1305
+ });
1306
+ }
844
1307
  return true;
845
1308
  }
846
1309
  else if (message instanceof MoreSymbols) {
847
- const obj = this.ingoingSyncProcesses.get(getSyncIdString(message));
1310
+ const syncId = getSyncIdString(message);
1311
+ const obj = this.ingoingSyncProcesses.get(syncId);
848
1312
  if (!obj) {
849
1313
  return true;
850
1314
  }
@@ -856,28 +1320,55 @@ export class RatelessIBLTSynchronizer {
856
1320
  return true; // we don't have enough information, or received information that is redundant
857
1321
  }
858
1322
  // we are not done
859
- this.simple.rpc.send(new RequestMoreSymbols({
1323
+ const sendStartedAt = syncProfileStart(profile);
1324
+ const sendResult = this.simple.rpc.send(new RequestMoreSymbols({
860
1325
  lastSeqNo: message.seqNo,
861
1326
  syncId: message.syncId,
862
1327
  }), {
863
1328
  mode: new SilentDelivery({ to: [context.from], redundancy: 1 }),
864
- priority: 1,
1329
+ priority: SYNC_MESSAGE_PRIORITY,
865
1330
  });
1331
+ if (profile) {
1332
+ void Promise.resolve(sendResult).then(() => emitSyncProfileDuration(profile, sendStartedAt, {
1333
+ name: "rateless.sendRequestMoreSymbols",
1334
+ messages: 1,
1335
+ targets: 1,
1336
+ syncId,
1337
+ }), () => emitSyncProfileDuration(profile, sendStartedAt, {
1338
+ name: "rateless.sendRequestMoreSymbols",
1339
+ messages: 1,
1340
+ targets: 1,
1341
+ syncId,
1342
+ details: { rejected: true },
1343
+ }));
1344
+ }
866
1345
  return true;
867
1346
  }
868
1347
  else if (message instanceof RequestMoreSymbols) {
869
- const obj = this.outgoingSyncProcesses.get(getSyncIdString(message));
1348
+ const syncId = getSyncIdString(message);
1349
+ const obj = this.outgoingSyncProcesses.get(syncId);
870
1350
  if (!obj) {
871
1351
  return true;
872
1352
  }
1353
+ const symbols = obj.next(message);
1354
+ const sendStartedAt = syncProfileStart(profile);
873
1355
  await this.properties.rpc.send(new MoreSymbols({
874
1356
  lastSeqNo: message.lastSeqNo,
875
1357
  syncId: message.syncId,
876
- symbols: obj.next(message).map((x) => new SymbolSerialized(x)),
1358
+ symbols,
877
1359
  }), {
878
1360
  mode: new SilentDelivery({ to: [context.from], redundancy: 1 }),
879
- priority: 1,
1361
+ priority: SYNC_MESSAGE_PRIORITY,
880
1362
  });
1363
+ if (profile) {
1364
+ emitSyncProfileDuration(profile, sendStartedAt, {
1365
+ name: "rateless.sendMoreSymbols",
1366
+ messages: 1,
1367
+ symbols: symbols.length,
1368
+ targets: 1,
1369
+ syncId,
1370
+ });
1371
+ }
881
1372
  return true;
882
1373
  }
883
1374
  else if (message instanceof RequestAll) {