From 3b7187c5f19ebdb1bb003e24a7c889efa0dc0e6c Mon Sep 17 00:00:00 2001 From: point Date: Tue, 23 Apr 2024 19:37:02 +0300 Subject: [PATCH] Type.Map; unify highest_clock_* set of functions --- lib/y/decoder.ex | 2 +- lib/y/doc.ex | 84 ++++--- lib/y/encoder.ex | 2 +- lib/y/item.ex | 10 +- lib/y/transaction.ex | 2 +- lib/y/type.ex | 10 +- lib/y/type/array.ex | 12 +- lib/y/type/array/array_tree.ex | 20 +- lib/y/type/map.ex | 373 ++++++++++++++++++++++++++++++++ lib/y/type/unknown.ex | 66 +++--- test/array_test.exs | 26 +++ test/encoding_decoding_test.exs | 25 +-- test/map_test.exs | 55 +++++ 13 files changed, 599 insertions(+), 88 deletions(-) create mode 100644 lib/y/type/map.ex create mode 100644 test/map_test.exs diff --git a/lib/y/decoder.ex b/lib/y/decoder.ex index 7bb8806..b322709 100644 --- a/lib/y/decoder.ex +++ b/lib/y/decoder.ex @@ -24,7 +24,7 @@ defmodule Y.Decoder do ] end - def decode(msg, transaction) do + def apply(msg, transaction) do {_u, msg} = read_uint(msg) {key_clock, msg} = read_uint_array(msg) {client, msg} = read_uint_array(msg) diff --git a/lib/y/doc.ex b/lib/y/doc.ex index 50cb0d2..a48319d 100644 --- a/lib/y/doc.ex +++ b/lib/y/doc.ex @@ -82,6 +82,10 @@ defmodule Y.Doc do GenServer.call(doc_name, {:get_array, array_name}) end + def get_map(doc_name, map_name \\ "") do + GenServer.call(doc_name, {:get_map, map_name}) + end + def transact(doc_name, f, opts \\ []) do GenServer.call(doc_name, {:transact, f, opts}) end @@ -117,38 +121,51 @@ defmodule Y.Doc do end end - def highest_clock(transaction, client_id \\ nil) - - def highest_clock(%Transaction{doc: doc}, :all) do - Enum.reduce(Map.values(doc.share), %{}, fn t, acc -> - Map.merge(acc, Type.highest_clock(t, :all), fn _k, v1, v2 -> - max(v1, v2) - end) - end) + def highest_clock(%Transaction{doc: doc}, client_id \\ nil) do + highest_clock!(doc, client_id) end - def highest_clock(%Transaction{doc: doc}, client_id) do - client_id = client_id || doc.client_id - + def highest_clock!(%Doc{} = doc, client_id \\ nil) do Enum.reduce(Map.values(doc.share), 0, fn t, acc -> max(acc, Type.highest_clock(t, client_id)) end) end - def highest_clock_with_length(transaction, client_id \\ nil) + def highest_clock_by_client_id(%Transaction{doc: doc}), do: highest_clock_by_client_id!(doc) - def highest_clock_with_length(%Transaction{doc: doc}, :all) do - highest_clock_with_length!(doc) + def highest_clock_by_client_id!(%Doc{} = doc) do + Enum.reduce(Map.values(doc.share), %{}, fn t, acc -> + Map.merge(acc, Type.highest_clock_by_client_id(t), fn _k, v1, v2 -> + max(v1, v2) + end) + end) end - def highest_clock_with_length(%Transaction{doc: doc}, client_id) do - client_id = client_id || doc.client_id + def highest_clock_with_length(%Transaction{doc: doc}, client_id \\ nil) do + highest_clock_with_length!(doc, client_id) + end + @doc """ + Make sure that %Doc{} = doc is not stale. + Better to use highest_clock_with_length within transaction + """ + def highest_clock_with_length!(%Doc{} = doc, client_id) do Enum.reduce(Map.values(doc.share), 0, fn t, acc -> max(acc, Type.highest_clock_with_length(t, client_id)) end) end + def highest_clock_with_length_by_client_id(%Transaction{doc: doc}), + do: highest_clock_with_length_by_client_id!(doc) + + def highest_clock_with_length_by_client_id!(%Doc{} = doc) do + Enum.reduce(Map.values(doc.share), %{}, fn t, acc -> + Map.merge(acc, Type.highest_clock_with_length_by_client_id(t), fn _k, v1, v2 -> + max(v1, v2) + end) + end) + end + def find_item(transaction, type_name \\ nil, id, default \\ nil) def find_item(%Transaction{doc: doc}, nil, %ID{} = id, default) do @@ -169,18 +186,6 @@ defmodule Y.Doc do |> Enum.find_value(fn t -> Type.find(t, id, default) end) end - @doc """ - Make sure that %Doc{} = doc is not stale. - Better to use highest_clock_with_length within transaction - """ - def highest_clock_with_length!(%Doc{} = doc) do - Enum.reduce(Map.values(doc.share), %{}, fn t, acc -> - Map.merge(acc, Type.highest_clock_with_length(t, :all), fn _k, v1, v2 -> - max(v1, v2) - end) - end) - end - def items_of_client!(%Doc{} = doc, client) do doc.share |> Map.values() @@ -195,7 +200,7 @@ defmodule Y.Doc do def type_with_id!(%Doc{} = doc, %ID{} = id) do doc.share |> Map.values() - |> Enum.filter(fn type -> Type.highest_clock_with_length(type, :all) >= id.clock end) + |> Enum.filter(fn type -> Type.highest_clock_with_length(type) >= id.clock end) |> Enum.find(fn type -> type |> Type.to_list(as_items: true) @@ -233,9 +238,9 @@ defmodule Y.Doc do %{doc | share: share} end - def apply_update(update, transaction) when is_bitstring(update) do + def apply_update(transaction, update) when is_bitstring(update) do update - |> Decoder.decode(transaction) + |> Decoder.apply(transaction) end def put_pending_structs( @@ -275,6 +280,23 @@ defmodule Y.Doc do {:reply, {:ok, array}, %Doc{doc | share: Map.put_new(share, array_name, array)}} end + def handle_call({:get_map, name}, _, %{share: share} = doc) when is_map_key(share, name) do + case share[name] do + %Unknown{} = u -> + map = Y.Type.Map.from_unknown(u) + {:reply, {:ok, map}, %{doc | share: Map.replace(share, name, map)}} + + _ -> + {:reply, {:error, "Type with the name #{name} has already been added"}, doc} + end + end + + def handle_call({:get_map, map_name}, _, %{share: share} = doc) do + map = Y.Type.Map.new(doc, map_name) + + {:reply, {:ok, map}, %Doc{doc | share: Map.put_new(share, map_name, map)}} + end + def handle_call( {:transact, f, opts}, _, diff --git a/lib/y/encoder.ex b/lib/y/encoder.ex index b4b850a..f492d70 100644 --- a/lib/y/encoder.ex +++ b/lib/y/encoder.ex @@ -14,7 +14,7 @@ defmodule Y.Encoder do doc <- Doc.pack!(doc), sm <- doc - |> Doc.highest_clock_with_length!() + |> Doc.highest_clock_with_length_by_client_id!() |> Enum.map(fn {k, _v} -> {k, 0} end) |> Enum.into(%{}) do Buffer.new() diff --git a/lib/y/item.ex b/lib/y/item.ex index ecd94e5..ab292d7 100644 --- a/lib/y/item.ex +++ b/lib/y/item.ex @@ -25,13 +25,15 @@ defmodule Y.Item do origin = Keyword.get(opts, :origin) right_origin = Keyword.get(opts, :right_origin) parent_name = Keyword.get(opts, :parent_name) + parent_sub = Keyword.get(opts, :parent_sub) item = %Item{ id: id, content: content, origin: origin, right_origin: right_origin, - parent_name: parent_name + parent_name: parent_name, + parent_sub: parent_sub } %{item | length: content_length(item)} @@ -197,7 +199,7 @@ defmodule Y.Item do {:invalid, transaction} nil -> - case Type.add_before(type, Type.first(type), item) do + case Type.add_before(type, Type.first(type, item), item) do {:ok, updated_type} -> Transaction.update(transaction, updated_type) @@ -315,13 +317,13 @@ defmodule Y.Item do %Item{} = o <- Type.next(type, item_left) do o else - _ -> Type.first(type) + _ -> Type.first(type, item) end end_range = case item_right do %Item{} -> item_right - _ -> Type.last(type) + _ -> Type.last(type, item) end with %Item{} <- start_range, diff --git a/lib/y/transaction.ex b/lib/y/transaction.ex index 404b885..4265bf8 100644 --- a/lib/y/transaction.ex +++ b/lib/y/transaction.ex @@ -29,7 +29,7 @@ defmodule Y.Transaction do local: local } - %{t | before_state: Doc.highest_clock_with_length(t, :all)} + %{t | before_state: Doc.highest_clock_with_length(t)} end def update(transaction, type) do diff --git a/lib/y/type.ex b/lib/y/type.ex index 317ecc6..55a30f3 100644 --- a/lib/y/type.ex +++ b/lib/y/type.ex @@ -1,6 +1,8 @@ defprotocol Y.Type do - def highest_clock(type, client) - def highest_clock_with_length(type, client) + def highest_clock(type, client \\ nil) + def highest_clock_with_length(type, client \\ nil) + def highest_clock_by_client_id(type) + def highest_clock_with_length_by_client_id(type) def pack(type) def to_list(type, opts \\ []) def find(type, id, default \\ nil) @@ -10,7 +12,7 @@ defprotocol Y.Type do def add_before(type, before_item, item) def next(type, item) def prev(type, item) - def first(type) - def last(type) + def first(type, reference_item) + def last(type, reference_item) def delete(type, transaction, id) end diff --git a/lib/y/type/array.ex b/lib/y/type/array.ex index 4b40d64..1bc9e98 100644 --- a/lib/y/type/array.ex +++ b/lib/y/type/array.ex @@ -55,7 +55,7 @@ defmodule Y.Type.Array do def from_unknown(%Unknown{} = u) do tree = u - |> Type.to_list(as_items: true) + |> Type.to_list(as_items: true, with_deleted: true) |> Enum.reduce(ArrayTree.new(), fn item, tree -> ArrayTree.conj!(tree, item) end) @@ -141,6 +141,12 @@ defmodule Y.Type.Array do def highest_clock_with_length(%Array{tree: tree}, client), do: ArrayTree.highest_clock_with_length(tree, client) + def highest_clock_by_client_id(%Array{tree: tree}), + do: ArrayTree.highest_clock_by_client_id(tree) + + def highest_clock_with_length_by_client_id(%Array{tree: tree}), + do: ArrayTree.highest_clock_with_length_by_client_id(tree) + def pack(%Array{tree: tree} = array) do new_tree = tree @@ -236,11 +242,11 @@ defmodule Y.Type.Array do ArrayTree.prev(tree, item) end - def first(%Array{tree: tree}) do + def first(%Array{tree: tree}, _) do ArrayTree.first(tree) end - def last(%Array{tree: tree}) do + def last(%Array{tree: tree}, _) do ArrayTree.last(tree) end diff --git a/lib/y/type/array/array_tree.ex b/lib/y/type/array/array_tree.ex index c596b6f..19824a1 100644 --- a/lib/y/type/array/array_tree.ex +++ b/lib/y/type/array/array_tree.ex @@ -17,9 +17,12 @@ defmodule Y.Type.Array.ArrayTree do %ArrayTree{ft: FingerTree.finger_tree(meter_object())} end - def highest_clock_with_length(%ArrayTree{ft: tree}, :all) do + def highest_clock_with_length(%ArrayTree{ft: tree}, nil) do %Meter{highest_clocks_with_length: cl} = FingerTree.measure(tree) + cl + |> Map.values() + |> Enum.max(fn -> 0 end) end def highest_clock_with_length(%ArrayTree{ft: tree}, client_id) do @@ -31,9 +34,12 @@ defmodule Y.Type.Array.ArrayTree do end end - def highest_clock(%ArrayTree{ft: tree}, :all) do + def highest_clock(%ArrayTree{ft: tree}, nil) do %Meter{highest_clocks: c} = FingerTree.measure(tree) + c + |> Map.values() + |> Enum.max(fn -> 0 end) end def highest_clock(%ArrayTree{ft: tree}, client_id) do @@ -45,6 +51,16 @@ defmodule Y.Type.Array.ArrayTree do end end + def highest_clock_with_length_by_client_id(%ArrayTree{ft: tree}) do + %Meter{highest_clocks_with_length: cl} = FingerTree.measure(tree) + cl + end + + def highest_clock_by_client_id(%ArrayTree{ft: tree}) do + %Meter{highest_clocks: c} = FingerTree.measure(tree) + c + end + def put(%ArrayTree{ft: %EmptyTree{} = tree} = array_tree, _index, %Item{} = item) do items = Item.explode(item) |> Enum.reverse() diff --git a/lib/y/type/map.ex b/lib/y/type/map.ex new file mode 100644 index 0000000..6bcc527 --- /dev/null +++ b/lib/y/type/map.ex @@ -0,0 +1,373 @@ +defmodule Y.Type.Map do + alias Y.Transaction + alias Y.Type + alias Y.Type.Unknown + alias Y.Doc + alias Y.Item + alias Y.ID + + defstruct map: %{}, + doc_name: nil, + name: nil + + def new(%Doc{name: doc_name}, name) do + %Y.Type.Map{doc_name: doc_name, name: name} + end + + def put({:ok, %Y.Type.Map{} = map_type, %Transaction{} = transaction}, key, content), + do: put(map_type, transaction, key, content) + + def put( + %Y.Type.Map{map: map, name: parent_name} = map_type, + %Transaction{} = transaction, + key, + content + ) do + clock_length = Doc.highest_clock_with_length(transaction) + + item = + Item.new( + id: ID.new(transaction.doc.client_id, clock_length), + content: [content], + parent_name: parent_name, + parent_sub: key + ) + + new_map = + Map.update(map, key, [item], fn [active_item | rest] -> + # origin (thus ID to the left) because items are in reversed order + # `item` would be to the right of the last element + item = %{item | origin: active_item.id} + old_active = Item.delete(active_item) + [item | [old_active | rest]] + end) + + new_map_type = %{map_type | map: new_map} + + case Transaction.update(transaction, new_map_type) do + {:ok, transaction} -> {:ok, new_map_type, transaction} + err -> err + end + end + + def get(%Y.Type.Map{map: map}, key, default \\ nil) do + case Map.fetch(map, key) do + {:ok, [%Item{deleted?: false, content: [content]} | _]} -> content + _ -> default + end + end + + def get_item(%Y.Type.Map{map: map}, key, default \\ nil) do + case Map.fetch(map, key) do + {:ok, [%Item{deleted?: false} = item | _]} -> item + _ -> default + end + end + + def has_key?(%Y.Type.Map{map: map}, key) do + case Map.fetch(map, key) do + {:ok, [%Item{deleted?: false} | _]} -> true + _ -> false + end + end + + def keys(%Y.Type.Map{map: map}) do + map + |> Enum.reduce([], fn {k, v}, acc -> + case v do + [%Item{deleted?: false} | _] -> [k | acc] + _ -> acc + end + end) + |> Enum.reverse() + end + + def from_unknown(%Unknown{} = u) do + map = + u + |> Type.to_list(as_items: true, with_deleted: true) + |> Enum.reduce(%{}, fn + %Item{parent_sub: nil}, map -> map + item, map -> Map.update(map, item.parent_sub, [item], fn items -> [item | items] end) + end) + |> Enum.map(fn {k, items} -> + {[live | _], deleted} = Enum.split_with(items, & &1.deleted?) + {k, [live | deleted]} + end) + |> Enum.into(%{}) + + %Y.Type.Map{doc_name: u.doc_name, name: u.name, map: map} + end + + defdelegate to_list(array), to: Type + defdelegate to_list(array, opts), to: Type + + defimpl Type do + def highest_clock(%Y.Type.Map{map: map}, client_id) do + map + |> Map.values() + |> then(fn items -> + case client_id do + nil -> items + client_id -> Enum.reject(items, fn %Item{id: %ID{client: cl}} -> cl != client_id end) + end + end) + |> Enum.reduce(0, fn %Item{id: %ID{clock: clock}}, acc -> + max(clock, acc) + end) + end + + def highest_clock_with_length(%Y.Type.Map{map: map}, client_id) do + map + |> Map.values() + |> List.flatten() + |> then(fn items -> + case client_id do + nil -> items + client_id -> Enum.reject(items, fn %Item{id: %ID{client: cl}} -> cl != client_id end) + end + end) + |> Enum.reduce(0, fn %Item{id: %ID{clock: clock}, length: length}, acc -> + max(clock + length, acc) + end) + end + + def highest_clock_by_client_id(%Y.Type.Map{map: map}) do + map + |> Map.values() + |> Enum.reduce(%{}, fn {_k, item}, acc -> + Map.update(acc, item.id.client, item.id.clock, fn existing -> + max(existing, item.id.clock) + end) + end) + end + + def highest_clock_with_length_by_client_id(%Y.Type.Map{map: map}) do + map + |> Map.values() + |> Enum.reduce(%{}, fn {_k, item}, acc -> + Map.update(acc, item.id.client, item.id.clock + Item.content_length(item), fn existing -> + max(existing, item.id.clock + Item.content_length(item)) + end) + end) + end + + def pack(%Y.Type.Map{map: map} = map_type) do + new_map = + map + |> Enum.map(fn {k, items} -> + {k, + Enum.reduce(Enum.reverse(items), [], fn + e, [] -> + [e] + + e, [%Item{} = head | tail] = acc -> + if Item.mergeable?(head, e) do + [Item.merge!(head, e) | tail] + else + [e | acc] + end + end)} + end) + |> Enum.into(%{}) + + %{map_type | map: new_map} + end + + def to_list(%Y.Type.Map{map: map}, opts \\ []) do + as_items = Keyword.get(opts, :as_items, false) + with_deleted = Keyword.get(opts, :with_deleted, false) + + items = + map + |> Enum.reduce([], fn {_k, v}, acc -> + case v do + [%Item{} = item | _] -> [item | acc] + _ -> acc + end + end) + + items = + if with_deleted do + items + else + items |> Enum.reject(& &1.deleted?) + end + + if as_items, + do: items, + else: + items + |> Enum.map(fn %Item{parent_sub: parent_sub, content: [content | _]} -> + {parent_sub, content} + end) + end + + def find(%Y.Type.Map{map: map}, %ID{} = id, default) do + map + |> Map.values() + |> List.flatten() + |> Enum.find(default, fn %Item{id: i_id} -> i_id == id end) + end + + def unsafe_replace(_, %Item{parent_sub: nil}, _), do: {:error, "Item has no parent_sub set"} + + def unsafe_replace( + %Y.Type.Map{map: map} = map_type, + %Item{id: %ID{clock: item_clock}, parent_sub: parent_sub} = item, + with_items + ) + when is_list(with_items) do + [%{id: %ID{clock: f_clock}} | _] = with_items + + with_items_length = + Enum.reduce(with_items, 0, fn i, acc -> acc + Item.content_length(i) end) + + with_items_parent_sub = Enum.map(with_items, & &1.parent_sub) |> Enum.uniq() + + cond do + f_clock != item_clock -> + {:error, "Clocks diverge"} + + Item.content_length(item) != with_items_length -> + {:error, "Total content length of items != length of item to replace"} + + Map.has_key?(map, parent_sub) == false -> + {:error, "Item's parent_sub key is missing in the map"} + + [parent_sub] != with_items_parent_sub -> + {:error, + "Some item(s) to replace has different parent_sub than the item to be replaced"} + + :otherwise -> + new_map = + Map.update!(map, parent_sub, fn items -> + items + |> Enum.reverse() + |> Enum.flat_map(fn next_item -> + if next_item == item, do: with_items, else: [next_item] + end) + end) + + if Map.fetch!(new_map, parent_sub) == Map.fetch!(map, parent_sub) do + {:error, "Item not found"} + else + %{map_type | map: new_map} + end + end + end + + def between(%Y.Type.Map{map: map}, %ID{} = left, %ID{} = right) do + Enum.reduce_while(map, [], fn {_, items}, _ -> + items + |> Enum.reverse() + |> Enum.reduce_while([], fn + item, [] when item == left -> {:cont, [item]} + item, [] when item != left -> {:cont, []} + item, i_acc when item == right -> {:halt, [item | i_acc]} + item, i_acc when item != right -> {:cont, [item | i_acc]} + end) + |> case do + [] -> {:cont, []} + acc -> {:halt, Enum.reverse(acc)} + end + end) + end + + def add_after(_, %Item{parent_sub: ps1}, %Item{parent_sub: ps2}) when ps1 != ps2, + do: {:error, "Items' parent_sub deffers"} + + def add_after( + %Y.Type.Map{map: map} = map_type, + %Item{parent_sub: parent_sub} = after_item, + %Item{} = item + ) do + new_map = + Map.update!(map, parent_sub, fn items -> + items + |> Enum.reverse() + |> Enum.flat_map(fn next_item -> + if next_item == after_item, do: [next_item, item], else: [next_item] + end) + end) + + if Map.fetch!(new_map, parent_sub) == Map.fetch!(map, parent_sub) do + {:error, "Item not found"} + else + %{map_type | map: new_map} + end + end + + def add_before(_, %Item{parent_sub: ps1}, %Item{parent_sub: ps2}) when ps1 != ps2, + do: {:error, "Items' parent_sub deffers"} + + def add_before( + %Y.Type.Map{map: map} = map_type, + %Item{parent_sub: parent_sub} = before_item, + %Item{} = item + ) do + new_map = + Map.update!(map, parent_sub, fn items -> + items + |> Enum.reverse() + |> Enum.flat_map(fn next_item -> + if next_item == before_item, do: [item, next_item], else: [next_item] + end) + end) + + if Map.fetch!(new_map, parent_sub) == Map.fetch!(map, parent_sub) do + {:error, "Item not found"} + else + %{map_type | map: new_map} + end + end + + def next(%Item{parent_sub: nil}), do: nil + + def next(%Y.Type.Map{map: map}, %Item{parent_sub: parent_sub} = item) do + map + |> Map.get(parent_sub, []) + |> Enum.reverse() + |> Enum.reduce_while(nil, fn + i_item, nil when i_item == item -> {:cont, :take_this} + _i_item, nil -> {:cont, nil} + i_item, :take_this -> {:halt, i_item} + end) + |> case do + %Item{} = f -> f + _ -> nil + end + end + + def prev(%Item{parent_sub: nil}), do: nil + + def prev(%Y.Type.Map{map: map}, %Item{parent_sub: parent_sub} = item) do + map + |> Map.get(parent_sub, []) + # no reverse + |> Enum.reduce_while(nil, fn + i_item, nil when i_item == item -> {:cont, :take_this} + _i_item, nil -> {:cont, nil} + i_item, :take_this -> {:halt, i_item} + end) + |> case do + %Item{} = f -> f + _ -> nil + end + end + + def first(_, %Item{parent_sub: nil}), do: nil + + def first(%Y.Type.Map{} = map_type, %Item{parent_sub: parent_sub}) do + Map.get(map_type, parent_sub) + end + + def last(_, %Item{parent_sub: nil}), do: nil + + def last(%Y.Type.Map{} = map_type, %Item{parent_sub: parent_sub}) do + Map.get(map_type, parent_sub) + end + + defdelegate delete(map_type, transaction, id), to: Y.Type.Map, as: :delete + end +end diff --git a/lib/y/type/unknown.ex b/lib/y/type/unknown.ex index c6f7e68..d979ee0 100644 --- a/lib/y/type/unknown.ex +++ b/lib/y/type/unknown.ex @@ -17,29 +17,40 @@ defmodule Y.Type.Unknown do end defimpl Type do - def highest_clock(%Unknown{items: items}, client) do - items - |> Enum.filter(fn %Item{id: %ID{client: c}} -> c == client end) - |> Enum.sort_by(fn %Item{id: %ID{clock: clock}} -> clock end, :desc) - |> hd - |> case do - %Item{} = item -> item.id.clock - _ -> 0 + def highest_clock(%Unknown{items: items}, client_id) do + case client_id do + nil -> items + client_id -> Enum.reject(items, fn %Item{id: %ID{client: cl}} -> cl != client_id end) end + |> Enum.reduce(0, fn %Item{id: %ID{clock: clock}}, acc -> + max(clock, acc) + end) end - def highest_clock_with_length(%Unknown{items: items}, client) do - items - |> Enum.filter(fn %Item{id: %ID{client: c}} -> c == client end) - |> Enum.sort_by( - fn %Item{id: %ID{clock: clock}} = item -> clock + Item.content_length(item) end, - :desc - ) - |> hd - |> case do - %Item{} = item -> item.id.clock + Item.content_length(item) - _ -> 0 + def highest_clock_with_length(%Unknown{items: items}, client_id) do + case client_id do + nil -> items + client_id -> Enum.reject(items, fn %Item{id: %ID{client: cl}} -> cl != client_id end) end + |> Enum.reduce(0, fn %Item{id: %ID{clock: clock}, length: length}, acc -> + max(clock + length, acc) + end) + end + + def highest_clock_by_client_id(%Unknown{items: items}) do + Enum.reduce(items, %{}, fn item, acc -> + Map.update(acc, item.id.client, item.id.clock, fn existing -> + max(existing, item.id.clock) + end) + end) + end + + def highest_clock_with_length_by_client_id(%Unknown{items: items}) do + Enum.reduce(items, %{}, fn item, acc -> + Map.update(acc, item.id.client, item.id.clock + Item.content_length(item), fn existing -> + max(existing, item.id.clock + Item.content_length(item)) + end) + end) end def pack(%Unknown{items: items} = type) do @@ -61,10 +72,13 @@ defmodule Y.Type.Unknown do %{type | items: new_items} end - def to_list(%Unknown{items: items}, as_items: false), - do: items |> Enum.flat_map(& &1.content) + def to_list(%Unknown{items: items}, opts \\ []) do + as_items = Keyword.get(opts, :as_items, false) + with_deleted = Keyword.get(opts, :with_deleted, false) - def to_list(%Unknown{items: items}, as_items: true), do: items + items = if with_deleted, do: items, else: Enum.reject(items, & &1.deleted?) + if as_items, do: items, else: items |> Enum.flat_map(& &1.content) + end def find(%Unknown{items: items}, %ID{} = id, default \\ nil), do: items |> Enum.find(default, &(&1.id == id)) @@ -162,10 +176,10 @@ defmodule Y.Type.Unknown do end end - def first(%Unknown{items: []}), do: nil - def first(%Unknown{items: [h | _]}), do: h + def first(%Unknown{items: []}, _), do: nil + def first(%Unknown{items: [h | _]}, _), do: h - def last(%Unknown{items: []}), do: nil - def last(%Unknown{items: items}), do: List.last(items) + def last(%Unknown{items: []}, _), do: nil + def last(%Unknown{items: items}, _), do: List.last(items) end end diff --git a/test/array_test.exs b/test/array_test.exs index 89eb8f0..2e13631 100644 --- a/test/array_test.exs +++ b/test/array_test.exs @@ -461,4 +461,30 @@ defmodule Y.ArrayTest do {:ok, array} = Doc.get(doc, "array") assert 2 == Enum.at(array, 0) |> Enum.at(2) |> Enum.at(0) end + + test "map in array" do + {:ok, doc} = Doc.new(name: :map_nested) + {:ok, array} = Doc.get_array(doc, "array") + {:ok, map} = Doc.get_map(doc, "map") + + Doc.transact(doc, fn transaction -> + {:ok, map, transaction} = Y.Type.Map.put(map, transaction, "key", "value") + {:ok, _, transaction} = Array.put(array, transaction, 0, map) + {:ok, transaction} + end) + + {:ok, array} = Doc.get(doc, "array") + assert %Y.Type.Map{} = map = Enum.at(array, 0) + assert "value" = Y.Type.Map.get(map, "key") + + Doc.transact(doc, fn transaction -> + {:ok, map} = Doc.get(transaction, "map") + {:ok, _, transaction} = Y.Type.Map.put(map, transaction, "key", "new_value") + {:ok, transaction} + end) + + {:ok, array} = Doc.get(doc, "array") + %Y.Type.Map{} = map = Enum.at(array, 0) + assert "new_value" = Y.Type.Map.get(map, "key") + end end diff --git a/test/encoding_decoding_test.exs b/test/encoding_decoding_test.exs index e3fdaf4..85e666a 100644 --- a/test/encoding_decoding_test.exs +++ b/test/encoding_decoding_test.exs @@ -19,7 +19,7 @@ defmodule Y.EncodingDecodingTest do {:ok, doc} = Doc.new(name: :decode_message_from_js) {:ok, doc_instance} = Doc.get_instance(doc) %Transaction{} = transaction = Transaction.new(doc_instance, nil, true) - %Transaction{} = transaction = Decoder.decode(js_msg, transaction) + %Transaction{} = transaction = Decoder.apply(js_msg, transaction) assert {:ok, %Y.Type.Unknown{ @@ -58,8 +58,7 @@ defmodule Y.EncodingDecodingTest do assert {:ok, _} = Doc.transact(doc, fn transaction -> - transaction = Decoder.decode(msg, transaction) - {:ok, transaction} + {:ok, Doc.apply_update(transaction, msg)} end) {:ok, array} = Doc.get(doc, "array") @@ -94,7 +93,7 @@ defmodule Y.EncodingDecodingTest do end end) |> Doc.transact!(fn transaction -> - {:ok, Decoder.decode(js_msg, transaction)} + {:ok, Doc.apply_update(transaction, js_msg)} end) |> Doc.get_array("array") @@ -115,7 +114,7 @@ defmodule Y.EncodingDecodingTest do end end) |> Doc.transact!(fn transaction -> - {:ok, Decoder.decode(js_msg, transaction)} + {:ok, Doc.apply_update(transaction, js_msg)} end) |> Doc.get("array") @@ -241,7 +240,7 @@ defmodule Y.EncodingDecodingTest do |> then(fn msg -> doc2 |> Doc.transact!(fn transaction -> - {:ok, Decoder.decode(msg, transaction)} + {:ok, Doc.apply_update(transaction, msg)} end) |> Doc.get_array("array") |> elem(1) @@ -302,7 +301,7 @@ defmodule Y.EncodingDecodingTest do |> then(fn msg -> doc2 |> Doc.transact!(fn transaction -> - {:ok, Decoder.decode(msg, transaction)} + {:ok, Doc.apply_update(transaction, msg)} end) |> Doc.get_array("array") |> elem(1) @@ -328,8 +327,7 @@ defmodule Y.EncodingDecodingTest do doc2 |> Doc.transact!(fn transaction -> - transaction = Decoder.decode(msg, transaction) - {:ok, transaction} + {:ok, Doc.apply_update(transaction, msg)} end) assert {:ok, _array2} = Doc.get(doc2, "array") @@ -348,8 +346,7 @@ defmodule Y.EncodingDecodingTest do doc2 |> Doc.transact!(fn transaction -> - transaction = Decoder.decode(msg2, transaction) - {:ok, transaction} + {:ok, Doc.apply_update(transaction, msg2)} end) assert {:ok, array2} = Doc.get(doc2, "array") @@ -376,8 +373,7 @@ defmodule Y.EncodingDecodingTest do doc2 |> Doc.transact!(fn transaction -> - transaction = Decoder.decode(msg, transaction) - {:ok, transaction} + {:ok, Doc.apply_update(transaction, msg)} end) assert {:ok, array2} = Doc.get(doc2, "array") @@ -397,8 +393,7 @@ defmodule Y.EncodingDecodingTest do doc2 |> Doc.transact!(fn transaction -> - transaction = Decoder.decode(msg2, transaction) - {:ok, transaction} + {:ok, Doc.apply_update(transaction, msg2)} end) assert {:ok, array2} = Doc.get(doc2, "array") diff --git a/test/map_test.exs b/test/map_test.exs new file mode 100644 index 0000000..bbdfae3 --- /dev/null +++ b/test/map_test.exs @@ -0,0 +1,55 @@ +defmodule Y.MapTest do + use ExUnit.Case + # alias Y.Transaction + alias Y.Doc + alias Y.Type.Map, as: TMap + # alias Y.ID + alias Y.Type.Array + alias Y.Item + + test "insert" do + {:ok, doc} = Doc.new(name: :map_insert) + {:ok, _map} = Doc.get_map(doc, "map") + + Doc.transact(doc, fn transaction -> + {:ok, map} = Doc.get(transaction, "map") + {:ok, _, transaction} = Y.Type.Map.put(map, transaction, "key", [1, 2, 3]) + {:ok, transaction} + end) + + {:ok, map} = Doc.get(doc, "map") + assert [1, 2, 3] == TMap.get(map, "key") + assert %Item{content: [[1, 2, 3]]} = TMap.get_item(map, "key") + end + + test "insert into array" do + {:ok, doc} = Doc.new(name: :map_insert_array) + {:ok, array} = Doc.get_array(doc, "array") + {:ok, map} = Doc.get_map(doc, "map") + + Doc.transact(doc, fn transaction -> + {:ok, map, transaction} = + TMap.put(map, transaction, "key", [1, 2, 3]) + |> TMap.put("key2", %{}) + + {:ok, _, transaction} = + Array.put(array, transaction, 0, 0) + |> Array.put(1, map) + + {:ok, transaction} + end) + + {:ok, array} = Doc.get(doc, "array") + assert [_, %TMap{}] = Array.to_list(array) + + Doc.transact(doc, fn transaction -> + {:ok, map} = Doc.get(transaction, "map") + {:ok, _, transaction} = Y.Type.Map.put(map, transaction, "other key", 123) + {:ok, transaction} + end) + + {:ok, array} = Doc.get(doc, "array") + assert [_, map_from_list] = Array.to_list(array) + assert [{"other key", 123}, {"key2", %{}}, {"key", [1, 2, 3]}] = TMap.to_list(map_from_list) + end +end