Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 22 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
# vector-clock
# version-vector

Vector clock implementation for Clojure and ClojureScript.
> **⚠️ Project Renamed:** This project was previously `net.clojars.bru/vector-clock`.
> That artifact is deprecated. Please migrate to `net.clojars.bru/version-vector`.

Version vector implementation for Clojure and ClojureScript.

## Overview

This library provides vector clocks for building distributed systems in both Clojure (JVM) and ClojureScript.
This library provides version vectors for building distributed systems in both Clojure (JVM) and ClojureScript.

**Vector Clocks** - A mechanism for determining causality and detecting conflicts in distributed systems.
**Version Vectors** - A mechanism for determining causality and detecting conflicts in distributed systems.

This implementation uses a map where keys represent devices in a distributed db, and values
represent the version last contributed by that device.

## Features

- **CRDT Operations**: tick, check, merge operations for vector clocks
- **CRDT Operations**: tick, check, merge operations for version vectors
- **Causality Detection**: Determine if events are concurrent, causally related, or identical
- **Well-Tested**: Comprehensive test suite including property-based tests

Expand All @@ -22,7 +25,7 @@ represent the version last contributed by that device.
Add to your `deps.edn`:

```clojure
{:deps {net.clojars.bru/vector-clock {:mvn/version "0.1.0"}}}
{:deps {net.clojars.bru/version-vector {:mvn/version "0.2.0"}}}
```

## Platform Support
Expand All @@ -39,26 +42,26 @@ The core algorithm uses only platform-agnostic Clojure functions with no depende

```clojure
(ns my-app.sync
(:require [vector-clock.core :as vc]))
(:require [version-vector.core :as vv]))

;; Create a clock for this device
(def my-clock (vc/make-clock "device-1"))
(def my-clock (vv/make-clock "device-1"))

;; Increment the clock when making changes
(def updated-clock (vc/tick my-clock "device-1"))
(def updated-clock (vv/tick my-clock "device-1"))
; => {"device-1" 1}

;; Compare with another device's clock
(def other-clock {"device-2" 1})
(vc/check updated-clock other-clock)
(vv/check updated-clock other-clock)
; => :concurrent (both devices made independent changes)

;; Merge clocks when syncing
(def merged (vc/merge-clocks updated-clock other-clock))
(def merged (vv/merge-clocks updated-clock other-clock))
; => {"device-1" 1 "device-2" 1}
```

## Vector Clock API
## Version Vector API

### Core Operations

Expand Down Expand Up @@ -103,18 +106,18 @@ Merge two clocks by taking the maximum value for each device. Used when synchron
### Detecting Conflicts

```clojure
(require '[vector-clock.core :as vc])
(require '[version-vector.core :as vv])

;; Device 1 makes a change
(def device1-clock (vc/tick (vc/make-clock "d1") "d1"))
(def device1-clock (vv/tick (vv/make-clock "d1") "d1"))
; => {"d1" 1}

;; Device 2 makes an independent change
(def device2-clock (vc/tick (vc/make-clock "d2") "d2"))
(def device2-clock (vv/tick (vv/make-clock "d2") "d2"))
; => {"d2" 1}

;; Check for conflicts
(vc/check device1-clock device2-clock)
(vv/check device1-clock device2-clock)
; => :concurrent (conflict detected!)
```

Expand All @@ -128,16 +131,16 @@ Example: distributed note taking app
(defn update-note [note new-content device-id]
(->Note (:id note)
new-content
(vc/tick (:clock note) device-id)))
(vv/tick (:clock note) device-id)))

