guides/streams.md

# Phoenix Streams

LiveSvelte has native support for [Phoenix Streams](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream/4), enabling efficient DOM list management without holding full lists in memory on the server.

## Basic Streams

**LiveView:**

```elixir
defmodule MyAppWeb.ItemsLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, stream(socket, :items, MyApp.list_items())}
  end

  def handle_event("delete", %{"id" => id}, socket) do
    item = MyApp.get_item!(id)
    MyApp.delete_item!(item)
    {:noreply, stream_delete(socket, :items, item)}
  end

  def render(assigns) do
    ~H"""
    <.svelte name="ItemList" props={%{items: @streams.items}} socket={@socket} />
    """
  end
end
```

**Svelte Component:**

```svelte
<!-- assets/svelte/ItemList.svelte -->
<script>
  let { items } = $props()
</script>

{#each items as item (item.__dom_id)}
  <div id={item.__dom_id}>
    <p>{item.name}</p>
  </div>
{/each}
```

> #### Use `__dom_id` as the key {: .info}
>
> Always use `item.__dom_id` as the `{#each}` key. LiveSvelte uses this to track item identity for efficient updates.

## Stream Operations

All Phoenix stream operations work automatically:

```elixir
# Insert at the end (default)
socket |> stream_insert(socket, :items, new_item)

# Insert at the beginning
socket |> stream_insert(socket, :items, new_item, at: 0)

# Delete by item (must have :id field)
socket |> stream_delete(socket, :items, item)

# Reset the entire stream
socket |> stream(socket, :items, new_items, reset: true)
```

## Efficient Stream Patches

LiveSvelte sends stream changes as compact JSON Patch operations via `data-streams-diff`, rather than re-sending the full list on every change. This makes stream updates extremely efficient — inserting a single item sends a single operation regardless of list size.

The patch operations used:

| Operation | Description |
|-----------|-------------|
| `upsert` | Insert or update an item at a specific position |
| `remove` | Delete an item by `__dom_id` |
| `replace` | Reset the entire list |
| `limit` | Trim the list to the given max size |

These are applied client-side by the `SvelteHook` before updating the Svelte component's `items` prop.

## Accessing Stream Data in Components

Streams are passed as arrays to Svelte components. Each item has all its original fields plus `__dom_id`:

```svelte
<script>
  let { messages } = $props()
</script>

<ul>
  {#each messages as message (message.__dom_id)}
    <li id={message.__dom_id}>
      <strong>{message.user}</strong>: {message.text}
    </li>
  {/each}
</ul>
```

## Multiple Streams

Pass multiple streams to a single component:

```elixir
def mount(_, _, socket) do
  {:ok,
   socket
   |> stream(:messages, [])
   |> stream(:users, [])}
end

def render(assigns) do
  ~H"""
  <.svelte
    name="Chat"
    props={%{messages: @streams.messages, users: @streams.users}}
    socket={@socket}
  />
  """
end
```

```svelte
<script>
  let { messages, users } = $props()
</script>

<!-- Both streams update independently and efficiently -->
```

## Encoding Stream Items

Stream items go through `LiveSvelte.Encoder` before being sent to the client. For custom structs, add `@derive`:

```elixir
defmodule MyApp.Message do
  @derive {LiveSvelte.Encoder, only: [:id, :user, :text, :inserted_at]}
  defstruct [:id, :user, :text, :inserted_at]
end
```

The `@derive` restriction is enforced — fields not in `only:` are excluded even after `__dom_id` is added.

## ID-Based Diffing

For arrays where items have an `:id` field, LiveSvelte uses ID-based list diffing (Tier 3 of the props diffing system). This means:

- Inserting at position 0 sends a single `upsert` op, not N `replace` ops
- Reordering sends minimal operations
- List updates stay efficient regardless of list size

Items must have an `:id` field for ID-based diffing to activate. The `__dom_id` set by Phoenix Streams already guarantees this.