@optimystic/db-p2p 0.1.3 → 0.2.1

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 (41) hide show
  1. package/README.md +44 -0
  2. package/dist/index.min.js +17 -17
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/cluster/client.d.ts +1 -2
  5. package/dist/src/cluster/client.d.ts.map +1 -1
  6. package/dist/src/cluster/client.js.map +1 -1
  7. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  8. package/dist/src/libp2p-key-network.js +4 -4
  9. package/dist/src/libp2p-key-network.js.map +1 -1
  10. package/dist/src/libp2p-node-base.d.ts +53 -0
  11. package/dist/src/libp2p-node-base.d.ts.map +1 -0
  12. package/dist/src/libp2p-node-base.js +291 -0
  13. package/dist/src/libp2p-node-base.js.map +1 -0
  14. package/dist/src/libp2p-node-rn.d.ts +11 -0
  15. package/dist/src/libp2p-node-rn.d.ts.map +1 -0
  16. package/dist/src/libp2p-node-rn.js +19 -0
  17. package/dist/src/libp2p-node-rn.js.map +1 -0
  18. package/dist/src/libp2p-node.d.ts +3 -37
  19. package/dist/src/libp2p-node.d.ts.map +1 -1
  20. package/dist/src/libp2p-node.js +6 -287
  21. package/dist/src/libp2p-node.js.map +1 -1
  22. package/dist/src/protocol-client.d.ts +1 -2
  23. package/dist/src/protocol-client.d.ts.map +1 -1
  24. package/dist/src/protocol-client.js.map +1 -1
  25. package/dist/src/repo/client.d.ts +1 -2
  26. package/dist/src/repo/client.d.ts.map +1 -1
  27. package/dist/src/repo/client.js.map +1 -1
  28. package/dist/src/rn.d.ts +29 -0
  29. package/dist/src/rn.d.ts.map +1 -0
  30. package/dist/src/rn.js +29 -0
  31. package/dist/src/rn.js.map +1 -0
  32. package/package.json +6 -2
  33. package/src/cluster/client.ts +1 -2
  34. package/src/index.ts +0 -2
  35. package/src/libp2p-key-network.ts +5 -6
  36. package/src/libp2p-node-base.ts +386 -0
  37. package/src/libp2p-node-rn.ts +30 -0
  38. package/src/libp2p-node.ts +14 -364
  39. package/src/protocol-client.ts +3 -3
  40. package/src/repo/client.ts +1 -2
  41. package/src/rn.ts +28 -0