(defn merge-notes [note1 note2]
(case (vc/check (:clock note1) (:clock note2))
(case (vv/check (:clock note1) (:clock note2))
(:same :ancestor) note2 ; Keep newer version
:descendant note1 ; Keep newer version
:concurrent ; Conflict - needs resolution strategy
(->Note (:id note1)
(str (:content note1) "\n---CONFLICT---\n" (:content note2))
(vc/merge-clocks (:clock note1) (:clock note2)))))
(vv/merge-clocks (:clock note1) (:clock note2)))))
```

## Testing
Expand Down
14 changes: 7 additions & 7 deletions build.clj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(ns build
(:require [clojure.tools.build.api :as b]))

(def lib 'net.clojars.bru/vector-clock)
(def version "0.1.2")
(def lib 'net.clojars.bru/version-vector)
(def version "0.2.0")
(def class-dir "target/classes")
(def basis (b/create-basis {:project "deps.edn"}))
(def jar-file (format "target/%s-%s.jar" (name lib) version))
Expand All @@ -17,12 +17,12 @@
:version version
:basis basis
:src-dirs ["src"]
:scm {:url "https://github.com/bru/vector-clock"
:connection "scm:git:git://github.com/bru/vector-clock.git"
:developerConnection "scm:git:ssh://git@github.com/bru/vector-clock.git"
:scm {:url "https://github.com/bru/version-vector-clj"
:connection "scm:git:git://github.com/bru/version-vector-clj.git"
:developerConnection "scm:git:ssh://git@github.com/bru/version-vector-clj.git"
:tag (str "v" version)}
:pom-data [[:description "Vector clock implementation for Clojure and ClojureScript"]
[:url "https://github.com/bru/vector-clock"]
:pom-data [[:description "Version vector implementation for Clojure and ClojureScript"]
[:url "https://github.com/bru/version-vector-clj"]
[:licenses
[:license
[:name "Eclipse Public License 1.0"]
Expand Down
8 changes: 4 additions & 4 deletions src/vector_clock/core.cljc → src/version_vector/core.cljc
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
(ns vector-clock.core
"Vector clock implementation for distributed causality tracking.
(ns version-vector.core
"Version vector implementation for distributed causality tracking.

Vector clocks are a mechanism for determining the partial ordering of events
Version vectors are a mechanism for determining the partial ordering of events
in a distributed system. They are used to detect causality violations and
concurrent updates in distributed databases.

This implementation uses a map where keys represent devices in a distributed
db, and values represent the version last contributed by that device.")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,70 +1,70 @@
(ns vector-clock.core-test
(ns version-vector.core-test
(:require [clojure.test :refer [deftest testing is]]
[vector-clock.core :as vc]))
[version-vector.core :as vv]))

;; Vector Clock Tests

(deftest test-make-clock
(testing "Creating a new vector clock"
(let [clock (vc/make-clock "device-1")]
(let [clock (vv/make-clock "device-1")]
(is (map? clock))
(is (= 0 (get clock "device-1"))))))

(deftest test-tick
(testing "Incrementing a clock"
(let [clock (vc/make-clock "device-1")
ticked (vc/tick clock "device-1")]
(let [clock (vv/make-clock "device-1")
ticked (vv/tick clock "device-1")]
(is (= 1 (get ticked "device-1")))

(let [ticked-twice (vc/tick ticked "device-1")]
(let [ticked-twice (vv/tick ticked "device-1")]
(is (= 2 (get ticked-twice "device-1")))))))

(deftest test-tick-new-device
(testing "Ticking a clock for a new device"
(let [clock (vc/make-clock "device-1")
ticked (vc/tick clock "device-2")]
(let [clock (vv/make-clock "device-1")
ticked (vv/tick clock "device-2")]
(is (= 1 (get ticked "device-2")))
(is (= 0 (get ticked "device-1"))))))

(deftest test-check-same
(testing "Checking identical clocks"
(let [c1 {"a" 1 "b" 2}
c2 {"a" 1 "b" 2}]
(is (= :same (vc/check c1 c2))))))
(is (= :same (vv/check c1 c2))))))

(deftest test-check-ancestor
(testing "Checking ancestor relationship"
(let [c1 {"a" 1 "b" 1}
c2 {"a" 2 "b" 1}]
(is (= :ancestor (vc/check c1 c2))))))
(is (= :ancestor (vv/check c1 c2))))))

(deftest test-check-descendant
(testing "Checking descendant relationship"
(let [c1 {"a" 2 "b" 1}
c2 {"a" 1 "b" 1}]
(is (= :descendant (vc/check c1 c2))))))
(is (= :descendant (vv/check c1 c2))))))

(deftest test-check-concurrent
(testing "Checking concurrent clocks"
(let [c1 {"a" 2 "b" 1}
c2 {"a" 1 "b" 2}]
(is (= :concurrent (vc/check c1 c2))))))
(is (= :concurrent (vv/check c1 c2))))))

(deftest test-check-with-missing-keys
(testing "Checking clocks with different devices"
(let [c1 {"a" 1}
c2 {"b" 1}]
(is (= :concurrent (vc/check c1 c2))))
(is (= :concurrent (vv/check c1 c2))))

(let [c1 {"a" 1}
c2 {"a" 1 "b" 1}]
(is (= :ancestor (vc/check c1 c2))))))
(is (= :ancestor (vv/check c1 c2))))))

(deftest test-merge-clocks
(testing "Merging two clocks"
(let [c1 {"a" 3 "b" 1}
c2 {"a" 2 "b" 5 "c" 1}
merged (vc/merge-clocks c1 c2)]
merged (vv/merge-clocks c1 c2)]
(is (= 3 (get merged "a")))
(is (= 5 (get merged "b")))
(is (= 1 (get merged "c"))))))
Expand All @@ -73,87 +73,87 @@
(testing "Merging completely disjoint clocks"
(let [c1 {"a" 1}
c2 {"b" 2}
merged (vc/merge-clocks c1 c2)]
merged (vv/merge-clocks c1 c2)]
(is (= 1 (get merged "a")))
(is (= 2 (get merged "b"))))))

;; Property-based tests (simple versions without test.check)

(deftest test-tick-monotonic
(testing "Ticking always increases the counter"
(let [clock (vc/make-clock "device-1")
ticked1 (vc/tick clock "device-1")
ticked2 (vc/tick ticked1 "device-1")]
(let [clock (vv/make-clock "device-1")
ticked1 (vv/tick clock "device-1")
ticked2 (vv/tick ticked1 "device-1")]
(is (< (get clock "device-1") (get ticked1 "device-1")))
(is (< (get ticked1 "device-1") (get ticked2 "device-1"))))))

(deftest test-merge-commutative
(testing "Merge is commutative"
(let [c1 {"a" 1 "b" 5}
c2 {"a" 3 "c" 2}
merged1 (vc/merge-clocks c1 c2)
merged2 (vc/merge-clocks c2 c1)]
merged1 (vv/merge-clocks c1 c2)
merged2 (vv/merge-clocks c2 c1)]
(is (= merged1 merged2)))))

(deftest test-merge-associative
(testing "Merge is associative"
(let [c1 {"a" 1}
c2 {"b" 2}
c3 {"c" 3}
merged1 (vc/merge-clocks (vc/merge-clocks c1 c2) c3)
merged2 (vc/merge-clocks c1 (vc/merge-clocks c2 c3))]
merged1 (vv/merge-clocks (vv/merge-clocks c1 c2) c3)
merged2 (vv/merge-clocks c1 (vv/merge-clocks c2 c3))]
(is (= merged1 merged2)))))

(deftest test-merge-idempotent
(testing "Merging a clock with itself returns the same clock"
(let [clock {"a" 1 "b" 2}
merged (vc/merge-clocks clock clock)]
merged (vv/merge-clocks clock clock)]
(is (= clock merged)))))

(deftest test-check-reflexive
(testing "A clock is always :same with itself"
(let [clock {"a" 1 "b" 2}]
(is (= :same (vc/check clock clock))))))
(is (= :same (vv/check clock clock))))))

(deftest test-check-antisymmetric
(testing "If c1 is ancestor of c2, then c2 is descendant of c1"
(let [c1 {"a" 1}
c2 {"a" 2}]
(when (= :ancestor (vc/check c1 c2))
(is (= :descendant (vc/check c2 c1)))))))
(when (= :ancestor (vv/check c1 c2))
(is (= :descendant (vv/check c2 c1)))))))

(deftest test-check-transitive
(testing "If c1 < c2 and c2 < c3, then c1 < c3"
(let [c1 {"a" 1}
c2 {"a" 2}
c3 {"a" 3}]
(is (= :ancestor (vc/check c1 c2)))
(is (= :ancestor (vc/check c2 c3)))
(is (= :ancestor (vc/check c1 c3))))))
(is (= :ancestor (vv/check c1 c2)))
(is (= :ancestor (vv/check c2 c3)))
(is (= :ancestor (vv/check c1 c3))))))

;; Practical scenario tests

(deftest test-distributed-scenario
(testing "Simulating a distributed update scenario"
;; Device 1 makes an update
(let [d1-clock (vc/tick (vc/make-clock "d1") "d1")
(let [d1-clock (vv/tick (vv/make-clock "d1") "d1")

;; Device 2 makes an independent update
d2-clock (vc/tick (vc/make-clock "d2") "d2")]
d2-clock (vv/tick (vv/make-clock "d2") "d2")]

;; These are concurrent
(is (= :concurrent (vc/check d1-clock d2-clock)))
(is (= :concurrent (vv/check d1-clock d2-clock)))

;; Both devices sync and merge
(let [merged-at-d1 (vc/merge-clocks d1-clock d2-clock)
merged-at-d2 (vc/merge-clocks d2-clock d1-clock)]
(let [merged-at-d1 (vv/merge-clocks d1-clock d2-clock)
merged-at-d2 (vv/merge-clocks d2-clock d1-clock)]

;; Merged clocks are identical
(is (= merged-at-d1 merged-at-d2))

;; Device 1 makes another update after sync
(let [d1-after-sync (vc/tick merged-at-d1 "d1")]
(let [d1-after-sync (vv/tick merged-at-d1 "d1")]

;; This new update is a descendant of the merged clock
(is (= :ancestor (vc/check merged-at-d1 d1-after-sync)))
(is (= :descendant (vc/check d1-after-sync merged-at-d1))))))))
(is (= :ancestor (vv/check merged-at-d1 d1-after-sync)))
(is (= :descendant (vv/check d1-after-sync merged-at-d1))))))))