defmodule Phoenix.Tracker.Clock do @moduledoc false alias Phoenix.Tracker.State @type context :: State.context @type clock :: {State.name, context} @doc """ Returns a list of replicas from a list of contexts. """ @spec clockset_replicas([clock]) :: [State.name] def clockset_replicas(clockset) do for {replica, _} <- clockset, do: replica end @doc """ Adds a replicas context to a clockset, keeping only dominate contexts. """ @spec append_clock([clock], clock) :: [clock] def append_clock(clockset, {_, clock}) when map_size(clock) == 0, do: clockset def append_clock(clockset, {node, clock}) do big_clock = combine_clocks(clockset) cond do dominates?(clock, big_clock) -> [{node, clock}] dominates?(big_clock, clock) -> clockset true -> filter_clocks(clockset, {node, clock}) end end @doc """ Checks if one clock causally dominates the other for all replicas. """ @spec dominates?(context, context) :: boolean def dominates?(c1, c2) when map_size(c1) < map_size(c2), do: false def dominates?(c1, c2) do Enum.reduce_while(c2, true, fn {replica, clock}, true -> if Map.get(c1, replica, 0) >= clock do {:cont, true} else {:halt, false} end end) end @doc """ Checks if one clock causally dominates the other for their shared replicas. """ def dominates_or_equal?(c1, c2) when c1 == %{} and c2 == %{}, do: true def dominates_or_equal?(c1, _c2) when c1 == %{}, do: false def dominates_or_equal?(c1, c2) do Enum.reduce_while(c1, true, fn {replica, clock}, true -> if clock >= Map.get(c2, replica, 0) do {:cont, true} else {:halt, false} end end) end @doc """ Returns the upper bound causal context of two clocks. """ def upperbound(c1, c2) do Map.merge(c1, c2, fn _, v1, v2 -> max(v1, v2) end) end @doc """ Returns the lower bound causal context of two clocks. """ def lowerbound(c1, c2) do Map.merge(c1, c2, fn _, v1, v2 -> min(v1, v2) end) end @doc """ Returns the clock with just provided replicas. """ def filter_replicas(c, replicas), do: Map.take(c, replicas) @doc """ Returns replicas from the given clock. """ def replicas(c), do: Map.keys(c) defp filter_clocks(clockset, {node, clock}) do clockset |> Enum.reduce({[], false}, fn {node2, clock2}, {set, insert} -> if dominates?(clock, clock2) do {set, true} else {[{node2, clock2}| set], insert || !dominates?(clock2, clock)} end end) |> case do {new_clockset, true} -> [{node, clock} | new_clockset] {new_clockset, false} -> new_clockset end end defp combine_clocks(clockset) do clockset |> Enum.map(fn {_, clocks} -> clocks end) |> Enum.reduce(%{}, &upperbound(&1, &2)) end end