@peerbit/shared-log 3.1.10 → 4.0.2

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,10 @@ import {
7
7
  LogEvents,
8
8
  LogProperties
9
9
  } from "@peerbit/log";
10
- import { Program } from "@peerbit/program";
11
- import {
12
- BinaryReader,
13
- BinaryWriter,
14
- BorshError,
15
- deserialize,
16
- field,
17
- serialize,
18
- variant
19
- } from "@dao-xyz/borsh";
10
+ import { Program, ProgramEvents } from "@peerbit/program";
11
+ import { BinaryWriter, BorshError, field, variant } from "@dao-xyz/borsh";
20
12
  import {
21
13
  AccessError,
22
- getPublicKeyFromPeerId,
23
14
  PublicSignKey,
24
15
  sha256,
25
16
  sha256Base64Sync
@@ -36,24 +27,35 @@ import {
36
27
  SubscriptionEvent,
37
28
  UnsubcriptionEvent
38
29
  } 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";
30
+ import { AbortError, delay, TimeoutError, waitFor } from "@peerbit/time";
31
+ import { Observer, Replicator, Role } from "./role.js";
42
32
  import {
43
33
  AbsoluteReplicas,
44
- MinReplicas,
45
34
  ReplicationError,
35
+ ReplicationLimits,
36
+ ReplicatorRect,
37
+ RequestRoleMessage,
38
+ ResponseRoleMessage,
46
39
  decodeReplicas,
47
40
  encodeReplicas,
41
+ hashToUniformNumber,
48
42
  maxReplicas
49
43
  } from "./replication.js";
50
44
  import pDefer, { DeferredPromise } from "p-defer";
51
45
  import { Cache } from "@peerbit/cache";
52
-
46
+ import { CustomEvent } from "@libp2p/interface";
47
+ import yallist from "yallist";
48
+ import { AcknowledgeDelivery, SilentDelivery } from "@peerbit/stream-interface";
49
+ import { AnyBlockStore, RemoteBlocks } from "@peerbit/blocks";
50
+ import { BlocksMessage } from "./blocks.js";
51
+ import debounce from "p-debounce";
52
+ import { PIDReplicationController, ReplicationErrorFunction } from "./pid.js";
53
+ export type { ReplicationErrorFunction };
53
54
  export * from "./replication.js";
55
+
54
56
  export { Observer, Replicator, Role };
55
57
 
56
- export const logger = loggerFn({ module: "peer" });
58
+ export const logger = loggerFn({ module: "shared-log" });
57
59
 
58
60
  const groupByGid = async <T extends Entry<any> | EntryWithRefs<any>>(
59
61
  entries: T[]
@@ -73,30 +75,74 @@ const groupByGid = async <T extends Entry<any> | EntryWithRefs<any>>(
73
75
  return groupByGid;
74
76
  };
75
77
 
76
- export type SyncFilter = (entries: Entry<any>) => Promise<boolean> | boolean;
77
-
78
- type ReplicationLimits = { min: MinReplicas; max?: MinReplicas };
79
78
  export type ReplicationLimitsOptions =
80
79
  | Partial<ReplicationLimits>
81
80
  | { min?: number; max?: number };
82
81
 
83
- export interface SharedLogOptions {
82
+ type StringRoleOptions = "observer" | "replicator";
83
+
84
+ type AdaptiveReplicatorOptions = {
85
+ type: "replicator";
86
+ limits?: { memory: number };
87
+ error?: ReplicationErrorFunction;
88
+ };
89
+
90
+ type FixedReplicatorOptions = {
91
+ type: "replicator";
92
+ factor: number;
93
+ };
94
+
95
+ type ObserverType = {
96
+ type: "observer";
97
+ };
98
+
99
+ export type RoleOptions =
100
+ | StringRoleOptions
101
+ | ObserverType
102
+ | FixedReplicatorOptions
103
+ | AdaptiveReplicatorOptions;
104
+
105
+ const isAdaptiveReplicatorOption = (
106
+ options: FixedReplicatorOptions | AdaptiveReplicatorOptions
107
+ ): options is AdaptiveReplicatorOptions => {
108
+ if (
109
+ (options as AdaptiveReplicatorOptions).limits ||
110
+ (options as AdaptiveReplicatorOptions).error ||
111
+ (options as FixedReplicatorOptions).factor == null
112
+ ) {
113
+ return true;
114
+ }
115
+ return false;
116
+ };
117
+
118
+ export type SharedLogOptions = {
119
+ role?: RoleOptions;
84
120
  replicas?: ReplicationLimitsOptions;
85
- sync?: SyncFilter;
86
- role?: Role;
87
121
  respondToIHaveTimeout?: number;
88
122
  canReplicate?: (publicKey: PublicSignKey) => Promise<boolean> | boolean;
89
- }
123
+ };
90
124
 
91
125
  export const DEFAULT_MIN_REPLICAS = 2;
126
+ export const WAIT_FOR_REPLICATOR_TIMEOUT = 9000;
127
+ export const WAIT_FOR_ROLE_MATURITY = 5000;
128
+ const REBALANCE_DEBOUNCE_INTERAVAL = 50;
92
129
 
93
130
  export type Args<T> = LogProperties<T> & LogEvents<T> & SharedLogOptions;
131
+
94
132
  export type SharedAppendOptions<T> = AppendOptions<T> & {
95
133
  replicas?: AbsoluteReplicas | number;
96
134
  };
97
135
 
136
+ type UpdateRoleEvent = { publicKey: PublicSignKey; role: Role };
137
+ export interface SharedLogEvents extends ProgramEvents {
138
+ role: CustomEvent<UpdateRoleEvent>;
139
+ }
140
+
98
141
  @variant("shared_log")
99
- export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
142
+ export class SharedLog<T = Uint8Array> extends Program<
143
+ Args<T>,
144
+ SharedLogEvents
145
+ > {
100
146
  @field({ type: Log })
101
147
  log: Log<T>;
102
148
 
@@ -104,22 +150,23 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
104
150
  rpc: RPC<TransportMessage, TransportMessage>;
105
151
 
106
152
  // options
107
- private _sync?: SyncFilter;
108
153
  private _role: Observer | Replicator;
154
+ private _roleOptions: AdaptiveReplicatorOptions | Observer | Replicator;
109
155
 
110
- private _sortedPeersCache: { hash: string; timestamp: number }[] | undefined;
111
- private _lastSubscriptionMessageId: number;
156
+ private _sortedPeersCache: yallist<ReplicatorRect> | undefined;
157
+ private _totalParticipation: number;
112
158
  private _gidPeersHistory: Map<string, Set<string>>;
113
159
 
114
160
  private _onSubscriptionFn: (arg: any) => any;
115
161
  private _onUnsubscriptionFn: (arg: any) => any;
116
162
 
117
163
  private _canReplicate?: (
118
- publicKey: PublicSignKey
164
+ publicKey: PublicSignKey,
165
+ role: Replicator
119
166
  ) => Promise<boolean> | boolean;
120
167
 
121
168
  private _logProperties?: LogProperties<T> & LogEvents<T>;
122
-
169
+ private _closeController: AbortController;
123
170
  private _loadedOnce = false;
124
171
  private _gidParentCache: Cache<Entry<any>[]>;
125
172
  private _respondToIHaveTimeout;
@@ -134,9 +181,19 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
134
181
 
135
182
  private _pendingIHave: Map<
136
183
  string,
137
- { clear: () => void; callback: () => void }
184
+ { clear: () => void; callback: (entry: Entry<T>) => void }
138
185
  >;
139
186
 
187
+ private latestRoleMessages: Map<string, bigint>;
188
+
189
+ private remoteBlocks: RemoteBlocks;
190
+
191
+ private openTime: number;
192
+
193
+ private rebalanceParticipationDebounced:
194
+ | ReturnType<typeof debounce>
195
+ | undefined;
196
+
140
197
  replicas: ReplicationLimits;
141
198
 
142
199
  constructor(properties?: { id?: Uint8Array }) {
@@ -149,37 +206,93 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
149
206
  return this._role;
150
207
  }
151
208
 
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));
157
-
158
- if (wasRepicators) {
159
- await this.replicationReorganization();
160
- }
209
+ get totalParticipation(): number {
210
+ return this._totalParticipation;
161
211
  }
162
212
 
163
- private async initializeWithRole() {
164
- try {
165
- await this.modifySortedSubscriptionCache(
166
- this._role instanceof Replicator ? true : false,
167
- getPublicKeyFromPeerId(this.node.peerId)
213
+ private setupRole(options?: RoleOptions) {
214
+ this.rebalanceParticipationDebounced = undefined;
215
+
216
+ const setupDebouncedRebalancing = (options?: AdaptiveReplicatorOptions) => {
217
+ this.replicationController = new PIDReplicationController({
218
+ targetMemoryLimit: options?.limits?.memory,
219
+ errorFunction: options?.error
220
+ });
221
+
222
+ this.rebalanceParticipationDebounced = debounce(
223
+ () => this.rebalanceParticipation(),
224
+ REBALANCE_DEBOUNCE_INTERAVAL // TODO make dynamic
168
225
  );
226
+ };
169
227
 
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
- );
228
+ if (options instanceof Observer || options instanceof Replicator) {
229
+ throw new Error("Unsupported role option type");
230
+ } else if (options === "observer") {
231
+ this._roleOptions = new Observer();
232
+ } else if (options === "replicator") {
233
+ setupDebouncedRebalancing();
234
+ this._roleOptions = { type: options };
235
+ } else if (options) {
236
+ if (options.type === "replicator") {
237
+ if (isAdaptiveReplicatorOption(options)) {
238
+ setupDebouncedRebalancing(options);
239
+ this._roleOptions = options;
240
+ } else {
241
+ this._roleOptions = new Replicator({ factor: options.factor });
242
+ }
179
243
  } else {
180
- throw error;
244
+ this._roleOptions = new Observer();
181
245
  }
246
+ } else {
247
+ // Default option
248
+ setupDebouncedRebalancing();
249
+ this._roleOptions = { type: "replicator" };
250
+ }
251
+
252
+ // setup the initial role
253
+ if (
254
+ this._roleOptions instanceof Replicator ||
255
+ this._roleOptions instanceof Observer
256
+ ) {
257
+ this._role = this._roleOptions; // Fixed
258
+ } else {
259
+ this._role = new Replicator({
260
+ // initial role in a dynamic setup
261
+ factor: 1,
262
+ timestamp: BigInt(+new Date())
263
+ });
264
+ }
265
+
266
+ return this._role;
267
+ }
268
+
269
+ async updateRole(role: RoleOptions, onRoleChange = true) {
270
+ return this._updateRole(this.setupRole(role), onRoleChange);
271
+ }
272
+
273
+ private async _updateRole(
274
+ role: Observer | Replicator = this._role,
275
+ onRoleChange = true
276
+ ) {
277
+ this._role = role;
278
+ const { changed } = await this._modifyReplicators(
279
+ this.role,
280
+ this.node.identity.publicKey
281
+ );
282
+
283
+ if (!this._loadedOnce) {
284
+ await this.log.load();
285
+ this._loadedOnce = true;
182
286
  }
287
+ await this.rpc.subscribe();
288
+
289
+ await this.rpc.send(new ResponseRoleMessage(role));
290
+
291
+ if (onRoleChange && changed) {
292
+ this.onRoleChange(undefined, this._role, this.node.identity.publicKey);
293
+ }
294
+
295
+ return changed;
183
296
  }
184
297
 
185
298
  async append(
@@ -207,14 +320,27 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
207
320
  }
208
321
 
209
322
  const result = await this.log.append(data, appendOptions);
323
+ const leaders = await this.findLeaders(
324
+ result.entry.meta.gid,
325
+ decodeReplicas(result.entry).getValue(this)
326
+ );
327
+ const isLeader = leaders.includes(this.node.identity.publicKey.hashcode());
210
328
 
211
329
  await this.rpc.send(
212
330
  await createExchangeHeadsMessage(
213
331
  this.log,
214
332
  [result.entry],
215
333
  this._gidParentCache
216
- )
334
+ ),
335
+ {
336
+ mode: isLeader
337
+ ? new SilentDelivery({ redundancy: 1, to: leaders })
338
+ : new AcknowledgeDelivery({ redundancy: 1, to: leaders })
339
+ }
217
340
  );
341
+
342
+ this.rebalanceParticipationDebounced?.();
343
+
218
344
  return result;
219
345
  }
220
346
 
@@ -231,21 +357,24 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
231
357
  : options.replicas.max
232
358
  : undefined
233
359
  };
