@optimystic/db-p2p 0.11.2 → 0.12.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.
Files changed (99) hide show
  1. package/dist/src/cluster/block-transfer-service.d.ts +0 -1
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  3. package/dist/src/cluster/block-transfer-service.js +0 -2
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  5. package/dist/src/cluster/cluster-repo.d.ts +1 -2
  6. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-repo.js +5 -5
  8. package/dist/src/cluster/cluster-repo.js.map +1 -1
  9. package/dist/src/cluster/partition-detector.d.ts.map +1 -1
  10. package/dist/src/cluster/partition-detector.js +0 -1
  11. package/dist/src/cluster/partition-detector.js.map +1 -1
  12. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  13. package/dist/src/cluster/service.d.ts +0 -6
  14. package/dist/src/cluster/service.d.ts.map +1 -1
  15. package/dist/src/cluster/service.js +0 -13
  16. package/dist/src/cluster/service.js.map +1 -1
  17. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  18. package/dist/src/dispute/dispute-service.js +1 -1
  19. package/dist/src/dispute/dispute-service.js.map +1 -1
  20. package/dist/src/libp2p-key-network.js.map +1 -1
  21. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  22. package/dist/src/libp2p-node-base.js +14 -1
  23. package/dist/src/libp2p-node-base.js.map +1 -1
  24. package/dist/src/network/network-manager-service.js +1 -1
  25. package/dist/src/network/network-manager-service.js.map +1 -1
  26. package/dist/src/repo/cluster-coordinator.d.ts +1 -1
  27. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  28. package/dist/src/repo/cluster-coordinator.js +3 -3
  29. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  30. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  31. package/dist/src/repo/coordinator-repo.js +48 -17
  32. package/dist/src/repo/coordinator-repo.js.map +1 -1
  33. package/dist/src/repo/service.d.ts +0 -1
  34. package/dist/src/repo/service.d.ts.map +1 -1
  35. package/dist/src/repo/service.js +0 -2
  36. package/dist/src/repo/service.js.map +1 -1
  37. package/dist/src/routing/simple-cluster-coordinator.d.ts +2 -2
  38. package/dist/src/routing/simple-cluster-coordinator.d.ts.map +1 -1
  39. package/dist/src/routing/simple-cluster-coordinator.js +2 -2
  40. package/dist/src/routing/simple-cluster-coordinator.js.map +1 -1
  41. package/dist/src/storage/block-storage.d.ts +4 -0
  42. package/dist/src/storage/block-storage.d.ts.map +1 -1
  43. package/dist/src/storage/block-storage.js +29 -0
  44. package/dist/src/storage/block-storage.js.map +1 -1
  45. package/dist/src/storage/i-block-storage.d.ts +17 -0
  46. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  47. package/dist/src/storage/i-raw-storage.d.ts +9 -0
  48. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  49. package/dist/src/storage/memory-storage.d.ts +18 -0
  50. package/dist/src/storage/memory-storage.d.ts.map +1 -1
  51. package/dist/src/storage/memory-storage.js +47 -2
  52. package/dist/src/storage/memory-storage.js.map +1 -1
  53. package/dist/src/storage/ring-selector.d.ts.map +1 -1
  54. package/dist/src/storage/ring-selector.js +23 -5
  55. package/dist/src/storage/ring-selector.js.map +1 -1
  56. package/dist/src/storage/storage-monitor.d.ts.map +1 -1
  57. package/dist/src/storage/storage-monitor.js +10 -14
  58. package/dist/src/storage/storage-monitor.js.map +1 -1
  59. package/dist/src/storage/storage-repo.d.ts +10 -2
  60. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  61. package/dist/src/storage/storage-repo.js +39 -10
  62. package/dist/src/storage/storage-repo.js.map +1 -1
  63. package/dist/src/sync/service.d.ts +0 -1
  64. package/dist/src/sync/service.d.ts.map +1 -1
  65. package/dist/src/sync/service.js +0 -2
  66. package/dist/src/sync/service.js.map +1 -1
  67. package/dist/src/testing/index.d.ts +2 -0
  68. package/dist/src/testing/index.d.ts.map +1 -0
  69. package/dist/src/testing/index.js +2 -0
  70. package/dist/src/testing/index.js.map +1 -0
  71. package/dist/src/testing/mesh-harness.d.ts +60 -0
  72. package/dist/src/testing/mesh-harness.d.ts.map +1 -0
  73. package/dist/src/testing/mesh-harness.js +220 -0
  74. package/dist/src/testing/mesh-harness.js.map +1 -0
  75. package/package.json +8 -3
  76. package/src/cluster/block-transfer-service.ts +1 -1
  77. package/src/cluster/cluster-repo.ts +5 -3
  78. package/src/cluster/partition-detector.ts +0 -2
  79. package/src/cluster/rebalance-monitor.ts +1 -1
  80. package/src/cluster/service.ts +0 -13
  81. package/src/dispute/dispute-service.ts +1 -2
  82. package/src/dispute/service.ts +1 -1
  83. package/src/libp2p-key-network.ts +1 -1
  84. package/src/libp2p-node-base.ts +13 -3
  85. package/src/network/network-manager-service.ts +1 -1
  86. package/src/repo/cluster-coordinator.ts +4 -4
  87. package/src/repo/coordinator-repo.ts +51 -19
  88. package/src/repo/service.ts +0 -2
  89. package/src/routing/simple-cluster-coordinator.ts +2 -2
  90. package/src/storage/block-storage.ts +32 -0
  91. package/src/storage/i-block-storage.ts +15 -0
  92. package/src/storage/i-raw-storage.ts +10 -0
  93. package/src/storage/memory-storage.ts +48 -2
  94. package/src/storage/ring-selector.ts +26 -5
  95. package/src/storage/storage-monitor.ts +10 -17
  96. package/src/storage/storage-repo.ts +40 -11
  97. package/src/sync/service.ts +2 -3
  98. package/src/testing/index.ts +1 -0
  99. package/src/testing/mesh-harness.ts +294 -0
