chart downloading
This commit is contained in:
parent
8b070ca1dd
commit
db6dd41554
15 changed files with 183 additions and 26 deletions
10
lib/chartDownloader/chartDownloader.worker.ts
Normal file
10
lib/chartDownloader/chartDownloader.worker.ts
Normal 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();
|
13
lib/chartDownloader/client.ts
Normal file
13
lib/chartDownloader/client.ts
Normal 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");
|
||||
});
|
|
@ -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
6
lib/db/constants.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export enum DbStatusCode {
|
||||
LOADED_SQLITE = "dbStatus/loadedSqlite",
|
||||
OPENED_DATABASE = "dbStatus/openedDatabase",
|
||||
RAN_MIGRATIONS = "dbStatus/ranMigrations",
|
||||
READY = "dbStatus/ready",
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
16
src/App.tsx
16
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 (
|
||||
<>
|
||||
<JotaiProvider store={jotaiStore}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</JotaiProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,4 +19,5 @@
|
|||
|
||||
.searchEntry {
|
||||
font-size: 20pt;
|
||||
padding: 2px 8px;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
18
src/loader.tsx
Normal 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 />;
|
||||
}
|
|
@ -10,6 +10,7 @@ const baseUrls = {
|
|||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
base: baseUrls[mode],
|
||||
build: {},
|
||||
server: {
|
||||
https: {},
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue