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 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
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 { 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;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
16
src/App.tsx
16
src/App.tsx
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
<JotaiProvider store={jotaiStore}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</JotaiProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,4 +19,5 @@
|
||||||
|
|
||||||
.searchEntry {
|
.searchEntry {
|
||||||
font-size: 20pt;
|
font-size: 20pt;
|
||||||
|
padding: 2px 8px;
|
||||||
}
|
}
|
|
@ -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";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
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 }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
base: baseUrls[mode],
|
base: baseUrls[mode],
|
||||||
|
build: {},
|
||||||
server: {
|
server: {
|
||||||
https: {},
|
https: {},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue