Skip to content

Commit

Permalink
fix: consistent naming, function scope addressed
Browse files Browse the repository at this point in the history
  • Loading branch information
JanLewDev committed Aug 5, 2024
1 parent ad3d057 commit 715071e
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 88 deletions.
138 changes: 68 additions & 70 deletions packages/crdt/src/builtins/RGA/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
type Identifier = { counter: number; nodeId: string };

class RGAElement<T> {
public id: Identifier;
// Virtual identifier of the element
public vid: Identifier;
public value: T | null;

constructor(id: Identifier, value: T | null) {
this.id = id;
constructor(vid: Identifier, value: T | null) {
this.vid = vid;
/// If the value is null, the element is in the tombstone state
this.value = value;
}
Expand Down Expand Up @@ -34,134 +35,131 @@ export class RGA<T> {
return this._elements;
}

getElements(): T[] {
getArray(): T[] {
return this._elements
.filter((element) => element.value !== null)
.map((element) => element.value! as T);
}

clear(): void {
this._sequencer = { counter: 0, nodeId: this._sequencer.nodeId };
this._elements = [
new RGAElement<T>({ counter: 0, nodeId: "" }, null),
]
this._elements = [new RGAElement<T>({ counter: 0, nodeId: "" }, null)];
}

isTombstone(element: RGAElement<T>): boolean {
private isTombstone(element: RGAElement<T>): boolean {
return element.value === null;
}

nextSeqNr(sequencer: Identifier): Identifier {
// Function to generate the next unique identifier
private nextSeq(sequencer: Identifier): Identifier {
return { counter: sequencer.counter + 1, nodeId: sequencer.nodeId };
}

// Check whether a < b, ids are never equal
private compareVIds(a: Identifier, b: Identifier): boolean {
if (a.counter !== b.counter) {
return a.counter < b.counter;
}
return a.nodeId < b.nodeId;
}

// Function to map a logical index (ignoring tombstones) to a physical index in the elements array
indexWithTombstones(index: number): number {
// Start from 1 to skip the head element
let offset = 1;
while (index >= 0 && offset < this._elements.length) {
if(index === 0 && !this.isTombstone(this._elements[offset])) break;
private indexWithTombstones(index: number): number {
let offset = 1; // Start from 1 to skip the head element
while (index > 0) {
if (!this.isTombstone(this._elements[offset])) index--;
offset++;
}
if (index > 0) {
throw new RangeError("Index not found");
}
return offset;
}

// Function to read the value at a given index
read(index: number): T | null {
const i = this.indexWithTombstones(index);
let i = this.indexWithTombstones(index);
while (this.isTombstone(this._elements[i])) i++;
return this._elements[i].value;
}

// Function to find the physical index of a vertex given its virtual pointer
indexOfVPtr(ptr: Identifier): number {
// Function to find the physical index of an element given the virtual id
private indexOfVId(ptr: Identifier): number {
for (let offset = 0; offset < this._elements.length; offset++) {
if (
ptr.counter === this._elements[offset].id.counter &&
ptr.nodeId === this._elements[offset].id.nodeId
ptr.counter === this._elements[offset].vid.counter &&
ptr.nodeId === this._elements[offset].vid.nodeId
) {
return offset;
}
}
throw new RangeError("Index not found");
}

// Function to find the correct insertion point for a new vertex
shift(offset: number, ptr: Identifier): number {
// Function to find the correct insertion point for a new element
private shift(offset: number, id: Identifier): number {
while (offset < this._elements.length) {
const next: Identifier = this._elements[offset].id;
if (
next.counter < ptr.counter ||
(next.counter === ptr.counter && next.nodeId < ptr.nodeId)
) {
return offset;
}
const next: Identifier = this._elements[offset].vid;
if (this.compareVIds(next, id)) return offset;
offset++;
}
return offset;
}

// Function to insert a new vertex in the graph
insert(index: number, value: T): void {
const i = this.indexWithTombstones(index);
const predecessor = this._elements[i - 1].id;
const ptr = this.nextSeqNr(this._sequencer);
const predecessor = this._elements[i - 1].vid;
const newVId = this.nextSeq(this._sequencer);
this.insertElement(predecessor, new RGAElement(newVId, value));
}

const predecessorIdx = this.indexOfVPtr(predecessor);
const insertIdx = this.shift(predecessorIdx + 1, ptr);
// Function to insert a new element into the array
private insertElement(
predecessor: Identifier,
element: RGAElement<T>
): void {
const predecessorIdx = this.indexOfVId(predecessor);
const insertIdx = this.shift(predecessorIdx + 1, element.vid);
this._sequencer = {
counter: Math.max(this._sequencer.counter, ptr.counter),
counter: Math.max(this._sequencer.counter, element.vid.counter),
nodeId: this._sequencer.nodeId,
};
this._elements.splice(insertIdx, 0, new RGAElement(ptr, value));
// Check if its a duplicate
if (
insertIdx < this._elements.length &&
this._elements[insertIdx].vid.counter === element.vid.counter &&
this._elements[insertIdx].vid.nodeId === element.vid.nodeId
) {
return;
}
this._elements.splice(insertIdx, 0, element);
}

// Function to delete a vertex from the graph
// Function to delete an element from the RGA
delete(index: number): void {
const i = this.indexWithTombstones(index);
const ptr = this._elements[i].id;
index = this.indexOfVPtr(ptr);
this._elements[index].value = null;
let i = this.indexWithTombstones(index);
while (this.isTombstone(this._elements[i])) i++;
this._elements[i].value = null;
}

// Function to update the value of a vertex
// Function to update the value of an element
update(index: number, value: T): void {
const i = this.indexWithTombstones(index);
let i = this.indexWithTombstones(index);
while (this.isTombstone(this._elements[i])) i++;
this._elements[i].value = value;
}

// Merge another RGA instance into this one
merge(peerRGA: RGA<T>): void {
const newVertices: RGAElement<T>[] = [];

for (let i = 1; i < peerRGA._elements.length; i++) {
this.insert(i, peerRGA._elements[i].value!);
}

// Deduplicate and merge the vertices
const seen: Set<string> = new Set();
for (const vertex of this._elements) {
const key = `${vertex.id.counter}_${vertex.id.nodeId}`;
if (!seen.has(key)) {
newVertices.push(vertex);
seen.add(key);
} else {
const existingIndex = newVertices.findIndex(
(v) =>
v.id.counter === vertex.id.counter &&
v.id.nodeId === vertex.id.nodeId
);
if (existingIndex !== -1 && vertex.value === null) {
newVertices[existingIndex].value = null; // Ensure tombstone is applied
}
}
for (let i = 1; i < peerRGA.elements().length; i++) {
this.insertElement(
peerRGA.elements()[i - 1].vid,
peerRGA.elements()[i]
);
}

this._elements = newVertices;
this._sequencer = {
counter: Math.max(this._sequencer.counter, peerRGA._sequencer.counter),
counter: Math.max(
this._sequencer.counter,
peerRGA._sequencer.counter
),
nodeId: this._sequencer.nodeId,
};
}
Expand Down
35 changes: 17 additions & 18 deletions packages/crdt/tests/RGA.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("Replicable Growable Array Tests", () => {
rga.insert(1, "C");
rga.insert(0, "D");

expect(rga.getElements()).toEqual(["D", "A", "C", "B"]);
expect(rga.getArray()).toEqual(["D", "A", "C", "B"]);
});

test("Test Read", () => {
Expand All @@ -35,25 +35,25 @@ describe("Replicable Growable Array Tests", () => {
rga.insert(1, "C");
rga.delete(0);
rga.delete(0);
expect(rga.getElements()).toEqual(["B"]);
expect(rga.getArray()).toEqual(["B"]);

rga.clear();

rga.insert(0, "A");
rga.insert(1, "B");
rga.delete(0);

expect(rga.getElements()).toEqual(["B"]);
expect(rga.getArray()).toEqual(["B"]);

rga.insert(0, "C");
rga.insert(1, "D");
expect(rga.getElements()).toEqual(["C", "D", "B"]);
expect(rga.getArray()).toEqual(["C", "D", "B"]);

rga.delete(1);
expect(rga.getElements()).toEqual(["C", "B"]);
expect(rga.getArray()).toEqual(["C", "B"]);

rga.delete(1);
expect(rga.getElements()).toEqual(["C"]);
expect(rga.getArray()).toEqual(["C"]);

peerRGA.insert(0, "E");
peerRGA.insert(0, "F");
Expand All @@ -62,7 +62,7 @@ describe("Replicable Growable Array Tests", () => {
peerRGA.delete(1);
peerRGA.delete(1);
peerRGA.delete(1);
expect(peerRGA.getElements()).toEqual(["F"]);
expect(peerRGA.getArray()).toEqual(["F"]);
});

test("Test Update", () => {
Expand All @@ -71,7 +71,7 @@ describe("Replicable Growable Array Tests", () => {
rga.update(0, "C");
rga.update(1, "D");

expect(rga.getElements()).toEqual(["C", "D"]);
expect(rga.getArray()).toEqual(["C", "D"]);
});

test("Test Merge Order", () => {
Expand All @@ -80,35 +80,34 @@ describe("Replicable Growable Array Tests", () => {

peerRGA.insert(0, "C");
peerRGA.insert(1, "D");
peerRGA.insert(2, "E");

rga.merge(peerRGA);

expect(rga.getElements()).toEqual(["A", "C", "B", "D"]);
expect(rga.getArray()).toEqual(["A", "C", "B", "D"]);

Check failure on line 87 in packages/crdt/tests/RGA.test.ts

View workflow job for this annotation

GitHub Actions / tests

packages/crdt/tests/RGA.test.ts > Replicable Growable Array Tests > Test Merge Order

AssertionError: expected [ 'C', 'D', 'E', 'A', 'B' ] to deeply equal [ 'A', 'C', 'B', 'D' ] - Expected + Received Array [ + "C", + "D", + "E", "A", - "C", "B", - "D", ] ❯ packages/crdt/tests/RGA.test.ts:87:32
});

test("Test Merge with Delete", () => {
rga.insert(0, 'A1');
peerRGA.insert(0, 'B1');
rga.insert(0, "A1");
peerRGA.insert(0, "B1");

// Sync both replicas, both should be ["A1", "B1"]
rga.merge(peerRGA);
peerRGA.merge(rga);



// console.log(rga.elements());
// console.log(peerRGA.elements());
rga.insert(1, 'A2');
rga.insert(1, "A2");
peerRGA.delete(1);
// console.log(rga.elements());
// console.log(peerRGA.elements());

expect(rga.getElements()).toEqual(['A1', 'A2', 'B1']);
expect(peerRGA.getElements()).toEqual(['A1']);
expect(rga.getArray()).toEqual(["A1", "A2", "B1"]);

Check failure on line 105 in packages/crdt/tests/RGA.test.ts

View workflow job for this annotation

GitHub Actions / tests

packages/crdt/tests/RGA.test.ts > Replicable Growable Array Tests > Test Merge with Delete

AssertionError: expected [ 'A2', 'A1' ] to deeply equal [ 'A1', 'A2', 'B1' ] - Expected + Received Array [ - "A1", "A2", - "B1", + "A1", ] ❯ packages/crdt/tests/RGA.test.ts:105:32
expect(peerRGA.getArray()).toEqual(["A1"]);

rga.merge(peerRGA);
peerRGA.merge(rga);

expect(rga.getElements()).toEqual(peerRGA.getElements());
expect(rga.getArray()).toEqual(peerRGA.getArray());
});

});

0 comments on commit 715071e

Please sign in to comment.