@optimystic/db-p2p 0.17.0 → 0.19.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/dist/src/cluster/block-transfer-service.d.ts +10 -0
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +2 -1
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +112 -0
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -0
- package/dist/src/cluster/cluster-policy.js +88 -0
- package/dist/src/cluster/cluster-policy.js.map +1 -0
- package/dist/src/cluster/cluster-repo.d.ts +35 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +95 -19
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/quorum-restore.d.ts +25 -3
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +27 -3
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts +10 -2
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +38 -18
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +8 -0
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/inbound-authorization.d.ts +6 -0
- package/dist/src/inbound-authorization.d.ts.map +1 -1
- package/dist/src/inbound-authorization.js +6 -0
- package/dist/src/inbound-authorization.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +66 -4
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +130 -17
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +22 -23
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +45 -34
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +21 -3
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +27 -5
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +88 -28
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +287 -81
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +9 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +77 -7
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +17 -0
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +27 -4
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +20 -0
- package/src/cluster/block-transfer-service.ts +9 -1
- package/src/cluster/cluster-policy.ts +152 -0
- package/src/cluster/cluster-repo.ts +100 -22
- package/src/cluster/quorum-restore.ts +28 -3
- package/src/cluster/reconcile-block.ts +52 -19
- package/src/cluster/spread-on-churn.ts +8 -0
- package/src/inbound-authorization.ts +6 -0
- package/src/libp2p-key-network.ts +958 -807
- package/src/libp2p-node-base.ts +65 -57
- package/src/repo/cluster-coordinator.ts +30 -6
- package/src/repo/coordinator-repo.ts +329 -91
- package/src/storage/storage-repo.ts +81 -10
- package/src/testing/mesh-harness.ts +34 -4
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
IRepo, MessageOptions, BlockId, CommitRequest, CommitResult, GetBlockResults, PendRequest, PendResult, ActionBlocks,
|
|
3
3
|
ActionId, BlockGets, ActionPending, PendSuccess, ActionTransform, ActionTransforms,
|
|
4
|
-
GetBlockResult, IBlock, ActionRev,
|
|
4
|
+
GetBlockResult, IBlock, ActionRev, BlockUnavailableReason,
|
|
5
5
|
PendValidationHook,
|
|
6
|
-
CollectionId, IBlockChangeNotifier, CollectionChangeListener, CollectionChangeEvent
|
|
6
|
+
CollectionId, IBlockChangeNotifier, CollectionChangeListener, CollectionChangeEvent,
|
|
7
|
+
StaleFailure
|
|
7
8
|
} from "@optimystic/db-core";
|
|
8
9
|
import {
|
|
9
10
|
Latches, transformForBlockId, applyTransform, groupBy, concatTransform, emptyTransforms,
|
|
10
|
-
blockIdsForTransforms, transformsFromTransform
|
|
11
|
+
blockIdsForTransforms, transformsFromTransform, highestStaleAt
|
|
11
12
|
} from "@optimystic/db-core";
|
|
12
13
|
import { asyncIteratorToArray } from "../it-utility.js";
|
|
13
14
|
import type { IBlockStorage } from "./i-block-storage.js";
|
|
@@ -186,6 +187,11 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
186
187
|
const promotions: { collectionId: CollectionId, blockId: BlockId, actionId: ActionId, rev: number }[] = [];
|
|
187
188
|
const results = await Promise.all(distinctBlockIds.map(async (blockId) => {
|
|
188
189
|
const blockStorage = this.createBlockStorage(blockId);
|
|
190
|
+
// Set when this node KNOWS its answer for the block is a guess: the promotion
|
|
191
|
+
// below refused for a missing base, or getBlock() threw (truncated history /
|
|
192
|
+
// failed restore). An absent-reading block then reports `unavailable` instead of
|
|
193
|
+
// posing as an authoritative "never existed" — see BlockUnavailableReason.
|
|
194
|
+
let unavailable: BlockUnavailableReason | undefined;
|
|
189
195
|
|
|
190
196
|
// Ensure that all outstanding transactions in the context are committed.
|
|
191
197
|
// This promotes a landed-elsewhere pending via internalCommit, which mutates
|
|
@@ -235,6 +241,10 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
235
241
|
if (!(err instanceof MissingBaseRevisionError)) {
|
|
236
242
|
throw err;
|
|
237
243
|
}
|
|
244
|
+
// This node holds records PROVING the block exists (a pending it could not
|
|
245
|
+
// promote); if the block then reads as absent below, the answer is a guess,
|
|
246
|
+
// not an authoritative "never existed".
|
|
247
|
+
unavailable = 'unmaterializable';
|
|
238
248
|
log('get:promote-skipped-missing-base blockId=%s rev=%d reason=%s', blockId, err.rev, err.message);
|
|
239
249
|
}
|
|
240
250
|
});
|
|
@@ -248,7 +258,26 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
248
258
|
// (actionId, rev) self-heals it via storage.recover() in commit(). Not repaired lazily on
|
|
249
259
|
// the read path because get() holds no commit latch; if stale reads on unwritten blocks
|
|
250
260
|
// ever become a problem, add a latched lazy recover() here.
|
|
251
|
-
|
|
261
|
+
//
|
|
262
|
+
// getBlock() THROWS when this node holds a `latest` it cannot materialize (truncated
|
|
263
|
+
// history: "Failed to find materialized block", or a failed restore). Caught PER BLOCK so
|
|
264
|
+
// one broken block cannot fail the whole batch's Promise.all and take healthy siblings
|
|
265
|
+
// down with it. The read still fails for THIS block — TransactorSource throws
|
|
266
|
+
// BlockUnavailableError on the flagged entry — so nothing is swallowed.
|
|
267
|
+
let blockRev: Awaited<ReturnType<IBlockStorage['getBlock']>>;
|
|
268
|
+
try {
|
|
269
|
+
blockRev = await blockStorage.getBlock(context?.rev);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
// NOTE: the entry drops `state.latest`, which this node does know (getLatest() does not
|
|
272
|
+
// materialize, so it does not throw). Empty state is what makes CoordinatorRepo treat the
|
|
273
|
+
// block as missing and consult the cohort — exactly the repair this block needs. If a
|
|
274
|
+
// consumer ever needs the revision behind an unavailable answer (e.g. to ask the cohort
|
|
275
|
+
// for a specific rev instead of the whole block), carry `latest` here and widen the
|
|
276
|
+
// coordinator's consult trigger to `isMissing || unavailable` so repair still fires.
|
|
277
|
+
log('get:unmaterializable blockId=%s error=%s', blockId,
|
|
278
|
+
err instanceof Error ? err.message : String(err));
|
|
279
|
+
return [blockId, { state: {}, unavailable: 'unmaterializable' } as GetBlockResult];
|
|
280
|
+
}
|
|
252
281
|
|
|
253
282
|
// Include pending action if requested — handled first so a pending-only
|
|
254
283
|
// insert (no committed revision yet) can still be served by applying the
|
|
@@ -256,6 +285,8 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
256
285
|
if (context?.actionId !== undefined) {
|
|
257
286
|
const pendingTransform = await blockStorage.getPendingTransaction(context.actionId);
|
|
258
287
|
if (!pendingTransform) {
|
|
288
|
+
// Caller-contract violation (the caller asserted a pending this repo never had, or
|
|
289
|
+
// cancelled) — an error, not an availability question. Deliberately NOT `unavailable`.
|
|
259
290
|
throw new Error(`Pending action ${context.actionId} not found`);
|
|
260
291
|
}
|
|
261
292
|
const block = applyTransform(blockRev?.block, pendingTransform);
|
|
@@ -264,12 +295,21 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
264
295
|
state: {
|
|
265
296
|
latest: await blockStorage.getLatest(),
|
|
266
297
|
pendings: [context.actionId]
|
|
267
|
-
}
|
|
268
|
-
|
|
298
|
+
},
|
|
299
|
+
// A pending applied to a missing base can materialize nothing (applyTransform drops
|
|
300
|
+
// updates with no block to apply them to) — that absence is a guess, and is flagged.
|
|
301
|
+
// A materialized block is a real answer regardless of the earlier refusal.
|
|
302
|
+
...(unavailable !== undefined && block === undefined ? { unavailable } : {})
|
|
303
|
+
} as GetBlockResult];
|
|
269
304
|
}
|
|
270
305
|
|
|
271
306
|
if (!blockRev) {
|
|
272
|
-
|
|
307
|
+
// `unavailable` distinguishes "never existed" (the common insert-probe case, no flag)
|
|
308
|
+
// from "this node cannot reconstruct it" (the promotion above refused for a missing
|
|
309
|
+
// base). A tombstoned block also lands here with meta.latest set, but it never enters
|
|
310
|
+
// the missing-base catch, so it stays an authoritative absent — keyed off the explicit
|
|
311
|
+
// flag, not off "no block".
|
|
312
|
+
return [blockId, { state: {}, ...(unavailable !== undefined ? { unavailable } : {}) } as GetBlockResult];
|
|
273
313
|
}
|
|
274
314
|
|
|
275
315
|
const pendings = await asyncIteratorToArray(blockStorage.listPendingTransactions());
|
|
@@ -322,6 +362,8 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
322
362
|
if (this.validatePend && request.transaction && request.operationsHash) {
|
|
323
363
|
const validationResult = await this.validatePend(request.transaction, request.operationsHash);
|
|
324
364
|
if (!validationResult.valid) {
|
|
365
|
+
// Hard rejection: the transaction itself is invalid, so no `conflict` flag — a
|
|
366
|
+
// re-read and re-pend would fail the same way and only burn the retry budget.
|
|
325
367
|
return {
|
|
326
368
|
success: false,
|
|
327
369
|
reason: validationResult.reason ?? 'Transaction validation failed'
|
|
@@ -333,6 +375,10 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
333
375
|
log('pend actionId=%s blockIds=%d rev=%s', request.actionId, blockIds.length, request.rev);
|
|
334
376
|
const pendings: ActionPending[] = [];
|
|
335
377
|
const missing: ActionTransforms[] = [];
|
|
378
|
+
// Highest revision this node confirms holding among the blocks that are at or past the
|
|
379
|
+
// requested one — reported as StaleFailure.staleAt so a losing writer learns the number
|
|
380
|
+
// instead of parsing prose. Confirmed-local only: we read it from our own storage below.
|
|
381
|
+
let staleAt: StaleFailure['staleAt'];
|
|
336
382
|
|
|
337
383
|
// Potential race condition: A concurrent commit operation could complete
|
|
338
384
|
// between the conflict checks (latest.rev, listPendingTransactions) and the
|
|
@@ -353,6 +399,13 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
353
399
|
if (request.rev !== undefined || transforms.insert) {
|
|
354
400
|
const latest = await blockStorage.getLatest();
|
|
355
401
|
if (latest && latest.rev >= (request.rev ?? 0)) {
|
|
402
|
+
// Only a real revision race yields a meaningful `staleAt`. When `request.rev` is
|
|
403
|
+
// undefined this same branch fires for an insert collision (the comparison degrades
|
|
404
|
+
// to `latest.rev >= 0`, true for any existing block), and reporting that block's
|
|
405
|
+
// revision would be a number that answers a question nobody asked.
|
|
406
|
+
if (request.rev !== undefined) {
|
|
407
|
+
staleAt = highestStaleAt([staleAt, { blockId, rev: latest.rev }]);
|
|
408
|
+
}
|
|
356
409
|
const transforms = await asyncIteratorToArray(blockStorage.listRevisions(request.rev ?? 0, latest.rev));
|
|
357
410
|
for (const actionRev of transforms) {
|
|
358
411
|
const transform = await blockStorage.getTransaction(actionRev.actionId);
|
|
@@ -373,16 +426,19 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
373
426
|
log('pend:stale actionId=%s missing=%d', request.actionId, missing.length);
|
|
374
427
|
return {
|
|
375
428
|
success: false,
|
|
376
|
-
|
|
429
|
+
conflict: true,
|
|
430
|
+
missing,
|
|
431
|
+
...(staleAt === undefined ? {} : { staleAt })
|
|
377
432
|
};
|
|
378
433
|
}
|
|
379
434
|
|
|
380
435
|
if (pendings.length > 0) {
|
|
381
436
|
if (request.policy === 'f') { // Fail on pending actions
|
|
382
|
-
return { success: false, pending: pendings };
|
|
437
|
+
return { success: false, conflict: true, pending: pendings };
|
|
383
438
|
} else if (request.policy === 'r') { // Return populated pending actions
|
|
384
439
|
return {
|
|
385
440
|
success: false,
|
|
441
|
+
conflict: true,
|
|
386
442
|
pending: await Promise.all(pendings.map(async action => {
|
|
387
443
|
const blockStorage = this.createBlockStorage(action.blockId);
|
|
388
444
|
return {
|
|
@@ -459,6 +515,10 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
459
515
|
// - toCommit: latest.rev < request.rev or no latest yet → run internalCommit.
|
|
460
516
|
const toCommit: { blockId: BlockId, storage: IBlockStorage }[] = [];
|
|
461
517
|
const missedCommits: { blockId: BlockId, transforms: ActionTransform[] }[] = [];
|
|
518
|
+
// Highest revision among the blocks confirmed lost to a newer one — reported as
|
|
519
|
+
// StaleFailure.staleAt. The idempotent-retry `continue` below is a no-op, not a loss,
|
|
520
|
+
// so it never seeds this.
|
|
521
|
+
let staleAt: StaleFailure['staleAt'];
|
|
462
522
|
for (const entry of blockStorages) {
|
|
463
523
|
const { blockId, storage } = entry;
|
|
464
524
|
const latest = await storage.getLatest();
|
|
@@ -467,6 +527,7 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
467
527
|
// Idempotent no-op for this block — already committed with this exact (actionId, rev).
|
|
468
528
|
continue;
|
|
469
529
|
}
|
|
530
|
+
staleAt = highestStaleAt([staleAt, { blockId, rev: latest.rev }]);
|
|
470
531
|
const transforms: ActionTransform[] = [];
|
|
471
532
|
for await (const actionRev of storage.listRevisions(request.rev, latest.rev)) {
|
|
472
533
|
const transform = await storage.getTransaction(actionRev.actionId);
|
|
@@ -489,7 +550,8 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
489
550
|
log('commit:stale actionId=%s missed=%d', request.actionId, missedCommits.length);
|
|
490
551
|
return { // Return directly, locks will be released in finally
|
|
491
552
|
success: false,
|
|
492
|
-
missing: perBlockActionTransformsToPerAction(missedCommits)
|
|
553
|
+
missing: perBlockActionTransformsToPerAction(missedCommits),
|
|
554
|
+
...(staleAt === undefined ? {} : { staleAt })
|
|
493
555
|
};
|
|
494
556
|
}
|
|
495
557
|
|
|
@@ -745,6 +807,15 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
745
807
|
* all, so it is translated into {@link MissingBaseRevisionError} rather than surfacing as an opaque
|
|
746
808
|
* storage fault: the healing path can then repair the block instead of the fault resetting the
|
|
747
809
|
* cluster stream, and a wedged node recovers on the next write touching the block.
|
|
810
|
+
*
|
|
811
|
+
* The catch is deliberately UNNARROWED — it also absorbs a transient fault (a raw-storage read
|
|
812
|
+
* error, a `restoreCallback` timeout on a block whose `ranges` do not cover its own `latest`).
|
|
813
|
+
* BlockStorage reports every one of these as a bare `Error`, so they cannot be told apart here,
|
|
814
|
+
* and treating them as divergence is the safe default: this node genuinely cannot materialize the
|
|
815
|
+
* base right now, and the cluster's policy is to heal rather than throw out of consensus. The
|
|
816
|
+
* price is that a transient fault ALSO drops the pending (see {@link refuseMissingBase}), so the
|
|
817
|
+
* block converges by replication instead of by a replay the retry could have done. Narrowing this
|
|
818
|
+
* would require typed faults out of BlockStorage; until then, prefer the tolerant reading.
|
|
748
819
|
*/
|
|
749
820
|
private async readCommitBase(
|
|
750
821
|
blockId: BlockId,
|
|
@@ -42,6 +42,13 @@ export interface MeshFailureConfig {
|
|
|
42
42
|
failingPeers?: Set<string>;
|
|
43
43
|
/** Make findCluster return empty (simulate DHT failure) */
|
|
44
44
|
findClusterFails?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Peers that are unreachable on the READ path: their latest-revision consult
|
|
47
|
+
* (`ClusterLatestCallback`) REJECTS — silence the coordinator must count as "did not
|
|
48
|
+
* answer", never as the peer claiming absence — and the reconcile/acquire transfer skips
|
|
49
|
+
* them as a source. Distinct from `failingPeers`, which fails cluster (write) updates.
|
|
50
|
+
*/
|
|
51
|
+
silentPeers?: Set<string>;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
class MockPeerNetwork implements IPeerNetwork {
|
|
@@ -118,10 +125,13 @@ export interface Mesh {
|
|
|
118
125
|
*
|
|
119
126
|
* `nodes` is captured by reference and is fully populated by the time either caller invokes it.
|
|
120
127
|
*/
|
|
121
|
-
const makeReconcileBlock = (nodes: MeshNode[], selfPeerId: string, storageRepo: StorageRepo): ReconcileBlockCallback =>
|
|
128
|
+
const makeReconcileBlock = (nodes: MeshNode[], selfPeerId: string, storageRepo: StorageRepo, failures: MeshFailureConfig): ReconcileBlockCallback =>
|
|
122
129
|
async (blockId, committed, cohortPeerIds) => {
|
|
123
130
|
for (const peerIdStr of cohortPeerIds) {
|
|
124
131
|
if (peerIdStr === selfPeerId) continue;
|
|
132
|
+
// A silent peer cannot serve bytes either — without this, a test that silenced a
|
|
133
|
+
// peer's consult could still accidentally converge THROUGH that peer.
|
|
134
|
+
if (failures.silentPeers?.has(peerIdStr)) continue;
|
|
125
135
|
const target = nodes.find(n => n.peerId.toString() === peerIdStr);
|
|
126
136
|
if (!target) continue;
|
|
127
137
|
const result = await target.storageRepo.get({ blockIds: [blockId] }, { skipClusterFetch: true } as any);
|
|
@@ -177,7 +187,7 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
177
187
|
|
|
178
188
|
// Active reconciliation: when a member commits a block it never pended (cohort drift),
|
|
179
189
|
// pull the committed revision from a sibling cohort node that holds it.
|
|
180
|
-
const reconcileBlock = makeReconcileBlock(nodes, peerId.toString(), storageRepo);
|
|
190
|
+
const reconcileBlock = makeReconcileBlock(nodes, peerId.toString(), storageRepo, failures);
|
|
181
191
|
|
|
182
192
|
const member = clusterMember({
|
|
183
193
|
storageRepo,
|
|
@@ -223,6 +233,12 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
223
233
|
// existed to expose. Transfer now happens where it does in production: through
|
|
224
234
|
// `acquireBlockFromCohort` below, gated on a corroborated revision.
|
|
225
235
|
const clusterLatestCallback: ClusterLatestCallback = async (peerId: PeerId, blockId: BlockId, context?): Promise<ActionRev | undefined> => {
|
|
236
|
+
// Silence: the peer never answers. REJECTS, mirroring what a dial failure does to the
|
|
237
|
+
// production callback — the coordinator must count this as "did not answer", never as
|
|
238
|
+
// an absent claim (a resolved `undefined` remains the peer answering "I hold nothing").
|
|
239
|
+
if (failures.silentPeers?.has(peerId.toString())) {
|
|
240
|
+
throw new Error(`Peer ${peerId.toString()} is silent`);
|
|
241
|
+
}
|
|
226
242
|
const target = nodes.find(n => n.peerId.equals(peerId));
|
|
227
243
|
if (!target) return undefined;
|
|
228
244
|
const result = await target.storageRepo.get(
|
|
@@ -248,7 +264,7 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
248
264
|
};
|
|
249
265
|
const factory = coordinatorRepo(
|
|
250
266
|
nodeKeyNetwork,
|
|
251
|
-
|
|
267
|
+
createClusterClient,
|
|
252
268
|
{
|
|
253
269
|
clusterSize: options.clusterSize ?? nodeCount,
|
|
254
270
|
superMajorityThreshold: options.superMajorityThreshold ?? DEFAULT_SUPER_MAJORITY_THRESHOLD,
|
|
@@ -265,13 +281,27 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
265
281
|
clusterLatestCallback,
|
|
266
282
|
// The read path's transfer mechanism — the same callback the member uses on the commit path,
|
|
267
283
|
// mirroring how `libp2p-node-base` shares one `reconcileBlock` between both.
|
|
268
|
-
acquireBlockFromCohort: makeReconcileBlock(nodes, node.peerId.toString(), node.storageRepo)
|
|
284
|
+
acquireBlockFromCohort: makeReconcileBlock(nodes, node.peerId.toString(), node.storageRepo, failures)
|
|
269
285
|
});
|
|
270
286
|
}
|
|
271
287
|
|
|
272
288
|
return { nodes, failures, keyNetwork };
|
|
273
289
|
}
|
|
274
290
|
|
|
291
|
+
/**
|
|
292
|
+
* The nodes the key network keeps OUT of `blockId`'s cohort — peers that receive none of the
|
|
293
|
+
* block's cluster traffic, and so hold none of its content until something repairs them.
|
|
294
|
+
*
|
|
295
|
+
* Peer ids are generated fresh per mesh, so which node is responsible for a given block is random
|
|
296
|
+
* from run to run: in a 3-node `responsibilityK: 1` mesh, `nodes[1]` is the block's sole responsible
|
|
297
|
+
* peer about a third of the time, and then it receives the writer's commit directly. A test that
|
|
298
|
+
* needs a genuinely non-responsible node has to ask the routing layer rather than assume an index.
|
|
299
|
+
*/
|
|
300
|
+
export async function nonResponsibleNodes(mesh: Mesh, blockId: string): Promise<MeshNode[]> {
|
|
301
|
+
const cohort = await mesh.keyNetwork.findCluster(new TextEncoder().encode(blockId));
|
|
302
|
+
return mesh.nodes.filter(node => !(node.peerId.toString() in cohort));
|
|
303
|
+
}
|
|
304
|
+
|
|
275
305
|
export interface BuildTransactorOptions {
|
|
276
306
|
timeoutMs?: number;
|
|
277
307
|
abortOrCancelTimeoutMs?: number;
|