@optimystic/reference-peer 0.0.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.
- package/README.md +265 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +742 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mesh.d.ts +2 -0
- package/dist/src/mesh.d.ts.map +1 -0
- package/dist/src/mesh.js +114 -0
- package/dist/src/mesh.js.map +1 -0
- package/package.json +68 -0
- package/src/cli.ts +811 -0
- package/src/index.ts +1 -0
- package/src/mesh.ts +136 -0
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import debug from 'debug';
|
|
4
|
+
import { getNetworkManager, createLibp2pNode, StorageRepo, BlockStorage, MemoryRawStorage, FileRawStorage, Libp2pKeyPeerNetwork, RepoClient, ArachnodeFretAdapter } from '@optimystic/db-p2p';
|
|
5
|
+
import { Diary, NetworkTransactor } from '@optimystic/db-core';
|
|
6
|
+
import * as readline from 'readline';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
const logDebug = debug('optimystic:ref-peer');
|
|
10
|
+
// Simple local transactor for single-node scenarios
|
|
11
|
+
class LocalTransactor {
|
|
12
|
+
storageRepo;
|
|
13
|
+
constructor(storageRepo) {
|
|
14
|
+
this.storageRepo = storageRepo;
|
|
15
|
+
}
|
|
16
|
+
async get(blockGets) {
|
|
17
|
+
return await this.storageRepo.get(blockGets);
|
|
18
|
+
}
|
|
19
|
+
async getStatus(_actionRefs) {
|
|
20
|
+
throw new Error("Method not implemented.");
|
|
21
|
+
}
|
|
22
|
+
async pend(blockAction) {
|
|
23
|
+
return await this.storageRepo.pend(blockAction);
|
|
24
|
+
}
|
|
25
|
+
async commit(request) {
|
|
26
|
+
return await this.storageRepo.commit(request);
|
|
27
|
+
}
|
|
28
|
+
async cancel(actionRef) {
|
|
29
|
+
return await this.storageRepo.cancel(actionRef);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class PeerSession {
|
|
33
|
+
session = null;
|
|
34
|
+
rl = null;
|
|
35
|
+
parseStorageCapacity(options) {
|
|
36
|
+
if (!options.storageCapacity)
|
|
37
|
+
return undefined;
|
|
38
|
+
const parsed = Number(options.storageCapacity);
|
|
39
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
40
|
+
throw new Error('--storage-capacity must be a positive number of bytes');
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
buildArachnodeOptions(storageCapacityBytes) {
|
|
45
|
+
return {
|
|
46
|
+
enableRingZulu: true,
|
|
47
|
+
storage: {
|
|
48
|
+
totalBytes: storageCapacityBytes
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async waitForFirstConnection(node, timeoutMs) {
|
|
53
|
+
if (node.getConnections().length > 0)
|
|
54
|
+
return;
|
|
55
|
+
await new Promise((resolve) => {
|
|
56
|
+
const onConnect = (_evt) => {
|
|
57
|
+
cleanup();
|
|
58
|
+
resolve();
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
cleanup();
|
|
62
|
+
resolve();
|
|
63
|
+
}, timeoutMs);
|
|
64
|
+
const add = (node.addEventListener ?? node.connectionManager?.addEventListener)?.bind(node.addEventListener ? node : node.connectionManager);
|
|
65
|
+
const remove = (node.removeEventListener ?? node.connectionManager?.removeEventListener)?.bind(node.removeEventListener ? node : node.connectionManager);
|
|
66
|
+
const cleanup = () => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
try {
|
|
69
|
+
remove?.('peer:connect', onConnect);
|
|
70
|
+
}
|
|
71
|
+
catch { /* ignore */ }
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
add?.('peer:connect', onConnect);
|
|
75
|
+
}
|
|
76
|
+
catch { /* ignore */ }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async waitForFretReady(node) {
|
|
80
|
+
try {
|
|
81
|
+
const fret = node.services?.fret;
|
|
82
|
+
if (!fret) {
|
|
83
|
+
console.log('⚠️ FRET service not available');
|
|
84
|
+
logDebug('FRET service missing on node');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const hadConn = node.getConnections().length > 0;
|
|
88
|
+
if (!hadConn) {
|
|
89
|
+
console.log('🔗 No connections yet, skipping FRET warm-up');
|
|
90
|
+
logDebug('skipping FRET ready check - no connections');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (typeof fret.ready !== 'function') {
|
|
94
|
+
console.log('⚠️ FRET service does not expose ready()');
|
|
95
|
+
logDebug('FRET ready() not present, skipping wait');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
logDebug('waiting for FRET ready');
|
|
99
|
+
await fret.ready();
|
|
100
|
+
console.log('✅ FRET service ready');
|
|
101
|
+
// Log FRET diagnostics
|
|
102
|
+
const knownPeers = typeof fret.listPeers === 'function' ? fret.listPeers() : [];
|
|
103
|
+
const netSize = typeof fret.getNetworkSizeEstimate === 'function' ? fret.getNetworkSizeEstimate() : undefined;
|
|
104
|
+
logDebug('FRET diagnostics after ready', {
|
|
105
|
+
knownPeers: knownPeers.map((p) => p.id ?? p.toString()),
|
|
106
|
+
knownPeerCount: knownPeers.length,
|
|
107
|
+
networkSizeEstimate: netSize
|
|
108
|
+
});
|
|
109
|
+
console.log(`📊 FRET knows ${knownPeers.length} peer(s)`);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.warn('⚠️ FRET readiness check issue:', error);
|
|
113
|
+
logDebug('FRET readiness check issue', error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async waitForFretConvergence(node, minPeers, timeoutMs) {
|
|
117
|
+
const fret = node.services?.fret;
|
|
118
|
+
if (!fret || typeof fret.listPeers !== 'function') {
|
|
119
|
+
logDebug('FRET convergence check skipped - no listPeers method');
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const start = Date.now();
|
|
123
|
+
let lastCount = 0;
|
|
124
|
+
while (Date.now() - start < timeoutMs) {
|
|
125
|
+
const peers = fret.listPeers();
|
|
126
|
+
const count = Array.isArray(peers) ? peers.length : 0;
|
|
127
|
+
if (count !== lastCount) {
|
|
128
|
+
logDebug('FRET convergence progress', { peerCount: count, target: minPeers, peers: peers.map((p) => p.id) });
|
|
129
|
+
lastCount = count;
|
|
130
|
+
}
|
|
131
|
+
if (count >= minPeers) {
|
|
132
|
+
console.log(`✅ FRET discovered ${count} peer(s)`);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
await new Promise(r => setTimeout(r, 200));
|
|
136
|
+
}
|
|
137
|
+
const finalPeers = fret.listPeers();
|
|
138
|
+
const finalCount = Array.isArray(finalPeers) ? finalPeers.length : 0;
|
|
139
|
+
console.log(`✅ FRET discovered ${finalCount} peer(s)`);
|
|
140
|
+
logDebug('FRET convergence completed', {
|
|
141
|
+
finalPeerCount: finalCount,
|
|
142
|
+
targetPeers: minPeers,
|
|
143
|
+
elapsed: timeoutMs,
|
|
144
|
+
peers: finalPeers.map((p) => p.id)
|
|
145
|
+
});
|
|
146
|
+
// Return true if we have at least some peers, even if not the target
|
|
147
|
+
return finalCount > 0;
|
|
148
|
+
}
|
|
149
|
+
logRingInfo(node, totalCapacityOverride) {
|
|
150
|
+
const fret = node.services?.fret;
|
|
151
|
+
if (!fret) {
|
|
152
|
+
logDebug('ring info unavailable (no FRET service)');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const adapter = new ArachnodeFretAdapter(fret);
|
|
156
|
+
const myInfo = adapter.getMyArachnodeInfo();
|
|
157
|
+
if (myInfo) {
|
|
158
|
+
logDebug('local arachnode info', myInfo);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
logDebug('no arachnode info published yet');
|
|
162
|
+
}
|
|
163
|
+
const rings = adapter.getKnownRings();
|
|
164
|
+
const stats = adapter.getRingStats();
|
|
165
|
+
logDebug('discovered ring depths', rings);
|
|
166
|
+
if (stats.length) {
|
|
167
|
+
logDebug('ring statistics', stats);
|
|
168
|
+
}
|
|
169
|
+
if (totalCapacityOverride) {
|
|
170
|
+
logDebug('storage capacity override active', { bytes: totalCapacityOverride });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async ensureDiary(name) {
|
|
174
|
+
const session = this.requireSession();
|
|
175
|
+
const existing = session.diaries.get(name);
|
|
176
|
+
if (existing)
|
|
177
|
+
return existing;
|
|
178
|
+
// Diary.create uses Collection.createOrOpen under the hood, so this will
|
|
179
|
+
// attach to an existing diary in the network or create it if missing.
|
|
180
|
+
const diary = await Diary.create(session.transactor, name);
|
|
181
|
+
session.diaries.set(name, diary);
|
|
182
|
+
return diary;
|
|
183
|
+
}
|
|
184
|
+
async startNetwork(options) {
|
|
185
|
+
if (this.session?.isConnected) {
|
|
186
|
+
console.log('⚠️ Already connected to network');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.log('🚀 Starting P2P node...');
|
|
190
|
+
// Validate storage options
|
|
191
|
+
if (options.storage === 'file' && !options.storagePath) {
|
|
192
|
+
throw new Error('--storage-path is required when using file storage');
|
|
193
|
+
}
|
|
194
|
+
// Create storage directory if needed
|
|
195
|
+
if (options.storage === 'file' && options.storagePath) {
|
|
196
|
+
fs.mkdirSync(options.storagePath, { recursive: true });
|
|
197
|
+
}
|
|
198
|
+
// Resolve bootstrap nodes from CLI args and/or file
|
|
199
|
+
let bootstrapNodes = options.bootstrap ? options.bootstrap.split(',') : [];
|
|
200
|
+
if (options.bootstrapFile) {
|
|
201
|
+
try {
|
|
202
|
+
const filePath = path.resolve(options.bootstrapFile);
|
|
203
|
+
const stat = fs.statSync(filePath);
|
|
204
|
+
// If it's a directory, wait for mesh-ready.json and use it
|
|
205
|
+
if (stat.isDirectory()) {
|
|
206
|
+
const readyFile = path.join(filePath, 'mesh-ready.json');
|
|
207
|
+
// Wait for mesh-ready.json (up to 30 seconds)
|
|
208
|
+
console.log('⏳ Waiting for mesh to be ready...');
|
|
209
|
+
const waitStart = Date.now();
|
|
210
|
+
while (!fs.existsSync(readyFile) && Date.now() - waitStart < 30000) {
|
|
211
|
+
await new Promise(r => setTimeout(r, 100));
|
|
212
|
+
}
|
|
213
|
+
if (fs.existsSync(readyFile)) {
|
|
214
|
+
// Use mesh-ready.json which has current node info
|
|
215
|
+
const readyContents = fs.readFileSync(readyFile, 'utf-8');
|
|
216
|
+
const readyJson = JSON.parse(readyContents);
|
|
217
|
+
logDebug('loading bootstrap from mesh-ready', { nodeCount: readyJson.nodes.length });
|
|
218
|
+
for (const node of readyJson.nodes) {
|
|
219
|
+
if (Array.isArray(node.multiaddrs) && node.multiaddrs.length > 0) {
|
|
220
|
+
// Prefer localhost for same-machine testing
|
|
221
|
+
const localAddr = node.multiaddrs.find(a => a.includes('/ip4/127.0.0.1/'));
|
|
222
|
+
bootstrapNodes.push(localAddr ?? node.multiaddrs[0]);
|
|
223
|
+
logDebug('loaded bootstrap from mesh-ready', { peerId: node.peerId, addr: localAddr ?? node.multiaddrs[0] });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
console.log(`📋 Loaded ${bootstrapNodes.length} bootstrap address(es) from mesh-ready.json`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.warn('⚠️ Timeout waiting for mesh-ready.json, falling back to node-*.json files');
|
|
230
|
+
// Fallback to old behavior
|
|
231
|
+
const files = fs.readdirSync(filePath).filter(f => f.startsWith('node-') && f.endsWith('.json'));
|
|
232
|
+
logDebug('loading bootstrap from directory (fallback)', { dir: filePath, files });
|
|
233
|
+
for (const file of files) {
|
|
234
|
+
const fullPath = path.join(filePath, file);
|
|
235
|
+
try {
|
|
236
|
+
const contents = fs.readFileSync(fullPath, 'utf-8');
|
|
237
|
+
const json = JSON.parse(contents);
|
|
238
|
+
if (Array.isArray(json.multiaddrs) && json.multiaddrs.length > 0) {
|
|
239
|
+
const localAddr = json.multiaddrs.find(a => a.includes('/ip4/127.0.0.1/'));
|
|
240
|
+
bootstrapNodes.push(localAddr ?? json.multiaddrs[0]);
|
|
241
|
+
logDebug('loaded bootstrap from file', { file, peerId: json.peerId, addr: localAddr ?? json.multiaddrs[0] });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
logDebug('failed to read bootstrap file', { file: fullPath, error: err.message });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
console.log(`📋 Loaded ${bootstrapNodes.length} bootstrap address(es) from ${filePath}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Single file - existing logic
|
|
253
|
+
const contents = fs.readFileSync(filePath, 'utf-8');
|
|
254
|
+
const json = JSON.parse(contents);
|
|
255
|
+
if (Array.isArray(json.nodes)) {
|
|
256
|
+
for (const n of json.nodes) {
|
|
257
|
+
if (Array.isArray(n.multiaddrs))
|
|
258
|
+
bootstrapNodes.push(...n.multiaddrs);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (Array.isArray(json.multiaddrs)) {
|
|
262
|
+
bootstrapNodes.push(...json.multiaddrs);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
console.error('❌ Failed to read bootstrap file:', err.message);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const storageCapacityBytes = this.parseStorageCapacity(options);
|
|
271
|
+
if (storageCapacityBytes) {
|
|
272
|
+
const humanReadable = storageCapacityBytes >= 1024 * 1024 * 1024 ? `${(storageCapacityBytes / (1024 * 1024 * 1024)).toFixed(2)} GB` : `${storageCapacityBytes} bytes`;
|
|
273
|
+
console.log(`📦 Storage capacity override set to ${humanReadable}`);
|
|
274
|
+
logDebug('storage capacity override set', { bytes: storageCapacityBytes, human: humanReadable });
|
|
275
|
+
}
|
|
276
|
+
logDebug('starting libp2p node', {
|
|
277
|
+
port: options.port,
|
|
278
|
+
bootstrapCount: bootstrapNodes.length,
|
|
279
|
+
storage: options.storage,
|
|
280
|
+
storagePath: options.storagePath,
|
|
281
|
+
storageCapacityBytes,
|
|
282
|
+
mode: options.offline ? 'offline' : 'distributed'
|
|
283
|
+
});
|
|
284
|
+
const node = await createLibp2pNode({
|
|
285
|
+
port: parseInt(options.port || '0'),
|
|
286
|
+
bootstrapNodes,
|
|
287
|
+
id: options.id,
|
|
288
|
+
relay: options.relay || false,
|
|
289
|
+
networkName: options.network || 'optimystic',
|
|
290
|
+
fretProfile: options.fretProfile,
|
|
291
|
+
storageType: options.storage || 'memory',
|
|
292
|
+
storagePath: options.storagePath,
|
|
293
|
+
arachnode: {
|
|
294
|
+
enableRingZulu: true,
|
|
295
|
+
storage: storageCapacityBytes ? { totalBytes: storageCapacityBytes } : undefined
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
console.log(`✅ Node started with ID: ${node.peerId.toString()}`);
|
|
299
|
+
console.log(`📡 Listening on:`);
|
|
300
|
+
const addrs = [];
|
|
301
|
+
node.getMultiaddrs().forEach((ma) => {
|
|
302
|
+
const s = ma.toString();
|
|
303
|
+
addrs.push(s);
|
|
304
|
+
console.log(` ${s}`);
|
|
305
|
+
});
|
|
306
|
+
// Set up storage layer
|
|
307
|
+
const rawStorage = options.storage === 'file'
|
|
308
|
+
? new FileRawStorage(options.storagePath)
|
|
309
|
+
: new MemoryRawStorage();
|
|
310
|
+
const storageRepo = new StorageRepo((blockId) => new BlockStorage(blockId, rawStorage));
|
|
311
|
+
// Create key network implementation using libp2p
|
|
312
|
+
const keyNetwork = new Libp2pKeyPeerNetwork(node);
|
|
313
|
+
// Create peer network implementation
|
|
314
|
+
const peerNetwork = {
|
|
315
|
+
async connect(peerId, protocol, options) {
|
|
316
|
+
return node.dialProtocol(peerId, [protocol], { ...(options ?? {}), runOnLimitedConnection: true, negotiateFully: false });
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
// Get the coordinated repo that includes cluster consensus
|
|
320
|
+
const coordinatedRepo = node.coordinatedRepo;
|
|
321
|
+
if (!coordinatedRepo) {
|
|
322
|
+
throw new Error('coordinatedRepo not available on node');
|
|
323
|
+
}
|
|
324
|
+
// Determine operating mode
|
|
325
|
+
const isOffline = Boolean(options.offline);
|
|
326
|
+
console.log(`🔧 Mode: ${isOffline ? 'Offline (LocalTransactor)' : 'Distributed (NetworkTransactor)'}`);
|
|
327
|
+
// Create appropriate transactor based on mode
|
|
328
|
+
let transactor;
|
|
329
|
+
if (isOffline) {
|
|
330
|
+
transactor = new LocalTransactor(storageRepo);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
transactor = new NetworkTransactor({
|
|
334
|
+
timeoutMs: 30000,
|
|
335
|
+
abortOrCancelTimeoutMs: 10000,
|
|
336
|
+
keyNetwork,
|
|
337
|
+
getRepo: (peerId) => {
|
|
338
|
+
return peerId.toString() === node.peerId.toString()
|
|
339
|
+
? coordinatedRepo // Use coordinated repo for self to enable cluster consensus
|
|
340
|
+
: RepoClient.create(peerId, keyNetwork, `/optimystic/${options.network || 'optimystic'}`);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
this.session = {
|
|
345
|
+
node,
|
|
346
|
+
transactor,
|
|
347
|
+
diaries: new Map(),
|
|
348
|
+
trees: new Map(),
|
|
349
|
+
isConnected: true,
|
|
350
|
+
isSingleNode: isOffline
|
|
351
|
+
};
|
|
352
|
+
console.log('✅ Distributed transaction system initialized');
|
|
353
|
+
logDebug('session initialized', { peerId: node.peerId.toString(), offline: isOffline, bootstrap: bootstrapNodes });
|
|
354
|
+
// Network manager readiness (service-based)
|
|
355
|
+
const nm = getNetworkManager(node);
|
|
356
|
+
await nm.ready();
|
|
357
|
+
if (bootstrapNodes.length > 0 && nm.awaitHealthy) {
|
|
358
|
+
// Require at least 2 connections when multiple bootstrap nodes available
|
|
359
|
+
// This ensures better mesh connectivity before operations begin
|
|
360
|
+
const minConnections = Math.min(2, bootstrapNodes.length);
|
|
361
|
+
const ok = await nm.awaitHealthy(minConnections, 10000);
|
|
362
|
+
console.log(`🧭 Network ${ok ? 'healthy' : 'not healthy yet'} (active connections=${ok ? `>=${minConnections}` : '<' + minConnections})`);
|
|
363
|
+
logDebug('network manager status', { healthy: ok, status: nm.getStatus?.() });
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
console.log('🧭 Network ready');
|
|
367
|
+
logDebug('network manager ready', { status: nm.getStatus?.() });
|
|
368
|
+
}
|
|
369
|
+
// Wait for FRET to be ready
|
|
370
|
+
await this.waitForFretReady(node);
|
|
371
|
+
this.logRingInfo(node, storageCapacityBytes);
|
|
372
|
+
// Optionally announce node info to a file for launchers/mesh setups
|
|
373
|
+
if (options.announceFile) {
|
|
374
|
+
try {
|
|
375
|
+
const info = {
|
|
376
|
+
peerId: node.peerId.toString(),
|
|
377
|
+
multiaddrs: addrs,
|
|
378
|
+
port: parseInt(options.port || '0'),
|
|
379
|
+
networkName: options.network || 'optimystic',
|
|
380
|
+
timestamp: Date.now(),
|
|
381
|
+
pid: process.pid
|
|
382
|
+
};
|
|
383
|
+
fs.mkdirSync(path.dirname(options.announceFile), { recursive: true });
|
|
384
|
+
fs.writeFileSync(options.announceFile, JSON.stringify(info, null, 2), 'utf-8');
|
|
385
|
+
console.log(`📝 Announced node info to ${options.announceFile}`);
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
console.error('❌ Failed to write announce file:', err.message);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Wait for network bootstrap if we have bootstrap nodes
|
|
392
|
+
if (bootstrapNodes.length > 0) {
|
|
393
|
+
console.log('🔄 Bootstrapping to network...');
|
|
394
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
395
|
+
console.log('✅ Network bootstrap complete');
|
|
396
|
+
this.logRingInfo(node, storageCapacityBytes);
|
|
397
|
+
// Wait for FRET to discover peers (best effort, non-blocking)
|
|
398
|
+
// FRET neighbor announcements happen asynchronously and take time to propagate
|
|
399
|
+
const minPeers = 1; // At least discover one peer
|
|
400
|
+
if (bootstrapNodes.length > 0) {
|
|
401
|
+
console.log(`🔍 Waiting for FRET to discover peers...`);
|
|
402
|
+
await this.waitForFretConvergence(node, minPeers, 8000);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async stopNetwork() {
|
|
407
|
+
if (!this.session?.isConnected) {
|
|
408
|
+
console.log('⚠️ Not connected to network');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
console.log('🛑 Stopping network...');
|
|
412
|
+
await this.session.node.stop();
|
|
413
|
+
this.session.isConnected = false;
|
|
414
|
+
console.log('✅ Network stopped');
|
|
415
|
+
}
|
|
416
|
+
requireSession() {
|
|
417
|
+
if (!this.session?.isConnected) {
|
|
418
|
+
throw new Error('Not connected to network. Start network first.');
|
|
419
|
+
}
|
|
420
|
+
return this.session;
|
|
421
|
+
}
|
|
422
|
+
async createDiary(name) {
|
|
423
|
+
const session = this.requireSession();
|
|
424
|
+
if (session.diaries.has(name)) {
|
|
425
|
+
throw new Error(`Diary '${name}' already exists`);
|
|
426
|
+
}
|
|
427
|
+
console.log(`📝 Creating diary: ${name}`);
|
|
428
|
+
const diary = await Diary.create(session.transactor, name);
|
|
429
|
+
session.diaries.set(name, diary);
|
|
430
|
+
console.log(`✅ Successfully created diary: ${name}`);
|
|
431
|
+
}
|
|
432
|
+
async addEntry(diaryName, content) {
|
|
433
|
+
const session = this.requireSession();
|
|
434
|
+
const diary = await this.ensureDiary(diaryName);
|
|
435
|
+
console.log(`📝 Adding entry to diary ${diaryName}: ${content}`);
|
|
436
|
+
const entry = {
|
|
437
|
+
content,
|
|
438
|
+
timestamp: new Date().toISOString(),
|
|
439
|
+
author: session.node.peerId.toString()
|
|
440
|
+
};
|
|
441
|
+
await diary.append(entry);
|
|
442
|
+
console.log(`✅ Successfully added entry to diary: ${diaryName}`);
|
|
443
|
+
}
|
|
444
|
+
async listDiaries() {
|
|
445
|
+
const session = this.requireSession();
|
|
446
|
+
if (session.diaries.size === 0) {
|
|
447
|
+
console.log('📁 No diaries created yet');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
console.log('📚 Created diaries:');
|
|
451
|
+
for (const [name, _] of session.diaries) {
|
|
452
|
+
console.log(` - ${name}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async readDiary(diaryName) {
|
|
456
|
+
const session = this.requireSession();
|
|
457
|
+
const diary = await this.ensureDiary(diaryName);
|
|
458
|
+
console.log(`📖 Reading entries from diary: ${diaryName}`);
|
|
459
|
+
let entryCount = 0;
|
|
460
|
+
for await (const entry of diary.select()) {
|
|
461
|
+
entryCount++;
|
|
462
|
+
console.log(`📄 Entry ${entryCount}:`);
|
|
463
|
+
console.log(` Content: ${entry.content}`);
|
|
464
|
+
console.log(` Timestamp: ${entry.timestamp}`);
|
|
465
|
+
if (entry.author) {
|
|
466
|
+
console.log(` Author: ${entry.author}`);
|
|
467
|
+
}
|
|
468
|
+
console.log('');
|
|
469
|
+
}
|
|
470
|
+
if (entryCount === 0) {
|
|
471
|
+
console.log('📁 No entries found in this diary');
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
console.log(`📊 Total entries: ${entryCount}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async showStatus() {
|
|
478
|
+
if (!this.session?.isConnected) {
|
|
479
|
+
console.log('❌ Not connected to network');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const session = this.session;
|
|
483
|
+
console.log('🌐 Network Status:');
|
|
484
|
+
console.log(` Node ID: ${session.node.peerId.toString()}`);
|
|
485
|
+
console.log(` Connected: ${session.isConnected}`);
|
|
486
|
+
console.log(` Diaries: ${session.diaries.size}`);
|
|
487
|
+
console.log(` Trees: ${session.trees.size}`);
|
|
488
|
+
const multiaddrs = session.node.getMultiaddrs();
|
|
489
|
+
console.log(` Addresses (${multiaddrs.length}):`);
|
|
490
|
+
multiaddrs.forEach((ma) => {
|
|
491
|
+
console.log(` ${ma.toString()}`);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
async startInteractive() {
|
|
495
|
+
this.rl = readline.createInterface({
|
|
496
|
+
input: process.stdin,
|
|
497
|
+
output: process.stdout,
|
|
498
|
+
prompt: 'optimystic> '
|
|
499
|
+
});
|
|
500
|
+
console.log('\n🎮 Interactive mode started. Type "help" for commands, "exit" to quit.');
|
|
501
|
+
this.rl.prompt();
|
|
502
|
+
this.rl.on('line', async (line) => {
|
|
503
|
+
const args = line.trim().split(/\s+/);
|
|
504
|
+
const command = args[0];
|
|
505
|
+
try {
|
|
506
|
+
switch (command) {
|
|
507
|
+
case 'help':
|
|
508
|
+
this.showHelp();
|
|
509
|
+
break;
|
|
510
|
+
case 'status':
|
|
511
|
+
await this.showStatus();
|
|
512
|
+
break;
|
|
513
|
+
case 'create-diary':
|
|
514
|
+
if (args.length < 2) {
|
|
515
|
+
console.log('Usage: create-diary <name>');
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
await this.createDiary(args[1]);
|
|
519
|
+
}
|
|
520
|
+
break;
|
|
521
|
+
case 'add-entry':
|
|
522
|
+
if (args.length < 3) {
|
|
523
|
+
console.log('Usage: add-entry <diary-name> <content>');
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
const content = args.slice(2).join(' ');
|
|
527
|
+
await this.addEntry(args[1], content);
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
case 'list-diaries':
|
|
531
|
+
await this.listDiaries();
|
|
532
|
+
break;
|
|
533
|
+
case 'read-diary':
|
|
534
|
+
if (args.length < 2) {
|
|
535
|
+
console.log('Usage: read-diary <name>');
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
await this.readDiary(args[1]);
|
|
539
|
+
}
|
|
540
|
+
break;
|
|
541
|
+
case 'exit':
|
|
542
|
+
case 'quit':
|
|
543
|
+
console.log('👋 Goodbye!');
|
|
544
|
+
await this.stopNetwork();
|
|
545
|
+
process.exit(0);
|
|
546
|
+
break;
|
|
547
|
+
case '':
|
|
548
|
+
// Empty line, just re-prompt
|
|
549
|
+
break;
|
|
550
|
+
default:
|
|
551
|
+
console.log(`Unknown command: ${command}. Type "help" for available commands.`);
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
console.error(`❌ Error: ${error instanceof Error ? error.message : error}`);
|
|
557
|
+
}
|
|
558
|
+
this.rl?.prompt();
|
|
559
|
+
});
|
|
560
|
+
this.rl.on('SIGINT', async () => {
|
|
561
|
+
console.log('\n👋 Shutting down...');
|
|
562
|
+
await this.stopNetwork();
|
|
563
|
+
process.exit(0);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
showHelp() {
|
|
567
|
+
console.log(`
|
|
568
|
+
🎮 Available Commands:
|
|
569
|
+
help - Show this help message
|
|
570
|
+
status - Show network and session status
|
|
571
|
+
create-diary <name> - Create a new diary collection
|
|
572
|
+
add-entry <diary> <content> - Add an entry to a diary
|
|
573
|
+
list-diaries - List all created diaries
|
|
574
|
+
read-diary <name> - Read all entries from a diary
|
|
575
|
+
exit/quit - Exit interactive mode
|
|
576
|
+
`);
|
|
577
|
+
}
|
|
578
|
+
async cleanup() {
|
|
579
|
+
if (this.rl) {
|
|
580
|
+
this.rl.close();
|
|
581
|
+
}
|
|
582
|
+
if (this.session && this.session.isConnected) {
|
|
583
|
+
await this.stopNetwork();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const program = new Command();
|
|
588
|
+
const session = new PeerSession();
|
|
589
|
+
// Ensure cleanup on process exit
|
|
590
|
+
process.on('SIGINT', async () => {
|
|
591
|
+
await session.cleanup();
|
|
592
|
+
process.exit(0);
|
|
593
|
+
});
|
|
594
|
+
process.on('SIGTERM', async () => {
|
|
595
|
+
await session.cleanup();
|
|
596
|
+
process.exit(0);
|
|
597
|
+
});
|
|
598
|
+
program
|
|
599
|
+
.name('optimystic-reference-peer')
|
|
600
|
+
.description('Optimystic P2P Database Reference Peer')
|
|
601
|
+
.version('0.0.1');
|
|
602
|
+
// Interactive mode - network-first approach
|
|
603
|
+
program
|
|
604
|
+
.command('interactive')
|
|
605
|
+
.description('Start interactive mode (default behavior)')
|
|
606
|
+
.option('-p, --port <number>', 'Port to listen on', '0')
|
|
607
|
+
.option('-b, --bootstrap <string>', 'Comma-separated list of bootstrap nodes')
|
|
608
|
+
.option('-i, --id <string>', 'Peer ID')
|
|
609
|
+
.option('-r, --relay', 'Enable relay service')
|
|
610
|
+
.option('-n, --network <string>', 'Network name', 'optimystic')
|
|
611
|
+
.option('--fret-profile <profile>', "FRET profile: 'edge' or 'core'", 'edge')
|
|
612
|
+
.option('-s, --storage <type>', 'Storage type: memory or file', 'memory')
|
|
613
|
+
.option('--storage-path <path>', 'Path for file storage')
|
|
614
|
+
.option('--storage-capacity <bytes>', 'Override storage capacity in bytes (for ring selection)')
|
|
615
|
+
.option('--bootstrap-file <path>', 'Path to JSON containing bootstrap multiaddrs or node list')
|
|
616
|
+
.option('--announce-file <path>', 'Write node info (peerId, multiaddrs) to this JSON file for mesh launchers')
|
|
617
|
+
.action(async (options) => {
|
|
618
|
+
try {
|
|
619
|
+
await session.startNetwork(options);
|
|
620
|
+
await session.startInteractive();
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
if (error instanceof Error) {
|
|
624
|
+
console.error('❌ Error:', error.message);
|
|
625
|
+
if (error.stack)
|
|
626
|
+
console.error(error.stack);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
console.error('❌ Error:', error);
|
|
630
|
+
}
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
// Headless service node (no REPL); useful for mesh nodes in launch profiles
|
|
635
|
+
program
|
|
636
|
+
.command('service')
|
|
637
|
+
.description('Start a headless service node (no interactive prompt)')
|
|
638
|
+
.option('-p, --port <number>', 'Port to listen on', '0')
|
|
639
|
+
.option('-b, --bootstrap <string>', 'Comma-separated list of bootstrap nodes')
|
|
640
|
+
.option('--bootstrap-file <path>', 'Path to JSON containing bootstrap multiaddrs or node list')
|
|
641
|
+
.option('-i, --id <string>', 'Peer ID')
|
|
642
|
+
.option('-r, --relay', 'Enable relay service')
|
|
643
|
+
.option('-n, --network <string>', 'Network name', 'optimystic')
|
|
644
|
+
.option('--fret-profile <profile>', "FRET profile: 'edge' or 'core'", 'edge')
|
|
645
|
+
.option('-s, --storage <type>', 'Storage type: memory or file', 'memory')
|
|
646
|
+
.option('--storage-path <path>', 'Path for file storage')
|
|
647
|
+
.option('--storage-capacity <bytes>', 'Override storage capacity in bytes (for ring selection)')
|
|
648
|
+
.option('--announce-file <path>', 'Write node info (peerId, multiaddrs) to this JSON file for mesh launchers')
|
|
649
|
+
.action(async (options) => {
|
|
650
|
+
try {
|
|
651
|
+
await session.startNetwork(options);
|
|
652
|
+
// Keep process alive
|
|
653
|
+
process.stdin.resume();
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
if (error instanceof Error) {
|
|
657
|
+
console.error('❌ Error:', error.message);
|
|
658
|
+
if (error.stack)
|
|
659
|
+
console.error(error.stack);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
console.error('❌ Error:', error);
|
|
663
|
+
}
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
// Single-action mode commands
|
|
668
|
+
program
|
|
669
|
+
.command('run')
|
|
670
|
+
.description('Connect to network, run a single action, optionally stay connected')
|
|
671
|
+
.option('-p, --port <number>', 'Port to listen on', '0')
|
|
672
|
+
.option('-b, --bootstrap <string>', 'Comma-separated list of bootstrap nodes')
|
|
673
|
+
.option('-i, --id <string>', 'Peer ID')
|
|
674
|
+
.option('-r, --relay', 'Enable relay service')
|
|
675
|
+
.option('-n, --network <string>', 'Network name', 'optimystic')
|
|
676
|
+
.option('--fret-profile <profile>', "FRET profile: 'edge' or 'core'", 'edge')
|
|
677
|
+
.option('-s, --storage <type>', 'Storage type: memory or file', 'memory')
|
|
678
|
+
.option('--storage-path <path>', 'Path for file storage')
|
|
679
|
+
.option('--storage-capacity <bytes>', 'Override storage capacity in bytes (for ring selection)')
|
|
680
|
+
.option('--bootstrap-file <path>', 'Path to JSON containing bootstrap multiaddrs or node list')
|
|
681
|
+
.option('--stay-connected', 'Stay connected after action completes')
|
|
682
|
+
.option('--announce-file <path>', 'Write node info (peerId, multiaddrs) to this JSON file for mesh launchers')
|
|
683
|
+
.requiredOption('-a, --action <action>', 'Action to perform: create-diary, add-entry, list-diaries, read-diary')
|
|
684
|
+
.option('--diary <name>', 'Diary name (required for diary operations)')
|
|
685
|
+
.option('--content <content>', 'Entry content (required for add-entry)')
|
|
686
|
+
.action(async (options) => {
|
|
687
|
+
try {
|
|
688
|
+
await session.startNetwork(options);
|
|
689
|
+
// Perform the requested action
|
|
690
|
+
switch (options.action) {
|
|
691
|
+
case 'create-diary':
|
|
692
|
+
if (!options.diary) {
|
|
693
|
+
throw new Error('--diary is required for create-diary action');
|
|
694
|
+
}
|
|
695
|
+
await session.createDiary(options.diary);
|
|
696
|
+
break;
|
|
697
|
+
case 'add-entry':
|
|
698
|
+
if (!options.diary || !options.content) {
|
|
699
|
+
throw new Error('--diary and --content are required for add-entry action');
|
|
700
|
+
}
|
|
701
|
+
await session.addEntry(options.diary, options.content);
|
|
702
|
+
break;
|
|
703
|
+
case 'list-diaries':
|
|
704
|
+
await session.listDiaries();
|
|
705
|
+
break;
|
|
706
|
+
case 'read-diary':
|
|
707
|
+
if (!options.diary) {
|
|
708
|
+
throw new Error('--diary is required for read-diary action');
|
|
709
|
+
}
|
|
710
|
+
await session.readDiary(options.diary);
|
|
711
|
+
break;
|
|
712
|
+
default:
|
|
713
|
+
throw new Error(`Unknown action: ${options.action}`);
|
|
714
|
+
}
|
|
715
|
+
if (options.stayConnected) {
|
|
716
|
+
console.log('🔄 Staying connected. Starting interactive mode...');
|
|
717
|
+
await session.startInteractive();
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
console.log('✅ Action completed. Disconnecting...');
|
|
721
|
+
await session.stopNetwork();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
if (error instanceof Error) {
|
|
726
|
+
console.error('❌ Error:', error.message);
|
|
727
|
+
if (error.stack)
|
|
728
|
+
console.error(error.stack);
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
console.error('❌ Error:', error);
|
|
732
|
+
}
|
|
733
|
+
await session.cleanup();
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
// Default to interactive mode if no command specified
|
|
738
|
+
if (process.argv.length <= 2) {
|
|
739
|
+
process.argv.push('interactive');
|
|
740
|
+
}
|
|
741
|
+
program.parse();
|
|
742
|
+
//# sourceMappingURL=cli.js.map
|