From 8e15e168ca2efb8178ba0300ca069e21236a3c0f Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Fri, 11 Feb 2022 12:05:42 -0600 Subject: [PATCH] Add talk timer --- .formatter.exs | 4 ++ .gitignore | 26 +++++++++ README.md | 21 +++++++ lib/ranch_talk.ex | 18 ++++++ lib/ranch_talk/talk_timer.ex | 105 +++++++++++++++++++++++++++++++++++ mix.exs | 25 +++++++++ mix.lock | 3 + ranch-talk.livemd | 23 ++++++++ readme.md | 29 ++++++++++ 9 files changed, 254 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/ranch_talk.ex create mode 100644 lib/ranch_talk/talk_timer.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 ranch-talk.livemd diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0791a9 --- /dev/null +++ b/.gitignore @@ -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"). +ranch_talk-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..401ad1a --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# RanchTalk + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `ranch_talk` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:ranch_talk, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/lib/ranch_talk.ex b/lib/ranch_talk.ex new file mode 100644 index 0000000..b8e79b1 --- /dev/null +++ b/lib/ranch_talk.ex @@ -0,0 +1,18 @@ +defmodule RanchTalk do + @moduledoc """ + Documentation for `RanchTalk`. + """ + + @doc """ + Hello world. + + ## Examples + + iex> RanchTalk.hello() + :world + + """ + def hello do + :world + end +end diff --git a/lib/ranch_talk/talk_timer.ex b/lib/ranch_talk/talk_timer.ex new file mode 100644 index 0000000..9d71f9d --- /dev/null +++ b/lib/ranch_talk/talk_timer.ex @@ -0,0 +1,105 @@ +defmodule RanchTalk.TalkTimer do + use Kino.JS + use Kino.JS.Live + + require Logger + + defmodule Opts do + @ten_minutes_in_seconds 60 * 10 + + defstruct done_message: "Timer finished!", + seconds_to_count: @ten_minutes_in_seconds + end + + @default_opts %{ + done_message: "Timer finished!", + seconds_to_count: 600 + } + + @spec new(opts :: Opts.t()) :: Kino.JS.Live.t() + def new(opts) do + state = + @default_opts + |> Map.merge(opts) + |> update_state_html() + + Kino.JS.Live.new(__MODULE__, state) + end + + def set_html(widget, html) do + Kino.JS.Live.cast(widget, {:set_html, html}) + end + + def get_state(widget) do + Kino.JS.Live.call(widget, :get_state) + end + + @impl true + def init(state, ctx) do + {:ok, assign(ctx, state)} + end + + def tick(widget) do + state = get_state(widget) + tick(widget, state) + end + + def tick(widget, %{seconds_to_count: seconds_to_count, done_message: done_message} = state) do + if seconds_to_count < 1 do + set_html(widget, done_message) + Kino.JS.Live.cast(widget, :timer_finished) + else + new_state = + state + |> Map.put(:seconds_to_count, seconds_to_count - 1) + |> update_state_html() + + set_html(widget, state.html) + Process.sleep(1000) + tick(widget, new_state) + end + end + + @impl true + def handle_connect(ctx) do + {:ok, ctx.assigns, ctx} + end + + @impl true + def handle_cast({:set_html, html}, ctx) do + broadcast_event(ctx, "set_html", html) + {:noreply, assign(ctx, html: html)} + end + + @impl true + def handle_cast(:timer_finished, ctx) do + broadcast_event(ctx, "timer_finished", ctx.assigns.done_message) + {:noreply, ctx} + end + + @impl true + def handle_call(:get_state, _from, ctx) do + {:reply, ctx.assigns, ctx} + end + + def start(widget) do + spawn(fn -> tick(widget) end) + widget + end + + defp update_state_html(%{seconds_to_count: seconds} = state) do + minutes = seconds |> div(60) |> to_string() + seconds = seconds |> rem(60) |> to_string() |> String.pad_leading(2, "0") + Map.put(state, :html, Enum.join([minutes, ":", seconds])) + end + + asset "main.js" do + """ + export function init(ctx, state) { + ctx.root.innerHTML = state.html; + ctx.handleEvent("set_html", html => ctx.root.innerHTML = html) + ctx.handleEvent("timer_finished", message => alert(message)) + } + """ + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..c6f91ed --- /dev/null +++ b/mix.exs @@ -0,0 +1,25 @@ +defmodule RanchTalk.MixProject do + use Mix.Project + + def project do + [ + app: :ranch_talk, + version: "0.1.0", + elixir: "~> 1.13", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:kino, "~> 0.5.2"} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..6b66c0b --- /dev/null +++ b/mix.lock @@ -0,0 +1,3 @@ +%{ + "kino": {:hex, :kino, "0.5.2", "42716edc9e41f17325f81a30985f81366cc673dfa5c559f58b8d7f8560b62212", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:vega_lite, "~> 0.1.0", [hex: :vega_lite, repo: "hexpm", optional: true]}], "hexpm", "0911fb49ea6a26b4ac2ee10a075d9a08f5c8e5b5b9bbd385b7fb06f811c9949b"}, +} diff --git a/ranch-talk.livemd b/ranch-talk.livemd new file mode 100644 index 0000000..4a58feb --- /dev/null +++ b/ranch-talk.livemd @@ -0,0 +1,23 @@ +# About Ranch + +## Talk Timer + +This talk is supposed to be 5-15 minutes long, so let's make sure we keep it that way with a timer and a super annoying alert! + +```elixir +alias RanchTalk.TalkTimer + +%{ + # 10 minutes + seconds_to_count: 3, + done_message: "Time's up! Shut up and get back to work!" +} +|> TalkTimer.new() +|> TalkTimer.start() +``` + +## Ranch Overview + +```elixir +:observer.start() +``` diff --git a/readme.md b/readme.md index 48a2708..670d8f2 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,30 @@ # ranch-talk + +I was asked to give a 5-15 minute talk on [Ranch][ranch], a TCP socket acceptor +pool, to show how OTP constructs are used in the real world and expose some of +[Phoenix's][phoenix] underpinnings. This [Livebook][livebook] contains my code +and notes for that talk. + +Thanks to [Divvy][divvy] for inviting me to give this talk. + +# Usage + +Install and run a local Livebook in `attached` mode and automatically grab my +code: + +```bash +mix escript.install github livebook-dev/livebook +git clone https://git.lyte.dev/lytedev/ranch-talk.git +mix deps.get +env LIVEBOOK_PORT=5588 LIVEBOOK_IFRAME_PORT=5589 \ + livebook server --name ranch_is_neat@localhost --cookie yes-please \ + --default-runtime attached:ranch_is_neat@localhost:yes-please \ + "$(pwd)/ranch-talk.livemd" +``` + +Enjoy! + +[ranch]: https://github.com/ninenines/ranch +[phoenix]: https://www.phoenixframework.org/ +[livebook]: https://github.com/livebook-dev/livebook +[divvy]: https://getdivvy.com/