@peerbit/shared-log 3.1.10 → 4.0.1

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