Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
799
vendor/ruvector/examples/edge-net/pkg/sync.js
vendored
Normal file
799
vendor/ruvector/examples/edge-net/pkg/sync.js
vendored
Normal file
@@ -0,0 +1,799 @@
|
||||
/**
|
||||
* @ruvector/edge-net Hybrid Sync Service
|
||||
*
|
||||
* Multi-device identity and ledger synchronization using:
|
||||
* - P2P sync via WebRTC (fast, direct when devices online together)
|
||||
* - Firestore sync (persistent fallback, cross-session)
|
||||
* - Identity linking via PiKey signatures
|
||||
*
|
||||
* @module @ruvector/edge-net/sync
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { createHash, randomBytes } from 'crypto';
|
||||
|
||||
// ============================================
|
||||
// SYNC CONFIGURATION
|
||||
// ============================================
|
||||
|
||||
export const SYNC_CONFIG = {
|
||||
// Firestore endpoints (Genesis nodes)
|
||||
firestore: {
|
||||
projectId: 'ruvector-edge-net',
|
||||
collection: 'ledger-sync',
|
||||
identityCollection: 'identity-links',
|
||||
},
|
||||
// Sync intervals
|
||||
intervals: {
|
||||
p2pHeartbeat: 5000, // 5s P2P sync check
|
||||
firestoreSync: 30000, // 30s Firestore sync
|
||||
staleThreshold: 60000, // 1min before considering state stale
|
||||
},
|
||||
// CRDT merge settings
|
||||
crdt: {
|
||||
maxBatchSize: 1000, // Max entries per merge
|
||||
conflictResolution: 'lww', // Last-write-wins
|
||||
},
|
||||
// Genesis node endpoints
|
||||
genesisNodes: [
|
||||
{ region: 'us-central1', url: 'https://edge-net-genesis-us.ruvector.dev' },
|
||||
{ region: 'europe-west1', url: 'https://edge-net-genesis-eu.ruvector.dev' },
|
||||
{ region: 'asia-east1', url: 'https://edge-net-genesis-asia.ruvector.dev' },
|
||||
],
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// IDENTITY LINKER
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Links a PiKey identity across multiple devices
|
||||
* Uses cryptographic challenge-response to prove ownership
|
||||
*/
|
||||
export class IdentityLinker extends EventEmitter {
|
||||
constructor(piKey, options = {}) {
|
||||
super();
|
||||
this.piKey = piKey;
|
||||
this.publicKeyHex = this.toHex(piKey.getPublicKey());
|
||||
this.shortId = piKey.getShortId();
|
||||
this.options = {
|
||||
genesisUrl: options.genesisUrl || SYNC_CONFIG.genesisNodes[0].url,
|
||||
...options,
|
||||
};
|
||||
this.linkedDevices = new Map();
|
||||
this.authToken = null;
|
||||
this.deviceId = this.generateDeviceId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique device ID
|
||||
*/
|
||||
generateDeviceId() {
|
||||
const platform = typeof window !== 'undefined' ? 'browser' : 'node';
|
||||
const random = randomBytes(8).toString('hex');
|
||||
const timestamp = Date.now().toString(36);
|
||||
return `${platform}-${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with genesis node using PiKey signature
|
||||
*/
|
||||
async authenticate() {
|
||||
try {
|
||||
// Step 1: Request challenge
|
||||
const challengeRes = await this.fetchWithTimeout(
|
||||
`${this.options.genesisUrl}/api/v1/identity/challenge`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
publicKey: this.publicKeyHex,
|
||||
deviceId: this.deviceId,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!challengeRes.ok) {
|
||||
throw new Error(`Challenge request failed: ${challengeRes.status}`);
|
||||
}
|
||||
|
||||
const { challenge, nonce } = await challengeRes.json();
|
||||
|
||||
// Step 2: Sign challenge with PiKey
|
||||
const challengeBytes = this.fromHex(challenge);
|
||||
const signature = this.piKey.sign(challengeBytes);
|
||||
|
||||
// Step 3: Submit signature for verification
|
||||
const authRes = await this.fetchWithTimeout(
|
||||
`${this.options.genesisUrl}/api/v1/identity/verify`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
publicKey: this.publicKeyHex,
|
||||
deviceId: this.deviceId,
|
||||
nonce,
|
||||
signature: this.toHex(signature),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!authRes.ok) {
|
||||
throw new Error(`Authentication failed: ${authRes.status}`);
|
||||
}
|
||||
|
||||
const { token, expiresAt, linkedDevices } = await authRes.json();
|
||||
|
||||
this.authToken = token;
|
||||
this.tokenExpiry = new Date(expiresAt);
|
||||
|
||||
// Update linked devices
|
||||
for (const device of linkedDevices || []) {
|
||||
this.linkedDevices.set(device.deviceId, device);
|
||||
}
|
||||
|
||||
this.emit('authenticated', {
|
||||
deviceId: this.deviceId,
|
||||
linkedDevices: this.linkedDevices.size,
|
||||
});
|
||||
|
||||
return { success: true, token, linkedDevices: this.linkedDevices.size };
|
||||
|
||||
} catch (error) {
|
||||
// Fallback: Generate local-only token for P2P sync
|
||||
console.warn('[Sync] Genesis authentication failed, using local mode:', error.message);
|
||||
this.authToken = this.generateLocalToken();
|
||||
this.emit('authenticated', { deviceId: this.deviceId, mode: 'local' });
|
||||
return { success: true, mode: 'local' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate local token for P2P-only mode
|
||||
*/
|
||||
generateLocalToken() {
|
||||
const payload = {
|
||||
sub: this.publicKeyHex,
|
||||
dev: this.deviceId,
|
||||
iat: Date.now(),
|
||||
mode: 'local',
|
||||
};
|
||||
return Buffer.from(JSON.stringify(payload)).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Link a new device to this identity
|
||||
*/
|
||||
async linkDevice(deviceInfo) {
|
||||
if (!this.authToken) {
|
||||
await this.authenticate();
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.fetchWithTimeout(
|
||||
`${this.options.genesisUrl}/api/v1/identity/link`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.authToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
publicKey: this.publicKeyHex,
|
||||
newDevice: deviceInfo,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Link failed: ${res.status}`);
|
||||
}
|
||||
|
||||
const result = await res.json();
|
||||
this.linkedDevices.set(deviceInfo.deviceId, deviceInfo);
|
||||
|
||||
this.emit('device_linked', { deviceId: deviceInfo.deviceId });
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
// P2P fallback: Store in local linked devices for gossip
|
||||
this.linkedDevices.set(deviceInfo.deviceId, {
|
||||
...deviceInfo,
|
||||
linkedAt: Date.now(),
|
||||
mode: 'p2p',
|
||||
});
|
||||
return { success: true, mode: 'p2p' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all linked devices
|
||||
*/
|
||||
getLinkedDevices() {
|
||||
return Array.from(this.linkedDevices.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a device is linked to this identity
|
||||
*/
|
||||
isDeviceLinked(deviceId) {
|
||||
return this.linkedDevices.has(deviceId);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
toHex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
fromHex(hex) {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
async fetchWithTimeout(url, options, timeout = 10000) {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeout);
|
||||
try {
|
||||
const response = await fetch(url, { ...options, signal: controller.signal });
|
||||
clearTimeout(id);
|
||||
return response;
|
||||
} catch (error) {
|
||||
clearTimeout(id);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LEDGER SYNC SERVICE
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Hybrid sync service for credit ledger
|
||||
* Combines P2P (fast) and Firestore (persistent) sync
|
||||
*/
|
||||
export class LedgerSyncService extends EventEmitter {
|
||||
constructor(identityLinker, ledger, options = {}) {
|
||||
super();
|
||||
this.identity = identityLinker;
|
||||
this.ledger = ledger;
|
||||
this.options = {
|
||||
enableP2P: true,
|
||||
enableFirestore: true,
|
||||
syncInterval: SYNC_CONFIG.intervals.firestoreSync,
|
||||
...options,
|
||||
};
|
||||
|
||||
// Sync state
|
||||
this.lastSyncTime = 0;
|
||||
this.syncInProgress = false;
|
||||
this.pendingChanges = [];
|
||||
this.peerStates = new Map(); // deviceId -> { earned, spent, timestamp }
|
||||
this.vectorClock = new Map(); // deviceId -> counter
|
||||
|
||||
// P2P connections
|
||||
this.p2pPeers = new Map();
|
||||
|
||||
// Intervals
|
||||
this.syncIntervalId = null;
|
||||
this.heartbeatId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start sync service
|
||||
*/
|
||||
async start() {
|
||||
// Authenticate first
|
||||
await this.identity.authenticate();
|
||||
|
||||
// Start periodic sync
|
||||
if (this.options.enableFirestore) {
|
||||
this.syncIntervalId = setInterval(
|
||||
() => this.syncWithFirestore(),
|
||||
this.options.syncInterval
|
||||
);
|
||||
}
|
||||
|
||||
// Start P2P heartbeat
|
||||
if (this.options.enableP2P) {
|
||||
this.heartbeatId = setInterval(
|
||||
() => this.p2pHeartbeat(),
|
||||
SYNC_CONFIG.intervals.p2pHeartbeat
|
||||
);
|
||||
}
|
||||
|
||||
// Initial sync
|
||||
await this.fullSync();
|
||||
|
||||
this.emit('started', { deviceId: this.identity.deviceId });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop sync service
|
||||
*/
|
||||
stop() {
|
||||
if (this.syncIntervalId) {
|
||||
clearInterval(this.syncIntervalId);
|
||||
this.syncIntervalId = null;
|
||||
}
|
||||
if (this.heartbeatId) {
|
||||
clearInterval(this.heartbeatId);
|
||||
this.heartbeatId = null;
|
||||
}
|
||||
this.emit('stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Full sync - fetch from all sources and merge
|
||||
*/
|
||||
async fullSync() {
|
||||
if (this.syncInProgress) return;
|
||||
this.syncInProgress = true;
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled([
|
||||
this.options.enableFirestore ? this.fetchFromFirestore() : null,
|
||||
this.options.enableP2P ? this.fetchFromP2PPeers() : null,
|
||||
]);
|
||||
|
||||
// Merge all fetched states
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled' && result.value) {
|
||||
await this.mergeState(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Push our state
|
||||
await this.pushState();
|
||||
|
||||
this.lastSyncTime = Date.now();
|
||||
this.emit('synced', {
|
||||
timestamp: this.lastSyncTime,
|
||||
balance: this.ledger.balance(),
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.emit('sync_error', { error: error.message });
|
||||
} finally {
|
||||
this.syncInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ledger state from Firestore
|
||||
*/
|
||||
async fetchFromFirestore() {
|
||||
if (!this.identity.authToken) return null;
|
||||
|
||||
try {
|
||||
const res = await this.identity.fetchWithTimeout(
|
||||
`${this.identity.options.genesisUrl}/api/v1/ledger/${this.identity.publicKeyHex}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.identity.authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) return null; // No state yet
|
||||
throw new Error(`Firestore fetch failed: ${res.status}`);
|
||||
}
|
||||
|
||||
const { states } = await res.json();
|
||||
return states; // Array of { deviceId, earned, spent, timestamp }
|
||||
|
||||
} catch (error) {
|
||||
console.warn('[Sync] Firestore fetch failed:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ledger state from P2P peers
|
||||
*/
|
||||
async fetchFromP2PPeers() {
|
||||
const states = [];
|
||||
|
||||
for (const [peerId, peer] of this.p2pPeers) {
|
||||
try {
|
||||
if (peer.dataChannel?.readyState === 'open') {
|
||||
const state = await this.requestStateFromPeer(peer);
|
||||
if (state) {
|
||||
states.push({ deviceId: peerId, ...state });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[Sync] P2P fetch from ${peerId} failed:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return states.length > 0 ? states : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request state from a P2P peer
|
||||
*/
|
||||
requestStateFromPeer(peer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestId = randomBytes(8).toString('hex');
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('P2P state request timeout'));
|
||||
}, 5000);
|
||||
|
||||
const handler = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'ledger_state' && msg.requestId === requestId) {
|
||||
clearTimeout(timeout);
|
||||
peer.dataChannel.removeEventListener('message', handler);
|
||||
resolve(msg.state);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
};
|
||||
|
||||
peer.dataChannel.addEventListener('message', handler);
|
||||
peer.dataChannel.send(JSON.stringify({
|
||||
type: 'ledger_state_request',
|
||||
requestId,
|
||||
from: this.identity.deviceId,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge remote state into local ledger (CRDT)
|
||||
*/
|
||||
async mergeState(states) {
|
||||
if (!states || !Array.isArray(states)) return;
|
||||
|
||||
for (const state of states) {
|
||||
// Skip our own state
|
||||
if (state.deviceId === this.identity.deviceId) continue;
|
||||
|
||||
// Check vector clock for freshness
|
||||
const lastSeen = this.vectorClock.get(state.deviceId) || 0;
|
||||
if (state.timestamp <= lastSeen) continue;
|
||||
|
||||
// CRDT merge
|
||||
try {
|
||||
if (state.earned && state.spent) {
|
||||
const earned = typeof state.earned === 'string'
|
||||
? JSON.parse(state.earned)
|
||||
: state.earned;
|
||||
const spent = typeof state.spent === 'string'
|
||||
? JSON.parse(state.spent)
|
||||
: state.spent;
|
||||
|
||||
this.ledger.merge(
|
||||
JSON.stringify(earned),
|
||||
JSON.stringify(spent)
|
||||
);
|
||||
}
|
||||
|
||||
// Update vector clock
|
||||
this.vectorClock.set(state.deviceId, state.timestamp);
|
||||
this.peerStates.set(state.deviceId, state);
|
||||
|
||||
this.emit('state_merged', {
|
||||
deviceId: state.deviceId,
|
||||
newBalance: this.ledger.balance(),
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`[Sync] Merge failed for ${state.deviceId}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push local state to sync destinations
|
||||
*/
|
||||
async pushState() {
|
||||
const state = this.exportState();
|
||||
|
||||
// Push to Firestore
|
||||
if (this.options.enableFirestore && this.identity.authToken) {
|
||||
await this.pushToFirestore(state);
|
||||
}
|
||||
|
||||
// Broadcast to P2P peers
|
||||
if (this.options.enableP2P) {
|
||||
this.broadcastToP2P(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export current ledger state
|
||||
*/
|
||||
exportState() {
|
||||
return {
|
||||
deviceId: this.identity.deviceId,
|
||||
publicKey: this.identity.publicKeyHex,
|
||||
earned: this.ledger.exportEarned(),
|
||||
spent: this.ledger.exportSpent(),
|
||||
balance: this.ledger.balance(),
|
||||
totalEarned: this.ledger.totalEarned(),
|
||||
totalSpent: this.ledger.totalSpent(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Push state to Firestore
|
||||
*/
|
||||
async pushToFirestore(state) {
|
||||
try {
|
||||
const res = await this.identity.fetchWithTimeout(
|
||||
`${this.identity.options.genesisUrl}/api/v1/ledger/${this.identity.publicKeyHex}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.identity.authToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
deviceId: state.deviceId,
|
||||
earned: state.earned,
|
||||
spent: state.spent,
|
||||
timestamp: state.timestamp,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Firestore push failed: ${res.status}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.warn('[Sync] Firestore push failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast state to P2P peers
|
||||
*/
|
||||
broadcastToP2P(state) {
|
||||
const message = JSON.stringify({
|
||||
type: 'ledger_state_broadcast',
|
||||
state: {
|
||||
deviceId: state.deviceId,
|
||||
earned: state.earned,
|
||||
spent: state.spent,
|
||||
timestamp: state.timestamp,
|
||||
},
|
||||
});
|
||||
|
||||
for (const [peerId, peer] of this.p2pPeers) {
|
||||
try {
|
||||
if (peer.dataChannel?.readyState === 'open') {
|
||||
peer.dataChannel.send(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[Sync] P2P broadcast to ${peerId} failed:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* P2P heartbeat - discover and sync with nearby devices
|
||||
*/
|
||||
async p2pHeartbeat() {
|
||||
// Broadcast presence to linked devices
|
||||
const presence = {
|
||||
type: 'presence',
|
||||
deviceId: this.identity.deviceId,
|
||||
publicKey: this.identity.publicKeyHex,
|
||||
balance: this.ledger.balance(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
for (const [peerId, peer] of this.p2pPeers) {
|
||||
try {
|
||||
if (peer.dataChannel?.readyState === 'open') {
|
||||
peer.dataChannel.send(JSON.stringify(presence));
|
||||
}
|
||||
} catch (error) {
|
||||
// Remove stale peer
|
||||
this.p2pPeers.delete(peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a P2P peer for sync
|
||||
*/
|
||||
registerP2PPeer(peerId, dataChannel) {
|
||||
this.p2pPeers.set(peerId, { dataChannel, connectedAt: Date.now() });
|
||||
|
||||
// Handle incoming messages
|
||||
dataChannel.addEventListener('message', (event) => {
|
||||
this.handleP2PMessage(peerId, event.data);
|
||||
});
|
||||
|
||||
this.emit('peer_registered', { peerId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming P2P message
|
||||
*/
|
||||
async handleP2PMessage(peerId, data) {
|
||||
try {
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
switch (msg.type) {
|
||||
case 'ledger_state_request':
|
||||
// Respond with our state
|
||||
const state = this.exportState();
|
||||
const peer = this.p2pPeers.get(peerId);
|
||||
if (peer?.dataChannel?.readyState === 'open') {
|
||||
peer.dataChannel.send(JSON.stringify({
|
||||
type: 'ledger_state',
|
||||
requestId: msg.requestId,
|
||||
state: {
|
||||
earned: state.earned,
|
||||
spent: state.spent,
|
||||
timestamp: state.timestamp,
|
||||
},
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ledger_state_broadcast':
|
||||
// Merge incoming state
|
||||
if (msg.state) {
|
||||
await this.mergeState([{ deviceId: peerId, ...msg.state }]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'presence':
|
||||
// Update peer info
|
||||
const existingPeer = this.p2pPeers.get(peerId);
|
||||
if (existingPeer) {
|
||||
existingPeer.lastSeen = Date.now();
|
||||
existingPeer.balance = msg.balance;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`[Sync] P2P message handling failed:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync with Firestore (called periodically)
|
||||
*/
|
||||
async syncWithFirestore() {
|
||||
if (this.syncInProgress) return;
|
||||
|
||||
try {
|
||||
const states = await this.fetchFromFirestore();
|
||||
if (states) {
|
||||
await this.mergeState(states);
|
||||
}
|
||||
await this.pushToFirestore(this.exportState());
|
||||
} catch (error) {
|
||||
console.warn('[Sync] Periodic Firestore sync failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force immediate sync
|
||||
*/
|
||||
async forceSync() {
|
||||
return this.fullSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sync status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
deviceId: this.identity.deviceId,
|
||||
publicKey: this.identity.publicKeyHex,
|
||||
shortId: this.identity.shortId,
|
||||
linkedDevices: this.identity.getLinkedDevices().length,
|
||||
p2pPeers: this.p2pPeers.size,
|
||||
lastSyncTime: this.lastSyncTime,
|
||||
balance: this.ledger.balance(),
|
||||
totalEarned: this.ledger.totalEarned(),
|
||||
totalSpent: this.ledger.totalSpent(),
|
||||
syncEnabled: {
|
||||
p2p: this.options.enableP2P,
|
||||
firestore: this.options.enableFirestore,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SYNC MANAGER (CONVENIENCE WRAPPER)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* High-level sync manager for easy integration
|
||||
*/
|
||||
export class SyncManager extends EventEmitter {
|
||||
constructor(piKey, ledger, options = {}) {
|
||||
super();
|
||||
this.identityLinker = new IdentityLinker(piKey, options);
|
||||
this.syncService = new LedgerSyncService(this.identityLinker, ledger, options);
|
||||
|
||||
// Forward events
|
||||
this.syncService.on('synced', (data) => this.emit('synced', data));
|
||||
this.syncService.on('state_merged', (data) => this.emit('state_merged', data));
|
||||
this.syncService.on('sync_error', (data) => this.emit('sync_error', data));
|
||||
this.identityLinker.on('authenticated', (data) => this.emit('authenticated', data));
|
||||
this.identityLinker.on('device_linked', (data) => this.emit('device_linked', data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start sync
|
||||
*/
|
||||
async start() {
|
||||
await this.syncService.start();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop sync
|
||||
*/
|
||||
stop() {
|
||||
this.syncService.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force sync
|
||||
*/
|
||||
async sync() {
|
||||
return this.syncService.forceSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register P2P peer
|
||||
*/
|
||||
registerPeer(peerId, dataChannel) {
|
||||
this.syncService.registerP2PPeer(peerId, dataChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status
|
||||
*/
|
||||
getStatus() {
|
||||
return this.syncService.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export identity for another device
|
||||
*/
|
||||
exportIdentity(password) {
|
||||
return this.identityLinker.piKey.createEncryptedBackup(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link devices via QR code data
|
||||
*/
|
||||
generateLinkData() {
|
||||
return {
|
||||
publicKey: this.identityLinker.publicKeyHex,
|
||||
shortId: this.identityLinker.shortId,
|
||||
genesisUrl: this.identityLinker.options.genesisUrl,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EXPORTS
|
||||
// ============================================
|
||||
|
||||
export default SyncManager;
|
||||
Reference in New Issue
Block a user