@peerbit/shared-log 12.1.3 → 12.2.0-62829ef

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.
package/src/index.ts CHANGED
@@ -135,7 +135,11 @@ import {
135
135
  maxReplicas,
136
136
  } from "./replication.js";
137
137
  import { Observer, Replicator } from "./role.js";
138
- import type { SynchronizerConstructor, Syncronizer } from "./sync/index.js";
138
+ import type {
139
+ SyncOptions,
140
+ SynchronizerConstructor,
141
+ Syncronizer,
142
+ } from "./sync/index.js";
139
143
  import { RatelessIBLTSynchronizer } from "./sync/rateless-iblt.js";
140
144
  import { SimpleSyncronizer } from "./sync/simple.js";
141
145
  import { groupByGid } from "./utils.js";
@@ -149,6 +153,12 @@ export {
149
153
  };
150
154
  export { type CPUUsage, CPUUsageIntervalLag };
151
155
  export * from "./replication.js";
156
+ export type {
157
+ LogLike,
158
+ LogResultsIterator,
159
+ SharedLogLike,
160
+ SharedLogReplicationIndexLike,
161
+ } from "./like.js";
152
162
  export {
153
163
  type ReplicationRangeIndexable,
154
164
  ReplicationRangeIndexableU32,
@@ -352,6 +362,7 @@ export type SharedLogOptions<
352
362
  keep?: (
353
363
  entry: ShallowOrFullEntry<T> | EntryReplicated<R>,
354
364
  ) => Promise<boolean> | boolean;
365
+ sync?: SyncOptions<R>;
355
366
  syncronizer?: SynchronizerConstructor<R>;
356
367
  timeUntilRoleMaturity?: number;
357
368
  waitForReplicatorTimeout?: number;
@@ -2037,6 +2048,7 @@ export class SharedLog<
2037
2048
  rangeIndex: this._replicationRangeIndex,
2038
2049
  rpc: this.rpc,
2039
2050
  coordinateToHash: this.coordinateToHash,
2051
+ sync: options?.sync,
2040
2052
  });
2041
2053
  } else {
2042
2054
  if (
@@ -2048,6 +2060,7 @@ export class SharedLog<
2048
2060
  rpc: this.rpc,
2049
2061
  entryIndex: this.entryCoordinatesIndex,
2050
2062
  coordinateToHash: this.coordinateToHash,
2063
+ sync: options?.sync,
2051
2064
  });
2052
2065
  } else {
2053
2066
  if (this.domain.resolution === "u32") {
@@ -2063,6 +2076,7 @@ export class SharedLog<
2063
2076
  rangeIndex: this._replicationRangeIndex,
2064
2077
  rpc: this.rpc,
2065
2078
  coordinateToHash: this.coordinateToHash,
2079
+ sync: options?.sync,
2066
2080
  }) as Syncronizer<R>;
2067
2081
  }
2068
2082
  }
package/src/like.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type { PublicSignKey } from "@peerbit/crypto";
2
+ import type {
3
+ CountOptions,
4
+ IndexIterator,
5
+ IterateOptions,
6
+ } from "@peerbit/indexer-interface";
7
+ import type { Entry } from "@peerbit/log";
8
+ import type { ShallowEntry } from "@peerbit/log";
9
+ import type { ReplicationOptions, ReplicationRangeIndexable } from "./index.js";
10
+
11
+ export type LogBlocksLike = {
12
+ has: (hash: string) => Promise<boolean> | boolean;
13
+ };
14
+
15
+ export type LogResultsIterator<T> = {
16
+ close: () => void | Promise<void>;
17
+ next: (amount: number) => T[] | Promise<T[]>;
18
+ done: () => boolean | undefined;
19
+ all: () => T[] | Promise<T[]>;
20
+ };
21
+
22
+ export type LogLike<T = any> = {
23
+ idString?: string;
24
+ length: number;
25
+ get: (
26
+ hash: string,
27
+ options?: any,
28
+ ) => Promise<Entry<T> | undefined> | Entry<T> | undefined;
29
+ has: (hash: string) => Promise<boolean> | boolean;
30
+ getHeads: (resolve?: boolean) => LogResultsIterator<Entry<T> | ShallowEntry>;
31
+ toArray: () => Promise<Entry<T>[]>;
32
+ blocks?: LogBlocksLike;
33
+ };
34
+
35
+ export type SharedLogReplicationIndexLike<R extends "u32" | "u64" = any> = {
36
+ iterate: (
37
+ request?: IterateOptions,
38
+ ) => IndexIterator<ReplicationRangeIndexable<R>, undefined>;
39
+ count: (options?: CountOptions) => Promise<number> | number;
40
+ getSize?: () => Promise<number> | number;
41
+ };
42
+
43
+ export type SharedLogLike<T = any, R extends "u32" | "u64" = any> = {
44
+ closed?: boolean;
45
+ events: EventTarget;
46
+ log: LogLike<T>;
47
+ replicationIndex: SharedLogReplicationIndexLike<R>;
48
+ node?: { identity: { publicKey: PublicSignKey } };
49
+ getReplicators: () => Promise<Set<string>>;
50
+ waitForReplicator: (
51
+ publicKey: PublicSignKey,
52
+ options?: {
53
+ eager?: boolean;
54
+ roleAge?: number;
55
+ timeout?: number;
56
+ signal?: AbortSignal;
57
+ },
58
+ ) => Promise<void>;
59
+ waitForReplicators: (options?: {
60
+ timeout?: number;
61
+ roleAge?: number;
62
+ coverageThreshold?: number;
63
+ waitForNewPeers?: boolean;
64
+ signal?: AbortSignal;
65
+ }) => Promise<void>;
66
+ replicate: (
67
+ rangeOrEntry?: ReplicationOptions<R> | any,
68
+ options?: {
69
+ reset?: boolean;
70
+ checkDuplicates?: boolean;
71
+ rebalance?: boolean;
72
+ mergeSegments?: boolean;
73
+ },
74
+ ) => Promise<void | ReplicationRangeIndexable<R>[]>;
75
+ unreplicate: (rangeOrEntry?: { id: Uint8Array }[]) => Promise<void>;
76
+ calculateCoverage: (options?: {
77
+ start?: number | bigint;
78
+ end?: number | bigint;
79
+ roleAge?: number;
80
+ }) => Promise<number>;
81
+ getMyReplicationSegments: () => Promise<ReplicationRangeIndexable<R>[]>;
82
+ getAllReplicationSegments: () => Promise<ReplicationRangeIndexable<R>[]>;
83
+ close: () => Promise<void | boolean>;
84
+ };
package/src/sync/index.ts CHANGED
@@ -8,6 +8,24 @@ import type { Numbers } from "../integers.js";
8
8
  import type { TransportMessage } from "../message.js";
