Files
wifi-densepose/vendor/ruvector/examples/edge-net/pkg/ledger.js

664 lines
16 KiB
JavaScript

/**
* @ruvector/edge-net Persistent Ledger with CRDT
*
* Conflict-free Replicated Data Type for distributed credit tracking
* Features:
* - G-Counter for earned credits
* - PN-Counter for balance
* - LWW-Register for metadata
* - File-based persistence
* - Network synchronization
*
* @module @ruvector/edge-net/ledger
*/
import { EventEmitter } from 'events';
import { randomBytes, createHash } from 'crypto';
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// ============================================
// CRDT PRIMITIVES
// ============================================
/**
* G-Counter (Grow-only Counter)
* Can only increment, never decrement
*/
export class GCounter {
constructor(nodeId) {
this.nodeId = nodeId;
this.counters = new Map(); // nodeId -> count
}
increment(amount = 1) {
const current = this.counters.get(this.nodeId) || 0;
this.counters.set(this.nodeId, current + amount);
}
value() {
let total = 0;
for (const count of this.counters.values()) {
total += count;
}
return total;
}
merge(other) {
for (const [nodeId, count] of other.counters) {
const current = this.counters.get(nodeId) || 0;
this.counters.set(nodeId, Math.max(current, count));
}
}
toJSON() {
return {
nodeId: this.nodeId,
counters: Object.fromEntries(this.counters),
};
}
static fromJSON(json) {
const counter = new GCounter(json.nodeId);
counter.counters = new Map(Object.entries(json.counters));
return counter;
}
}
/**
* PN-Counter (Positive-Negative Counter)
* Can increment and decrement
*/
export class PNCounter {
constructor(nodeId) {
this.nodeId = nodeId;
this.positive = new GCounter(nodeId);
this.negative = new GCounter(nodeId);
}
increment(amount = 1) {
this.positive.increment(amount);
}
decrement(amount = 1) {
this.negative.increment(amount);
}
value() {
return this.positive.value() - this.negative.value();
}
merge(other) {
this.positive.merge(other.positive);
this.negative.merge(other.negative);
}
toJSON() {
return {
nodeId: this.nodeId,
positive: this.positive.toJSON(),
negative: this.negative.toJSON(),
};
}
static fromJSON(json) {
const counter = new PNCounter(json.nodeId);
counter.positive = GCounter.fromJSON(json.positive);
counter.negative = GCounter.fromJSON(json.negative);
return counter;
}
}
/**
* LWW-Register (Last-Writer-Wins Register)
* Stores a single value with timestamp
*/
export class LWWRegister {
constructor(nodeId, value = null) {
this.nodeId = nodeId;
this.value = value;
this.timestamp = Date.now();
}
set(value) {
this.value = value;
this.timestamp = Date.now();
}
get() {
return this.value;
}
merge(other) {
if (other.timestamp > this.timestamp) {
this.value = other.value;
this.timestamp = other.timestamp;
}
}
toJSON() {
return {
nodeId: this.nodeId,
value: this.value,
timestamp: this.timestamp,
};
}
static fromJSON(json) {
const register = new LWWRegister(json.nodeId);
register.value = json.value;
register.timestamp = json.timestamp;
return register;
}
}
/**
* LWW-Map (Last-Writer-Wins Map)
* Map with LWW semantics per key
*/
export class LWWMap {
constructor(nodeId) {
this.nodeId = nodeId;
this.entries = new Map(); // key -> { value, timestamp }
}
set(key, value) {
this.entries.set(key, {
value,
timestamp: Date.now(),
});
}
get(key) {
const entry = this.entries.get(key);
return entry ? entry.value : undefined;
}
delete(key) {
this.entries.set(key, {
value: null,
timestamp: Date.now(),
deleted: true,
});
}
has(key) {
const entry = this.entries.get(key);
return entry && !entry.deleted;
}
keys() {
return Array.from(this.entries.keys()).filter(k => !this.entries.get(k).deleted);
}
values() {
return this.keys().map(k => this.entries.get(k).value);
}
merge(other) {
for (const [key, entry] of other.entries) {
const current = this.entries.get(key);
if (!current || entry.timestamp > current.timestamp) {
this.entries.set(key, { ...entry });
}
}
}
toJSON() {
return {
nodeId: this.nodeId,
entries: Object.fromEntries(this.entries),
};
}
static fromJSON(json) {
const map = new LWWMap(json.nodeId);
map.entries = new Map(Object.entries(json.entries));
return map;
}
}
// ============================================
// PERSISTENT LEDGER
// ============================================
/**
* Distributed Ledger with CRDT and persistence
*/
export class Ledger extends EventEmitter {
constructor(options = {}) {
super();
this.nodeId = options.nodeId || `node-${randomBytes(8).toString('hex')}`;
// Storage path
this.dataDir = options.dataDir ||
join(homedir(), '.ruvector', 'edge-net', 'ledger');
// CRDT state
this.earned = new GCounter(this.nodeId);
this.spent = new GCounter(this.nodeId);
this.metadata = new LWWMap(this.nodeId);
this.transactions = [];
// Configuration
this.autosaveInterval = options.autosaveInterval || 30000; // 30 seconds
this.maxTransactions = options.maxTransactions || 10000;
// Sync
this.lastSync = 0;
this.syncPeers = new Set();
// Initialize
this.initialized = false;
}
/**
* Initialize ledger and load from disk
*/
async initialize() {
// Create data directory
if (!existsSync(this.dataDir)) {
mkdirSync(this.dataDir, { recursive: true });
}
// Load existing state
await this.load();
// Start autosave
this.autosaveTimer = setInterval(() => {
this.save().catch(err => console.error('[Ledger] Autosave error:', err));
}, this.autosaveInterval);
this.initialized = true;
this.emit('ready', { nodeId: this.nodeId });
return this;
}
/**
* Credit (earn) amount
*/
credit(amount, memo = '') {
if (amount <= 0) throw new Error('Amount must be positive');
this.earned.increment(amount);
const tx = {
id: `tx-${randomBytes(8).toString('hex')}`,
type: 'credit',
amount,
memo,
timestamp: Date.now(),
nodeId: this.nodeId,
};
this.transactions.push(tx);
this.pruneTransactions();
this.emit('credit', { amount, balance: this.balance(), tx });
return tx;
}
/**
* Debit (spend) amount
*/
debit(amount, memo = '') {
if (amount <= 0) throw new Error('Amount must be positive');
if (amount > this.balance()) throw new Error('Insufficient balance');
this.spent.increment(amount);
const tx = {
id: `tx-${randomBytes(8).toString('hex')}`,
type: 'debit',
amount,
memo,
timestamp: Date.now(),
nodeId: this.nodeId,
};
this.transactions.push(tx);
this.pruneTransactions();
this.emit('debit', { amount, balance: this.balance(), tx });
return tx;
}
/**
* Get current balance
*/
balance() {
return this.earned.value() - this.spent.value();
}
/**
* Get total earned
*/
totalEarned() {
return this.earned.value();
}
/**
* Get total spent
*/
totalSpent() {
return this.spent.value();
}
/**
* Set metadata
*/
setMetadata(key, value) {
this.metadata.set(key, value);
this.emit('metadata', { key, value });
}
/**
* Get metadata
*/
getMetadata(key) {
return this.metadata.get(key);
}
/**
* Get recent transactions
*/
getTransactions(limit = 50) {
return this.transactions.slice(-limit);
}
/**
* Prune old transactions
*/
pruneTransactions() {
if (this.transactions.length > this.maxTransactions) {
this.transactions = this.transactions.slice(-this.maxTransactions);
}
}
/**
* Merge with another ledger state (CRDT merge)
*/
merge(other) {
// Merge counters
if (other.earned) {
this.earned.merge(
other.earned instanceof GCounter
? other.earned
: GCounter.fromJSON(other.earned)
);
}
if (other.spent) {
this.spent.merge(
other.spent instanceof GCounter
? other.spent
: GCounter.fromJSON(other.spent)
);
}
if (other.metadata) {
this.metadata.merge(
other.metadata instanceof LWWMap
? other.metadata
: LWWMap.fromJSON(other.metadata)
);
}
// Merge transactions (deduplicate by id)
if (other.transactions) {
const existingIds = new Set(this.transactions.map(t => t.id));
for (const tx of other.transactions) {
if (!existingIds.has(tx.id)) {
this.transactions.push(tx);
}
}
// Sort by timestamp and prune
this.transactions.sort((a, b) => a.timestamp - b.timestamp);
this.pruneTransactions();
}
this.lastSync = Date.now();
this.emit('merged', { balance: this.balance() });
}
/**
* Export state for synchronization
*/
export() {
return {
nodeId: this.nodeId,
timestamp: Date.now(),
earned: this.earned.toJSON(),
spent: this.spent.toJSON(),
metadata: this.metadata.toJSON(),
transactions: this.transactions,
};
}
/**
* Save to disk
*/
async save() {
const filePath = join(this.dataDir, 'ledger.json');
const data = this.export();
writeFileSync(filePath, JSON.stringify(data, null, 2));
this.emit('saved', { path: filePath });
}
/**
* Load from disk
*/
async load() {
const filePath = join(this.dataDir, 'ledger.json');
if (!existsSync(filePath)) {
return;
}
try {
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
this.earned = GCounter.fromJSON(data.earned);
this.spent = GCounter.fromJSON(data.spent);
this.metadata = LWWMap.fromJSON(data.metadata);
this.transactions = data.transactions || [];
this.emit('loaded', { balance: this.balance() });
} catch (error) {
console.error('[Ledger] Load error:', error.message);
}
}
/**
* Get ledger summary
*/
getSummary() {
return {
nodeId: this.nodeId,
balance: this.balance(),
earned: this.totalEarned(),
spent: this.totalSpent(),
transactions: this.transactions.length,
lastSync: this.lastSync,
initialized: this.initialized,
};
}
/**
* Shutdown ledger
*/
async shutdown() {
if (this.autosaveTimer) {
clearInterval(this.autosaveTimer);
}
await this.save();
this.initialized = false;
this.emit('shutdown');
}
}
// ============================================
// SYNC CLIENT
// ============================================
/**
* Ledger sync client for relay communication
*/
export class LedgerSyncClient extends EventEmitter {
constructor(options = {}) {
super();
this.ledger = options.ledger;
this.relayUrl = options.relayUrl || 'ws://localhost:8080';
this.ws = null;
this.connected = false;
this.syncInterval = options.syncInterval || 60000; // 1 minute
}
/**
* Connect to relay for syncing
*/
async connect() {
return new Promise(async (resolve, reject) => {
try {
let WebSocket;
if (typeof globalThis.WebSocket !== 'undefined') {
WebSocket = globalThis.WebSocket;
} else {
const ws = await import('ws');
WebSocket = ws.default || ws.WebSocket;
}
this.ws = new WebSocket(this.relayUrl);
const timeout = setTimeout(() => {
reject(new Error('Connection timeout'));
}, 10000);
this.ws.onopen = () => {
clearTimeout(timeout);
this.connected = true;
// Register for ledger sync
this.send({
type: 'register',
nodeId: this.ledger.nodeId,
capabilities: ['ledger_sync'],
});
this.emit('connected');
resolve(true);
};
this.ws.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data));
};
this.ws.onclose = () => {
this.connected = false;
this.emit('disconnected');
};
this.ws.onerror = (error) => {
clearTimeout(timeout);
reject(error);
};
} catch (error) {
reject(error);
}
});
}
/**
* Handle incoming message
*/
handleMessage(message) {
switch (message.type) {
case 'welcome':
this.startSyncLoop();
break;
case 'ledger_state':
this.handleLedgerState(message);
break;
case 'ledger_update':
this.ledger.merge(message.state);
break;
default:
this.emit('message', message);
}
}
/**
* Handle ledger state from relay
*/
handleLedgerState(message) {
if (message.state) {
this.ledger.merge(message.state);
}
this.emit('synced', { balance: this.ledger.balance() });
}
/**
* Start periodic sync
*/
startSyncLoop() {
// Initial sync
this.sync();
// Periodic sync
this.syncTimer = setInterval(() => {
this.sync();
}, this.syncInterval);
}
/**
* Sync with relay
*/
sync() {
if (!this.connected) return;
this.send({
type: 'ledger_sync',
state: this.ledger.export(),
});
}
/**
* Send message
*/
send(message) {
if (this.connected && this.ws?.readyState === 1) {
this.ws.send(JSON.stringify(message));
return true;
}
return false;
}
/**
* Close connection
*/
close() {
if (this.syncTimer) {
clearInterval(this.syncTimer);
}
if (this.ws) {
this.ws.close();
}
}
}
// ============================================
// EXPORTS
// ============================================
export default Ledger;