@@ -0,0 +1,294 @@
1
+ import type { PeerId, PrivateKey } from '@libp2p/interface';
2
+ import type { IKeyNetwork, ClusterPeers, ICluster, ClusterRecord, IRepo, BlockId, ActionRev, ClusterConsensusConfig, ITransactor, PeerId as DbPeerId } from '@optimystic/db-core';
3
+ import type { FindCoordinatorOptions } from '@optimystic/db-core';
4
+ import type { IPeerNetwork } from '@optimystic/db-core';
5
+ import { NetworkTransactor } from '@optimystic/db-core';
6
+ import { peerIdFromPrivateKey } from '@libp2p/peer-id';
7
+ import { generateKeyPair } from '@libp2p/crypto/keys';
8
+ import { ClusterMember, clusterMember } from '../cluster/cluster-repo.js';
9
+ import { StorageRepo } from '../storage/storage-repo.js';
10
+ import { MemoryRawStorage } from '../storage/memory-storage.js';
11
+ import { BlockStorage } from '../storage/block-storage.js';
12
+ import type { IRawStorage } from '../storage/i-raw-storage.js';
13
+ import { coordinatorRepo, type ClusterLatestCallback } from '../repo/coordinator-repo.js';
14
+ import type { CoordinatorRepo } from '../repo/coordinator-repo.js';
15
+ import { sortPeersByDistance, type KnownPeer } from '../routing/responsibility.js';
16
+ import { toString as u8ToString } from 'uint8arrays';
17
+
18
+ export interface MeshNode {
19
+ peerId: PeerId;
20
+ privateKey: PrivateKey;
21
+ storageRepo: StorageRepo;
22
+ clusterMember: ClusterMember;
23
+ coordinatorRepo: CoordinatorRepo;
24
+ }
25
+
26
+ export interface MeshOptions {
27
+ responsibilityK: number;
28
+ clusterSize?: number;
29
+ superMajorityThreshold?: number;
30
+ allowClusterDownsize?: boolean;
31
+ /**
32
+ * Optional per-node raw-storage factory. Invoked once per node (indexed from 0)
33
+ * to supply the IRawStorage that backs StorageRepo. If omitted, each node gets
34
+ * a fresh `MemoryRawStorage`. Used by fault-injection tests to wrap the store
35
+ * with a crashing proxy, or by restart tests to rebuild over preserved state.
36
+ */
37
+ rawStorageFactory?: (index: number) => IRawStorage;
38
+ }
39
+
40
+ export interface MeshFailureConfig {
41
+ /** Peers that should fail on cluster update (simulate unreachable) */
42
+ failingPeers?: Set<string>;
43
+ /** Make findCluster return empty (simulate DHT failure) */
44
+ findClusterFails?: boolean;
45
+ }
46
+
47
+ class MockPeerNetwork implements IPeerNetwork {
48
+ async connect(_peerId: PeerId, _protocol: string): Promise<any> {
49
+ return {};
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Mock IKeyNetwork that returns peers based on XOR distance.
55
+ * With responsibilityK >= nodeCount, all nodes are returned.
56
+ * Otherwise, K-nearest by XOR distance are returned.
57
+ */
58
+ class MockMeshKeyNetwork implements IKeyNetwork {
59
+ constructor(
60
+ private readonly nodes: MeshNode[],
61
+ private readonly responsibilityK: number,
62
+ private readonly failures: MeshFailureConfig = {}
63
+ ) {}
64
+
65
+ async findCoordinator(key: Uint8Array, options?: Partial<FindCoordinatorOptions>): Promise<PeerId> {
66
+ const excluded = new Set((options?.excludedPeers ?? []).map(p => p.toString()));
67
+ const sorted = this.sortedByDistance(key);
68
+ const pick = sorted.find(n => !excluded.has(n.peerId.toString()));
69
+ if (!pick) {
70
+ throw new Error('No coordinator available for key (all candidates excluded)');
71
+ }
72
+ return pick.peerId;
73
+ }
74
+
75
+ async findCluster(key: Uint8Array): Promise<ClusterPeers> {
76
+ if (this.failures.findClusterFails) {
77
+ return {} as ClusterPeers;
78
+ }
79
+
80
+ const sorted = this.sortedByDistance(key);
81
+ const k = Math.min(this.responsibilityK, sorted.length);
82
+ const selected = sorted.slice(0, k);
83
+
84
+ const peers: ClusterPeers = {};
85
+ for (const node of selected) {
86
+ peers[node.peerId.toString()] = {
87
+ multiaddrs: ['/ip4/127.0.0.1/tcp/8000'],
88
+ publicKey: u8ToString(node.peerId.publicKey!.raw, 'base64url')
89
+ };
90
+ }
91
+ return peers;
92
+ }
93
+
94
+ private sortedByDistance(key: Uint8Array): MeshNode[] {
95
+ const knownPeers: KnownPeer[] = this.nodes.map(n => ({
96
+ id: n.peerId,
97
+ addrs: ['/ip4/127.0.0.1/tcp/8000']
98
+ }));
99
+ const sorted = sortPeersByDistance(knownPeers, key);
100
+ return sorted.map(kp => this.nodes.find(n => n.peerId.equals(kp.id))!);
101
+ }
102
+ }
103
+
104
+ export interface Mesh {
105
+ nodes: MeshNode[];
106
+ failures: MeshFailureConfig;
107
+ keyNetwork: IKeyNetwork;
108
+ }
109
+
110
+ /**
111
+ * Creates N interconnected mesh nodes with real components and mock transport.
112
+ * ClusterClient calls route directly to target ClusterMember instances.
113
+ */
114
+ export async function createMesh(nodeCount: number, options: MeshOptions): Promise<Mesh> {
115
+ const failures: MeshFailureConfig = {};
116
+
117
+ // Generate key pairs for all nodes
118
+ const keyPairs = await Promise.all(
119
+ Array.from({ length: nodeCount }, async () => {
120
+ const privateKey = await generateKeyPair('Ed25519');
121
+ return { peerId: peerIdFromPrivateKey(privateKey), privateKey };
122
+ })
123
+ );
124
+
125
+ // Build nodes array (partially — coordinatorRepo added after keyNetwork is ready)
126
+ const nodes: MeshNode[] = [];
127
+ const peerNetwork = new MockPeerNetwork();
128
+ // Map peerId → rawStorage for data sync simulation in clusterLatestCallback
129
+ const rawStorages = new Map<string, IRawStorage>();
130
+
131
+ // Phase 1: create storage + cluster members
132
+ let nodeIndex = 0;
133
+ for (const { peerId, privateKey } of keyPairs) {
134
+ const rawStorage = options.rawStorageFactory
135
+ ? options.rawStorageFactory(nodeIndex)
136
+ : new MemoryRawStorage();
137
+ rawStorages.set(peerId.toString(), rawStorage);
138
+ nodeIndex++;
139
+ const storageRepo = new StorageRepo(
140
+ (blockId: BlockId) => new BlockStorage(blockId, rawStorage)
141
+ );
142
+
143
+ const consensusConfig: ClusterConsensusConfig = {
144
+ superMajorityThreshold: options.superMajorityThreshold ?? 0.75,
145
+ simpleMajorityThreshold: 0.51,
146
+ minAbsoluteClusterSize: 2,
147
+ allowClusterDownsize: options.allowClusterDownsize ?? true,
148
+ clusterSizeTolerance: 0.5,
149
+ partitionDetectionWindow: 60000
150
+ };
151
+
152
+ const member = clusterMember({
153
+ storageRepo,
154
+ peerNetwork,
155
+ peerId,
156
+ privateKey,
157
+ consensusConfig
158
+ });
159
+
160
+ nodes.push({
161
+ peerId,
162
+ privateKey,
163
+ storageRepo,
164
+ clusterMember: member,
165
+ coordinatorRepo: undefined as any // filled in phase 2
166
+ });
167
+ }
168
+
169
+ // Phase 2: create key network and coordinator repos (needs all nodes for routing)
170
+ const keyNetwork = new MockMeshKeyNetwork(nodes, options.responsibilityK, failures);
171
+
172
+ const createClusterClient = (targetPeerId: PeerId): ICluster => {
173
+ const target = nodes.find(n => n.peerId.equals(targetPeerId));
174
+ if (!target) {
175
+ throw new Error(`Unknown peer: ${targetPeerId.toString()}`);
176
+ }
177
+ return {
178
+ async update(record: ClusterRecord): Promise<ClusterRecord> {
179
+ if (failures.failingPeers?.has(targetPeerId.toString())) {
180
+ throw new Error(`Peer ${targetPeerId.toString()} is unreachable`);
181
+ }
182
+ return target.clusterMember.update(record);
183
+ }
184
+ };
185
+ };
186
+
187
+ for (const node of nodes) {
188
+ const localRawStorage = rawStorages.get(node.peerId.toString())!;
189
+
190
+ // Per-node callback: queries remote peer and replicates committed data locally
191
+ // (simulates what SyncClient does in production)
192
+ const clusterLatestCallback: ClusterLatestCallback = async (peerId: PeerId, blockId: BlockId, context?): Promise<ActionRev | undefined> => {
193
+ const target = nodes.find(n => n.peerId.equals(peerId));
194
+ if (!target) return undefined;
195
+ const result = await target.storageRepo.get(
196
+ { blockIds: [blockId], context },
197
+ { skipClusterFetch: true } as any
198
+ );
199
+ const entry = result[blockId];
200
+ const latest = entry?.state?.latest;
201
+
202
+ // Simulate data sync: replicate committed block data to local storage
203
+ if (latest && entry?.block) {
204
+ const localBlockStorage = new BlockStorage(blockId as BlockId, localRawStorage);
205
+ const localLatest = await localBlockStorage.getLatest();
206
+ if (!localLatest || localLatest.rev < latest.rev) {
207
+ // Ensure metadata exists
208
+ const meta = await localRawStorage.getMetadata(blockId as BlockId);
209
+ if (!meta) {
210
+ await localRawStorage.saveMetadata(blockId as BlockId, { latest: undefined, ranges: [[0]] });
211
+ }
212
+ await localBlockStorage.saveMaterializedBlock(latest.actionId, entry.block);
213
+ await localBlockStorage.saveRevision(latest.rev, latest.actionId);
214
+ await localBlockStorage.setLatest(latest);
215
+ }
216
+ }
217
+
218
+ return latest;
219
+ };
220
+ // Wrap key network to include self in findCluster (matches real Libp2pKeyPeerNetwork behavior)
221
+ const nodeKeyNetwork: IKeyNetwork = {
222
+ findCoordinator: (key, opts) => keyNetwork.findCoordinator(key, opts),
223
+ async findCluster(key) {
224
+ const peers = await keyNetwork.findCluster(key);
225
+ const selfStr = node.peerId.toString();
226
+ if (!(selfStr in peers)) {
227
+ peers[selfStr] = {
228
+ multiaddrs: ['/ip4/127.0.0.1/tcp/8000'],
229
+ publicKey: u8ToString(node.peerId.publicKey!.raw, 'base64url')
230
+ };
231
+ }
232
+ return peers;
233
+ }
234
+ };
235
+ const factory = coordinatorRepo(
236
+ nodeKeyNetwork,
237
+ (peerId: PeerId) => createClusterClient(peerId) as any,
238
+ {
239
+ clusterSize: options.clusterSize ?? nodeCount,
240
+ superMajorityThreshold: options.superMajorityThreshold ?? 0.75,
241
+ allowClusterDownsize: options.allowClusterDownsize ?? true
242
+ }
243
+ );
244
+ node.coordinatorRepo = factory({
245
+ storageRepo: node.storageRepo,
246
+ localCluster: node.clusterMember,
247
+ localPeerId: node.peerId,
248
+ clusterLatestCallback
249
+ });
250
+ }
251
+
252
+ return { nodes, failures, keyNetwork };
253
+ }
254
+
255
+ export interface BuildTransactorOptions {
256
+ timeoutMs?: number;
257
+ abortOrCancelTimeoutMs?: number;
258
+ }
259
+
260
+ /**
261
+ * Builds a NetworkTransactor over a mesh. All nodes share the same mock
262
+ * infrastructure so a single transactor routes to every peer via `getRepo`.
263
+ * Suitable for solo-mesh tests; for multi-node tests prefer
264
+ * `buildNetworkTransactors` to label "which node is driving".
265
+ */
266
+ export const buildNetworkTransactor = (mesh: Mesh, options: BuildTransactorOptions = {}): ITransactor => {
267
+ const repoByPeer = new Map<string, IRepo>();
268
+ for (const node of mesh.nodes) {
269
+ repoByPeer.set(node.peerId.toString(), node.coordinatorRepo as unknown as IRepo);
270
+ }
271
+ return new NetworkTransactor({
272
+ timeoutMs: options.timeoutMs ?? 5_000,
273
+ abortOrCancelTimeoutMs: options.abortOrCancelTimeoutMs ?? 5_000,
274
+ keyNetwork: mesh.keyNetwork,
275
+ getRepo: (peerId: DbPeerId) => {
276
+ const repo = repoByPeer.get(peerId.toString());
277
+ if (!repo) throw new Error(`Unknown peer ${peerId.toString()}`);
278
+ return repo;
279
+ }
280
+ });
281
+ };
282
+
283
+ /**
284
+ * Builds one NetworkTransactor per mesh node, keyed by peer-id string. Each
285
+ * transactor shares the mesh's key network and peer→repo map — the separate
286
+ * instances exist so tests can semantically say "driven by node A".
287
+ */
288
+ export const buildNetworkTransactors = (mesh: Mesh, options: BuildTransactorOptions = {}): Map<string, ITransactor> => {
289
+ const transactors = new Map<string, ITransactor>();
290
+ for (const node of mesh.nodes) {
291
+ transactors.set(node.peerId.toString(), buildNetworkTransactor(mesh, options));
292
+ }
293
+ return transactors;
294
+ };