360
+
234
361
  this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 10 * 1000; // TODO make into arg
235
362
  this._pendingDeletes = new Map();
236
363
  this._pendingIHave = new Map();
364
+ this.latestRoleMessages = new Map();
365
+ this.openTime = +new Date();
237
366
 
238
367
  this._gidParentCache = new Cache({ max: 1000 });
368
+ this._closeController = new AbortController();
239
369
 
240
370
  this._canReplicate = options?.canReplicate;
241
- this._sync = options?.sync;
242
371
  this._logProperties = options;
243
- this._role = options?.role || new Replicator();
244
372
 
245
- this._lastSubscriptionMessageId = 0;
246
- this._onSubscriptionFn = this._onSubscription.bind(this);
373
+ this.setupRole(options?.role);
247
374
 
248
- this._sortedPeersCache = [];
375
+ this._onSubscriptionFn = this._onSubscription.bind(this);
376
+ this._totalParticipation = 0;
377
+ this._sortedPeersCache = yallist.create();
249
378
  this._gidPeersHistory = new Map();
250
379
 
251
380
  await this.node.services.pubsub.addEventListener(
@@ -259,9 +388,26 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
259
388
  this._onUnsubscriptionFn
260
389
  );
261
390
 
262
- await this.log.open(this.node.services.blocks, this.node.identity, {
263
- keychain: this.node.keychain,
391
+ const id = sha256Base64Sync(this.log.id);
392
+ const storage = await this.node.memory.sublevel(id);
393
+ const localBlocks = await new AnyBlockStore(
394
+ await storage.sublevel("blocks")
395
+ );
396
+ const cache = await storage.sublevel("cache");
397
+
398
+ this.remoteBlocks = new RemoteBlocks({
399
+ local: localBlocks,
400
+ publish: (message, options) =>
401
+ this.rpc.send(new BlocksMessage(message), {
402
+ to: options?.to
403
+ }),
404
+ waitFor: this.rpc.waitFor.bind(this.rpc)
405
+ });
264
406
 
407
+ await this.remoteBlocks.start();
408
+
409
+ await this.log.open(this.remoteBlocks, this.node.identity, {
410
+ keychain: this.node.services.keychain,
265
411
  ...this._logProperties,
266
412
  onChange: (change) => {
267
413
  if (this._pendingIHave.size > 0) {
@@ -269,7 +415,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
269
415
  const ih = this._pendingIHave.get(added.hash);
270
416
  if (ih) {
271
417
  ih.clear();
272
- ih.callback();
418
+ ih.callback(added);
273
419
  }
274
420
  }
275
421
  }
@@ -304,42 +450,37 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
304
450
  return this._logProperties?.canAppend?.(entry) ?? true;
305
451
  },
306
452
  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
- }
453
+ ...this._logProperties?.trim
316
454
  },
317
- cache:
318
- this.node.memory &&
319
- (await this.node.memory.sublevel(sha256Base64Sync(this.log.id)))
455
+ cache: cache
320
456
  });
