@peerbit/shared-log 3.1.10 → 4.0.1

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
@@ -7,19 +7,16 @@ import {
7
7
  LogEvents,
8
8
  LogProperties
9
9
  } from "@peerbit/log";
10
- import { Program } from "@peerbit/program";
10
+ import { Program, ProgramEvents } from "@peerbit/program";
11
11
  import {
12
- BinaryReader,
13
12
  BinaryWriter,
14
13
  BorshError,
15
- deserialize,
16
14
  field,
17
15
  serialize,
18
16
  variant
19
17
  } from "@dao-xyz/borsh";
20
18
  import {
21
19
  AccessError,
22
- getPublicKeyFromPeerId,
23
20
  PublicSignKey,
24
21
  sha256,
25
22
  sha256Base64Sync
@@ -36,24 +33,34 @@ import {
36
33
  SubscriptionEvent,
37
34
  UnsubcriptionEvent
38
35
  } from "@peerbit/pubsub-interface";
39
- import { startsWith } from "@peerbit/uint8arrays";
40
- import { TimeoutError } from "@peerbit/time";
41
- import { REPLICATOR_TYPE_VARIANT, Observer, Replicator, Role } from "./role.js";
36
+ import { AbortError, delay, TimeoutError, waitFor } from "@peerbit/time";
37
+ import { Observer, Replicator, Role } from "./role.js";
42
38
  import {
43
39
  AbsoluteReplicas,
44
- MinReplicas,
45
40
  ReplicationError,
41
+ ReplicationLimits,
42
+ ReplicatorRect,
43
+ RequestRoleMessage,
44
+ ResponseRoleMessage,
46
45
  decodeReplicas,
47
46
  encodeReplicas,
47
+ hashToUniformNumber,
48
48
  maxReplicas
49
49
  } from "./replication.js";
50
50
  import pDefer, { DeferredPromise } from "p-defer";
51
51
  import { Cache } from "@peerbit/cache";
52
+ import { CustomEvent } from "@libp2p/interface";
53
+ import yallist from "yallist";
54
+ import { AcknowledgeDelivery, SilentDelivery } from "@peerbit/stream-interface";
55
+ import { AnyBlockStore, RemoteBlocks } from "@peerbit/blocks";
56
+ import { BlocksMessage } from "./blocks.js";
57
+ import debounce from "p-debounce";
52
58
 
53
59
  export * from "./replication.js";
60
+
54
61
  export { Observer, Replicator, Role };
55
62
 
56
- export const logger = loggerFn({ module: "peer" });
63
+ export const logger = loggerFn({ module: "shared-log" });
57
64
 
58
65
  const groupByGid = async <T extends Entry<any> | EntryWithRefs<any>>(
59
66
  entries: T[]
@@ -73,30 +80,80 @@ const groupByGid = async <T extends Entry<any> | EntryWithRefs<any>>(
73
80
  return groupByGid;
74
81
  };
75
82
 
76
- export type SyncFilter = (entries: Entry<any>) => Promise<boolean> | boolean;
77
-
78
- type ReplicationLimits = { min: MinReplicas; max?: MinReplicas };
79
83
  export type ReplicationLimitsOptions =
80
84
  | Partial<ReplicationLimits>
81
85
  | { min?: number; max?: number };
82
86
 
83
- export interface SharedLogOptions {
87
+ type StringRoleOptions = "observer" | "replicator";
88
+
89
+ export type ReplicationErrorFunction = (objectives: {
90
+ coverage: number;
91
+ balance: number;
92
+ memory: number;
93
+ }) => number;
94
+
95
+ type AdaptiveReplicatorOptions = {
96
+ type: "replicator";
97
+ limits?: { memory: number };
98
+ error?: ReplicationErrorFunction;
99
+ };
100
+
101
+ type FixedReplicatorOptions = {
102
+ type: "replicator";
103
+ factor: number;
104
+ };
105
+
106
+ type ObserverType = {
107
+ type: "observer";
108
+ };
109
+
110
+ export type RoleOptions =
111
+ | StringRoleOptions
112
+ | ObserverType
113
+ | FixedReplicatorOptions
114
+ | AdaptiveReplicatorOptions;
115
+
116
+ const isAdaptiveReplicatorOption = (
117
+ options: FixedReplicatorOptions | AdaptiveReplicatorOptions
118
+ ): options is AdaptiveReplicatorOptions => {
119
+ if (
120
+ (options as AdaptiveReplicatorOptions).limits ||
121
+ (options as AdaptiveReplicatorOptions).error ||
122
+ (options as FixedReplicatorOptions).factor == null
123
+ ) {
124
+ return true;
125
+ }
126
+ return false;
127
+ };
128
+
129
+ export type SharedLogOptions = {
130
+ role?: RoleOptions;
84
131
  replicas?: ReplicationLimitsOptions;
85
- sync?: SyncFilter;
86
- role?: Role;
87
132
  respondToIHaveTimeout?: number;
88
133
  canReplicate?: (publicKey: PublicSignKey) => Promise<boolean> | boolean;
89
- }
134
+ };
90
135
 
91
136
  export const DEFAULT_MIN_REPLICAS = 2;
137
+ export const WAIT_FOR_REPLICATOR_TIMEOUT = 9000;
138
+ export const WAIT_FOR_ROLE_MATURITY = 5000;
139
+ const REBALANCE_DEBOUNCE_INTERAVAL = 50;
92
140
 
93
141
  export type Args<T> = LogProperties<T> & LogEvents<T> & SharedLogOptions;
142
+
94
143
  export type SharedAppendOptions<T> = AppendOptions<T> & {
95
144
  replicas?: AbsoluteReplicas | number;
96
145
  };
97
146
 
147
+ type UpdateRoleEvent = { publicKey: PublicSignKey; role: Role };
148
+ export interface SharedLogEvents extends ProgramEvents {
149
+ role: CustomEvent<UpdateRoleEvent>;
150
+ }
151
+
98
152
  @variant("shared_log")
99
- export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
153
+ export class SharedLog<T = Uint8Array> extends Program<
154
+ Args<T>,
155
+ SharedLogEvents
156
+ > {
100
157
  @field({ type: Log })
101
158
  log: Log<T>;
102
159
 
@@ -104,22 +161,22 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
104
161
  rpc: RPC<TransportMessage, TransportMessage>;
105
162
 
106
163
  // options
107
- private _sync?: SyncFilter;
108
164
  private _role: Observer | Replicator;
165
+ private _roleOptions: AdaptiveReplicatorOptions | Observer | Replicator;
109
166
 
110
- private _sortedPeersCache: { hash: string; timestamp: number }[] | undefined;
111
- private _lastSubscriptionMessageId: number;
167
+ private _sortedPeersCache: yallist<ReplicatorRect> | undefined;
112
168
  private _gidPeersHistory: Map<string, Set<string>>;
113
169
 
114
170
  private _onSubscriptionFn: (arg: any) => any;
115
171
  private _onUnsubscriptionFn: (arg: any) => any;
116
172
 
117
173
  private _canReplicate?: (
118
- publicKey: PublicSignKey
174
+ publicKey: PublicSignKey,
175
+ role: Replicator
119
176
  ) => Promise<boolean> | boolean;
120
177
 
121
178
  private _logProperties?: LogProperties<T> & LogEvents<T>;
122
-
179
+ private _closeController: AbortController;
123
180
  private _loadedOnce = false;
124
181
  private _gidParentCache: Cache<Entry<any>[]>;
125
182
  private _respondToIHaveTimeout;
@@ -134,9 +191,19 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
134
191
 
135
192
  private _pendingIHave: Map<
136
193
  string,
137
- { clear: () => void; callback: () => void }
194
+ { clear: () => void; callback: (entry: Entry<T>) => void }
138
195
  >;
139
196
 
197
+ private latestRoleMessages: Map<string, bigint>;
198
+
199
+ private remoteBlocks: RemoteBlocks;
200
+
201
+ private openTime: number;
202
+
203
+ private rebalanceParticipationDebounced:
204
+ | ReturnType<typeof debounce>
205
+ | undefined;
206
+
140
207
  replicas: ReplicationLimits;
141
208
 
142
209
  constructor(properties?: { id?: Uint8Array }) {
@@ -149,37 +216,89 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
149
216
  return this._role;
150
217
  }
151
218
 
152
- async updateRole(role: Observer | Replicator) {
153
- const wasRepicators = this._role instanceof Replicator;
154
- this._role = role;
155
- await this.initializeWithRole();
156
- await this.rpc.subscribe(serialize(this._role));
219
+ private setupRole(options?: RoleOptions) {
220
+ this.rebalanceParticipationDebounced = undefined;
157
221
 
158
- if (wasRepicators) {
159
- await this.replicationReorganization();
160
- }
161
- }
222
+ const setupDebouncedRebalancing = (options?: AdaptiveReplicatorOptions) => {
223
+ this.replicationController = new ReplicationController({
224
+ targetMemoryLimit: options?.limits?.memory,
225
+ errorFunction: options?.error
226
+ });
162
227
 
163
- private async initializeWithRole() {
164
- try {
165
- await this.modifySortedSubscriptionCache(
166
- this._role instanceof Replicator ? true : false,
167
- getPublicKeyFromPeerId(this.node.peerId)
228
+ this.rebalanceParticipationDebounced = debounce(
229
+ () => this.rebalanceParticipation(),
230
+ REBALANCE_DEBOUNCE_INTERAVAL // TODO make dynamic
168
231
  );
232
+ };
169
233
 
170
- if (!this._loadedOnce) {
171
- await this.log.load();
172
- this._loadedOnce = true;
173
- }
174
- } catch (error) {
175
- if (error instanceof AccessError) {
176
- logger.error(
177
- "Failed to load all entries due to access error, make sure you are opening the program with approate keychain configuration"
178
- );
234
+ if (options instanceof Observer || options instanceof Replicator) {
235
+ throw new Error("Unsupported role option type");
236
+ } else if (options === "observer") {
237
+ this._roleOptions = new Observer();
238
+ } else if (options === "replicator") {
239
+ setupDebouncedRebalancing();
240
+ this._roleOptions = { type: options };
241
+ } else if (options) {
242
+ if (options.type === "replicator") {
243
+ if (isAdaptiveReplicatorOption(options)) {
244
+ setupDebouncedRebalancing(options);
245
+ this._roleOptions = options;
246
+ } else {
247
+ this._roleOptions = new Replicator({ factor: options.factor });
248
+ }
179
249
  } else {
180
- throw error;
250
+ this._roleOptions = new Observer();
181
251
  }
252
+ } else {
253
+ // Default option
254
+ setupDebouncedRebalancing();
255
+ this._roleOptions = { type: "replicator" };
182
256
  }
257
+
258
+ // setup the initial role
259
+ if (
260
+ this._roleOptions instanceof Replicator ||
261
+ this._roleOptions instanceof Observer
262
+ ) {
263
+ this._role = this._roleOptions; // Fixed
264
+ } else {
265
+ this._role = new Replicator({
266
+ // initial role in a dynamic setup
267
+ factor: 1,
268
+ timestamp: BigInt(+new Date())
269
+ });
270
+ }
271
+
272
+ return this._role;
273
+ }
274
+
275
+ async updateRole(role: RoleOptions, onRoleChange = true) {
276
+ return this._updateRole(this.setupRole(role), onRoleChange);
277
+ }
278
+
279
+ private async _updateRole(
280
+ role: Observer | Replicator = this._role,
281
+ onRoleChange = true
282
+ ) {
283
+ this._role = role;
284
+ const { changed } = await this._modifyReplicators(
285
+ this.role,
286
+ this.node.identity.publicKey
287
+ );
288
+
289
+ if (!this._loadedOnce) {
290
+ await this.log.load();
291
+ this._loadedOnce = true;
292
+ }
293
+ await this.rpc.subscribe();
294
+
295
+ await this.rpc.send(new ResponseRoleMessage(role));
296
+
297
+ if (onRoleChange && changed) {
298
+ this.onRoleChange(undefined, this._role, this.node.identity.publicKey);
299
+ }
300
+
301
+ return changed;
183
302
  }
184
303
 
185
304
  async append(
@@ -207,14 +326,27 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
207
326
  }
208
327
 
209
328
  const result = await this.log.append(data, appendOptions);
329
+ const leaders = await this.findLeaders(
330
+ result.entry.meta.gid,
331
+ decodeReplicas(result.entry).getValue(this)
332
+ );
333
+ const isLeader = leaders.includes(this.node.identity.publicKey.hashcode());
210
334
 
211
335
  await this.rpc.send(
212
336
  await createExchangeHeadsMessage(
213
337
  this.log,
214
338
  [result.entry],
215
339
  this._gidParentCache
216
- )
340
+ ),
341
+ {
342
+ mode: isLeader
343
+ ? new SilentDelivery({ redundancy: 1, to: leaders })
344
+ : new AcknowledgeDelivery({ redundancy: 1, to: leaders })
345
+ }
217
346
  );
347
+
348
+ this.rebalanceParticipationDebounced?.();
349
+
218
350
  return result;
219
351
  }
220
352
 
@@ -231,21 +363,23 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
231
363
  : options.replicas.max
232
364
  : undefined
233
365
  };
366
+
234
367
  this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 10 * 1000; // TODO make into arg
235
368
  this._pendingDeletes = new Map();
236
369
  this._pendingIHave = new Map();
370
+ this.latestRoleMessages = new Map();
371
+ this.openTime = +new Date();
237
372
 
238
373
  this._gidParentCache = new Cache({ max: 1000 });
374
+ this._closeController = new AbortController();
239
375
 
240
376
  this._canReplicate = options?.canReplicate;
241
- this._sync = options?.sync;
242
377
  this._logProperties = options;
243
- this._role = options?.role || new Replicator();
244
378
 
245
- this._lastSubscriptionMessageId = 0;
246
- this._onSubscriptionFn = this._onSubscription.bind(this);
379
+ this.setupRole(options?.role);
247
380
 
248
- this._sortedPeersCache = [];
381
+ this._onSubscriptionFn = this._onSubscription.bind(this);
382
+ this._sortedPeersCache = yallist.create();
249
383
  this._gidPeersHistory = new Map();
250
384
 
251
385
  await this.node.services.pubsub.addEventListener(
@@ -259,9 +393,26 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
259
393
  this._onUnsubscriptionFn
260
394
  );
261
395
 
262
- await this.log.open(this.node.services.blocks, this.node.identity, {
263
- keychain: this.node.keychain,
396
+ const id = sha256Base64Sync(this.log.id);
397
+ const storage = await this.node.memory.sublevel(id);
398
+ const localBlocks = await new AnyBlockStore(
399
+ await storage.sublevel("blocks")
400
+ );
401
+ const cache = await storage.sublevel("cache");
402
+
403
+ this.remoteBlocks = new RemoteBlocks({
404
+ local: localBlocks,
405
+ publish: (message, options) =>
406
+ this.rpc.send(new BlocksMessage(message), {
407
+ to: options?.to
408
+ }),
409
+ waitFor: this.rpc.waitFor.bind(this.rpc)
410
+ });
264
411
 
412
+ await this.remoteBlocks.start();
413
+
414
+ await this.log.open(this.remoteBlocks, this.node.identity, {
415
+ keychain: this.node.services.keychain,
265
416
  ...this._logProperties,
266
417
  onChange: (change) => {
267
418
  if (this._pendingIHave.size > 0) {
@@ -269,7 +420,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
269
420
  const ih = this._pendingIHave.get(added.hash);
270
421
  if (ih) {
271
422
  ih.clear();
272
- ih.callback();
423
+ ih.callback(added);
273
424
  }
274
425
  }
275
426
  }
@@ -304,42 +455,37 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
304
455
  return this._logProperties?.canAppend?.(entry) ?? true;
305
456
  },
306
457
  trim: this._logProperties?.trim && {
307
- ...this._logProperties?.trim,
308
- filter: {
309
- canTrim: async (entry) =>
310
- !(await this.isLeader(
311
- entry.meta.gid,
312
- decodeReplicas(entry).getValue(this)
313
- )), // TODO types
314
- cacheId: () => this._lastSubscriptionMessageId
315
- }
458
+ ...this._logProperties?.trim
316
459
  },
317
- cache:
318
- this.node.memory &&
319
- (await this.node.memory.sublevel(sha256Base64Sync(this.log.id)))
460
+ cache: cache
320
461
  });
321
462
 
322
- await this.initializeWithRole();
463
+ // Open for communcation
464
+ await this.rpc.open({
465
+ queryType: TransportMessage,
466
+ responseType: TransportMessage,
467
+ responseHandler: this._onMessage.bind(this),
468
+ topic: this.topic
469
+ });
470
+
471
+ await this._updateRole();
472
+ await this.rebalanceParticipation();
323
473
 
324
474
  // Take into account existing subscription
325
475
  (await this.node.services.pubsub.getSubscribers(this.topic))?.forEach(
326
476
  (v, k) => {
327
- this.handleSubscriptionChange(
328
- v.publicKey,
329
- [{ topic: this.topic, data: v.data }],
330
- true
331
- );
477
+ if (v.equals(this.node.identity.publicKey)) {
478
+ return;
479
+ }
480
+ this.handleSubscriptionChange(v, [this.topic], true);
332
481
  }
333
482
  );
483
+ }
334
484
 
335
- // Open for communcation
336
- await this.rpc.open({
337
- queryType: TransportMessage,
338
- responseType: TransportMessage,
339
- responseHandler: this._onMessage.bind(this),
340
- topic: this.topic,
341
- subscriptionData: serialize(this.role)
342
- });
485
+ async getMemoryUsage() {
486
+ return (
487
+ ((await this.log.memory?.size()) || 0) + (await this.log.blocks.size())
488
+ );
343
489
  }
344
490
 
345
491
  get topic() {
@@ -347,6 +493,19 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
347
493
  }
348
494
 
349
495
  private async _close() {
496
+ this._closeController.abort();
497
+
498
+ this.node.services.pubsub.removeEventListener(
499
+ "subscribe",
500
+ this._onSubscriptionFn
501
+ );
502
+
503
+ this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
504
+ this.node.services.pubsub.removeEventListener(
505
+ "unsubscribe",
506
+ this._onUnsubscriptionFn
507
+ );
508
+
350
509
  for (const [k, v] of this._pendingDeletes) {
351
510
  v.clear();
352
511
  v.promise.resolve(); // TODO or reject?
@@ -355,24 +514,15 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
355
514
  v.clear();
356
515
  }
357
516
 
517
+ await this.remoteBlocks.stop();
358
518
  this._gidParentCache.clear();
359
519
  this._pendingDeletes = new Map();
360
520
  this._pendingIHave = new Map();
521
+ this.latestRoleMessages.clear();
361
522
 
362
523
  this._gidPeersHistory = new Map();
363
524
  this._sortedPeersCache = undefined;
364
525
  this._loadedOnce = false;
365
-
366
- this.node.services.pubsub.removeEventListener(
367
- "subscribe",
368
- this._onSubscriptionFn
369
- );
370
-
371
- this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
372
- this.node.services.pubsub.removeEventListener(
373
- "unsubscribe",
374
- this._onUnsubscriptionFn
375
- );
376
526
  }
377
527
  async close(from?: Program): Promise<boolean> {
378
528
  const superClosed = await super.close(from);
@@ -389,8 +539,8 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
389
539
  if (!superDropped) {
390
540
  return superDropped;
391
541
  }
392
- await this._close();
393
542
  await this.log.drop();
543
+ await this._close();
394
544
  return true;
395
545
  }
396
546
 
@@ -411,7 +561,6 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
411
561
  */
412
562
 
413
563
  const { heads } = msg;
414
- // replication topic === trustedNetwork address
415
564
 
416
565
  logger.debug(
417
566
  `${this.node.identity.publicKey.hashcode()}: Recieved heads: ${
@@ -431,26 +580,32 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
431
580
  }
432
581
  }
433
582
 
434
- if (!this._sync) {
435
- const toMerge: EntryWithRefs<any>[] = [];
583
+ if (filteredHeads.length === 0) {
584
+ return;
585
+ }
436
586
 
437
- let toDelete: Entry<any>[] | undefined = undefined;
438
- let maybeDelete: EntryWithRefs<any>[][] | undefined = undefined;
587
+ const toMerge: EntryWithRefs<any>[] = [];
588
+ let toDelete: Entry<any>[] | undefined = undefined;
589
+ let maybeDelete: EntryWithRefs<any>[][] | undefined = undefined;
439
590
 
440
- const groupedByGid = await groupByGid(filteredHeads);
591
+ const groupedByGid = await groupByGid(filteredHeads);
592
+ const promises: Promise<void>[] = [];
441
593
 
442
- for (const [gid, entries] of groupedByGid) {
594
+ for (const [gid, entries] of groupedByGid) {
595
+ const fn = async () => {
443
596
  const headsWithGid = this.log.headsIndex.gids.get(gid);
597
+
444
598
  const maxReplicasFromHead =
445
599
  headsWithGid && headsWithGid.size > 0
446
600
  ? maxReplicas(this, [...headsWithGid.values()])
447
601
  : this.replicas.min.getValue(this);
448
602
 
449
- const maxReplicasFromNewEntries = maxReplicas(this, [
450
- ...entries.map((x) => x.entry)
451
- ]);
603
+ const maxReplicasFromNewEntries = maxReplicas(
604
+ this,
605
+ entries.map((x) => x.entry)
606
+ );
452
607
 
453
- const isLeader = await this.isLeader(
608
+ const isLeader = await this.waitForIsLeader(
454
609
  gid,
455
610
  Math.max(maxReplicasFromHead, maxReplicasFromNewEntries)
456
611
  );
@@ -481,50 +636,49 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
481
636
  }. Because not leader`
482
637
  );
483
638
  }
484
- }
639
+ };
640
+ promises.push(fn());
641
+ }
642
+ await Promise.all(promises);
485
643
 
