mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
fix(playlists): serialize per-playlist collection writes to prevent lost updates (#1255)
* fix(playlists): serialize per-playlist collection writes to prevent lost updates All per-playlist mutations (portal favorites, recently viewed, playlist meta/favorites updates) used an uncoordinated read -> patch -> replace-whole-row pattern, so two overlapping mutations on the same playlist were last-write-wins and silently dropped each other's changes (flagged by Greptile on PR #1253). Chain every read-modify-write through a per-playlist promise queue (Map<playlistId, Promise>) inside defer(), covering both the SQLite upsert and IndexedDB update paths while keeping the Observable-based public API, laziness, and emitted values unchanged. A failed mutation does not wedge the queue, and different playlists are not serialized against each other. Regression coverage: overlapping favorite+recently-viewed adds, two rapid favorite adds, IndexedDB-path overlap (all three fail on the old code), plus queue-continues-after-error and cross-playlist independence guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playlists): close serialization gaps flagged by review bots Greptile P1 (writers bypassing the queue): route updateManyPlaylists and updatePlaylistPositions through the same per-playlist write queue. Auto-refresh batch writes now re-read the stored row inside the queue and preserve user-owned fields (favorites, recently viewed, position) instead of writing a pre-refresh snapshot over them; position updates re-read and patch inside the queue on both storage paths. Codex P1 (callers precompute stale snapshots): add an atomic PlaylistsService.transformPlaylistFavorites(playlistId, transform) that applies the favorites transform to the freshly-read row inside the queue, and convert every read-then-set call site to it: UnifiedFavoritesDataService M3U add/remove/clear/reorder and Stalker reorder/clear, GlobalFavoritesService M3U removal, DashboardDataService M3U removal. Reorders now keep concurrently added favorites (appended after the dragged order) instead of dropping them. New coverage: overlapping favorites transforms, auto-refresh batch write vs queued favorite add, position update vs queued favorite add, and a public addFavorite race through UnifiedFavoritesDataService; existing specs updated to the transform-based contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playlists): share the canonical refresh merge with the auto-refresh batch The batch path previously spread the stale refresh snapshot over the freshly read row and pinned only favorites/recently-viewed/position, so a queued metadata mutation (hiddenGroupTitles, curated EPG sources) finishing before the refresh write could be reverted. Extract updatePlaylist's merge into mergeRefreshedPlaylist() and use it for both the single-playlist update flow and updateManyPlaylists: refresh-owned data (parsed content, count, EPG detection) comes from the payload, user-owned state comes from the current row, and manual/disabled EPG configuration is resolved through resolvePlaylistEpgSourceState instead of being overwritten. Regression test: queued hiddenGroupTitles meta update overlapping an auto-refresh batch write keeps both the metadata change and the refreshed content, including preserved manualEpgUrls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playlists): let the current row decide autoRefresh during batch refresh The batch path force-set autoRefresh: true after the merge, so disabling auto-refresh while a refresh was in flight was reverted by the completing write. Drop the override — mergeRefreshedPlaylist already prefers the current row's autoRefresh over the snapshot — and add a count fallback to the snapshot value for rows without a stored copy. Regression test: disabling auto-refresh concurrently with the batch write keeps autoRefresh false while still applying the refreshed content. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
270350c2e1
commit
6bdd6fd8a8
7 files changed
+1092
-537
No files matched your search
@@ -86,17 +86,11 @@ export class GlobalFavoritesService {
|
||||
async removeFavorite(channel: UnifiedFavoriteChannel): Promise<void> {
|
||||
switch (channel.sourceType) {
|
||||
case 'm3u': {
|
||||
const playlist = await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(channel.playlistId)
|
||||
);
|
||||
const currentFavs = (playlist.favorites as string[]) ?? [];
|
||||
const filtered = currentFavs.filter(
|
||||
(f) => f !== channel.streamUrl
|
||||
);
|
||||
await firstValueFrom(
|
||||
this.playlistsService.setFavorites(
|
||||
this.playlistsService.transformPlaylistFavorites(
|
||||
channel.playlistId,
|
||||
filtered
|
||||
(current) =>
|
||||
current.filter((f) => f !== channel.streamUrl)
|
||||
)
|
||||
);
|
||||
break;
|
||||
@@ -134,9 +128,7 @@ export class GlobalFavoritesService {
|
||||
// Persist xtream positions to DB
|
||||
const xtreamUpdates = channels
|
||||
.filter(
|
||||
(
|
||||
ch
|
||||
): ch is UnifiedFavoriteChannel & { contentId: number } =>
|
||||
(ch): ch is UnifiedFavoriteChannel & { contentId: number } =>
|
||||
ch.sourceType === 'xtream' && ch.contentId != null
|
||||
)
|
||||
.map((ch, index) => ({
|
||||
|
||||
Reference in new issue
Block a user