How to build a ticking clock?

Hello beautiful people :smiling_face_with_three_hearts: I have this code that adds a time recursively to a DateTime struct created with Timex. I have failed to identify where I could place it so that when it fires, it may send consistent time updates to my app. How can I implement this clock feature because I think iā€™m doing it the wrong way. How can I consistently send time events so that the clock may just tick on its own?

Thank you for any suggestions :smiling_face_with_three_hearts:

ā€˜ā€™ā€™
import DateTime

def date_adder(date_struct) do
date_struct = add(date_struct, 1, :second)
# IO.puts(date_struct)
date_adder(date_struct)
end
ā€˜ā€™ā€™

You can do something like this:

iex(1)> defmodule Clock do
...(1)>   def tick do
...(1)>     IO.puts(DateTime.utc_now())
...(1)>     Process.sleep(1_000)
...(1)>     tick()
...(1)>   end
...(1)> end
iex(2)> Task.start_link(fn -> Clock.tick() end)
2024-06-23 17:07:20.089097
# ...
iex(3)>
2 Likes

Given you tagged this as phoenix I wonder if you want to render the clock on a web frontend? If so then thereā€™s a few more things involved. You might want to look into phoenix live view if you want to stay within elixir or you might need some javascript to render updates.

1 Like

If you want it to independently do its stuff then you need a GenServer that is started together with your app. And you can then ask it for its state which is going to be the date/time.

1 Like

I think this is one of those things that seems simple, but starts getting complicated once you dig into it (especially as a beginner).

As mentioned, you can have a stateful process running on your server using a GenServer:

defmodule Clock do
  use GenServer

  def new_clock() do
    {:ok, pid} = GenServer.start_link(Clock, [])
    pid
  end

  def get_current_time(pid) do
    :sys.get_state(pid)
  end

  # Server callbacks
  @impl true
  def init(_init_arg) do
    # schedule the first `tick` event in 1 second (1000 milliseconds)
    Process.send_after(self(), :tick, 1000)

    # set initial state
    {:ok, DateTime.utc_now(:second)}
  end

  def handle_info(:tick, state) do
    # each `tick` event schedules the next one
    Process.send_after(self(), :tick, 1000)

    {:noreply, DateTime.add(state, 1, :second)}
  end
end

Then, you could use the clock like this:

# make a new clock
clock_pid = Clock.new_clock()

# get the current time
Clock.get_current_time(clock_pid)

You could then hook that into your LiveView and send periodic updates to the client (I donā€™t have time to remember how to implement that part since I havenā€™t touched LiveView in a while :sweat_smile:)


If you are making a web app, you could probably just use Javascript for this instead of making the server do all the work. Alpine.js is a great tool to easily to add interactivity to websites. Hereā€™s an example to get you started.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Hello Alpine.js!</title>

    <!-- js -->
    <script
      defer
      src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.js"
    ></script>
  </head>
  <body>
    <h1>Hello Alpine.js!</h1>

    <div
      data-time="1719168461000"
      x-data="{ time: Number($el.dataset.time) }"
      x-init="setInterval(() => { time = time + 1000}, 1000)"
      x-text="new Date(time)"
    >

    </div>
  </body>
</html>

If you needed to pass an initial value to the HTML from the server, you could just do something like data-time="<%= @time %>" to get the server-rendered data into your template, then have Javascript do the work on the client side from that point forward.

4 Likes

Book marking for :sys.get_state(pid). I really gotta learn some Erlang

3 Likes

Just the parts that havenā€™t been translated into Elixir. :rofl:

The GenServer docs use a bit of Erlang for examining the GenServer processes:

https://hexdocs.pm/elixir/GenServer.html#module-debugging-with-the-sys-module

Ya, :sys.get_state is meant for debugging (source). You should send a message to get state. :sys.get_state is super useful for inspecting the state of LiveViews in tests! Though again, you shouldnā€™t really use it for assertions as that is reaching into the implementation.

Regarding the general post, this is best to do in JS because waiting 1 sec when a network is involved (and other problems with computer :upside_down_face:) isnā€™t going to work. Of course if itā€™s just for learning thatā€™s totally different.

Ya, :sys.get_state is meant for debugging (source). You should send a message to get state. :sys.get_state is super useful for inspecting the state of LiveViews in tests! Though again, you shouldnā€™t really use it for assertions as that is reaching into the implementation.

Interesting point. Do you think I should be making a get_state function as part of my ā€œpublic APIā€ in my GenServer code?