321
457
 
322
- await this.initializeWithRole();
458
+ // Open for communcation
459
+ await this.rpc.open({
460
+ queryType: TransportMessage,
461
+ responseType: TransportMessage,
462
+ responseHandler: this._onMessage.bind(this),
463
+ topic: this.topic
464
+ });
465
+
466
+ await this._updateRole();
467
+ await this.rebalanceParticipation();
323
468
 
324
469
  // Take into account existing subscription
325
470
  (await this.node.services.pubsub.getSubscribers(this.topic))?.forEach(
326
471
  (v, k) => {
327
- this.handleSubscriptionChange(
328
- v.publicKey,
329
- [{ topic: this.topic, data: v.data }],
330
- true
331
- );
472
+ if (v.equals(this.node.identity.publicKey)) {
473
+ return;
474
+ }
475
+ this.handleSubscriptionChange(v, [this.topic], true);
332
476
  }
333
477
  );
478
+ }
334
479
 
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
- });
480
+ async getMemoryUsage() {
481
+ return (
482
+ ((await this.log.memory?.size()) || 0) + (await this.log.blocks.size())
483
+ );
343
484
  }
344
485
 
345
486
  get topic() {
@@ -347,6 +488,19 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
347
488
  }
348
489
 
349
490
  private async _close() {
491
+ this._closeController.abort();
492
+
493
+ this.node.services.pubsub.removeEventListener(
494
+ "subscribe",
495
+ this._onSubscriptionFn
496
+ );
497
+
498
+ this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
499
+ this.node.services.pubsub.removeEventListener(
500
+ "unsubscribe",
501
+ this._onUnsubscriptionFn
502
+ );
503
+
350
504
  for (const [k, v] of this._pendingDeletes) {
351
505
  v.clear();
352
506
  v.promise.resolve(); // TODO or reject?
@@ -355,24 +509,15 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
355
509
  v.clear();
356
510
  }
357
511
 
512
+ await this.remoteBlocks.stop();
358
513
  this._gidParentCache.clear();
359
514
  this._pendingDeletes = new Map();
360
515
  this._pendingIHave = new Map();
516
+ this.latestRoleMessages.clear();
361
517
 
362
518
  this._gidPeersHistory = new Map();
363
519
  this._sortedPeersCache = undefined;
364
520
  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
521
  }
