Skip to content

Commit 2b15302

Browse files
Erik Soehnelhoeck
Erik Soehnel
authored andcommitted
dispatch hook to update the database
1 parent 392031f commit 2b15302

10 files changed

+168
-28
lines changed

app/postcss.config.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ module.exports = {
1111
},
1212
},
1313
},
14-
};
14+
};

app/src/App.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { AppShell, Group, MantineProvider, rem } from "@mantine/core";
2+
import "@mantine/core/styles.css";
23
import "./App.css";
4+
import { Filter } from "./components/Filter";
35
import { Graph } from "./components/Graph";
46
import { DatabaseContextProvider, useDatabase } from "./database";
57

@@ -42,6 +44,7 @@ function App() {
4244
</AppShell.Header>
4345

4446
<AppShell.Main pt={`calc(${rem(60)} + var(--mantine-spacing-md))`}>
47+
<Filter />
4548
<Graph />
4649
<h1>Results</h1>
4750
<ResultsTable />

app/src/components/Filter.tsx

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Checkbox, Stack } from "@mantine/core";
2+
import { useDatabase, useDatabaseDispatch } from "../database";
3+
4+
function BenchmarksFilter() {
5+
const benchmarks = useDatabase((db) => {
6+
return db.findBenchmarks();
7+
});
8+
const setFilter = useDatabaseDispatch("setBenchmarkSelected");
9+
10+
return (
11+
<Stack gap="xs">
12+
{benchmarks?.map((b) => (
13+
<Checkbox
14+
key={b.id}
15+
label={b.name}
16+
checked={!!b.selected}
17+
onChange={(ev) => {
18+
setFilter(b.id, !ev.currentTarget.checked);
19+
}}
20+
/>
21+
))}
22+
</Stack>
23+
);
24+
}
25+
26+
export function Filter() {
27+
return (
28+
<div>
29+
<BenchmarksFilter />
30+
</div>
31+
);
32+
}

