Optimize performance: batch DB ops, N+1 fixes, Suspense streaming, and Jotai best practices

- Add composite indexes on userEpisodeWatches and userMovieWatches for hot queries
- Batch episode tracking: wrap season/batch watches in single transaction (~8 queries vs 8*N)
- Fix N+1 patterns in credits, recommendations, and filmography with batch prefetch+insert
- Stream TV season hydration via Suspense instead of blocking page render
- Optimize webhook logs (per-connection LIMIT 10) and system health queries
- Merge genre filter waterfalls into single Promise.all fetch
- Migrate deprecated Jotai loadable() to unwrap()
- Replace isolated createStore()+Provider with useHydrateAtoms on root store
- Remove unnecessary atomWithStorage SSR guards (handled by Jotai internally)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 17:33:27 -05:00
co-authored by Claude Opus 4.6
parent ed825f3a78
commit f36ca0cbf8
23 changed files with 839 additions and 571 deletions
+22 -25
View File
@@ -1,5 +1,5 @@
import { atom } from "jotai";
import { loadable } from "jotai/utils";
import { unwrap } from "jotai/utils";
import {
fetchEpisodeProgress,
fetchUserStatuses,
@@ -35,30 +35,27 @@ const genreResultsAsyncAtom = atom(async (get) => {
return (data.results ?? []) as TitleRowItem[];
});
export const genreResultsLoadable = loadable(genreResultsAsyncAtom);
export const genreResultsAtom = unwrap(genreResultsAsyncAtom);
const genreUserStatusesAsyncAtom = atom(async (get) => {
const genre = get(selectedGenreAtom);
if (genre === null) return null;
const genreResults = await get(genreResultsAsyncAtom);
if (!genreResults || genreResults.length === 0) return {};
return fetchUserStatuses(
genreResults.map((r) => ({ tmdbId: r.tmdbId, type: r.type })),
);
});
interface GenreEnrichments {
statuses: Record<string, TitleStatus>;
progress: Record<string, { watched: number; total: number }>;
}
export const genreUserStatusesLoadable = loadable(genreUserStatusesAsyncAtom);
const genreEpisodeProgressAsyncAtom = atom(async (get) => {
const genre = get(selectedGenreAtom);
if (genre === null) return null;
const genreResults = await get(genreResultsAsyncAtom);
if (!genreResults || genreResults.length === 0) return {};
return fetchEpisodeProgress(
genreResults.map((r) => ({ tmdbId: r.tmdbId, type: r.type })),
);
});
export const genreEpisodeProgressLoadable = loadable(
genreEpisodeProgressAsyncAtom,
const genreEnrichmentsAsyncAtom = atom(
async (get): Promise<GenreEnrichments | null> => {
const genre = get(selectedGenreAtom);
if (genre === null) return null;
const genreResults = await get(genreResultsAsyncAtom);
if (!genreResults || genreResults.length === 0)
return { statuses: {}, progress: {} };
const items = genreResults.map((r) => ({ tmdbId: r.tmdbId, type: r.type }));
const [statuses, progress] = await Promise.all([
fetchUserStatuses(items),
fetchEpisodeProgress(items),
]);
return { statuses, progress };
},
);
export const genreEnrichmentsAtom = unwrap(genreEnrichmentsAsyncAtom);
+3 -3
View File
@@ -1,5 +1,5 @@
import { atom } from "jotai";
import { loadable } from "jotai/utils";
import { unwrap } from "jotai/utils";
import type { HistoryBucket, TimePeriod } from "@/lib/services/discovery";
export const moviePeriodAtom = atom<TimePeriod>("this_month");
@@ -25,5 +25,5 @@ const episodeStatsAsyncAtom = atom(async (get) => {
return fetchStats("episodes", period);
});
export const movieStatsLoadable = loadable(movieStatsAsyncAtom);
export const episodeStatsLoadable = loadable(episodeStatsAsyncAtom);
export const movieStatsAtom = unwrap(movieStatsAsyncAtom);
export const episodeStatsAtom = unwrap(episodeStatsAsyncAtom);