From 8ffdcec7d34cf7a0ada6c3ae013b1f322f11b3d6 Mon Sep 17 00:00:00 2001 From: Sacha Froment Date: Mon, 17 Feb 2025 13:28:53 +0100 Subject: [PATCH] refactor(object): linearize pair semantic (#476) --- .../object/src/linearize/pairSemantics.ts | 51 ++-- packages/object/tests/linearize/multi.test.ts | 240 ++++++++++++++++++ 2 files changed, 263 insertions(+), 28 deletions(-) create mode 100644 packages/object/tests/linearize/multi.test.ts diff --git a/packages/object/src/linearize/pairSemantics.ts b/packages/object/src/linearize/pairSemantics.ts index c1036fb3..9c3d3066 100644 --- a/packages/object/src/linearize/pairSemantics.ts +++ b/packages/object/src/linearize/pairSemantics.ts @@ -6,44 +6,39 @@ export function linearizePairSemantics( origin: Hash, subgraph: ObjectSet ): Operation[] { - const order: Hash[] = hashGraph.topologicalSort(true, origin, subgraph); - const dropped = new Array(order.length).fill(false); - const result = []; - // alway remove the first operation - let i = 1; + const order = hashGraph.topologicalSort(true, origin, subgraph); + const dropped = new Array(order.length).fill(false); + const result: Operation[] = []; + + // Skip root operation + for (let i = 1; i < order.length; i++) { + if (dropped[i]) continue; - while (i < order.length) { - if (dropped[i]) { - i++; - continue; - } let anchor = order[i]; - let j = i + 1; + let modified = false; - while (j < order.length) { - if (hashGraph.areCausallyRelatedUsingBitsets(anchor, order[j]) || dropped[j]) { - j++; + // Compare with all later operations + for (let j = i + 1; j < order.length; j++) { + if (dropped[j] || hashGraph.areCausallyRelatedUsingBitsets(anchor, order[j])) { continue; } - const moving = order[j]; const v1 = hashGraph.vertices.get(anchor); - const v2 = hashGraph.vertices.get(moving); - let action: ActionType; + const v2 = hashGraph.vertices.get(order[j]); + if (!v1 || !v2) { - action = ActionType.Nop; - } else { - action = hashGraph.resolveConflicts([v1, v2]).action; + continue; } + const { action } = hashGraph.resolveConflicts([v1, v2]); + switch (action) { case ActionType.DropLeft: dropped[i] = true; - j = order.length; + modified = true; break; case ActionType.DropRight: dropped[j] = true; - j++; break; case ActionType.Swap: hashGraph.swapReachablePredecessors(order[i], order[j]); @@ -51,17 +46,17 @@ export function linearizePairSemantics( j = i + 1; anchor = order[i]; break; - case ActionType.Nop: - j++; - break; } + + if (modified) break; } if (!dropped[i]) { - const op = hashGraph.vertices.get(order[i])?.operation; - if (op && op.value !== null) result.push(op); + const vertex = hashGraph.vertices.get(order[i]); + if (vertex?.operation && vertex.operation.value !== null) { + result.push(vertex.operation); + } } - i++; } return result; diff --git a/packages/object/tests/linearize/multi.test.ts b/packages/object/tests/linearize/multi.test.ts new file mode 100644 index 00000000..c81bc562 --- /dev/null +++ b/packages/object/tests/linearize/multi.test.ts @@ -0,0 +1,240 @@ +import { newVertex } from "@ts-drp/object/src/index.js"; +import { describe, expect, test } from "vitest"; + +import { ActionType, HashGraph, type Vertex, SemanticsType } from "../../src/hashgraph/index.js"; +import { DrpType } from "../../src/index.js"; +import { linearizeMultipleSemantics } from "../../src/linearize/multipleSemantics.js"; +import { ObjectSet } from "../../src/utils/objectSet.js"; + +describe("linearizeMultipleSemantics", () => { + test("should linearize operations in a simple sequence", () => { + const hashGraph = new HashGraph( + "", + (_vertices: Vertex[]) => ({ + action: ActionType.Nop, + }), + () => ({ + action: ActionType.Nop, + }), + SemanticsType.multiple + ); + const origin = HashGraph.rootHash; + + // Add vertices to the graph + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [1], + drpType: DrpType.DRP, + }, + hashGraph.getFrontier(), + Date.now(), + new Uint8Array() + ) + ); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [2], + drpType: DrpType.DRP, + }, + hashGraph.getFrontier(), + Date.now(), + new Uint8Array() + ) + ); + + const subgraph = new ObjectSet(); + hashGraph.getAllVertices().forEach((vertex) => subgraph.add(vertex.hash)); + + const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); + expect(result.map((op) => op.value)).toEqual([[1], [2]]); + }); + + test("should handle concurrent operations with conflict resolution", () => { + const hashGraph = new HashGraph( + "", + (_vertices: Vertex[]) => ({ + action: ActionType.Drop, + vertices: _vertices.filter((_, index) => index !== 0).map((vertex) => vertex.hash), + }), + (_vertices: Vertex[]) => ({ + action: ActionType.Drop, + vertices: _vertices.filter((_, index) => index !== 0).map((vertex) => vertex.hash), + }), + SemanticsType.multiple + ); + const origin = HashGraph.rootHash; + let frontier = hashGraph.getFrontier(); + + // Add concurrent vertices + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [1], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ) + ); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [2], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ) + ); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [3], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ) + ); + + // Get the frontier + frontier = hashGraph.getFrontier(); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [4], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ) + ); + console.log(`frontier: ${frontier}`); + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [5], + drpType: DrpType.DRP, + }, + frontier.filter((_, idx) => idx !== 0), + Date.now(), + new Uint8Array() + ) + ); + + frontier = hashGraph.getFrontier(); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [6], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ) + ); + + const subgraph = new ObjectSet(); + hashGraph.getAllVertices().forEach((vertex) => subgraph.add(vertex.hash)); + + const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); + expect(result.map((op) => op.value)).toEqual([[1], [4], [6]]); + }); + + test("should handle operations with null values", () => { + const hashGraph = new HashGraph( + "", + () => ({ + action: ActionType.Nop, + }), + () => ({ + action: ActionType.Nop, + }), + SemanticsType.multiple + ); + const origin = HashGraph.rootHash; + + // Add vertices to the graph + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: null, + drpType: DrpType.DRP, + }, + hashGraph.getFrontier(), + Date.now(), + new Uint8Array() + ) + ); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [2], + drpType: DrpType.DRP, + }, + hashGraph.getFrontier(), + Date.now(), + new Uint8Array() + ) + ); + + const subgraph = new ObjectSet(); + hashGraph.getAllVertices().forEach((vertex) => subgraph.add(vertex.hash)); + + const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); + expect(result.map((op) => op.value)).toEqual([[2]]); + }); + + test("should handle empty subgraph", () => { + const hashGraph = new HashGraph( + "", + () => ({ + action: ActionType.Nop, + }), + () => ({ + action: ActionType.Nop, + }), + SemanticsType.multiple + ); + const origin = HashGraph.rootHash; + + const subgraph = new ObjectSet(); + subgraph.add(origin); + + const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); + expect(result).toEqual([]); + }); +});