# `PhoenixKitCatalogue.Catalogue.PubSub`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/0.8.0/lib/phoenix_kit_catalogue/catalogue/pub_sub.ex#L1)

Real-time fan-out for catalogue mutations.

Every successful write in the Catalogue context broadcasts a small
`{:catalogue_data_changed, kind, uuid, parent_catalogue_uuid}` event
to a single shared topic. List/detail LiveViews `subscribe/0` once in
`mount/3` (after `connected?(socket)`) and re-fetch the affected
slice on any event, so two admins editing the same data converge
without manual refresh.

`parent_catalogue_uuid` lets a detail LV cheaply ignore broadcasts
for unrelated catalogues — without it, *every* item edit anywhere in
the system would force every open detail page to reload its slice.
Global resources (manufacturers, suppliers, manufacturer↔supplier
links) carry `nil` here; consumers that care about them subscribe
to the `kind` regardless of parent.

Payloads are intentionally minimal — UUID + kind + parent, no record
data — to (a) avoid leaking field-level changes through PubSub, and
(b) keep the consumer in charge of how much to re-load (single row
vs full list).

Subscriptions are cleaned up automatically when the LV process
terminates; callers don't need to unsubscribe.

# `event`

```elixir
@type event() ::
  {:catalogue_data_changed, kind(), Ecto.UUID.t() | nil, Ecto.UUID.t() | nil}
```

Event message format for `handle_info/2`.

# `kind`

```elixir
@type kind() ::
  :catalogue
  | :category
  | :item
  | :folder
  | :manufacturer
  | :supplier
  | :smart_rule
  | :links
  | :pdf
```

Resource kind that mutated.

# `broadcast`

```elixir
@spec broadcast(kind(), Ecto.UUID.t() | nil, Ecto.UUID.t() | nil) :: :ok
```

Broadcasts a `{:catalogue_data_changed, kind, uuid, parent_catalogue_uuid}`
event after a successful write.

* `uuid` — UUID of the resource that mutated; `nil` when the change
  isn't tied to a specific record (e.g. a bulk link sync).
* `parent_catalogue_uuid` — UUID of the catalogue that contains the
  mutated resource, or the UUID itself for `kind: :catalogue` events.
  Pass `nil` for resources that aren't scoped to a single catalogue
  (`:manufacturer`, `:supplier`, `:links`); detail LVs use this to
  filter out cross-catalogue noise.

# `broadcast_bulk_change`

```elixir
@spec broadcast_bulk_change(
  Ecto.UUID.t(),
  :trashed | :restored | :moved | :permanent_delete,
  [Ecto.UUID.t()],
  pid()
) :: :ok
```

Broadcasts a bulk-change event so other open detail pages animate
the affected items leaving / arriving on screen.

`kind`:
  * `:trashed` — items are going away (red flash → state refresh).
  * `:restored` — items are coming back (state refresh → green flash).
  * `:moved` — items are leaving one scope and entering another
    (red flash on source DOM → state refresh → green flash on
    destination DOM).
  * `:permanent_delete` — items are gone for good (same animation
    as `:trashed`; the row removal is harder to undo but the visual
    cue is the same).

`uuids` is the affected item list. The receiver does a full
`reset_and_load` since a bulk change can rearrange every visible card;
per-scope filtering would only be a worthwhile optimisation once
catalogues routinely render dozens of cards.

# `broadcast_card_refresh`

```elixir
@spec broadcast_card_refresh(
  Ecto.UUID.t(),
  Ecto.UUID.t() | :uncategorized,
  Ecto.UUID.t() | nil,
  atom(),
  pid()
) :: :ok
```

Broadcasts a card-refresh event so other open detail pages re-fetch
a single category card's items after a reorder.

`scope` is a category UUID or `:uncategorized`. `from` is the
originating process; receivers compare against `self()` to skip
self-originated events (the source LV already updated locally).

`flash_uuid` + `flash_status` let the receiver fire a
`sortable:flash` push_event keyed to the moved row, so a second
open tab sees the same green/red flash the originator did.

# `broadcast_category_reorder`

```elixir
@spec broadcast_category_reorder(
  Ecto.UUID.t(),
  Ecto.UUID.t() | nil,
  atom(),
  pid()
) :: :ok
```

Broadcasts a category-reorder event so other open detail pages
re-fetch the category list (positions changed). Heavier than
`broadcast_card_refresh/5` — receivers do a full reset_and_load
since category order affects every streamed card on the page.

# `subscribe`

```elixir
@spec subscribe() :: :ok | {:error, term()}
```

Subscribes the current process to the catalogue PubSub topic.

Call from `mount/3` guarded by `connected?(socket)` so the
disconnected (initial render) pass doesn't subscribe and never
unsubscribes. Do this **after** any subscription requirements but
**before** the initial DB load to avoid a race where a write between
the load and the subscribe leaves the UI stale.

# `topic`

```elixir
@spec topic() :: String.t()
```

Returns the canonical topic name. Useful for tests.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
