201 lines
7.3 KiB
JavaScript
201 lines
7.3 KiB
JavaScript
"use strict";
|
|
/**
|
|
* Sync Manager Implementation
|
|
* Manages data synchronization across replicas
|
|
*/
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.SyncManager = exports.ReplicationLog = void 0;
|
|
const eventemitter3_1 = __importDefault(require("eventemitter3"));
|
|
const types_js_1 = require("./types.js");
|
|
const vector_clock_js_1 = require("./vector-clock.js");
|
|
/** Default sync configuration */
|
|
const DEFAULT_SYNC_CONFIG = {
|
|
mode: types_js_1.SyncMode.Asynchronous,
|
|
batchSize: 100,
|
|
maxLag: 5000,
|
|
};
|
|
/** Replication log for tracking changes */
|
|
class ReplicationLog {
|
|
constructor(replicaId) {
|
|
this.entries = [];
|
|
this.sequence = 0;
|
|
this.replicaId = replicaId;
|
|
this.vectorClock = new vector_clock_js_1.VectorClock();
|
|
}
|
|
/** Get the current sequence number */
|
|
get currentSequence() {
|
|
return this.sequence;
|
|
}
|
|
/** Get the current vector clock */
|
|
get clock() {
|
|
return this.vectorClock.clone();
|
|
}
|
|
/** Append an entry to the log */
|
|
append(data) {
|
|
this.sequence++;
|
|
this.vectorClock.increment(this.replicaId);
|
|
const entry = {
|
|
id: `${this.replicaId}-${this.sequence}`,
|
|
sequence: this.sequence,
|
|
data,
|
|
timestamp: Date.now(),
|
|
vectorClock: this.vectorClock.getValue(),
|
|
};
|
|
this.entries.push(entry);
|
|
return entry;
|
|
}
|
|
/** Get entries since a sequence number */
|
|
getEntriesSince(sequence, limit) {
|
|
const filtered = this.entries.filter((e) => e.sequence > sequence);
|
|
return limit ? filtered.slice(0, limit) : filtered;
|
|
}
|
|
/** Get entry by ID */
|
|
getEntry(id) {
|
|
return this.entries.find((e) => e.id === id);
|
|
}
|
|
/** Get all entries */
|
|
getAllEntries() {
|
|
return [...this.entries];
|
|
}
|
|
/** Apply entries from another replica */
|
|
applyEntries(entries) {
|
|
for (const entry of entries) {
|
|
const entryClock = new vector_clock_js_1.VectorClock(entry.vectorClock);
|
|
this.vectorClock.merge(entryClock);
|
|
}
|
|
// Note: In a real implementation, entries would be merged properly
|
|
}
|
|
/** Clear the log */
|
|
clear() {
|
|
this.entries = [];
|
|
this.sequence = 0;
|
|
this.vectorClock = new vector_clock_js_1.VectorClock();
|
|
}
|
|
}
|
|
exports.ReplicationLog = ReplicationLog;
|
|
/** Manages synchronization across replicas */
|
|
class SyncManager extends eventemitter3_1.default {
|
|
constructor(replicaSet, log, config) {
|
|
super();
|
|
this.pendingChanges = [];
|
|
this.syncTimer = null;
|
|
this.replicaSet = replicaSet;
|
|
this.log = log;
|
|
this.config = { ...DEFAULT_SYNC_CONFIG, ...config };
|
|
// Default to timestamp-based resolution
|
|
this.conflictResolver = new vector_clock_js_1.LastWriteWins();
|
|
}
|
|
/** Set sync mode */
|
|
setSyncMode(mode, minReplicas) {
|
|
this.config.mode = mode;
|
|
if (minReplicas !== undefined) {
|
|
this.config.minReplicas = minReplicas;
|
|
}
|
|
}
|
|
/** Set custom conflict resolver */
|
|
setConflictResolver(resolver) {
|
|
this.conflictResolver = resolver;
|
|
}
|
|
/** Record a change for replication */
|
|
async recordChange(key, operation, value, previousValue) {
|
|
const primary = this.replicaSet.primary;
|
|
if (!primary) {
|
|
throw types_js_1.ReplicationError.noPrimary();
|
|
}
|
|
const entry = this.log.append({ key, operation, value, previousValue });
|
|
const change = {
|
|
id: entry.id,
|
|
operation,
|
|
key,
|
|
value,
|
|
previousValue,
|
|
timestamp: entry.timestamp,
|
|
sourceReplica: primary.id,
|
|
vectorClock: entry.vectorClock,
|
|
};
|
|
this.emit(types_js_1.ReplicationEvent.ChangeReceived, change);
|
|
// Handle based on sync mode
|
|
switch (this.config.mode) {
|
|
case types_js_1.SyncMode.Synchronous:
|
|
await this.syncAll(change);
|
|
break;
|
|
case types_js_1.SyncMode.SemiSync:
|
|
await this.syncMinimum(change);
|
|
break;
|
|
case types_js_1.SyncMode.Asynchronous:
|
|
this.pendingChanges.push(change);
|
|
break;
|
|
}
|
|
}
|
|
/** Sync a change to all replicas */
|
|
async syncAll(change) {
|
|
const secondaries = this.replicaSet.secondaries;
|
|
if (secondaries.length === 0)
|
|
return;
|
|
this.emit(types_js_1.ReplicationEvent.SyncStarted, { replicas: secondaries.map((r) => r.id) });
|
|
// In a real implementation, this would send to all replicas
|
|
// For now, we just emit the completion event
|
|
this.emit(types_js_1.ReplicationEvent.SyncCompleted, { change, replicas: secondaries.map((r) => r.id) });
|
|
}
|
|
/** Sync to minimum number of replicas (semi-sync) */
|
|
async syncMinimum(change) {
|
|
const minReplicas = this.config.minReplicas ?? 1;
|
|
const secondaries = this.replicaSet.secondaries;
|
|
if (secondaries.length < minReplicas) {
|
|
throw types_js_1.ReplicationError.quorumNotMet(minReplicas, secondaries.length);
|
|
}
|
|
// Sync to minimum number of replicas
|
|
const targetReplicas = secondaries.slice(0, minReplicas);
|
|
this.emit(types_js_1.ReplicationEvent.SyncStarted, { replicas: targetReplicas.map((r) => r.id) });
|
|
// In a real implementation, this would wait for acknowledgments
|
|
this.emit(types_js_1.ReplicationEvent.SyncCompleted, { change, replicas: targetReplicas.map((r) => r.id) });
|
|
}
|
|
/** Start background sync for async mode */
|
|
startBackgroundSync(interval = 1000) {
|
|
if (this.syncTimer)
|
|
return;
|
|
this.syncTimer = setInterval(async () => {
|
|
if (this.pendingChanges.length > 0) {
|
|
const batch = this.pendingChanges.splice(0, this.config.batchSize);
|
|
for (const change of batch) {
|
|
await this.syncAll(change);
|
|
}
|
|
}
|
|
}, interval);
|
|
}
|
|
/** Stop background sync */
|
|
stopBackgroundSync() {
|
|
if (this.syncTimer) {
|
|
clearInterval(this.syncTimer);
|
|
this.syncTimer = null;
|
|
}
|
|
}
|
|
/** Resolve a conflict between local and remote values */
|
|
resolveConflict(local, remote, localClock, remoteClock) {
|
|
// Check for causal relationship
|
|
if (localClock.happensBefore(remoteClock)) {
|
|
return remote; // Remote is newer
|
|
}
|
|
else if (localClock.happensAfter(remoteClock)) {
|
|
return local; // Local is newer
|
|
}
|
|
// Concurrent - need conflict resolution
|
|
this.emit(types_js_1.ReplicationEvent.ConflictDetected, { local, remote });
|
|
const resolved = this.conflictResolver.resolve(local, remote, localClock, remoteClock);
|
|
this.emit(types_js_1.ReplicationEvent.ConflictResolved, { local, remote, resolved });
|
|
return resolved;
|
|
}
|
|
/** Get sync statistics */
|
|
getStats() {
|
|
return {
|
|
pendingChanges: this.pendingChanges.length,
|
|
lastSequence: this.log.currentSequence,
|
|
syncMode: this.config.mode,
|
|
};
|
|
}
|
|
}
|
|
exports.SyncManager = SyncManager;
|
|
//# sourceMappingURL=sync-manager.js.map
|