377
522
  async close(from?: Program): Promise<boolean> {
378
523
  const superClosed = await super.close(from);
@@ -389,8 +534,8 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
389
534
  if (!superDropped) {
390
535
  return superDropped;
391
536
  }
392
- await this._close();
393
537
  await this.log.drop();
538
+ await this._close();
394
539
  return true;
395
540
  }
396
541
 
@@ -411,7 +556,6 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
411
556
  */
412
557
 
413
558
  const { heads } = msg;
414
- // replication topic === trustedNetwork address
415
559
 
416
560
  logger.debug(
417
561
  `${this.node.identity.publicKey.hashcode()}: Recieved heads: ${
@@ -431,26 +575,32 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
431
575
  }
432
576
  }
433
577
 
434
- if (!this._sync) {
435
- const toMerge: EntryWithRefs<any>[] = [];
578
+ if (filteredHeads.length === 0) {
579
+ return;
580
+ }
436
581
 
437
- let toDelete: Entry<any>[] | undefined = undefined;
438
- let maybeDelete: EntryWithRefs<any>[][] | undefined = undefined;
582
+ const toMerge: EntryWithRefs<any>[] = [];
583
+ let toDelete: Entry<any>[] | undefined = undefined;
584
+ let maybeDelete: EntryWithRefs<any>[][] | undefined = undefined;
439
585
 
440
- const groupedByGid = await groupByGid(filteredHeads);
586
+ const groupedByGid = await groupByGid(filteredHeads);
587
+ const promises: Promise<void>[] = [];
441
588
 
442
- for (const [gid, entries] of groupedByGid) {
589
+ for (const [gid, entries] of groupedByGid) {
590
+ const fn = async () => {
443
591
  const headsWithGid = this.log.headsIndex.gids.get(gid);
592
+
444
593
  const maxReplicasFromHead =
445
594
  headsWithGid && headsWithGid.size > 0
446
595
  ? maxReplicas(this, [...headsWithGid.values()])
447
596
  : this.replicas.min.getValue(this);
448
597
 
449
- const maxReplicasFromNewEntries = maxReplicas(this, [
450
- ...entries.map((x) => x.entry)
451
- ]);
598
+ const maxReplicasFromNewEntries = maxReplicas(
599
+ this,
600
+ entries.map((x) => x.entry)
601
+ );
452
602
 
453
- const isLeader = await this.isLeader(
603
+ const isLeader = await this.waitForIsLeader(
454
604
  gid,
455
605
  Math.max(maxReplicasFromHead, maxReplicasFromNewEntries)
456
606
  );
@@ -481,50 +631,49 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
481
631
  }. Because not leader`
482
632
  );
483
633
  }
484
- }
634
+ };
635
+ promises.push(fn());
636
+ }
637
+ await Promise.all(promises);
485
638
 
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
- }
639
+ if (this.closed) {
640
+ return;
641
+ }
642
+
643
+ if (toMerge.length > 0) {
644
+ await this.log.join(toMerge);
645
+ toDelete &&
646
+ this.prune(toDelete).catch((e) => {
647
+ logger.error(e.toString());
648
+ });
649
+ this.rebalanceParticipationDebounced?.();
650
+ }
493
651
 
