mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
perf(recents): batch Xtream remove-recent-item into a single IPC call
Clearing all recents of a given content type fired N concurrent IPC calls, each opening its own implicit transaction in the recently_viewed table. For users with hundreds of recently-watched VOD/series rows this added real overhead even though the UI updates optimistically. Add a new DB_REMOVE_RECENT_ITEMS_BATCH path end-to-end: - removeRecentItemsBatch() Drizzle op: one transaction, one prepared statement reused per row - Wire through worker → IPC handler → preload → window.electron typings → DatabaseService - UnifiedRecentDataService.removeRecentItemsBatch() groups items by source. Xtream items go through the new batch IPC. M3U/Stalker items still go per-playlist because they update a JSON column on the playlist row, not the recently_viewed table — but they now run in parallel with the Xtream batch via a single Promise.all. - Single call site updated: unified-collection-page "Clear all of type" confirmation handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 4bfb62f988b7
This commit is contained in:
1 parent
21fe51e73b
commit
39ddaa2f9c
12 files changed
+111
-5
No files matched your search
@@ -540,6 +540,9 @@ const electronApi = {
|
||||
ipcRenderer.invoke('DB_CLEAR_PLAYLIST_RECENT_ITEMS', playlistId),
|
||||
dbRemoveRecentItem: (contentId: number, playlistId: string) =>
|
||||
ipcRenderer.invoke('DB_REMOVE_RECENT_ITEM', contentId, playlistId),
|
||||
dbRemoveRecentItemsBatch: (
|
||||
items: { contentId: number; playlistId: string }[]
|
||||
) => ipcRenderer.invoke('DB_REMOVE_RECENT_ITEMS_BATCH', items),
|
||||
dbGetContentByXtreamId: (
|
||||
xtreamId: number,
|
||||
playlistId: string,
|
||||
|
||||
@@ -150,3 +150,36 @@ export async function removeRecentItem(
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function removeRecentItemsBatch(
|
||||
db: AppDatabase,
|
||||
items: { contentId: number; playlistId: string }[]
|
||||
): Promise<{ success: boolean; count: number }> {
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return { success: true, count: 0 };
|
||||
}
|
||||
|
||||
const stmt = db
|
||||
.delete(schema.recentlyViewed)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
schema.recentlyViewed.contentId,
|
||||
sql.placeholder('contentId')
|
||||
),
|
||||
eq(
|
||||
schema.recentlyViewed.playlistId,
|
||||
sql.placeholder('playlistId')
|
||||
)
|
||||
)
|
||||
)
|
||||
.prepare();
|
||||
|
||||
await db.transaction(async () => {
|
||||
for (const { contentId, playlistId } of items) {
|
||||
await stmt.execute({ contentId, playlistId });
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, count: items.length };
|
||||
}
|
||||
@@ -35,3 +35,10 @@ handleWorkerRequest(
|
||||
playlistId,
|
||||
})
|
||||
);
|
||||
|
||||
handleWorkerRequest(
|
||||
'DB_REMOVE_RECENT_ITEMS_BATCH',
|
||||
(items: { contentId: number; playlistId: string }[]) => ({
|
||||
items,
|
||||
})
|
||||
);
|
||||
@@ -39,6 +39,7 @@ export const DB_WORKER_OPERATIONS = [
|
||||
'DB_ADD_RECENT_ITEM',
|
||||
'DB_CLEAR_PLAYLIST_RECENT_ITEMS',
|
||||
'DB_REMOVE_RECENT_ITEM',
|
||||
'DB_REMOVE_RECENT_ITEMS_BATCH',
|
||||
'DB_SAVE_PLAYBACK_POSITION',
|
||||
'DB_GET_PLAYBACK_POSITION',
|
||||
'DB_GET_SERIES_PLAYBACK_POSITIONS',
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
getRecentItems,
|
||||
getRecentlyViewed,
|
||||
removeRecentItem,
|
||||
removeRecentItemsBatch,
|
||||
} from '../database/operations/recently-viewed.operations';
|
||||
import {
|
||||
deleteXtreamContent,
|
||||
@@ -761,6 +762,13 @@ async function executeRequest(message: DbWorkerRequestMessage) {
|
||||
return removeRecentItem(db, payload.contentId, payload.playlistId);
|
||||
}
|
||||
|
||||
case 'DB_REMOVE_RECENT_ITEMS_BATCH': {
|
||||
const payload = message.payload as {
|
||||
items: { contentId: number; playlistId: string }[];
|
||||
};
|
||||
return removeRecentItemsBatch(db, payload.items);
|
||||
}
|
||||
|
||||
case 'DB_SAVE_PLAYBACK_POSITION': {
|
||||
const payload = message.payload as {
|
||||
playlistId: string;
|
||||
|
||||
Vendored
+3
@@ -273,6 +273,9 @@ declare global {
|
||||
contentId: number,
|
||||
playlistId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbRemoveRecentItemsBatch: (
|
||||
items: { contentId: number; playlistId: string }[]
|
||||
) => Promise<{ success: boolean; count: number }>;
|
||||
dbGetContentByXtreamId: (
|
||||
xtreamId: number,
|
||||
playlistId: string,
|
||||
|
||||
Vendored
+3
@@ -266,6 +266,9 @@ declare global {
|
||||
contentId: number,
|
||||
playlistId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbRemoveRecentItemsBatch: (
|
||||
items: { contentId: number; playlistId: string }[]
|
||||
) => Promise<{ success: boolean; count: number }>;
|
||||
dbGetContentByXtreamId: (
|
||||
xtreamId: number,
|
||||
playlistId: string,
|
||||
|
||||
+1
@@ -132,6 +132,7 @@ describe('UnifiedCollectionPageComponent', () => {
|
||||
const recentData = {
|
||||
getRecentItems: jest.fn().mockResolvedValue([]),
|
||||
removeRecentItem: jest.fn(),
|
||||
removeRecentItemsBatch: jest.fn(),
|
||||
clearRecentItems: jest.fn(),
|
||||
};
|
||||
const dialogService = {
|
||||
|
||||
+1
-5
@@ -556,11 +556,7 @@ export class UnifiedCollectionPageComponent implements AfterContentInit {
|
||||
if (remaining.length > 0) {
|
||||
this.selectedContentType.set(remaining[0]);
|
||||
}
|
||||
void Promise.all(
|
||||
itemsToRemove.map((item) =>
|
||||
this.recentData.removeRecentItem(item)
|
||||
)
|
||||
);
|
||||
void this.recentData.removeRecentItemsBatch(itemsToRemove);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ describe('UnifiedRecentDataService', () => {
|
||||
getGlobalRecentlyViewed: jest.Mock;
|
||||
getRecentItems: jest.Mock;
|
||||
removeRecentItem: jest.Mock;
|
||||
removeRecentItemsBatch: jest.Mock;
|
||||
clearPlaylistRecentItems: jest.Mock;
|
||||
clearGlobalRecentlyViewed: jest.Mock;
|
||||
addRecentItem: jest.Mock;
|
||||
@@ -119,6 +120,7 @@ describe('UnifiedRecentDataService', () => {
|
||||
getGlobalRecentlyViewed: jest.fn().mockResolvedValue([]),
|
||||
getRecentItems: jest.fn().mockResolvedValue([]),
|
||||
removeRecentItem: jest.fn().mockResolvedValue(true),
|
||||
removeRecentItemsBatch: jest.fn().mockResolvedValue(true),
|
||||
clearPlaylistRecentItems: jest.fn().mockResolvedValue(true),
|
||||
clearGlobalRecentlyViewed: jest.fn().mockResolvedValue(undefined),
|
||||
addRecentItem: jest.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -81,6 +81,40 @@ export class UnifiedRecentDataService {
|
||||
this.dispatchPlaylistRecentUpdate(item.playlistId, updatedPlaylist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk remove. Xtream items are batched into a single IPC call;
|
||||
* m3u/stalker items still go per-playlist because they update a JSON
|
||||
* column on the playlist row (not the recently_viewed SQL table).
|
||||
*/
|
||||
async removeRecentItemsBatch(
|
||||
items: UnifiedCollectionItem[]
|
||||
): Promise<void> {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const xtreamBatch: { contentId: number; playlistId: string }[] = [];
|
||||
const nonXtreamItems: UnifiedCollectionItem[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.sourceType === 'xtream' && item.contentId != null) {
|
||||
xtreamBatch.push({
|
||||
contentId: item.contentId,
|
||||
playlistId: item.playlistId,
|
||||
});
|
||||
} else if (item.sourceType !== 'xtream') {
|
||||
nonXtreamItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
xtreamBatch.length > 0
|
||||
? this.dbService.removeRecentItemsBatch(xtreamBatch)
|
||||
: Promise.resolve(),
|
||||
...nonXtreamItems.map((item) => this.removeRecentItem(item)),
|
||||
]);
|
||||
}
|
||||
|
||||
async clearRecentItems(
|
||||
scope: CollectionScope,
|
||||
playlistId?: string
|
||||
|
||||
@@ -838,6 +838,21 @@ export class DatabaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async removeRecentItemsBatch(
|
||||
items: { contentId: number; playlistId: string }[]
|
||||
): Promise<boolean> {
|
||||
if (items.length === 0) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
await window.electron.dbRemoveRecentItemsBatch(items);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error removing recent items batch:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content by xtream ID
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user