feat: cache stats sources

This commit is contained in:
Samuel 2024-12-18 19:03:44 +01:00
parent d87d9fb301
commit b97fa88893
No known key found for this signature in database
14 changed files with 98 additions and 92 deletions

View file

@ -1,16 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
<title>Solid App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script> <head>
</body> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!-- <link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" /> -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html> </html>

View file

@ -47,6 +47,7 @@
"kysely": "^0.27.5", "kysely": "^0.27.5",
"kysely-wasm": "^0.7.0", "kysely-wasm": "^0.7.0",
"lucide-solid": "^0.468.0", "lucide-solid": "^0.468.0",
"seroval": "^1.1.1",
"solid-js": "^1.9.3", "solid-js": "^1.9.3",
"sql.js": "^1.12.0", "sql.js": "^1.12.0",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",

3
pnpm-lock.yaml generated
View file

@ -56,6 +56,9 @@ importers:
lucide-solid: lucide-solid:
specifier: ^0.468.0 specifier: ^0.468.0
version: 0.468.0(solid-js@1.9.3) version: 0.468.0(solid-js@1.9.3)
seroval:
specifier: ^1.1.1
version: 1.1.1
solid-js: solid-js:
specifier: ^1.9.3 specifier: ^1.9.3
version: 1.9.3 version: 1.9.3

View file

@ -1,4 +1,5 @@
import { createRoot, on, createDeferred } from "solid-js"; import { createRoot, on, createDeferred } from "solid-js";
import { serialize, deserialize } from "seroval";
const DATABASE_HASH_PREFIX = "database"; const DATABASE_HASH_PREFIX = "database";
@ -64,10 +65,12 @@ class LocalStorageCacheAdapter {
this.keys.add(fullKey); this.keys.add(fullKey);
try { try {
localStorage.setItem(fullKey, JSON.stringify(value)); localStorage.setItem(fullKey, serialize(value));
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof DOMException && error.name === "QUOTA_EXCEEDED_ERR") { if (error instanceof DOMException && error.name === "QUOTA_EXCEEDED_ERR") {
console.error("Storage quota exceeded, not caching new function calls"); console.error("Storage quota exceeded, not caching new function calls");
} else {
console.error(error);
} }
} }
} }
@ -80,7 +83,7 @@ class LocalStorageCacheAdapter {
get<R>(cacheName: string, key: string): R | undefined { get<R>(cacheName: string, key: string): R | undefined {
const item = localStorage.getItem(this.#createKey(cacheName, key)); const item = localStorage.getItem(this.#createKey(cacheName, key));
if (item) { if (item) {
return JSON.parse(item) as R; return deserialize(item) as R;
} }
} }
} }

View file

