defmodule ExTimer.Node do @type msg :: tuple() | atom() @type time_ms() :: non_neg_integer() @type t :: %ExTimer.Node{delay: time_ms(), msg: msg()} defstruct delay: 0, msg: {} end defmodule ExTimer do @moduledoc """ ExTimer module. """ alias ExTimer.Node @type timer_node :: ExTimer.Node.t() @type timer_node_msg :: ExTimer.Node.msg() @type time_ms :: ExTimer.Node.time_ms() @type state :: %{ :elapsed_ticks => time_ms(), :timers => [timer_node()], optional(any()) => any() } import Bitwise @int_max (1 <<< 31) - 1 @doc ~S""" add new timer to send msg after time milliseconds. ## Examples ```elixir iex> state = %{timers: [], elapsed_ticks: 0} %{elapsed_ticks: 0, timers: []} iex> state = ExTimer.add(state, {:handler, :name, "uhaha"}, 2000) %{elapsed_ticks: 0, timers: [%ExTimer.Node{delay: 2000, msg: {:handler, :name, "uhaha"}}]} iex> [timer] = state.timers [%ExTimer.Node{delay: 2000, msg: {:handler, :name, "uhaha"}}] iex> timer.msg == {:handler, :name, "uhaha"} true iex> timer.delay == 2000 true ``` """ @spec add(state(), timer_node_msg(), time_ms()) :: state() def add(state, msg, delta_ms) when is_tuple(msg) or is_atom(msg) do put_in(state.timers, insert(state.timers, %Node{delay: delta_ms, msg: msg})) end @doc """ delete the previous registerd timer. ## Examples ```elixir iex> state = %{timers: [%ExTimer.Node{msg: {:handler, :name, "uhaha"}, delay: 2000}], elapsed_ticks: 0} %{ elapsed_ticks: 0, timers: [%ExTimer.Node{delay: 2000, msg: {:handler, :name, "uhaha"}}] } iex> ExTimer.remove(state, {:handler, :name, "uhaha"}) %{timers: [], elapsed_ticks: 0} ``` """ @spec remove(state(), timer_node_msg()) :: state() def remove(state, msg) when is_tuple(msg) or is_atom(msg) do timers = state.timers timers = delete(timers, msg) put_in(state.timers, timers) end @doc ~S""" delete all the registerd timers. ## Examples ```elixir iex> state = %{timers: [%ExTimer.Node{msg: {:handler, :name, "uhaha"}, delay: 2000}], elapsed_ticks: 0} %{elapsed_ticks: 0, timers: [%ExTimer.Node{delay: 2000, msg: {:handler, :name, "uhaha"}}]} iex> ExTimer.clear(state) %{timers: [], elapsed_ticks: 0} ``` """ defmacro clear(state, callback? \\ false) do timer = __ENV__.module quote bind_quoted: [state: state, timer: timer, callback?: callback?] do timer.clear_expired(state, __ENV__.module, callback?) end end @doc false @spec clear_expired(state(), module(), boolean()) :: state() def clear_expired(state, caller, callback?) do if callback? do state = Enum.reduce(state.timers, state, fn timer, state -> {:noreply, state} = caller.handle_info(timer.msg, state) state end) put_in(state.timers, []) else put_in(state.timers, []) end end @doc """ call the callback handler (handle_info) for the timer that has elapsed. ## Examples ```elixir now = System.system_time(:millisecond) delta_ticks = now - state.last_update_tick state = put_in(state.last_update_tick, now) state = ExTimer.update(state, delta_ticks) ``` """ defmacro update(state, delta_ms) do timer = __ENV__.module quote bind_quoted: [state: state, delta_ms: delta_ms, timer: timer] do timer.update_expired(state, delta_ms, __ENV__.module) end end @doc false @spec update_expired(state(), time_ms(), module()) :: state() def update_expired(state, delta_ms, caller) do state = put_in(state.elapsed_ticks, state.elapsed_ticks + delta_ms) {state, timers} = reduce(state.timers, state, fn timer, state -> {:noreply, state} = caller.handle_info(timer.msg, state) state end) state = put_in(state.timers, timers) if state.timers == [] or state.elapsed_ticks + delta_ms > @int_max do adjust(state) else state end end @doc """ return true if found the timer at the given msg, otherwise return false ## Examples ```elixir iex> state = %{timers: [%ExTimer.Node{msg: {:handler, :name, "uhaha"}, delay: 2000}], elapsed_ticks: 0} %{ elapsed_ticks: 0, timers: [%ExTimer.Node{delay: 2000, msg: {:handler, :name, "uhaha"}}] } iex> ExTimer.exist?(state, {:handler, :name, "uhaha"}) true ``` """ @spec exist?(state(), timer_node_msg()) :: boolean def exist?(state, msg), do: exist_internal?(state.timers, msg) @spec exist_internal?(nil | [timer_node()], timer_node_msg()) :: boolean defp exist_internal?(nil, _msg), do: false defp exist_internal?([], _msg), do: false defp exist_internal?([h | t], msg) do if equal?(h.msg, msg) do true else exist_internal?(t, msg) end end @spec insert(nil | [timer_node()], timer_node()) :: [timer_node()] defp insert(nil, timer), do: [timer] defp insert([], timer), do: [timer] defp insert([h | t] = sorted, timer) do if h.delay < timer.delay do [h | insert(t, timer)] else [timer | sorted] end end @doc """ get the minimum time(milliseconds) for the timer to expired. ## Examples ```elixir def handle_info(:tick, state) do now = System.system_time(:millisecond) delta_ticks = now - state.last_update_tick state = put_in(state.last_update_tick, now) state = ExTimer.update(state, delta_ticks) Process.send_after(self(), :tick, ExTimer.next_expire_ticks(state, 1000)) {:noreply, state} end ``` """ @spec next_expire_ticks(state(), time_ms()) :: time_ms() def next_expire_ticks(state, min_time) do if Enum.empty?(state.timers) do min_time else min(hd(state.timers).delay - state.elapsed_ticks, 0) end end @spec delete(nil | [timer_node()], timer_node_msg()) :: [timer_node()] defp delete(nil, _msg), do: [] defp delete([], _msg), do: [] defp delete([h | t], msg) do if equal?(h.msg, msg) do t else [h | delete(t, msg)] end end @spec equal?(timer_node_msg(), timer_node_msg()) :: boolean defp equal?(lhs, rhs) when is_tuple(lhs) and is_tuple(rhs) do size = tuple_size(lhs) size == tuple_size(rhs) and Enum.all?(0..(size - 1), fn i -> elem(lhs, i) == elem(rhs, i) end) end defp equal?(lhs, rhs) when is_atom(lhs) and is_atom(rhs) do lhs == rhs end defp equal?(_lhs, _rhs), do: false @spec reduce(nil | [timer_node()], state(), function()) :: {state(), [timer_node()]} defp reduce(nil, state, _func), do: {state, []} defp reduce([], state, _func), do: {state, []} defp reduce([h | t] = list, state, func) do if h.delay <= state.elapsed_ticks do reduce(t, func.(h, state), func) else {state, list} end end @doc false @spec adjust(state) :: state() defp adjust(state) do timers = adjust_internal(state.elapsed_ticks, state.timers) state = put_in(state.elapsed_ticks, 0) put_in(state.timers, timers) end defp adjust_internal(_elapsed, []), do: [] defp adjust_internal(elapsed_ticks, [timer | t]) do delay = if timer.delay > elapsed_ticks do timer.delay - elapsed_ticks else 0 end timer = put_in(timer.delay, delay) [timer | adjust_internal(elapsed_ticks, t)] end end