import { VectorClock } from "../../core/vector-clock/vector-clock.js"; import { CRDT_CODES, CrdtError } from "../../errors/crdt-error.js"; import type { CRDT, MVRegisterState } from "../../types/crdt.types.js"; /** * Multi-Value Register CRDT. * * On concurrent writes from different nodes, all concurrent values are * preserved. `get()` returns all current values; if only one remains, * there is no conflict. */ export class MVRegister implements CRDT> { public readonly id: string; protected nodeId: string; protected entries: Array<{ value: T; vectorClock: Record; }> = []; protected vc: VectorClock; public constructor(nodeId: string, id = crypto.randomUUID()) { this.nodeId = nodeId; this.id = id; this.vc = new VectorClock(); } public set(value: T): void { this.vc.increment(this.nodeId); // Replace all existing entries with the new value at the current clock this.entries = [{ value, vectorClock: this.vc.state() }]; } public get(): T[] { return this.entries.map((e) => e.value); } public conflict(): boolean { return this.entries.length > 1; } public value(): T[] { return this.get(); } public merge(remote: MVRegisterState): void { const local = this.entries; const merged: Array<{ value: T; vectorClock: Record }> = []; // Keep entries that are not dominated by remote for (const le of local) { const dominated = remote.entries.some((re) => { const rc = new VectorClock(re.vectorClock); return rc.compare(le.vectorClock) === "after"; }); if (!dominated) merged.push(le); } // Add remote entries that are not dominated by local for (const re of remote.entries) { const dominated = local.some((le) => { const lc = new VectorClock(le.vectorClock); return lc.compare(re.vectorClock) === "after"; }); if (!dominated) { const alreadyPresent = merged.some((e) => { const ec = new VectorClock(e.vectorClock); return ec.compare(re.vectorClock) === "equal"; }); if (!alreadyPresent) merged.push({ ...re }); } } this.entries = merged; // Advance our VC to cover the merged state for (const entry of merged) { this.vc.merge(entry.vectorClock); } } public state(): MVRegisterState { return { type: "mv-register", entries: this.entries.map((e) => ({ value: e.value, vectorClock: { ...e.vectorClock }, })), }; } public toJSON(): string { return JSON.stringify(this.state()); } public fromJSON(json: string): void { try { const parsed = JSON.parse(json) as MVRegisterState; if (parsed.type !== "mv-register") throw new Error("Invalid type"); this.entries = parsed.entries.map((e) => ({ value: e.value, vectorClock: { ...e.vectorClock }, })); } catch (err) { throw new CrdtError( CRDT_CODES.DESERIALIZE_FAILED, "Invalid MVRegister JSON", { cause: err as Error } ); } } public clone(): this { const c = new MVRegister( this.nodeId, this.id as `${string}-${string}-${string}-${string}-${string}` ) as this; c.entries = this.entries.map((e) => ({ value: e.value, vectorClock: { ...e.vectorClock }, })); c.vc = this.vc.clone(); return c; } }