Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
nmbrone committed Jul 28, 2023
0 parents commit 717d3f4
Show file tree
Hide file tree
Showing 12 changed files with 910 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
minch-*.tar

# Temporary files, for example, from tests.
/tmp/
104 changes: 104 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Minch

A WebSocket client build around [`Mint.WebSocket`](https://github.com/elixir-mint/mint_web_socket).

## Installation

The package can be installed by adding `minch` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:minch, "~> 0.1.0"}
]
end
```

<!-- @moduledoc -->

## Usage

### Supervised client

```elixir
defmodule EchoClient do
use MintSocket

require Logger

@impl true
def init(_init_arg) do
{:ok, %{connected?: false}}
end

@impl true
def connect(_state) do
url = "wss://ws.postman-echo.com/raw"
headers = [{"authorization", "bearer: example"}]
# don't do this in production
options = [transport_opts: [{:verify, :verify_none}]]
{url, headers, options}
end

@impl true
def handle_connect(state) do
Logger.info("connected")
Process.send_after(self(), :produce, 5000)
{:reply, {:text, "welcome"}, %{state | connected?: true}}
end

@impl true
def handle_disconnect(reason, state) do
Logger.warning("disconnected: #{inspect(reason)}")
{:reconnect, 1000, %{state | connected?: false}}
end

@impl true
def handle_info(:produce, state) do
Process.send_after(self(), :produce, 5000)
{:reply, {:text, DateTime.utc_now() |> DateTime.to_iso8601()}, state}
end

@impl true
def handle_frame(frame, state) do
Logger.info(inspect(frame))
{:ok, state}
end
end
```

### Simple client

```elixir
url = "wss://ws.postman-echo.com/raw"
headers = []
# don't do this in production
options = [transport_opts: [{:verify, :verify_none}]]

IO.puts("checking ping to #{url}...")

case Minch.connect(url, headers, options) do
{:ok, pid, ref} ->
Minch.send_frame(pid, {:text, to_string(System.monotonic_time())})

case Minch.await_frame(ref) do
{:text, start} ->
ping =
System.convert_time_unit(
System.monotonic_time() - String.to_integer(start),
:native,
:millisecond
)

IO.puts("#{ping}ms")

:timeout ->
IO.puts("timeout")
end

Minch.close(pid)

{:error, error} ->
IO.puts("connection error: #{inspect(error)}")
end
```
105 changes: 105 additions & 0 deletions lib/minch.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
defmodule Minch do
@external_resource "README.md"
@moduledoc """
A WebSocket client build around `Mint.WebSocket`.
"""
@moduledoc @moduledoc <>
(File.read!(@external_resource)
|> String.split("<!-- @moduledoc -->")
|> List.last())

@type client :: GenServer.server()

@callback init(init_arg :: term()) :: {:ok, state :: term()}

@callback connect(state :: term()) ::
url
| {url, headers}
| {url, headers, options}
when url: String.t() | URI.t(), headers: Mint.Types.headers(), options: Keyword.t()

@callback handle_info(msg :: :timeout | term(), state :: term()) ::
{:noreply, new_state}
| {:reply, frame :: Mint.WebSocket.frame(), new_state}
| {:reconnect, new_state}
when new_state: term()

@callback handle_connect(state :: term()) ::
{:ok, new_state}
| {:reply, frame :: Mint.WebSocket.frame(), new_state}
when new_state: term()

@callback handle_disconnect(reason :: term(), state :: term()) ::
{:ok, new_state}
| {:reconnect, backoff :: pos_integer(), new_state}
when new_state: term()

@callback handle_frame(frame :: Mint.WebSocket.frame(), state :: term()) ::
{:ok, new_state}
| {:reply, frame :: Mint.WebSocket.frame(), new_state}
when new_state: term()

@callback terminate(reason, state :: term()) :: term()
when reason: :normal | :shutdown | {:shutdown, term()} | term()

@optional_callbacks connect: 1

defdelegate start_link(module, init_arg, options \\ []), to: Minch.Client

defdelegate connect(url, headers \\ [], options \\ []), to: Minch.SimpleClient

@spec close(client()) :: :ok
def close(client) do
GenServer.stop(client)
end

@spec send_frame(client(), Mint.WebSocket.frame()) :: :ok | {:error, Mint.WebSocket.error()}
def send_frame(client, frame) do
GenServer.call(client, {:send_frame, frame})
end

@spec await_frame(Mint.Types.request_ref(), timeout()) :: Mint.WebSocket.frame() | :timeout
def await_frame(ref, timeout \\ 5_000) do
receive do
{:frame, ^ref, frame} -> frame
after
timeout -> :timeout
end
end

defmacro __using__(_) do
quote do
@behaviour Minch

def child_spec(init_arg) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [init_arg]},
restart: :transient
}
end

def start_link(init_arg) do
Minch.start_link(__MODULE__, init_arg, name: __MODULE__)
end

def init(init_arg) do
{:connect, init_arg}
end

def terminate(_reason, _state) do
:ok
end

def handle_info(_message, state) do
{:noreply, state}
end

defoverridable child_spec: 1,
start_link: 1,
init: 1,
terminate: 2,
handle_info: 2
end
end
end
Loading

0 comments on commit 717d3f4

Please sign in to comment.