@@ -0,0 +1,386 @@
1
+ import { createLibp2p, type Libp2p } from 'libp2p';
2
+ import { noise } from '@chainsafe/libp2p-noise';
3
+ import { yamux } from '@chainsafe/libp2p-yamux';
4
+ import { identify } from '@libp2p/identify';
5
+ import { ping } from '@libp2p/ping';
6
+ import { gossipsub } from '@chainsafe/libp2p-gossipsub';
7
+ import { bootstrap } from '@libp2p/bootstrap';
8
+ import { circuitRelayServer } from '@libp2p/circuit-relay-v2';
9
+ import { peerIdFromString } from '@libp2p/peer-id';
10
+ import { clusterService } from './cluster/service.js';
11
+ import { repoService } from './repo/service.js';
12
+ import { StorageRepo } from './storage/storage-repo.js';
13
+ import { BlockStorage } from './storage/block-storage.js';
14
+ import { MemoryRawStorage } from './storage/memory-storage.js';
15
+ import type { IRawStorage } from './storage/i-raw-storage.js';
16
+ import { clusterMember } from './cluster/cluster-repo.js';
17
+ import { coordinatorRepo } from './repo/coordinator-repo.js';
18
+ import { Libp2pKeyPeerNetwork } from './libp2p-key-network.js';
19
+ import { ClusterClient } from './cluster/client.js';
20
+ import type { IRepo, ICluster, ITransactionValidator } from '@optimystic/db-core';
21
+ import { networkManagerService } from './network/network-manager-service.js';
22
+ import { fretService, Libp2pFretService } from 'p2p-fret';
23
+ import { syncService } from './sync/service.js';
24
+ import { SyncClient } from './sync/client.js';
25
+ import type { ClusterLatestCallback } from './repo/coordinator-repo.js';
26
+ import { RestorationCoordinator } from './storage/restoration-coordinator-v2.js';
27
+ import { RingSelector } from './storage/ring-selector.js';
28
+ import { StorageMonitor } from './storage/storage-monitor.js';
29
+ import type { StorageMonitorConfig } from './storage/storage-monitor.js';
30
+ import { ArachnodeFretAdapter } from './storage/arachnode-fret-adapter.js';
31
+ import type { RestoreCallback } from './storage/struct.js';
32
+ import type { FretService } from 'p2p-fret';
33
+ import { PartitionDetector } from './cluster/partition-detector.js';
34
+
35
+ type Libp2pInit = NonNullable<Parameters<typeof createLibp2p>[0]>;
36
+ export type Libp2pTransports = NonNullable<Libp2pInit['transports']>;
37
+
38
+ /** Factory function or instance for creating raw storage */
39
+ export type RawStorageProvider = IRawStorage | (() => IRawStorage);
40
+
41
+ export type NodeOptions = {
42
+ /**
43
+ * Network port. Only used by the default `listenAddrs` fallback.
44
+ * For non-TCP transports (e.g. WebSockets), set `listenAddrs` explicitly.
45
+ */
46
+ port?: number;
47
+ bootstrapNodes: string[];
48
+ networkName: string;
49
+ fretProfile?: 'edge' | 'core';
50
+ id?: string; // optional peer id
51
+ relay?: boolean; // enable relay service
52
+ /** Storage provider - either an IRawStorage instance or a factory function. Defaults to MemoryRawStorage if not provided. */
53
+ storage?: RawStorageProvider;
54
+ clusterSize?: number; // desired cluster size per key
55
+ clusterPolicy?: {
56
+ allowDownsize?: boolean;
57
+ sizeTolerance?: number; // acceptable relative difference (e.g. 0.5 = +/-50%)
58
+ superMajorityThreshold?: number; // fraction of peers needed for super-majority (default: 0.67)
59
+ };
60
+
61
+ /** Override libp2p listen multiaddrs. */
62
+ listenAddrs?: string[];
63
+ /** Override libp2p transports. */
64
+ transports?: Libp2pTransports;
65
+
66
+ /**
67
+ * Responsibility K - the replica set size for determining cluster membership.
68
+ * This is distinct from kBucketSize (DHT routing) and clusterSize (consensus quorum).
69
+ * When a node receives a request, it checks if it's in the top responsibilityK
70
+ * peers (by XOR distance) for the key. If not, it redirects to closer peers.
71
+ * Default: 1 (only the closest peer is responsible)
72
+ */
73
+ responsibilityK?: number;
74
+
75
+ /** Arachnode storage configuration */
76
+ arachnode?: {
77
+ enableRingZulu?: boolean; // default: true
78
+ storage?: StorageMonitorConfig;
79
+ };
80
+
81
+ /** Transaction validator for cluster consensus */
82
+ validator?: ITransactionValidator;
83
+ };
84
+
85
+ function resolveStorage(provider: RawStorageProvider | undefined): IRawStorage {
86
+ if (!provider) {
87
+ return new MemoryRawStorage();
88
+ }
89
+ return typeof provider === 'function' ? provider() : provider;
90
+ }
91
+
92
+ export async function createLibp2pNodeBase(
93
+ options: NodeOptions,
94
+ defaults: {
95
+ listenAddrs: string[];
96
+ transports: Libp2pTransports;
97
+ }
98
+ ): Promise<Libp2p> {
99
+ const rawStorage = resolveStorage(options.storage);
100
+
101
+ // Create placeholder restore callback (will be replaced after node starts)
102
+ let restoreCallback: RestoreCallback = async (_blockId, _rev?) => {
103
+ return undefined;
104
+ };
105
+
106
+ // Create shared storage layers with restoration callback
107
+ const storageRepo = new StorageRepo((blockId) =>
108
+ new BlockStorage(blockId, rawStorage, restoreCallback)
109
+ );
110
+
111
+ let clusterImpl: ICluster | undefined;
112
+ let coordinatedRepo: IRepo | undefined;
113
+
114
+ const clusterProxy: ICluster = {
115
+ async update(record) {
116
+ if (!clusterImpl) {
117
+ throw new Error('ClusterMember not initialized');
118
+ }
119
+ return await clusterImpl.update(record);
120
+ }
121
+ };
122
+
123
+ const repoProxy: IRepo = {
124
+ async get(blockGets, options) {
125
+ const target = coordinatedRepo ?? storageRepo;
126
+ return await target.get(blockGets, options);
127
+ },
128
+ async pend(request, options) {
129
+ const target = coordinatedRepo ?? storageRepo;
130
+ return await target.pend(request, options);
131
+ },
132
+ async cancel(trxRef, options) {
133
+ const target = coordinatedRepo ?? storageRepo;
134
+ return await target.cancel(trxRef, options);
135
+ },
136
+ async commit(request, options) {
137
+ const target = coordinatedRepo ?? storageRepo;
138
+ return await target.commit(request, options);
139
+ }
140
+ };
141
+
142
+ // Parse peer ID if provided
143
+ const peerId = options.id ? await peerIdFromString(options.id) : undefined;
144
+
145
+ const listenAddrs = options.listenAddrs ?? defaults.listenAddrs;
146
+ const transports = options.transports ?? defaults.transports;
147
+
148
+ const libp2pOptions: unknown = {
149
+ start: false,
150
+ ...(peerId ? { peerId } : {}),
151
+ addresses: {
152
+ listen: listenAddrs
153
+ },
154
+ connectionManager: {
155
+ autoDial: true,
156
+ minConnections: 1,
157
+ maxConnections: 16,
158
+ inboundConnectionUpgradeTimeout: 10_000,
159
+ dialQueue: { concurrency: 2, attempts: 2 }
160
+ },
161
+ transports,
162
+ connectionEncrypters: [noise()],
163
+ streamMuxers: [yamux()],
164
+ services: {
165
+ identify: identify({
166
+ protocolPrefix: `/optimystic/${options.networkName}`
167
+ }),
168
+ ping: ping(),
169
+ pubsub: gossipsub({
170
+ allowPublishToZeroTopicPeers: true,
171
+ heartbeatInterval: 7000
172
+ }),
173
+ // Circuit relay server - enables this node to relay connections for other peers
174
+ ...(options.relay ? { relay: circuitRelayServer() } : {}),
175
+
176
+ // Custom services - create wrapper factories that inject dependencies
177
+ cluster: (components: any) => {
178
+ const serviceFactory = clusterService({
179
+ protocolPrefix: `/optimystic/${options.networkName}`,
180
+ configuredClusterSize: options.clusterSize ?? 10,
181
+ allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
182
+ clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
183
+ responsibilityK: options.responsibilityK ?? 1
184
+ });
185
+ return serviceFactory({
186
+ logger: components.logger,
187
+ registrar: components.registrar,
188
+ cluster: clusterProxy
189
+ });
190
+ },
191
+
192
+ repo: (components: any) => {
193
+ const serviceFactory = repoService({
194
+ protocolPrefix: `/optimystic/${options.networkName}`,
195
+ responsibilityK: options.responsibilityK ?? 1
196
+ });
197
+ return serviceFactory({
198
+ logger: components.logger,
199
+ registrar: components.registrar,
200
+ repo: repoProxy
201
+ });
202
+ },
203
+
204
+ sync: (components: any) => {
205
+ const serviceFactory = syncService({
206
+ protocolPrefix: `/optimystic/${options.networkName}`
207
+ });
208
+ return serviceFactory({
209
+ logger: components.logger,
210
+ registrar: components.registrar,
211
+ repo: repoProxy
212
+ });
213
+ },
214
+
215
+ networkManager: (components: any) => {
216
+ const svcFactory = networkManagerService({
217
+ clusterSize: options.clusterSize ?? 10,
218
+ expectedRemotes: (options.bootstrapNodes?.length ?? 0) > 0,
219
+ allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
220
+ clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5
221
+ });
222
+ const svc = svcFactory(components);
223
+ try { (svc as any).setLibp2p?.(components.libp2p); } catch { }
224
+ return svc;
225
+ },
226
+ fret: (components: any) => {
227
+ const svcFactory = fretService({
228
+ k: 15,
229
+ m: 8,
230
+ capacity: 2048,
231
+ profile: options.fretProfile ?? ((options.bootstrapNodes?.length ?? 0) > 0 ? 'core' : 'edge'),
232
+ networkName: options.networkName,
233
+ bootstraps: options.bootstrapNodes ?? []
234
+ });
235
+ const svc = svcFactory(components) as Libp2pFretService;
236
+ try { svc.setLibp2p(components.libp2p); } catch { }
237
+ return svc;
238
+ }
239
+ },
240
+ // Add bootstrap nodes as needed
241
+ peerDiscovery: [
242
+ ...(options.bootstrapNodes?.length ? [bootstrap({ list: options.bootstrapNodes })] : [])
243
+ ],
244
+ };
245
+
246
+ const node = await createLibp2p(libp2pOptions as any);
247
+
248
+ // Inject libp2p reference into services that need it before start
249
+ try { ((node as any).services?.fret as any)?.setLibp2p?.(node); } catch { }
250
+ try { ((node as any).services?.networkManager as any)?.setLibp2p?.(node); } catch { }
251
+
252
+ await node.start();
253
+
254
+ // Initialize cluster coordination components
255
+ const keyNetwork = new Libp2pKeyPeerNetwork(node);
256
+ const protocolPrefix = `/optimystic/${options.networkName}`;
257
+ const createClusterClient = (peerId: any) => ClusterClient.create(peerId, keyNetwork, protocolPrefix);
258
+
259
+ // Create partition detector and get FRET service
260
+ const partitionDetector = new PartitionDetector();
261
+ const fretSvc = (node as any).services?.fret as FretService | undefined;
262
+
263
+ clusterImpl = clusterMember({
264
+ storageRepo,
265
+ peerNetwork: keyNetwork,
266
+ peerId: node.peerId,
267
+ protocolPrefix,
268
+ partitionDetector,
269
+ fretService: fretSvc,
270
+ validator: options.validator
271
+ });
272
+
273
+ const coordinatorRepoFactory = coordinatorRepo(
274
+ keyNetwork,
275
+ createClusterClient,
276
+ {
277
+ clusterSize: options.clusterSize ?? 10,
278
+ superMajorityThreshold: options.clusterPolicy?.superMajorityThreshold ?? 0.67,
279
+ simpleMajorityThreshold: 0.51,
280
+ minAbsoluteClusterSize: 2,
281
+ allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
282
+ clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
283
+ partitionDetectionWindow: 60000
284
+ },
285
+ fretSvc
286
+ );
287
+
288
+ // Create callback for querying cluster peers for their latest block revision
289
+ const clusterLatestCallback: ClusterLatestCallback = async (peerId, blockId) => {
290
+ const syncClient = new SyncClient(peerId, keyNetwork, protocolPrefix);
291
+ try {
292
+ const response = await syncClient.requestBlock({ blockId, rev: undefined });
293
+ if (response.success && response.archive) {
294
+ const revisions = Object.keys(response.archive.revisions).map(Number);
295
+ if (revisions.length > 0) {
296
+ const maxRev = Math.max(...revisions);
297
+ const revisionData = response.archive.revisions[maxRev];
298
+ if (revisionData?.action) {
299
+ return { actionId: revisionData.action.actionId, rev: maxRev };
300
+ }
301
+ }
302
+ }
303
+ } catch {
304
+ // Peer may be unreachable - return undefined to skip this peer
305
+ }
306
+ return undefined;
307
+ };
308
+
309
+ coordinatedRepo = coordinatorRepoFactory({
310
+ storageRepo,
311
+ localCluster: clusterImpl,
312
+ localPeerId: node.peerId,
313
+ clusterLatestCallback
314
+ });
315
+
316
+ // Initialize Arachnode ring membership and restoration
317
+ const enableArachnode = options.arachnode?.enableRingZulu ?? true;
318
+ if (enableArachnode) {
319
+ const log = (node as any).logger?.forComponent?.('db-p2p:arachnode');
320
+ const fret = (node as any).services?.fret as any;
321
+
322
+ if (fret) {
323
+ const fretAdapter = new ArachnodeFretAdapter(fret);
324
+
325
+ const storageMonitor = new StorageMonitor(rawStorage, options.arachnode?.storage ?? {});
326
+ const ringSelector = new RingSelector(fretAdapter, storageMonitor, {
327
+ minCapacity: 100 * 1024 * 1024,
328
+ thresholds: {
329
+ moveOut: 0.85,
330
+ moveIn: 0.40
331
+ }
332
+ });
333
+
334
+ // Determine and announce ring membership
335
+ const peerId = node.peerId.toString();
336
+ const arachnodeInfo = await ringSelector.createArachnodeInfo(peerId);
337
+ fretAdapter.setArachnodeInfo(arachnodeInfo);
338
+
339
+ log?.('Announced Arachnode membership: Ring %d', arachnodeInfo.ringDepth);
340
+
341
+ // Setup restoration coordinator with FRET adapter
342
+ const restorationCoordinatorV2 = new RestorationCoordinator(
343
+ fretAdapter,
344
+ { connect: (pid, protocol) => node.dialProtocol(pid as Parameters<typeof node.dialProtocol>[0], [protocol]) },
345
+ `/optimystic/${options.networkName}`
346
+ );
347
+
348
+ // Update restore callback to use new coordinator
349
+ const newRestoreCallback: RestoreCallback = async (blockId, rev?) => {
350
+ return await restorationCoordinatorV2.restore(blockId, rev);
351
+ };
352
+
353
+ // Replace the restore callback (this is a bit hacky, but works for now)
354
+ (storageRepo as any).createBlockStorage = (blockId: string) =>
355
+ new BlockStorage(blockId, rawStorage, newRestoreCallback);
356
+
357
+ // Monitor capacity and adjust ring periodically
358
+ const monitorInterval = setInterval(async () => {
359
+ const transition = await ringSelector.shouldTransition();
360
+ if (transition.shouldMove) {
361
+ log?.('Ring transition needed: moving %s to Ring %d', transition.direction, transition.newRingDepth);
362
+
363
+ // Update Arachnode info with new ring
364
+ const updatedInfo = await ringSelector.createArachnodeInfo(peerId);
365
+ fretAdapter.setArachnodeInfo(updatedInfo);
366
+ }
367
+ }, 60_000);
368
+
369
+ // Cleanup on node stop
370
+ const originalStop = node.stop.bind(node);
371
+ node.stop = async () => {
372
+ clearInterval(monitorInterval);
373
+ await originalStop();
374
+ };
375
+ } else {
376
+ log?.('FRET service not available, Arachnode disabled');
377
+ }
378
+ }
379
+
380
+ // Expose coordinated repo and storage for external use
381
+ (node as any).coordinatedRepo = coordinatedRepo;
382
+ (node as any).storageRepo = storageRepo;
383
+ (node as any).keyNetwork = keyNetwork;
384
+
385
+ return node;
386
+ }
@@ -0,0 +1,30 @@
1
+ import type { Libp2p } from 'libp2p';
2
+ import {
3
+ createLibp2pNodeBase,
4
+ type Libp2pTransports,
5
+ type NodeOptions,
6
+ type RawStorageProvider,
7
+ } from './libp2p-node-base.js';
8
+
9
+ export type { Libp2pTransports, NodeOptions, RawStorageProvider };
10
+
11
+ /**
12
+ * React Native-friendly libp2p node factory.
13
+ *
14
+ * This entrypoint intentionally does not import Node-only transports (like `@libp2p/tcp`).
15
+ * Callers must provide `options.transports` (and typically `options.listenAddrs`).
16
+ */
17
+ export async function createLibp2pNode(options: NodeOptions): Promise<Libp2p> {
18
+ const transports = options.transports;
19
+ if (!transports || transports.length === 0) {
20
+ throw new Error(
21
+ 'createLibp2pNode (RN) requires options.transports. ' +
22
+ 'Provide an RN-compatible transport (e.g. WebSockets) and any required listenAddrs.'
23
+ );
24
+ }
25
+
26
+ return await createLibp2pNodeBase(options, {
27
+ listenAddrs: [],
28
+ transports,
29
+ });
30
+ }