DurableStash (durable_stash v0.1.1)

Copy Markdown View Source

Keep Phoenix LiveView state alive across reconnects, crashes, and redeploys.

A LiveView's assigns live in memory and vanish when the socket drops, the process crashes, or you deploy. DurableStash saves the assigns you pick to object storage and restores them the next time the LiveView mounts. The saved copy belongs to the browser session, so every LiveView the user opens shares it. Under the hood it plugs into LiveStash and stores state through DurableServer. State dies with the browser session.

Usage

defmodule MyAppWeb.SomeLive do
  use MyAppWeb, :live_view
  use LiveStash, adapter: DurableStash, stored_keys: [:count, :username]

  def mount(_params, _session, socket) do
    socket = assign(socket, count: 0, username: nil)
    {_status, socket} = LiveStash.recover_state(socket)
    {:ok, socket}
  end
end

Call LiveStash.recover_state/1 in mount/3 after assigning defaults (recovered values overwrite them), and LiveStash.stash/1 whenever a stored assign changes — or pass auto_stash: true.

Unlike the stock ETS adapter, DurableStash recovers on every mount — fresh navigations included — and never deletes the stash on mount. State is keyed by the browser session, not by the socket.

Setup

  1. Register the adapter and configure the backend:

    config :live_stash, adapters: [DurableStash]
    
    config :durable_stash,
      backend: {DurableServer.Backends.ObjectStore, bucket: "...", ...},
      prefix: "durable_stash/",
      secret: "some-stable-secret"

    With that config, LiveStash.Application starts the DurableServer supervisor automatically through child_spec/1. Alternatively, run your own DurableServer.Supervisor and point the adapter at it with the :supervisor option.

  2. Put a session id into the cookie session, in the :browser pipeline after plug :fetch_session:

    plug :ensure_session_id
    
    defp ensure_session_id(conn, _opts) do
      if get_session(conn, "sid") do
        conn
      else
        sid = 16 |> :crypto.strong_rand_bytes() |> Base.url_encode64(padding: false)
        put_session(conn, "sid", sid)
      end
    end

Scopes

Not all state wants the same recovery policy. Each stored key declares a scope:

use LiveStash, adapter: DurableStash,
  stored_keys: [
    theme: :session,    # recover on every mount (the default)
    draft: :reconnect   # recover only on reconnects; cleared on fresh mounts
  ]
  • :session — recovered on every mount: live navigation, reconnects, crashes, redeploys. Right for settings the user expects to stick.
  • :reconnect — recovered only when the client rejoins an existing view (_mounts > 0): Wi-Fi drops, LiveView crashes, and redeploys — the browser stays on the page through all of these. A fresh navigation to the view clears the stored values, so starting a "new thing" starts blank. Right for in-progress form drafts.

Options (via use LiveStash, adapter: DurableStash, ...)

  • :stored_keys (required) — assigns to persist. Bare atoms mean :session scope; see Scopes above for :reconnect. :permanent is reserved and raises for now.
  • :vsn (default 1) — version of this view's stored shape. On recovery, a stored slice with a different vsn is discarded to defaults unless :migrate is given.
  • :migrate — 2-arity function (old_vsn, data) :: data receiving the stored string-keyed data map and returning the migrated one. The migrated set is written back under the new vsn.
  • :supervisor — DurableServer supervisor name (default config :durable_stash, :supervisor_name, falling back to DurableStash.Supervisor).
  • :secret — mixed into the storage-key hash (default config :durable_stash, :secret).
  • :session_id_key — cookie-session key holding the session id (default "sid").

What's storable

JSON-safe values only: no structs, tuples, pids, or functions; maps come back with string keys. Offending values are skipped with a logged error, or raise when config :durable_stash, on_invalid_value: :raise is set (recommended for dev and test). Values are normalized through a JSON round-trip at stash time, so what you recover in dev is byte-for-byte what you'd recover after a redeploy in prod.

Summary

Functions

Starts the DurableServer supervisor from :durable_stash config when a :backend is configured; otherwise starts an empty, harmless supervisor. Invoked automatically by LiveStash.Application for registered adapters.

Functions

child_spec(args)

Starts the DurableServer supervisor from :durable_stash config when a :backend is configured; otherwise starts an empty, harmless supervisor. Invoked automatically by LiveStash.Application for registered adapters.