Integrating Cizen with Phoenix (Part 2)

Yuki Kodama
Cizen
Published in
5 min readNov 18, 2018

In the previous article “Integrating Cizen with Phoenix”, I introduced Cizen and explained how to integrate with Phoenix Framework through building an example application “Hello World”. Most of you feel that the example is too simple because it’s not an interactive application. So, in this article and upcoming articles, I’d like to add more features to the application to explore other Cizen’s features.

  • Ask visitor’s name before greeting (handling HTTP POST request)
  • Show hit counter (using a state of automaton)
  • One-to-one: automaton per client (launching automaton dynamically)

Ask visitor’s name before greeting

Let’s start with a simple but interactive feature; Asking visitor’s name before greeting. For this feature, Greeting automaton needs another event, so we add MyNameIs event which has name field to get visitor’s name.

defmodule HelloWorld.Events.MyNameIs do
defstruct [:name]
end

Greeting automaton subscribes the new event in the spawn/2 function. Since the original “Hello World” application doesn’t have a state, the spawn/2 function simply returned :loop atom. To keep a name of a visitor in the automaton’s state, we return a map with a field name. This is a way to have a state in Cizen.

Notice that the automaton has a possibility to receive two events; Greeting and MyNameIs. Pattern matching is useful for switching what to do based on an event type. In case of receiving a MyNameIs event, the yield/2 function returns a map with the given name to update the state. When you receive a Greeting event, you need to dispatch a response event Reply and return the current state without modifying.

defmodule HelloWorld.Greeting do
use Cizen.Automaton
defstruct [] @impl true
def spawn(id, _) do
# Subscribe "MyNameIs" event
perform id, %Subscribe{
event_filter: Filter.new(
fn %Event{body: %HelloWorld.Events.MyNameIs{}} -> true end
)
}
# Subscribe "Greeting" event
perform id, %Subscribe{
event_filter: Filter.new(
fn %Event{body: %HelloWorld.Events.Greeting{}} -> true end
)
}
# Blank on init
%{name: ""}
end
@impl true
def yield(id, %{name: name}) do
# Wait for events
event = perform id, %Receive{}
case event.body do
%HelloWorld.Events.MyNameIs{name: name} ->
# Update state with the given name
%{name: name}
%HelloWorld.Events.Greeting{} ->
# Prepare a message based on the current state
message = case name do
"" -> "What is your name?"
_ -> "Hello #{name}"
end
# Respond to "Greeting" event
perform id, %Dispatch{
body: %HelloWorld.Events.Greeting.Reply{
greeting_id: event.id,
message: message,
name: name
}
}
# No changes
%{name: name}
end
end
end

CurrentGreeting automaton has a single yield/2 function, but you can define multiple yield/2 functions to reduce a number of combinations with a received events and possible states. For example, we can consider to change the above automaton so that it has two states; :dont_know (instead of %{name: “”}) and {:know, %{name: "kuy"}}. As a slightly complicated behavior, we think about an automaton that accepts MyNameIs event only once. You cannot change the name after the automaton transited to :know state. Here is a transition diagram that represents what I explained.

And this is an example code that implements separated yield/2 functions.

@impl true
def yield(id, :dont_know) do
# Wait for events
event = perform id, %Receive{}
case event.body do
%HelloWorld.Events.MyNameIs{name: name} ->
# Update state to :know with the name
{:know, %{name: name}}
%HelloWorld.Events.Greeting{} ->
# Respond to "Greeting" event
perform id, %Dispatch{
body: %HelloWorld.Events.Greeting.Reply{
greeting_id: event.id,
message: "What is your name?",
name: ""
}
}
# No changes
:dont_know
end
end
@impl true
def yield(id, {:know, %{name: name}}) do
# Wait for events
event = perform id, %Receive{}
case event.body do
%HelloWorld.Events.Greeting{} ->
# Respond to "Greeting" event
perform id, %Dispatch{
body: %HelloWorld.Events.Greeting.Reply{
greeting_id: event.id,
message: "Hello #{name}",
name: name
}
}
# No changes
{:know, %{name: name}}
# Ignore "MyNameIs" event
_ -> {:know, %{name: name}}
end
end

The full code of this implementation is here.

Let’s take a look at PageController to connect both Cizen and Phoenix worlds. Before adding tell action to the controller, we need to add it in the router first.

scope "/", HelloWorldWeb do
pipe_through :browser
get "/", PageController, :index
post "/name", PageController, :tell
end

Okay, add tell action to receive a name of a visitor through an HTTP POST request. On a tell action, we use Dispatch effect to send a MyNameIs event with the visitor’s name. The difference between Request and Dispatch effects is that Dispatch effect doesn’t wait for a response event.

alias Cizen.Effects.{Dispatch, Request}
alias HelloWorld.Events
defmodule HelloWorldWeb.PageController do
use HelloWorldWeb, :controller
# Required to use "handle" below
use Cizen.Effectful
def index(conn, _params) do
# Ask a message
%{body: %{message: message, name: name}} = handle fn id ->
perform id, %Request{body: %Events.Greeting{}}
end
render(conn, "index.html", message: message, name: name, token: get_csrf_token())
end
def tell(conn, %{"name" => name}) do
# Tell visitor's name
handle fn id ->
perform id, %Dispatch{body: %Events.MyNameIs{name: name}}
end
redirect(conn, to: "/")
end
end

We also change the template to add a form.

<h1><%= @message %></h1>
<p>
<form method="POST" action="/name">
<input type="hidden" name="_csrf_token" value="<%= @token %>" />
<input type="text" name="name" value="<%= @name %>" />
<input type="submit" value="Tell" />
</form>
</p>

That’s all! Finally, start Phoenix’s dev server mix phx.server and open http://localhost:4000 in your browser. If you’re running Version 1 (single yield/2 function), you can change your name anytime. In contrast, Version 2 (separated yield/2 functions) doesn’t allow you to change your name multiple times.

By the way, close your browser tab/window, and then open http://localhost:4000 again. Can you still see your name? It’s like a session! Greeting automaton still alive after closing the browser tab. You will see the same name even if you open more tabs because we share the same instance of the automaton for all visitors. An upcoming article will show you that the application launches an automaton for each client.

Here is the full code for this post: https://github.com/kuy/cizen-phoenix-hw2

--

--

Yuki Kodama
Cizen
Writer for

Web Developer. JavaScript / Ruby on Rails / AWS. Using Vim and loving coffee. ex-TortoiseHg maintainer.