486
- if (toMerge.length > 0) {
487
- await this.log.join(toMerge);
488
- toDelete &&
489
- Promise.all(this.pruneSafely(toDelete)).catch((e) => {
490
- logger.error(e.toString());
491
- });
492
- }
644
+ if (this.closed) {
645
+ return;
646
+ }
493
647
 
494
- if (maybeDelete) {
495
- for (const entries of maybeDelete) {
496
- const headsWithGid = this.log.headsIndex.gids.get(
497
- entries[0].entry.meta.gid
648
+ if (toMerge.length > 0) {
649
+ await this.log.join(toMerge);
650
+ toDelete &&
651
+ this.prune(toDelete).catch((e) => {
652
+ logger.error(e.toString());
653
+ });
654
+ this.rebalanceParticipationDebounced?.();
655
+ }
656
+
657
+ if (maybeDelete) {
658
+ for (const entries of maybeDelete as EntryWithRefs<any>[][]) {
659
+ const headsWithGid = this.log.headsIndex.gids.get(
660
+ entries[0].entry.meta.gid
661
+ );
662
+ if (headsWithGid && headsWithGid.size > 0) {
663
+ const minReplicas = maxReplicas(this, headsWithGid.values());
664
+
665
+ const isLeader = await this.isLeader(
666
+ entries[0].entry.meta.gid,
667
+ minReplicas
498
668
  );
499
- if (headsWithGid && headsWithGid.size > 0) {
500
- const minReplicas = maxReplicas(this, [
501
- ...headsWithGid.values()
502
- ]);
503
-
504
- const isLeader = await this.isLeader(
505
- entries[0].entry.meta.gid,
506
- minReplicas
507
- );
508
- if (!isLeader) {
509
- Promise.all(
510
- this.pruneSafely(entries.map((x) => x.entry))
511
- ).catch((e) => {
512
- logger.error(e.toString());
513
- });
514
- }
669
+
670
+ if (!isLeader) {
671
+ this.prune(entries.map((x) => x.entry)).catch((e) => {
672
+ logger.error(e.toString());
673
+ });
515
674
  }
516
675
  }
517
676
  }
518
- } else {
519
- await this.log.join(
520
- await Promise.all(
521
- filteredHeads.map((x) => this._sync!(x.entry))
522
- ).then((filter) => filteredHeads.filter((v, ix) => filter[ix]))
523
- );
524
677
  }
525
678
  }
526
679
  } else if (msg instanceof RequestIHave) {
527
680
  const hasAndIsLeader: string[] = [];
681
+
528
682
  for (const hash of msg.hashes) {
529
683
  const indexedEntry = this.log.entryIndex.getShallow(hash);
530
684
  if (
@@ -542,12 +696,21 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
542
696
  clearTimeout(timeout);
543
697
  prevPendingIHave?.clear();
544
698
  },
545
- callback: () => {
546
- prevPendingIHave && prevPendingIHave.callback();
547
- this.rpc.send(new ResponseIHave({ hashes: [hash] }), {
548
- to: [context.from!]
549
- });
550
- this._pendingIHave.delete(hash);
699
+ callback: async (entry) => {
700
+ if (
701
+ await this.isLeader(
702
+ entry.meta.gid,
703
+ decodeReplicas(entry).getValue(this)
704
+ )
705
+ ) {
706
+ this.rpc.send(new ResponseIHave({ hashes: [entry.hash] }), {
707
+ to: [context.from!]
708
+ });
709
+ }
710
+
711
+ prevPendingIHave && prevPendingIHave.callback(entry);
712
+
713
+ this._pendingIHave.delete(entry.hash);
551
714
  }
552
715
  };
553
716
  const timeout = setTimeout(() => {
@@ -560,17 +723,71 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
560
723
  this._pendingIHave.set(hash, pendingIHave);
561
724
  }
562
725
  }
563
- this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
726
+ await this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
564
727
  to: [context.from!]
565
728
  });
566
729
  } else if (msg instanceof ResponseIHave) {
567
730
  for (const hash of msg.hashes) {
568
731
  this._pendingDeletes.get(hash)?.callback(context.from!.hashcode());
569
732
  }
733
+ } else if (msg instanceof BlocksMessage) {
734
+ await this.remoteBlocks.onMessage(msg.message);
735
+ } else if (msg instanceof RequestRoleMessage) {
736
+ if (!context.from) {
737
+ throw new Error("Missing form in update role message");
738
+ }
739
+
740
+ if (context.from.equals(this.node.identity.publicKey)) {
741
+ return;
742
+ }
743
+
744
+ await this.rpc.send(new ResponseRoleMessage({ role: this.role }), {
745
+ to: [context.from!]
746
+ });
747
+ } else if (msg instanceof ResponseRoleMessage) {
748
+ if (!context.from) {
749
+ throw new Error("Missing form in update role message");
750
+ }
751
+
752
+ if (context.from.equals(this.node.identity.publicKey)) {
753
+ return;
754
+ }
755
+
756
+ this.waitFor(context.from, {
757
+ signal: this._closeController.signal,
758
+ timeout: WAIT_FOR_REPLICATOR_TIMEOUT
759
+ })
760
+ .then(async () => {
761
+ /* await delay(1000 * Math.random()) */
762
+
763
+ const prev = this.latestRoleMessages.get(context.from!.hashcode());
764
+ if (prev && prev > context.timestamp) {
765
+ return;
766
+ }
767
+ this.latestRoleMessages.set(
768
+ context.from!.hashcode(),
769
+ context.timestamp
770
+ );
771
+
772
+ await this.modifyReplicators(msg.role, context.from!);
773
+ })
774
+ .catch((e) => {
775
+ if (e instanceof AbortError) {
776
+ return;
777
+ }
778
+
779
+ logger.error(
780
+ "Failed to find peer who updated their role: " + e?.message
781
+ );
782
+ });
570
783
  } else {
571
784
  throw new Error("Unexpected message");
572
785
  }
573
786
  } catch (e: any) {
787
+ if (e instanceof AbortError) {
788
+ return;
789
+ }
790
+
574
791
  if (e instanceof BorshError) {
575
792
  logger.trace(
576
793
  `${this.node.identity.publicKey.hashcode()}: Failed to handle message on topic: ${JSON.stringify(
@@ -579,6 +796,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
579
796
  );
580
797
  return;
581
798
  }
799
+
582
800
  if (e instanceof AccessError) {
583
801
  logger.trace(
584
802
  `${this.node.identity.publicKey.hashcode()}: Failed to handle message for log: ${JSON.stringify(
@@ -591,41 +809,89 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
591
809
  }
592
810
  }
593
811
 
594
- getReplicatorsSorted(): { hash: string; timestamp: number }[] | undefined {
812
+ getReplicatorsSorted(): yallist<ReplicatorRect> | undefined {
595
813
  return this._sortedPeersCache;
596
814
  }
597
815
 
816
+ async waitForReplicator(...keys: PublicSignKey[]) {
817
+ const check = () => {
818
+ for (const k of keys) {
819
+ if (
820
+ !this.getReplicatorsSorted()
821
+ ?.toArray()
822
+ ?.find((x) => x.publicKey.equals(k))
823
+ ) {
824
+ return false;
825
+ }
826
+ }
827
+ return true;
828
+ };
829
+ return waitFor(() => check(), { signal: this._closeController.signal });
830
+ }
831
+
598
832
  async isLeader(
599
833
  slot: { toString(): string },
600
- numberOfLeaders: number
834
+ numberOfLeaders: number,
835
+ options?: {
836
+ candidates?: string[];
837
+ roleAge?: number;
838
+ }
601
839
  ): Promise<boolean> {
602
- const isLeader = (await this.findLeaders(slot, numberOfLeaders)).find(
603
- (l) => l === this.node.identity.publicKey.hashcode()
604
- );
840
+ const isLeader = (
841
+ await this.findLeaders(slot, numberOfLeaders, options)
842
+ ).find((l) => l === this.node.identity.publicKey.hashcode());
605
843
  return !!isLeader;
606
844
  }
607
845
 
846
+ private async waitForIsLeader(
847
+ slot: { toString(): string },
848
+ numberOfLeaders: number,
849
+ timeout = WAIT_FOR_REPLICATOR_TIMEOUT
850
+ ): Promise<boolean> {
851
+ return new Promise((res, rej) => {
852
+ const removeListeners = () => {
853
+ this.events.removeEventListener("role", roleListener);
854
+ this._closeController.signal.addEventListener("abort", abortListener);
855
+ };
856
+ const abortListener = () => {
857
+ removeListeners();
858
+ clearTimeout(timer);
859
+ res(false);
860
+ };
861
+
862
+ const timer = setTimeout(() => {
863
+ removeListeners();
864
+ res(false);
865
+ }, timeout);
866
+
867
+ const check = () =>
868
+ this.isLeader(slot, numberOfLeaders).then((isLeader) => {
869
+ if (isLeader) {
870
+ removeListeners();
871
+ clearTimeout(timer);
872
+ res(isLeader);
873
+ }
874
+ });
875
+
876
+ const roleListener = () => {
877
+ check();
878
+ };
879
+ this.events.addEventListener("role", roleListener);
880
+ this._closeController.signal.addEventListener("abort", abortListener);
881
+
882
+ check();
883
+ });
884
+ }
885
+
608
886
  async findLeaders(
609
887
  subject: { toString(): string },
610
- numberOfLeadersUnbounded: number
888
+ numberOfLeaders: number,
889
+ options?: {
890
+ roleAge?: number;
891
+ }
611
892
  ): Promise<string[]> {
612
- const lower = this.replicas.min.getValue(this);
613
- const higher = this.replicas.max?.getValue(this) ?? Number.MAX_SAFE_INTEGER;
614
- let numberOfLeaders = Math.max(
615
- Math.min(higher, numberOfLeadersUnbounded),
616
- lower
617
- );
618
-
619
893
  // For a fixed set or members, the choosen leaders will always be the same (address invariant)
620
894
  // This allows for that same content is always chosen to be distributed to same peers, to remove unecessary copies
621
- const peers: { hash: string; timestamp: number }[] =
622
- this.getReplicatorsSorted() || [];
623
-
624
- if (peers.length === 0) {
625
- return [];
626
- }
627
-
628
- numberOfLeaders = Math.min(numberOfLeaders, peers.length);
629
895
 
630
896
  // Convert this thing we wan't to distribute to 8 bytes so we get can convert it into a u64
631
897
  // modulus into an index
@@ -634,31 +900,220 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
634
900
  const seed = await sha256(utf8writer.finalize());
635
901
 
636
902
  // convert hash of slot to a number
637
- const seedNumber = new BinaryReader(
638
- seed.subarray(seed.length - 8, seed.length)
639
- ).u64();
640
- const startIndex = Number(seedNumber % BigInt(peers.length));
641
-
642
- // we only step forward 1 step (ignoring that step backward 1 could be 'closer')
643
- // This does not matter, we only have to make sure all nodes running the code comes to somewhat the
644
- // same conclusion (are running the same leader selection algorithm)
645
- const leaders = new Array(numberOfLeaders);
903
+ const cursor = hashToUniformNumber(seed); // bounded between 0 and 1
904
+ return this.findLeadersFromUniformNumber(cursor, numberOfLeaders, options);
905
+ }
906
+
907
+ private findLeadersFromUniformNumber(
908
+ cursor: number,
909
+ numberOfLeaders: number,
910
+ options?: {
911
+ roleAge?: number;
912
+ }
913
+ ) {
914
+ const leaders: Set<string> = new Set();
915
+ const width = 1; // this.getParticipationSum(roleAge);
916
+ const peers = this.getReplicatorsSorted();
917
+ if (!peers || peers?.length === 0) {
918
+ return [];
919
+ }
920
+ numberOfLeaders = Math.min(numberOfLeaders, peers.length);
921
+
922
+ const t = +new Date();
923
+ const roleAge =
924
+ options?.roleAge ??
925
+ Math.min(WAIT_FOR_ROLE_MATURITY, +new Date() - this.openTime);
926
+
646
927
  for (let i = 0; i < numberOfLeaders; i++) {
647
- leaders[i] = peers[(i + startIndex) % peers.length].hash;
928
+ let matured = 0;
929
+ const maybeIncrementMatured = (role: Replicator) => {
930
+ if (t - Number(role.timestamp) > roleAge) {
931
+ matured++;
932
+ }
933
+ };
934
+
935
+ const x = ((cursor + i / numberOfLeaders) % 1) * width;
936
+ let currentNode = peers.head;
937
+ const diffs: { diff: number; rect: ReplicatorRect }[] = [];
938
+ while (currentNode) {
939
+ const start = currentNode.value.offset % width;
940
+ const absDelta = Math.abs(start - x);
941
+ const diff = Math.min(absDelta, width - absDelta);
942
+
943
+ if (diff < currentNode.value.role.factor / 2 + 0.00001) {
944
+ leaders.add(currentNode.value.publicKey.hashcode());
945
+ maybeIncrementMatured(currentNode.value.role);
946
+ } else {
947
+ diffs.push({
948
+ diff:
949
+ currentNode.value.role.factor > 0
950
+ ? diff / currentNode.value.role.factor
951
+ : Number.MAX_SAFE_INTEGER,
952
+ rect: currentNode.value
953
+ });
954
+ }
955
+
956
+ currentNode = currentNode.next;
957
+ }
958
+
959
+ if (matured === 0) {
960
+ diffs.sort((x, y) => x.diff - y.diff);
961
+ for (const node of diffs) {
962
+ leaders.add(node.rect.publicKey.hashcode());
963
+ maybeIncrementMatured(node.rect.role);
964
+ if (matured > 0) {
965
+ break;
966
+ }
967
+ }
968
+ }
969
+ }
970
+
971
+ return [...leaders];
972
+ }
973
+
974
+ /**
975
+ *
976
+ * @returns groups where at least one in any group will have the entry you are looking for
977
+ */
978
+ getReplicatorUnion(roleAge: number = WAIT_FOR_ROLE_MATURITY) {
979
+ // Total replication "width"
980
+ const width = 1; //this.getParticipationSum(roleAge);
981
+
982
+ // How much width you need to "query" to
983
+
984
+ const peers = this.getReplicatorsSorted()!; // TODO types
985
+ const minReplicas = Math.min(
986
+ peers.length,
987
+ this.replicas.min.getValue(this)
988
+ );
989
+ const coveringWidth = width / minReplicas;
990
+
991
+ let walker = peers.head;
992
+ if (this.role instanceof Replicator) {
993
+ // start at our node (local first)
994
+ while (walker) {
995
+ if (walker.value.publicKey.equals(this.node.identity.publicKey)) {
996
+ break;
997
+ }
998
+ walker = walker.next;
999
+ }
1000
+ } else {
1001
+ const seed = Math.round(peers.length * Math.random()); // start at a random point
1002
+ for (let i = 0; i < seed - 1; i++) {
1003
+ if (walker?.next == null) {
1004
+ break;
1005
+ }
1006
+ walker = walker.next;
1007
+ }
1008
+ }
1009
+
1010
+ const set: string[] = [];
1011
+ let distance = 0;
1012
+ const startNode = walker;
1013
+ if (!startNode) {
1014
+ return [];
1015
+ }
1016
+
1017
+ let nextPoint = startNode.value.offset;
1018
+ const t = +new Date();
1019
+ while (walker && distance < coveringWidth) {
1020
+ const absDelta = Math.abs(walker!.value.offset - nextPoint);
1021
+ const diff = Math.min(absDelta, width - absDelta);
1022
+
1023
+ if (diff < walker!.value.role.factor / 2 + 0.00001) {
1024
+ set.push(walker!.value.publicKey.hashcode());
1025
+ if (
1026
+ t - Number(walker!.value.role.timestamp) >
1027
+ roleAge /* ||
1028
+ walker!.value.publicKey.equals(this.node.identity.publicKey)) */
1029
+ ) {
1030
+ nextPoint = (nextPoint + walker!.value.role.factor) % 1;
1031
+ distance += walker!.value.role.factor;
1032
+ }
1033
+ }
1034
+
1035
+ walker = walker.next || peers.head;
1036
+
1037
+ if (
1038
+ walker?.value.publicKey &&
1039
+ startNode?.value.publicKey.equals(walker?.value.publicKey)
1040
+ ) {
1041
+ break; // TODO throw error for failing to fetch ffull width
1042
+ }
1043
+ }
1044
+
1045
+ return set;
1046
+ }
1047
+
1048
+ async replicator(
1049
+ entry: Entry<any>,
1050
+ options?: {
1051
+ candidates?: string[];
1052
+ roleAge?: number;
648
1053
  }
649
- return leaders;
1054
+ ) {
1055
+ return this.isLeader(
1056
+ entry.gid,
1057
+ decodeReplicas(entry).getValue(this),
1058
+ options
1059
+ );
650
1060
  }
651
1061
 
652
- private async modifySortedSubscriptionCache(
653
- subscribed: boolean,
1062
+ private onRoleChange(
1063
+ prev: Observer | Replicator | undefined,
1064
+ role: Observer | Replicator,
654
1065
  publicKey: PublicSignKey
655
1066
  ) {
1067
+ if (this.closed) {
1068
+ return;
1069
+ }
1070
+
1071
+ this.distribute();
1072
+
1073
+ if (role instanceof Replicator) {
1074
+ const timer = setTimeout(async () => {
1075
+ this._closeController.signal.removeEventListener("abort", listener);
1076
+ await this.rebalanceParticipationDebounced?.();
1077
+ this.distribute();
1078
+ }, WAIT_FOR_ROLE_MATURITY + 2000);
1079
+
1080
+ const listener = () => {
1081
+ clearTimeout(timer);
1082
+ };
1083
+
1084
+ this._closeController.signal.addEventListener("abort", listener);
1085
+ }
1086
+
1087
+ this.events.dispatchEvent(
1088
+ new CustomEvent<UpdateRoleEvent>("role", {
1089
+ detail: { publicKey, role }
1090
+ })
1091
+ );
1092
+ }
1093
+
1094
+ private async modifyReplicators(
1095
+ role: Observer | Replicator,
1096
+ publicKey: PublicSignKey
1097
+ ) {
1098
+ const { prev, changed } = await this._modifyReplicators(role, publicKey);
1099
+ if (changed) {
1100
+ await this.rebalanceParticipationDebounced?.(); // await this.rebalanceParticipation(false);
1101
+ this.onRoleChange(prev, role, publicKey);
1102
+ return true;
1103
+ }
1104
+ return false;
1105
+ }
1106
+
1107
+ private async _modifyReplicators(
1108
+ role: Observer | Replicator,
1109
+ publicKey: PublicSignKey
1110
+ ): Promise<{ prev?: Replicator; changed: boolean }> {
656
1111
  if (
657
- subscribed &&
1112
+ role instanceof Replicator &&
658
1113
  this._canReplicate &&
659
- !(await this._canReplicate(publicKey))
1114
+ !(await this._canReplicate(publicKey, role))
660
1115
  ) {
661
- return false;
1116
+ return { changed: false };
662
1117
  }
663
1118
 
664
1119
  const sortedPeer = this._sortedPeersCache;
@@ -666,81 +1121,125 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
666
1121
  if (this.closed === false) {
667
1122
  throw new Error("Unexpected, sortedPeersCache is undefined");
668
1123
  }
669
- return false;
1124
+ return { changed: false };
670
1125
  }
671
- const code = publicKey.hashcode();
672
- if (subscribed) {
1126
+
1127
+ if (role instanceof Replicator && role.factor > 0) {
673
1128
  // TODO use Set + list for fast lookup
674
- if (!sortedPeer.find((x) => x.hash === code)) {
675
- sortedPeer.push({ hash: code, timestamp: +new Date() });
676
- sortedPeer.sort((a, b) => a.hash.localeCompare(b.hash));
677
- return true;
1129
+ // check also that peer is online
1130
+
1131
+ const isOnline =
1132
+ this.node.identity.publicKey.equals(publicKey) ||
1133
+ (await this.waitFor(publicKey, { signal: this._closeController.signal })
1134
+ .then(() => true)
1135
+ .catch(() => false));
1136
+
1137
+ if (isOnline) {
1138
+ // insert or if already there do nothing
1139
+ const code = hashToUniformNumber(publicKey.bytes);
1140
+ const rect: ReplicatorRect = {
1141
+ publicKey,
1142
+ offset: code,
1143
+ role
1144
+ };
1145
+
1146
+ let currentNode = sortedPeer.head;
1147
+ if (!currentNode) {
1148
+ sortedPeer.push(rect);
1149
+ return { changed: true };
1150
+ } else {
1151
+ while (currentNode) {
1152
+ if (currentNode.value.publicKey.equals(publicKey)) {
1153
+ // update the value
1154
+ // rect.timestamp = currentNode.value.timestamp;
1155
+ const prev = currentNode.value;
1156
+ currentNode.value = rect;
1157
+ // TODO change detection and only do change stuff if diff?
1158
+ return { prev: prev.role, changed: true };
1159
+ }
1160
+
1161
+ if (code > currentNode.value.offset) {
1162
+ const next = currentNode?.next;
1163
+ if (next) {
1164
+ currentNode = next;
1165
+ continue;
1166
+ } else {
1167
+ break;
1168
+ }
1169
+ } else {
1170
+ currentNode = currentNode.prev;
1171
+ break;
1172
+ }
1173
+ }
1174
+
1175
+ const prev = currentNode;
1176
+ if (!prev?.next?.value.publicKey.equals(publicKey)) {
1177
+ _insertAfter(sortedPeer, prev || undefined, rect);
1178
+ } else {
1179
+ throw new Error("Unexpected");
1180
+ }
1181
+ return { changed: true };
1182
+ }
678
1183
  } else {
679
- return false;
1184
+ return { changed: false };
680
1185
  }
681
1186
  } else {
682
- const deleteIndex = sortedPeer.findIndex((x) => x.hash === code);
683
- if (deleteIndex >= 0) {
684
- sortedPeer.splice(deleteIndex, 1);
685
- return true;
686
- } else {
687
- return false;
1187
+ let currentNode = sortedPeer.head;
1188
+ while (currentNode) {
1189
+ if (currentNode.value.publicKey.equals(publicKey)) {
1190
+ sortedPeer.removeNode(currentNode);
1191
+ return { prev: currentNode.value.role, changed: true };
1192
+ }
1193
+ currentNode = currentNode.next;
688
1194
  }
1195
+ return { changed: false };
689
1196
  }
690
1197
  }
691
1198
 
692
1199
  async handleSubscriptionChange(
693
1200
  publicKey: PublicSignKey,
694
- changes: { topic: string; data?: Uint8Array }[],
1201
+ changes: string[],
695
1202
  subscribed: boolean
696
1203
  ) {
697
- // TODO why are we doing two loops?
698
- const prev: boolean[] = [];
699
- for (const subscription of changes) {
700
- if (this.log.idString !== subscription.topic) {
701
- continue;
702
- }
703
-
704
- if (
705
- !subscription.data ||
706
- !startsWith(subscription.data, REPLICATOR_TYPE_VARIANT)
707
- ) {
708
- prev.push(await this.modifySortedSubscriptionCache(false, publicKey));
709
- continue;
710
- } else {
711
- this._lastSubscriptionMessageId += 1;
712
- prev.push(
713
- await this.modifySortedSubscriptionCache(subscribed, publicKey)
714
- );
715
- }
716
- }
717
-
718
- // TODO don't do this i fnot is replicator?
719
- for (const [i, subscription] of changes.entries()) {
720
- if (this.log.idString !== subscription.topic) {
721
- continue;
1204
+ if (subscribed) {
1205
+ if (this.role instanceof Replicator) {
1206
+ for (const subscription of changes) {
1207
+ if (this.log.idString !== subscription) {
1208
+ continue;
1209
+ }
1210
+ this.rpc
1211
+ .send(new ResponseRoleMessage(this.role), {
1212
+ mode: new AcknowledgeDelivery({ redundancy: 1, to: [publicKey] })
1213
+ })
1214
+ .catch((e) => logger.error(e.toString()));
1215
+ }
722
1216
  }
723
- if (subscription.data) {
724
- try {
725
- const type = deserialize(subscription.data, Role);
726
1217
 
727
- // Reorganize if the new subscriber is a replicator, or observers AND was replicator
728
- if (type instanceof Replicator || prev[i]) {
729
- await this.replicationReorganization();
730
- }
731
- } catch (error: any) {
732
- logger.warn(
733
- "Recieved subscription with invalid data on topic: " +
734
- subscription.topic +
735
- ". Error: " +
736
- error?.message
737
- );
1218
+ //if(evt.detail.subscriptions.map((x) => x.topic).includes())
1219
+ } else {
1220
+ for (const topic of changes) {
1221
+ if (this.log.idString !== topic) {
1222
+ continue;
738
1223
  }
1224
+
1225
+ await this.modifyReplicators(new Observer(), publicKey);
739
1226
  }
740
1227
  }
741
1228
  }
742
1229
 
743
- pruneSafely(entries: Entry<any>[], options?: { timeout: number }) {
1230
+ async prune(
1231
+ entries: Entry<any>[],
1232
+ options?: { timeout?: number; unchecked?: boolean }
1233
+ ): Promise<any> {
1234
+ if (options?.unchecked) {
1235
+ return Promise.all(
1236
+ entries.map((x) =>
1237
+ this.log.remove(x, {
1238
+ recursively: true
1239
+ })
1240
+ )
1241
+ );
1242
+ }
744
1243
  // ask network if they have they entry,
745
1244
  // so I can delete it
746
1245
 
@@ -754,14 +1253,17 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
754
1253
  const filteredEntries: Entry<any>[] = [];
755
1254
  for (const entry of entries) {
756
1255
  const pendingPrev = this._pendingDeletes.get(entry.hash);
757
-
1256
+ if (pendingPrev) {
1257
+ promises.push(pendingPrev.promise.promise);
1258
+ continue;
1259
+ }
758
1260
  filteredEntries.push(entry);
759
1261
  const existCounter = new Set<string>();
760
1262
  const minReplicas = decodeReplicas(entry);
761
1263
  const deferredPromise: DeferredPromise<void> = pDefer();
762
1264
 
763
1265
  const clear = () => {
764
- pendingPrev?.clear();
1266
+ //pendingPrev?.clear();
765
1267
  const pending = this._pendingDeletes.get(entry.hash);
766
1268
  if (pending?.promise == deferredPromise) {
767
1269
  this._pendingDeletes.delete(entry.hash);
@@ -780,7 +1282,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
780
1282
 
781
1283
  const timeout = setTimeout(
782
1284
  () => {
783
- reject(new Error("Timeout"));
1285
+ reject(new Error("Timeout for checked pruning"));
784
1286
  },
785
1287
  options?.timeout ?? 10 * 1000
786
1288
  );
@@ -792,10 +1294,25 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
792
1294
  },
793
1295
  callback: async (publicKeyHash: string) => {
794
1296
  const minReplicasValue = minReplicas.getValue(this);
795
- const l = await this.findLeaders(entry.gid, minReplicasValue);
796
- if (l.find((x) => x === publicKeyHash)) {
1297
+ const minMinReplicasValue = this.replicas.max
1298
+ ? Math.min(minReplicasValue, this.replicas.max.getValue(this))
1299
+ : minReplicasValue;
1300
+
1301
+ const leaders = await this.findLeaders(
1302
+ entry.gid,
1303
+ minMinReplicasValue
1304
+ );
1305
+
1306
+ if (
1307
+ leaders.find((x) => x === this.node.identity.publicKey.hashcode())
1308
+ ) {
1309
+ reject(new Error("Failed to delete, is leader"));
1310
+ return;
1311
+ }
1312
+
1313
+ if (leaders.find((x) => x === publicKeyHash)) {
797
1314
  existCounter.add(publicKeyHash);
798
- if (minReplicas.getValue(this) <= existCounter.size) {
1315
+ if (minMinReplicasValue <= existCounter.size) {
799
1316
  this.log
800
1317
  .remove(entry, {
801
1318
  recursively: true
@@ -812,28 +1329,52 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
812
1329
  });
813
1330
  promises.push(deferredPromise.promise);
814
1331
  }
815
- if (filteredEntries.length > 0) {
816
- this.rpc.send(
817
- new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) })
818
- );
1332
+ if (filteredEntries.length == 0) {
1333
+ return;
819
1334
  }
820
1335
 
821
- return promises;
1336
+ this.rpc.send(
1337
+ new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) })
1338
+ );
1339
+
1340
+ const onNewPeer = async (e: CustomEvent<UpdateRoleEvent>) => {
1341
+ if (e.detail.role instanceof Replicator) {
1342
+ await this.rpc.send(
1343
+ new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) }),
1344
+ {
1345
+ to: [e.detail.publicKey.hashcode()]
1346
+ }
1347
+ );
1348
+ }
1349
+ };
1350
+ // check joining peers
1351
+ this.events.addEventListener("role", onNewPeer);
1352
+ return Promise.all(promises).finally(() =>
1353
+ this.events.removeEventListener("role", onNewPeer)
1354
+ );
822
1355
  }
823
1356
 
824
- /**
825
- * When a peers join the networkk and want to participate the leaders for particular log subgraphs might change, hence some might start replicating, might some stop
826
- * This method will go through my owned entries, and see whether I should share them with a new leader, and/or I should stop care about specific entries
827
- * @param channel
828
- */
829
- async replicationReorganization() {
1357
+ async distribute() {
1358
+ /**
1359
+ * TODO use information of new joined/leaving peer to create a subset of heads
1360
+ * that we potentially need to share with other peers
1361
+ */
1362
+
1363
+ if (this.closed) {
1364
+ return;
1365
+ }
1366
+
830
1367
  const changed = false;
1368
+ await this.log.trim();
831
1369
  const heads = await this.log.getHeads();
832
1370
  const groupedByGid = await groupByGid(heads);
833
- let storeChanged = false;
1371
+ const toDeliver: Map<string, Entry<any>[]> = new Map();
1372
+ const allEntriesToDelete: Entry<any>[] = [];
1373
+
834
1374
  for (const [gid, entries] of groupedByGid) {
835
- const toSend: Map<string, Entry<any>> = new Map();
836
- const newPeers: string[] = [];
1375
+ if (this.closed) {
1376
+ break;
1377
+ }
837
1378
 
838
1379
  if (entries.length === 0) {
839
1380
  continue; // TODO maybe close store?
@@ -844,121 +1385,91 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
844
1385
  gid,
845
1386
  maxReplicas(this, entries) // pick max replication policy of all entries, so all information is treated equally important as the most important
846
1387
  );
1388
+ const currentPeersSet = new Set(currentPeers);
1389
+ this._gidPeersHistory.set(gid, currentPeersSet);
847
1390
 
848
1391
  for (const currentPeer of currentPeers) {
849
- if (
850
- !oldPeersSet?.has(currentPeer) &&
851
- currentPeer !== this.node.identity.publicKey.hashcode()
852
- ) {
853
- storeChanged = true;
1392
+ if (currentPeer == this.node.identity.publicKey.hashcode()) {
1393
+ continue;
1394
+ }
1395
+
1396
+ if (!oldPeersSet?.has(currentPeer)) {
854
1397
  // second condition means that if the new peer is us, we should not do anything, since we are expecting to receive heads, not send
855
- newPeers.push(currentPeer);
856
-
857
- // send heads to the new peer
858
- // console.log('new gid for peer', newPeers.length, this.id.toString(), newPeer, gid, entries.length, newPeers)
859
- try {
860
- logger.debug(
861
- `${this.node.identity.publicKey.hashcode()}: Exchange heads ${
862
- entries.length === 1 ? entries[0].hash : "#" + entries.length
863
- } on rebalance`
864
- );
865
- for (const entry of entries) {
866
- toSend.set(entry.hash, entry);
867
- }
868
- } catch (error) {
869
- if (error instanceof TimeoutError) {
870
- logger.error(
871
- "Missing channel when reorg to peer: " + currentPeer.toString()
872
- );
873
- continue;
874
- }
875
- throw error;
1398
+ let arr = toDeliver.get(currentPeer);
1399
+ if (!arr) {
1400
+ arr = [];
1401
+ toDeliver.set(currentPeer, arr);
1402
+ }
1403
+
1404
+ for (const entry of entries) {
1405
+ arr.push(entry);
876
1406
  }
877
1407
  }
878
1408
  }
879
1409
 
880
- // We don't need this clause anymore because we got the trim option!
881
1410
  if (
882
1411
  !currentPeers.find((x) => x === this.node.identity.publicKey.hashcode())
883
1412
  ) {
884
- let entriesToDelete = entries.filter((e) => !e.createdLocally);
885
-
886
- if (this._sync) {
887
- // dont delete entries which we wish to keep
888
- entriesToDelete = await Promise.all(
889
- entriesToDelete.map((x) => this._sync!(x))
890
- ).then((filter) => entriesToDelete.filter((v, ix) => !filter[ix]));
1413
+ if (currentPeers.length > 0) {
1414
+ // If we are observer, never prune locally created entries, since we dont really know who can store them
1415
+ // if we are replicator, we will always persist entries that we need to so filtering on createdLocally will not make a difference
1416
+ const entriesToDelete =
1417
+ this._role instanceof Observer
1418
+ ? entries.filter((e) => !e.createdLocally)
1419
+ : entries;
1420
+ entriesToDelete.map((x) => this._gidPeersHistory.delete(x.meta.gid));
1421
+ allEntriesToDelete.push(...entriesToDelete);
891
1422
  }
892
-
893
- // delete entries since we are not suppose to replicate this anymore
894
- // TODO add delay? freeze time? (to ensure resiliance for bad io)
895
- if (entriesToDelete.length > 0) {
896
- Promise.all(this.pruneSafely(entriesToDelete)).catch((e) => {
897
- logger.error(e.toString());
898
- });
1423
+ } else {
1424
+ for (const entry of entries) {
1425
+ this._pendingDeletes
1426
+ .get(entry.hash)
1427
+ ?.promise.reject(
1428
+ new Error(
1429
+ "Failed to delete, is leader: " +
1430
+ this.role.constructor.name +
1431
+ ". " +
1432
+ this.node.identity.publicKey.hashcode()
1433
+ )
1434
+ );
899
1435
  }
900
-
901
- // TODO if length === 0 maybe close store?
902
1436
  }
903
- this._gidPeersHistory.set(gid, new Set(currentPeers));
1437
+ }
904
1438
 
905
- if (toSend.size === 0) {
906
- continue;
907
- }
1439
+ for (const [target, entries] of toDeliver) {
908
1440
  const message = await createExchangeHeadsMessage(
909
1441
  this.log,
910
- [...toSend.values()], // TODO send to peers directly
1442
+ entries, // TODO send to peers directly
911
1443
  this._gidParentCache
912
1444
  );
913
-
914
1445
  // TODO perhaps send less messages to more receivers for performance reasons?
915
1446
  await this.rpc.send(message, {
916
- to: newPeers,
917
- strict: true
1447
+ to: [target]
918
1448
  });
919
1449
  }
920
- if (storeChanged) {
921
- await this.log.trim(); // because for entries createdLocally,we can have trim options that still allow us to delete them
922
- }
923
- return storeChanged || changed;
924
- }
925
1450
 
926
- /**
927
- *
928
- * @returns groups where at least one in any group will have the entry you are looking for
929
- */
930
- getDiscoveryGroups() {
931
- // TODO Optimize this so we don't have to recreate the array all the time!
932
- const minReplicas = this.replicas.min.getValue(this);
933
- const replicators = this.getReplicatorsSorted();
934
- if (!replicators) {
935
- return []; // No subscribers and we are not replicating
936
- }
937
- const numberOfGroups = Math.min(
938
- Math.ceil(replicators!.length / minReplicas)
939
- );
940
- const groups = new Array<{ hash: string; timestamp: number }[]>(
941
- numberOfGroups
942
- );
943
- for (let i = 0; i < groups.length; i++) {
944
- groups[i] = [];
945
- }
946
- for (let i = 0; i < replicators!.length; i++) {
947
- groups[i % numberOfGroups].push(replicators![i]);
1451
+ if (allEntriesToDelete.length > 0) {
1452
+ this.prune(allEntriesToDelete).catch((e) => {
1453
+ logger.error(e.toString());
1454
+ });
948
1455
  }
949
1456
 
950
- return groups;
951
- }
952
- async replicator(entry: Entry<any>) {
953
- return this.isLeader(entry.gid, decodeReplicas(entry).getValue(this));
1457
+ return changed;
954
1458
  }
955
1459
 
956
1460
  async _onUnsubscription(evt: CustomEvent<UnsubcriptionEvent>) {
957
1461
  logger.debug(
958
1462
  `Peer disconnected '${evt.detail.from.hashcode()}' from '${JSON.stringify(
959
- evt.detail.unsubscriptions.map((x) => x.topic)
1463
+ evt.detail.unsubscriptions.map((x) => x)
960
1464
  )}'`
961
1465
  );
1466
+ this.latestRoleMessages.delete(evt.detail.from.hashcode());
1467
+
1468
+ this.events.dispatchEvent(
1469
+ new CustomEvent<UpdateRoleEvent>("role", {
1470
+ detail: { publicKey: evt.detail.from, role: new Observer() }
1471
+ })
1472
+ );
962
1473
 
963
1474
  return this.handleSubscriptionChange(
964
1475
  evt.detail.from,
@@ -970,13 +1481,257 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
970
1481
  async _onSubscription(evt: CustomEvent<SubscriptionEvent>) {
971
1482
  logger.debug(
972
1483
  `New peer '${evt.detail.from.hashcode()}' connected to '${JSON.stringify(
973
- evt.detail.subscriptions.map((x) => x.topic)
1484
+ evt.detail.subscriptions.map((x) => x)
974
1485
  )}'`
975
1486
  );
1487
+ this.remoteBlocks.onReachable(evt.detail.from);
1488
+
976
1489
  return this.handleSubscriptionChange(
977
1490
  evt.detail.from,
978
1491
  evt.detail.subscriptions,
979
1492
  true
980
1493
  );
981
1494
  }
1495
+ replicationController: ReplicationController;
1496
+
1497
+ history: { usedMemory: number; factor: number }[];
1498
+ async addToHistory(usedMemory: number, factor: number) {
1499
+ (this.history || (this.history = [])).push({ usedMemory, factor });
1500
+
1501
+ // Keep only the last N entries in the history array (you can adjust N based on your needs)
1502
+ const maxHistoryLength = 10;
1503
+ if (this.history.length > maxHistoryLength) {
1504
+ this.history.shift();
1505
+ }
1506
+ }
1507
+
1508
+ async calculateTrend() {
1509
+ // Calculate the average change in factor per unit change in memory usage
1510
+ const factorChanges = this.history.map((entry, index) => {
1511
+ if (index > 0) {
1512
+ const memoryChange =
1513
+ entry.usedMemory - this.history[index - 1].usedMemory;
1514
+ if (memoryChange !== 0) {
1515
+ const factorChange = entry.factor - this.history[index - 1].factor;
1516
+ return factorChange / memoryChange;
1517
+ }
1518
+ }
1519
+ return 0;
1520
+ });
1521
+
1522
+ // Return the average factor change per unit memory change
1523
+ return (
1524
+ factorChanges.reduce((sum, change) => sum + change, 0) /
1525
+ factorChanges.length
1526
+ );
1527
+ }
1528
+
1529
+ private sumFactors() {
1530
+ let sum = 0;
1531
+ for (const element of this.getReplicatorsSorted()?.toArray() || []) {
1532
+ sum += element.role.factor;
1533
+ }
1534
+
1535
+ return sum;
1536
+ }
1537
+
1538
+ async rebalanceParticipation(onRoleChange = true) {
1539
+ // update more participation rate to converge to the average expected rate or bounded by
1540
+ // resources such as memory and or cpu
1541
+
1542
+ if (this.closed) {
1543
+ return false;
1544
+ }
1545
+
1546
+ // The role is fixed (no changes depending on memory usage or peer count etc)
1547
+ if (this._roleOptions instanceof Role) {
1548
+ return false;
1549
+ }
1550
+
1551
+ // TODO second condition: what if the current role is Observer?
1552
+ if (
1553
+ this._roleOptions.type == "replicator" &&
1554
+ this._role instanceof Replicator
1555
+ ) {
1556
+ const peers = this.getReplicatorsSorted();
1557
+ const usedMemory = await this.getMemoryUsage();
1558
+
1559
+ const newFactor =
1560
+ await this.replicationController.adjustReplicationFactor(
1561
+ usedMemory,
1562
+ this._role.factor,
1563
+ this.sumFactors(),
1564
+ peers?.length || 1
1565
+ );
1566
+
1567
+ const newRole = new Replicator({
1568
+ factor: newFactor,
1569
+ timestamp: this._role.timestamp
1570
+ });
1571
+
1572
+ const relativeDifference =
1573
+ Math.abs(this._role.factor - newRole.factor) / this._role.factor;
1574
+
1575
+ if (relativeDifference > 0.0001) {
1576
+ const canReplicate =
1577
+ !this._canReplicate ||
1578
+ (await this._canReplicate(this.node.identity.publicKey, newRole));
1579
+ if (!canReplicate) {
1580
+ return false;
1581
+ }
1582
+
1583
+ await this._updateRole(newRole, onRoleChange);
1584
+ return true;
1585
+ }
1586
+ return false;
1587
+ }
1588
+ return false;
1589
+ }
1590
+ }
1591
+
1592
+ function _insertAfter(
1593
+ self: yallist<any>,
1594
+ node: yallist.Node<ReplicatorRect> | undefined,
1595
+ value: ReplicatorRect
1596
+ ) {
1597
+ const inserted = !node
1598
+ ? new yallist.Node(
1599
+ value,
1600
+ null as any,
1601
+ self.head as yallist.Node<ReplicatorRect> | undefined,
1602
+ self
1603
+ )
1604
+ : new yallist.Node(
1605
+ value,
1606
+ node,
1607
+ node.next as yallist.Node<ReplicatorRect> | undefined,
1608
+ self
1609
+ );
1610
+
1611
+ // is tail
1612
+ if (inserted.next === null) {
1613
+ self.tail = inserted;
1614
+ }
1615
+
1616
+ // is head
1617
+ if (inserted.prev === null) {
1618
+ self.head = inserted;
1619
+ }
1620
+
1621
+ self.length++;
1622
+ return inserted;
1623
+ }
1624
+
1625
+ class ReplicationController {
1626
+ integral = 0;
1627
+ prevError = 0;
1628
+ prevMemoryUsage = 0;
1629
+ lastTs = 0;
1630
+ kp: number;
1631
+ ki: number;
1632
+ kd: number;
1633
+ errorFunction: ReplicationErrorFunction;
1634
+ targetMemoryLimit?: number;
1635
+ constructor(
1636
+ options: {
1637
+ errorFunction?: ReplicationErrorFunction;
1638
+ targetMemoryLimit?: number;
1639
+ kp?: number;
1640
+ ki?: number;
1641
+ kd?: number;
1642
+ } = {}
1643
+ ) {
1644
+ const {
1645
+ targetMemoryLimit,
1646
+ kp = 0.1,
1647
+ ki = 0 /* 0.01, */,
1648
+ kd = 0.1,
1649
+ errorFunction = ({ balance, coverage, memory }) =>
1650
+ memory * 0.8 + balance * 0.1 + coverage * 0.1
1651
+ } = options;
1652
+ this.kp = kp;
1653
+ this.ki = ki;
1654
+ this.kd = kd;
1655
+ this.targetMemoryLimit = targetMemoryLimit;
1656
+ this.errorFunction = errorFunction;
1657
+ }
1658
+
1659
+ async adjustReplicationFactor(
1660
+ memoryUsage: number,
1661
+ currentFactor: number,
1662
+ totalFactor: number,
1663
+ peerCount: number
1664
+ ) {
1665
+ /* if (memoryUsage === this.prevMemoryUsage) {
1666
+ return Math.max(Math.min(currentFactor, maxFraction), minFraction);
1667
+ } */
1668
+
1669
+ this.prevMemoryUsage = memoryUsage;
1670
+ if (memoryUsage <= 0) {
1671
+ this.integral = 0;
1672
+ }
1673
+ const normalizedFactor = currentFactor / totalFactor;
1674
+ /*const estimatedTotalSize = memoryUsage / normalizedFactor;
1675
+
1676
+ const errorMemory =
1677
+ currentFactor > 0 && memoryUsage > 0
1678
+ ? (Math.max(Math.min(maxFraction, this.targetMemoryLimit / estimatedTotalSize), minFraction) -
1679
+ normalizedFactor) * totalFactor
1680
+ : minFraction; */
1681
+
1682
+ const estimatedTotalSize = memoryUsage / currentFactor;
1683
+ const errorCoverage = Math.min(1 - totalFactor, 1);
1684
+
1685
+ let errorMemory = 0;
1686
+ const errorTarget = 1 / peerCount - currentFactor;
1687
+
1688
+ if (this.targetMemoryLimit != null) {
1689
+ errorMemory =
1690
+ currentFactor > 0 && memoryUsage > 0
1691
+ ? Math.max(
1692
+ Math.min(1, this.targetMemoryLimit / estimatedTotalSize),
1693
+ 0
1694
+ ) - currentFactor
1695
+ : 0.0001;
1696
+
1697
+ /* errorTarget = errorMemory + (1 / peerCount - currentFactor) / 10; */
1698
+ }
1699
+
1700
+ // alpha * errorCoverage + (1 - alpha) * errorTarget;
1701
+ const totalError = this.errorFunction({
1702
+ balance: errorTarget,
1703
+ coverage: errorCoverage,
1704
+ memory: errorMemory
1705
+ });
1706
+
1707
+ if (this.lastTs === 0) {
1708
+ this.lastTs = +new Date();
1709
+ }
1710
+ const kpAdjusted = Math.min(
1711
+ Math.max(this.kp, (+new Date() - this.lastTs) / 100),
1712
+ 0.8
1713
+ );
1714
+ const pTerm = kpAdjusted * totalError;
1715
+
1716
+ this.lastTs = +new Date();
1717
+
1718
+ // Integral term
1719
+ this.integral += totalError;
1720
+ const beta = 0.4;
1721
+ this.integral = beta * totalError + (1 - beta) * this.integral;
1722
+
1723
+ const iTerm = this.ki * this.integral;
1724
+
1725
+ // Derivative term
1726
+ const derivative = totalError - this.prevError;
1727
+ const dTerm = this.kd * derivative;
1728
+
1729
+ // Calculate the new replication factor
1730
+ const newFactor = currentFactor + pTerm + iTerm + dTerm;
1731
+
1732
+ // Update state for the next iteration
1733
+ this.prevError = totalError;
1734
+
1735
+ return Math.max(Math.min(newFactor, 1), 0);
1736
+ }
982
1737
  }