DurableStash (durable_stash v0.1.0)

Copy Markdown View Source

Durable, browser-session-scoped server state for Phoenix LiveView.

DurableStash is a LiveStash adapter backed by DurableServer: one durable process per browser session, persisted to S3-compatible object storage, shared by every LiveView of that session. State survives live navigation, WebSocket reconnects, LiveView crashes, and redeploys — and 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.