494
- if (maybeDelete) {
495
- for (const entries of maybeDelete) {
496
- const headsWithGid = this.log.headsIndex.gids.get(
497
- entries[0].entry.meta.gid
652
+ if (maybeDelete) {
653
+ for (const entries of maybeDelete as EntryWithRefs<any>[][]) {
654
+ const headsWithGid = this.log.headsIndex.gids.get(
655
+ entries[0].entry.meta.gid
656
+ );
657
+ if (headsWithGid && headsWithGid.size > 0) {
658
+ const minReplicas = maxReplicas(this, headsWithGid.values());
659
+
660
+ const isLeader = await this.isLeader(
661
+ entries[0].entry.meta.gid,
662
+ minReplicas
498
663
  );
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
- }
664
+
665
+ if (!isLeader) {
666
+ this.prune(entries.map((x) => x.entry)).catch((e) => {
667
+ logger.error(e.toString());
668
+ });
515
669
  }
516
670
  }
517
671
  }
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
672
  }
525
673
  }
526
674
  } else if (msg instanceof RequestIHave) {
527
675
  const hasAndIsLeader: string[] = [];
676
+
528
677
  for (const hash of msg.hashes) {
529
678
  const indexedEntry = this.log.entryIndex.getShallow(hash);
530
679
  if (
@@ -542,12 +691,21 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
542
691
  clearTimeout(timeout);
543
692
  prevPendingIHave?.clear();
544
693
  },
545
- callback: () => {
546
- prevPendingIHave && prevPendingIHave.callback();
547
- this.rpc.send(new ResponseIHave({ hashes: [hash] }), {
548
- to: [context.from!]
549
- });
550
- this._pendingIHave.delete(hash);
694
+ callback: async (entry) => {
695
+ if (
696
+ await this.isLeader(
697
+ entry.meta.gid,
698
+ decodeReplicas(entry).getValue(this)
699
+ )
700
+ ) {
701
+ this.rpc.send(new ResponseIHave({ hashes: [entry.hash] }), {
702
+ to: [context.from!]
703
+ });
704
+ }
705
+
706
+ prevPendingIHave && prevPendingIHave.callback(entry);
707
+
708
+ this._pendingIHave.delete(entry.hash);
551
709
  }
552
710
  };
553
711
  const timeout = setTimeout(() => {
@@ -560,17 +718,71 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
560
718
  this._pendingIHave.set(hash, pendingIHave);
561
719
  }
562
720
  }
563
- this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
721
+ await this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
564
722
  to: [context.from!]
565
723
  });
566
724
  } else if (msg instanceof ResponseIHave) {
567
725
  for (const hash of msg.hashes) {
568
726
  this._pendingDeletes.get(hash)?.callback(context.from!.hashcode());
569
727
  }
728
+ } else if (msg instanceof BlocksMessage) {
729
+ await this.remoteBlocks.onMessage(msg.message);
730
+ } else if (msg instanceof RequestRoleMessage) {
731
+ if (!context.from) {
732
+ throw new Error("Missing form in update role message");
733
+ }
734
+
735
+ if (context.from.equals(this.node.identity.publicKey)) {
736
+ return;
737
+ }
738
+
739
+ await this.rpc.send(new ResponseRoleMessage({ role: this.role }), {
740
+ to: [context.from!]
741
+ });
742
+ } else if (msg instanceof ResponseRoleMessage) {
743
+ if (!context.from) {
744
+ throw new Error("Missing form in update role message");
745
+ }
746
+
747
+ if (context.from.equals(this.node.identity.publicKey)) {
748
+ return;
749
+ }
750
+
751
+ this.waitFor(context.from, {
752
+ signal: this._closeController.signal,
753
+ timeout: WAIT_FOR_REPLICATOR_TIMEOUT
754
+ })
755
+ .then(async () => {
756
+ /* await delay(1000 * Math.random()) */
757
+
758
+ const prev = this.latestRoleMessages.get(context.from!.hashcode());
759
+ if (prev && prev > context.timestamp) {
760
+ return;
761
+ }
762
+ this.latestRoleMessages.set(
763
+ context.from!.hashcode(),
764
+ context.timestamp
765
+ );
766
+
767
+ await this.modifyReplicators(msg.role, context.from!);
768
+ })
769
+ .catch((e) => {
770
+ if (e instanceof AbortError) {
771
+ return;
772
+ }
773
+
774
+ logger.error(
775
+ "Failed to find peer who updated their role: " + e?.message
776
+ );
777
+ });
570
778
  } else {
571
779
  throw new Error("Unexpected message");
572
780
  }