Absolutely. Do that because with this youā€™ll be obeying the ā€œonly one message is processed at a timeā€ rule. Whereas with :sys.get_stateā€¦ not sure actually, itā€™s probably safe. Iā€™d still prefer to serialize both reads from and writes to the encapsulated state.

3 Likes

This is certainly the main thing. I also think of it as akin to not using getters in OO, though even kinda worse since youā€™re reaching in and grabbing everything.

1 Like

When in doubt, always consult the documentation:

These functions are intended only to help with debugging. They are provided for convenience, allowing developers to avoid having to create their own state extraction functions and also avoid having to interactively extract the state from the return values of get_status/1 or get_status/2 while debugging.

I think itā€™s wrong to use this functionality even in tests, the state of a process is private (unless it has an API that makes it public), even in the OOP world this is considered an anti-pattern.

3 Likes

System messages are still messages. Just that theyā€™re handled by the :gen machinery and not forwarded to the callback module to handle.

Ah, that would be a complete no-go for me.

Your plan is too vague.

Is the clock meant to show the correct, real, accurate time?

You can always get the true current time with DateTime.utc_now() in Elixir.

But if you are simply going to be incrementing seconds on a %DateTime{} struct then it will eventually drift out of sync with reality.

Remember too that if this is a LiveView project and you want to send regular updates from the server to the client then there will be unpredictable network latency, which will exacerbate the ā€œdrifting out of syncā€ problem.

is the thing that I find vague. What do you mean by ā€œconsistentā€?

Anyway, here is a LiveView that shows the latest time sent to it from the server.

The server sends a new time every second. Note that because of network latency and stuff like that, this will sometimes (very rarely) skip a second.

defmodule MyAppWeb.ClockLive do
  @moduledoc false
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    socket
    |> assign_now_and_schedule_next_tick()
    |> then(&{:ok, &1})
  end

  def render(assigns) do
    ~H"""
    <div id="my-silly-clock" class="text-xl p-8 rounded-full border border-2 text-center">
      <%= @now %>
    </div>
    """
  end

  def handle_info(:tick, socket) do
    socket
    |> assign_now_and_schedule_next_tick()
    |> then(&{:noreply, &1})
  end

  defp assign_now_and_schedule_next_tick(socket) do
    now = DateTime.utc_now()

    socket
    |> assign(:now, now)
    |> tap(&schedule_tick/1)
  end

  defp schedule_tick(socket) do
    Process.send_after(self(), :tick, 1_000)
  end
end

Important: what I have written is a bad idea. Getting your server to tell the client what time it is probably not what you want. Javascript in the browser can get the current time.

EDIT: You can really see the network latency. The digits after the decimal place are slowly increasing. It is kind of hypnotic.

silly-clock

2 Likes

this looks very solid ā€¦let me try it. Thanks

All your answers are really enlightening ā€¦let me read on them ā€¦I am still in my early months of elixir. Thank you all :blush:

1 Like

My bad ā€¦Forget that this was LiveView Related

I probably should use JS.

3 Likes

I thought id chime in with an example of a my JS/LV clock.

In my LV component, I have a div using a phx-hook. I also pass down the timezone and locale in data attributes, as I want the date/time to be of my server. This is entirely optional though.

  def render(assigns) do
    ~H"""
    <div
      id="clock"
      phx-hook="Clock"
      data-timezone={@timezone}
      data-locale={@locale}
    >
    </div>
    """
  end

I then create a hook with a simple setInterval, that updates my div every second with the current date/time.

I also make sure to clear the interval when the component is destroyed, as otherwise every time itā€™s mounted the intervals will pile up.

function setTime(
  el,
  timezone,
  locale
) {
  el.innerHTML = new Date()
    .toLocaleString(locale, {
      timeZone: timezone
    })
}

const Clock = {
  mounted() {
    const el = this.el;
    const timezone = this.el.dataset.timezone;
    const locale = this.el.dataset.locale;

    setTime(el, timezone, locale); // as otherwise the component will 'flicker' in the first 100ms
    const clockInterval = setInterval(() => {
      setTime(el, timezone, locale);
    }, 100); // 100ms as otherwise the clock might lag a second behind your actual system clock

    Object.assign(this, { clockInterval });
  },
  destroyed() {
    clearInterval(this.clockInterval);
  }
};

...

const hooks = { Clock };

Hope this helps!

I wasnā€™t so sure about the Object.assign(this, { clockInterval }); for referencing the interval in destroyed, but it seems to work. However any better suggestions on doing this is very welcome!