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