success pt. 2

This commit is contained in:
2026-02-05 03:26:23 -08:00
parent e403e355ae
commit 8e5f8596b4
2 changed files with 19 additions and 87 deletions

View File

@@ -6,34 +6,22 @@ import { animeTable } from "$lib/db/schema";
import { db, overwriteDatabaseFile } from "./client-db"; import { db, overwriteDatabaseFile } from "./client-db";
/** /**
* Client-side, read-only AMQ database access. * Client-side, read-only AMQ database helper.
* *
* - Seeds SQLocal's managed database file from `static/data/amq.sqlite` * Responsibilities:
* - Uses a version key so you control when clients refresh their local snapshot * - Seed SQLocal's managed DB file from `static/data/amq.sqlite` (served at `/data/amq.sqlite`)
* - Exposes a small query helper to fetch a short anime list for display * - Gate the overwrite behind a manual version key (so you decide when clients refresh)
* - Provide minimal query helpers for browsing
* *
* Notes: * IMPORTANT:
* - This module MUST only be used in the browser. Guard with `browser`, or call * - Call these functions only in the browser (e.g. from `onMount`)
* the exported functions from `onMount`. * - `overwriteDatabaseFile()` resets connections; don't call it repeatedly
* - `overwriteDatabaseFile()` resets connections, so only call it when needed.
*/ */
/** /** Bump when you ship a new `static/data/amq.sqlite`. */
* Bump this when you ship a new `static/data/amq.sqlite`.
* This is intentionally manual so you control when clients refresh.
*/
export const AMQ_DB_SEED_VERSION = 1; export const AMQ_DB_SEED_VERSION = 1;
/** /** Static seed location. `static/data/amq.sqlite` -> `/data/amq.sqlite` */
* The file name SQLocal uses internally (you can change this; it does not need
* to match the static seed file name).
*/
const SQLOCAL_DB_FILE_NAME = "amq.sqlite3";
/**
* Where the shipped, static database lives.
* `static/data/amq.sqlite` -> served at `/data/amq.sqlite`
*/
const SEED_ASSET_PATH = "/data/amq.sqlite"; const SEED_ASSET_PATH = "/data/amq.sqlite";
const seededStorageKey = (version: number) => `amq.sqlocal.seeded.v${version}`; const seededStorageKey = (version: number) => `amq.sqlocal.seeded.v${version}`;
@@ -41,11 +29,8 @@ const seededStorageKey = (version: number) => `amq.sqlocal.seeded.v${version}`;
let initPromise: Promise<void> | null = null; let initPromise: Promise<void> | null = null;
function ensureBrowser() { function ensureBrowser() {
if (!browser) { if (!browser)
throw new Error( throw new Error("Client AMQ DB can only be used in the browser.");
"Client AMQ DB can only be used in the browser (SSR is not supported).",
);
}
} }
/** /**
@@ -149,8 +134,8 @@ export async function getAnimeList(limit = 25): Promise<
} }
/** /**
* Helper: basic text search by `mainName` (and optionally other columns). * Helper: basic text search by `mainName`.
* This is deliberately small and safe for client-side browsing. * (Good enough for POC browsing; consider FTS5 later for better UX/perf.)
*/ */
export async function searchAnimeByName( export async function searchAnimeByName(
query: string, query: string,
@@ -165,13 +150,9 @@ export async function searchAnimeByName(
if (!q) return []; if (!q) return [];
const safeLimit = Math.max(1, Math.min(200, Math.floor(limit))); const safeLimit = Math.max(1, Math.min(200, Math.floor(limit)));
// Simple LIKE search. If you want better search later, consider FTS5 in the shipped snapshot.
// Note: we don't escape %/_ here; if you need literal matching, we'll switch to a raw SQL
// fragment with an explicit ESCAPE clause.
const pattern = `%${q}%`; const pattern = `%${q}%`;
const rows = await db return db
.select({ .select({
annId: animeTable.annId, annId: animeTable.annId,
mainName: animeTable.mainName, mainName: animeTable.mainName,
@@ -186,9 +167,4 @@ export async function searchAnimeByName(
desc(animeTable.annId), desc(animeTable.annId),
) )
.limit(safeLimit); .limit(safeLimit);
return rows;
} }
// Re-export for convenience in components that need to ensure seeding first.
export { db, overwriteDatabaseFile, SQLOCAL_DB_FILE_NAME };

View File

@@ -1,70 +1,26 @@
<script lang="ts"> <script lang="ts">
import { desc } from "drizzle-orm";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { db, overwriteDatabaseFile } from "$lib/db/client-db"; import { getAnimeList } from "$lib/db/client-amq";
import { animeTable } from "$lib/db/schema";
type AnimeListItem = { type AnimeListItem = {
annId: number; annId: number;
mainName: string; mainName: string;
year: number; year: number;
seasonId: number; seasonId: number;
malId: number;
}; };
const SEED_URL = "/data/amq.sqlite";
const SEED_VERSION = "amq.sqlite.v1"; // bump when static/data/amq.sqlite changes
const SEED_KEY = "amq.seed.version";
let status = $state<"idle" | "loading" | "ready" | "error">("idle"); let status = $state<"idle" | "loading" | "ready" | "error">("idle");
let error = $state<string | null>(null); let error = $state<string | null>(null);
let anime = $state<AnimeListItem[]>([]); let anime = $state<AnimeListItem[]>([]);
async function ensureSeeded() {
const current = localStorage.getItem(SEED_KEY);
if (current === SEED_VERSION) return;
const res = await fetch(SEED_URL, { cache: "no-cache" });
if (!res.ok) {
throw new Error(
`Failed to fetch seed DB: ${res.status} ${res.statusText}`,
);
}
const stream = res.body;
if (stream) {
await overwriteDatabaseFile(stream);
} else {
const blob = await res.blob();
await overwriteDatabaseFile(blob);
}
localStorage.setItem(SEED_KEY, SEED_VERSION);
}
onMount(() => { onMount(() => {
(async () => { (async () => {
status = "loading"; status = "loading";
error = null; error = null;
try { try {
await ensureSeeded(); anime = await getAnimeList(20);
const rows = await db
.select({
annId: animeTable.annId,
mainName: animeTable.mainName,
year: animeTable.year,
seasonId: animeTable.seasonId,
})
.from(animeTable)
.orderBy(
desc(animeTable.year),
desc(animeTable.seasonId),
desc(animeTable.annId),
)
.limit(20);
anime = rows;
status = "ready"; status = "ready";
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : String(e); error = e instanceof Error ? e.message : String(e);
@@ -109,7 +65,7 @@
<div class="font-medium">{a.mainName}</div> <div class="font-medium">{a.mainName}</div>
<div class="text-sm text-muted-foreground"> <div class="text-sm text-muted-foreground">
{a.year} {a.year}
{seasonName(a.seasonId)} • ANN {a.annId} {seasonName(a.seasonId)} • ANN {a.annId} • MAL {a.malId}
</div> </div>
</li> </li>
{/each} {/each}