Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add socket tree and schemas caching #7

Merged
merged 3 commits into from
May 13, 2024
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
37 changes: 37 additions & 0 deletions lib/channel_spec/cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule ChannelSpec.Cache do
@moduledoc """
Cache for ChannelSpec specs.

Settings:

```elixir
config :channel_spec, :cache_adapter, Module
```

ChannelSpec ships with two cache adapters:

* `ChannelSpec.Cache.PersistentTermCache` - default
* `ChannelSpec.Cache.NoneCache` - none cache

If you are constantly modifying specs during development, you can configure the cache adapter
in `dev.exs` as follows to disable caching:

```elixir
config :channel_spec, :cache_adapter, ChannelSpec.Cache.NoneCache
```
"""

@callback get(module) :: nil | map()
@callback put(module, map()) :: :ok
@callback erase(module) :: :ok

@default_adapter ChannelSpec.Cache.PersistentTermCache

@doc """
Get cache adapter
"""
@spec adapter() :: module()
def adapter() do
Application.get_env(:channel_spec, :cache_adapter, @default_adapter)
end
end
23 changes: 23 additions & 0 deletions lib/channel_spec/cache/none_cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule ChannelSpec.Cache.NoneCache do
@moduledoc """
A cache adapter to disable caching. Intended to be used in development.

Configure it with:

```elixir
# config/runtime.exs
config :channel_handler, :cache_adapter, ChannelSpec.Cache.NoneCache
```
"""

@behaviour ChannelSpec.Cache

@impl true
def get(_spec_module), do: nil

@impl true
def put(_spec_module, _spec), do: :ok

@impl true
def erase(_spec_module), do: :ok
end
24 changes: 24 additions & 0 deletions lib/channel_spec/cache/persistent_term_cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule ChannelSpec.Cache.PersistentTermCache do
@moduledoc """
A cache adapter that stores the specs in memory.

This is the default cache adapter.
"""

@behaviour ChannelSpec.Cache

@impl true
def get(spec_module) do
:persistent_term.get(spec_module, nil)
end

@impl true
def put(spec_module, specs) do
:persistent_term.put(spec_module, specs)
end

@impl true
def erase(spec_module) do
:persistent_term.erase(spec_module)
end
end
63 changes: 43 additions & 20 deletions lib/channel_spec/operations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,28 @@ defmodule ChannelSpec.Operations do
_other, lines ->
lines
end)
|> Macro.escape()

quote location: :keep,
bind_quoted: [
name: name,
schema: params,
file: file,
line: line,
module: module,
line_metadata: line_metadata
] do
operation = %{
schema: schema,
file: file,
line: line,
module: module,
line_metadata: line_metadata
}

quote location: :keep do
@operations {unquote(name),
%{
schema: unquote(params),
file: unquote(file),
line: unquote(line),
module: unquote(module),
line_metadata: unquote(Macro.escape(line_metadata))
}}
@operations {name, operation}

def __channel_spec_operation__(unquote(name)), do: unquote(Macro.escape(operation))
end
end

