# Phoenix Setup

Musubi is Phoenix-first. The runtime uses Phoenix sockets and channels as the
transport, while application modules implement Musubi callbacks.

## Endpoint

Register one Musubi socket in the endpoint:

```elixir
defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  socket "/socket", MyAppWeb.UserSocket,
    websocket: true,
    longpoll: false
end
```

If the application needs Phoenix session data in stores, pass the session
through `connect_info`:

```elixir
socket "/socket", MyAppWeb.UserSocket,
  websocket: [connect_info: [session: @session_options]],
  longpoll: false
```

The Musubi socket captures `connect_info[:session]` during connect. Every root
mounted on the same Musubi connection can read it with:

```elixir
Musubi.Socket.session(socket)
```

## Socket Module

Use `Musubi.Socket` instead of `Phoenix.Socket` in application code:

```elixir
defmodule MyAppWeb.UserSocket do
  use Musubi.Socket,
    roots: [
      MyApp.Stores.DashboardStore,
      MyApp.Stores.PollRoomStore
    ]

  @impl Musubi.Socket
  def handle_connect(params, socket) do
    case MyApp.Auth.verify(params["token"]) do
      {:ok, user} -> {:ok, Musubi.Socket.assign(socket, :current_user, user)}
      :error -> :error
    end
  end

  @impl Musubi.Socket
  def handle_join(_params, socket), do: {:ok, socket}
end
```

`use Musubi.Socket` generates the Phoenix socket adapter internally and
registers the `"musubi:*"` channel route. Do not add a separate Musubi channel
entry by hand.

## Callback Responsibilities

`handle_connect/2` runs once when Phoenix establishes the socket. It receives
transport params from the client. Use it for:

- user authentication
- long-lived assigns shared by every Musubi connection and mounted root
- rejecting the socket with `:error` or `{:error, :unauthorized}`

`handle_join/2` runs once when the Musubi connection channel joins. It receives
the connection join params, not root mount params. Use it for:

- workspace or tenant scope checks
- connection-level authorization
- setting assigns shared by every root mounted on that Musubi connection

Root store `mount/2` runs once for each mounted root. It receives the root's
own mount params:

```elixir
@impl Musubi.Store
def mount(%{"poll_id" => poll_id}, socket) do
  socket =
    socket
    |> assign(:poll_id, poll_id)
    |> assign(:current_user, socket.assigns.current_user)

  {:ok, socket}
end
```

## Params, Session, And Connect Info

Use these scopes consistently:

| Scope | Source | Lifetime | Use for |
| :-- | :-- | :-- | :-- |
| connect params | `new Socket("/socket", params: ...)` | Phoenix socket | auth tokens and transport credentials |
| connect info | Phoenix endpoint `connect_info` | Phoenix socket | session, peer data, headers configured by the endpoint |
| session | `connect_info[:session]` | Musubi socket | browser session data shared by mounted roots |
| join params | `connect(socket, { topic: ... })` channel join payload | Musubi connection | connection-wide scopes |
| mount params | `connection.mountStore({ module, id, params })` | one root store | business identity such as `poll_id` or `cart_id` |

Child stores do not receive `mount/2`. They run `init/1` and can still read
root params, session, connect info, and inherited assigns from the socket.

## Root Declaration Rules

Only stores declared in `roots: [...]` may be mounted by the client:

```elixir
use Musubi.Socket,
  roots: [
    MyApp.Stores.CartPageStore,
    MyApp.Stores.InboxStore
  ]
```

The client mounts by module string and root id. `mountStore` returns a
`{ store, unmount }` pair:

```ts
const { store: cart, unmount } = await connection.mountStore({
  module: "MyApp.Stores.CartPageStore",
  id: "cart:current-user",
  params: { cart_id: "current-user" },
})
```

Calling `mountStore` again with the same root id on one Musubi connection
attaches to the existing mounted root instead of creating a second
server-side root. Each caller receives its own `unmount` handle, but the
underlying root stays mounted until the last caller unmounts:

```ts
await unmount()
```

Child stores are server-owned and cannot be mounted or unmounted directly by
the client.

## Live Reloading on Store Changes

For dev-time auto-refresh of the generated TypeScript bundle, include your
Musubi store directory in Phoenix's live-reload patterns and point the
codegen output into your JS dev server's watched tree:

```elixir
# config/dev.exs
config :my_app, MyAppWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r"lib/.*/stores/.*\\.ex$",
      # ... existing patterns
    ]
  ]

config :musubi, :ts_codegen_output_path, "ui/src/generated/musubi.d.ts"
```

The chain is: edit a store, save, `Phoenix.CodeReloader` recompiles on the
next HTTP request, `:musubi_ts` rewrites the bundle, the JS dev server (Vite
HMR / equivalent) picks up the file change.

If you change a store in IEx without a request firing, `recompile` in the
IEx prompt runs the same compiler chain and updates the bundle.
