feat(web): add route loaders to prefetch dashboard, explore, and settings queries

This commit is contained in:
2026-03-18 22:04:19 -04:00
parent 9db4285bc9
commit d94ec16fae
17 changed files with 287 additions and 204 deletions
+2 -3
View File
@@ -4,9 +4,8 @@ import { mkdir, readdir } from "node:fs/promises";
import path from "node:path";
import { BACKUP_DIR, DATABASE_URL } from "@sofa/config";
import { closeDatabase } from "@sofa/db/client";
import { closeDatabase, vacuumDatabase } from "@sofa/db/client";
import { runMigrations } from "@sofa/db/migrate";
import { vacuumInto } from "@sofa/db/utils";
import { createLogger } from "@sofa/logger";
function formatTimestamp(date: Date): string {
@@ -153,7 +152,7 @@ async function createBackupInternal(prefix: BackupPrefix): Promise<BackupInfo> {
const filename = `${prefix}-${timestamp}.db`;
const dest = path.join(BACKUP_DIR, filename);
vacuumInto(dest);
vacuumDatabase(dest);
const s = await Bun.file(dest).stat();
log.info(`Created backup: ${filename} (${s.size} bytes)`);
-1
View File
@@ -8,7 +8,6 @@
"./migrate": "./src/migrate.ts",
"./schema": "./src/schema.ts",
"./test-utils": "./src/test-utils.ts",
"./utils": "./src/utils.ts",
"./queries/*": "./src/queries/*.ts"
},
"scripts": {
+13
View File
@@ -38,6 +38,10 @@ function getClient() {
globalForDb._client.run("PRAGMA journal_mode = WAL");
globalForDb._client.run("PRAGMA foreign_keys = ON");
globalForDb._client.run("PRAGMA busy_timeout = 5000");
globalForDb._client.run("PRAGMA synchronous = NORMAL");
globalForDb._client.run("PRAGMA cache_size = -64000");
globalForDb._client.run("PRAGMA temp_store = MEMORY");
globalForDb._client.run("PRAGMA mmap_size = 268435456");
}
return globalForDb._client;
}
@@ -59,6 +63,15 @@ export const db = new Proxy({} as ReturnType<typeof drizzle>, {
},
});
/** Run PRAGMA optimize to refresh query planner statistics. */
export function optimizeDatabase() {
getClient().run("PRAGMA optimize");
}
export function vacuumDatabase(into: string): void {
getClient().run("VACUUM INTO ?", [into.replace(/'/g, "''")]);
}
/** Close the current connection, and clear singletons so the Proxy re-initializes on next access. */
export function closeDatabase() {
globalForDb._client?.close();
+13 -9
View File
@@ -170,14 +170,7 @@ export function getLibraryFeed(userId: string, page = 1, limit = 20) {
eq(userTitleStatus.userId, userId),
);
const [{ count }] = db
.select({ count: sql<number>`count(*)` })
.from(titles)
.innerJoin(userTitleStatus, joinCondition)
.where(availabilityFilter)
.all();
const items = db
const rows = db
.select({
titleId: titles.id,
title: titles.title,
@@ -190,6 +183,7 @@ export function getLibraryFeed(userId: string, page = 1, limit = 20) {
voteAverage: titles.voteAverage,
popularity: titles.popularity,
userStatus: userTitleStatus.status,
totalCount: sql<number>`count(*) over()`.as("totalCount"),
})
.from(titles)
.innerJoin(userTitleStatus, joinCondition)
@@ -199,7 +193,17 @@ export function getLibraryFeed(userId: string, page = 1, limit = 20) {
.offset(offset)
.all();
const totalResults = count ?? 0;
let totalResults = rows[0]?.totalCount ?? 0;
if (rows.length === 0 && offset > 0) {
const [{ count }] = db
.select({ count: sql<number>`count(*)` })
.from(titles)
.innerJoin(userTitleStatus, joinCondition)
.where(availabilityFilter)
.all();
totalResults = count ?? 0;
}
const items = rows.map(({ totalCount: _, ...item }) => item);
return {
items,
page,
+21 -36
View File
@@ -275,49 +275,34 @@ export function getEpisodeProgressByTitleIds(userId: string, titleIds: string[])
}
export function getUserTitleInfo(userId: string, titleId: string) {
const status = db
.select()
const info = db
.select({
status: userTitleStatus.status,
ratingStars: userRatings.ratingStars,
})
.from(userTitleStatus)
.leftJoin(
userRatings,
and(
eq(userRatings.userId, userTitleStatus.userId),
eq(userRatings.titleId, userTitleStatus.titleId),
),
)
.where(and(eq(userTitleStatus.userId, userId), eq(userTitleStatus.titleId, titleId)))
.get();
const rating = db
.select()
.from(userRatings)
.where(and(eq(userRatings.userId, userId), eq(userRatings.titleId, titleId)))
.get();
const allEps = db
.select({ id: episodes.id })
.from(episodes)
const watchedEpisodeIds = db
.selectDistinct({ episodeId: userEpisodeWatches.episodeId })
.from(userEpisodeWatches)
.innerJoin(episodes, eq(userEpisodeWatches.episodeId, episodes.id))
.innerJoin(seasons, eq(episodes.seasonId, seasons.id))
.where(eq(seasons.titleId, titleId))
.all();
const epIds = allEps.map((ep) => ep.id);
const watchedEpisodeIds =
epIds.length > 0
? Array.from(
new Set(
db
.select({ episodeId: userEpisodeWatches.episodeId })
.from(userEpisodeWatches)
.where(
and(
eq(userEpisodeWatches.userId, userId),
inArray(userEpisodeWatches.episodeId, epIds),
),
)
.all()
.map((w) => w.episodeId),
),
)
: [];
.where(and(eq(userEpisodeWatches.userId, userId), eq(seasons.titleId, titleId)))
.all()
.map((w) => w.episodeId);
return {
status: status?.status ?? null,
rating: rating?.ratingStars ?? null,
status: info?.status ?? null,
rating: info?.ratingStars ?? null,
episodeWatches: watchedEpisodeIds,
};
}
-7
View File
@@ -1,7 +0,0 @@
import { sql } from "drizzle-orm";
import { db } from "./client";
export function vacuumInto(destPath: string): void {
db.run(sql.raw(`VACUUM INTO '${destPath.replace(/'/g, "''")}'`));
}