Expand All @@ -106,7 +118,7 @@ defmodule ChannelSpec.Operations do
@doc """
Defines a subscription for the channel.

Subscriptions are messages that are sent from the server to the client, withuot
Subscriptions are messages that are sent from the server to the client, without
the client requesting them. They are defined with a name and a schema that
describes the payload.

Expand All @@ -128,17 +140,28 @@ defmodule ChannelSpec.Operations do
file = __CALLER__.file
line = __CALLER__.line
module = __CALLER__.module
line_metadata = %{"event" => %{line: line}}
line_metadata = Macro.escape(%{"event" => %{line: line}})

quote location: :keep,
bind_quoted: [
event: event,
file: file,
line: line,
module: module,
schema: schema,
line_metadata: line_metadata
] do
subscription = %{
schema: schema,
file: file,
line: line,
module: module,
line_metadata: line_metadata
}

quote do
@subscriptions {unquote(event),
%{
schema: unquote(schema),
file: unquote(file),
line: unquote(line),
module: unquote(module),
line_metadata: unquote(Macro.escape(line_metadata))
}}
@subscriptions {event, subscription}

def __channel_spec_subscription__(unquote(event)), do: unquote(Macro.escape(subscription))
end
end

Expand Down
53 changes: 39 additions & 14 deletions lib/channel_spec/socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ defmodule ChannelSpec.Socket do
pattern = String.replace_suffix(topic_pattern, "*", "{string}")
topic_pattern = String.replace(topic_pattern, ~r/\{.*\}.*/, "*")

quote do
quote location: :keep do
opts =
Keyword.update(
unquote(opts),
Expand Down Expand Up @@ -60,11 +60,27 @@ defmodule ChannelSpec.Socket do
end

def __socket_tree__() do
unquote(__MODULE__).build_ops_tree(@__channels)
case ChannelSpec.Cache.adapter().get({__MODULE__, :socket_tree}) do
nil ->
tree = unquote(__MODULE__).build_ops_tree(@__channels)
ChannelSpec.Cache.adapter().put({__MODULE__, :socket_tree}, tree)
tree

tree ->
tree
end
end

def __socket_schemas__() do
unquote(__MODULE__).build_schemas(__socket_tree__())
case ChannelSpec.Cache.adapter().get({__MODULE__, :socket_schemas}) do
nil ->
tree = unquote(__MODULE__).build_schemas(__socket_tree__())
ChannelSpec.Cache.adapter().put({__MODULE__, :socket_schemas}, tree)
tree

tree ->
tree
end
end
end
end
Expand All @@ -79,6 +95,7 @@ defmodule ChannelSpec.Socket do
def build_ops_tree(channels) do
channels =
for {topic, module, _opts} <- channels,
Code.ensure_compiled(module),
function_exported?(module, :spark_dsl_config, 0),
into: %{} do
router = module.spark_dsl_config()[[:router]]
Expand Down Expand Up @@ -235,6 +252,8 @@ defmodule ChannelSpec.Socket do
end

defp get_operations(%ChannelHandler.Dsl.Event{} = event, _router, _prefix) do
Code.ensure_compiled(event.module)

if function_exported?(event.module, :__channel_operations__, 0) do
operations = event.module.__channel_operations__()

Expand All @@ -260,17 +279,23 @@ defmodule ChannelSpec.Socket do
end

defp get_operations(%ChannelHandler.Dsl.Delegate{} = delegate, _router, _prefix) do
operations = delegate.module.__channel_operations__()

for {event, operation} <- operations, is_binary(event) do
%{
event: delegate.prefix <> event,
schema: operation.schema,
module: delegate.module,
function: :handle_in,
file: operation.file,
line: operation.line
}
Code.ensure_compiled(delegate.module)

if function_exported?(delegate.module, :__channel_operations__, 0) do
operations = delegate.module.__channel_operations__()

for {event, operation} <- operations, is_binary(event) do
%{
event: delegate.prefix <> event,
schema: operation.schema,
module: delegate.module,
function: :handle_in,
file: operation.file,
line: operation.line
}
end
else
[]
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/mix/channel_spec_routes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule Mix.Tasks.ChannelSpec.Routes do

for {path, socket_module, _} <- endpoint_module.__sockets__() do
try do
ChannelSpec.Cache.adapter().erase({socket_module, :socket_tree})
socket_tree = socket_module.__socket_tree__()
IO.puts(IO.ANSI.faint() <> "#{path}" <> IO.ANSI.reset())
print_socket_routes(socket_module, socket_tree, opts)
Expand Down
10 changes: 1 addition & 9 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,16 @@ defmodule ChannelSpec.MixProject do
app: :channel_spec,
version: @version,
elixir: "~> 1.13",
start_permanent: Mix.env() == :prod,
deps: deps(),
docs: docs(),
package: package()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:channel_handler, "~> 0.1"},
{:channel_handler, "~> 0.6"},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:mneme, "~> 0.5", only: [:dev, :test]},
{:json_xema, "~> 0.6"},
Expand Down
23 changes: 23 additions & 0 deletions test/channel_spec/cache/none_cache_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule ChannelSpec.Cache.NoneCacheTest do
use ExUnit.Case, async: true

alias ChannelSpec.Cache.NoneCache

describe "get/1" do
test "returns nil" do
assert is_nil(NoneCache.get(SomeModule))
end
end

describe "put/2" do
test "returns :ok" do
assert :ok = NoneCache.put(SomeModule, %{})
end
end

describe "erase/1" do
test "returns :ok" do
assert :ok = NoneCache.erase(SomeModule)
end
end
end
Loading