@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/lib/esm/index.js CHANGED
@@ -11,19 +11,25 @@ import { RPC } from "@peerbit/rpc";
11
11
  import { TransportMessage } from "./message.js";
12
12
  import { Entry, Log } from "@peerbit/log";
13
13
  import { Program } from "@peerbit/program";
14
- import { BinaryReader, BinaryWriter, BorshError, deserialize, field, serialize, variant } from "@dao-xyz/borsh";
15
- import { AccessError, getPublicKeyFromPeerId, sha256, sha256Base64Sync } from "@peerbit/crypto";
14
+ import { BinaryWriter, BorshError, field, variant } from "@dao-xyz/borsh";
15
+ import { AccessError, sha256, sha256Base64Sync } from "@peerbit/crypto";
16
16
  import { logger as loggerFn } from "@peerbit/logger";
17
17
  import { ExchangeHeadsMessage, RequestIHave, ResponseIHave, createExchangeHeadsMessage } from "./exchange-heads.js";
18
- import { startsWith } from "@peerbit/uint8arrays";
19
- import { TimeoutError } from "@peerbit/time";
20
- import { REPLICATOR_TYPE_VARIANT, Observer, Replicator, Role } from "./role.js";
21
- import { AbsoluteReplicas, ReplicationError, decodeReplicas, encodeReplicas, maxReplicas } from "./replication.js";
18
+ import { AbortError, waitFor } from "@peerbit/time";
19
+ import { Observer, Replicator, Role } from "./role.js";
20
+ import { AbsoluteReplicas, ReplicationError, RequestRoleMessage, ResponseRoleMessage, decodeReplicas, encodeReplicas, hashToUniformNumber, maxReplicas } from "./replication.js";
22
21
  import pDefer from "p-defer";
23
22
  import { Cache } from "@peerbit/cache";
23
+ import { CustomEvent } from "@libp2p/interface";
24
+ import yallist from "yallist";
25
+ import { AcknowledgeDelivery, SilentDelivery } from "@peerbit/stream-interface";
26
+ import { AnyBlockStore, RemoteBlocks } from "@peerbit/blocks";
27
+ import { BlocksMessage } from "./blocks.js";
28
+ import debounce from "p-debounce";
29
+ import { PIDReplicationController } from "./pid.js";
24
30
  export * from "./replication.js";
25
31
  export { Observer, Replicator, Role };
