Ctrl K
ComunidadGuías…

proposals/2026-07-30-favorites-live-update.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# A favorite made on one device appears on the others without reopening the app

**Goal** - the host tells a person's OTHER live devices when their favorites change, so the
hearts and the Favorites list update while the app is open instead of on the next relaunch.

**Tier** - T2. A new push kind on the media channel (an IPC message shape).

## Why

Reported by Tim 2026-07-30, twice: favoriting an artist on the Pixel did not appear in the TCL's
Favorites list, and the same for a track. "If I dismiss the app and reopen it, everything gets
updated and looks to be in sync." An earlier report the same day was the same bug seen from the
other side: the Favorites LIST had the item but the heart on the album/artist/song was not filled.

The cause is that **no push exists for user state.** The host pushes `library-renamed`,
`devices:changed`, `request:new`, `request:resolved` and `session-superseded` - and nothing at all
for favorites, so a device that is already connected is never told. `favorites()` in the worklet
always reads live from the host (its on-disk copy is only an offline fallback), so the staleness is
entirely in WHEN the app asks: on mount, on `host:connected`, on a merged host joining, on a host
switch, and on the local toggle. A phone sitting connected asks at none of those moments, which is
exactly why a relaunch fixes it.

That also explains the list-and-heart disagreement in the earlier report. The Favorites list
refetches its resolved rows when the screen is opened (`setFavItems(null)`), but the id sets that
fill the hearts do not - so opening the list showed the new favorite while the album page it came
from still showed an empty heart. One root cause, two symptoms.

## Scope

- `presence.notifyOwner` gains an `exceptDevice` option, so a push reaches a person's OTHER
  devices and not the one that just made the change (which already knows, and re-rendering it from
  a push would fight its own optimistic update).
- `fav.set` pushes `favorites:changed` with `{ kind, id, on, libraryId }` after a successful
  write. The payload is enough for a client to be specific later; today the client just refreshes.
- The worklet forwards it to the UI as an event. It does NOT refresh its own copy - `favorites()`
  is already a live read, so a second fetch would be wasted.
- The UI reloads the id sets and drops the resolved list so it refetches when next opened.

**Not in scope.** Playlists have the same gap - one created on another device does not appear
until a reload - and so, less visibly, do resume positions and play counts. The mechanism here
generalises to them, but favorites is what was reported and what has a visible control on nearly
every screen. Playlists go in TODO.md as a follow-up rather than being fixed blind.

**Deliberately not a full sync protocol.** The host stays the single authority and the client
stays a live reader; this only removes the need to ASK at the right moment.

## Compat

- The push is additive. An OLD client never receives the kind because an old host never sends it;
  a NEW client against an OLD host just never gets the event and behaves exactly as today, which
  is the current behavior, not a regression.
- An old client receiving an unknown push kind ignores it - `onPush` already falls through
  unknown kinds without throwing. Verified rather than assumed: the handler is an if/else-if chain
  with no else.
- No stored data changes. Nothing to migrate, nothing to roll back on disk.
- A device that was OFFLINE when the change happened misses the push and picks the change up on
  its next `host:connected`, which already reloads favorites.

## Verify

- Unit: `notifyOwner` reaches a person's other devices and skips `exceptDevice`; it still reaches
  every device when no exception is given.
- Integration, two devices under one person against a real host: B favorites, A receives
  `favorites:changed` with the right kind and id, and A does NOT receive its own.
- On hardware, the exact report: favorite an artist on one phone, and the other phone's Favorites
  list and heart update WITHOUT the app being reopened. Both phones must stay in the foreground
  for the whole test, since backgrounding drops the connection and would make a later reload look
  like a success.

## Rollback

Revert. The push stops being sent, clients stop hearing it, and the app is back to reloading on
connect. Nothing persists.

## Open questions

- Should the client use the payload to patch its id sets in place rather than refetching the whole
  list? Cheaper on a large library, but it has to be right about merged mode (the sets are a UNION
  across hosts, so a removal on one host is only a removal if no other host still has it). Starting
  with a refetch, which cannot be wrong; revisit if the round-trip is felt.