Skip to content

Commit

Permalink
feat: implement observe/observe_deep
Browse files Browse the repository at this point in the history
Unlike the original observe/observe_deep, it is not called in the middle of a transaction.
It is called in the form of a process message after the transaction is completed.
  • Loading branch information
satoren committed Dec 5, 2024
1 parent acc3f38 commit 3a0106c
Show file tree
Hide file tree
Showing 26 changed files with 1,580 additions and 18 deletions.
6 changes: 6 additions & 0 deletions lib/nif.ex
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ defmodule Yex.Nif do

def xml_text_parent(_xml_text, _cur_txn), do: :erlang.nif_error(:nif_not_loaded)

def shared_type_observe(_map, _cur_txn, _pid, _ref, _metadata),
do: :erlang.nif_error(:nif_not_loaded)

def shared_type_observe_deep(_map, _cur_txn, _pid, _ref, _metadata),
do: :erlang.nif_error(:nif_not_loaded)

def encode_state_vector_v1(_doc, _cur_txn), do: :erlang.nif_error(:nif_not_loaded)
def encode_state_as_update_v1(_doc, _cur_txn, _diff), do: :erlang.nif_error(:nif_not_loaded)
def apply_update_v1(_doc, _cur_txn, _update), do: :erlang.nif_error(:nif_not_loaded)
Expand Down
5 changes: 1 addition & 4 deletions lib/protocols/awareness.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
defmodule Yex.Awareness do
@moduledoc """
Awareness is an optional feature that works well together with Yjs.
## Examples
iex> doc = Yex.Doc.new()
iex> {:ok, _awareness} = Yex.Awareness.new(doc)
"""

