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