9
9
  import type { EntryReplicated, ReplicationRangeIndexable } from "../ranges.js";
10
10
 
11
+ export type SyncPriorityFn<R extends "u32" | "u64"> = (
12
+ entry: EntryReplicated<R>,
13
+ ) => number;
14
+
15
+ export type SyncOptions<R extends "u32" | "u64"> = {
16
+ /**
17
+ * Higher numbers are synced first.
18
+ * The callback should be fast and side-effect free.
19
+ */
20
+ priority?: SyncPriorityFn<R>;
21
+
22
+ /**
23
+ * When using rateless IBLT sync, optionally pre-sync up to this many
24
+ * high-priority entries using the simple synchronizer.
25
+ */
26
+ maxSimpleEntries?: number;
27
+ };
28
+
11
29
  export type SynchronizerComponents<R extends "u32" | "u64"> = {
12
30
  rpc: RPC<TransportMessage, TransportMessage>;
13
31
  rangeIndex: Index<ReplicationRangeIndexable<R>, any>;
@@ -15,6 +33,7 @@ export type SynchronizerComponents<R extends "u32" | "u64"> = {
15
33
  log: Log<any>;
16
34
  coordinateToHash: Cache<string>;
17
35
  numbers: Numbers<R>;
36
+ sync?: SyncOptions<R>;
18
37
  };
19
38
  export type SynchronizerConstructor<R extends "u32" | "u64"> = new (
20
39
  properties: SynchronizerComponents<R>,
@@ -4,18 +4,24 @@ import { type PublicSignKey, randomBytes, toBase64 } from "@peerbit/crypto";
4
4
  import { type Index } from "@peerbit/indexer-interface";
5
5
  import type { Entry, Log } from "@peerbit/log";
6
6
  import { logger as loggerFn } from "@peerbit/logger";
7
- import { DecoderWrapper, EncoderWrapper } from "@peerbit/riblt";
7
+ import {
8
+ DecoderWrapper,
9
+ EncoderWrapper,
10
+ ready as ribltReady,
11
+ } from "@peerbit/riblt";
8
12
  import type { RPC, RequestContext } from "@peerbit/rpc";
9
13
  import { SilentDelivery } from "@peerbit/stream-interface";
10
14
  import { type EntryWithRefs } from "../exchange-heads.js";
11
- import { type Numbers } from "../integers.js";
12
15
  import { TransportMessage } from "../message.js";
13
16
  import {
14
17
  type EntryReplicated,
15
- type ReplicationRangeIndexable,
16
18
  matchEntriesInRangeQuery,
17
19
  } from "../ranges.js";
18
- import type { SyncableKey, Syncronizer } from "./index.js";
20
+ import type {
21
+ SyncableKey,
22
+ SynchronizerComponents,
23
+ Syncronizer,
24
+ } from "./index.js";
19
25
  import { SimpleSyncronizer } from "./simple.js";
20
26
 
21
27
  export const logger = loggerFn("peerbit:shared-log:rateless");
@@ -142,6 +148,7 @@ const buildEncoderOrDecoderFromRange = async <
142
148
  entryIndex: Index<EntryReplicated<D>>,
143
149
  type: T,
144
150
  ): Promise<E | false> => {
151
+ await ribltReady;
145
152
  const encoder =
146
153
  type === "encoder" ? new EncoderWrapper() : new DecoderWrapper();
147
154
 
@@ -180,6 +187,13 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
180
187
  simple: SimpleSyncronizer<D>;
181
188
 
182
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
+
183
197
  ingoingSyncProcesses: Map<
184
198
  string,
185
199
  {
@@ -207,14 +221,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
207
221
  >;
208
222
 
209
223
  constructor(
210
- readonly properties: {
211
- rpc: RPC<TransportMessage, TransportMessage>;
212
- rangeIndex: Index<ReplicationRangeIndexable<D>, any>;
213
- entryIndex: Index<EntryReplicated<D>, any>;
214
- log: Log<any>;
215
- coordinateToHash: Cache<string>;
216
- numbers: Numbers<D>;
217
- },
224
+ readonly properties: SynchronizerComponents<D>,
218
225
  ) {
219
226
  this.simple = new SimpleSyncronizer(properties);
220
227
  this.outgoingSyncProcesses = new Map();
@@ -222,6 +229,91 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
222
229
  this.startedOrCompletedSynchronizations = new Cache({ max: 1e4 });
223
230
  }
224
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
+
225
317
  async onMaybeMissingEntries(properties: {
226
318
  entries: Map<string, EntryReplicated<D>>;
227
319
  targets: string[];
@@ -233,7 +325,6 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
233
325
  // such as those assigned to range boundaries.
234
326
 
235
327
  let entriesToSyncNaively: Map<string, EntryReplicated<D>> = new Map();
236
- let allCoordinatesToSyncWithIblt: bigint[] = [];
237
328
  let minSyncIbltSize = 333; // TODO: make configurable
238
329
  let maxSyncWithSimpleMethod = 1e3;
239
330
 
@@ -246,15 +337,57 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
246
337
  return;
247
338
  }
248
339
 
249
- // Mixed strategy for larger batches
340
+ const nonBoundaryEntries: EntryReplicated<D>[] = [];
250
341
  for (const entry of properties.entries.values()) {
251
342
  if (entry.assignedToRangeBoundary) {
252
343
  entriesToSyncNaively.set(entry.hash, entry);
253
344
  } else {
254
- allCoordinatesToSyncWithIblt.push(coerceBigInt(entry.hashNumber));
345
+ nonBoundaryEntries.push(entry);
255
346
  }
256
347
  }
257
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 = nonBoundaryEntries
388
+ .filter((entry) => !entriesToSyncNaively.has(entry.hash))
389
+ .map((entry) => coerceBigInt(entry.hashNumber));
390
+
258
391
  if (entriesToSyncNaively.size > 0) {
259
392
  // If there are special-case entries, sync them simply in parallel
260
393
  await this.simple.onMaybeMissingEntries({
@@ -277,6 +410,8 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
277
410
  return;
278
411
  }
279
412
 
413
+ await ribltReady;
414
+
280
415
  const sortedEntries = allCoordinatesToSyncWithIblt.sort((a, b) => {
281
416
  if (a > b) {
282
417
  return 1;
@@ -399,16 +534,12 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
399
534
  this.startedOrCompletedSynchronizations.add(syncId);
400
535
 
401
536
  const wrapped = message.end < message.start;
402
- const decoder = await buildEncoderOrDecoderFromRange(
403
- {
404
- start1: message.start,
405
- end1: wrapped ? this.properties.numbers.maxValue : message.end,
406
- start2: 0n,
407
- end2: wrapped ? message.end : 0n,
408
- },
409
- this.properties.entryIndex,
410
- "decoder",
411
- );
537
+ const decoder = await this.getLocalDecoderForRange({
538
+ start1: message.start,
539
+ end1: wrapped ? this.properties.numbers.maxValue : message.end,
540
+ start2: 0n,
541
+ end2: wrapped ? message.end : 0n,
542
+ });
412
543
 
413
544
  if (!decoder) {
414
545
  await this.simple.rpc.send(
@@ -613,10 +744,12 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
613
744
  }
614
745
 
615
746
  onEntryAdded(entry: Entry<any>): void {
747
+ this.invalidateLocalRangeEncoderCache();
616
748
  return this.simple.onEntryAdded(entry);
617
749
  }
618
750
 
619
751
  onEntryRemoved(hash: string) {
752
+ this.invalidateLocalRangeEncoderCache();
620
753
  return this.simple.onEntryRemoved(hash);
621
754
  }
622
755
 
@@ -635,6 +768,7 @@ export class RatelessIBLTSynchronizer<D extends "u32" | "u64">
635
768
  for (const [, obj] of this.outgoingSyncProcesses) {
636
769
  obj.free();
637
770
  }
771
+ this.clearLocalRangeEncoderCache();
638
772
  return this.simple.close();
639
773
  }
640
774
 
@@ -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 {
@@ -57,7 +57,7 @@ const getHashesFromSymbols = async (
57
57
  coordinateToHash: Cache<string>,
58
58
  ) => {
59
59
  let queries: IntegerCompare[] = [];
60
- let batchSize = 1; // TODO arg
60
+ let batchSize = 128; // TODO arg
61
61
  let results = new Set<string>();
62
62
  const handleBatch = async (end = false) => {
63
63
  if (queries.length >= batchSize || (end && queries.length > 0)) {
@@ -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 }),