Initial Commit
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
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<T> implements CRDT<T[], MVRegisterState<T>> {
|
||||
public readonly id: string;
|
||||
protected nodeId: string;
|
||||
protected entries: Array<{
|
||||
value: T;
|
||||
vectorClock: Record<string, number>;
|
||||
}> = [];
|
||||
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<T>): void {
|
||||
const local = this.entries;
|
||||
const merged: Array<{ value: T; vectorClock: Record<string, number> }> =
|
||||
[];
|
||||
|
||||
// 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<T> {
|
||||
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<T>;
|
||||
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<T>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user