https://github.com/NatTuck/hangman-2019-01 (branch: 01-31-channel-hangman)
This snippet of code is useful for managing secrets in config/prod.exs and rel/config.exs
Feel free to use this in your projects with the attribution comment. This assumes that your build machine and your production server are the same. That may not be a good idea for larger deployments and stops working once you have two web servers.
get_secret = fn name ->
# Secret generation hack by Nat Tuck for CS4550
# This function is dedicated to the public domain.
base = Path.expand("~/.config/phx-secrets")
File.mkdir_p!(base)
path = Path.join(base, name)
unless File.exists?(path) do
secret = Base.encode16(:crypto.strong_rand_bytes(32))
File.write!(path, secret)
end
String.trim(File.read!(path))
end
## In config/prod.exs
secret_key_base: get_secret.("key_base");
...
password: get_secret.("db_pass") # Manually make file match password
## In rel/config.exs
set cookie: String.to_atom(get_secret.("dev_cookie"))
e.g. Hangman from last week.
A single-page / single component React app.
One state as value object.
Pure function renders state to web page.
In last week's version, update logic was in browser.
Client-Side Only like this is plan A for state.
Pro: Pure JS
Pro: Free for Server
Con: User actually controls what the code does
Con: State is stuck in one user's browser
Con: State is lost if user navigates away from page.
Plan B: Server-Side
But how to manage the state on the server?
Can't easily use HTTP requests.
Plan B.1: Store State in Channel
e.g. Hangman from Tuesday
Plan B.2: Outside of Channel
Let's start looking at strategies for managing state.
Where are we?
Keep going:
We're going to add an agent to our hangman game to back up the game state.
lib/hangman/backup_agent.ex:
defmodule Hangman.BackupAgent do
use Agent
# This is basically just a global mutable map.
# TODO: Add timestamps and expiration.
def start_link(_args) do
Agent.start_link(fn -> %{} end, name: __MODULE__)
end
def put(name, val) do
Agent.update __MODULE__, fn state ->
Map.put(state, name, val)
end
end
def get(name) do
Agent.get __MODULE__, fn state ->
Map.get(state, name)
end
end
end
Start this process with the app.
lib/hangman/application.ex, in children list:
...
# Starts a worker by calling: Hangman.Worker.start_link(arg)
# {Hangman.Worker, arg},
Hangman.BackupAgent,
...
Modify the channel to make backups:
def join("games:" <> name, payload, socket) do
if authorized?(payload) do
game = BackupAgent.get(name) || Game.new()
socket = socket
|> assign(:game, game)
|> assign(:name, name)
BackupAgent.put(name, game)
{:ok, %{"join" => name, "game" => Game.client_view(game)}, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
def handle_in("guess", %{"letter" => ll}, socket) do
name = socket.assigns[:name]
game = Game.guess(socket.assigns[:game], ll)
socket = assign(socket, :game, game)
BackupAgent.put(name, game)
{:reply, {:ok, %{ "game" => Game.client_view(game)}}, socket}
end
iex -S mix phx.server
:observer.start()
Look at state for the BackupAgent process.
New problem: No way to get rid of z word.