defstruct [
Expand Down
16 changes: 16 additions & 0 deletions lib/shared_type/array.ex
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,22 @@ defmodule Yex.Array do
Yex.Nif.array_to_json(array, cur_txn(array))
end

@doc """
see `Yex.SharedType.observe/2`
"""
@spec observe(t, keyword()) :: reference()
def observe(%__MODULE__{} = array, opt \\ []) do
Yex.SharedType.observe(array, opt)
end

@doc """
see `Yex.SharedType.observe_deep/2`
"""
@spec observe_deep(t, keyword()) :: reference()
def observe_deep(%__MODULE__{} = array, opt \\ []) do
Yex.SharedType.observe_deep(array, opt)
end

defp cur_txn(%__MODULE__{doc: doc_ref}) do
Process.get(doc_ref, nil)
end
Expand Down
111 changes: 111 additions & 0 deletions lib/shared_type/event.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
defmodule Yex.ArrayEvent do
@moduledoc """
Event when Array type changes
@see Yex.SharedType.observe/1
@see Yex.SharedType.observe_deep/1
"""
defstruct [
:path,
:target,
:change
]

@type t :: %__MODULE__{
path: list(number() | String.t()),
target: Yex.Array.t(),
change: %{insert: list()} | %{delete: number()} | %{}
}
end

defmodule Yex.MapEvent do
@moduledoc """
Event when Map type changes
@see Yex.SharedType.observe/1
@see Yex.SharedType.observe_deep/1
"""
defstruct [
:path,
:target,
:keys
]

@type change ::
%{action: :add, new_value: term()}
| %{action: :delete, old_value: term()}
| %{action: :update, old_value: term(), new_value: term()}
@type keys :: %{String.t() => %{}}

@type t :: %__MODULE__{
path: list(number() | String.t()),
target: Yex.Map.t(),
keys: keys
}
end

defmodule Yex.TextEvent do
@moduledoc """
Event when Text type changes
@see Yex.SharedType.observe/1
@see Yex.SharedType.observe_deep/1
"""
defstruct [
:path,
:target,
:delta
]

@type t :: %__MODULE__{
path: list(number() | String.t()),
target: Yex.Map.t(),
delta: Yex.Text.delta()
}
end

defmodule Yex.XmlEvent do
@moduledoc """
Event when XMLFragment/Element type changes
@see Yex.SharedType.observe/1
@see Yex.SharedType.observe_deep/1
"""
defstruct [
:path,
:target,
:delta,
:keys
]

@type t :: %__MODULE__{
path: list(number() | String.t()),
target: Yex.Map.t(),
delta: Yex.Text.delta(),
keys: %{insert: list()} | %{delete: number()} | %{}
}
end

defmodule Yex.XmlTextEvent do
@moduledoc """
Event when Text type changes
@see Yex.SharedType.observe/1
@see Yex.SharedType.observe_deep/1
"""
defstruct [
:path,
:target,
:delta
]

@type t :: %__MODULE__{
path: list(number() | String.t()),
target: Yex.Map.t(),
delta: Yex.Text.delta()
}
end
18 changes: 17 additions & 1 deletion lib/shared_type/map.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ defmodule Yex.Map do

@doc """
get a key from the map.
## Examples
## Examples
iex> doc = Yex.Doc.new()
iex> map = Yex.Doc.get_map(doc, "map")
iex> Yex.Map.set(map, "plane", ["Hello", "World"])
Expand Down Expand Up @@ -115,6 +115,22 @@ defmodule Yex.Map do
Yex.Nif.map_to_json(map, cur_txn(map))
end

@doc """
see `Yex.SharedType.observe/2`
"""
@spec observe(t, keyword()) :: reference()
def observe(%__MODULE__{} = map, opt \\ []) do
Yex.SharedType.observe(map, opt)
end

@doc """
see `Yex.SharedType.observe_deep/2`
"""
@spec observe_deep(t, keyword()) :: reference()
def observe_deep(%__MODULE__{} = map, opt \\ []) do
Yex.SharedType.observe_deep(map, opt)
end

defp cur_txn(%__MODULE__{doc: doc_ref}) do
Process.get(doc_ref, nil)
end
Expand Down
118 changes: 118 additions & 0 deletions lib/shared_type/shared_type.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
defmodule Yex.SharedType do
@moduledoc """
The SharedType protocol defines the behavior of shared types in Yex.
"""

@doc """
Registers a change observer that will be message every time this shared type is modified.
If the shared type changes, a message is delivered to the
monitoring process in the shape of:
{:observe_event, ref, event, origin, metadata}
where:
* `ref` is a monitor reference returned by this function;
* `event` is a struct that describes the change;
* `origin` is the origin passed to the `Yex.Doc.transaction()` function.
* `metadata` is the metadata passed to the `observe` function.
## Options
* `:metadata` - provides metadata to be attached to this observe.
"""

@type t ::
%Yex.Array{}
| %Yex.Map{}
| %Yex.Text{}
| %Yex.XmlElement{}
| %Yex.XmlText{}
| %Yex.XmlFragment{}

@spec observe(t, keyword()) :: reference()
def observe(shared_type, opt \\ []) do
ref = make_ref()

sub =
Yex.Nif.shared_type_observe(
shared_type,
cur_txn(shared_type),
self(),
ref,
Keyword.get(opt, :metadata)
)

Process.put(ref, sub)

ref
end

@doc """
Unobserve the shared type for changes.
"""
@spec unobserve(reference()) :: :ok
def unobserve(observe_ref) do
unsubscribe(observe_ref)
end

@doc """
Registers a change observer that will be message every time this shared type or any of its children is modified.
If the shared type changes, a message is delivered to the
monitoring process in the shape of:
{:observe_deep_event, ref, events, origin, metadata}
where:
* `ref` is a monitor reference returned by this function;
* `events` is a array of event struct that describes the change;
* `origin` is the origin passed to the `Yex.Doc.transaction()` function.
* `metadata` is the metadata passed to the `observe_deep` function.
## Options
* `:metadata` - provides metadata to be attached to this observe.
"""
@spec observe_deep(t, keyword()) :: reference()
def observe_deep(shared_type, opt \\ []) do
ref = make_ref()

sub =
Yex.Nif.shared_type_observe_deep(
shared_type,
cur_txn(shared_type),
self(),
ref,
Keyword.get(opt, :metadata)
)

Process.put(ref, sub)
ref
end

@doc """
Unobserve the shared type for changes.
"""
@spec unobserve_deep(reference()) :: :ok
def unobserve_deep(observe_ref) do
unsubscribe(observe_ref)
end

defp cur_txn(%{doc: doc_ref}) do
Process.get(doc_ref, nil)
end

defp unsubscribe(ref) do
case Process.get(ref) do
nil ->
:ok

sub ->
Process.delete(ref)
Yex.Nif.sub_unsubscribe(sub)
end
end
end
16 changes: 16 additions & 0 deletions lib/shared_type/text.ex
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ defmodule Yex.Text do
Yex.Nif.text_to_delta(text, cur_txn(text))
end

@doc """
see `Yex.SharedType.observe/2`
"""
@spec observe(t, keyword()) :: reference()
def observe(%__MODULE__{} = text, opt \\ []) do
Yex.SharedType.observe(text, opt)
end

@doc """
see `Yex.SharedType.observe_deep/2`
"""
@spec observe_deep(t, keyword()) :: reference()
def observe_deep(%__MODULE__{} = text, opt \\ []) do
Yex.SharedType.observe_deep(text, opt)
end

defp cur_txn(%__MODULE__{doc: doc_ref}) do
Process.get(doc_ref, nil)
end
Expand Down
14 changes: 14 additions & 0 deletions lib/shared_type/xml_element.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,20 @@ defmodule Yex.XmlElement do
Yex.Nif.xml_element_to_string(xml_element, cur_txn(xml_element))
end

@doc """
see `Yex.SharedType.observe/2`
"""
@spec observe(t, keyword()) :: reference()
def observe(%__MODULE__{} = xml, opt \\ []) do
Yex.SharedType.observe(xml, opt)
end

@doc false
@spec observe_deep(t, keyword()) :: reference()
def observe_deep(%__MODULE__{} = xml, opt \\ []) do
Yex.SharedType.observe_deep(xml, opt)
end

defp cur_txn(%__MODULE__{doc: doc_ref}) do
Process.get(doc_ref, nil)
end
Expand Down
16 changes: 16 additions & 0 deletions lib/shared_type/xml_fragment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ defmodule Yex.XmlFragment do
Yex.Nif.xml_fragment_to_string(xml_fragment, cur_txn(xml_fragment))
end

@doc """
see `Yex.SharedType.observe/2`
"""
@spec observe(t, keyword()) :: reference()
def observe(%__MODULE__{} = xml, opt \\ []) do
Yex.SharedType.observe(xml, opt)
end

@doc """
see `Yex.SharedType.observe_deep/2`
"""
@spec observe_deep(t, keyword()) :: reference()
def observe_deep(%__MODULE__{} = xml, opt \\ []) do
Yex.SharedType.observe_deep(xml, opt)
end

defp cur_txn(%__MODULE__{doc: doc_ref}) do
Process.get(doc_ref, nil)
end
Expand Down
Loading

0 comments on commit 3a0106c

Please sign in to comment.