Code validation without persisting data?

I want to implement “add friend” functionality by code validation. No search, users would

  1. A generates a four-digit code.
  2. B enters the code to add user A.

where A and B manually share code on their sides.

Can I achieve this without DB or ETS? I thought LiveView process could temporarily remember the code. And PubSub could help communication. But I’m not making progress beyond the idea.

The beauty of ETS is that it is process specific, so it acts as a process cache in similar ways to a process dictionary, but you can transfer it if you don’t want to lose the data.

So, you need to figure out if it’s okay for your data to be lost if the process dies, which could happen in case of redirect and etc.

2 Likes

It kinda works with Phoenix.Endpoint callbacks!

  def mount(_params, _session, socket) do
    # User A generates a code.
    code = ...
    Feder.Endpoint.subscribe("watch:#{code}")
    ...
  end

  def handle_event("watch", %{"code" => code}, socket) do
    # User A tells User B the code.
    # User B broadcasts the code.
    Feder.Endpoint.broadcast("watch:#{code}", "watch", watching_profile)
    ...
  end

  def handle_info(%{topic: "watch:" <> code, payload: watching_profile}, socket) do
    # User A receives the code and creates the relationship.
    Watch.insert(%{watching_profile: watching_profile.id, watched_profile: watched_profile.id})

    Feder.Endpoint.unsubscribe("watch:#{code}")
    ...
  end
1 Like

Nice, this is a neat approach!

That said, there may be a potential gotcha if each LiveView process randomly generates a code because a LiveView process may unintentionally generate a code that clashes with an existing active code generated by another LiveView process.

  1. User A generates a random code
  2. User C generates the same code
  3. User A tells User B the code
  4. User A and User C receive the code and create the relationship

Maybe it’s not a big concern since it may be unlikely given expected volume/usage and the short real world time window for that conflict assuming users are exchanging codes in person/face to face aka synchronously. Just thought I’d mention this edge case since it came to mind and I found it kind of interesting. ¯\_(ツ)_/¯

Also, I was just wondering if there’s a reason you’re going through Phoenix.Endpoint rather than Phoenix.PubSub directly e.g.

Feder.Endpoint.subscribe/unsubscribe/broadcast
# rather than
Feder.PubSub.subscribe/unsubscribe/broadcast
# assuming application.ex includes
def start(_type, _args) do
  children = [..., {Phoenix.PubSub, name: Feder.PubSub}]
  ...
end

It shouldn’t make a difference since Endpoint uses the PubSub server under the hood.

1 Like

Thanks for pointing it out! I want to figure out how to go about it. Maybe I can check all existing topics and avoid those?

I wonder that too. I actually want to know why Phoenix.Endpoint’s got all those PubSub functions inlined to it. I don’t see much explanation in docs. I went with Phoenix.Endpoint because it has one less parameter. I’m glad it would make no difference. But could there be any semantic difference to it?

Yeah, maybe it’s just a convenient wrapper to use the default app pub sub. The Phoenix.Channel docs show examples of using Phoenix.Endpoint to interact with it.

I’m not sure it’s possible or practical to check through existing topics. If you want to achieve this without using a DB or ETS, maybe look into an Agent and optionally a Registry to store a counter value that cycles from 0000 and 9999. That way your app would need to have over 10000 active codes for there to be a danger of collision.

2 Likes