From 96f1d9117842f35a13d0fbc7337e315cb090453e Mon Sep 17 00:00:00 2001 From: joao Date: Mon, 29 Jul 2024 10:51:56 +0100 Subject: [PATCH 1/6] Add G-Counter CRDT --- packages/crdt/src/builtins/GCounter/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/crdt/src/builtins/GCounter/index.ts b/packages/crdt/src/builtins/GCounter/index.ts index bf73bb28..1e7cad65 100644 --- a/packages/crdt/src/builtins/GCounter/index.ts +++ b/packages/crdt/src/builtins/GCounter/index.ts @@ -27,15 +27,20 @@ export class GCounter { } return true; } - + merge(peerCounter: GCounter): void { let temp: { [nodeKey: string]: number } = Object.assign( {}, this.counts, peerCounter.counts, ); + Object.keys(temp).forEach((key) => { this.counts[key] = Math.max(this.counts[key], peerCounter.counts[key]); + }); + + this.globalCounter = Object.values(this.counts).reduce((a, b) => a + b, 0); + } -} +} \ No newline at end of file From bc7992e5c635034efe0ba84f747039a8d2bef4fb Mon Sep 17 00:00:00 2001 From: joao Date: Mon, 29 Jul 2024 11:09:49 +0100 Subject: [PATCH 2/6] Tests for GCounter & PNCounter CRDT --- packages/crdt/tests/GCounter.test.ts | 69 +++++++++++++++++++++++++++ packages/crdt/tests/PNCounter.test.ts | 60 +++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 packages/crdt/tests/GCounter.test.ts create mode 100644 packages/crdt/tests/PNCounter.test.ts diff --git a/packages/crdt/tests/GCounter.test.ts b/packages/crdt/tests/GCounter.test.ts new file mode 100644 index 00000000..159933f0 --- /dev/null +++ b/packages/crdt/tests/GCounter.test.ts @@ -0,0 +1,69 @@ +import { describe, test, expect, beforeEach } from "vitest"; +import { GCounter } from "../src/builtins/GCounter"; + +describe("G-Counter Tests", () => { + + let set1: GCounter; + let set2: GCounter; + + beforeEach(() => { + set1 = new GCounter({ "node1": 5, "node2": 10}); + set2 = new GCounter({ "node1": 5, "node2": 10}); + }); + + test("Test Initial Values", () => { + expect(set1.value()).toBe(15); + expect(set2.value()).toBe(15); + }); + + test("Test Increment", () => { + + set1.increment("node1", 10); + set1.increment("node2", 5); + + expect(set1.value()).toBe(30); + }); + + test("Test Compare", () => { + + expect(set1.compare(set2)).toBe(true); + + set2.increment("node1", 5); + + expect(set1.compare(set2)).toBe(false); + + let set3 = new GCounter({ "node1": 5, "node2": 10, "node3": 15 }); + + expect(set1.compare(set3)).toBe(false); + }); + + test("Test Merge", () => { + + const counter1 = new GCounter({ "node1": 5 }); + const counter2 = new GCounter({ "node2": 10 }); + + counter1.merge(counter2); + + expect(counter1.counts).toEqual({ "node1": 5, "node2": 10 }); + expect(counter1.value()).toBe(15); + + set1.increment("node1", 5); + set2.increment("node2", 10); + expect(set1.value()).toBe(35); + expect(set2.value()).toBe(40); + set1.merge(set2); + expect(set1.value()).toBe(45); + expect(set1.counts[0]).toBe(10); + expect(set1.counts[1]).toBe(20); + expect(set1.counts[2]).toBe(15); + + set2.merge(set1); + expect(set2.value()).toBe(45); + expect(set2.counts[0]).toBe(10); + expect(set2.counts[1]).toBe(20); + expect(set2.counts[2]).toBe(15); + + expect(set2.compare(set1)).toBe(true); + }); + +}); \ No newline at end of file diff --git a/packages/crdt/tests/PNCounter.test.ts b/packages/crdt/tests/PNCounter.test.ts new file mode 100644 index 00000000..060d00ee --- /dev/null +++ b/packages/crdt/tests/PNCounter.test.ts @@ -0,0 +1,60 @@ +import { describe, test, expect, beforeEach, afterEach } from "vitest"; +import { PNCounter } from "../src/builtins/PNCounter"; +import { GCounter } from "../src/builtins/GCounter"; + +describe("PN-Counter Tests", () => { + + let set1: PNCounter; + let set2: PNCounter; + + beforeEach(() => { + set1 = new PNCounter(new GCounter({ "node1": 5, "node2": 10, "node3": 15 }), new GCounter({ "node1": 3, "node2": 4, "node3": 3 })); + set2 = new PNCounter(new GCounter({ "node1": 5, "node2": 10, "node3": 15 }), new GCounter({ "node1": 3, "node2": 4, "node3": 3 })); + }); + + test("Test Initial Value", () => { + expect(set1.value()).toBe(20); + expect(set2.value()).toBe(20); + }); + + test("Test Increment", () => { + set1.increment("node1",10); + set2.increment("node1",20); + expect(set1.value()).toBe(30); + expect(set2.value()).toBe(40); + }); + + test("Test Decrement", () => { + set1.decrement("node1",10); + set2.decrement("node1",20); + expect(set1.value()).toBe(10); + expect(set2.value()).toBe(0); + }); + + test("Test Compare", () => { + expect(set1.compare(set2)).toBe(true); + + set1.decrement("node1",10); + + expect(set1.compare(set2)).toBe(false); + + set2.decrement("node1",10); + + expect(set1.compare(set2)).toBe(true); + }); + + test("Test Merge", () => { + set1.increment("node1",10); + set2.decrement("node2",5); + + expect(set1.compare(set2)).toBe(false); + expect(set2.compare(set1)).toBe(false); + + set1.merge(set2); + set2.merge(set1); + + expect(set1.compare(set2)).toBe(true); + expect(set2.compare(set1)).toBe(true); + }); + +}); \ No newline at end of file From 568394ace0ae96625c7daf3e59b16ae63e249019 Mon Sep 17 00:00:00 2001 From: Oak Date: Mon, 29 Jul 2024 11:42:13 +0100 Subject: [PATCH 3/6] Apply suggestions from code review --- packages/crdt/src/builtins/GCounter/index.ts | 2 -- packages/crdt/tests/GCounter.test.ts | 3 --- packages/crdt/tests/PNCounter.test.ts | 1 - 3 files changed, 6 deletions(-) diff --git a/packages/crdt/src/builtins/GCounter/index.ts b/packages/crdt/src/builtins/GCounter/index.ts index 1e7cad65..3b0af74d 100644 --- a/packages/crdt/src/builtins/GCounter/index.ts +++ b/packages/crdt/src/builtins/GCounter/index.ts @@ -27,7 +27,6 @@ export class GCounter { } return true; } - merge(peerCounter: GCounter): void { let temp: { [nodeKey: string]: number } = Object.assign( {}, @@ -37,7 +36,6 @@ export class GCounter { Object.keys(temp).forEach((key) => { this.counts[key] = Math.max(this.counts[key], peerCounter.counts[key]); - }); this.globalCounter = Object.values(this.counts).reduce((a, b) => a + b, 0); diff --git a/packages/crdt/tests/GCounter.test.ts b/packages/crdt/tests/GCounter.test.ts index 159933f0..ceeb4334 100644 --- a/packages/crdt/tests/GCounter.test.ts +++ b/packages/crdt/tests/GCounter.test.ts @@ -17,7 +17,6 @@ describe("G-Counter Tests", () => { }); test("Test Increment", () => { - set1.increment("node1", 10); set1.increment("node2", 5); @@ -25,7 +24,6 @@ describe("G-Counter Tests", () => { }); test("Test Compare", () => { - expect(set1.compare(set2)).toBe(true); set2.increment("node1", 5); @@ -38,7 +36,6 @@ describe("G-Counter Tests", () => { }); test("Test Merge", () => { - const counter1 = new GCounter({ "node1": 5 }); const counter2 = new GCounter({ "node2": 10 }); diff --git a/packages/crdt/tests/PNCounter.test.ts b/packages/crdt/tests/PNCounter.test.ts index 060d00ee..d4e4ae28 100644 --- a/packages/crdt/tests/PNCounter.test.ts +++ b/packages/crdt/tests/PNCounter.test.ts @@ -3,7 +3,6 @@ import { PNCounter } from "../src/builtins/PNCounter"; import { GCounter } from "../src/builtins/GCounter"; describe("PN-Counter Tests", () => { - let set1: PNCounter; let set2: PNCounter; From 8a4db4c43fc4383da4b74d63104a1a9c71febe23 Mon Sep 17 00:00:00 2001 From: Oak Date: Mon, 29 Jul 2024 11:43:53 +0100 Subject: [PATCH 4/6] Apply suggestions from code review --- packages/crdt/tests/GCounter.test.ts | 2 -- packages/crdt/tests/PNCounter.test.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/packages/crdt/tests/GCounter.test.ts b/packages/crdt/tests/GCounter.test.ts index ceeb4334..77bea4f8 100644 --- a/packages/crdt/tests/GCounter.test.ts +++ b/packages/crdt/tests/GCounter.test.ts @@ -2,7 +2,6 @@ import { describe, test, expect, beforeEach } from "vitest"; import { GCounter } from "../src/builtins/GCounter"; describe("G-Counter Tests", () => { - let set1: GCounter; let set2: GCounter; @@ -62,5 +61,4 @@ describe("G-Counter Tests", () => { expect(set2.compare(set1)).toBe(true); }); - }); \ No newline at end of file diff --git a/packages/crdt/tests/PNCounter.test.ts b/packages/crdt/tests/PNCounter.test.ts index d4e4ae28..a6c4488d 100644 --- a/packages/crdt/tests/PNCounter.test.ts +++ b/packages/crdt/tests/PNCounter.test.ts @@ -55,5 +55,4 @@ describe("PN-Counter Tests", () => { expect(set1.compare(set2)).toBe(true); expect(set2.compare(set1)).toBe(true); }); - }); \ No newline at end of file From f4ca061ba707d8b5ddb8dcc700de9420897ce926 Mon Sep 17 00:00:00 2001 From: joao Date: Mon, 29 Jul 2024 18:57:24 +0100 Subject: [PATCH 5/6] fix: merge & compare fixed --- packages/crdt/src/builtins/GCounter/index.ts | 10 ++--- packages/crdt/tests/GCounter.test.ts | 44 +++++--------------- packages/crdt/tests/PNCounter.test.ts | 7 ---- 3 files changed, 14 insertions(+), 47 deletions(-) diff --git a/packages/crdt/src/builtins/GCounter/index.ts b/packages/crdt/src/builtins/GCounter/index.ts index 3b0af74d..a5f03b3a 100644 --- a/packages/crdt/src/builtins/GCounter/index.ts +++ b/packages/crdt/src/builtins/GCounter/index.ts @@ -20,13 +20,9 @@ export class GCounter { } compare(peerCounter: GCounter): boolean { - for (let key in Object.keys(this.counts)) { - if (this.counts[key] > peerCounter.counts[key]) { - return false; - } - } - return true; + return (this.counts.length === peerCounter.counts.length && Object.keys(this.counts).every(key => this.counts[key] <= peerCounter.counts[key])); } + merge(peerCounter: GCounter): void { let temp: { [nodeKey: string]: number } = Object.assign( {}, @@ -35,7 +31,7 @@ export class GCounter { ); Object.keys(temp).forEach((key) => { - this.counts[key] = Math.max(this.counts[key], peerCounter.counts[key]); + this.counts[key] = Math.max(this.counts[key] || 0, peerCounter.counts[key] || 0); }); this.globalCounter = Object.values(this.counts).reduce((a, b) => a + b, 0); diff --git a/packages/crdt/tests/GCounter.test.ts b/packages/crdt/tests/GCounter.test.ts index 77bea4f8..7aecf6e6 100644 --- a/packages/crdt/tests/GCounter.test.ts +++ b/packages/crdt/tests/GCounter.test.ts @@ -3,16 +3,13 @@ import { GCounter } from "../src/builtins/GCounter"; describe("G-Counter Tests", () => { let set1: GCounter; - let set2: GCounter; beforeEach(() => { set1 = new GCounter({ "node1": 5, "node2": 10}); - set2 = new GCounter({ "node1": 5, "node2": 10}); }); test("Test Initial Values", () => { expect(set1.value()).toBe(15); - expect(set2.value()).toBe(15); }); test("Test Increment", () => { @@ -23,42 +20,23 @@ describe("G-Counter Tests", () => { }); test("Test Compare", () => { - expect(set1.compare(set2)).toBe(true); - - set2.increment("node1", 5); - - expect(set1.compare(set2)).toBe(false); - + let set2 = new GCounter({ "node1": 5, "node2": 10}); let set3 = new GCounter({ "node1": 5, "node2": 10, "node3": 15 }); + expect(set1.compare(set2)).toBe(true); + set1.increment("node1", 5); + expect(set1.compare(set2)).toBe(false); expect(set1.compare(set3)).toBe(false); }); test("Test Merge", () => { - const counter1 = new GCounter({ "node1": 5 }); - const counter2 = new GCounter({ "node2": 10 }); - - counter1.merge(counter2); - - expect(counter1.counts).toEqual({ "node1": 5, "node2": 10 }); - expect(counter1.value()).toBe(15); - - set1.increment("node1", 5); - set2.increment("node2", 10); - expect(set1.value()).toBe(35); - expect(set2.value()).toBe(40); - set1.merge(set2); - expect(set1.value()).toBe(45); - expect(set1.counts[0]).toBe(10); - expect(set1.counts[1]).toBe(20); - expect(set1.counts[2]).toBe(15); - + let set2 = new GCounter({ "node1": 3, "node2": 10}); + let set3 = new GCounter({ "node1": 5, "node3": 15}); + + expect(set1.counts).toEqual({"node1": 5, "node2": 10}); set2.merge(set1); - expect(set2.value()).toBe(45); - expect(set2.counts[0]).toBe(10); - expect(set2.counts[1]).toBe(20); - expect(set2.counts[2]).toBe(15); - - expect(set2.compare(set1)).toBe(true); + expect(set2.counts).toEqual({"node1": 5, "node2": 10}); + set1.merge(set3); + expect(set1.counts).toEqual({"node1": 5, "node2": 10, "node3": 15}); }); }); \ No newline at end of file diff --git a/packages/crdt/tests/PNCounter.test.ts b/packages/crdt/tests/PNCounter.test.ts index a6c4488d..89c6819f 100644 --- a/packages/crdt/tests/PNCounter.test.ts +++ b/packages/crdt/tests/PNCounter.test.ts @@ -32,26 +32,19 @@ describe("PN-Counter Tests", () => { test("Test Compare", () => { expect(set1.compare(set2)).toBe(true); - set1.decrement("node1",10); - expect(set1.compare(set2)).toBe(false); - set2.decrement("node1",10); - expect(set1.compare(set2)).toBe(true); }); test("Test Merge", () => { set1.increment("node1",10); set2.decrement("node2",5); - expect(set1.compare(set2)).toBe(false); expect(set2.compare(set1)).toBe(false); - set1.merge(set2); set2.merge(set1); - expect(set1.compare(set2)).toBe(true); expect(set2.compare(set1)).toBe(true); }); From 5b614ab094942e3709940111e50867c38f9eaefc Mon Sep 17 00:00:00 2001 From: Oak Date: Tue, 30 Jul 2024 05:40:45 +0100 Subject: [PATCH 6/6] Update packages/crdt/src/builtins/GCounter/index.ts --- packages/crdt/src/builtins/GCounter/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/crdt/src/builtins/GCounter/index.ts b/packages/crdt/src/builtins/GCounter/index.ts index a5f03b3a..e518c359 100644 --- a/packages/crdt/src/builtins/GCounter/index.ts +++ b/packages/crdt/src/builtins/GCounter/index.ts @@ -35,6 +35,5 @@ export class GCounter { }); this.globalCounter = Object.values(this.counts).reduce((a, b) => a + b, 0); - } } \ No newline at end of file