chart downloading

This commit is contained in:
Michael Zhang 2024-05-15 23:37:23 -05:00
parent 8b070ca1dd
commit db6dd41554
15 changed files with 183 additions and 26 deletions

View file

@ -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();

View file

@ -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");
});

View file

@ -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<QueryExecResult[]>;
run(sql: string, params?: BindParams): Promise<Database>;

6
lib/db/constants.ts Normal file
View file

@ -0,0 +1,6 @@
export enum DbStatusCode {
LOADED_SQLITE = "dbStatus/loadedSqlite",
OPENED_DATABASE = "dbStatus/openedDatabase",
RAN_MIGRATIONS = "dbStatus/ranMigrations",
READY = "dbStatus/ready",
}

View file

@ -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;
}

View file

@ -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<void>]
>((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
)
`);
}

View file

@ -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",

View file

@ -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

View file

@ -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 (
<>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
<JotaiProvider store={jotaiStore}>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</JotaiProvider>
</>
);
}

View file

@ -19,4 +19,5 @@
.searchEntry {
font-size: 20pt;
padding: 2px 8px;
}

View file

@ -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;
}
}

View file

@ -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<DbStatus>({
persistence: "unknown",

View file

@ -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(
<StrictMode>
<App />
<Loader />
</StrictMode>,
);

18
src/loader.tsx Normal file
View file

@ -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 <Module />;
}

View file

@ -10,6 +10,7 @@ const baseUrls = {
export default defineConfig(({ mode }) => ({
base: baseUrls[mode],
build: {},
server: {
https: {},
},