@ -1,7 +1,7 @@
import { createMemo, type Accessor } from "solid-js";
import { getDateList, getHourList, getMonthList, getWeekdayList } from "./date"; import { getDateList, getHourList, getMonthList, getWeekdayList } from "./date";
import type { MessageOverview, MessageStats, Recipients } from "~/types"; import type { MessageOverview, MessageStats, Recipients } from "~/types";
import { isSameDay } from "date-fns"; import { isSameDay } from "date-fns";
import { cached } from "./db-cache";
export const hourNames = getHourList(); export const hourNames = getHourList();
@ -15,16 +15,14 @@ export const weekdayNames = getWeekdayList();
const initialWeekdayMap = [...weekdayNames.keys()]; const initialWeekdayMap = [...weekdayNames.keys()];
export const createMessageStatsSources = ( const createMessageStatsSourcesRaw = (messageOverview: MessageOverview, recipients: Recipients) => {
messageOverview: Accessor<MessageOverview>, const initialRecipientMap = () => {
recipients: Accessor<Recipients>, return Object.fromEntries(recipients.map(({ recipientId }) => [recipientId, 0]));
) => { };
const initialRecipientMap = () => Object.fromEntries(recipients().map(({ recipientId }) => [recipientId, 0]));
const dateList = () => { const dateList = () => {
const currentDmMessagesOverview = messageOverview(); const firstDate = messageOverview?.at(0)?.messageDate;
const firstDate = currentDmMessagesOverview?.at(0)?.messageDate; const lastDate = messageOverview?.at(-1)?.messageDate;
const lastDate = currentDmMessagesOverview?.at(-1)?.messageDate;
if (firstDate && lastDate) { if (firstDate && lastDate) {
return getDateList(firstDate, lastDate).map((date) => ({ return getDateList(firstDate, lastDate).map((date) => ({
totalMessages: 0, totalMessages: 0,
@ -34,52 +32,51 @@ export const createMessageStatsSources = (
} }
}; };
return createMemo(() => { const currentDateList = dateList();
const currentMessageOverview = messageOverview(); const currentInitialRecipientMap = initialRecipientMap();
const currentDateList = dateList();
const currentInitialRecipientMap = initialRecipientMap();
const messageStats: MessageStats = { const messageStats: MessageStats = {
person: { ...currentInitialRecipientMap }, person: { ...currentInitialRecipientMap },
month: initialMonthMap.map(() => ({ ...currentInitialRecipientMap })), month: initialMonthMap.map(() => ({ ...currentInitialRecipientMap })),
date: currentDateList ?? [], date: currentDateList ?? [],
weekday: initialWeekdayMap.map(() => ({ ...currentInitialRecipientMap })), weekday: initialWeekdayMap.map(() => ({ ...currentInitialRecipientMap })),
daytime: initialHoursMap.map(() => ({ ...currentInitialRecipientMap })), daytime: initialHoursMap.map(() => ({ ...currentInitialRecipientMap })),
}; };
if (currentMessageOverview && currentDateList) { if (currentDateList) {
const { person, month, date, weekday, daytime } = messageStats; const { person, month, date, weekday, daytime } = messageStats;
for (const message of currentMessageOverview) { for (const message of messageOverview) {
const { messageDate } = message; const { messageDate } = message;
// increment overall message count of a person // increment overall message count of a person
person[message.fromRecipientId] += 1; person[message.fromRecipientId] += 1;
// increment the message count of the message's month for this recipient // increment the message count of the message's month for this recipient
month[messageDate.getMonth()][message.fromRecipientId] += 1; month[messageDate.getMonth()][message.fromRecipientId] += 1;
// biome-ignore lint/style/noNonNullAssertion: <explanation> // biome-ignore lint/style/noNonNullAssertion: <explanation>
const dateStatsEntry = date.find(({ date }) => isSameDay(date, messageDate))!; const dateStatsEntry = date.find(({ date }) => isSameDay(date, messageDate))!;
// increment the message count of the message's date for this recipient // increment the message count of the message's date for this recipient
dateStatsEntry[message.fromRecipientId] += 1; dateStatsEntry[message.fromRecipientId] += 1;
// increment the overall message count of the message's date // increment the overall message count of the message's date
dateStatsEntry.totalMessages += 1; dateStatsEntry.totalMessages += 1;
const weekdayOfDate = messageDate.getDay(); const weekdayOfDate = messageDate.getDay();
// we index starting with monday while the `Date` object indexes starting with Sunday // we index starting with monday while the `Date` object indexes starting with Sunday
const weekdayIndex = weekdayOfDate === 0 ? 6 : weekdayOfDate - 1; const weekdayIndex = weekdayOfDate === 0 ? 6 : weekdayOfDate - 1;
// increment the message count of the message's weekday for this recipient // increment the message count of the message's weekday for this recipient
weekday[weekdayIndex][message.fromRecipientId] += 1; weekday[weekdayIndex][message.fromRecipientId] += 1;
// increment the message count of the message's daytime for this recipient // increment the message count of the message's daytime for this recipient
daytime[messageDate.getHours()][message.fromRecipientId] += 1; daytime[messageDate.getHours()][message.fromRecipientId] += 1;
}
} }
}
return messageStats; return messageStats;
});
}; };
export const createMessageStatsSources = cached(createMessageStatsSourcesRaw);

View file

@ -1,4 +1,4 @@
import { type Component, createResource } from "solid-js"; import { type Component, createMemo, createResource } from "solid-js";
import type { RouteSectionProps } from "@solidjs/router"; import type { RouteSectionProps } from "@solidjs/router";
import { dmPartnerRecipientQuery, SELF_ID, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db"; import { dmPartnerRecipientQuery, SELF_ID, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db";
@ -35,7 +35,7 @@ export const DmId: Component<RouteSectionProps> = (props) => {
} }
}); });
const [dmMessagesOverview] = createResource<MessageOverview>(async () => { const [dmMessagesOverview] = createResource<MessageOverview | undefined>(async () => {
const dmMessageOverview = await threadSentMessagesOverviewQuery(dmId()); const dmMessageOverview = await threadSentMessagesOverviewQuery(dmId());
if (dmMessageOverview) { if (dmMessageOverview) {
return dmMessageOverview.map((row) => { return dmMessageOverview.map((row) => {
@ -61,16 +61,16 @@ export const DmId: Component<RouteSectionProps> = (props) => {
}, },
]; ];
} }
return [
{
recipientId: SELF_ID,
name: "You",
},
];
}; };
const dmMessageStats = createMessageStatsSources(dmMessagesOverview, recipients); const dmMessageStats = createMemo(() => {
const currentDmMessagesOverview = dmMessagesOverview();
const currentRecipients = recipients();
if (currentDmMessagesOverview && currentRecipients) {
return createMessageStatsSources(currentDmMessagesOverview, currentRecipients);
}
});
return ( return (
<> <>
@ -78,26 +78,26 @@ export const DmId: Component<RouteSectionProps> = (props) => {
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<Heading level={1}>DM with {dmPartner()?.name}</Heading> <Heading level={1}>DM with {dmPartner()?.name}</Heading>
<Heading level={2}>Chat timeline</Heading> <Heading level={2}>Chat timeline</Heading>
<DmMessagesPerDate dateStats={dmMessageStats().date} recipients={recipients()} /> <DmMessagesPerDate dateStats={dmMessageStats()?.date} recipients={recipients()} />
<DmOverview messages={dmMessagesOverview()} /> <DmOverview messages={dmMessagesOverview()} />
<Heading level={2}>Messages per</Heading> <Heading level={2}>Messages per</Heading>
<Grid cols={1} colsMd={2} class="gap-x-16 gap-y-16"> <Grid cols={1} colsMd={2} class="gap-x-16 gap-y-16">
<div> <div>
<Heading level={3}>Person</Heading> <Heading level={3}>Person</Heading>
<DmMessagesPerRecipient personStats={dmMessageStats().person} recipients={recipients()} /> <DmMessagesPerRecipient personStats={dmMessageStats()?.person} recipients={recipients()} />
</div> </div>
<div> <div>
<Heading level={3}>Daytime</Heading> <Heading level={3}>Daytime</Heading>
<DmMessagesPerDaytime daytimeStats={dmMessageStats().daytime} recipients={recipients()} /> <DmMessagesPerDaytime daytimeStats={dmMessageStats()?.daytime} recipients={recipients()} />
</div> </div>
<div> <div>
<Heading level={3}>Month</Heading> <Heading level={3}>Month</Heading>
<DmMessagesPerMonth monthStats={dmMessageStats().month} recipients={recipients()} /> <DmMessagesPerMonth monthStats={dmMessageStats()?.month} recipients={recipients()} />
</div> </div>
<div> <div>
<Heading level={3}>Weekday</Heading> <Heading level={3}>Weekday</Heading>
<DmMessagesPerWeekday weekdayStats={dmMessageStats().weekday} recipients={recipients()} /> <DmMessagesPerWeekday weekdayStats={dmMessageStats()?.weekday} recipients={recipients()} />
</div> </div>
</Grid> </Grid>
<Heading level={2}>Word cloud</Heading> <Heading level={2}>Word cloud</Heading>

View file

@ -4,14 +4,14 @@ import { LineChart } from "~/components/ui/charts";
import type { MessageStats, Recipients } from "~/types"; import type { MessageStats, Recipients } from "~/types";
export const DmMessagesPerDate: Component<{ export const DmMessagesPerDate: Component<{
dateStats: MessageStats["date"]; dateStats: MessageStats["date"] | undefined;
recipients: Recipients; recipients: Recipients | undefined;
}> = (props) => { }> = (props) => {
const dateChartData: Accessor<ChartData<"line"> | undefined> = () => { const dateChartData: Accessor<ChartData<"line"> | undefined> = () => {
const currentDmMessages = props.dateStats; const currentDmMessages = props.dateStats;
const currentRecipients = props.recipients; const currentRecipients = props.recipients;
if (currentDmMessages) { if (currentDmMessages && currentRecipients) {
const currentDmMessagesValues = Object.values(currentDmMessages); const currentDmMessagesValues = Object.values(currentDmMessages);
return { return {

View file

@ -5,8 +5,8 @@ import type { MessageStats, Recipients } from "~/types";
import { hourNames } from "~/lib/messages"; import { hourNames } from "~/lib/messages";
export const DmMessagesPerDaytime: Component<{ export const DmMessagesPerDaytime: Component<{
daytimeStats: MessageStats["daytime"]; daytimeStats: MessageStats["daytime"] | undefined;
recipients: Recipients; recipients: Recipients | undefined;
}> = (props) => { }> = (props) => {
const daytimeChartData: Accessor<ChartData<"bar"> | undefined> = () => { const daytimeChartData: Accessor<ChartData<"bar"> | undefined> = () => {
const currentMessagesPerHour = props.daytimeStats; const currentMessagesPerHour = props.daytimeStats;

View file

@ -5,8 +5,8 @@ import { monthNames } from "~/lib/messages";
import type { MessageStats, Recipients } from "~/types"; import type { MessageStats, Recipients } from "~/types";
export const DmMessagesPerMonth: Component<{ export const DmMessagesPerMonth: Component<{
monthStats: MessageStats["month"]; monthStats: MessageStats["month"] | undefined;
recipients: Recipients; recipients: Recipients | undefined;
}> = (props) => { }> = (props) => {
const monthChartData: Accessor<ChartData<"radar"> | undefined> = () => { const monthChartData: Accessor<ChartData<"radar"> | undefined> = () => {
const currentMessagesPerMonth = props.monthStats; const currentMessagesPerMonth = props.monthStats;

View file

@ -4,8 +4,8 @@ import { PieChart } from "~/components/ui/charts";
import type { MessageStats, Recipients } from "~/types"; import type { MessageStats, Recipients } from "~/types";
export const DmMessagesPerRecipient: Component<{ export const DmMessagesPerRecipient: Component<{
personStats: MessageStats["person"]; personStats: MessageStats["person"] | undefined;
recipients: Recipients; recipients: Recipients | undefined;
}> = (props) => { }> = (props) => {
const recipientChartData: Accessor<ChartData<"pie"> | undefined> = () => { const recipientChartData: Accessor<ChartData<"pie"> | undefined> = () => {
const currentMessagesPerRecipient = props.personStats; const currentMessagesPerRecipient = props.personStats;

View file

@ -5,8 +5,8 @@ import { weekdayNames } from "~/lib/messages";
import type { MessageStats, Recipients } from "~/types"; import type { MessageStats, Recipients } from "~/types";
export const DmMessagesPerWeekday: Component<{ export const DmMessagesPerWeekday: Component<{
weekdayStats: MessageStats["weekday"]; weekdayStats: MessageStats["weekday"] | undefined;
recipients: Recipients; recipients: Recipients | undefined;
}> = (props) => { }> = (props) => {
const weekdayChartData: Accessor<ChartData<"radar"> | undefined> = () => { const weekdayChartData: Accessor<ChartData<"radar"> | undefined> = () => {
const currentMessagesPerWeekday = props.weekdayStats; const currentMessagesPerWeekday = props.weekdayStats;

View file

@ -6,7 +6,7 @@ import { getDistanceBetweenDatesInDays } from "~/lib/date";
import type { MessageOverview } from "~/types"; import type { MessageOverview } from "~/types";
export const DmOverview: Component<{ export const DmOverview: Component<{
messages: MessageOverview; messages: MessageOverview | undefined;
}> = (props) => { }> = (props) => {
const dmOverview = () => { const dmOverview = () => {
const firstMessageDate = props.messages?.at(0)?.messageDate; const firstMessageDate = props.messages?.at(0)?.messageDate;

View file

@ -4,6 +4,7 @@ import { type RouteSectionProps, useNavigate } from "@solidjs/router";
import { setDb, SQL } from "~/db"; import { setDb, SQL } from "~/db";
import { Portal } from "solid-js/web"; import { Portal } from "solid-js/web";
import { Flex } from "~/components/ui/flex"; import { Flex } from "~/components/ui/flex";
import { Title } from "@solidjs/meta";
export const Home: Component<RouteSectionProps> = () => { export const Home: Component<RouteSectionProps> = () => {
const [isLoadingDb, setIsLoadingDb] = createSignal(false); const [isLoadingDb, setIsLoadingDb] = createSignal(false);
@ -38,6 +39,7 @@ export const Home: Component<RouteSectionProps> = () => {
</Flex> </Flex>
</Show> </Show>
</Portal> </Portal>
<Title>Signal stats</Title>
<div> <div>
<input type="file" accept=".sqlite" onChange={onFileChange}></input> <input type="file" accept=".sqlite" onChange={onFileChange}></input>
</div> </div>

View file

@ -1,9 +1,7 @@
export type MessageOverview = export type MessageOverview = {
| { messageDate: Date;
messageDate: Date; fromRecipientId: number;
fromRecipientId: number; }[];
}[]
| undefined;
export type Recipients = { export type Recipients = {
recipientId: number; recipientId: number;