From db6dd415549dbb65d0a9c86b05b7d011c0d50faa Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Wed, 15 May 2024 23:37:23 -0500 Subject: [PATCH] chart downloading --- lib/chartDownloader/chartDownloader.worker.ts | 10 +++ lib/chartDownloader/client.ts | 13 ++++ lib/db/client.ts | 7 +- lib/db/constants.ts | 6 ++ lib/db/db.worker.ts | 30 ++++++--- lib/db/migrations.ts | 65 ++++++++++++++++++- package.json | 1 + pnpm-lock.yaml | 15 +++++ src/App.tsx | 22 +++++-- src/components/NavBar.module.scss | 1 + src/components/WarningBars.tsx | 9 +-- src/globals.ts | 7 +- src/index.tsx | 4 +- src/loader.tsx | 18 +++++ vite.config.ts | 1 + 15 files changed, 183 insertions(+), 26 deletions(-) create mode 100644 lib/chartDownloader/chartDownloader.worker.ts create mode 100644 lib/chartDownloader/client.ts create mode 100644 lib/db/constants.ts create mode 100644 src/loader.tsx diff --git a/lib/chartDownloader/chartDownloader.worker.ts b/lib/chartDownloader/chartDownloader.worker.ts new file mode 100644 index 0000000..d49871a --- /dev/null +++ b/lib/chartDownloader/chartDownloader.worker.ts @@ -0,0 +1,10 @@ +import { RpcProvider } from "worker-rpc"; + +async function init() { + const rpcProvider = new RpcProvider((message, transfer) => + self.postMessage(message, undefined, transfer), + ); + self.addEventListener("message", (evt) => rpcProvider.dispatch(evt.data)); +} + +init(); diff --git a/lib/chartDownloader/client.ts b/lib/chartDownloader/client.ts new file mode 100644 index 0000000..ea4aaee --- /dev/null +++ b/lib/chartDownloader/client.ts @@ -0,0 +1,13 @@ +import { RpcProvider } from "worker-rpc"; +import ChartDownloaderWorker from "./chartDownloader.worker?worker"; + +const worker = new ChartDownloaderWorker(); +const rpcProvider = new RpcProvider((message, transfer) => + worker.postMessage(message, transfer), +); +worker.onmessage = (e) => rpcProvider.dispatch(e.data); + +export const chartDownloaderEvent = new Event("startDownloadingCharts"); +document.addEventListener("startDownloadingCharts", () => { + console.log("SHIET"); +}); diff --git a/lib/db/client.ts b/lib/db/client.ts index 55d1ed7..75145c4 100644 --- a/lib/db/client.ts +++ b/lib/db/client.ts @@ -1,15 +1,18 @@ import type { BindParams, Database, QueryExecResult } from "sql.js"; import { RpcProvider } from "worker-rpc"; import DbWorker from "./db.worker?worker"; +import { dbStatusAtom, jotaiStore } from "../../src/globals"; const worker = new DbWorker(); - const rpcProvider = new RpcProvider((message, transfer) => worker.postMessage(message, transfer), ); - worker.onmessage = (e) => rpcProvider.dispatch(e.data); +rpcProvider.registerSignalHandler("db", ({ status }) => { + jotaiStore.set(dbStatusAtom, (prevState) => ({ ...prevState, status })); +}); + interface AsyncDatabase { exec(sql: string, params?: BindParams): Promise; run(sql: string, params?: BindParams): Promise; diff --git a/lib/db/constants.ts b/lib/db/constants.ts new file mode 100644 index 0000000..89b6de9 --- /dev/null +++ b/lib/db/constants.ts @@ -0,0 +1,6 @@ +export enum DbStatusCode { + LOADED_SQLITE = "dbStatus/loadedSqlite", + OPENED_DATABASE = "dbStatus/openedDatabase", + RAN_MIGRATIONS = "dbStatus/ranMigrations", + READY = "dbStatus/ready", +} diff --git a/lib/db/db.worker.ts b/lib/db/db.worker.ts index 76de4f9..c833f7d 100644 --- a/lib/db/db.worker.ts +++ b/lib/db/db.worker.ts @@ -2,13 +2,18 @@ import sqlWasmUrl from "@jlongster/sql.js/dist/sql-wasm.wasm?url"; import { MigrateDeploy } from "@prisma/migrate"; import initSqlJs from "@jlongster/sql.js"; import { SQLiteFS } from "absurd-sql"; -import type { Database } from "sql.js"; +import type { BindParams, Database } from "sql.js"; import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend"; import { RpcProvider } from "worker-rpc"; -import { Umzug } from "umzug"; import executeMigrations from "./migrations"; +import { DbStatusCode } from "./constants"; async function init() { + const rpcProvider = new RpcProvider((message, transfer) => + self.postMessage(message, undefined, transfer), + ); + self.addEventListener("message", (evt) => rpcProvider.dispatch(evt.data)); + const SQL = await initSqlJs({ locateFile: (file) => { switch (file) { @@ -19,6 +24,8 @@ async function init() { }, }); + rpcProvider.signal("db", { status: DbStatusCode.LOADED_SQLITE }); + const sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend()); SQL.register_for_idb(sqlFS); @@ -35,19 +42,22 @@ async function init() { const db: Database = new SQL.Database(path, { filename: true }); db.exec("PRAGMA journal_mode=MEMORY;"); + rpcProvider.signal("db", { status: DbStatusCode.OPENED_DATABASE }); + await executeMigrations(db); - const rpcProvider = new RpcProvider((message, transfer) => - self.postMessage(message, undefined, transfer), - ); + rpcProvider.signal("db", { status: DbStatusCode.RAN_MIGRATIONS }); - self.addEventListener("message", (evt) => rpcProvider.dispatch(evt.data)); + rpcProvider.registerRpcHandler("run", ({ s, p }: Args) => db.run(s, p)); - rpcProvider.registerRpcHandler("run", ({ s, p }) => db.run(s, p)); + rpcProvider.registerRpcHandler("exec", ({ s, p }: Args) => db.exec(s, p)); - rpcProvider.registerRpcHandler("exec", ({ s, p }) => db.exec(s, p)); - - rpcProvider.signal("ready"); + rpcProvider.signal("db", { status: DbStatusCode.READY }); } init(); + +interface Args { + s: string; + p?: BindParams; +} diff --git a/lib/db/migrations.ts b/lib/db/migrations.ts index b0b6362..ff44cc8 100644 --- a/lib/db/migrations.ts +++ b/lib/db/migrations.ts @@ -1,3 +1,66 @@ import type { Database } from "sql.js"; -export default async function executeMigrations(db: Database) {} +const migrations = [m01_initial]; + +export default async function executeMigrations(db: Database) { + // Check last migration status + const migrationsWithNames = migrations.map< + [string, (_: Database) => Promise] + >((func) => [func.name, func]); + let startMigrationAt = 0; + + try { + const rows = await db.exec("SELECT name FROM _migrations LIMIT 1"); + if (rows.length < 1) throw new Error("wtf"); + const migrationStatus = rows?.[0]; + const lastMigrationName = migrationStatus.values[0][0]; + console.log("status", migrationStatus); + if (lastMigrationName) { + const foundIndex = migrationsWithNames.findIndex( + ([name]) => name === migrationStatus.values[0][0], + ); + if (foundIndex >= 0) startMigrationAt = foundIndex + 1; + } + } catch (e) { + // Don't have the table + await db.run("BEGIN TRANSACTION"); + await db.run(` + CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY); + INSERT INTO _migrations (name) VALUES (NULL); + + CREATE TABLE IF NOT EXISTS _appDataVersion (version INTEGER PRIMARY KEY); + INSERT INTO _appDataVersion (version) VALUES (NULL); + `); + await db.exec("COMMIT TRANSACTION"); + console.log("Created table."); + } + + console.log(migrationsWithNames); + console.log(startMigrationAt); + + const migrationsToRun = migrationsWithNames.slice(startMigrationAt); + console.log(`Running ${migrationsToRun.length} migrations...`); + for (const [name, migration] of migrationsToRun) { + console.log("Running migration", name); + await db.exec("BEGIN TRANSACTION"); + await migration(db); + await db.exec("UPDATE _migrations SET name = $name", { $name: name }); + await db.exec("COMMIT TRANSACTION"); + } +} + +async function m01_initial(db: Database) { + db.exec(` + CREATE TABLE charts ( + chart_id INTEGER PRIMARY KEY AUTOINCREMENT + ); + + CREATE TABLE scores ( + score_id INTEGER PRIMARY KEY AUTOINCREMENT + ); + + CREATE TABLE banners ( + hash TEXT PRIMARY KEY + ) + `); +} diff --git a/package.json b/package.json index e331c26..a346385 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@prisma/migrate": "^5.14.0", "@tanstack/react-query": "^5.36.1", "@tanstack/react-table": "^8.17.3", + "@uidotdev/usehooks": "^2.4.1", "@zlepper/rpc": "^0.0.10", "@zlepper/web-worker-rpc": "^0.0.10", "absurd-sql": "^0.0.54", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c598853..a62c549 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@tanstack/react-table': specifier: ^8.17.3 version: 8.17.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@uidotdev/usehooks': + specifier: ^2.4.1 + version: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@zlepper/rpc': specifier: ^0.0.10 version: 0.0.10 @@ -1622,6 +1625,13 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@uidotdev/usehooks@2.4.1': + resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==} + engines: {node: '>=16'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + '@vite-pwa/assets-generator@0.2.4': resolution: {integrity: sha512-DXyPLPR/IpbZPSpo1amZEPghY/ziIwpTUKNaz0v1xG+ELzCXmrVQhVzEMqr2JLSqRxjc+UzKfGJA/YdUuaao3w==} engines: {node: '>=16.14.0'} @@ -4929,6 +4939,11 @@ snapshots: '@types/trusted-types@2.0.7': {} + '@uidotdev/usehooks@2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@vite-pwa/assets-generator@0.2.4': dependencies: cac: 6.7.14 diff --git a/src/App.tsx b/src/App.tsx index d65ffed..b444c90 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,10 +16,19 @@ import Scores from "./pages/Scores"; import Settings from "./pages/Settings"; import ImportChartWorker from "./importCharts?worker"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { CHART_STORE_CREATION_EVENT, dbStatusAtom } from "./globals"; +import { + CHART_STORE_CREATION_EVENT, + dbStatusAtom, + jotaiStore, +} from "./globals"; import NavBar from "./components/NavBar"; import WarningBars from "./components/WarningBars"; -import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import { + useAtom, + useAtomValue, + useSetAtom, + Provider as JotaiProvider, +} from "jotai"; import { dbClient, getDbClient } from "../lib/db/client"; const queryClient = new QueryClient(); @@ -77,6 +86,7 @@ export function AppWrapper() { } export default function App() { + // Init code useEffect(() => { (async () => { await getDbClient(); @@ -85,9 +95,11 @@ export default function App() { return ( <> - - - + + + + + ); } diff --git a/src/components/NavBar.module.scss b/src/components/NavBar.module.scss index 19aed2c..7fc6e7c 100644 --- a/src/components/NavBar.module.scss +++ b/src/components/NavBar.module.scss @@ -19,4 +19,5 @@ .searchEntry { font-size: 20pt; + padding: 2px 8px; } \ No newline at end of file diff --git a/src/components/WarningBars.tsx b/src/components/WarningBars.tsx index 194b8fd..514b078 100644 --- a/src/components/WarningBars.tsx +++ b/src/components/WarningBars.tsx @@ -1,4 +1,5 @@ import classNames from "classnames"; +import { usePrevious } from "@uidotdev/usehooks"; import styles from "./WarningBars.module.scss"; import WarningIcon from "@mui/icons-material/Warning"; import Chip from "./Chip"; @@ -52,10 +53,10 @@ function StatusBar() { function DbStatusChip(): ReactNode { const dbStatus = useAtomValue(dbStatusAtom); + const prevStatus = usePrevious(dbStatus); + switch (dbStatus.status) { - case "uninit": - return "uninit"; - case "upgrading": - return "upgrading"; + default: + return dbStatus.status; } } diff --git a/src/globals.ts b/src/globals.ts index 3c597e6..116a42f 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -1,15 +1,18 @@ -import { atom } from "jotai"; +import { atom, createStore } from "jotai"; +import type { DbStatusCode } from "../lib/db/constants"; const PROD_DATA_VERSION = 1; const DEV_DATA_VERSION = Math.floor(new Date().getTime() / 1000) % 1000000; +export const jotaiStore = createStore(); + export const APP_DATA_VERSION = import.meta.env.MODE === "production" ? PROD_DATA_VERSION : DEV_DATA_VERSION; export const CHART_STORE_CREATION_EVENT = new Event("chartStoreCreate"); export interface DbStatus { persistence: "unknown" | "persisted" | "unpersisted"; - status: "uninit" | "upgrading"; + status: "uninit" | DbStatusCode | "upgrading"; } export const dbStatusAtom = atom({ persistence: "unknown", diff --git a/src/index.tsx b/src/index.tsx index 601dcdb..a360aa6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,13 @@ -import App from "./App"; import { createRoot } from "react-dom/client"; import { StrictMode } from "react"; import "@fontsource/inter"; import "./global.scss"; +import Loader from "./loader"; // biome-ignore lint/style/noNonNullAssertion: don't care const el = document.getElementById("root")!; createRoot(el).render( - + , ); diff --git a/src/loader.tsx b/src/loader.tsx new file mode 100644 index 0000000..bcbe953 --- /dev/null +++ b/src/loader.tsx @@ -0,0 +1,18 @@ +import { useEffect, useState } from "react"; + +export default function Loader() { + const [Module, setModule] = useState(null); + + useEffect(() => { + (async () => { + const result = await import("./App"); + setModule(() => result.default); + })(); + }, []); + + if (Module === null) { + return <>Loading...; + } + + return ; +} diff --git a/vite.config.ts b/vite.config.ts index 2bb70e1..6fdcad3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,6 +10,7 @@ const baseUrls = { export default defineConfig(({ mode }) => ({ base: baseUrls[mode], + build: {}, server: { https: {}, },