VectorClock.java

package com.oliwier.listmebackend.crdt;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Immutable vector clock keyed by deviceId (String UUID).
 *
 * Tracks causal ordering of operations across devices:
 *   - BEFORE: this happened before other (all counters <=, at least one <)
 *   - AFTER:  this happened after other  (all counters >=, at least one >)
 *   - CONCURRENT: neither dominates → conflict, needs CRDT merge
 *   - EQUAL: identical clocks
 */
public final class VectorClock {

    private final Map<String, Long> clock;

    public VectorClock() {
        this.clock = Collections.emptyMap();
    }

    private VectorClock(Map<String, Long> clock) {
        this.clock = Collections.unmodifiableMap(new HashMap<>(clock));
    }

    public static VectorClock of(Map<String, Long> map) {
        if (map == null || map.isEmpty()) return new VectorClock();
        return new VectorClock(map);
    }

    /** Increment this device's counter, returning a new VectorClock. */
    public VectorClock increment(String deviceId) {
        Map<String, Long> next = new HashMap<>(clock);
        next.merge(deviceId, 1L, (a, b) -> a + b);
        return new VectorClock(next);
    }

    /** Merge two clocks by taking the maximum counter per device. */
    public VectorClock merge(VectorClock other) {
        Set<String> allDevices = Stream.concat(
                clock.keySet().stream(),
                other.clock.keySet().stream()
        ).collect(Collectors.toSet());

        Map<String, Long> merged = new HashMap<>();
        for (String device : allDevices) {
            long a = clock.getOrDefault(device, 0L);
            long b = other.clock.getOrDefault(device, 0L);
            merged.put(device, Math.max(a, b));
        }
        return new VectorClock(merged);
    }

    public enum Relation { BEFORE, AFTER, CONCURRENT, EQUAL }

    /**
     * Compare this clock against other.
     * Returns the causal relationship of this relative to other.
     */
    public Relation compare(VectorClock other) {
        Set<String> allDevices = Stream.concat(
                clock.keySet().stream(),
                other.clock.keySet().stream()
        ).collect(Collectors.toSet());

        boolean thisHasGreater = false;
        boolean otherHasGreater = false;

        for (String device : allDevices) {
            long a = clock.getOrDefault(device, 0L);
            long b = other.clock.getOrDefault(device, 0L);
            if (a > b) thisHasGreater = true;
            if (b > a) otherHasGreater = true;
        }

        if (!thisHasGreater && !otherHasGreater) return Relation.EQUAL;
        if (thisHasGreater && !otherHasGreater) return Relation.AFTER;
        if (!thisHasGreater) return Relation.BEFORE;
        return Relation.CONCURRENT;
    }

    public long get(String deviceId) {
        return clock.getOrDefault(deviceId, 0L);
    }

    /** Returns a mutable copy suitable for DB storage (JSONB map). */
    public Map<String, Long> toMap() {
        return new HashMap<>(clock);
    }

    @Override
    public String toString() {
        return clock.toString();
    }
}