success pt. 2
This commit is contained in:
@@ -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 };
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user