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 type { BindParams, Database, QueryExecResult } from "sql.js";
import { RpcProvider } from "worker-rpc"; import { RpcProvider } from "worker-rpc";
import DbWorker from "./db.worker?worker"; import DbWorker from "./db.worker?worker";
import { dbStatusAtom, jotaiStore } from "../../src/globals";
const worker = new DbWorker(); const worker = new DbWorker();
const rpcProvider = new RpcProvider((message, transfer) => const rpcProvider = new RpcProvider((message, transfer) =>
worker.postMessage(message, transfer), worker.postMessage(message, transfer),
); );
worker.onmessage = (e) => rpcProvider.dispatch(e.data); worker.onmessage = (e) => rpcProvider.dispatch(e.data);
rpcProvider.registerSignalHandler("db", ({ status }) => {
jotaiStore.set(dbStatusAtom, (prevState) => ({ ...prevState, status }));
});
interface AsyncDatabase { interface AsyncDatabase {
exec(sql: string, params?: BindParams): Promise<QueryExecResult[]>; exec(sql: string, params?: BindParams): Promise<QueryExecResult[]>;
run(sql: string, params?: BindParams): Promise<Database>; 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 { MigrateDeploy } from "@prisma/migrate";
import initSqlJs from "@jlongster/sql.js"; import initSqlJs from "@jlongster/sql.js";
import { SQLiteFS } from "absurd-sql"; 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 IndexedDBBackend from "absurd-sql/dist/indexeddb-backend";
import { RpcProvider } from "worker-rpc"; import { RpcProvider } from "worker-rpc";
import { Umzug } from "umzug";
import executeMigrations from "./migrations"; import executeMigrations from "./migrations";
import { DbStatusCode } from "./constants";
async function init() { 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({ const SQL = await initSqlJs({
locateFile: (file) => { locateFile: (file) => {
switch (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()); const sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend());
SQL.register_for_idb(sqlFS); SQL.register_for_idb(sqlFS);
@ -35,19 +42,22 @@ async function init() {
const db: Database = new SQL.Database(path, { filename: true }); const db: Database = new SQL.Database(path, { filename: true });
db.exec("PRAGMA journal_mode=MEMORY;"); db.exec("PRAGMA journal_mode=MEMORY;");
rpcProvider.signal("db", { status: DbStatusCode.OPENED_DATABASE });
await executeMigrations(db); await executeMigrations(db);
const rpcProvider = new RpcProvider((message, transfer) => rpcProvider.signal("db", { status: DbStatusCode.RAN_MIGRATIONS });
self.postMessage(message, undefined, transfer),
);
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("db", { status: DbStatusCode.READY });
rpcProvider.signal("ready");
} }
init(); init();
interface Args {
s: string;
p?: BindParams;
}

View file

@ -1,3 +1,66 @@
import type { Database } from "sql.js"; 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", "@prisma/migrate": "^5.14.0",
"@tanstack/react-query": "^5.36.1", "@tanstack/react-query": "^5.36.1",
"@tanstack/react-table": "^8.17.3", "@tanstack/react-table": "^8.17.3",
"@uidotdev/usehooks": "^2.4.1",
"@zlepper/rpc": "^0.0.10", "@zlepper/rpc": "^0.0.10",
"@zlepper/web-worker-rpc": "^0.0.10", "@zlepper/web-worker-rpc": "^0.0.10",
"absurd-sql": "^0.0.54", "absurd-sql": "^0.0.54",

View file

@ -41,6 +41,9 @@ importers:
'@tanstack/react-table': '@tanstack/react-table':
specifier: ^8.17.3 specifier: ^8.17.3
version: 8.17.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 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': '@zlepper/rpc':
specifier: ^0.0.10 specifier: ^0.0.10
version: 0.0.10 version: 0.0.10
@ -1622,6 +1625,13 @@ packages:
'@types/trusted-types@2.0.7': '@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} 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': '@vite-pwa/assets-generator@0.2.4':
resolution: {integrity: sha512-DXyPLPR/IpbZPSpo1amZEPghY/ziIwpTUKNaz0v1xG+ELzCXmrVQhVzEMqr2JLSqRxjc+UzKfGJA/YdUuaao3w==} resolution: {integrity: sha512-DXyPLPR/IpbZPSpo1amZEPghY/ziIwpTUKNaz0v1xG+ELzCXmrVQhVzEMqr2JLSqRxjc+UzKfGJA/YdUuaao3w==}
engines: {node: '>=16.14.0'} engines: {node: '>=16.14.0'}
@ -4929,6 +4939,11 @@ snapshots:
'@types/trusted-types@2.0.7': {} '@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': '@vite-pwa/assets-generator@0.2.4':
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14

View file

@ -16,10 +16,19 @@ import Scores from "./pages/Scores";
import Settings from "./pages/Settings"; import Settings from "./pages/Settings";
import ImportChartWorker from "./importCharts?worker"; import ImportChartWorker from "./importCharts?worker";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 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 NavBar from "./components/NavBar";
import WarningBars from "./components/WarningBars"; 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"; import { dbClient, getDbClient } from "../lib/db/client";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -77,6 +86,7 @@ export function AppWrapper() {
} }
export default function App() { export default function App() {
// Init code
useEffect(() => { useEffect(() => {
(async () => { (async () => {
await getDbClient(); await getDbClient();
@ -85,9 +95,11 @@ export default function App() {
return ( return (
<> <>
<QueryClientProvider client={queryClient}> <JotaiProvider store={jotaiStore}>
<RouterProvider router={router} /> <QueryClientProvider client={queryClient}>
</QueryClientProvider> <RouterProvider router={router} />
</QueryClientProvider>
</JotaiProvider>
</> </>
); );
} }

View file

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

View file

@ -1,4 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import { usePrevious } from "@uidotdev/usehooks";
import styles from "./WarningBars.module.scss"; import styles from "./WarningBars.module.scss";
import WarningIcon from "@mui/icons-material/Warning"; import WarningIcon from "@mui/icons-material/Warning";
import Chip from "./Chip"; import Chip from "./Chip";
@ -52,10 +53,10 @@ function StatusBar() {
function DbStatusChip(): ReactNode { function DbStatusChip(): ReactNode {
const dbStatus = useAtomValue(dbStatusAtom); const dbStatus = useAtomValue(dbStatusAtom);
const prevStatus = usePrevious(dbStatus);
switch (dbStatus.status) { switch (dbStatus.status) {
case "uninit": default:
return "uninit"; return dbStatus.status;
case "upgrading":
return "upgrading";
} }
} }

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 PROD_DATA_VERSION = 1;
const DEV_DATA_VERSION = Math.floor(new Date().getTime() / 1000) % 1000000; const DEV_DATA_VERSION = Math.floor(new Date().getTime() / 1000) % 1000000;
export const jotaiStore = createStore();
export const APP_DATA_VERSION = export const APP_DATA_VERSION =
import.meta.env.MODE === "production" ? PROD_DATA_VERSION : DEV_DATA_VERSION; import.meta.env.MODE === "production" ? PROD_DATA_VERSION : DEV_DATA_VERSION;
export const CHART_STORE_CREATION_EVENT = new Event("chartStoreCreate"); export const CHART_STORE_CREATION_EVENT = new Event("chartStoreCreate");
export interface DbStatus { export interface DbStatus {
persistence: "unknown" | "persisted" | "unpersisted"; persistence: "unknown" | "persisted" | "unpersisted";
status: "uninit" | "upgrading"; status: "uninit" | DbStatusCode | "upgrading";
} }
export const dbStatusAtom = atom<DbStatus>({ export const dbStatusAtom = atom<DbStatus>({
persistence: "unknown", persistence: "unknown",

View file

@ -1,13 +1,13 @@
import App from "./App";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { StrictMode } from "react"; import { StrictMode } from "react";
import "@fontsource/inter"; import "@fontsource/inter";
import "./global.scss"; import "./global.scss";
import Loader from "./loader";
// biome-ignore lint/style/noNonNullAssertion: don't care // biome-ignore lint/style/noNonNullAssertion: don't care
const el = document.getElementById("root")!; const el = document.getElementById("root")!;
createRoot(el).render( createRoot(el).render(
<StrictMode> <StrictMode>
<App /> <Loader />
</StrictMode>, </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 }) => ({ export default defineConfig(({ mode }) => ({
base: baseUrls[mode], base: baseUrls[mode],
build: {},
server: { server: {
https: {}, https: {},
}, },