@optimystic/db-p2p 0.9.3 → 0.11.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/README.md +32 -6
- package/dist/src/cluster/cluster-repo.d.ts +23 -1
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +110 -8
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/i-transaction-state-store.d.ts +36 -0
- package/dist/src/cluster/i-transaction-state-store.d.ts.map +1 -0
- package/dist/src/cluster/i-transaction-state-store.js +2 -0
- package/dist/src/cluster/i-transaction-state-store.js.map +1 -0
- package/dist/src/cluster/memory-transaction-state-store.d.ts +19 -0
- package/dist/src/cluster/memory-transaction-state-store.d.ts.map +1 -0
- package/dist/src/cluster/memory-transaction-state-store.js +44 -0
- package/dist/src/cluster/memory-transaction-state-store.js.map +1 -0
- package/dist/src/cluster/persistent-transaction-state-store.d.ts +26 -0
- package/dist/src/cluster/persistent-transaction-state-store.d.ts.map +1 -0
- package/dist/src/cluster/persistent-transaction-state-store.js +79 -0
- package/dist/src/cluster/persistent-transaction-state-store.js.map +1 -0
- package/dist/src/cluster/spread-on-churn.d.ts +67 -0
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -0
- package/dist/src/cluster/spread-on-churn.js +192 -0
- package/dist/src/cluster/spread-on-churn.js.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +13 -0
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +17 -4
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +8 -0
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +21 -0
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +12 -1
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +67 -2
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +5 -2
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +8 -4
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/storage/i-kv-store.d.ts +9 -0
- package/dist/src/storage/i-kv-store.d.ts.map +1 -0
- package/dist/src/storage/i-kv-store.js +2 -0
- package/dist/src/storage/i-kv-store.js.map +1 -0
- package/dist/src/storage/memory-kv-store.d.ts +10 -0
- package/dist/src/storage/memory-kv-store.d.ts.map +1 -0
- package/dist/src/storage/memory-kv-store.js +23 -0
- package/dist/src/storage/memory-kv-store.js.map +1 -0
- package/package.json +2 -2
- package/src/cluster/cluster-repo.ts +112 -8
- package/src/cluster/i-transaction-state-store.ts +43 -0
- package/src/cluster/memory-transaction-state-store.ts +56 -0
- package/src/cluster/persistent-transaction-state-store.ts +92 -0
- package/src/cluster/spread-on-churn.ts +285 -0
- package/src/index.ts +6 -0
- package/src/libp2p-node-base.ts +35 -4
- package/src/network/network-manager-service.ts +32 -0
- package/src/repo/cluster-coordinator.ts +72 -2
- package/src/repo/coordinator-repo.ts +13 -4
- package/src/storage/i-kv-store.ts +8 -0
- package/src/storage/memory-kv-store.ts +28 -0
package/src/libp2p-node-base.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { bootstrap } from '@libp2p/bootstrap';
|
|
|
8
8
|
import { circuitRelayServer } from '@libp2p/circuit-relay-v2';
|
|
9
9
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
10
10
|
import { generateKeyPair } from '@libp2p/crypto/keys';
|
|
11
|
+
import type { PrivateKey } from '@libp2p/interface';
|
|
11
12
|
import { clusterService } from './cluster/service.js';
|
|
12
13
|
import { repoService } from './repo/service.js';
|
|
13
14
|
import { StorageRepo } from './storage/storage-repo.js';
|
|
@@ -19,6 +20,7 @@ import { coordinatorRepo } from './repo/coordinator-repo.js';
|
|
|
19
20
|
import { Libp2pKeyPeerNetwork, type NetworkMode, type NetworkStatePersistence } from './libp2p-key-network.js';
|
|
20
21
|
import { ClusterClient } from './cluster/client.js';
|
|
21
22
|
import type { IRepo, ICluster, ITransactionValidator } from '@optimystic/db-core';
|
|
23
|
+
import type { ITransactionStateStore } from './cluster/i-transaction-state-store.js';
|
|
22
24
|
import { networkManagerService } from './network/network-manager-service.js';
|
|
23
25
|
import { fretService, Libp2pFretService } from 'p2p-fret';
|
|
24
26
|
import { syncService } from './sync/service.js';
|
|
@@ -93,6 +95,19 @@ export type NodeOptions = {
|
|
|
93
95
|
|
|
94
96
|
/** Dispute protocol configuration */
|
|
95
97
|
dispute?: Partial<DisputeConfig>;
|
|
98
|
+
|
|
99
|
+
/** Optional persistent store for 2PC transaction state (enables crash recovery) */
|
|
100
|
+
transactionStateStore?: ITransactionStateStore;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Optional Ed25519 private key for this node. When provided, the libp2p
|
|
104
|
+
* node uses this identity instead of generating a fresh keypair. Use this
|
|
105
|
+
* to persist peer identity across process restarts.
|
|
106
|
+
*
|
|
107
|
+
* Accepts a libp2p `PrivateKey` (as returned by `generateKeyPair('Ed25519')`
|
|
108
|
+
* or `privateKeyFromProtobuf(...)` from `@libp2p/crypto/keys`).
|
|
109
|
+
*/
|
|
110
|
+
privateKey?: PrivateKey;
|
|
96
111
|
};
|
|
97
112
|
|
|
98
113
|
function resolveStorage(provider: RawStorageProvider | undefined): IRawStorage {
|
|
@@ -152,8 +167,7 @@ export async function createLibp2pNodeBase(
|
|
|
152
167
|
}
|
|
153
168
|
};
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
const nodePrivateKey = await generateKeyPair('Ed25519');
|
|
170
|
+
const nodePrivateKey = options.privateKey ?? await generateKeyPair('Ed25519');
|
|
157
171
|
|
|
158
172
|
const listenAddrs = options.listenAddrs ?? defaults.listenAddrs;
|
|
159
173
|
const transports = options.transports ?? defaults.transports;
|
|
@@ -300,7 +314,8 @@ export async function createLibp2pNodeBase(
|
|
|
300
314
|
fretService: fretSvc,
|
|
301
315
|
validator: options.validator,
|
|
302
316
|
reputation,
|
|
303
|
-
consensusConfig
|
|
317
|
+
consensusConfig,
|
|
318
|
+
stateStore: options.transactionStateStore
|
|
304
319
|
});
|
|
305
320
|
|
|
306
321
|
const coordinatorRepoFactory = coordinatorRepo(
|
|
@@ -311,7 +326,8 @@ export async function createLibp2pNodeBase(
|
|
|
311
326
|
...consensusConfig
|
|
312
327
|
},
|
|
313
328
|
fretSvc,
|
|
314
|
-
reputation
|
|
329
|
+
reputation,
|
|
330
|
+
options.transactionStateStore
|
|
315
331
|
);
|
|
316
332
|
|
|
317
333
|
// Create callback for querying cluster peers for their latest block revision
|
|
@@ -342,6 +358,12 @@ export async function createLibp2pNodeBase(
|
|
|
342
358
|
clusterLatestCallback
|
|
343
359
|
});
|
|
344
360
|
|
|
361
|
+
// Recover persisted transaction state before accepting new requests
|
|
362
|
+
if (options.transactionStateStore) {
|
|
363
|
+
await (clusterImpl as import('./cluster/cluster-repo.js').ClusterMember).recoverTransactions();
|
|
364
|
+
await (coordinatedRepo as import('./repo/coordinator-repo.js').CoordinatorRepo).recoverTransactions();
|
|
365
|
+
}
|
|
366
|
+
|
|
345
367
|
// Initialize Arachnode ring membership and restoration
|
|
346
368
|
const enableArachnode = options.arachnode?.enableRingZulu ?? true;
|
|
347
369
|
if (enableArachnode) {
|
|
@@ -439,6 +461,15 @@ export async function createLibp2pNodeBase(
|
|
|
439
461
|
});
|
|
440
462
|
}
|
|
441
463
|
|
|
464
|
+
// Cleanup cluster member intervals on node stop
|
|
465
|
+
{
|
|
466
|
+
const previousStop = node.stop.bind(node);
|
|
467
|
+
node.stop = async () => {
|
|
468
|
+
(clusterImpl as import('./cluster/cluster-repo.js').ClusterMember).dispose();
|
|
469
|
+
await previousStop();
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
442
473
|
// Expose coordinated repo and storage for external use
|
|
443
474
|
(node as any).coordinatedRepo = coordinatedRepo;
|
|
444
475
|
(node as any).storageRepo = storageRepo;
|
|
@@ -6,6 +6,7 @@ import { toString as u8ToString } from 'uint8arrays/to-string'
|
|
|
6
6
|
import type { IPeerReputation } from '../reputation/types.js'
|
|
7
7
|
import { PenaltyReason } from '../reputation/types.js'
|
|
8
8
|
import { RebalanceMonitor, type RebalanceMonitorConfig } from '../cluster/rebalance-monitor.js'
|
|
9
|
+
import { SpreadOnChurnMonitor, type SpreadOnChurnConfig, type SpreadOnChurnDeps } from '../cluster/spread-on-churn.js'
|
|
9
10
|
import type { PartitionDetector } from '../cluster/partition-detector.js'
|
|
10
11
|
import type { ArachnodeFretAdapter } from '../storage/arachnode-fret-adapter.js'
|
|
11
12
|
|
|
@@ -41,6 +42,7 @@ export class NetworkManagerService implements Startable {
|
|
|
41
42
|
private reputation?: IPeerReputation
|
|
42
43
|
private libp2pRef: Libp2p | undefined
|
|
43
44
|
private rebalanceMonitor?: RebalanceMonitor
|
|
45
|
+
private spreadOnChurnMonitor?: SpreadOnChurnMonitor
|
|
44
46
|
|
|
45
47
|
constructor(private readonly components: Components, init: NetworkManagerServiceInit = {}) {
|
|
46
48
|
this.log = components.logger.forComponent('db-p2p:network-manager')
|
|
@@ -88,6 +90,33 @@ export class NetworkManagerService implements Startable {
|
|
|
88
90
|
return this.rebalanceMonitor
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Initialize the spread-on-churn monitor. Call after libp2p, FRET are available.
|
|
95
|
+
* Caller provides repo and peerNetwork (not held by NetworkManagerService directly).
|
|
96
|
+
*/
|
|
97
|
+
initSpreadOnChurnMonitor(
|
|
98
|
+
partitionDetector: PartitionDetector,
|
|
99
|
+
repo: SpreadOnChurnDeps['repo'],
|
|
100
|
+
peerNetwork: SpreadOnChurnDeps['peerNetwork'],
|
|
101
|
+
clusterSize: number,
|
|
102
|
+
config?: Partial<SpreadOnChurnConfig>
|
|
103
|
+
): SpreadOnChurnMonitor {
|
|
104
|
+
const libp2p = this.getLibp2p()
|
|
105
|
+
const fret = this.getFret()
|
|
106
|
+
if (!libp2p || !fret) {
|
|
107
|
+
throw new Error('Cannot init SpreadOnChurnMonitor: libp2p or FRET not available')
|
|
108
|
+
}
|
|
109
|
+
this.spreadOnChurnMonitor = new SpreadOnChurnMonitor(
|
|
110
|
+
{ libp2p, fret, partitionDetector, repo, peerNetwork, clusterSize },
|
|
111
|
+
config
|
|
112
|
+
)
|
|
113
|
+
return this.spreadOnChurnMonitor
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getSpreadOnChurnMonitor(): SpreadOnChurnMonitor | undefined {
|
|
117
|
+
return this.spreadOnChurnMonitor
|
|
118
|
+
}
|
|
119
|
+
|
|
91
120
|
private getLibp2p(): Libp2p | undefined {
|
|
92
121
|
return this.libp2pRef ?? this.components.libp2p;
|
|
93
122
|
}
|
|
@@ -113,6 +142,9 @@ export class NetworkManagerService implements Startable {
|
|
|
113
142
|
if (this.rebalanceMonitor) {
|
|
114
143
|
await this.rebalanceMonitor.stop()
|
|
115
144
|
}
|
|
145
|
+
if (this.spreadOnChurnMonitor) {
|
|
146
|
+
await this.spreadOnChurnMonitor.stop()
|
|
147
|
+
}
|
|
116
148
|
this.running = false
|
|
117
149
|
}
|
|
118
150
|
|
|
@@ -10,6 +10,7 @@ import type { ClusterLogPeerOutcome } from './types.js'
|
|
|
10
10
|
import type { FretService } from "p2p-fret";
|
|
11
11
|
import type { IPeerReputation } from "../reputation/types.js";
|
|
12
12
|
import { PenaltyReason } from "../reputation/types.js";
|
|
13
|
+
import type { ITransactionStateStore } from "../cluster/i-transaction-state-store.js";
|
|
13
14
|
|
|
14
15
|
const log = createLogger('cluster')
|
|
15
16
|
|
|
@@ -35,7 +36,6 @@ interface ClusterTransactionState {
|
|
|
35
36
|
|
|
36
37
|
/** Manages distributed transactions across clusters */
|
|
37
38
|
export class ClusterCoordinator {
|
|
38
|
-
// TODO: move this into a state management interface so that transaction state can be persisted
|
|
39
39
|
private transactions: Map<string, ClusterTransactionState> = new Map();
|
|
40
40
|
private readonly retryInitialIntervalMs = 2000;
|
|
41
41
|
private readonly retryBackoffFactor = 2;
|
|
@@ -52,7 +52,8 @@ export class ClusterCoordinator {
|
|
|
52
52
|
wasTransactionExecuted?: (messageHash: string) => boolean;
|
|
53
53
|
},
|
|
54
54
|
private readonly fretService?: FretService,
|
|
55
|
-
private readonly reputation?: IPeerReputation
|
|
55
|
+
private readonly reputation?: IPeerReputation,
|
|
56
|
+
private readonly stateStore?: ITransactionStateStore
|
|
56
57
|
) { }
|
|
57
58
|
|
|
58
59
|
/**
|
|
@@ -156,6 +157,7 @@ export class ClusterCoordinator {
|
|
|
156
157
|
lastUpdate: Date.now()
|
|
157
158
|
};
|
|
158
159
|
this.transactions.set(messageHash, state);
|
|
160
|
+
this.persistCoordinatorState(messageHash, record, 'promising');
|
|
159
161
|
log('cluster-tx:transaction-store', {
|
|
160
162
|
messageHash,
|
|
161
163
|
transactionKeys: Array.from(this.transactions.keys())
|
|
@@ -185,6 +187,7 @@ export class ClusterCoordinator {
|
|
|
185
187
|
// Wait a bit before cleanup to allow any in-flight responses to arrive
|
|
186
188
|
setTimeout(() => {
|
|
187
189
|
this.transactions.delete(messageHash);
|
|
190
|
+
this.deleteCoordinatorState(messageHash);
|
|
188
191
|
log('cluster-tx:transaction-remove', {
|
|
189
192
|
messageHash,
|
|
190
193
|
remaining: Array.from(this.transactions.keys())
|
|
@@ -282,6 +285,7 @@ export class ClusterCoordinator {
|
|
|
282
285
|
});
|
|
283
286
|
}
|
|
284
287
|
|
|
288
|
+
this.persistCoordinatorState(promised.record.messageHash, promised.record, 'committing');
|
|
285
289
|
return await this.commitTransaction(promised.record);
|
|
286
290
|
}
|
|
287
291
|
|
|
@@ -606,6 +610,11 @@ export class ClusterCoordinator {
|
|
|
606
610
|
intervalMs: baseInterval,
|
|
607
611
|
timer
|
|
608
612
|
};
|
|
613
|
+
this.persistCoordinatorState(messageHash, state.record, 'broadcasting', {
|
|
614
|
+
pendingPeers: Array.from(pendingPeers),
|
|
615
|
+
attempt: nextAttempt,
|
|
616
|
+
intervalMs: baseInterval
|
|
617
|
+
});
|
|
609
618
|
log('cluster-tx:retry-scheduled', { messageHash, attempt: nextAttempt, missingPeers, delayMs: baseInterval });
|
|
610
619
|
}
|
|
611
620
|
|
|
@@ -671,10 +680,71 @@ export class ClusterCoordinator {
|
|
|
671
680
|
// Clean up the transaction after retry is complete
|
|
672
681
|
setTimeout(() => {
|
|
673
682
|
this.transactions.delete(messageHash);
|
|
683
|
+
this.deleteCoordinatorState(messageHash);
|
|
674
684
|
log('cluster-tx:transaction-remove', {
|
|
675
685
|
messageHash,
|
|
676
686
|
remaining: Array.from(this.transactions.keys())
|
|
677
687
|
});
|
|
678
688
|
}, 100);
|
|
679
689
|
}
|
|
690
|
+
|
|
691
|
+
/** Fire-and-forget persist — errors are logged, never thrown. */
|
|
692
|
+
private persistCoordinatorState(
|
|
693
|
+
messageHash: string,
|
|
694
|
+
record: ClusterRecord,
|
|
695
|
+
phase: 'promising' | 'committing' | 'broadcasting',
|
|
696
|
+
retryState?: { pendingPeers: string[]; attempt: number; intervalMs: number }
|
|
697
|
+
): void {
|
|
698
|
+
if (!this.stateStore) return;
|
|
699
|
+
this.stateStore.saveCoordinatorState(messageHash, {
|
|
700
|
+
messageHash,
|
|
701
|
+
record,
|
|
702
|
+
lastUpdate: Date.now(),
|
|
703
|
+
phase,
|
|
704
|
+
retryState
|
|
705
|
+
}).catch(err => log('cluster-tx:persist-error', { messageHash, error: (err as Error).message }));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Fire-and-forget delete — errors are logged, never thrown. */
|
|
709
|
+
private deleteCoordinatorState(messageHash: string): void {
|
|
710
|
+
if (!this.stateStore) return;
|
|
711
|
+
this.stateStore.deleteCoordinatorState(messageHash)
|
|
712
|
+
.catch(err => log('cluster-tx:persist-delete-error', { messageHash, error: (err as Error).message }));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Recover coordinator transactions from persistent store after a restart.
|
|
717
|
+
* Called during node startup, before accepting new requests.
|
|
718
|
+
*/
|
|
719
|
+
async recoverTransactions(): Promise<void> {
|
|
720
|
+
if (!this.stateStore) return;
|
|
721
|
+
const states = await this.stateStore.getAllCoordinatorStates();
|
|
722
|
+
for (const state of states) {
|
|
723
|
+
const { messageHash } = state;
|
|
724
|
+
// Expired — clean up
|
|
725
|
+
if (state.record.message.expiration && state.record.message.expiration < Date.now()) {
|
|
726
|
+
log('cluster-tx:recovery-expired', { messageHash });
|
|
727
|
+
await this.stateStore.deleteCoordinatorState(messageHash);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
// Broadcasting phase with retry state — resume retries
|
|
731
|
+
if (state.phase === 'broadcasting' && state.retryState) {
|
|
732
|
+
log('cluster-tx:recovery-resume-broadcast', { messageHash, attempt: state.retryState.attempt });
|
|
733
|
+
const pending = new Pending(Promise.resolve(state.record));
|
|
734
|
+
const txState: ClusterTransactionState = {
|
|
735
|
+
messageHash,
|
|
736
|
+
record: state.record,
|
|
737
|
+
pending,
|
|
738
|
+
lastUpdate: state.lastUpdate
|
|
739
|
+
};
|
|
740
|
+
this.transactions.set(messageHash, txState);
|
|
741
|
+
// Schedule retry from where we left off
|
|
742
|
+
this.scheduleCommitRetry(messageHash, state.record, state.retryState.pendingPeers);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
// Promising or committing — cannot resume (caller context is gone)
|
|
746
|
+
log('cluster-tx:recovery-stale', { messageHash, phase: state.phase });
|
|
747
|
+
await this.stateStore.deleteCoordinatorState(messageHash);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
680
750
|
}
|
|
@@ -7,6 +7,7 @@ import { peerIdFromString } from "@libp2p/peer-id";
|
|
|
7
7
|
import type { FretService } from "p2p-fret";
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
9
|
import type { IPeerReputation } from "../reputation/types.js";
|
|
10
|
+
import type { ITransactionStateStore } from "../cluster/i-transaction-state-store.js";
|
|
10
11
|
|
|
11
12
|
const log = createLogger('coordinator-repo');
|
|
12
13
|
|
|
@@ -40,7 +41,8 @@ export function coordinatorRepo(
|
|
|
40
41
|
createClusterClient: (peerId: PeerId) => ClusterClient,
|
|
41
42
|
cfg?: Partial<ClusterConsensusConfig> & { clusterSize?: number },
|
|
42
43
|
fretService?: FretService,
|
|
43
|
-
reputation?: IPeerReputation
|
|
44
|
+
reputation?: IPeerReputation,
|
|
45
|
+
stateStore?: ITransactionStateStore
|
|
44
46
|
): (components: CoordinatorRepoComponents) => CoordinatorRepo {
|
|
45
47
|
return (components: CoordinatorRepoComponents) => new CoordinatorRepo(
|
|
46
48
|
keyNetwork,
|
|
@@ -51,7 +53,8 @@ export function coordinatorRepo(
|
|
|
51
53
|
components.localPeerId,
|
|
52
54
|
fretService,
|
|
53
55
|
components.clusterLatestCallback,
|
|
54
|
-
reputation
|
|
56
|
+
reputation,
|
|
57
|
+
stateStore
|
|
55
58
|
);
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -72,7 +75,8 @@ export class CoordinatorRepo implements IRepo {
|
|
|
72
75
|
localPeerId?: PeerId,
|
|
73
76
|
fretService?: FretService,
|
|
74
77
|
private readonly clusterLatestCallback?: ClusterLatestCallback,
|
|
75
|
-
reputation?: IPeerReputation
|
|
78
|
+
reputation?: IPeerReputation,
|
|
79
|
+
stateStore?: ITransactionStateStore
|
|
76
80
|
) {
|
|
77
81
|
this.localPeerId = localPeerId;
|
|
78
82
|
const policy: ClusterConsensusConfig & { clusterSize: number } = {
|
|
@@ -89,7 +93,12 @@ export class CoordinatorRepo implements IRepo {
|
|
|
89
93
|
peerId: localPeerId,
|
|
90
94
|
wasTransactionExecuted: localCluster.wasTransactionExecuted?.bind(localCluster)
|
|
91
95
|
} : undefined;
|
|
92
|
-
this.coordinator = new ClusterCoordinator(keyNetwork, createClusterClient, policy, localClusterRef, fretService, reputation);
|
|
96
|
+
this.coordinator = new ClusterCoordinator(keyNetwork, createClusterClient, policy, localClusterRef, fretService, reputation, stateStore);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Recover coordinator transactions from persistent store after a restart. */
|
|
100
|
+
async recoverTransactions(): Promise<void> {
|
|
101
|
+
await this.coordinator.recoverTransactions();
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
/**
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Portable async key-value store. Platform packages provide implementations. */
|
|
2
|
+
export interface IKVStore {
|
|
3
|
+
get(key: string): Promise<string | undefined>;
|
|
4
|
+
set(key: string, value: string): Promise<void>;
|
|
5
|
+
delete(key: string): Promise<void>;
|
|
6
|
+
/** Return all keys matching the given prefix */
|
|
7
|
+
list(prefix: string): Promise<string[]>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IKVStore } from "./i-kv-store.js";
|
|
2
|
+
|
|
3
|
+
/** In-memory IKVStore backed by a Map. Used for testing. */
|
|
4
|
+
export class MemoryKVStore implements IKVStore {
|
|
5
|
+
private readonly store = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
async get(key: string): Promise<string | undefined> {
|
|
8
|
+
return this.store.get(key);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async set(key: string, value: string): Promise<void> {
|
|
12
|
+
this.store.set(key, value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async delete(key: string): Promise<void> {
|
|
16
|
+
this.store.delete(key);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async list(prefix: string): Promise<string[]> {
|
|
20
|
+
const result: string[] = [];
|
|
21
|
+
for (const key of this.store.keys()) {
|
|
22
|
+
if (key.startsWith(prefix)) {
|
|
23
|
+
result.push(key);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
}
|