@ruvector/edge-net 0.1.6 → 0.2.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/ledger.js +663 -0
- package/monitor.js +675 -0
- package/onnx-worker.js +482 -0
- package/package.json +41 -5
- package/qdag.js +582 -0
- package/real-agents.js +252 -39
- package/scheduler.js +764 -0
- package/signaling.js +732 -0
package/ledger.js
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ruvector/edge-net Persistent Ledger with CRDT
|
|
3
|
+
*
|
|
4
|
+
* Conflict-free Replicated Data Type for distributed credit tracking
|
|
5
|
+
* Features:
|
|
6
|
+
* - G-Counter for earned credits
|
|
7
|
+
* - PN-Counter for balance
|
|
8
|
+
* - LWW-Register for metadata
|
|
9
|
+
* - File-based persistence
|
|
10
|
+
* - Network synchronization
|
|
11
|
+
*
|
|
12
|
+
* @module @ruvector/edge-net/ledger
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { EventEmitter } from 'events';
|
|
16
|
+
import { randomBytes, createHash } from 'crypto';
|
|
17
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { homedir } from 'os';
|
|
20
|
+
|
|
21
|
+
// ============================================
|
|
22
|
+
// CRDT PRIMITIVES
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* G-Counter (Grow-only Counter)
|
|
27
|
+
* Can only increment, never decrement
|
|
28
|
+
*/
|
|
29
|
+
export class GCounter {
|
|
30
|
+
constructor(nodeId) {
|
|
31
|
+
this.nodeId = nodeId;
|
|
32
|
+
this.counters = new Map(); // nodeId -> count
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
increment(amount = 1) {
|
|
36
|
+
const current = this.counters.get(this.nodeId) || 0;
|
|
37
|
+
this.counters.set(this.nodeId, current + amount);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
value() {
|
|
41
|
+
let total = 0;
|
|
42
|
+
for (const count of this.counters.values()) {
|
|
43
|
+
total += count;
|
|
44
|
+
}
|
|
45
|
+
return total;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
merge(other) {
|
|
49
|
+
for (const [nodeId, count] of other.counters) {
|
|
50
|
+
const current = this.counters.get(nodeId) || 0;
|
|
51
|
+
this.counters.set(nodeId, Math.max(current, count));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toJSON() {
|
|
56
|
+
return {
|
|
57
|
+
nodeId: this.nodeId,
|
|
58
|
+
counters: Object.fromEntries(this.counters),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static fromJSON(json) {
|
|
63
|
+
const counter = new GCounter(json.nodeId);
|
|
64
|
+
counter.counters = new Map(Object.entries(json.counters));
|
|
65
|
+
return counter;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* PN-Counter (Positive-Negative Counter)
|
|
71
|
+
* Can increment and decrement
|
|
72
|
+
*/
|
|
73
|
+
export class PNCounter {
|
|
74
|
+
constructor(nodeId) {
|
|
75
|
+
this.nodeId = nodeId;
|
|
76
|
+
this.positive = new GCounter(nodeId);
|
|
77
|
+
this.negative = new GCounter(nodeId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
increment(amount = 1) {
|
|
81
|
+
this.positive.increment(amount);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
decrement(amount = 1) {
|
|
85
|
+
this.negative.increment(amount);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
value() {
|
|
89
|
+
return this.positive.value() - this.negative.value();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
merge(other) {
|
|
93
|
+
this.positive.merge(other.positive);
|
|
94
|
+
this.negative.merge(other.negative);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
toJSON() {
|
|
98
|
+
return {
|
|
99
|
+
nodeId: this.nodeId,
|
|
100
|
+
positive: this.positive.toJSON(),
|
|
101
|
+
negative: this.negative.toJSON(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static fromJSON(json) {
|
|
106
|
+
const counter = new PNCounter(json.nodeId);
|
|
107
|
+
counter.positive = GCounter.fromJSON(json.positive);
|
|
108
|
+
counter.negative = GCounter.fromJSON(json.negative);
|
|
109
|
+
return counter;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* LWW-Register (Last-Writer-Wins Register)
|
|
115
|
+
* Stores a single value with timestamp
|
|
116
|
+
*/
|
|
117
|
+
export class LWWRegister {
|
|
118
|
+
constructor(nodeId, value = null) {
|
|
119
|
+
this.nodeId = nodeId;
|
|
120
|
+
this.value = value;
|
|
121
|
+
this.timestamp = Date.now();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
set(value) {
|
|
125
|
+
this.value = value;
|
|
126
|
+
this.timestamp = Date.now();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get() {
|
|
130
|
+
return this.value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
merge(other) {
|
|
134
|
+
if (other.timestamp > this.timestamp) {
|
|
135
|
+
this.value = other.value;
|
|
136
|
+
this.timestamp = other.timestamp;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
toJSON() {
|
|
141
|
+
return {
|
|
142
|
+
nodeId: this.nodeId,
|
|
143
|
+
value: this.value,
|
|
144
|
+
timestamp: this.timestamp,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static fromJSON(json) {
|
|
149
|
+
const register = new LWWRegister(json.nodeId);
|
|
150
|
+
register.value = json.value;
|
|
151
|
+
register.timestamp = json.timestamp;
|
|
152
|
+
return register;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* LWW-Map (Last-Writer-Wins Map)
|
|
158
|
+
* Map with LWW semantics per key
|
|
159
|
+
*/
|
|
160
|
+
export class LWWMap {
|
|
161
|
+
constructor(nodeId) {
|
|
162
|
+
this.nodeId = nodeId;
|
|
163
|
+
this.entries = new Map(); // key -> { value, timestamp }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
set(key, value) {
|
|
167
|
+
this.entries.set(key, {
|
|
168
|
+
value,
|
|
169
|
+
timestamp: Date.now(),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get(key) {
|
|
174
|
+
const entry = this.entries.get(key);
|
|
175
|
+
return entry ? entry.value : undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
delete(key) {
|
|
179
|
+
this.entries.set(key, {
|
|
180
|
+
value: null,
|
|
181
|
+
timestamp: Date.now(),
|
|
182
|
+
deleted: true,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
has(key) {
|
|
187
|
+
const entry = this.entries.get(key);
|
|
188
|
+
return entry && !entry.deleted;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
keys() {
|
|
192
|
+
return Array.from(this.entries.keys()).filter(k => !this.entries.get(k).deleted);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
values() {
|
|
196
|
+
return this.keys().map(k => this.entries.get(k).value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
merge(other) {
|
|
200
|
+
for (const [key, entry] of other.entries) {
|
|
201
|
+
const current = this.entries.get(key);
|
|
202
|
+
if (!current || entry.timestamp > current.timestamp) {
|
|
203
|
+
this.entries.set(key, { ...entry });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
toJSON() {
|
|
209
|
+
return {
|
|
210
|
+
nodeId: this.nodeId,
|
|
211
|
+
entries: Object.fromEntries(this.entries),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
static fromJSON(json) {
|
|
216
|
+
const map = new LWWMap(json.nodeId);
|
|
217
|
+
map.entries = new Map(Object.entries(json.entries));
|
|
218
|
+
return map;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================
|
|
223
|
+
// PERSISTENT LEDGER
|
|
224
|
+
// ============================================
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Distributed Ledger with CRDT and persistence
|
|
228
|
+
*/
|
|
229
|
+
export class Ledger extends EventEmitter {
|
|
230
|
+
constructor(options = {}) {
|
|
231
|
+
super();
|
|
232
|
+
this.nodeId = options.nodeId || `node-${randomBytes(8).toString('hex')}`;
|
|
233
|
+
|
|
234
|
+
// Storage path
|
|
235
|
+
this.dataDir = options.dataDir ||
|
|
236
|
+
join(homedir(), '.ruvector', 'edge-net', 'ledger');
|
|
237
|
+
|
|
238
|
+
// CRDT state
|
|
239
|
+
this.earned = new GCounter(this.nodeId);
|
|
240
|
+
this.spent = new GCounter(this.nodeId);
|
|
241
|
+
this.metadata = new LWWMap(this.nodeId);
|
|
242
|
+
this.transactions = [];
|
|
243
|
+
|
|
244
|
+
// Configuration
|
|
245
|
+
this.autosaveInterval = options.autosaveInterval || 30000; // 30 seconds
|
|
246
|
+
this.maxTransactions = options.maxTransactions || 10000;
|
|
247
|
+
|
|
248
|
+
// Sync
|
|
249
|
+
this.lastSync = 0;
|
|
250
|
+
this.syncPeers = new Set();
|
|
251
|
+
|
|
252
|
+
// Initialize
|
|
253
|
+
this.initialized = false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Initialize ledger and load from disk
|
|
258
|
+
*/
|
|
259
|
+
async initialize() {
|
|
260
|
+
// Create data directory
|
|
261
|
+
if (!existsSync(this.dataDir)) {
|
|
262
|
+
mkdirSync(this.dataDir, { recursive: true });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Load existing state
|
|
266
|
+
await this.load();
|
|
267
|
+
|
|
268
|
+
// Start autosave
|
|
269
|
+
this.autosaveTimer = setInterval(() => {
|
|
270
|
+
this.save().catch(err => console.error('[Ledger] Autosave error:', err));
|
|
271
|
+
}, this.autosaveInterval);
|
|
272
|
+
|
|
273
|
+
this.initialized = true;
|
|
274
|
+
this.emit('ready', { nodeId: this.nodeId });
|
|
275
|
+
|
|
276
|
+
return this;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Credit (earn) amount
|
|
281
|
+
*/
|
|
282
|
+
credit(amount, memo = '') {
|
|
283
|
+
if (amount <= 0) throw new Error('Amount must be positive');
|
|
284
|
+
|
|
285
|
+
this.earned.increment(amount);
|
|
286
|
+
|
|
287
|
+
const tx = {
|
|
288
|
+
id: `tx-${randomBytes(8).toString('hex')}`,
|
|
289
|
+
type: 'credit',
|
|
290
|
+
amount,
|
|
291
|
+
memo,
|
|
292
|
+
timestamp: Date.now(),
|
|
293
|
+
nodeId: this.nodeId,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
this.transactions.push(tx);
|
|
297
|
+
this.pruneTransactions();
|
|
298
|
+
|
|
299
|
+
this.emit('credit', { amount, balance: this.balance(), tx });
|
|
300
|
+
|
|
301
|
+
return tx;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Debit (spend) amount
|
|
306
|
+
*/
|
|
307
|
+
debit(amount, memo = '') {
|
|
308
|
+
if (amount <= 0) throw new Error('Amount must be positive');
|
|
309
|
+
if (amount > this.balance()) throw new Error('Insufficient balance');
|
|
310
|
+
|
|
311
|
+
this.spent.increment(amount);
|
|
312
|
+
|
|
313
|
+
const tx = {
|
|
314
|
+
id: `tx-${randomBytes(8).toString('hex')}`,
|
|
315
|
+
type: 'debit',
|
|
316
|
+
amount,
|
|
317
|
+
memo,
|
|
318
|
+
timestamp: Date.now(),
|
|
319
|
+
nodeId: this.nodeId,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
this.transactions.push(tx);
|
|
323
|
+
this.pruneTransactions();
|
|
324
|
+
|
|
325
|
+
this.emit('debit', { amount, balance: this.balance(), tx });
|
|
326
|
+
|
|
327
|
+
return tx;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get current balance
|
|
332
|
+
*/
|
|
333
|
+
balance() {
|
|
334
|
+
return this.earned.value() - this.spent.value();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get total earned
|
|
339
|
+
*/
|
|
340
|
+
totalEarned() {
|
|
341
|
+
return this.earned.value();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get total spent
|
|
346
|
+
*/
|
|
347
|
+
totalSpent() {
|
|
348
|
+
return this.spent.value();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Set metadata
|
|
353
|
+
*/
|
|
354
|
+
setMetadata(key, value) {
|
|
355
|
+
this.metadata.set(key, value);
|
|
356
|
+
this.emit('metadata', { key, value });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get metadata
|
|
361
|
+
*/
|
|
362
|
+
getMetadata(key) {
|
|
363
|
+
return this.metadata.get(key);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get recent transactions
|
|
368
|
+
*/
|
|
369
|
+
getTransactions(limit = 50) {
|
|
370
|
+
return this.transactions.slice(-limit);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Prune old transactions
|
|
375
|
+
*/
|
|
376
|
+
pruneTransactions() {
|
|
377
|
+
if (this.transactions.length > this.maxTransactions) {
|
|
378
|
+
this.transactions = this.transactions.slice(-this.maxTransactions);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Merge with another ledger state (CRDT merge)
|
|
384
|
+
*/
|
|
385
|
+
merge(other) {
|
|
386
|
+
// Merge counters
|
|
387
|
+
if (other.earned) {
|
|
388
|
+
this.earned.merge(
|
|
389
|
+
other.earned instanceof GCounter
|
|
390
|
+
? other.earned
|
|
391
|
+
: GCounter.fromJSON(other.earned)
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (other.spent) {
|
|
396
|
+
this.spent.merge(
|
|
397
|
+
other.spent instanceof GCounter
|
|
398
|
+
? other.spent
|
|
399
|
+
: GCounter.fromJSON(other.spent)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (other.metadata) {
|
|
404
|
+
this.metadata.merge(
|
|
405
|
+
other.metadata instanceof LWWMap
|
|
406
|
+
? other.metadata
|
|
407
|
+
: LWWMap.fromJSON(other.metadata)
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Merge transactions (deduplicate by id)
|
|
412
|
+
if (other.transactions) {
|
|
413
|
+
const existingIds = new Set(this.transactions.map(t => t.id));
|
|
414
|
+
for (const tx of other.transactions) {
|
|
415
|
+
if (!existingIds.has(tx.id)) {
|
|
416
|
+
this.transactions.push(tx);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Sort by timestamp and prune
|
|
420
|
+
this.transactions.sort((a, b) => a.timestamp - b.timestamp);
|
|
421
|
+
this.pruneTransactions();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this.lastSync = Date.now();
|
|
425
|
+
this.emit('merged', { balance: this.balance() });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Export state for synchronization
|
|
430
|
+
*/
|
|
431
|
+
export() {
|
|
432
|
+
return {
|
|
433
|
+
nodeId: this.nodeId,
|
|
434
|
+
timestamp: Date.now(),
|
|
435
|
+
earned: this.earned.toJSON(),
|
|
436
|
+
spent: this.spent.toJSON(),
|
|
437
|
+
metadata: this.metadata.toJSON(),
|
|
438
|
+
transactions: this.transactions,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Save to disk
|
|
444
|
+
*/
|
|
445
|
+
async save() {
|
|
446
|
+
const filePath = join(this.dataDir, 'ledger.json');
|
|
447
|
+
const data = this.export();
|
|
448
|
+
|
|
449
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
450
|
+
this.emit('saved', { path: filePath });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Load from disk
|
|
455
|
+
*/
|
|
456
|
+
async load() {
|
|
457
|
+
const filePath = join(this.dataDir, 'ledger.json');
|
|
458
|
+
|
|
459
|
+
if (!existsSync(filePath)) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
465
|
+
|
|
466
|
+
this.earned = GCounter.fromJSON(data.earned);
|
|
467
|
+
this.spent = GCounter.fromJSON(data.spent);
|
|
468
|
+
this.metadata = LWWMap.fromJSON(data.metadata);
|
|
469
|
+
this.transactions = data.transactions || [];
|
|
470
|
+
|
|
471
|
+
this.emit('loaded', { balance: this.balance() });
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error('[Ledger] Load error:', error.message);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get ledger summary
|
|
479
|
+
*/
|
|
480
|
+
getSummary() {
|
|
481
|
+
return {
|
|
482
|
+
nodeId: this.nodeId,
|
|
483
|
+
balance: this.balance(),
|
|
484
|
+
earned: this.totalEarned(),
|
|
485
|
+
spent: this.totalSpent(),
|
|
486
|
+
transactions: this.transactions.length,
|
|
487
|
+
lastSync: this.lastSync,
|
|
488
|
+
initialized: this.initialized,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Shutdown ledger
|
|
494
|
+
*/
|
|
495
|
+
async shutdown() {
|
|
496
|
+
if (this.autosaveTimer) {
|
|
497
|
+
clearInterval(this.autosaveTimer);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
await this.save();
|
|
501
|
+
this.initialized = false;
|
|
502
|
+
this.emit('shutdown');
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ============================================
|
|
507
|
+
// SYNC CLIENT
|
|
508
|
+
// ============================================
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Ledger sync client for relay communication
|
|
512
|
+
*/
|
|
513
|
+
export class LedgerSyncClient extends EventEmitter {
|
|
514
|
+
constructor(options = {}) {
|
|
515
|
+
super();
|
|
516
|
+
this.ledger = options.ledger;
|
|
517
|
+
this.relayUrl = options.relayUrl || 'ws://localhost:8080';
|
|
518
|
+
this.ws = null;
|
|
519
|
+
this.connected = false;
|
|
520
|
+
this.syncInterval = options.syncInterval || 60000; // 1 minute
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Connect to relay for syncing
|
|
525
|
+
*/
|
|
526
|
+
async connect() {
|
|
527
|
+
return new Promise(async (resolve, reject) => {
|
|
528
|
+
try {
|
|
529
|
+
let WebSocket;
|
|
530
|
+
if (typeof globalThis.WebSocket !== 'undefined') {
|
|
531
|
+
WebSocket = globalThis.WebSocket;
|
|
532
|
+
} else {
|
|
533
|
+
const ws = await import('ws');
|
|
534
|
+
WebSocket = ws.default || ws.WebSocket;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
this.ws = new WebSocket(this.relayUrl);
|
|
538
|
+
|
|
539
|
+
const timeout = setTimeout(() => {
|
|
540
|
+
reject(new Error('Connection timeout'));
|
|
541
|
+
}, 10000);
|
|
542
|
+
|
|
543
|
+
this.ws.onopen = () => {
|
|
544
|
+
clearTimeout(timeout);
|
|
545
|
+
this.connected = true;
|
|
546
|
+
|
|
547
|
+
// Register for ledger sync
|
|
548
|
+
this.send({
|
|
549
|
+
type: 'register',
|
|
550
|
+
nodeId: this.ledger.nodeId,
|
|
551
|
+
capabilities: ['ledger_sync'],
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
this.emit('connected');
|
|
555
|
+
resolve(true);
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
this.ws.onmessage = (event) => {
|
|
559
|
+
this.handleMessage(JSON.parse(event.data));
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
this.ws.onclose = () => {
|
|
563
|
+
this.connected = false;
|
|
564
|
+
this.emit('disconnected');
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
this.ws.onerror = (error) => {
|
|
568
|
+
clearTimeout(timeout);
|
|
569
|
+
reject(error);
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
} catch (error) {
|
|
573
|
+
reject(error);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Handle incoming message
|
|
580
|
+
*/
|
|
581
|
+
handleMessage(message) {
|
|
582
|
+
switch (message.type) {
|
|
583
|
+
case 'welcome':
|
|
584
|
+
this.startSyncLoop();
|
|
585
|
+
break;
|
|
586
|
+
|
|
587
|
+
case 'ledger_state':
|
|
588
|
+
this.handleLedgerState(message);
|
|
589
|
+
break;
|
|
590
|
+
|
|
591
|
+
case 'ledger_update':
|
|
592
|
+
this.ledger.merge(message.state);
|
|
593
|
+
break;
|
|
594
|
+
|
|
595
|
+
default:
|
|
596
|
+
this.emit('message', message);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Handle ledger state from relay
|
|
602
|
+
*/
|
|
603
|
+
handleLedgerState(message) {
|
|
604
|
+
if (message.state) {
|
|
605
|
+
this.ledger.merge(message.state);
|
|
606
|
+
}
|
|
607
|
+
this.emit('synced', { balance: this.ledger.balance() });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Start periodic sync
|
|
612
|
+
*/
|
|
613
|
+
startSyncLoop() {
|
|
614
|
+
// Initial sync
|
|
615
|
+
this.sync();
|
|
616
|
+
|
|
617
|
+
// Periodic sync
|
|
618
|
+
this.syncTimer = setInterval(() => {
|
|
619
|
+
this.sync();
|
|
620
|
+
}, this.syncInterval);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Sync with relay
|
|
625
|
+
*/
|
|
626
|
+
sync() {
|
|
627
|
+
if (!this.connected) return;
|
|
628
|
+
|
|
629
|
+
this.send({
|
|
630
|
+
type: 'ledger_sync',
|
|
631
|
+
state: this.ledger.export(),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Send message
|
|
637
|
+
*/
|
|
638
|
+
send(message) {
|
|
639
|
+
if (this.connected && this.ws?.readyState === 1) {
|
|
640
|
+
this.ws.send(JSON.stringify(message));
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Close connection
|
|
648
|
+
*/
|
|
649
|
+
close() {
|
|
650
|
+
if (this.syncTimer) {
|
|
651
|
+
clearInterval(this.syncTimer);
|
|
652
|
+
}
|
|
653
|
+
if (this.ws) {
|
|
654
|
+
this.ws.close();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ============================================
|
|
660
|
+
// EXPORTS
|
|
661
|
+
// ============================================
|
|
662
|
+
|
|
663
|
+
export default Ledger;
|