A suspended tab's retry timer could run before it was shown again: touch()
recreated the record another tab had removed when it adopted the page's
writes, so the page sent them again over newer edits. The facade now
treats a page whose record is gone as superseded: the queue sends nothing
(maySend hook), its journal and record are not written again, and the
retry timer reloads it like a page shown again.
A re-read answered after another tab saved an entry no longer writes the
older answer over the shared cache: entries changed while the read
travelled are kept, with an unknown tag, so the next write here
conflicts and replays instead of removing what the other tab saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A background tab the browser suspended past the record lifetime can have
its writes adopted elsewhere, like a back-forward cache page: shown
again, it now checks resumed() and reloads instead of sending them.
Conflict replays appended a newly viewed item at the end of the history,
where the next 50-item cut dropped it; a replayed recently viewed list is
now kept newest first.
isFavorite() waits for the first user-data read like the list reads, so
a detail page opened during a slow first read does not toggle an
existing favorite the wrong way. A parked backup restore replayed when
the playlist opens waits too, and stays parked while no read succeeded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Going offline now marks the instance unchecked: the token and writes
wait until /instance answers with this page's instance (another one
reloads the page). A replaced volume at the same address no longer gets
the old token and queued user-state writes on reconnect. The session
checks as soon as it goes offline, so a blip costs no retry interval.
A page restored from the back-forward cache whose record another tab
adopted meanwhile (its timers were frozen, so the record went stale)
reloads instead of sending those writes again after newer edits.
The Stalker playlist metadata migration also runs on server rows, once
per server: a desktop database copied to the server can predate it, and
its Stalker rows lacked isFullStalkerPortal.
The take-over and change-diff loops move out of the user-data facade,
and the adoption specs get their own file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Startup waits for the first user-data read only a few seconds. ready()
now waits for a first read in flight without starting one (an offline
page is not held up) and tells whether one succeeded:
- PwaXtreamDataSource awaits it before reading favorites, recent items
and playback positions, so a new device whose /user-state read is slow
does not show empty lists or start playback without its resume point.
- A PWA backup export and restore fail while no read succeeded; the
restore stays parked, since its replacement lists would otherwise
merge into the server's (an empty favorites list changed nothing).
A page-hide keepalive write gets the request timeout: a server that
takes it and never answers no longer holds every later write, re-read
and delete behind it until reload.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user-data cache is keyed by server instance
(`iptvnator-server:cache:<instanceId>:<namespace>`): a tab still open on
the old backend after BACKEND_URL changed no longer writes over the new
server's cache, which the new tab's next edit would have sent there.
claimCache also removes the old owner's keys and the unscoped keys of
earlier builds.
touch(), which runs every 30 s, adopts closed tabs' saved writes once
this tab has read: a tab with nothing to sync never re-reads, so they
waited until a tab switch or reload.
UserDataStorage gains an optional ready(); the server storage resolves
it after its first read and rejects while that fails. A PWA backup
export awaits it, so a fresh browser whose /user-state read is slow or
failed cannot export empty favorites, history and positions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PlaylistEffects.setFavorites$ applies the drag's order through
transformPlaylistFavorites (reorderFavorites): favorites added on another
device stay, ones removed there stay out.
- The remembered server matches the backend URL by origin and path
without trailing slashes, so /api and /api/ start offline in server
mode alike.
- Maps keyed by playlist id are built without a prototype (idMap), so an
id such as __proto__ is an ordinary key in the cache, conflict replays,
re-reads and tags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ServerUserDataStorage records each successful delete; a re-read sent
before it drops the deleted playlist's entries and tags from the server's
answer (withoutDeleted), so a late answer cannot write the deleted
favorites and positions back into the cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When touch() finds that the previous page load no longer looks live, this
page may already have read and queued newer edits. Its saved writes and
early edits are now adopted like a closed tab's (their steps ahead of this
page's newer ones, queued at once) instead of restored as if this page
had just started, which replaced newer progress with older and parked
early edits that were never sent after the first read. Cache forgetting
moves into UserDataCache.forget().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the close flag could not be saved (storage full), a reload took the
previous page load for a running duplicated tab and left its saved writes
unsent. The page now remembers that page load and its 30 s touch takes it
over as soon as its record is closed, stale or gone, so the session's
retry sends the writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A page marks its unconfirmed-writes record closed on pagehide (not into
the back-forward cache); a new page takes over the previous page load's
saved writes only when that record is closed, stale or missing, so a
duplicated tab, which copies sessionStorage, leaves the running tab its
writes.
- PlaylistsService.removeAll lets queued playlist writes land first and
makes writes issued meanwhile wait until it finished; the server store
refuses a document without an id.
- Moving browser data sets the explicit Stalker portal-mode flag on rows
from before it (withExplicitStalkerPortalFlag, now shared with
PlaylistsService).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PlaylistEffects.updateFavorites$ applies the toggled channel through
transformPlaylistFavorites, on the favorites read at write time, instead
of writing the reducer's whole array: a conflict retry keeps another
device's favorites (#1919).
- Edits made before the first read on top of this tab's recovered unsent
write are appended to it as their own steps, so a removal of an item the
recovered write adds survives a conflict replay.
- Journal copies of earlier page loads kept because this page could not
save its own are removed once it can.
- adopt/extend move into server-user-data-adopt.ts over a small queue API
(unsent, isQueued, put); the queue exposes its tag map.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A served copy carries the version it came from (a symbol property that
spreads keep and JSON drops), so a mutation of a portal document names
the version it was built on, not the latest read.
- A 401 for a token replaced since (a new sign-in) neither clears the new
token nor reports unauthorized, in the API client, the HttpClient
interceptor and PwaService's proxy calls.
- Moving browser data counts a browser user-data map that cannot be read
as a failure, so the move stays on offer.
- An edit made before the first read on top of this tab's recovered
unsent write is applied to that write, not to the server's value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Every playlist mutation is sent on the version it was read at: the
payload's ETag, else the playlist's last read (portal documents have no
payload). Refused versions are tracked per playlist, so a conflict on
one never disables the check on another at the same revision. Imports
and start-up migrations (dbUpsertAppPlaylists) stay unconditional.
- A closed tab's adopted edits replay before this tab's queued ones, from
the base of its first step, so newer progress is not replaced by older.
- A reload or adoption removes the old page load's saved writes only when
this page saved its copy; each closed tab is adopted once per page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The 30 s retry also re-reads and flushes while online when the first
user-data read failed or writes the server refused (a 5xx) are queued;
the same failure notice shows at most every five minutes.
- Moving browser data adds a legacy snapshot alias only where no typed
snapshot exists, so a newer typed copy is not overwritten.
- writeServerAccessToken reports whether storage kept the token; the
sign-in page says so (SERVER_STORAGE.SIGN_IN_STORAGE, 19 locales)
instead of calling a right token rejected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ServerUserDataStorage remembers the map getItem handed out. When the
cache changed since (another tab saved, a re-read), a write applies only
the entries the caller changed to the current cache, so a map read
before another tab's save no longer reads as deleting that save. The
cache moves into UserDataCache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Steps of a write whose answer was lost, of saved writes taken over after
a reload, and of a closed tab's saved edits are marked uncertain, so a
later edit never joins them: adding and then removing an item after a
lost answer still removes it.
- A closed tab's saved edits are queued ahead of this tab's own even when
the cache already shows them, so a conflict replays them too.
- The tag map moves into UserStateTags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A queued write keeps its edits as steps: an edit continuing the last one
joins it, one made behind a travelling write or on another tab's change
stays apart, and a conflict replays them in order. Saved writes and
early edits store the steps too.
- The instance probe treats any refusal but 404 (a proxy's 429 or 403) as
unreachable, so a browser keeps its remembered server.
- Cache reads that browser storage refuses fall back to memory, so a
re-read and later edits still work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unconfirmed writes and early edits are saved in localStorage per page
load; the tab remembers its page load in sessionStorage, so a reload
takes them over, and another tab replays a closed tab's writes once its
record is stale. Live tabs renew their record every 30 s.
- An edit made on another tab's unsent change, and an entry a re-read
keeps from the cache, are sent on a version no entry has, so the write
conflicts and is replayed instead of carrying another tab's change past
a later removal.
- Moving browser data counts the parental-lock step as failed while the
lock store cannot be read, so the move stays on offer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An edit queued behind a travelling write keeps its own before → after
and carries the travelling write's edit as `prior`, also in the saved
writes. A conflict replays both in order, so adding and then removing a
favorite removes it whether or not the first write landed, and a lost
write is not dropped.
- Page-hide writes now report their new tag or a 412 like normal writes:
a confirmed one rebases the next edit, a conflict is replayed.
- After an offline start from a remembered instance, writes are held and
the retry only checks /instance before reloading, so saved writes never
reach another database behind the same address.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A re-read that keeps an entry from the cache (another tab's or this
tab's unconfirmed value) no longer takes the server's tag for it, so the
next edit conflicts and is replayed instead of overwriting another
device's change.
- Page-hide writes behind a travelling one are taken from the queue when
it is answered, so its new version becomes their baseline.
- A playlist delete holds that playlist's user-data edits instead of
discarding them up front; only a successful delete drops them, a failed
one sends them as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- User-state entries carry a content tag; PUT/DELETE accept If-Match or
If-None-Match: * and answer 412 with the current value and tag. The PWA
sends each edit on the tag it read and replays it (before → after) on
the server's value after a conflict, so another device's change to the
same playlist's favorites or positions is kept instead of overwritten.
- The access token goes only to the backend URL (same origin and path),
never to another host or a sibling path.
- Deleting a playlist (or all) first drops its queued user-data edits and
waits for writes in flight, so none recreates deleted data.
- The migration copy writes on the tags it read.
- The write queue moves out of ServerUserDataStorage into
UserDataWriteQueue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unconfirmed writes and early edits are kept in the tab's own
sessionStorage, so tabs never clear each other's; a shared registry of
unconfirmed entries (one record per tab, ignored after ten minutes)
keeps a re-read in one tab from replacing another tab's values.
- A cache value too big for localStorage stays readable from memory, so
later edits are diffed against what the app reads.
- Typing legacy Xtream ids keeps each snapshot's bare key as an alias, so
a reference not yet rewritten still resolves and a retry can type it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Edits made before the first read are saved (before → after) and
restored after a reload; a second early edit keeps the first one's
baseline, even an empty one; a cache-full first read still applies them
to the server's value.
- Queued writes are saved as soon as they are queued, and a re-read keeps
entries another tab has not confirmed yet.
- Legacy Xtream ids stored as numeric strings are typed too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Edits made before the first successful user-data read are kept as
before → after and applied to the server's value once it arrives
(applyEdit), so a new device or a stale cache adds a favorite instead of
replacing the server's list.
- Saved unsent writes keep only the newest per entry; an answered older
write no longer retires a newer one still travelling.
- A late failure after sign-out no longer turns unauthorized into offline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Moving a playlist under another server id takes its parental locks along
(they stay in the browser).
- Bare legacy Xtream ids in each copy are typed from that copy's own
snapshots before favorites, history and snapshots are combined, so a
live channel and a movie sharing an id both survive.
- Writes still travelling are saved on page hide too; saved writes and the
cache belong to one server database (cache owner = instanceId).
- A late successful answer no longer turns unauthorized back to online.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- User-data writes the server has not confirmed are kept under
iptvnator-server:unsent-writes and sent first on the next visit.
- Moving browser data does not start while user-data writes are unsent,
and a duplicate of a failed upload counts as failed, not merged.
- The session reaches localStorage through the lazy adapter, so a browser
that denies storage still boots.
- Raise launch/renderer.domMutationsToFirstCard by 2 (569 → 571): the rail
server shortcut and the offline banner each leave an @if anchor in every
build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Moving M3U favorites keeps URL favorites and anything the server cannot
resolve; only older channel ids are translated.
- Queued user-data edits are sent before the move writes combined values.
- A re-read that does not fit the cache keeps the old cache and baseline
together.
- A response body cut off after its headers counts as offline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Before the first user-data read finishes, an edit is compared with the
cache, so it sends only its own playlist entry.
- Queued entries carry their edit number; a failed write is retried only
when nothing newer replaced it.
- Moving M3U favorites and history translates channel ids through the
stream URL (ids are random per import).
- The server-storage E2E runs in Chromium only: one shared database.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Moving Stalker favorites and history keys items by id and category, so
a channel and a movie with one id both survive.
- Writes after a page-hide flush wait for its keepalive requests.
- A probe whose body is cut off counts as unreachable and keeps the
remembered server.
- Settings → Server renders without @defer so settings search can reveal
its rows on the first visit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Moved playback positions are matched by content type and Xtream id and
name the server playlist.
- Page-hide writes report whether the server took them; refused ones go
back into the queue. Re-reads run one at a time.
- The access guard's 401 carries WWW-Authenticate: Bearer
realm="iptvnator"; the interceptor and PwaService's raw fetches turn it
into sign-in, so a token changed on the server is noticed on provider
calls too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A merged playlist gains the browser document's favorites and history;
Xtream values for one server entry are combined (lists unioned, maps
merged, the server winning conflicts) and written once.
- A failed user-data write stays queued and survives a re-read; the
page-hide flush waits for a batch still travelling.
- The start-up screen lets the sign-in route through whatever its check
reports.
- The backend refuses IPTVNATOR_ACCESS_TOKEN without server storage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Moving browser playlists matches them to the server's by id or source and
carries the legacy Xtream maps under the server id; it no longer goes
through the backup format, whose PWA export has no Xtream user data. A
partial move stays on offer.
- User-data batches go out in order, and a re-read keeps entries edited
while it was in flight.
- A keepPayload write keeps its cached copy only when the server replaced
the version its payload came from (new X-Replaced-ETag header).
- 502/503/504 from the proxy count as offline.
- The start-up screen lets the sign-in page through while a token is
required.
- Settings → Server offers a reload after a move.
- The probe detects Electron through RuntimeCapabilitiesService.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Synced Xtream maps live in a write-through cache under the
iptvnator-server: prefix; writes send only the playlist entries that
changed, debounced, and flush with keepalive when the page hides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>