@peerbit/shared-log 2.0.1 → 3.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/lib/esm/index.js CHANGED
@@ -14,10 +14,14 @@ import { Program } from "@peerbit/program";
14
14
  import { BinaryReader, BinaryWriter, BorshError, deserialize, field, serialize, variant, } from "@dao-xyz/borsh";
15
15
  import { AccessError, getPublicKeyFromPeerId, sha256, sha256Base64Sync, } from "@peerbit/crypto";
16
16
  import { logger as loggerFn } from "@peerbit/logger";
17
- import { AbsolutMinReplicas, ExchangeHeadsMessage, createExchangeHeadsMessage, } from "./exchange-heads.js";
17
+ import { ExchangeHeadsMessage, RequestIHave, ResponseIHave, createExchangeHeadsMessage, } from "./exchange-heads.js";
18
18
  import { startsWith } from "@peerbit/uint8arrays";
19
19
  import { TimeoutError } from "@peerbit/time";
20
20
  import { REPLICATOR_TYPE_VARIANT, Observer, Replicator, Role } from "./role.js";
21
+ import { AbsoluteReplicas, decodeReplicas, encodeReplicas, maxReplicas, } from "./replication.js";
22
+ import pDefer from "p-defer";
23
+ import { Cache } from "@peerbit/cache";
24
+ export * from "./replication.js";
21
25
  export { Observer, Replicator, Role };
22
26
  export const logger = loggerFn({ module: "peer" });