573
781
  } catch (e: any) {
782
+ if (e instanceof AbortError) {
783
+ return;
784
+ }
785
+
574
786
  if (e instanceof BorshError) {
575
787
  logger.trace(
576
788
  `${this.node.identity.publicKey.hashcode()}: Failed to handle message on topic: ${JSON.stringify(
@@ -579,6 +791,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
579
791
  );
580
792
  return;
581
793
  }
794
+
582
795
  if (e instanceof AccessError) {
583
796
  logger.trace(
584
797
  `${this.node.identity.publicKey.hashcode()}: Failed to handle message for log: ${JSON.stringify(
@@ -591,41 +804,89 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
591
804
  }
592
805
  }
593
806
 
594
- getReplicatorsSorted(): { hash: string; timestamp: number }[] | undefined {
807
+ getReplicatorsSorted(): yallist<ReplicatorRect> | undefined {
595
808
  return this._sortedPeersCache;
596
809
  }
597
810
 
811
+ async waitForReplicator(...keys: PublicSignKey[]) {
812
+ const check = () => {
813
+ for (const k of keys) {
814
+ if (
815
+ !this.getReplicatorsSorted()
816
+ ?.toArray()
817
+ ?.find((x) => x.publicKey.equals(k))
818
+ ) {
819
+ return false;
820
+ }
821
+ }
822
+ return true;
823
+ };
824
+ return waitFor(() => check(), { signal: this._closeController.signal });
825
+ }
826
+
598
827
  async isLeader(
599
828
  slot: { toString(): string },
600
- numberOfLeaders: number
829
+ numberOfLeaders: number,
830
+ options?: {
831
+ candidates?: string[];
832
+ roleAge?: number;
833
+ }
601
834
  ): Promise<boolean> {
602
- const isLeader = (await this.findLeaders(slot, numberOfLeaders)).find(
603
- (l) => l === this.node.identity.publicKey.hashcode()
604
- );
835
+ const isLeader = (
836
+ await this.findLeaders(slot, numberOfLeaders, options)
837
+ ).find((l) => l === this.node.identity.publicKey.hashcode());
605
838
  return !!isLeader;
606
839
  }
607
840
 
841
+ private async waitForIsLeader(
842
+ slot: { toString(): string },
843
+ numberOfLeaders: number,
844
+ timeout = WAIT_FOR_REPLICATOR_TIMEOUT
845
+ ): Promise<boolean> {
846
+ return new Promise((res, rej) => {
847
+ const removeListeners = () => {
848
+ this.events.removeEventListener("role", roleListener);
849
+ this._closeController.signal.addEventListener("abort", abortListener);
850
+ };
851
+ const abortListener = () => {
852
+ removeListeners();
853
+ clearTimeout(timer);
854
+ res(false);
855
+ };
856
+
857
+ const timer = setTimeout(() => {
858
+ removeListeners();
859
+ res(false);
860
+ }, timeout);
861
+
862
+ const check = () =>
863
+ this.isLeader(slot, numberOfLeaders).then((isLeader) => {
864
+ if (isLeader) {
865
+ removeListeners();
866
+ clearTimeout(timer);
867
+ res(isLeader);
868
+ }
869
+ });
870
+
871
+ const roleListener = () => {
872
+ check();
873
+ };
874
+ this.events.addEventListener("role", roleListener);
875
+ this._closeController.signal.addEventListener("abort", abortListener);
876
+
877
+ check();
878
+ });
879
+ }
880
+
608
881
  async findLeaders(
609
882
  subject: { toString(): string },
610
- numberOfLeadersUnbounded: number
883
+ numberOfLeaders: number,
884
+ options?: {
885
+ roleAge?: number;
886
+ }
611
887
  ): 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
888
  // For a fixed set or members, the choosen leaders will always be the same (address invariant)
620
889
  // 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
890
 
630
891
  // Convert this thing we wan't to distribute to 8 bytes so we get can convert it into a u64
631
892
  // modulus into an index
@@ -634,31 +895,220 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
634
895
  const seed = await sha256(utf8writer.finalize());
635
896
 
636
897
  // 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);
898
+ const cursor = hashToUniformNumber(seed); // bounded between 0 and 1
899
+ return this.findLeadersFromUniformNumber(cursor, numberOfLeaders, options);
900
+ }
901
+
902
+ private findLeadersFromUniformNumber(
903
+ cursor: number,
904
+ numberOfLeaders: number,
905
+ options?: {
906
+ roleAge?: number;
907
+ }
908
+ ) {
909
+ const leaders: Set<string> = new Set();
910
+ const width = 1; // this.getParticipationSum(roleAge);
911
+ const peers = this.getReplicatorsSorted();
912
+ if (!peers || peers?.length === 0) {
913
+ return [];
914
+ }
915
+ numberOfLeaders = Math.min(numberOfLeaders, peers.length);
916
+
917
+ const t = +new Date();
918
+ const roleAge =
919
+ options?.roleAge ??
920
+ Math.min(WAIT_FOR_ROLE_MATURITY, +new Date() - this.openTime);
921
+
646
922
  for (let i = 0; i < numberOfLeaders; i++) {
647
- leaders[i] = peers[(i + startIndex) % peers.length].hash;
923
+ let matured = 0;
924
+ const maybeIncrementMatured = (role: Replicator) => {
925
+ if (t - Number(role.timestamp) > roleAge) {
926
+ matured++;
927
+ }
928
+ };
929
+
930
+ const x = ((cursor + i / numberOfLeaders) % 1) * width;
931
+ let currentNode = peers.head;
932
+ const diffs: { diff: number; rect: ReplicatorRect }[] = [];
933
+ while (currentNode) {
934
+ const start = currentNode.value.offset % width;
935
+ const absDelta = Math.abs(start - x);
936
+ const diff = Math.min(absDelta, width - absDelta);
937
+
938
+ if (diff < currentNode.value.role.factor / 2 + 0.00001) {
939
+ leaders.add(currentNode.value.publicKey.hashcode());
940
+ maybeIncrementMatured(currentNode.value.role);
941
+ } else {
942
+ diffs.push({
943
+ diff:
944
+ currentNode.value.role.factor > 0
945
+ ? diff / currentNode.value.role.factor
946
+ : Number.MAX_SAFE_INTEGER,
947
+ rect: currentNode.value
948
+ });
949
+ }
950
+
951
+ currentNode = currentNode.next;
952
+ }
953
+
954
+ if (matured === 0) {
955
+ diffs.sort((x, y) => x.diff - y.diff);
956
+ for (const node of diffs) {
957
+ leaders.add(node.rect.publicKey.hashcode());
958
+ maybeIncrementMatured(node.rect.role);
959
+ if (matured > 0) {
960
+ break;
961
+ }
962
+ }
963
+ }
648
964
  }
649
- return leaders;
965
+
966
+ return [...leaders];
650
967
  }
651
968
 
652
- private async modifySortedSubscriptionCache(
653
- subscribed: boolean,
969
+ /**
970
+ *
971
+ * @returns groups where at least one in any group will have the entry you are looking for
972
+ */
973
+ getReplicatorUnion(roleAge: number = WAIT_FOR_ROLE_MATURITY) {
974
+ // Total replication "width"
975
+ const width = 1; //this.getParticipationSum(roleAge);
976
+
977
+ // How much width you need to "query" to
978
+
979
+ const peers = this.getReplicatorsSorted()!; // TODO types
980
+ const minReplicas = Math.min(
981
+ peers.length,
982
+ this.replicas.min.getValue(this)
983
+ );
984
+ const coveringWidth = width / minReplicas;
985
+
986
+ let walker = peers.head;
987
+ if (this.role instanceof Replicator) {
988
+ // start at our node (local first)
989
+ while (walker) {
990
+ if (walker.value.publicKey.equals(this.node.identity.publicKey)) {
991
+ break;
992
+ }
993
+ walker = walker.next;
994
+ }
995
+ } else {
996
+ const seed = Math.round(peers.length * Math.random()); // start at a random point
997
+ for (let i = 0; i < seed - 1; i++) {
998
+ if (walker?.next == null) {
999
+ break;
1000
+ }
1001
+ walker = walker.next;
1002
+ }
1003
+ }
1004
+
1005
+ const set: string[] = [];
1006
+ let distance = 0;
1007
+ const startNode = walker;
1008
+ if (!startNode) {
1009
+ return [];
1010
+ }
1011
+
1012
+ let nextPoint = startNode.value.offset;
1013
+ const t = +new Date();
1014
+ while (walker && distance < coveringWidth) {
1015
+ const absDelta = Math.abs(walker!.value.offset - nextPoint);
1016
+ const diff = Math.min(absDelta, width - absDelta);
1017
+
1018
+ if (diff < walker!.value.role.factor / 2 + 0.00001) {
1019
+ set.push(walker!.value.publicKey.hashcode());
1020
+ if (
1021
+ t - Number(walker!.value.role.timestamp) >
1022
+ roleAge /* ||
1023
+ walker!.value.publicKey.equals(this.node.identity.publicKey)) */
1024
+ ) {
1025
+ nextPoint = (nextPoint + walker!.value.role.factor) % 1;
1026
+ distance += walker!.value.role.factor;
1027
+ }
1028
+ }
1029
+
1030
+ walker = walker.next || peers.head;
1031
+
1032
+ if (
1033
+ walker?.value.publicKey &&
1034
+ startNode?.value.publicKey.equals(walker?.value.publicKey)
1035
+ ) {
1036
+ break; // TODO throw error for failing to fetch ffull width
1037
+ }
1038
+ }
1039
+
1040
+ return set;
1041
+ }
1042
+
1043
+ async replicator(
1044
+ entry: Entry<any>,
1045
+ options?: {
1046
+ candidates?: string[];
1047
+ roleAge?: number;
1048
+ }
1049
+ ) {
1050
+ return this.isLeader(
1051
+ entry.gid,
1052
+ decodeReplicas(entry).getValue(this),
1053
+ options
1054
+ );
1055
+ }
1056
+
1057
+ private onRoleChange(
1058
+ prev: Observer | Replicator | undefined,
1059
+ role: Observer | Replicator,
654
1060
  publicKey: PublicSignKey
655
1061
  ) {
1062
+ if (this.closed) {
1063
+ return;
1064
+ }
1065
+
1066
+ this.distribute();
1067
+
1068
+ if (role instanceof Replicator) {
1069
+ const timer = setTimeout(async () => {
1070
+ this._closeController.signal.removeEventListener("abort", listener);
1071
+ await this.rebalanceParticipationDebounced?.();
1072
+ this.distribute();
1073
+ }, WAIT_FOR_ROLE_MATURITY + 2000);
1074
+
1075
+ const listener = () => {
1076
+ clearTimeout(timer);
1077
+ };
1078
+
1079
+ this._closeController.signal.addEventListener("abort", listener);
1080
+ }
1081
+
1082
+ this.events.dispatchEvent(
1083
+ new CustomEvent<UpdateRoleEvent>("role", {
1084
+ detail: { publicKey, role }
1085
+ })
1086
+ );
1087
+ }
1088
+
1089
+ private async modifyReplicators(
1090
+ role: Observer | Replicator,
1091
+ publicKey: PublicSignKey
1092
+ ) {
1093
+ const { prev, changed } = await this._modifyReplicators(role, publicKey);
1094
+ if (changed) {
1095
+ await this.rebalanceParticipationDebounced?.(); // await this.rebalanceParticipation(false);
1096
+ this.onRoleChange(prev, role, publicKey);
1097
+ return true;
1098
+ }
1099
+ return false;
1100
+ }
1101
+
1102
+ private async _modifyReplicators(
1103
+ role: Observer | Replicator,
1104
+ publicKey: PublicSignKey
1105
+ ): Promise<{ prev?: Replicator; changed: boolean }> {
656
1106
  if (
657
- subscribed &&
1107
+ role instanceof Replicator &&
658
1108
  this._canReplicate &&
659
- !(await this._canReplicate(publicKey))
1109
+ !(await this._canReplicate(publicKey, role))
660
1110
  ) {
661
- return false;
1111
+ return { changed: false };
662
1112
  }
663
1113
 
664
1114
  const sortedPeer = this._sortedPeersCache;
@@ -666,81 +1116,130 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
666
1116
  if (this.closed === false) {
667
1117
  throw new Error("Unexpected, sortedPeersCache is undefined");
668
1118
  }
669
- return false;
1119
+ return { changed: false };
670
1120
  }
671
- const code = publicKey.hashcode();
672
- if (subscribed) {
1121
+
1122
+ if (role instanceof Replicator && role.factor > 0) {
673
1123
  // 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;
1124
+ // check also that peer is online
1125
+
1126
+ const isOnline =
1127
+ this.node.identity.publicKey.equals(publicKey) ||
1128
+ (await this.waitFor(publicKey, { signal: this._closeController.signal })
1129
+ .then(() => true)
1130
+ .catch(() => false));
1131
+
1132
+ if (isOnline) {
1133
+ // insert or if already there do nothing
1134
+ const code = hashToUniformNumber(publicKey.bytes);
1135
+ const rect: ReplicatorRect = {
1136
+ publicKey,
1137
+ offset: code,
1138
+ role
1139
+ };
1140
+
1141
+ let currentNode = sortedPeer.head;
1142
+ if (!currentNode) {
1143
+ sortedPeer.push(rect);
1144
+ this._totalParticipation += rect.role.factor;
1145
+ return { changed: true };
1146
+ } else {
1147
+ while (currentNode) {
1148
+ if (currentNode.value.publicKey.equals(publicKey)) {
1149
+ // update the value
1150
+ // rect.timestamp = currentNode.value.timestamp;
1151
+ const prev = currentNode.value;
1152
+ currentNode.value = rect;
1153
+ this._totalParticipation += rect.role.factor;
1154
+ this._totalParticipation -= prev.role.factor;
1155
+ // TODO change detection and only do change stuff if diff?
1156
+ return { prev: prev.role, changed: true };
1157
+ }
1158
+
1159
+ if (code > currentNode.value.offset) {
1160
+ const next = currentNode?.next;
1161
+ if (next) {
1162
+ currentNode = next;
1163
+ continue;
1164
+ } else {
1165
+ break;
1166
+ }
1167
+ } else {
1168
+ currentNode = currentNode.prev;
1169
+ break;
1170
+ }
1171
+ }
1172
+
1173
+ const prev = currentNode;
1174
+ if (!prev?.next?.value.publicKey.equals(publicKey)) {
1175
+ this._totalParticipation += rect.role.factor;
1176
+ _insertAfter(sortedPeer, prev || undefined, rect);
1177
+ } else {
1178
+ throw new Error("Unexpected");
1179
+ }
1180
+ return { changed: true };
1181
+ }
678
1182
  } else {
679
- return false;
1183
+ return { changed: false };
680
1184
  }
681
1185
  } 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;
1186
+ let currentNode = sortedPeer.head;
1187
+ while (currentNode) {
1188
+ if (currentNode.value.publicKey.equals(publicKey)) {
1189
+ sortedPeer.removeNode(currentNode);
1190
+ this._totalParticipation -= currentNode.value.role.factor;
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,134 @@ 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: PIDReplicationController;
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
+ async rebalanceParticipation(onRoleChange = true) {
1530
+ // update more participation rate to converge to the average expected rate or bounded by
1531
+ // resources such as memory and or cpu
1532
+
1533
+ if (this.closed) {
1534
+ return false;
1535
+ }
1536
+
1537
+ // The role is fixed (no changes depending on memory usage or peer count etc)
1538
+ if (this._roleOptions instanceof Role) {
1539
+ return false;
1540
+ }
1541
+
1542
+ // TODO second condition: what if the current role is Observer?
1543
+ if (
1544
+ this._roleOptions.type == "replicator" &&
1545
+ this._role instanceof Replicator
1546
+ ) {
1547
+ const peers = this.getReplicatorsSorted();
1548
+ const usedMemory = await this.getMemoryUsage();
1549
+
1550
+ const newFactor =
1551
+ await this.replicationController.adjustReplicationFactor(
1552
+ usedMemory,
1553
+ this._role.factor,
1554
+ this._totalParticipation,
1555
+ peers?.length || 1
1556
+ );
1557
+
1558
+ const newRole = new Replicator({
1559
+ factor: newFactor,
1560
+ timestamp: this._role.timestamp
1561
+ });
1562
+
1563
+ const relativeDifference =
1564
+ Math.abs(this._role.factor - newRole.factor) / this._role.factor;
1565
+
1566
+ if (relativeDifference > 0.0001) {
1567
+ const canReplicate =
1568
+ !this._canReplicate ||
1569
+ (await this._canReplicate(this.node.identity.publicKey, newRole));
1570
+ if (!canReplicate) {
1571
+ return false;
1572
+ }
1573
+
1574
+ await this._updateRole(newRole, onRoleChange);
1575
+ return true;
1576
+ }
1577
+ return false;
1578
+ }
1579
+ return false;
1580
+ }
1581
+ }
1582
+
1583
+ function _insertAfter(
1584
+ self: yallist<any>,
1585
+ node: yallist.Node<ReplicatorRect> | undefined,
1586
+ value: ReplicatorRect
1587
+ ) {
1588
+ const inserted = !node
1589
+ ? new yallist.Node(
1590
+ value,
1591
+ null as any,
1592
+ self.head as yallist.Node<ReplicatorRect> | undefined,
1593
+ self
1594
+ )
1595
+ : new yallist.Node(
1596
+ value,
1597
+ node,
1598
+ node.next as yallist.Node<ReplicatorRect> | undefined,
1599
+ self
1600
+ );
1601
+
1602
+ // is tail
1603
+ if (inserted.next === null) {
1604
+ self.tail = inserted;
1605
+ }
1606
+
1607
+ // is head
1608
+ if (inserted.prev === null) {
1609
+ self.head = inserted;
1610
+ }
1611
+
1612
+ self.length++;
1613
+ return inserted;
982
1614
  }