@peerbit/shared-log 12.1.3-e209d2e → 12.2.0-10dfe9b

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.
@@ -12,14 +12,16 @@ import {
12
12
  import type { RPC, RequestContext } from "@peerbit/rpc";
13
13
  import { SilentDelivery } from "@peerbit/stream-interface";
14
14
  import { type EntryWithRefs } from "../exchange-heads.js";
15
- import { type Numbers } from "../integers.js";
16
15
  import { TransportMessage } from "../message.js";
17
16
  import {
18
17
  type EntryReplicated,
19
- type ReplicationRangeIndexable,
20
18
  matchEntriesInRangeQuery,
21
19
  } from "../ranges.js";
22
- import type { SyncableKey, Syncronizer } from "./index.js";
20
+ import type {
21
+ SyncableKey,
22
+ SynchronizerComponents,
23
+ Syncronizer,
24
+ } from "./index.js";
23
25
  import { SimpleSyncronizer } from "./simple.js";
24
26
 
25
27
  export const logger = loggerFn("peerbit:shared-log:rateless");
@@ -185,6 +187,13 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
185
187
  simple: SimpleSyncronizer<D>;
186
188
 
187
189
  startedOrCompletedSynchronizations: Cache<string>;
190
+ private localRangeEncoderCacheVersion = 0;
191
+ private localRangeEncoderCache: Map<
192
+ string,
193
+ { encoder: EncoderWrapper; version: number; lastUsed: number }
194
+ > = new Map();
195
+ private localRangeEncoderCacheMax = 2;
196
+
188
197
  ingoingSyncProcesses: Map<
189
198
  string,
190
199
  {
@@ -212,14 +221,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
212
221
  >;
213
222
 
214
223
  constructor(
215
- readonly properties: {
216
- rpc: RPC<TransportMessage, TransportMessage>;
217
- rangeIndex: Index<ReplicationRangeIndexable<D>, any>;
218
- entryIndex: Index<EntryReplicated<D>, any>;
219
- log: Log<any>;
220
- coordinateToHash: Cache<string>;
221
- numbers: Numbers<D>;
222
- },
224
+ readonly properties: SynchronizerComponents<D>,
223
225
  ) {
224
226
  this.simple = new SimpleSyncronizer(properties);
225
227
  this.outgoingSyncProcesses = new Map();
@@ -227,6 +229,91 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
227
229
  this.startedOrCompletedSynchronizations = new Cache({ max: 1e4 });
228
230
  }
229
231
 
232
+ private clearLocalRangeEncoderCache() {
233
+ for (const [, cached] of this.localRangeEncoderCache) {
234
+ cached.encoder.free();
235
+ }
236
+ this.localRangeEncoderCache.clear();
237
+ }
238
+
239
+ private invalidateLocalRangeEncoderCache() {
240
+ this.localRangeEncoderCacheVersion += 1;
241
+ this.clearLocalRangeEncoderCache();
242
+ }
243
+
244
+ private localRangeEncoderCacheKey(ranges: {
245
+ start1: NumberOrBigint;
246
+ end1: NumberOrBigint;
247
+ start2: NumberOrBigint;
248
+ end2: NumberOrBigint;
249
+ }) {
250
+ return `${String(ranges.start1)}:${String(ranges.end1)}:${String(
251
+ ranges.start2,
252
+ )}:${String(ranges.end2)}`;
253
+ }
254
+
255
+ private decoderFromCachedEncoder(encoder: EncoderWrapper): DecoderWrapper {
256
+ const clone = encoder.clone();
257
+ const decoder = clone.to_decoder();
258
+ clone.free();
259
+ return decoder;
260
+ }
261
+
262
+ private async getLocalDecoderForRange(ranges: {
263
+ start1: NumberOrBigint;
264
+ end1: NumberOrBigint;
265
+ start2: NumberOrBigint;
266
+ end2: NumberOrBigint;
267
+ }): Promise<DecoderWrapper | false> {
268
+ const key = this.localRangeEncoderCacheKey(ranges);
269
+ const cached = this.localRangeEncoderCache.get(key);
270
+ if (cached && cached.version === this.localRangeEncoderCacheVersion) {
271
+ cached.lastUsed = Date.now();
272
+ return this.decoderFromCachedEncoder(cached.encoder);
273
+ }
274
+
275
+ const encoder = (await buildEncoderOrDecoderFromRange(
276
+ ranges,
277
+ this.properties.entryIndex,
278
+ "encoder",
279
+ )) as EncoderWrapper | false;
280
+ if (!encoder) {
281
+ return false;
282
+ }
283
+
284
+ const now = Date.now();
285
+ const existing = this.localRangeEncoderCache.get(key);
286
+ if (existing) {
287
+ existing.encoder.free();
288
+ }
289
+ this.localRangeEncoderCache.set(key, {
290
+ encoder,
291
+ version: this.localRangeEncoderCacheVersion,
292
+ lastUsed: now,
293
+ });
294
+
295
+ while (this.localRangeEncoderCache.size > this.localRangeEncoderCacheMax) {
296
+ let oldestKey: string | undefined;
297
+ let oldestUsed = Number.POSITIVE_INFINITY;
298
+ for (const [candidateKey, value] of this.localRangeEncoderCache) {
299
+ if (value.lastUsed < oldestUsed) {
300
+ oldestUsed = value.lastUsed;
301
+ oldestKey = candidateKey;
302
+ }
303
+ }
304
+ if (!oldestKey) {
305
+ break;
306
+ }
307
+ const victim = this.localRangeEncoderCache.get(oldestKey);
308
+ if (victim) {
309
+ victim.encoder.free();
310
+ }
311
+ this.localRangeEncoderCache.delete(oldestKey);
312
+ }
313
+
314
+ return this.decoderFromCachedEncoder(encoder);
315
+ }
316
+
230
317
  async onMaybeMissingEntries(properties: {
231
318
  entries: Map<string, EntryReplicated<D>>;
232
319
  targets: string[];
@@ -238,7 +325,6 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
238
325
  // such as those assigned to range boundaries.
239
326
 
240
327
  let entriesToSyncNaively: Map<string, EntryReplicated<D>> = new Map();
241
- let allCoordinatesToSyncWithIblt: bigint[] = [];
242
328
  let minSyncIbltSize = 333; // TODO: make configurable
243
329
  let maxSyncWithSimpleMethod = 1e3;
244
330
 
@@ -251,15 +337,59 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
251
337
  return;
252
338
  }
253
339
 
254
- await ribltReady;
255
-
256
- // Mixed strategy for larger batches
340
+ const nonBoundaryEntries: EntryReplicated<D>[] = [];
257
341
  for (const entry of properties.entries.values()) {
258
342
  if (entry.assignedToRangeBoundary) {
259
343
  entriesToSyncNaively.set(entry.hash, entry);
260
344
  } else {
261
- allCoordinatesToSyncWithIblt.push(coerceBigInt(entry.hashNumber));
345
+ nonBoundaryEntries.push(entry);
346
+ }
347
+ }
348
+
349
+ const priorityFn = this.properties.sync?.priority;
350
+ const maxSimpleEntries = this.properties.sync?.maxSimpleEntries;
351
+ const maxAdditionalNaive =
352
+ priorityFn &&
353
+ typeof maxSimpleEntries === "number" &&
354
+ Number.isFinite(maxSimpleEntries) &&
355
+ maxSimpleEntries > 0
356
+ ? Math.max(
357
+ 0,
358
+ Math.min(
359
+ Math.floor(maxSimpleEntries),
360
+ maxSyncWithSimpleMethod - entriesToSyncNaively.size,
361
+ ),
362
+ )
363
+ : 0;
364
+
365
+ if (priorityFn && maxAdditionalNaive > 0 && nonBoundaryEntries.length > 0) {
366
+ let index = 0;
367
+ const scored: {
368
+ entry: EntryReplicated<D>;
369
+ index: number;
370
+ priority: number;
371
+ }[] = [];
372
+ for (const entry of nonBoundaryEntries) {
373
+ const priorityValue = priorityFn(entry);
374
+ scored.push({
375
+ entry,
376
+ index,
377
+ priority: Number.isFinite(priorityValue) ? priorityValue : 0,
378
+ });
379
+ index += 1;
380
+ }
381
+ scored.sort((a, b) => b.priority - a.priority || a.index - b.index);
382
+ for (const { entry } of scored.slice(0, maxAdditionalNaive)) {
383
+ entriesToSyncNaively.set(entry.hash, entry);
384
+ }
385
+ }
386
+
387
+ let allCoordinatesToSyncWithIblt: bigint[] = [];
388
+ for (const entry of nonBoundaryEntries) {
389
+ if (entriesToSyncNaively.has(entry.hash)) {
390
+ continue;
262
391
  }
392
+ allCoordinatesToSyncWithIblt.push(coerceBigInt(entry.hashNumber));
263
393
  }
264
394
 
265
395
  if (entriesToSyncNaively.size > 0) {
@@ -275,31 +405,44 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
275
405
  entriesToSyncNaively.size > maxSyncWithSimpleMethod
276
406
  ) {
277
407
  // Fallback: if nothing left for IBLT (or simple set is too large), include all in IBLT
278
- allCoordinatesToSyncWithIblt = Array.from(
279
- properties.entries.values(),
280
- ).map((x) => coerceBigInt(x.hashNumber));
408
+ allCoordinatesToSyncWithIblt = [];
409
+ for (const entry of properties.entries.values()) {
410
+ allCoordinatesToSyncWithIblt.push(coerceBigInt(entry.hashNumber));
411
+ }
281
412
  }
282
413
 
283
414
  if (allCoordinatesToSyncWithIblt.length === 0) {
284
415
  return;
285
416
  }
286
417
 
287
- const sortedEntries = allCoordinatesToSyncWithIblt.sort((a, b) => {
288
- if (a > b) {
289
- return 1;
290
- } else if (a < b) {
291
- return -1;
292
- } else {
293
- return 0;
418
+ await ribltReady;
419
+
420
+ let sortedEntries: bigint[] | BigUint64Array;
421
+ if (typeof BigUint64Array !== "undefined") {
422
+ const typed = new BigUint64Array(allCoordinatesToSyncWithIblt.length);
423
+ for (let i = 0; i < allCoordinatesToSyncWithIblt.length; i++) {
424
+ typed[i] = allCoordinatesToSyncWithIblt[i];
294
425
  }
295
- });
426
+ typed.sort();
427
+ sortedEntries = typed;
428
+ } else {
429
+ sortedEntries = allCoordinatesToSyncWithIblt.sort((a, b) => {
430
+ if (a > b) {
431
+ return 1;
432
+ } else if (a < b) {
433
+ return -1;
434
+ } else {
435
+ return 0;
436
+ }
437
+ });
438
+ }
296
439
 
297
440
  // assume sorted, and find the largest gap
298
441
  let largestGap = 0n;
299
442
  let largestGapIndex = 0;
300
- for (let i = 0; i < sortedEntries.length - 1; i++) {
443
+ for (let i = 0; i < sortedEntries.length; i++) {
301
444
  const current = sortedEntries[i];
302
- const next = sortedEntries[i + 1];
445
+ const next = sortedEntries[(i + 1) % sortedEntries.length];
303
446
  const gap =
304
447
  next >= current
305
448
  ? next - current
@@ -329,8 +472,12 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
329
472
 
330
473
  const startSync = new StartSync({ from: start, to: end, symbols: [] });
331
474
  const encoder = new EncoderWrapper();
332
- for (const entry of sortedEntries) {
333
- encoder.add_symbol(coerceBigInt(entry));
475
+ if (typeof BigUint64Array !== "undefined" && sortedEntries instanceof BigUint64Array) {
476
+ encoder.add_symbols(sortedEntries);
477
+ } else {
478
+ for (const entry of sortedEntries) {
479
+ encoder.add_symbol(coerceBigInt(entry));
480
+ }
334
481
  }
335
482
 
336
483
  let initialSymbols = Math.round(
@@ -406,16 +553,12 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
406
553
  this.startedOrCompletedSynchronizations.add(syncId);
407
554
 
408
555
  const wrapped = message.end < message.start;
409
- const decoder = await buildEncoderOrDecoderFromRange(
410
- {
411
- start1: message.start,
412
- end1: wrapped ? this.properties.numbers.maxValue : message.end,
413
- start2: 0n,
414
- end2: wrapped ? message.end : 0n,
415
- },
416
- this.properties.entryIndex,
417
- "decoder",
418
- );
556
+ const decoder = await this.getLocalDecoderForRange({
557
+ start1: message.start,
558
+ end1: wrapped ? this.properties.numbers.maxValue : message.end,
559
+ start2: 0n,
560
+ end2: wrapped ? message.end : 0n,
561
+ });
419
562
 
420
563
  if (!decoder) {
421
564
  await this.simple.rpc.send(
@@ -620,10 +763,12 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
620
763
  }
621
764
 
622
765
  onEntryAdded(entry: Entry<any>): void {
766
+ this.invalidateLocalRangeEncoderCache();
623
767
  return this.simple.onEntryAdded(entry);
624
768
  }
625
769
 
626
770
  onEntryRemoved(hash: string) {
771
+ this.invalidateLocalRangeEncoderCache();
627
772
  return this.simple.onEntryRemoved(hash);
628
773
  }
629
774
 
@@ -642,6 +787,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
642
787
  for (const [, obj] of this.outgoingSyncProcesses) {
643
788
  obj.free();
644
789
  }
790
+ this.clearLocalRangeEncoderCache();
645
791
  return this.simple.close();
646
792
  }
647
793
 
@@ -16,7 +16,7 @@ import {
16
16
  } from "../exchange-heads.js";
17
17
  import { TransportMessage } from "../message.js";
18
18
  import type { EntryReplicated } from "../ranges.js";
19
- import type { SyncableKey, Syncronizer } from "./index.js";
19
+ import type { SyncableKey, SyncOptions, Syncronizer } from "./index.js";
20
20
 
21
21
  @variant([0, 1])
22
22
  export class RequestMaybeSync extends TransportMessage {
@@ -109,6 +109,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
109
109
  log: Log<any>;
110
110
  entryIndex: Index<EntryReplicated<R>, any>;
111
111
  coordinateToHash: Cache<string>;
112
+ private syncOptions?: SyncOptions<R>;
112
113
 
113
114
  // Syncing and dedeplucation work
114
115
  syncMoreInterval?: ReturnType<typeof setTimeout>;
@@ -120,6 +121,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
120
121
  entryIndex: Index<EntryReplicated<R>, any>;
121
122
  log: Log<any>;
122
123
  coordinateToHash: Cache<string>;
124
+ sync?: SyncOptions<R>;
123
125
  }) {
124
126
  this.syncInFlightQueue = new Map();
125
127
  this.syncInFlightQueueInverted = new Map();
@@ -128,14 +130,35 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
128
130
  this.log = properties.log;
129
131
  this.entryIndex = properties.entryIndex;
130
132
  this.coordinateToHash = properties.coordinateToHash;
133
+ this.syncOptions = properties.sync;
131
134
  }
132
135
 
133
136
  onMaybeMissingEntries(properties: {
134
137
  entries: Map<string, EntryReplicated<R>>;
135
138
  targets: string[];
136
139
  }): Promise<void> {
140
+ let hashes: string[];
141
+ const priorityFn = this.syncOptions?.priority;
142
+ if (priorityFn) {
143
+ let index = 0;
144
+ const scored: { hash: string; index: number; priority: number }[] = [];
145
+ for (const [hash, entry] of properties.entries) {
146
+ const priorityValue = priorityFn(entry);
147
+ scored.push({
148
+ hash,
149
+ index,
150
+ priority: Number.isFinite(priorityValue) ? priorityValue : 0,
151
+ });
152
+ index += 1;
153
+ }
154
+ scored.sort((a, b) => b.priority - a.priority || a.index - b.index);
155
+ hashes = scored.map((x) => x.hash);
156
+ } else {
157
+ hashes = [...properties.entries.keys()];
158
+ }
159
+
137
160
  return this.rpc.send(
138
- new RequestMaybeSync({ hashes: [...properties.entries.keys()] }),
161
+ new RequestMaybeSync({ hashes }),
139
162
  {
140
163
  priority: 1,
141
164
  mode: new SilentDelivery({ to: properties.targets, redundancy: 1 }),