app/src/components/Graph.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ export function Graph() {
111111
const [svg, setSvg] = useState<string | null>(null);
112112

113113
useDatabase(async (db) => {
114+
// delay the (expensive) graph rendering for 1 frame in order to not block
115+
// updating other parts of the ui
116+
await new Promise((resolve) => setTimeout(resolve, 16));
117+
114118
setSvg(
115119
await graph({
116120
colors: COLORS,

app/src/database/DatabaseContext.tsx

+13-8
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,27 @@ export function DatabaseContextProvider(props: PropsWithChildren) {
88

99
useEffect(() => {
1010
if (db) {
11-
return;
11+
return () => {
12+
void db.close();
13+
};
1214
}
1315

1416
Database.create()
15-
.then(async (conn) => {
17+
.then((conn) => {
1618
// fetch results lazily
17-
conn.fetchResults().catch(console.error);
19+
// TODO: move this init code outside of the db contextprovider
20+
conn.fetchResults().catch((e: unknown) => {
21+
console.error(e);
22+
});
1823

1924
setDb(conn);
2025
})
21-
.catch(console.error);
26+
.catch((e: unknown) => {
27+
console.error(e);
28+
});
2229

23-
return () => {
24-
db!?.close();
25-
};
26-
});
30+
return;
31+
}, [db]);
2732

2833
return (
2934
<DatabaseContext.Provider value={db}>

app/src/database/database.ts

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
// Without using runtypes, an orm or query-generator, we need to use any on
2+
// the db results and trust our SQL-skills.
3+
/* eslint-disable @typescript-eslint/no-unsafe-return */
4+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
5+
/* eslint-disable @typescript-eslint/no-explicit-any */
6+
17
import { Database as SqliteDatabase } from "./sqlite";
28
import { schema } from "./schema";
39

@@ -158,11 +164,13 @@ export class Database {
158164
await this._insertResults(data.results);
159165
}
160166

161-
async findBenchmarks(): Promise<{
162-
id: number;
163-
name: string;
164-
selected: 0 | 1;
165-
}> {
167+
async findBenchmarks(): Promise<
168+
{
169+
id: number;
170+
name: string;
171+
selected: 0 | 1;
172+
}[]
173+
> {
166174
return (await this._db.query(
167175
"SELECT id, name, selected FROM benchmarks",
168176
)) as any;
@@ -198,25 +206,27 @@ export class Database {
198206
selected: boolean,
199207
): Promise<void> {
200208
await this._db.query(
201-
"UPDATE benchmarks SET selected = :selected WHERE id = :benchmarkId ORDER BY name ASC",
209+
"UPDATE benchmarks SET selected = :selected WHERE id = :benchmarkId",
202210
{
203211
":benchmarkId": benchmarkId,
204212
":selected": selected ? 0 : 1,
205213
},
206214
);
215+
this._notifyUpdateCallbacks();
207216
}
208217

209218
async setRuntimeSelected(
210219
runtimeId: number,
211220
selected: boolean,
212221
): Promise<void> {
213222
await this._db.query(
214-
"UPDATE runtimes SET selected = :selected WHERE id = :runtimeId ORDER BY name ASC, version ASC",
223+
"UPDATE runtimes SET selected = :selected WHERE id = :runtimeId",
215224
{
216225
":runtimeId": runtimeId,
217226
":selected": selected ? 0 : 1,
218227
},
219228
);
229+
this._notifyUpdateCallbacks();
220230
}
221231

222232
async findResults(): Promise<Result[]> {

app/src/database/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { BENCHMARKS, COLORS, Database, type Result } from "./database";
22
export { DatabaseContextProvider } from "./DatabaseContext";
33
export { useDatabase } from "./useDatabase";
4+
export { useDatabaseDispatch } from "./useDatabaseDispatch";

app/src/database/sqlite.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { sqlite3Worker1Promiser, Promiser } from "@sqlite.org/sqlite-wasm";
22

3+
// sqlite wrapper is only barely typed so I need to use lots of anys
4+
/* eslint-disable */
5+
36
export class Database {
47
static async create(): Promise<Database> {
58
const promiser = await new Promise<Promiser>((resolve) => {
@@ -29,7 +32,7 @@ export class Database {
2932
async query(sqlString: string, params?: any) {
3033
let rows: Record<string, any>[] = [];
3134

32-
return new Promise<typeof rows>((resolve) => {
35+
return new Promise<typeof rows>((resolve, reject) => {
3336
this._promiser("exec", {
3437
sql: sqlString,
3538
bind: params,
@@ -51,6 +54,43 @@ export class Database {
5154
);
5255
}
5356
},
57+
}).catch((err: any) => {
58+
if (
59+
!err ||
60+
typeof err !== "object" ||
61+
err.type !== "error" ||
62+
typeof err.dbId !== "string" ||
63+
typeof err.result?.message !== "string" ||
64+
err.result?.errorClass !== "SQLite3Error"
65+
) {
66+
// not an sqlite error
67+
const wrappedError: any = new Error(
68+
"Error while executing query " +
69+
JSON.stringify(sqlString) +
70+
":" +
71+
err?.message,
72+
);
73+
74+
wrappedError.query = sqlString;
75+
wrappedError.queryParams = params;
76+
wrappedError.sqliteError = err;
77+
78+
reject(wrappedError);
79+
}
80+
81+
// sqlite error
82+
const wrappedError: any = new Error(
83+
"Sqlite Error while executing query " +
84+
JSON.stringify(sqlString) +
85+
":" +
86+
err.result.message,
87+
);
88+
89+
wrappedError.query = sqlString;
90+
wrappedError.queryParams = params;
91+
wrappedError.sqliteError = err;
92+
93+
reject(wrappedError);
5494
});
5595
});
5696
}

app/src/database/useDatabase.ts

+19-11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { useContext, useEffect, useState } from "react";
22
import { DatabaseContext } from "./DatabaseContext";
33
import { Database } from "./database";
44

5+
// TODO: check https://react.dev/reference/react/useSyncExternalStore
6+
// const stuff = useDatabase("fetchStuff")
57
export function useDatabase<T>(
68
callback: (db: Database) => Promise<T>,
79
dependencies?: readonly unknown[],
@@ -22,18 +24,24 @@ export function useDatabase<T>(
2224
};
2325
}, [db]);
2426

25-
useEffect(() => {
26-
if (!db) {
27-
return;
28-
}
27+
useEffect(
28+
() => {
29+
if (!db) {
30+
return;
31+
}
2932

30-
callback(db)
31-
.then((r) => setResult(r))
32-
.catch((err) => {
33-
setResult(null);
34-
console.error(err);
35-
});
36-
}, [db, timestamp, ...(dependencies || [])]);
33+
callback(db)
34+
.then((r) => {
35+
setResult(r);
36+
})
37+
.catch((err: unknown) => {
38+
setResult(null);
39+
console.error(err);
40+
});
41+
},
42+
// eslint-disable-next-line react-hooks/exhaustive-deps
43+
[db, timestamp, ...(dependencies ?? [])],
44+
);
3745

3846
return result;
3947
}
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useContext, useEffect, useState } from "react";
2+
import { DatabaseContext } from "./DatabaseContext";
3+
import { Database } from "./database";
4+
5+
type MethodNames<T> = {
6+
[M in keyof T]: T[M] extends (...args: unknown[]) => unknown ? M : never;
7+
}[keyof T];
8+
9+
export function useDatabaseDispatch<T extends MethodNames<Database>>(
10+
methodName: T,
11+
): (...params: Parameters<Database[T]>) => void {
12+
const [cb, setCb] = useState<{
13+
value: ((...params: Parameters<Database[T]>) => void) | null;
14+
}>({ value: null });
15+
const db = useContext(DatabaseContext);
16+
17+
useEffect(() => {
18+
if (!db) {
19+
return;
20+
}
21+
22+
setCb({
23+
value: (...params) => {
24+
// need to cast to any as typescript cannot infer ...params
25+
// eslint-disable-next-line
26+
return (db[methodName] as any)(...params);
27+
},
28+
});
29+
}, [methodName, db, setCb]);
30+
31+
return (
32+
cb.value ??
33+
(() => {
34+
return;
35+
})
36+
);
37+
}

0 commit comments

Comments
 (0)