@peerbit/shared-log 2.0.1 → 3.0.0

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
@@ -1,4 +1,4 @@
1
- import { QueryContext, RPC } from "@peerbit/rpc";
1
+ import { RequestContext, RPC } from "@peerbit/rpc";
2
2
  import { TransportMessage } from "./message.js";
3
3
  import {
4
4
  AppendOptions,
@@ -20,15 +20,16 @@ import {
20
20
  import {
21
21
  AccessError,
22
22
  getPublicKeyFromPeerId,
23
+ PublicSignKey,
23
24
  sha256,
24
25
  sha256Base64Sync,
25
26
  } from "@peerbit/crypto";
26
27
  import { logger as loggerFn } from "@peerbit/logger";
27
28
  import {
28
- AbsolutMinReplicas,
29
29
  EntryWithRefs,
30
30
  ExchangeHeadsMessage,
31
- MinReplicas,
31
+ RequestIHave,
32
+ ResponseIHave,
32
33
  createExchangeHeadsMessage,
33
34
  } from "./exchange-heads.js";
34
35
  import {
@@ -38,7 +39,17 @@ import {
38
39
  import { startsWith } from "@peerbit/uint8arrays";
39
40
  import { TimeoutError } from "@peerbit/time";
40
41
  import { REPLICATOR_TYPE_VARIANT, Observer, Replicator, Role } from "./role.js";
41
-
42
+ import {
43
+ AbsoluteReplicas,
44
+ MinReplicas,
45
+ decodeReplicas,
46
+ encodeReplicas,
47
+ maxReplicas,
48
+ } from "./replication.js";
49
+ import pDefer, { DeferredPromise } from "p-defer";
50
+ import { Cache } from "@peerbit/cache";
51
+
52
+ export * from "./replication.js";
42
53
  export { Observer, Replicator, Role };
43
54
 
44
55
  export const logger = loggerFn({ module: "peer" });
@@ -63,15 +74,25 @@ const groupByGid = async <T extends Entry<any> | EntryWithRefs<any>>(
63
74
 
64
75
  export type SyncFilter = (entries: Entry<any>) => Promise<boolean> | boolean;
65
76
 
77
+ type ReplicationLimits = { min: MinReplicas; max?: MinReplicas };
78
+ export type ReplicationLimitsOptions =
79
+ | Partial<ReplicationLimits>
80
+ | { min?: number; max?: number };
81
+
66
82
  export interface SharedLogOptions {
67
- minReplicas?: number;
83
+ replicas?: ReplicationLimitsOptions;
68
84
  sync?: SyncFilter;
69
85
  role?: Role;
86
+ respondToIHaveTimeout?: number;
87
+ canReplicate?: (publicKey: PublicSignKey) => Promise<boolean> | boolean;
70
88
  }
71
89
 
72
90
  export const DEFAULT_MIN_REPLICAS = 2;
73
91
 
74
92
  export type Args<T> = LogProperties<T> & LogEvents<T> & SharedLogOptions;
93
+ export type SharedAppendOptions<T> = AppendOptions<T> & {
94
+ replicas?: AbsoluteReplicas | number;
95
+ };
75
96
 
76
97
  @variant("shared_log")
77
98
  export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
@@ -82,9 +103,8 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
82
103
  rpc: RPC<TransportMessage, TransportMessage>;
83
104
 
84
105
  // options
85
- private _minReplicas: MinReplicas;
86
106
  private _sync?: SyncFilter;
87
- private _role: Role;
107
+ private _role: Observer | Replicator;
88
108
 
89
109
  private _sortedPeersCache: { hash: string; timestamp: number }[] | undefined;
90
110
  private _lastSubscriptionMessageId: number;
@@ -92,44 +112,134 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
92
112
 
93
113
  private _onSubscriptionFn: (arg: any) => any;
94
114
  private _onUnsubscriptionFn: (arg: any) => any;
115
+
116
+ private _canReplicate?: (
117
+ publicKey: PublicSignKey
118
+ ) => Promise<boolean> | boolean;
119
+
95
120
  private _logProperties?: LogProperties<T> & LogEvents<T>;
96
121
 
122
+ private _loadedOnce = false;
123
+ private _gidParentCache: Cache<Entry<any>[]>;
124
+ private _respondToIHaveTimeout;
125
+ private _pendingDeletes: Map<
126
+ string,
127
+ {
128
+ promise: DeferredPromise<void>;
129
+ clear: () => void;
130
+ callback: (publicKeyHash: string) => Promise<void> | void;
131
+ }
132
+ >;
133
+
134
+ private _pendingIHave: Map<
135
+ string,
136
+ { clear: () => void; callback: () => void }
137
+ >;
138
+
139
+ replicas: ReplicationLimits;
140
+
97
141
  constructor(properties?: { id?: Uint8Array }) {
98
142
  super();
99
143
  this.log = new Log(properties);
100
144
  this.rpc = new RPC();
101
145
  }
102
146
 
103
- get minReplicas() {
104
- return this._minReplicas;
147
+ get role(): Observer | Replicator {
148
+ return this._role;
105
149
  }
106
150
 
107
- set minReplicas(minReplicas: MinReplicas) {
108
- this._minReplicas = minReplicas;
151
+ async updateRole(role: Observer | Replicator) {
152
+ const wasRepicators = this._role instanceof Replicator;
153
+ this._role = role;
154
+ await this.initializeWithRole();
155
+ await this.rpc.subscribe(serialize(this._role));
156
+
157
+ if (wasRepicators) {
158
+ await this.replicationReorganization();
159
+ }
109
160
  }
110
- get role(): Role {
111
- return this._role;
161
+
162
+ private async initializeWithRole() {
163
+ try {
164
+ await this.modifySortedSubscriptionCache(
165
+ this._role instanceof Replicator ? true : false,
166
+ getPublicKeyFromPeerId(this.node.peerId)
167
+ );
168
+
169
+ if (!this._loadedOnce) {
170
+ await this.log.load();
171
+ this._loadedOnce = true;
172
+ }
173
+ } catch (error) {
174
+ if (error instanceof AccessError) {
175
+ logger.error(
176
+ "Failed to load all entries due to access error, make sure you are opening the program with approate keychain configuration"
177
+ );
178
+ } else {
179
+ throw error;
180
+ }
181
+ }
112
182
  }
113
183
 
114
184
  async append(
115
185
  data: T,
116
- options?: AppendOptions<T> | undefined
186
+ options?: SharedAppendOptions<T> | undefined
117
187
  ): Promise<{
118
188
  entry: Entry<T>;
119
189
  removed: Entry<T>[];
120
190
  }> {
121
- const result = await this.log.append(data, options);
191
+ const appendOptions: AppendOptions<T> = { ...options };
192
+ const minReplicasData = encodeReplicas(
193
+ options?.replicas
194
+ ? typeof options.replicas === "number"
195
+ ? new AbsoluteReplicas(options.replicas)
196
+ : options.replicas
197
+ : this.replicas.min
198
+ );
199
+
200
+ if (!appendOptions.meta) {
201
+ appendOptions.meta = {
202
+ data: minReplicasData,
203
+ };
204
+ } else {
205
+ appendOptions.meta.data = minReplicasData;
206
+ }
207
+
208
+ const result = await this.log.append(data, appendOptions);
209
+
122
210
  await this.rpc.send(
123
- await createExchangeHeadsMessage(this.log, [result.entry], true)
211
+ await createExchangeHeadsMessage(
212
+ this.log,
213
+ [result.entry],
214
+ this._gidParentCache
215
+ )
124
216
  );
125
217
  return result;
126
218
  }
127
219
 
128
220
  async open(options?: Args<T>): Promise<void> {
129
- this._minReplicas = new AbsolutMinReplicas(options?.minReplicas || 2);
221
+ this.replicas = {
222
+ min: options?.replicas?.min
223
+ ? typeof options?.replicas?.min === "number"
224
+ ? new AbsoluteReplicas(options?.replicas?.min)
225
+ : options?.replicas?.min
226
+ : new AbsoluteReplicas(DEFAULT_MIN_REPLICAS),
227
+ max: options?.replicas?.max
228
+ ? typeof options?.replicas?.max === "number"
229
+ ? new AbsoluteReplicas(options?.replicas?.max)
230
+ : options.replicas.max
231
+ : undefined,
232
+ };
233
+ this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 10 * 1000; // TODO make into arg
234
+ this._pendingDeletes = new Map();
235
+ this._pendingIHave = new Map();
236
+
237
+ this._gidParentCache = new Cache({ max: 1000 });
238
+
239
+ this._canReplicate = options?.canReplicate;
130
240
  this._sync = options?.sync;
131
- this._role = options?.role || new Replicator();
132
241
  this._logProperties = options;
242
+ this._role = options?.role || new Replicator();
133
243
 
134
244
  this._lastSubscriptionMessageId = 0;
135
245
  this._onSubscriptionFn = this._onSubscription.bind(this);
@@ -152,10 +262,39 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
152
262
  keychain: this.node.keychain,
153
263
 
154
264
  ...this._logProperties,
265
+ onChange: (change) => {
266
+ if (this._pendingIHave.size > 0) {
267
+ for (const added of change.added) {
268
+ const ih = this._pendingIHave.get(added.hash);
269
+ if (ih) {
270
+ ih.clear();
271
+ ih.callback();
272
+ }
273
+ }
274
+ }
275
+ return this._logProperties?.onChange?.(change);
276
+ },
277
+ canAppend: async (entry) => {
278
+ const replicas = decodeReplicas(entry).getValue(this);
279
+ if (Number.isFinite(replicas) === false) {
280
+ return false;
281
+ }
282
+
283
+ // Don't verify entries that we have created (TODO should we? perf impact?)
284
+ if (!entry.createdLocally && !(await entry.verifySignatures())) {
285
+ return false;
286
+ }
287
+
288
+ return this._logProperties?.canAppend?.(entry) ?? true;
289
+ },
155
290
  trim: this._logProperties?.trim && {
156
291
  ...this._logProperties?.trim,
157
292
  filter: {
158
- canTrim: async (gid) => !(await this.isLeader(gid)), // TODO types
293
+ canTrim: async (entry) =>
294
+ !(await this.isLeader(
295
+ entry.meta.gid,
296
+ decodeReplicas(entry).getValue(this)
297
+ )), // TODO types
159
298
  cacheId: () => this._lastSubscriptionMessageId,
160
299
  },
161
300
  },
@@ -164,31 +303,13 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
164
303
  (await this.node.memory.sublevel(sha256Base64Sync(this.log.id))),
165
304
  });
166
305
 
167
- try {
168
- if (this._role instanceof Replicator) {
169
- this.modifySortedSubscriptionCache(
170
- true,
171
- getPublicKeyFromPeerId(this.node.peerId).hashcode()
172
- );
173
- await this.log.load();
174
- } else {
175
- await this.log.load({ heads: true, reload: true });
176
- }
177
- } catch (error) {
178
- if (error instanceof AccessError) {
179
- logger.error(
180
- "Failed to load all entries due to access error, make sure you are opening the program with approate keychain configuration"
181
- );
182
- } else {
183
- throw error;
184
- }
185
- }
306
+ await this.initializeWithRole();
186
307
 
187
308
  // Take into account existing subscription
188
309
  (await this.node.services.pubsub.getSubscribers(this.topic))?.forEach(
189
310
  (v, k) => {
190
311
  this.handleSubscriptionChange(
191
- k,
312
+ v.publicKey,
192
313
  [{ topic: this.topic, data: v.data }],
193
314
  true
194
315
  );
@@ -210,8 +331,21 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
210
331
  }
211
332
 
212
333
  private async _close() {
334
+ for (const [k, v] of this._pendingDeletes) {
335
+ v.clear();
336
+ v.promise.resolve(); // TODO or reject?
337
+ }
338
+ for (const [k, v] of this._pendingIHave) {
339
+ v.clear();
340
+ }
341
+
342
+ this._gidParentCache.clear();
343
+ this._pendingDeletes = new Map();
344
+ this._pendingIHave = new Map();
345
+
213
346
  this._gidPeersHistory = new Map();
214
347
  this._sortedPeersCache = undefined;
348
+ this._loadedOnce = false;
215
349
 
216
350
  this.node.services.pubsub.removeEventListener(
217
351
  "subscribe",
@@ -247,12 +381,12 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
247
381
  // Callback for receiving a message from the network
248
382
  async _onMessage(
249
383
  msg: TransportMessage,
250
- context: QueryContext
384
+ context: RequestContext
251
385
  ): Promise<TransportMessage | undefined> {
252
386
  try {
253
387
  if (msg instanceof ExchangeHeadsMessage) {
254
388
  /**
255
- * I have recieved heads from someone else.
389
+ * I have received heads from someone else.
256
390
  * I can use them to load associated logs and join/sync them with the data stores I own
257
391
  */
258
392
 
@@ -277,30 +411,142 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
277
411
  }
278
412
  }
279
413
 
280
- let toMerge: EntryWithRefs<any>[];
281
414
  if (!this._sync) {
282
- toMerge = [];
283
- for (const [gid, value] of await groupByGid(filteredHeads)) {
284
- if (!(await this.isLeader(gid, this._minReplicas.value))) {
415
+ const toMerge: EntryWithRefs<any>[] = [];
416
+
417
+ let toDelete: Entry<any>[] | undefined = undefined;
418
+ let maybeDelete: EntryWithRefs<any>[][] | undefined = undefined;
419
+
420
+ const groupedByGid = await groupByGid(filteredHeads);
421
+
422
+ for (const [gid, entries] of groupedByGid) {
423
+ const headsWithGid = this.log.headsIndex.gids.get(gid);
424
+ const maxReplicasFromHead =
425
+ headsWithGid && headsWithGid.size > 0
426
+ ? maxReplicas(this, [...headsWithGid.values()])
427
+ : this.replicas.min.getValue(this);
428
+
429
+ const maxReplicasFromNewEntries = maxReplicas(this, [
430
+ ...entries.map((x) => x.entry),
431
+ ]);
432
+
433
+ const isLeader = await this.isLeader(
434
+ gid,
435
+ Math.max(maxReplicasFromHead, maxReplicasFromNewEntries)
436
+ );
437
+
438
+ if (maxReplicasFromNewEntries < maxReplicasFromHead && isLeader) {
439
+ (maybeDelete || (maybeDelete = [])).push(entries);
440
+ }
441
+
442
+ outer: for (const entry of entries) {
443
+ if (isLeader) {
444
+ toMerge.push(entry);
445
+ } else {
446
+ for (const ref of entry.references) {
447
+ const map = this.log.headsIndex.gids.get(
448
+ await ref.getGid()
449
+ );
450
+ if (map && map.size > 0) {
451
+ toMerge.push(entry);
452
+ (toDelete || (toDelete = [])).push(entry.entry);
453
+ continue outer;
454
+ }
455
+ }
456
+ }
457
+
285
458
  logger.debug(
286
- `${this.node.identity.publicKey.hashcode()}: Dropping heads with gid: ${gid}. Because not leader`
459
+ `${this.node.identity.publicKey.hashcode()}: Dropping heads with gid: ${
460
+ entry.entry.gid
461
+ }. Because not leader`
287
462
  );
288
- continue;
289
463
  }
290
- for (const head of value) {
291
- toMerge.push(head);
464
+ }
465
+
466
+ if (toMerge.length > 0) {
467
+ await this.log.join(toMerge);
468
+ toDelete &&
469
+ Promise.all(this.pruneSafely(toDelete)).catch((e) => {
470
+ logger.error(e.toString());
471
+ });
472
+ }
473
+
474
+ if (maybeDelete) {
475
+ for (const entries of maybeDelete) {
476
+ const headsWithGid = this.log.headsIndex.gids.get(
477
+ entries[0].entry.meta.gid
478
+ );
479
+ if (headsWithGid && headsWithGid.size > 0) {
480
+ const minReplicas = maxReplicas(this, [
481
+ ...headsWithGid.values(),
482
+ ]);
483
+
484
+ const isLeader = await this.isLeader(
485
+ entries[0].entry.meta.gid,
486
+ minReplicas
487
+ );
488
+ if (!isLeader) {
489
+ Promise.all(
490
+ this.pruneSafely(entries.map((x) => x.entry))
491
+ ).catch((e) => {
492
+ logger.error(e.toString());
493
+ });
494
+ }
495
+ }
292
496
  }
293
497
  }
294
498
  } else {
295
- toMerge = await Promise.all(
296
- filteredHeads.map((x) => this._sync!(x.entry))
297
- ).then((filter) => filteredHeads.filter((v, ix) => filter[ix]));
499
+ await this.log.join(
500
+ await Promise.all(
501
+ filteredHeads.map((x) => this._sync!(x.entry))
502
+ ).then((filter) => filteredHeads.filter((v, ix) => filter[ix]))
503
+ );
298
504
  }
505
+ }
506
+ } else if (msg instanceof RequestIHave) {
507
+ const hasAndIsLeader: string[] = [];
508
+ for (const hash of msg.hashes) {
509
+ const indexedEntry = this.log.entryIndex.getShallow(hash);
510
+ if (
511
+ indexedEntry &&
512
+ (await this.isLeader(
513
+ indexedEntry.meta.gid,
514
+ decodeReplicas(indexedEntry).getValue(this)
515
+ ))
516
+ ) {
517
+ hasAndIsLeader.push(hash);
518
+ } else {
519
+ const prevPendingIHave = this._pendingIHave.get(hash);
520
+ const pendingIHave = {
521
+ clear: () => {
522
+ clearTimeout(timeout);
523
+ prevPendingIHave?.clear();
524
+ },
525
+ callback: () => {
526
+ prevPendingIHave && prevPendingIHave.callback();
527
+ this.rpc.send(new ResponseIHave({ hashes: [hash] }), {
528
+ to: [context.from!],
529
+ });
530
+ this._pendingIHave.delete(hash);
531
+ },
532
+ };
533
+ const timeout = setTimeout(() => {
534
+ const pendingIHaveRef = this._pendingIHave.get(hash);
535
+ if (pendingIHave === pendingIHaveRef) {
536
+ this._pendingIHave.delete(hash);
537
+ }
538
+ }, this._respondToIHaveTimeout);
299
539
 
300
- if (toMerge.length > 0) {
301
- await this.log.join(toMerge);
540
+ this._pendingIHave.set(hash, pendingIHave);
302
541
  }
303
542
  }
543
+ this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
544
+ to: [context.from!],
545
+ });
546
+ } else if (msg instanceof ResponseIHave) {
547
+ for (const hash of msg.hashes) {
548
+ this._pendingDeletes.get(hash)?.callback(context.from!.hashcode());
549
+ }
304
550
  } else {
305
551
  throw new Error("Unexpected message");
306
552
  }
@@ -331,7 +577,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
331
577
 
332
578
  async isLeader(
333
579
  slot: { toString(): string },
334
- numberOfLeaders: number = this.minReplicas.value
580
+ numberOfLeaders: number
335
581
  ): Promise<boolean> {
336
582
  const isLeader = (await this.findLeaders(slot, numberOfLeaders)).find(
337
583
  (l) => l === this.node.identity.publicKey.hashcode()
@@ -341,8 +587,15 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
341
587
 
342
588
  async findLeaders(
343
589
  subject: { toString(): string },
344
- numberOfLeaders: number = this.minReplicas.value
590
+ numberOfLeadersUnbounded: number
345
591
  ): Promise<string[]> {
592
+ const lower = this.replicas.min.getValue(this);
593
+ const higher = this.replicas.max?.getValue(this) ?? Number.MAX_SAFE_INTEGER;
594
+ let numberOfLeaders = Math.max(
595
+ Math.min(higher, numberOfLeadersUnbounded),
596
+ lower
597
+ );
598
+
346
599
  // For a fixed set or members, the choosen leaders will always be the same (address invariant)
347
600
  // This allows for that same content is always chosen to be distributed to same peers, to remove unecessary copies
348
601
  const peers: { hash: string; timestamp: number }[] =
@@ -376,33 +629,53 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
376
629
  return leaders;
377
630
  }
378
631
 
379
- private modifySortedSubscriptionCache(subscribed: boolean, fromHash: string) {
632
+ private async modifySortedSubscriptionCache(
633
+ subscribed: boolean,
634
+ publicKey: PublicSignKey
635
+ ) {
636
+ if (
637
+ subscribed &&
638
+ this._canReplicate &&
639
+ !(await this._canReplicate(publicKey))
640
+ ) {
641
+ return false;
642
+ }
643
+
380
644
  const sortedPeer = this._sortedPeersCache;
381
645
  if (!sortedPeer) {
382
646
  if (this.closed === false) {
383
647
  throw new Error("Unexpected, sortedPeersCache is undefined");
384
648
  }
385
- return;
649
+ return false;
386
650
  }
387
- const code = fromHash;
651
+ const code = publicKey.hashcode();
388
652
  if (subscribed) {
389
653
  // TODO use Set + list for fast lookup
390
654
  if (!sortedPeer.find((x) => x.hash === code)) {
391
655
  sortedPeer.push({ hash: code, timestamp: +new Date() });
392
656
  sortedPeer.sort((a, b) => a.hash.localeCompare(b.hash));
657
+ return true;
658
+ } else {
659
+ return false;
393
660
  }
394
661
  } else {
395
662
  const deleteIndex = sortedPeer.findIndex((x) => x.hash === code);
396
- sortedPeer.splice(deleteIndex, 1);
663
+ if (deleteIndex >= 0) {
664
+ sortedPeer.splice(deleteIndex, 1);
665
+ return true;
666
+ } else {
667
+ return false;
668
+ }
397
669
  }
398
670
  }
399
671
 
400
672
  async handleSubscriptionChange(
401
- fromHash: string,
673
+ publicKey: PublicSignKey,
402
674
  changes: { topic: string; data?: Uint8Array }[],
403
675
  subscribed: boolean
404
676
  ) {
405
677
  // TODO why are we doing two loops?
678
+ const prev: boolean[] = [];
406
679
  for (const subscription of changes) {
407
680
  if (this.log.idString !== subscription.topic) {
408
681
  continue;
@@ -412,20 +685,27 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
412
685
  !subscription.data ||
413
686
  !startsWith(subscription.data, REPLICATOR_TYPE_VARIANT)
414
687
  ) {
688
+ prev.push(await this.modifySortedSubscriptionCache(false, publicKey));
415
689
  continue;
690
+ } else {
691
+ this._lastSubscriptionMessageId += 1;
692
+ prev.push(
693
+ await this.modifySortedSubscriptionCache(subscribed, publicKey)
694
+ );
416
695
  }
417
- this._lastSubscriptionMessageId += 1;
418
- this.modifySortedSubscriptionCache(subscribed, fromHash);
419
696
  }
420
697
 
421
- for (const subscription of changes) {
698
+ // TODO don't do this i fnot is replicator?
699
+ for (const [i, subscription] of changes.entries()) {
422
700
  if (this.log.idString !== subscription.topic) {
423
701
  continue;
424
702
  }
425
703
  if (subscription.data) {
426
704
  try {
427
705
  const type = deserialize(subscription.data, Role);
428
- if (type instanceof Replicator) {
706
+
707
+ // Reorganize if the new subscriber is a replicator, or observers AND was replicator
708
+ if (type instanceof Replicator || prev[i]) {
429
709
  await this.replicationReorganization();
430
710
  }
431
711
  } catch (error: any) {
@@ -440,6 +720,84 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
440
720
  }
441
721
  }
442
722
 
723
+ pruneSafely(entries: Entry<any>[], options?: { timeout: number }) {
724
+ // ask network if they have they entry,
725
+ // so I can delete it
726
+
727
+ // There is a few reasons why we might end up here
728
+
729
+ // - Two logs merge, and we should not anymore keep the joined log replicated (because we are not responsible for the resulting gid)
730
+ // - An entry is joined, where min replicas is lower than before (for all heads for this particular gid) and therefore we are not replicating anymore for this particular gid
731
+ // - Peers join and leave, which means we might not be a replicator anymore
732
+
733
+ const promises: Promise<any>[] = [];
734
+ const filteredEntries: Entry<any>[] = [];
735
+ for (const entry of entries) {
736
+ const pendingPrev = this._pendingDeletes.get(entry.hash);
737
+
738
+ filteredEntries.push(entry);
739
+ const existCounter = new Set<string>();
740
+ const minReplicas = decodeReplicas(entry);
741
+ const deferredPromise: DeferredPromise<void> = pDefer();
742
+
743
+ const clear = () => {
744
+ pendingPrev?.clear();
745
+ const pending = this._pendingDeletes.get(entry.hash);
746
+ if (pending?.promise == deferredPromise) {
747
+ this._pendingDeletes.delete(entry.hash);
748
+ }
749
+ clearTimeout(timeout);
750
+ };
751
+ const resolve = () => {
752
+ clear();
753
+ deferredPromise.resolve();
754
+ };
755
+
756
+ const reject = (e: any) => {
757
+ clear();
758
+ deferredPromise.reject(e);
759
+ };
760
+
761
+ const timeout = setTimeout(() => {
762
+ reject(new Error("Timeout"));
763
+ }, options?.timeout ?? 10 * 1000);
764
+
765
+ this._pendingDeletes.set(entry.hash, {
766
+ promise: deferredPromise,
767
+ clear: () => {
768
+ clear();
769
+ },
770
+ callback: async (publicKeyHash: string) => {
771
+ const minReplicasValue = minReplicas.getValue(this);
772
+ const l = await this.findLeaders(entry.gid, minReplicasValue);
773
+ if (l.find((x) => x === publicKeyHash)) {
774
+ existCounter.add(publicKeyHash);
775
+ if (minReplicas.getValue(this) <= existCounter.size) {
776
+ this.log
777
+ .remove(entry, {
778
+ recursively: true,
779
+ })
780
+ .then(() => {
781
+ resolve();
782
+ })
783
+ .catch((e: any) => {
784
+ reject(new Error("Failed to delete entry: " + e.toString()));
785
+ });
786
+ }
787
+ }
788
+ },
789
+ });
790
+ promises.push(deferredPromise.promise);
791
+ }
792
+ if (filteredEntries.length > 0) {
793
+ this.rpc.send(
794
+ new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) })
795
+ );
796
+ }
797
+
798
+ return promises;
799
+ }
800
+
443
801
  /**
444
802
  * 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
445
803
  * 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
@@ -459,14 +817,18 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
459
817
  }
460
818
 
461
819
  const oldPeersSet = this._gidPeersHistory.get(gid);
462
- const currentPeers = await this.findLeaders(gid);
820
+ const currentPeers = await this.findLeaders(
821
+ gid,
822
+ maxReplicas(this, entries) // pick max replication policy of all entries, so all information is treated equally important as the most important
823
+ );
824
+
463
825
  for (const currentPeer of currentPeers) {
464
826
  if (
465
827
  !oldPeersSet?.has(currentPeer) &&
466
828
  currentPeer !== this.node.identity.publicKey.hashcode()
467
829
  ) {
468
830
  storeChanged = true;
469
- // second condition means that if the new peer is us, we should not do anything, since we are expecting to recieve heads, not send
831
+ // second condition means that if the new peer is us, we should not do anything, since we are expecting to receive heads, not send
470
832
  newPeers.push(currentPeer);
471
833
 
472
834
  // send heads to the new peer
@@ -508,8 +870,8 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
508
870
  // delete entries since we are not suppose to replicate this anymore
509
871
  // TODO add delay? freeze time? (to ensure resiliance for bad io)
510
872
  if (entriesToDelete.length > 0) {
511
- await this.log.remove(entriesToDelete, {
512
- recursively: true,
873
+ Promise.all(this.pruneSafely(entriesToDelete)).catch((e) => {
874
+ logger.error(e.toString());
513
875
  });
514
876
  }
515
877
 
@@ -523,10 +885,10 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
523
885
  const message = await createExchangeHeadsMessage(
524
886
  this.log,
525
887
  [...toSend.values()], // TODO send to peers directly
526
- true
888
+ this._gidParentCache
527
889
  );
528
890
 
529
- // TODO perhaps send less messages to more recievers for performance reasons?
891
+ // TODO perhaps send less messages to more receivers for performance reasons?
530
892
  await this.rpc.send(message, {
531
893
  to: newPeers,
532
894
  strict: true,
@@ -538,9 +900,13 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
538
900
  return storeChanged || changed;
539
901
  }
540
902
 
541
- replicators() {
903
+ /**
904
+ *
905
+ * @returns groups where at least one in any group will have the entry you are looking for
906
+ */
907
+ getDiscoveryGroups() {
542
908
  // TODO Optimize this so we don't have to recreate the array all the time!
543
- const minReplicas = this.minReplicas.value;
909
+ const minReplicas = this.replicas.min.getValue(this);
544
910
  const replicators = this.getReplicatorsSorted();
545
911
  if (!replicators) {
546
912
  return []; // No subscribers and we are not replicating
@@ -560,8 +926,8 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
560
926
 
561
927
  return groups;
562
928
  }
563
- async replicator(gid) {
564
- return this.isLeader(gid);
929
+ async replicator(entry: Entry<any>) {
930
+ return this.isLeader(entry.gid, decodeReplicas(entry).getValue(this));
565
931
  }
566
932
 
567
933
  async _onUnsubscription(evt: CustomEvent<UnsubcriptionEvent>) {
@@ -572,7 +938,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
572
938
  );
573
939
 
574
940
  return this.handleSubscriptionChange(
575
- evt.detail.from.hashcode(),
941
+ evt.detail.from,
576
942
  evt.detail.unsubscriptions,
577
943
  false
578
944
  );
@@ -585,7 +951,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
585
951
  )}'`
586
952
  );
587
953
  return this.handleSubscriptionChange(
588
- evt.detail.from.hashcode(),
954
+ evt.detail.from,
589
955
  evt.detail.subscriptions,
590
956
  true
591
957
  );