23
27
  const groupByGid = async (entries) => {
@@ -40,7 +44,6 @@ export let SharedLog = class SharedLog extends Program {
40
44
  log;
41
45
  rpc;
42
46
  // options
43
- _minReplicas;
44
47
  _sync;
45
48
  _role;
46
49
  _sortedPeersCache;
@@ -48,31 +51,88 @@ export let SharedLog = class SharedLog extends Program {
48
51
  _gidPeersHistory;
49
52
  _onSubscriptionFn;
50
53
  _onUnsubscriptionFn;
54
+ _canReplicate;
51
55
  _logProperties;
56
+ _loadedOnce = false;
57
+ _gidParentCache;
58
+ _respondToIHaveTimeout;
59
+ _pendingDeletes;
60
+ _pendingIHave;
61
+ replicas;
52
62
  constructor(properties) {
53
63
  super();
54
64
  this.log = new Log(properties);
55
65
  this.rpc = new RPC();
56
66
  }
57
- get minReplicas() {
58
- return this._minReplicas;
59
- }
60
- set minReplicas(minReplicas) {
61
- this._minReplicas = minReplicas;
62
- }
63
67
  get role() {
64
68
  return this._role;
65
69
  }
70
+ async updateRole(role) {
71
+ const wasRepicators = this._role instanceof Replicator;
72
+ this._role = role;
73
+ await this.initializeWithRole();
74
+ await this.rpc.subscribe(serialize(this._role));
75
+ if (wasRepicators) {
76
+ await this.replicationReorganization();
77
+ }
78
+ }
79
+ async initializeWithRole() {
80
+ try {
81
+ await this.modifySortedSubscriptionCache(this._role instanceof Replicator ? true : false, getPublicKeyFromPeerId(this.node.peerId));
82
+ if (!this._loadedOnce) {
83
+ await this.log.load();
84
+ this._loadedOnce = true;
85
+ }
86
+ }
87
+ catch (error) {
88
+ if (error instanceof AccessError) {
89
+ logger.error("Failed to load all entries due to access error, make sure you are opening the program with approate keychain configuration");
90
+ }
91
+ else {
92
+ throw error;
93
+ }
94
+ }
95
+ }
66
96
  async append(data, options) {
67
- const result = await this.log.append(data, options);
68
- await this.rpc.send(await createExchangeHeadsMessage(this.log, [result.entry], true));
97
+ const appendOptions = { ...options };
98
+ const minReplicasData = encodeReplicas(options?.replicas
99
+ ? typeof options.replicas === "number"
100
+ ? new AbsoluteReplicas(options.replicas)
101
+ : options.replicas
102
+ : this.replicas.min);
103
+ if (!appendOptions.meta) {
104
+ appendOptions.meta = {
105
+ data: minReplicasData,
106
+ };
107
+ }
108
+ else {
109
+ appendOptions.meta.data = minReplicasData;
110
+ }
111
+ const result = await this.log.append(data, appendOptions);
112
+ await this.rpc.send(await createExchangeHeadsMessage(this.log, [result.entry], this._gidParentCache));
69
113
  return result;
70
114
  }
71
115
  async open(options) {
72
- this._minReplicas = new AbsolutMinReplicas(options?.minReplicas || 2);
116
+ this.replicas = {
117
+ min: options?.replicas?.min
118
+ ? typeof options?.replicas?.min === "number"
119
+ ? new AbsoluteReplicas(options?.replicas?.min)
120
+ : options?.replicas?.min
121
+ : new AbsoluteReplicas(DEFAULT_MIN_REPLICAS),
122
+ max: options?.replicas?.max
123
+ ? typeof options?.replicas?.max === "number"
124
+ ? new AbsoluteReplicas(options?.replicas?.max)
125
+ : options.replicas.max
126
+ : undefined,
127
+ };
128
+ this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 10 * 1000; // TODO make into arg
129
+ this._pendingDeletes = new Map();
130
+ this._pendingIHave = new Map();
131
+ this._gidParentCache = new Cache({ max: 1000 });
132
+ this._canReplicate = options?.canReplicate;
73
133
  this._sync = options?.sync;
74
- this._role = options?.role || new Replicator();
75
134
  this._logProperties = options;
135
+ this._role = options?.role || new Replicator();
76
136
  this._lastSubscriptionMessageId = 0;
77
137
  this._onSubscriptionFn = this._onSubscription.bind(this);
78
138
  this._sortedPeersCache = [];
@@ -83,36 +143,43 @@ export let SharedLog = class SharedLog extends Program {
83
143
  await this.log.open(this.node.services.blocks, this.node.identity, {
84
144
  keychain: this.node.keychain,
85
145
  ...this._logProperties,
146
+ onChange: (change) => {
147
+ if (this._pendingIHave.size > 0) {
148
+ for (const added of change.added) {
149
+ const ih = this._pendingIHave.get(added.hash);
150
+ if (ih) {
151
+ ih.clear();
152
+ ih.callback();
153
+ }
154
+ }
155
+ }
156
+ return this._logProperties?.onChange?.(change);
157
+ },
158
+ canAppend: async (entry) => {
159
+ const replicas = decodeReplicas(entry).getValue(this);
160
+ if (Number.isFinite(replicas) === false) {
161
+ return false;
162
+ }
163
+ // Don't verify entries that we have created (TODO should we? perf impact?)
164
+ if (!entry.createdLocally && !(await entry.verifySignatures())) {
165
+ return false;
166
+ }
167
+ return this._logProperties?.canAppend?.(entry) ?? true;
168
+ },
86
169
  trim: this._logProperties?.trim && {
87
170
  ...this._logProperties?.trim,
88
171
  filter: {
89
- canTrim: async (gid) => !(await this.isLeader(gid)),
172
+ canTrim: async (entry) => !(await this.isLeader(entry.meta.gid, decodeReplicas(entry).getValue(this))),
90
173
  cacheId: () => this._lastSubscriptionMessageId,
91
174
  },
92
175
  },
93
176
  cache: this.node.memory &&
94
177
  (await this.node.memory.sublevel(sha256Base64Sync(this.log.id))),
95
178
  });
96
- try {
97
- if (this._role instanceof Replicator) {
98
- this.modifySortedSubscriptionCache(true, getPublicKeyFromPeerId(this.node.peerId).hashcode());
99
- await this.log.load();
100
- }
101
- else {
102
- await this.log.load({ heads: true, reload: true });
103
- }
104
- }
105
- catch (error) {
106
- if (error instanceof AccessError) {
107
- logger.error("Failed to load all entries due to access error, make sure you are opening the program with approate keychain configuration");
108
- }
109
- else {
110
- throw error;
111
- }
112
- }
179
+ await this.initializeWithRole();
113
180
  // Take into account existing subscription
114
181
  (await this.node.services.pubsub.getSubscribers(this.topic))?.forEach((v, k) => {
115
- this.handleSubscriptionChange(k, [{ topic: this.topic, data: v.data }], true);
182
+ this.handleSubscriptionChange(v.publicKey, [{ topic: this.topic, data: v.data }], true);
116
183
  });
117
184
  // Open for communcation
118
185
  await this.rpc.open({
@@ -127,8 +194,19 @@ export let SharedLog = class SharedLog extends Program {
127
194
  return this.log.idString;
128
195
  }
129
196
  async _close() {
197
+ for (const [k, v] of this._pendingDeletes) {
198
+ v.clear();
199
+ v.promise.resolve(); // TODO or reject?
200
+ }
201
+ for (const [k, v] of this._pendingIHave) {
202
+ v.clear();
203
+ }
204
+ this._gidParentCache.clear();
205
+ this._pendingDeletes = new Map();
206
+ this._pendingIHave = new Map();
130
207
  this._gidPeersHistory = new Map();
131
208
  this._sortedPeersCache = undefined;
209
+ this._loadedOnce = false;
132
210
  this.node.services.pubsub.removeEventListener("subscribe", this._onSubscriptionFn);
133
211
  this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
134
212
  this.node.services.pubsub.removeEventListener("unsubscribe", this._onUnsubscriptionFn);
@@ -156,7 +234,7 @@ export let SharedLog = class SharedLog extends Program {
156
234
  try {
157
235
  if (msg instanceof ExchangeHeadsMessage) {
158
236
  /**
159
- * I have recieved heads from someone else.
237
+ * I have received heads from someone else.
160
238
  * I can use them to load associated logs and join/sync them with the data stores I own
161
239
  */
162
240
  const { heads } = msg;
@@ -174,26 +252,109 @@ export let SharedLog = class SharedLog extends Program {
174
252
  filteredHeads.push(head);
175
253
  }
176
254
  }
177
- let toMerge;
178
255
  if (!this._sync) {
179
- toMerge = [];
180
- for (const [gid, value] of await groupByGid(filteredHeads)) {
181
- if (!(await this.isLeader(gid, this._minReplicas.value))) {
182
- logger.debug(`${this.node.identity.publicKey.hashcode()}: Dropping heads with gid: ${gid}. Because not leader`);
183
- continue;
256
+ const toMerge = [];
257
+ let toDelete = undefined;
258
+ let maybeDelete = undefined;
259
+ const groupedByGid = await groupByGid(filteredHeads);
260
+ for (const [gid, entries] of groupedByGid) {
261
+ const headsWithGid = this.log.headsIndex.gids.get(gid);
262
+ const maxReplicasFromHead = headsWithGid && headsWithGid.size > 0
263
+ ? maxReplicas(this, [...headsWithGid.values()])
264
+ : this.replicas.min.getValue(this);
265
+ const maxReplicasFromNewEntries = maxReplicas(this, [
266
+ ...entries.map((x) => x.entry),
267
+ ]);
268
+ const isLeader = await this.isLeader(gid, Math.max(maxReplicasFromHead, maxReplicasFromNewEntries));
269
+ if (maxReplicasFromNewEntries < maxReplicasFromHead && isLeader) {
270
+ (maybeDelete || (maybeDelete = [])).push(entries);
271
+ }
272
+ outer: for (const entry of entries) {
273
+ if (isLeader) {
274
+ toMerge.push(entry);
275
+ }
276
+ else {
277
+ for (const ref of entry.references) {
278
+ const map = this.log.headsIndex.gids.get(await ref.getGid());
279
+ if (map && map.size > 0) {
280
+ toMerge.push(entry);
281
+ (toDelete || (toDelete = [])).push(entry.entry);
282
+ continue outer;
283
+ }
284
+ }
285
+ }
286
+ logger.debug(`${this.node.identity.publicKey.hashcode()}: Dropping heads with gid: ${entry.entry.gid}. Because not leader`);
184
287
  }
185
- for (const head of value) {
186
- toMerge.push(head);
288
+ }
289
+ if (toMerge.length > 0) {
290
+ await this.log.join(toMerge);
291
+ toDelete &&
292
+ Promise.all(this.pruneSafely(toDelete)).catch((e) => {
293
+ logger.error(e.toString());
294
+ });
295
+ }
296
+ if (maybeDelete) {
297
+ for (const entries of maybeDelete) {
298
+ const headsWithGid = this.log.headsIndex.gids.get(entries[0].entry.meta.gid);
299
+ if (headsWithGid && headsWithGid.size > 0) {
300
+ const minReplicas = maxReplicas(this, [
301
+ ...headsWithGid.values(),
302
+ ]);
303
+ const isLeader = await this.isLeader(entries[0].entry.meta.gid, minReplicas);
304
+ if (!isLeader) {
305
+ Promise.all(this.pruneSafely(entries.map((x) => x.entry))).catch((e) => {
306
+ logger.error(e.toString());
307
+ });
308
+ }
309
+ }
187
310
  }
188
311
  }
189
312
  }
190
313
  else {
191
- toMerge = await Promise.all(filteredHeads.map((x) => this._sync(x.entry))).then((filter) => filteredHeads.filter((v, ix) => filter[ix]));
314
+ await this.log.join(await Promise.all(filteredHeads.map((x) => this._sync(x.entry))).then((filter) => filteredHeads.filter((v, ix) => filter[ix])));
192
315
  }
193
- if (toMerge.length > 0) {
194
- await this.log.join(toMerge);
316
+ }
317
+ }
318
+ else if (msg instanceof RequestIHave) {
319
+ const hasAndIsLeader = [];
320
+ for (const hash of msg.hashes) {
321
+ const indexedEntry = this.log.entryIndex.getShallow(hash);
322
+ if (indexedEntry &&
323
+ (await this.isLeader(indexedEntry.meta.gid, decodeReplicas(indexedEntry).getValue(this)))) {
324
+ hasAndIsLeader.push(hash);
325
+ }
326
+ else {
327
+ const prevPendingIHave = this._pendingIHave.get(hash);
328
+ const pendingIHave = {
329
+ clear: () => {
330
+ clearTimeout(timeout);
331
+ prevPendingIHave?.clear();
332
+ },
333
+ callback: () => {
334
+ prevPendingIHave && prevPendingIHave.callback();
335
+ this.rpc.send(new ResponseIHave({ hashes: [hash] }), {
336
+ to: [context.from],
337
+ });
338
+ this._pendingIHave.delete(hash);
339
+ },
340
+ };
341
+ const timeout = setTimeout(() => {
342
+ const pendingIHaveRef = this._pendingIHave.get(hash);
343
+ if (pendingIHave === pendingIHaveRef) {
344
+ this._pendingIHave.delete(hash);
345
+ }
346
+ }, this._respondToIHaveTimeout);
347
+ this._pendingIHave.set(hash, pendingIHave);
195
348
  }
196
349
  }
350
+ this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
351
+ to: [context.from],
352
+ });
353
+ }
354
+ else if (msg instanceof ResponseIHave) {
355
+ for (const hash of msg.hashes) {
356
+ this._pendingDeletes.get(hash)?.callback(context.from.hashcode());
357
+ }
197
358
  }
198
359
  else {
199
360
  throw new Error("Unexpected message");
@@ -214,11 +375,14 @@ export let SharedLog = class SharedLog extends Program {
214
375
  getReplicatorsSorted() {
215
376
  return this._sortedPeersCache;
216
377
  }
217
- async isLeader(slot, numberOfLeaders = this.minReplicas.value) {
378
+ async isLeader(slot, numberOfLeaders) {
218
379
  const isLeader = (await this.findLeaders(slot, numberOfLeaders)).find((l) => l === this.node.identity.publicKey.hashcode());
219
380
  return !!isLeader;
220
381
  }
221
- async findLeaders(subject, numberOfLeaders = this.minReplicas.value) {
382
+ async findLeaders(subject, numberOfLeadersUnbounded) {
383
+ const lower = this.replicas.min.getValue(this);
384
+ const higher = this.replicas.max?.getValue(this) ?? Number.MAX_SAFE_INTEGER;
385
+ let numberOfLeaders = Math.max(Math.min(higher, numberOfLeadersUnbounded), lower);
222
386
  // For a fixed set or members, the choosen leaders will always be the same (address invariant)
223
387
  // This allows for that same content is always chosen to be distributed to same peers, to remove unecessary copies
224
388
  const peers = this.getReplicatorsSorted() || [];
@@ -243,48 +407,69 @@ export let SharedLog = class SharedLog extends Program {
243
407
  }
244
408
  return leaders;
245
409
  }
246
- modifySortedSubscriptionCache(subscribed, fromHash) {
410
+ async modifySortedSubscriptionCache(subscribed, publicKey) {
411
+ if (subscribed &&
412
+ this._canReplicate &&
413
+ !(await this._canReplicate(publicKey))) {
414
+ return false;
415
+ }
247
416
  const sortedPeer = this._sortedPeersCache;
248
417
  if (!sortedPeer) {
249
418
  if (this.closed === false) {
250
419
  throw new Error("Unexpected, sortedPeersCache is undefined");
251
420
  }
252
- return;
421
+ return false;
253
422
  }
254
- const code = fromHash;
423
+ const code = publicKey.hashcode();
255
424
  if (subscribed) {
256
425
  // TODO use Set + list for fast lookup
257
426
  if (!sortedPeer.find((x) => x.hash === code)) {
258
427
  sortedPeer.push({ hash: code, timestamp: +new Date() });
259
428
  sortedPeer.sort((a, b) => a.hash.localeCompare(b.hash));
429
+ return true;
430
+ }
431
+ else {
432
+ return false;
260
433
  }
261
434
  }
262
435
  else {
263
436
  const deleteIndex = sortedPeer.findIndex((x) => x.hash === code);
264
- sortedPeer.splice(deleteIndex, 1);
437
+ if (deleteIndex >= 0) {
438
+ sortedPeer.splice(deleteIndex, 1);
439
+ return true;
440
+ }
441
+ else {
442
+ return false;
443
+ }
265
444
  }
266
445
  }
267
- async handleSubscriptionChange(fromHash, changes, subscribed) {
446
+ async handleSubscriptionChange(publicKey, changes, subscribed) {
268
447
  // TODO why are we doing two loops?
448
+ const prev = [];
269
449
  for (const subscription of changes) {
270
450
  if (this.log.idString !== subscription.topic) {
271
451
  continue;
272
452
  }
273
453
  if (!subscription.data ||
274
454
  !startsWith(subscription.data, REPLICATOR_TYPE_VARIANT)) {
455
+ prev.push(await this.modifySortedSubscriptionCache(false, publicKey));
275
456
  continue;
276
457
  }
277
- this._lastSubscriptionMessageId += 1;
278
- this.modifySortedSubscriptionCache(subscribed, fromHash);
458
+ else {
459
+ this._lastSubscriptionMessageId += 1;
460
+ prev.push(await this.modifySortedSubscriptionCache(subscribed, publicKey));
461
+ }
279
462
  }
280
- for (const subscription of changes) {
463
+ // TODO don't do this i fnot is replicator?
464
+ for (const [i, subscription] of changes.entries()) {
281
465
  if (this.log.idString !== subscription.topic) {
282
466
  continue;
283
467
  }
284
468
  if (subscription.data) {
285
469
  try {
286
470
  const type = deserialize(subscription.data, Role);
287
- if (type instanceof Replicator) {
471
+ // Reorganize if the new subscriber is a replicator, or observers AND was replicator
472
+ if (type instanceof Replicator || prev[i]) {
288
473
  await this.replicationReorganization();
289
474
  }
290
475
  }
@@ -297,6 +482,72 @@ export let SharedLog = class SharedLog extends Program {
297
482
  }
298
483
  }
299
484
  }
485
+ pruneSafely(entries, options) {
486
+ // ask network if they have they entry,
487
+ // so I can delete it
488
+ // There is a few reasons why we might end up here
489
+ // - Two logs merge, and we should not anymore keep the joined log replicated (because we are not responsible for the resulting gid)
490
+ // - 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
491
+ // - Peers join and leave, which means we might not be a replicator anymore
492
+ const promises = [];
493
+ const filteredEntries = [];
494
+ for (const entry of entries) {
495
+ const pendingPrev = this._pendingDeletes.get(entry.hash);
496
+ filteredEntries.push(entry);
497
+ const existCounter = new Set();
498
+ const minReplicas = decodeReplicas(entry);
499
+ const deferredPromise = pDefer();
500
+ const clear = () => {
501
+ pendingPrev?.clear();
502
+ const pending = this._pendingDeletes.get(entry.hash);
503
+ if (pending?.promise == deferredPromise) {
504
+ this._pendingDeletes.delete(entry.hash);
505
+ }
506
+ clearTimeout(timeout);
507
+ };
508
+ const resolve = () => {
509
+ clear();
510
+ deferredPromise.resolve();
511
+ };
512
+ const reject = (e) => {
513
+ clear();
514
+ deferredPromise.reject(e);
515
+ };
516
+ const timeout = setTimeout(() => {
517
+ reject(new Error("Timeout"));
518
+ }, options?.timeout ?? 10 * 1000);
519
+ this._pendingDeletes.set(entry.hash, {
520
+ promise: deferredPromise,
521
+ clear: () => {
522
+ clear();
523
+ },
524
+ callback: async (publicKeyHash) => {
525
+ const minReplicasValue = minReplicas.getValue(this);
526
+ const l = await this.findLeaders(entry.gid, minReplicasValue);
527
+ if (l.find((x) => x === publicKeyHash)) {
528
+ existCounter.add(publicKeyHash);
529
+ if (minReplicas.getValue(this) <= existCounter.size) {
530
+ this.log
531
+ .remove(entry, {
532
+ recursively: true,
533
+ })
534
+ .then(() => {
535
+ resolve();
536
+ })
537
+ .catch((e) => {
538
+ reject(new Error("Failed to delete entry: " + e.toString()));
539
+ });
540
+ }
541
+ }
542
+ },
543
+ });
544
+ promises.push(deferredPromise.promise);
545
+ }
546
+ if (filteredEntries.length > 0) {
547
+ this.rpc.send(new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) }));
548
+ }
549
+ return promises;
550
+ }
300
551
  /**
301
552
  * 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
302
553
  * 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
@@ -314,12 +565,13 @@ export let SharedLog = class SharedLog extends Program {
314
565
  continue; // TODO maybe close store?
315
566
  }
316
567
  const oldPeersSet = this._gidPeersHistory.get(gid);
317
- const currentPeers = await this.findLeaders(gid);
568
+ const currentPeers = await this.findLeaders(gid, maxReplicas(this, entries) // pick max replication policy of all entries, so all information is treated equally important as the most important
569
+ );
318
570
  for (const currentPeer of currentPeers) {
319
571
  if (!oldPeersSet?.has(currentPeer) &&
320
572
  currentPeer !== this.node.identity.publicKey.hashcode()) {
321
573
  storeChanged = true;
322
- // second condition means that if the new peer is us, we should not do anything, since we are expecting to recieve heads, not send
574
+ // second condition means that if the new peer is us, we should not do anything, since we are expecting to receive heads, not send
323
575
  newPeers.push(currentPeer);
324
576
  // send heads to the new peer
325
577
  // console.log('new gid for peer', newPeers.length, this.id.toString(), newPeer, gid, entries.length, newPeers)
@@ -348,8 +600,8 @@ export let SharedLog = class SharedLog extends Program {
348
600
  // delete entries since we are not suppose to replicate this anymore
349
601
  // TODO add delay? freeze time? (to ensure resiliance for bad io)
350
602
  if (entriesToDelete.length > 0) {
351
- await this.log.remove(entriesToDelete, {
352
- recursively: true,
603
+ Promise.all(this.pruneSafely(entriesToDelete)).catch((e) => {
604
+ logger.error(e.toString());
353
605
  });
354
606
  }
355
607
  // TODO if length === 0 maybe close store?
@@ -359,8 +611,8 @@ export let SharedLog = class SharedLog extends Program {
359
611
  continue;
360
612
  }
361
613
  const message = await createExchangeHeadsMessage(this.log, [...toSend.values()], // TODO send to peers directly
362
- true);
363
- // TODO perhaps send less messages to more recievers for performance reasons?
614
+ this._gidParentCache);
615
+ // TODO perhaps send less messages to more receivers for performance reasons?
364
616
  await this.rpc.send(message, {
365
617
  to: newPeers,
366
618
  strict: true,
@@ -371,9 +623,13 @@ export let SharedLog = class SharedLog extends Program {
371
623
  }
372
624
  return storeChanged || changed;
373
625
  }
374
- replicators() {
626
+ /**
627
+ *
628
+ * @returns groups where at least one in any group will have the entry you are looking for
629
+ */
630
+ getDiscoveryGroups() {
375
631
  // TODO Optimize this so we don't have to recreate the array all the time!
376
- const minReplicas = this.minReplicas.value;
632
+ const minReplicas = this.replicas.min.getValue(this);
377
633
  const replicators = this.getReplicatorsSorted();
378
634
  if (!replicators) {
379
635
  return []; // No subscribers and we are not replicating
@@ -388,16 +644,16 @@ export let SharedLog = class SharedLog extends Program {
388
644
  }
389
645
  return groups;
390
646
  }
391
- async replicator(gid) {
392
- return this.isLeader(gid);
647
+ async replicator(entry) {
648
+ return this.isLeader(entry.gid, decodeReplicas(entry).getValue(this));
393
649
  }
394
650
  async _onUnsubscription(evt) {
395
651
  logger.debug(`Peer disconnected '${evt.detail.from.hashcode()}' from '${JSON.stringify(evt.detail.unsubscriptions.map((x) => x.topic))}'`);
396
- return this.handleSubscriptionChange(evt.detail.from.hashcode(), evt.detail.unsubscriptions, false);
652
+ return this.handleSubscriptionChange(evt.detail.from, evt.detail.unsubscriptions, false);
397
653
  }
398
654
  async _onSubscription(evt) {
399
655
  logger.debug(`New peer '${evt.detail.from.hashcode()}' connected to '${JSON.stringify(evt.detail.subscriptions.map((x) => x.topic))}'`);
400
- return this.handleSubscriptionChange(evt.detail.from.hashcode(), evt.detail.subscriptions, true);
656
+ return this.handleSubscriptionChange(evt.detail.from, evt.detail.subscriptions, true);
401
657
  }
402
658
  };
403
659
  __decorate([