26
- export const logger = loggerFn({ module: "peer" });
32
+ export const logger = loggerFn({ module: "shared-log" });
27
33
  const groupByGid = async (entries) => {
28
34
  const groupByGid = new Map();
29
35
  for (const head of entries) {
@@ -39,25 +45,41 @@ const groupByGid = async (entries) => {
39
45
  }
40
46
  return groupByGid;
41
47
  };
48
+ const isAdaptiveReplicatorOption = (options) => {
49
+ if (options.limits ||
50
+ options.error ||
51
+ options.factor == null) {
52
+ return true;
53
+ }
54
+ return false;
55
+ };
42
56
  export const DEFAULT_MIN_REPLICAS = 2;
57
+ export const WAIT_FOR_REPLICATOR_TIMEOUT = 9000;
58
+ export const WAIT_FOR_ROLE_MATURITY = 5000;
59
+ const REBALANCE_DEBOUNCE_INTERAVAL = 50;
43
60
  let SharedLog = class SharedLog extends Program {
44
61
  log;
45
62
  rpc;
46
63
  // options
47
- _sync;
48
64
  _role;
65
+ _roleOptions;
49
66
  _sortedPeersCache;
50
- _lastSubscriptionMessageId;
67
+ _totalParticipation;
51
68
  _gidPeersHistory;
52
69
  _onSubscriptionFn;
53
70
  _onUnsubscriptionFn;
54
71
  _canReplicate;
55
72
  _logProperties;
73
+ _closeController;
56
74
  _loadedOnce = false;
57
75
  _gidParentCache;
58
76
  _respondToIHaveTimeout;
59
77
  _pendingDeletes;
60
78
  _pendingIHave;
79
+ latestRoleMessages;
80
+ remoteBlocks;
81
+ openTime;
82
+ rebalanceParticipationDebounced;
61
83
  replicas;
62
84
  constructor(properties) {
63
85
  super();
@@ -67,31 +89,78 @@ let SharedLog = class SharedLog extends Program {
67
89
  get role() {
68
90
  return this._role;
69
91
  }
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
- }
92
+ get totalParticipation() {
93
+ return this._totalParticipation;
78
94
  }
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
- }
95
+ setupRole(options) {
96
+ this.rebalanceParticipationDebounced = undefined;
97
+ const setupDebouncedRebalancing = (options) => {
98
+ this.replicationController = new PIDReplicationController({
99
+ targetMemoryLimit: options?.limits?.memory,
100
+ errorFunction: options?.error
101
+ });
102
+ this.rebalanceParticipationDebounced = debounce(() => this.rebalanceParticipation(), REBALANCE_DEBOUNCE_INTERAVAL // TODO make dynamic
103
+ );
104
+ };
105
+ if (options instanceof Observer || options instanceof Replicator) {
106
+ throw new Error("Unsupported role option type");
86
107
  }
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");
108
+ else if (options === "observer") {
109
+ this._roleOptions = new Observer();
110
+ }
111
+ else if (options === "replicator") {
112
+ setupDebouncedRebalancing();
113
+ this._roleOptions = { type: options };
114
+ }
115
+ else if (options) {
116
+ if (options.type === "replicator") {
117
+ if (isAdaptiveReplicatorOption(options)) {
118
+ setupDebouncedRebalancing(options);
119
+ this._roleOptions = options;
120
+ }
121
+ else {
122
+ this._roleOptions = new Replicator({ factor: options.factor });
123
+ }
90
124
  }
91
125
  else {
92
- throw error;
126
+ this._roleOptions = new Observer();
93
127
  }
94
128
  }
129
+ else {
130
+ // Default option
131
+ setupDebouncedRebalancing();
132
+ this._roleOptions = { type: "replicator" };
133
+ }
134
+ // setup the initial role
135
+ if (this._roleOptions instanceof Replicator ||
136
+ this._roleOptions instanceof Observer) {
137
+ this._role = this._roleOptions; // Fixed
138
+ }
139
+ else {
140
+ this._role = new Replicator({
141
+ // initial role in a dynamic setup
142
+ factor: 1,
143
+ timestamp: BigInt(+new Date())
144
+ });
145
+ }
146
+ return this._role;
147
+ }
148
+ async updateRole(role, onRoleChange = true) {
149
+ return this._updateRole(this.setupRole(role), onRoleChange);
150
+ }
151
+ async _updateRole(role = this._role, onRoleChange = true) {
152
+ this._role = role;
153
+ const { changed } = await this._modifyReplicators(this.role, this.node.identity.publicKey);
154
+ if (!this._loadedOnce) {
155
+ await this.log.load();
156
+ this._loadedOnce = true;
157
+ }
158
+ await this.rpc.subscribe();
159
+ await this.rpc.send(new ResponseRoleMessage(role));
160
+ if (onRoleChange && changed) {
161
+ this.onRoleChange(undefined, this._role, this.node.identity.publicKey);
162
+ }
163
+ return changed;
95
164
  }
96
165
  async append(data, options) {
97
166
  const appendOptions = { ...options };
@@ -109,7 +178,14 @@ let SharedLog = class SharedLog extends Program {
109
178
  appendOptions.meta.data = minReplicasData;
110
179
  }
111
180
  const result = await this.log.append(data, appendOptions);
112
- await this.rpc.send(await createExchangeHeadsMessage(this.log, [result.entry], this._gidParentCache));
181
+ const leaders = await this.findLeaders(result.entry.meta.gid, decodeReplicas(result.entry).getValue(this));
182
+ const isLeader = leaders.includes(this.node.identity.publicKey.hashcode());
183
+ await this.rpc.send(await createExchangeHeadsMessage(this.log, [result.entry], this._gidParentCache), {
184
+ mode: isLeader
185
+ ? new SilentDelivery({ redundancy: 1, to: leaders })
186
+ : new AcknowledgeDelivery({ redundancy: 1, to: leaders })
187
+ });
188
+ this.rebalanceParticipationDebounced?.();
113
189
  return result;
114
190
  }
115
191
  async open(options) {
@@ -128,20 +204,34 @@ let SharedLog = class SharedLog extends Program {
128
204
  this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 10 * 1000; // TODO make into arg
129
205
  this._pendingDeletes = new Map();
130
206
  this._pendingIHave = new Map();
207
+ this.latestRoleMessages = new Map();
208
+ this.openTime = +new Date();
131
209
  this._gidParentCache = new Cache({ max: 1000 });
210
+ this._closeController = new AbortController();
132
211
  this._canReplicate = options?.canReplicate;
133
- this._sync = options?.sync;
134
212
  this._logProperties = options;
135
- this._role = options?.role || new Replicator();
136
- this._lastSubscriptionMessageId = 0;
213
+ this.setupRole(options?.role);
137
214
  this._onSubscriptionFn = this._onSubscription.bind(this);
138
- this._sortedPeersCache = [];
215
+ this._totalParticipation = 0;
216
+ this._sortedPeersCache = yallist.create();
139
217
  this._gidPeersHistory = new Map();
140
218
  await this.node.services.pubsub.addEventListener("subscribe", this._onSubscriptionFn);
141
219
  this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
142
220
  await this.node.services.pubsub.addEventListener("unsubscribe", this._onUnsubscriptionFn);
143
- await this.log.open(this.node.services.blocks, this.node.identity, {
144
- keychain: this.node.keychain,
221
+ const id = sha256Base64Sync(this.log.id);
222
+ const storage = await this.node.memory.sublevel(id);
223
+ const localBlocks = await new AnyBlockStore(await storage.sublevel("blocks"));
224
+ const cache = await storage.sublevel("cache");
225
+ this.remoteBlocks = new RemoteBlocks({
226
+ local: localBlocks,
227
+ publish: (message, options) => this.rpc.send(new BlocksMessage(message), {
228
+ to: options?.to
229
+ }),
230
+ waitFor: this.rpc.waitFor.bind(this.rpc)
231
+ });
232
+ await this.remoteBlocks.start();
233
+ await this.log.open(this.remoteBlocks, this.node.identity, {
234
+ keychain: this.node.services.keychain,
145
235
  ...this._logProperties,
146
236
  onChange: (change) => {
147
237
  if (this._pendingIHave.size > 0) {
@@ -149,7 +239,7 @@ let SharedLog = class SharedLog extends Program {
149
239
  const ih = this._pendingIHave.get(added.hash);
150
240
  if (ih) {
151
241
  ih.clear();
152
- ih.callback();
242
+ ih.callback(added);
153
243
  }
154
244
  }
155
245
  }
@@ -181,33 +271,38 @@ let SharedLog = class SharedLog extends Program {
181
271
  return this._logProperties?.canAppend?.(entry) ?? true;
182
272
  },
183
273
  trim: this._logProperties?.trim && {
184
- ...this._logProperties?.trim,
185
- filter: {
186
- canTrim: async (entry) => !(await this.isLeader(entry.meta.gid, decodeReplicas(entry).getValue(this))),
187
- cacheId: () => this._lastSubscriptionMessageId
188
- }
274
+ ...this._logProperties?.trim
189
275
  },
190
- cache: this.node.memory &&
191
- (await this.node.memory.sublevel(sha256Base64Sync(this.log.id)))
192
- });
193
- await this.initializeWithRole();
194
- // Take into account existing subscription
195
- (await this.node.services.pubsub.getSubscribers(this.topic))?.forEach((v, k) => {
196
- this.handleSubscriptionChange(v.publicKey, [{ topic: this.topic, data: v.data }], true);
276
+ cache: cache
197
277
  });
198
278
  // Open for communcation
199
279
  await this.rpc.open({
200
280
  queryType: TransportMessage,
201
281
  responseType: TransportMessage,
202
282
  responseHandler: this._onMessage.bind(this),
203
- topic: this.topic,
204
- subscriptionData: serialize(this.role)
283
+ topic: this.topic
205
284
  });
285
+ await this._updateRole();
286
+ await this.rebalanceParticipation();
287
+ // Take into account existing subscription
288
+ (await this.node.services.pubsub.getSubscribers(this.topic))?.forEach((v, k) => {
289
+ if (v.equals(this.node.identity.publicKey)) {
290
+ return;
291
+ }
292
+ this.handleSubscriptionChange(v, [this.topic], true);
293
+ });
294
+ }
295
+ async getMemoryUsage() {
296
+ return (((await this.log.memory?.size()) || 0) + (await this.log.blocks.size()));
206
297
  }
207
298
  get topic() {
208
299
  return this.log.idString;
209
300
  }
210
301
  async _close() {
302
+ this._closeController.abort();
303
+ this.node.services.pubsub.removeEventListener("subscribe", this._onSubscriptionFn);
304
+ this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
305
+ this.node.services.pubsub.removeEventListener("unsubscribe", this._onUnsubscriptionFn);
211
306
  for (const [k, v] of this._pendingDeletes) {
212
307
  v.clear();
213
308
  v.promise.resolve(); // TODO or reject?
@@ -215,15 +310,14 @@ let SharedLog = class SharedLog extends Program {
215
310
  for (const [k, v] of this._pendingIHave) {
216
311
  v.clear();
217
312
  }
313
+ await this.remoteBlocks.stop();
218
314
  this._gidParentCache.clear();
219
315
  this._pendingDeletes = new Map();
220
316
  this._pendingIHave = new Map();
317
+ this.latestRoleMessages.clear();
221
318
  this._gidPeersHistory = new Map();
222
319
  this._sortedPeersCache = undefined;
223
320
  this._loadedOnce = false;
224
- this.node.services.pubsub.removeEventListener("subscribe", this._onSubscriptionFn);
225
- this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
226
- this.node.services.pubsub.removeEventListener("unsubscribe", this._onUnsubscriptionFn);
227
321
  }
228
322
  async close(from) {
229
323
  const superClosed = await super.close(from);
@@ -239,8 +333,8 @@ let SharedLog = class SharedLog extends Program {
239
333
  if (!superDropped) {
240
334
  return superDropped;
241
335
  }
242
- await this._close();
243
336
  await this.log.drop();
337
+ await this._close();
244
338
  return true;
245
339
  }
246
340
  async recover() {
@@ -255,7 +349,6 @@ let SharedLog = class SharedLog extends Program {
255
349
  * I can use them to load associated logs and join/sync them with the data stores I own
256
350
  */
257
351
  const { heads } = msg;
258
- // replication topic === trustedNetwork address
259
352
  logger.debug(`${this.node.identity.publicKey.hashcode()}: Recieved heads: ${heads.length === 1 ? heads[0].entry.hash : "#" + heads.length}, logId: ${this.log.idString}`);
260
353
  if (heads) {
261
354
  const filteredHeads = [];
@@ -269,20 +362,22 @@ let SharedLog = class SharedLog extends Program {
269
362
  filteredHeads.push(head);
270
363
  }
271
364
  }
272
- if (!this._sync) {
273
- const toMerge = [];
274
- let toDelete = undefined;
275
- let maybeDelete = undefined;
276
- const groupedByGid = await groupByGid(filteredHeads);
277
- for (const [gid, entries] of groupedByGid) {
365
+ if (filteredHeads.length === 0) {
366
+ return;
367
+ }
368
+ const toMerge = [];
369
+ let toDelete = undefined;
370
+ let maybeDelete = undefined;
371
+ const groupedByGid = await groupByGid(filteredHeads);
372
+ const promises = [];
373
+ for (const [gid, entries] of groupedByGid) {
374
+ const fn = async () => {
278
375
  const headsWithGid = this.log.headsIndex.gids.get(gid);
279
376
  const maxReplicasFromHead = headsWithGid && headsWithGid.size > 0
280
377
  ? maxReplicas(this, [...headsWithGid.values()])
281
378
  : this.replicas.min.getValue(this);
282
- const maxReplicasFromNewEntries = maxReplicas(this, [
283
- ...entries.map((x) => x.entry)
284
- ]);
285
- const isLeader = await this.isLeader(gid, Math.max(maxReplicasFromHead, maxReplicasFromNewEntries));
379
+ const maxReplicasFromNewEntries = maxReplicas(this, entries.map((x) => x.entry));
380
+ const isLeader = await this.waitForIsLeader(gid, Math.max(maxReplicasFromHead, maxReplicasFromNewEntries));
286
381
  if (maxReplicasFromNewEntries < maxReplicasFromHead && isLeader) {
287
382
  (maybeDelete || (maybeDelete = [])).push(entries);
288
383
  }
@@ -302,34 +397,35 @@ let SharedLog = class SharedLog extends Program {
302
397
  }
303
398
  logger.debug(`${this.node.identity.publicKey.hashcode()}: Dropping heads with gid: ${entry.entry.gid}. Because not leader`);
304
399
  }
305
- }
306
- if (toMerge.length > 0) {
307
- await this.log.join(toMerge);
308
- toDelete &&
309
- Promise.all(this.pruneSafely(toDelete)).catch((e) => {
310
- logger.error(e.toString());
311
- });
312
- }
313
- if (maybeDelete) {
314
- for (const entries of maybeDelete) {
315
- const headsWithGid = this.log.headsIndex.gids.get(entries[0].entry.meta.gid);
316
- if (headsWithGid && headsWithGid.size > 0) {
317
- const minReplicas = maxReplicas(this, [
318
- ...headsWithGid.values()
319
- ]);
320
- const isLeader = await this.isLeader(entries[0].entry.meta.gid, minReplicas);
321
- if (!isLeader) {
322
- Promise.all(this.pruneSafely(entries.map((x) => x.entry))).catch((e) => {
323
- logger.error(e.toString());
324
- });
325
- }
400
+ };
401
+ promises.push(fn());
402
+ }
403
+ await Promise.all(promises);
404
+ if (this.closed) {
405
+ return;
406
+ }
407
+ if (toMerge.length > 0) {
408
+ await this.log.join(toMerge);
409
+ toDelete &&
410
+ this.prune(toDelete).catch((e) => {
411
+ logger.error(e.toString());
412
+ });
413
+ this.rebalanceParticipationDebounced?.();
414
+ }
415
+ if (maybeDelete) {
416
+ for (const entries of maybeDelete) {
417
+ const headsWithGid = this.log.headsIndex.gids.get(entries[0].entry.meta.gid);
418
+ if (headsWithGid && headsWithGid.size > 0) {
419
+ const minReplicas = maxReplicas(this, headsWithGid.values());
420
+ const isLeader = await this.isLeader(entries[0].entry.meta.gid, minReplicas);
421
+ if (!isLeader) {
422
+ this.prune(entries.map((x) => x.entry)).catch((e) => {
423
+ logger.error(e.toString());
424
+ });
326
425
  }
327
426
  }
328
427
  }
329
428
  }
330
- else {
331
- await this.log.join(await Promise.all(filteredHeads.map((x) => this._sync(x.entry))).then((filter) => filteredHeads.filter((v, ix) => filter[ix])));
332
- }
333
429
  }
334
430
  }
335
431
  else if (msg instanceof RequestIHave) {
@@ -347,12 +443,14 @@ let SharedLog = class SharedLog extends Program {
347
443
  clearTimeout(timeout);
348
444
  prevPendingIHave?.clear();
349
445
  },
350
- callback: () => {
351
- prevPendingIHave && prevPendingIHave.callback();
352
- this.rpc.send(new ResponseIHave({ hashes: [hash] }), {
353
- to: [context.from]
354
- });
355
- this._pendingIHave.delete(hash);
446
+ callback: async (entry) => {
447
+ if (await this.isLeader(entry.meta.gid, decodeReplicas(entry).getValue(this))) {
448
+ this.rpc.send(new ResponseIHave({ hashes: [entry.hash] }), {
449
+ to: [context.from]
450
+ });
451
+ }
452
+ prevPendingIHave && prevPendingIHave.callback(entry);
453
+ this._pendingIHave.delete(entry.hash);
356
454
  }
357
455
  };
358
456
  const timeout = setTimeout(() => {
@@ -364,7 +462,7 @@ let SharedLog = class SharedLog extends Program {
364
462
  this._pendingIHave.set(hash, pendingIHave);
365
463
  }
366
464
  }
367
- this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
465
+ await this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
368
466
  to: [context.from]
369
467
  });
370
468
  }
@@ -373,11 +471,55 @@ let SharedLog = class SharedLog extends Program {
373
471
  this._pendingDeletes.get(hash)?.callback(context.from.hashcode());
374
472
  }
375
473
  }
474
+ else if (msg instanceof BlocksMessage) {
475
+ await this.remoteBlocks.onMessage(msg.message);
476
+ }
477
+ else if (msg instanceof RequestRoleMessage) {
478
+ if (!context.from) {
479
+ throw new Error("Missing form in update role message");
480
+ }
481
+ if (context.from.equals(this.node.identity.publicKey)) {
482
+ return;
483
+ }
484
+ await this.rpc.send(new ResponseRoleMessage({ role: this.role }), {
485
+ to: [context.from]
486
+ });
487
+ }
488
+ else if (msg instanceof ResponseRoleMessage) {
489
+ if (!context.from) {
490
+ throw new Error("Missing form in update role message");
491
+ }
492
+ if (context.from.equals(this.node.identity.publicKey)) {
493
+ return;
494
+ }
495
+ this.waitFor(context.from, {
496
+ signal: this._closeController.signal,
497
+ timeout: WAIT_FOR_REPLICATOR_TIMEOUT
498
+ })
499
+ .then(async () => {
500
+ /* await delay(1000 * Math.random()) */
501
+ const prev = this.latestRoleMessages.get(context.from.hashcode());
502
+ if (prev && prev > context.timestamp) {
503
+ return;
504
+ }
505
+ this.latestRoleMessages.set(context.from.hashcode(), context.timestamp);
506
+ await this.modifyReplicators(msg.role, context.from);
507
+ })
508
+ .catch((e) => {
509
+ if (e instanceof AbortError) {
510
+ return;
511
+ }
512
+ logger.error("Failed to find peer who updated their role: " + e?.message);
513
+ });
514
+ }
376
515
  else {
377
516
  throw new Error("Unexpected message");
378
517
  }
379
518
  }
380
519
  catch (e) {
520
+ if (e instanceof AbortError) {
521
+ return;
522
+ }
381
523
  if (e instanceof BorshError) {
382
524
  logger.trace(`${this.node.identity.publicKey.hashcode()}: Failed to handle message on topic: ${JSON.stringify(this.log.idString)}: Got message for a different namespace`);
383
525
  return;
@@ -392,114 +534,327 @@ let SharedLog = class SharedLog extends Program {
392
534
  getReplicatorsSorted() {
393
535
  return this._sortedPeersCache;
394
536
  }
395
- async isLeader(slot, numberOfLeaders) {
396
- const isLeader = (await this.findLeaders(slot, numberOfLeaders)).find((l) => l === this.node.identity.publicKey.hashcode());
537
+ async waitForReplicator(...keys) {
538
+ const check = () => {
539
+ for (const k of keys) {
540
+ if (!this.getReplicatorsSorted()
541
+ ?.toArray()
542
+ ?.find((x) => x.publicKey.equals(k))) {
543
+ return false;
544
+ }
545
+ }
546
+ return true;
547
+ };
548
+ return waitFor(() => check(), { signal: this._closeController.signal });
549
+ }
550
+ async isLeader(slot, numberOfLeaders, options) {
551
+ const isLeader = (await this.findLeaders(slot, numberOfLeaders, options)).find((l) => l === this.node.identity.publicKey.hashcode());
397
552
  return !!isLeader;
398
553
  }
399
- async findLeaders(subject, numberOfLeadersUnbounded) {
400
- const lower = this.replicas.min.getValue(this);
401
- const higher = this.replicas.max?.getValue(this) ?? Number.MAX_SAFE_INTEGER;
402
- let numberOfLeaders = Math.max(Math.min(higher, numberOfLeadersUnbounded), lower);
554
+ async waitForIsLeader(slot, numberOfLeaders, timeout = WAIT_FOR_REPLICATOR_TIMEOUT) {
555
+ return new Promise((res, rej) => {
556
+ const removeListeners = () => {
557
+ this.events.removeEventListener("role", roleListener);
558
+ this._closeController.signal.addEventListener("abort", abortListener);
559
+ };
560
+ const abortListener = () => {
561
+ removeListeners();
562
+ clearTimeout(timer);
563
+ res(false);
564
+ };
565
+ const timer = setTimeout(() => {
566
+ removeListeners();
567
+ res(false);
568
+ }, timeout);
569
+ const check = () => this.isLeader(slot, numberOfLeaders).then((isLeader) => {
570
+ if (isLeader) {
571
+ removeListeners();
572
+ clearTimeout(timer);
573
+ res(isLeader);
574
+ }
575
+ });
576
+ const roleListener = () => {
577
+ check();
578
+ };
579
+ this.events.addEventListener("role", roleListener);
580
+ this._closeController.signal.addEventListener("abort", abortListener);
581
+ check();
582
+ });
583
+ }
584
+ async findLeaders(subject, numberOfLeaders, options) {
403
585
  // For a fixed set or members, the choosen leaders will always be the same (address invariant)
404
586
  // This allows for that same content is always chosen to be distributed to same peers, to remove unecessary copies
405
- const peers = this.getReplicatorsSorted() || [];
406
- if (peers.length === 0) {
407
- return [];
408
- }
409
- numberOfLeaders = Math.min(numberOfLeaders, peers.length);
410
587
  // Convert this thing we wan't to distribute to 8 bytes so we get can convert it into a u64
411
588
  // modulus into an index
412
589
  const utf8writer = new BinaryWriter();
413
590
  utf8writer.string(subject.toString());
414
591
  const seed = await sha256(utf8writer.finalize());
415
592
  // convert hash of slot to a number
416
- const seedNumber = new BinaryReader(seed.subarray(seed.length - 8, seed.length)).u64();
417
- const startIndex = Number(seedNumber % BigInt(peers.length));
418
- // we only step forward 1 step (ignoring that step backward 1 could be 'closer')
419
- // This does not matter, we only have to make sure all nodes running the code comes to somewhat the
420
- // same conclusion (are running the same leader selection algorithm)
421
- const leaders = new Array(numberOfLeaders);
593
+ const cursor = hashToUniformNumber(seed); // bounded between 0 and 1
594
+ return this.findLeadersFromUniformNumber(cursor, numberOfLeaders, options);
595
+ }
596
+ findLeadersFromUniformNumber(cursor, numberOfLeaders, options) {
597
+ const leaders = new Set();
598
+ const width = 1; // this.getParticipationSum(roleAge);
599
+ const peers = this.getReplicatorsSorted();
600
+ if (!peers || peers?.length === 0) {
601
+ return [];
602
+ }
603
+ numberOfLeaders = Math.min(numberOfLeaders, peers.length);
604
+ const t = +new Date();
605
+ const roleAge = options?.roleAge ??
606
+ Math.min(WAIT_FOR_ROLE_MATURITY, +new Date() - this.openTime);
422
607
  for (let i = 0; i < numberOfLeaders; i++) {
423
- leaders[i] = peers[(i + startIndex) % peers.length].hash;
608
+ let matured = 0;
609
+ const maybeIncrementMatured = (role) => {
610
+ if (t - Number(role.timestamp) > roleAge) {
611
+ matured++;
612
+ }
613
+ };
614
+ const x = ((cursor + i / numberOfLeaders) % 1) * width;
615
+ let currentNode = peers.head;
616
+ const diffs = [];
617
+ while (currentNode) {
618
+ const start = currentNode.value.offset % width;
619
+ const absDelta = Math.abs(start - x);
620
+ const diff = Math.min(absDelta, width - absDelta);
621
+ if (diff < currentNode.value.role.factor / 2 + 0.00001) {
622
+ leaders.add(currentNode.value.publicKey.hashcode());
623
+ maybeIncrementMatured(currentNode.value.role);
624
+ }
625
+ else {
626
+ diffs.push({
627
+ diff: currentNode.value.role.factor > 0
628
+ ? diff / currentNode.value.role.factor
629
+ : Number.MAX_SAFE_INTEGER,
630
+ rect: currentNode.value
631
+ });
632
+ }
633
+ currentNode = currentNode.next;
634
+ }
635
+ if (matured === 0) {
636
+ diffs.sort((x, y) => x.diff - y.diff);
637
+ for (const node of diffs) {
638
+ leaders.add(node.rect.publicKey.hashcode());
639
+ maybeIncrementMatured(node.rect.role);
640
+ if (matured > 0) {
641
+ break;
642
+ }
643
+ }
644
+ }
424
645
  }
425
- return leaders;
646
+ return [...leaders];
426
647
  }
427
- async modifySortedSubscriptionCache(subscribed, publicKey) {
428
- if (subscribed &&
648
+ /**
649
+ *
650
+ * @returns groups where at least one in any group will have the entry you are looking for
651
+ */
652
+ getReplicatorUnion(roleAge = WAIT_FOR_ROLE_MATURITY) {
653
+ // Total replication "width"
654
+ const width = 1; //this.getParticipationSum(roleAge);
655
+ // How much width you need to "query" to
656
+ const peers = this.getReplicatorsSorted(); // TODO types
657
+ const minReplicas = Math.min(peers.length, this.replicas.min.getValue(this));
658
+ const coveringWidth = width / minReplicas;
659
+ let walker = peers.head;
660
+ if (this.role instanceof Replicator) {
661
+ // start at our node (local first)
662
+ while (walker) {
663
+ if (walker.value.publicKey.equals(this.node.identity.publicKey)) {
664
+ break;
665
+ }
666
+ walker = walker.next;
667
+ }
668
+ }
669
+ else {
670
+ const seed = Math.round(peers.length * Math.random()); // start at a random point
671
+ for (let i = 0; i < seed - 1; i++) {
672
+ if (walker?.next == null) {
673
+ break;
674
+ }
675
+ walker = walker.next;
676
+ }
677
+ }
678
+ const set = [];
679
+ let distance = 0;
680
+ const startNode = walker;
681
+ if (!startNode) {
682
+ return [];
683
+ }
684
+ let nextPoint = startNode.value.offset;
685
+ const t = +new Date();
686
+ while (walker && distance < coveringWidth) {
687
+ const absDelta = Math.abs(walker.value.offset - nextPoint);
688
+ const diff = Math.min(absDelta, width - absDelta);
689
+ if (diff < walker.value.role.factor / 2 + 0.00001) {
690
+ set.push(walker.value.publicKey.hashcode());
691
+ if (t - Number(walker.value.role.timestamp) >
692
+ roleAge /* ||
693
+ walker!.value.publicKey.equals(this.node.identity.publicKey)) */) {
694
+ nextPoint = (nextPoint + walker.value.role.factor) % 1;
695
+ distance += walker.value.role.factor;
696
+ }
697
+ }
698
+ walker = walker.next || peers.head;
699
+ if (walker?.value.publicKey &&
700
+ startNode?.value.publicKey.equals(walker?.value.publicKey)) {
701
+ break; // TODO throw error for failing to fetch ffull width
702
+ }
703
+ }
704
+ return set;
705
+ }
706
+ async replicator(entry, options) {
707
+ return this.isLeader(entry.gid, decodeReplicas(entry).getValue(this), options);
708
+ }
709
+ onRoleChange(prev, role, publicKey) {
710
+ if (this.closed) {
711
+ return;
712
+ }
713
+ this.distribute();
714
+ if (role instanceof Replicator) {
715
+ const timer = setTimeout(async () => {
716
+ this._closeController.signal.removeEventListener("abort", listener);
717
+ await this.rebalanceParticipationDebounced?.();
718
+ this.distribute();
719
+ }, WAIT_FOR_ROLE_MATURITY + 2000);
720
+ const listener = () => {
721
+ clearTimeout(timer);
722
+ };
723
+ this._closeController.signal.addEventListener("abort", listener);
724
+ }
725
+ this.events.dispatchEvent(new CustomEvent("role", {
726
+ detail: { publicKey, role }
727
+ }));
728
+ }
729
+ async modifyReplicators(role, publicKey) {
730
+ const { prev, changed } = await this._modifyReplicators(role, publicKey);
731
+ if (changed) {
732
+ await this.rebalanceParticipationDebounced?.(); // await this.rebalanceParticipation(false);
733
+ this.onRoleChange(prev, role, publicKey);
734
+ return true;
735
+ }
736
+ return false;
737
+ }
738
+ async _modifyReplicators(role, publicKey) {
739
+ if (role instanceof Replicator &&
429
740
  this._canReplicate &&
430
- !(await this._canReplicate(publicKey))) {
431
- return false;
741
+ !(await this._canReplicate(publicKey, role))) {
742
+ return { changed: false };
432
743
  }
433
744
  const sortedPeer = this._sortedPeersCache;
434
745
  if (!sortedPeer) {
435
746
  if (this.closed === false) {
436
747
  throw new Error("Unexpected, sortedPeersCache is undefined");
437
748
  }
438
- return false;
749
+ return { changed: false };
439
750
  }
440
- const code = publicKey.hashcode();
441
- if (subscribed) {
751
+ if (role instanceof Replicator && role.factor > 0) {
442
752
  // TODO use Set + list for fast lookup
443
- if (!sortedPeer.find((x) => x.hash === code)) {
444
- sortedPeer.push({ hash: code, timestamp: +new Date() });
445
- sortedPeer.sort((a, b) => a.hash.localeCompare(b.hash));
446
- return true;
753
+ // check also that peer is online
754
+ const isOnline = this.node.identity.publicKey.equals(publicKey) ||
755
+ (await this.waitFor(publicKey, { signal: this._closeController.signal })
756
+ .then(() => true)
757
+ .catch(() => false));
758
+ if (isOnline) {
759
+ // insert or if already there do nothing
760
+ const code = hashToUniformNumber(publicKey.bytes);
761
+ const rect = {
762
+ publicKey,
763
+ offset: code,
764
+ role
765
+ };
766
+ let currentNode = sortedPeer.head;
767
+ if (!currentNode) {
768
+ sortedPeer.push(rect);
769
+ this._totalParticipation += rect.role.factor;
770
+ return { changed: true };
771
+ }
772
+ else {
773
+ while (currentNode) {
774
+ if (currentNode.value.publicKey.equals(publicKey)) {
775
+ // update the value
776
+ // rect.timestamp = currentNode.value.timestamp;
777
+ const prev = currentNode.value;
778
+ currentNode.value = rect;
779
+ this._totalParticipation += rect.role.factor;
780
+ this._totalParticipation -= prev.role.factor;
781
+ // TODO change detection and only do change stuff if diff?
782
+ return { prev: prev.role, changed: true };
783
+ }
784
+ if (code > currentNode.value.offset) {
785
+ const next = currentNode?.next;
786
+ if (next) {
787
+ currentNode = next;
788
+ continue;
789
+ }
790
+ else {
791
+ break;
792
+ }
793
+ }
794
+ else {
795
+ currentNode = currentNode.prev;
796
+ break;
797
+ }
798
+ }
799
+ const prev = currentNode;
800
+ if (!prev?.next?.value.publicKey.equals(publicKey)) {
801
+ this._totalParticipation += rect.role.factor;
802
+ _insertAfter(sortedPeer, prev || undefined, rect);
803
+ }
804
+ else {
805
+ throw new Error("Unexpected");
806
+ }
807
+ return { changed: true };
808
+ }
447
809
  }
448
810
  else {
449
- return false;
811
+ return { changed: false };
450
812
  }
451
813
  }
452
814
  else {
453
- const deleteIndex = sortedPeer.findIndex((x) => x.hash === code);
454
- if (deleteIndex >= 0) {
455
- sortedPeer.splice(deleteIndex, 1);
456
- return true;
457
- }
458
- else {
459
- return false;
815
+ let currentNode = sortedPeer.head;
816
+ while (currentNode) {
817
+ if (currentNode.value.publicKey.equals(publicKey)) {
818
+ sortedPeer.removeNode(currentNode);
819
+ this._totalParticipation -= currentNode.value.role.factor;
820
+ return { prev: currentNode.value.role, changed: true };
821
+ }
822
+ currentNode = currentNode.next;
460
823
  }
824
+ return { changed: false };
461
825
  }
462
826
  }
463
827
  async handleSubscriptionChange(publicKey, changes, subscribed) {
464
- // TODO why are we doing two loops?
465
- const prev = [];
466
- for (const subscription of changes) {
467
- if (this.log.idString !== subscription.topic) {
468
- continue;
469
- }
470
- if (!subscription.data ||
471
- !startsWith(subscription.data, REPLICATOR_TYPE_VARIANT)) {
472
- prev.push(await this.modifySortedSubscriptionCache(false, publicKey));
473
- continue;
474
- }
475
- else {
476
- this._lastSubscriptionMessageId += 1;
477
- prev.push(await this.modifySortedSubscriptionCache(subscribed, publicKey));
478
- }
479
- }
480
- // TODO don't do this i fnot is replicator?
481
- for (const [i, subscription] of changes.entries()) {
482
- if (this.log.idString !== subscription.topic) {
483
- continue;
484
- }
485
- if (subscription.data) {
486
- try {
487
- const type = deserialize(subscription.data, Role);
488
- // Reorganize if the new subscriber is a replicator, or observers AND was replicator
489
- if (type instanceof Replicator || prev[i]) {
490
- await this.replicationReorganization();
828
+ if (subscribed) {
829
+ if (this.role instanceof Replicator) {
830
+ for (const subscription of changes) {
831
+ if (this.log.idString !== subscription) {
832
+ continue;
491
833
  }
834
+ this.rpc
835
+ .send(new ResponseRoleMessage(this.role), {
836
+ mode: new AcknowledgeDelivery({ redundancy: 1, to: [publicKey] })
837
+ })
838
+ .catch((e) => logger.error(e.toString()));
492
839
  }
493
- catch (error) {
494
- logger.warn("Recieved subscription with invalid data on topic: " +
495
- subscription.topic +
496
- ". Error: " +
497
- error?.message);
840
+ }
841
+ //if(evt.detail.subscriptions.map((x) => x.topic).includes())
842
+ }
843
+ else {
844
+ for (const topic of changes) {
845
+ if (this.log.idString !== topic) {
846
+ continue;
498
847
  }
848
+ await this.modifyReplicators(new Observer(), publicKey);
499
849
  }
500
850
  }
501
851
  }
502
- pruneSafely(entries, options) {
852
+ async prune(entries, options) {
853
+ if (options?.unchecked) {
854
+ return Promise.all(entries.map((x) => this.log.remove(x, {
855
+ recursively: true
856
+ })));
857
+ }
503
858
  // ask network if they have they entry,
504
859
  // so I can delete it
505
860
  // There is a few reasons why we might end up here
@@ -510,12 +865,16 @@ let SharedLog = class SharedLog extends Program {
510
865
  const filteredEntries = [];
511
866
  for (const entry of entries) {
512
867
  const pendingPrev = this._pendingDeletes.get(entry.hash);
868
+ if (pendingPrev) {
869
+ promises.push(pendingPrev.promise.promise);
870
+ continue;
871
+ }
513
872
  filteredEntries.push(entry);
514
873
  const existCounter = new Set();
515
874
  const minReplicas = decodeReplicas(entry);
516
875
  const deferredPromise = pDefer();
517
876
  const clear = () => {
518
- pendingPrev?.clear();
877
+ //pendingPrev?.clear();
519
878
  const pending = this._pendingDeletes.get(entry.hash);
520
879
  if (pending?.promise == deferredPromise) {
521
880
  this._pendingDeletes.delete(entry.hash);
@@ -531,7 +890,7 @@ let SharedLog = class SharedLog extends Program {
531
890
  deferredPromise.reject(e);
532
891
  };
533
892
  const timeout = setTimeout(() => {
534
- reject(new Error("Timeout"));
893
+ reject(new Error("Timeout for checked pruning"));
535
894
  }, options?.timeout ?? 10 * 1000);
536
895
  this._pendingDeletes.set(entry.hash, {
537
896
  promise: deferredPromise,
@@ -540,10 +899,17 @@ let SharedLog = class SharedLog extends Program {
540
899
  },
541
900
  callback: async (publicKeyHash) => {
542
901
  const minReplicasValue = minReplicas.getValue(this);
543
- const l = await this.findLeaders(entry.gid, minReplicasValue);
544
- if (l.find((x) => x === publicKeyHash)) {
902
+ const minMinReplicasValue = this.replicas.max
903
+ ? Math.min(minReplicasValue, this.replicas.max.getValue(this))
904
+ : minReplicasValue;
905
+ const leaders = await this.findLeaders(entry.gid, minMinReplicasValue);
906
+ if (leaders.find((x) => x === this.node.identity.publicKey.hashcode())) {
907
+ reject(new Error("Failed to delete, is leader"));
908
+ return;
909
+ }
910
+ if (leaders.find((x) => x === publicKeyHash)) {
545
911
  existCounter.add(publicKeyHash);
546
- if (minReplicas.getValue(this) <= existCounter.size) {
912
+ if (minMinReplicasValue <= existCounter.size) {
547
913
  this.log
548
914
  .remove(entry, {
549
915
  recursively: true
@@ -560,118 +926,173 @@ let SharedLog = class SharedLog extends Program {
560
926
  });
561
927
  promises.push(deferredPromise.promise);
562
928
  }
563
- if (filteredEntries.length > 0) {
564
- this.rpc.send(new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) }));
929
+ if (filteredEntries.length == 0) {
930
+ return;
565
931
  }
566
- return promises;
932
+ this.rpc.send(new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) }));
933
+ const onNewPeer = async (e) => {
934
+ if (e.detail.role instanceof Replicator) {
935
+ await this.rpc.send(new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) }), {
936
+ to: [e.detail.publicKey.hashcode()]
937
+ });
938
+ }
939
+ };
940
+ // check joining peers
941
+ this.events.addEventListener("role", onNewPeer);
942
+ return Promise.all(promises).finally(() => this.events.removeEventListener("role", onNewPeer));
567
943
  }
568
- /**
569
- * 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
570
- * 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
571
- * @param channel
572
- */
573
- async replicationReorganization() {
944
+ async distribute() {
945
+ /**
946
+ * TODO use information of new joined/leaving peer to create a subset of heads
947
+ * that we potentially need to share with other peers
948
+ */
949
+ if (this.closed) {
950
+ return;
951
+ }
574
952
  const changed = false;
953
+ await this.log.trim();
575
954
  const heads = await this.log.getHeads();
576
955
  const groupedByGid = await groupByGid(heads);
577
- let storeChanged = false;
956
+ const toDeliver = new Map();
957
+ const allEntriesToDelete = [];
578
958
  for (const [gid, entries] of groupedByGid) {
579
- const toSend = new Map();
580
- const newPeers = [];
959
+ if (this.closed) {
960
+ break;
961
+ }
581
962
  if (entries.length === 0) {
582
963
  continue; // TODO maybe close store?
583
964
  }
584
965
  const oldPeersSet = this._gidPeersHistory.get(gid);
585
966
  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
586
967
  );
968
+ const currentPeersSet = new Set(currentPeers);
969
+ this._gidPeersHistory.set(gid, currentPeersSet);
587
970
  for (const currentPeer of currentPeers) {
588
- if (!oldPeersSet?.has(currentPeer) &&
589
- currentPeer !== this.node.identity.publicKey.hashcode()) {
590
- storeChanged = true;
971
+ if (currentPeer == this.node.identity.publicKey.hashcode()) {
972
+ continue;
973
+ }
974
+ if (!oldPeersSet?.has(currentPeer)) {
591
975
  // second condition means that if the new peer is us, we should not do anything, since we are expecting to receive heads, not send
592
- newPeers.push(currentPeer);
593
- // send heads to the new peer
594
- // console.log('new gid for peer', newPeers.length, this.id.toString(), newPeer, gid, entries.length, newPeers)
595
- try {
596
- logger.debug(`${this.node.identity.publicKey.hashcode()}: Exchange heads ${entries.length === 1 ? entries[0].hash : "#" + entries.length} on rebalance`);
597
- for (const entry of entries) {
598
- toSend.set(entry.hash, entry);
599
- }
976
+ let arr = toDeliver.get(currentPeer);
977
+ if (!arr) {
978
+ arr = [];
979
+ toDeliver.set(currentPeer, arr);
600
980
  }
601
- catch (error) {
602
- if (error instanceof TimeoutError) {
603
- logger.error("Missing channel when reorg to peer: " + currentPeer.toString());
604
- continue;
605
- }
606
- throw error;
981
+ for (const entry of entries) {
982
+ arr.push(entry);
607
983
  }
608
984
  }
609
985
  }
610
- // We don't need this clause anymore because we got the trim option!
611
986
  if (!currentPeers.find((x) => x === this.node.identity.publicKey.hashcode())) {
612
- let entriesToDelete = entries.filter((e) => !e.createdLocally);
613
- if (this._sync) {
614
- // dont delete entries which we wish to keep
615
- entriesToDelete = await Promise.all(entriesToDelete.map((x) => this._sync(x))).then((filter) => entriesToDelete.filter((v, ix) => !filter[ix]));
616
- }
617
- // delete entries since we are not suppose to replicate this anymore
618
- // TODO add delay? freeze time? (to ensure resiliance for bad io)
619
- if (entriesToDelete.length > 0) {
620
- Promise.all(this.pruneSafely(entriesToDelete)).catch((e) => {
621
- logger.error(e.toString());
622
- });
987
+ if (currentPeers.length > 0) {
988
+ // If we are observer, never prune locally created entries, since we dont really know who can store them
989
+ // if we are replicator, we will always persist entries that we need to so filtering on createdLocally will not make a difference
990
+ const entriesToDelete = this._role instanceof Observer
991
+ ? entries.filter((e) => !e.createdLocally)
992
+ : entries;
993
+ entriesToDelete.map((x) => this._gidPeersHistory.delete(x.meta.gid));
994
+ allEntriesToDelete.push(...entriesToDelete);
623
995
  }
624
- // TODO if length === 0 maybe close store?
625
996
  }
626
- this._gidPeersHistory.set(gid, new Set(currentPeers));
627
- if (toSend.size === 0) {
628
- continue;
997
+ else {
998
+ for (const entry of entries) {
999
+ this._pendingDeletes
1000
+ .get(entry.hash)
1001
+ ?.promise.reject(new Error("Failed to delete, is leader: " +
1002
+ this.role.constructor.name +
1003
+ ". " +
1004
+ this.node.identity.publicKey.hashcode()));
1005
+ }
629
1006
  }
630
- const message = await createExchangeHeadsMessage(this.log, [...toSend.values()], // TODO send to peers directly
1007
+ }
1008
+ for (const [target, entries] of toDeliver) {
1009
+ const message = await createExchangeHeadsMessage(this.log, entries, // TODO send to peers directly
631
1010
  this._gidParentCache);
632
1011
  // TODO perhaps send less messages to more receivers for performance reasons?
633
1012
  await this.rpc.send(message, {
634
- to: newPeers,
635
- strict: true
1013
+ to: [target]
636
1014
  });
637
1015
  }
638
- if (storeChanged) {
639
- await this.log.trim(); // because for entries createdLocally,we can have trim options that still allow us to delete them
640
- }
641
- return storeChanged || changed;
642
- }
643
- /**
644
- *
645
- * @returns groups where at least one in any group will have the entry you are looking for
646
- */
647
- getDiscoveryGroups() {
648
- // TODO Optimize this so we don't have to recreate the array all the time!
649
- const minReplicas = this.replicas.min.getValue(this);
650
- const replicators = this.getReplicatorsSorted();
651
- if (!replicators) {
652
- return []; // No subscribers and we are not replicating
653
- }
654
- const numberOfGroups = Math.min(Math.ceil(replicators.length / minReplicas));
655
- const groups = new Array(numberOfGroups);
656
- for (let i = 0; i < groups.length; i++) {
657
- groups[i] = [];
658
- }
659
- for (let i = 0; i < replicators.length; i++) {
660
- groups[i % numberOfGroups].push(replicators[i]);
1016
+ if (allEntriesToDelete.length > 0) {
1017
+ this.prune(allEntriesToDelete).catch((e) => {
1018
+ logger.error(e.toString());
1019
+ });
661
1020
  }
662
- return groups;
663
- }
664
- async replicator(entry) {
665
- return this.isLeader(entry.gid, decodeReplicas(entry).getValue(this));
1021
+ return changed;
666
1022
  }
667
1023
  async _onUnsubscription(evt) {
668
- logger.debug(`Peer disconnected '${evt.detail.from.hashcode()}' from '${JSON.stringify(evt.detail.unsubscriptions.map((x) => x.topic))}'`);
1024
+ logger.debug(`Peer disconnected '${evt.detail.from.hashcode()}' from '${JSON.stringify(evt.detail.unsubscriptions.map((x) => x))}'`);
1025
+ this.latestRoleMessages.delete(evt.detail.from.hashcode());
1026
+ this.events.dispatchEvent(new CustomEvent("role", {
1027
+ detail: { publicKey: evt.detail.from, role: new Observer() }
1028
+ }));
669
1029
  return this.handleSubscriptionChange(evt.detail.from, evt.detail.unsubscriptions, false);
670
1030
  }
671
1031
  async _onSubscription(evt) {
672
- logger.debug(`New peer '${evt.detail.from.hashcode()}' connected to '${JSON.stringify(evt.detail.subscriptions.map((x) => x.topic))}'`);
1032
+ logger.debug(`New peer '${evt.detail.from.hashcode()}' connected to '${JSON.stringify(evt.detail.subscriptions.map((x) => x))}'`);
1033
+ this.remoteBlocks.onReachable(evt.detail.from);
673
1034
  return this.handleSubscriptionChange(evt.detail.from, evt.detail.subscriptions, true);
674
1035
  }
1036
+ replicationController;
1037
+ history;
1038
+ async addToHistory(usedMemory, factor) {
1039
+ (this.history || (this.history = [])).push({ usedMemory, factor });
1040
+ // Keep only the last N entries in the history array (you can adjust N based on your needs)
1041
+ const maxHistoryLength = 10;
1042
+ if (this.history.length > maxHistoryLength) {
1043
+ this.history.shift();
1044
+ }
1045
+ }
1046
+ async calculateTrend() {
1047
+ // Calculate the average change in factor per unit change in memory usage
1048
+ const factorChanges = this.history.map((entry, index) => {
1049
+ if (index > 0) {
1050
+ const memoryChange = entry.usedMemory - this.history[index - 1].usedMemory;
1051
+ if (memoryChange !== 0) {
1052
+ const factorChange = entry.factor - this.history[index - 1].factor;
1053
+ return factorChange / memoryChange;
1054
+ }
1055
+ }
1056
+ return 0;
1057
+ });
1058
+ // Return the average factor change per unit memory change
1059
+ return (factorChanges.reduce((sum, change) => sum + change, 0) /
1060
+ factorChanges.length);
1061
+ }
1062
+ async rebalanceParticipation(onRoleChange = true) {
1063
+ // update more participation rate to converge to the average expected rate or bounded by
1064
+ // resources such as memory and or cpu
1065
+ if (this.closed) {
1066
+ return false;
1067
+ }
1068
+ // The role is fixed (no changes depending on memory usage or peer count etc)
1069
+ if (this._roleOptions instanceof Role) {
1070
+ return false;
1071
+ }
1072
+ // TODO second condition: what if the current role is Observer?
1073
+ if (this._roleOptions.type == "replicator" &&
1074
+ this._role instanceof Replicator) {
1075
+ const peers = this.getReplicatorsSorted();
1076
+ const usedMemory = await this.getMemoryUsage();
1077
+ const newFactor = await this.replicationController.adjustReplicationFactor(usedMemory, this._role.factor, this._totalParticipation, peers?.length || 1);
1078
+ const newRole = new Replicator({
1079
+ factor: newFactor,
1080
+ timestamp: this._role.timestamp
1081
+ });
1082
+ const relativeDifference = Math.abs(this._role.factor - newRole.factor) / this._role.factor;
1083
+ if (relativeDifference > 0.0001) {
1084
+ const canReplicate = !this._canReplicate ||
1085
+ (await this._canReplicate(this.node.identity.publicKey, newRole));
1086
+ if (!canReplicate) {
1087
+ return false;
1088
+ }
1089
+ await this._updateRole(newRole, onRoleChange);
1090
+ return true;
1091
+ }
1092
+ return false;
1093
+ }
1094
+ return false;
1095
+ }
675
1096
  };
676
1097
  __decorate([
677
1098
  field({ type: Log }),
@@ -686,4 +1107,19 @@ SharedLog = __decorate([
686
1107
  __metadata("design:paramtypes", [Object])
687
1108
  ], SharedLog);
688
1109
  export { SharedLog };
1110
+ function _insertAfter(self, node, value) {
1111
+ const inserted = !node
1112
+ ? new yallist.Node(value, null, self.head, self)
1113
+ : new yallist.Node(value, node, node.next, self);
1114
+ // is tail
1115
+ if (inserted.next === null) {
1116
+ self.tail = inserted;
1117
+ }
1118
+ // is head
1119
+ if (inserted.prev === null) {
1120
+ self.head = inserted;
1121
+ }
1122
+ self.length++;
1123
+ return inserted;
1124
+ }
689
1125
  //# sourceMappingURL=index.js.map