From df218b9a56ef76bbb501a9e8c9c0f963e4f571b0 Mon Sep 17 00:00:00 2001 From: Samuel Date: Thu, 19 Dec 2024 16:44:27 +0100 Subject: [PATCH] perf: offload long running tasks to workers, preloading dm page data --- .husky/pre-commit | 6 ++ package.json | 7 +- pnpm-lock.yaml | 128 +++++++++++++++++--------- src/App.tsx | 4 +- src/db-queries.ts | 117 +++++++++++++++++++++++ src/db.ts | 119 +----------------------- src/index.tsx | 19 +++- src/lib/db-cache.ts | 73 ++++++++++----- src/lib/messages-worker.ts | 109 ++++++++++++++++++++++ src/lib/messages.ts | 95 +++++-------------- src/pages/dm/dm-id.tsx | 108 +++++++++++++++------- src/pages/dm/dm-wordcloud.tsx | 2 +- src/pages/home.tsx | 2 +- src/pages/index.tsx | 1 + src/pages/overview/index.tsx | 3 +- src/pages/overview/overview-table.tsx | 24 ++++- tsconfig.json | 1 + vite.config.ts | 3 + 18 files changed, 524 insertions(+), 297 deletions(-) create mode 100644 src/db-queries.ts create mode 100644 src/lib/messages-worker.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index 0d4e3a9..b793c42 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,7 @@ +# pnpm +export PNPM_HOME="/home/samuel/.local/share/pnpm" +case ":$PATH:" in + *":$PNPM_HOME:"*) ;; + *) export PATH="$PNPM_HOME:$PATH" ;; +esac pnpm dlx lint-staged \ No newline at end of file diff --git a/package.json b/package.json index af7c484..f89bbb3 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@biomejs/biome": "1.9.4", "@commitlint/cli": "^19.6.1", "@commitlint/config-conventional": "^19.6.0", - "@types/node": "^22.10.1", + "@types/node": "^22.10.2", "@types/sql.js": "^1.4.9", "autoprefixer": "^10.4.20", "better-sqlite3": "^11.7.0", @@ -25,15 +25,16 @@ "kysely-codegen": "^0.17.0", "lint-staged": "^15.2.11", "postcss": "^8.4.49", - "tailwindcss": "^3.4.16", + "tailwindcss": "^3.4.17", "typescript": "^5.7.2", - "vite": "^6.0.3", + "vite": "^6.0.4", "vite-plugin-solid": "^2.11.0" }, "dependencies": { "@kobalte/core": "^0.13.7", "@kobalte/tailwindcss": "^0.9.0", "@solid-primitives/refs": "^1.0.8", + "@solid-primitives/workers": "^0.3.0", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.2", "@tanstack/solid-table": "^8.20.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f26ba29..37a3b43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,10 +13,13 @@ importers: version: 0.13.7(solid-js@1.9.3) '@kobalte/tailwindcss': specifier: ^0.9.0 - version: 0.9.0(tailwindcss@3.4.16) + version: 0.9.0(tailwindcss@3.4.17) '@solid-primitives/refs': specifier: ^1.0.8 version: 1.0.8(solid-js@1.9.3) + '@solid-primitives/workers': + specifier: ^0.3.0 + version: 0.3.0(solid-js@1.9.3) '@solidjs/meta': specifier: ^0.29.4 version: 0.29.4(solid-js@1.9.3) @@ -70,20 +73,20 @@ importers: version: 2.5.5 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.16) + version: 1.0.7(tailwindcss@3.4.17) devDependencies: '@biomejs/biome': specifier: 1.9.4 version: 1.9.4 '@commitlint/cli': specifier: ^19.6.1 - version: 19.6.1(@types/node@22.10.1)(typescript@5.7.2) + version: 19.6.1(@types/node@22.10.2)(typescript@5.7.2) '@commitlint/config-conventional': specifier: ^19.6.0 version: 19.6.0 '@types/node': - specifier: ^22.10.1 - version: 22.10.1 + specifier: ^22.10.2 + version: 22.10.2 '@types/sql.js': specifier: ^1.4.9 version: 1.4.9 @@ -106,17 +109,17 @@ importers: specifier: ^8.4.49 version: 8.4.49 tailwindcss: - specifier: ^3.4.16 - version: 3.4.16 + specifier: ^3.4.17 + version: 3.4.17 typescript: specifier: ^5.7.2 version: 5.7.2 vite: - specifier: ^6.0.3 - version: 6.0.3(@types/node@22.10.1)(jiti@2.4.2)(yaml@2.6.1) + specifier: ^6.0.4 + version: 6.0.4(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1) vite-plugin-solid: specifier: ^2.11.0 - version: 2.11.0(solid-js@1.9.3)(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.2)(yaml@2.6.1)) + version: 2.11.0(solid-js@1.9.3)(vite@6.0.4(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)) packages: @@ -499,6 +502,10 @@ packages: resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -697,6 +704,11 @@ packages: peerDependencies: solid-js: ^1.6.12 + '@solid-primitives/workers@0.3.0': + resolution: {integrity: sha512-NdfdHHNn4ut6zWoL/xSAZbIuzdrefwc2jin9Oaa/bI1o1QDKdOBQpR07ig7CzpiVdctyeSk6iE8ab6gnZVSSzQ==} + peerDependencies: + solid-js: ^1.6.12 + '@solidjs/meta@0.29.4': resolution: {integrity: sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g==} peerDependencies: @@ -750,8 +762,8 @@ packages: '@types/hammerjs@2.0.46': resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} - '@types/node@22.10.1': - resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} + '@types/node@22.10.2': + resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} '@types/sql.js@1.4.9': resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==} @@ -1277,6 +1289,10 @@ packages: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} + is-core-module@2.16.0: + resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1323,8 +1339,8 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jiti@1.21.6: - resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true jiti@2.4.2: @@ -1727,6 +1743,10 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true + resolve@1.22.9: + resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==} + hasBin: true + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -1892,8 +1912,8 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders' - tailwindcss@3.4.16: - resolution: {integrity: sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==} + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} hasBin: true @@ -1968,8 +1988,8 @@ packages: '@testing-library/jest-dom': optional: true - vite@6.0.3: - resolution: {integrity: sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==} + vite@6.0.4: + resolution: {integrity: sha512-zwlH6ar+6o6b4Wp+ydhtIKLrGM/LoqZzcdVmkGAFun0KHTzIzjh+h0kungEx7KJg/PYnC80I4TII9WkjciSR6Q==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -2213,11 +2233,11 @@ snapshots: '@biomejs/cli-win32-x64@1.9.4': optional: true - '@commitlint/cli@19.6.1(@types/node@22.10.1)(typescript@5.7.2)': + '@commitlint/cli@19.6.1(@types/node@22.10.2)(typescript@5.7.2)': dependencies: '@commitlint/format': 19.5.0 '@commitlint/lint': 19.6.0 - '@commitlint/load': 19.6.1(@types/node@22.10.1)(typescript@5.7.2) + '@commitlint/load': 19.6.1(@types/node@22.10.2)(typescript@5.7.2) '@commitlint/read': 19.5.0 '@commitlint/types': 19.5.0 tinyexec: 0.3.1 @@ -2264,7 +2284,7 @@ snapshots: '@commitlint/rules': 19.6.0 '@commitlint/types': 19.5.0 - '@commitlint/load@19.6.1(@types/node@22.10.1)(typescript@5.7.2)': + '@commitlint/load@19.6.1(@types/node@22.10.2)(typescript@5.7.2)': dependencies: '@commitlint/config-validator': 19.5.0 '@commitlint/execute-rule': 19.5.0 @@ -2272,7 +2292,7 @@ snapshots: '@commitlint/types': 19.5.0 chalk: 5.3.0 cosmiconfig: 9.0.0(typescript@5.7.2) - cosmiconfig-typescript-loader: 6.1.0(@types/node@22.10.1)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2) + cosmiconfig-typescript-loader: 6.1.0(@types/node@22.10.2)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -2434,6 +2454,12 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} @@ -2457,9 +2483,9 @@ snapshots: solid-presence: 0.1.8(solid-js@1.9.3) solid-prevent-scroll: 0.1.10(solid-js@1.9.3) - '@kobalte/tailwindcss@0.9.0(tailwindcss@3.4.16)': + '@kobalte/tailwindcss@0.9.0(tailwindcss@3.4.17)': dependencies: - tailwindcss: 3.4.16 + tailwindcss: 3.4.17 '@kobalte/utils@0.9.1(solid-js@1.9.3)': dependencies: @@ -2605,6 +2631,10 @@ snapshots: dependencies: solid-js: 1.9.3 + '@solid-primitives/workers@0.3.0(solid-js@1.9.3)': + dependencies: + solid-js: 1.9.3 + '@solidjs/meta@0.29.4(solid-js@1.9.3)': dependencies: solid-js: 1.9.3 @@ -2647,7 +2677,7 @@ snapshots: '@types/conventional-commits-parser@5.0.1': dependencies: - '@types/node': 22.10.1 + '@types/node': 22.10.2 '@types/d3-cloud@1.2.9': dependencies: @@ -2661,14 +2691,14 @@ snapshots: '@types/hammerjs@2.0.46': {} - '@types/node@22.10.1': + '@types/node@22.10.2': dependencies: undici-types: 6.20.0 '@types/sql.js@1.4.9': dependencies: '@types/emscripten': 1.39.13 - '@types/node': 22.10.1 + '@types/node': 22.10.2 JSONStream@1.3.5: dependencies: @@ -2900,9 +2930,9 @@ snapshots: convert-source-map@2.0.0: {} - cosmiconfig-typescript-loader@6.1.0(@types/node@22.10.1)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2): + cosmiconfig-typescript-loader@6.1.0(@types/node@22.10.2)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2): dependencies: - '@types/node': 22.10.1 + '@types/node': 22.10.2 cosmiconfig: 9.0.0(typescript@5.7.2) jiti: 2.4.2 typescript: 5.7.2 @@ -3183,6 +3213,10 @@ snapshots: dependencies: hasown: 2.0.2 + is-core-module@2.16.0: + dependencies: + hasown: 2.0.2 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -3217,7 +3251,7 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jiti@1.21.6: {} + jiti@1.21.7: {} jiti@2.4.2: {} @@ -3460,7 +3494,7 @@ snapshots: postcss: 8.4.49 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.8 + resolve: 1.22.9 postcss-js@4.0.1(postcss@8.4.49): dependencies: @@ -3553,6 +3587,12 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@1.22.9: + dependencies: + is-core-module: 2.16.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -3706,7 +3746,7 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/gen-mapping': 0.3.8 commander: 4.1.1 glob: 10.4.5 lines-and-columns: 1.2.4 @@ -3726,11 +3766,11 @@ snapshots: tailwind-merge@2.5.5: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.16): + tailwindcss-animate@1.0.7(tailwindcss@3.4.17): dependencies: - tailwindcss: 3.4.16 + tailwindcss: 3.4.17 - tailwindcss@3.4.16: + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -3740,7 +3780,7 @@ snapshots: fast-glob: 3.3.2 glob-parent: 6.0.2 is-glob: 4.0.3 - jiti: 1.21.6 + jiti: 1.21.7 lilconfig: 3.1.3 micromatch: 4.0.8 normalize-path: 3.0.0 @@ -3752,7 +3792,7 @@ snapshots: postcss-load-config: 4.0.2(postcss@8.4.49) postcss-nested: 6.2.0(postcss@8.4.49) postcss-selector-parser: 6.1.2 - resolve: 1.22.8 + resolve: 1.22.9 sucrase: 3.35.0 transitivePeerDependencies: - ts-node @@ -3814,7 +3854,7 @@ snapshots: validate-html-nesting@1.2.2: {} - vite-plugin-solid@2.11.0(solid-js@1.9.3)(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.2)(yaml@2.6.1)): + vite-plugin-solid@2.11.0(solid-js@1.9.3)(vite@6.0.4(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)): dependencies: '@babel/core': 7.26.0 '@types/babel__core': 7.20.5 @@ -3822,25 +3862,25 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.3 solid-refresh: 0.6.3(solid-js@1.9.3) - vite: 6.0.3(@types/node@22.10.1)(jiti@2.4.2)(yaml@2.6.1) - vitefu: 1.0.4(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.2)(yaml@2.6.1)) + vite: 6.0.4(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1) + vitefu: 1.0.4(vite@6.0.4(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)) transitivePeerDependencies: - supports-color - vite@6.0.3(@types/node@22.10.1)(jiti@2.4.2)(yaml@2.6.1): + vite@6.0.4(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1): dependencies: esbuild: 0.24.0 postcss: 8.4.49 rollup: 4.28.1 optionalDependencies: - '@types/node': 22.10.1 + '@types/node': 22.10.2 fsevents: 2.3.3 jiti: 2.4.2 yaml: 2.6.1 - vitefu@1.0.4(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.2)(yaml@2.6.1)): + vitefu@1.0.4(vite@6.0.4(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)): optionalDependencies: - vite: 6.0.3(@types/node@22.10.1)(jiti@2.4.2)(yaml@2.6.1) + vite: 6.0.4(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1) which@2.0.2: dependencies: diff --git a/src/App.tsx b/src/App.tsx index 980dd12..716b084 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { type Component } from "solid-js"; import { Route } from "@solidjs/router"; -import { DmId, GroupId, Home, Overview } from "./pages"; +import { DmId, GroupId, Home, Overview, preloadDmId } from "./pages"; import "./app.css"; @@ -9,7 +9,7 @@ const App: Component = () => { <> - + ); diff --git a/src/db-queries.ts b/src/db-queries.ts new file mode 100644 index 0000000..d725215 --- /dev/null +++ b/src/db-queries.ts @@ -0,0 +1,117 @@ +import { sql, type NotNull } from "kysely"; +import { cached } from "./lib/db-cache"; +import { kyselyDb, SELF_ID } from "./db"; + +const allThreadsOverviewQueryRaw = () => + kyselyDb() + ?.selectFrom("thread") + .innerJoin( + (eb) => + eb + .selectFrom("message") + .select((eb) => ["message.thread_id", eb.fn.countAll().as("message_count")]) + .where((eb) => { + return eb.and([eb("message.body", "is not", null), eb("message.body", "is not", "")]); + }) + .groupBy("message.thread_id") + .as("message"), + (join) => join.onRef("message.thread_id", "=", "thread._id"), + ) + .innerJoin("recipient", "thread.recipient_id", "recipient._id") + .leftJoin("groups", "recipient._id", "groups.recipient_id") + .select([ + "thread._id as thread_id", + "thread.recipient_id", + "thread.archived", + "recipient.profile_joined_name", + "recipient.system_joined_name", + "groups.title", + "message_count", + "thread.date as last_message_date", + "recipient.nickname_joined_name", + ]) + .where("message_count", ">", 0) + .$narrowType<{ + thread_id: NotNull; + archived: NotNull; + message_count: number; + }>() + .execute(); + +export const allThreadsOverviewQuery = cached(allThreadsOverviewQueryRaw); + +const overallSentMessagesQueryRaw = (recipientId: number) => + kyselyDb() + ?.selectFrom("message") + .select((eb) => eb.fn.countAll().as("messageCount")) + .where((eb) => + eb.and([ + eb("message.from_recipient_id", "=", recipientId), + eb("message.body", "is not", null), + eb("message.body", "!=", ""), + ]), + ) + .executeTakeFirst(); + +export const overallSentMessagesQuery = cached(overallSentMessagesQueryRaw); + +const dmPartnerRecipientQueryRaw = (dmId: number) => + kyselyDb() + ?.selectFrom("recipient") + .select([ + "recipient._id", + "recipient.system_joined_name", + "recipient.profile_joined_name", + "recipient.nickname_joined_name", + ]) + .innerJoin("thread", "recipient._id", "thread.recipient_id") + .where((eb) => eb.and([eb("thread._id", "=", dmId), eb("recipient._id", "!=", SELF_ID)])) + .$narrowType<{ + _id: number; + }>() + .executeTakeFirst(); + +export const dmPartnerRecipientQuery = cached(dmPartnerRecipientQueryRaw); + +const threadSentMessagesOverviewQueryRaw = (threadId: number) => + kyselyDb() + ?.selectFrom("message") + .select(["from_recipient_id", sql`datetime(date_sent / 1000, 'unixepoch')`.as("message_datetime")]) + .orderBy(["message_datetime"]) + .where((eb) => eb.and([eb("body", "is not", null), eb("body", "!=", ""), eb("thread_id", "=", threadId)])) + .execute(); + +export const threadSentMessagesOverviewQuery = cached(threadSentMessagesOverviewQueryRaw); + +const threadMostUsedWordsQueryRaw = (threadId: number, limit = 10) => + kyselyDb() + ?.withRecursive("words", (eb) => { + return eb + .selectFrom("message") + .select([ + sql`LOWER(substr(body, 1, instr(body || " ", " ") - 1))`.as("word"), + sql`(substr(body, instr(body || " ", " ") + 1))`.as("rest"), + ]) + .where((eb) => eb.and([eb("body", "is not", null), eb("thread_id", "=", threadId)])) + .unionAll((ebInner) => { + return ebInner + .selectFrom("words") + .select([ + sql`LOWER(substr(rest, 1, instr(rest || " ", " ") - 1))`.as("word"), + sql`(substr(rest, instr(rest || " ", " ") + 1))`.as("rest"), + ]) + .where("rest", "<>", ""); + }); + }) + .selectFrom("words") + .select((eb) => ["word", eb.fn.countAll().as("count")]) + .where("word", "<>", "") + .groupBy("word") + .orderBy("count desc") + .limit(limit) + .$narrowType<{ + count: number; + }>() + .execute(); + +export const threadMostUsedWordsQuery = cached(threadMostUsedWordsQueryRaw); diff --git a/src/db.ts b/src/db.ts index 08ea3ee..043302d 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,12 +1,11 @@ import { createEffect, createMemo, createRoot, createSignal } from "solid-js"; -import { Kysely, sql, type NotNull } from "kysely"; +import { Kysely } from "kysely"; import type { DB } from "kysely-codegen"; import { SqlJsDialect } from "kysely-wasm"; import initSqlJS, { type Database } from "sql.js"; import wasmURL from "./assets/sql-wasm.wasm?url"; -import { cached } from "./lib/db-cache"; export const SELF_ID = 2; @@ -26,7 +25,7 @@ const sqlJsDialect = () => { } }; -const kyselyDb = createRoot(() => { +export const kyselyDb = createRoot(() => { createEffect(() => { const currentDb = db(); @@ -49,117 +48,3 @@ const kyselyDb = createRoot(() => { }); }); }); - -const allThreadsOverviewQueryRaw = () => - kyselyDb() - ?.selectFrom("thread") - .innerJoin( - (eb) => - eb - .selectFrom("message") - .select((eb) => ["message.thread_id", eb.fn.countAll().as("message_count")]) - .where((eb) => { - return eb.and([eb("message.body", "is not", null), eb("message.body", "is not", "")]); - }) - .groupBy("message.thread_id") - .as("message"), - (join) => join.onRef("message.thread_id", "=", "thread._id"), - ) - .innerJoin("recipient", "thread.recipient_id", "recipient._id") - .leftJoin("groups", "recipient._id", "groups.recipient_id") - .select([ - "thread._id as thread_id", - "thread.recipient_id", - "thread.archived", - "recipient.profile_joined_name", - "recipient.system_joined_name", - "groups.title", - "message_count", - "thread.date as last_message_date", - "recipient.nickname_joined_name", - ]) - .where("message_count", ">", 0) - .$narrowType<{ - thread_id: NotNull; - archived: NotNull; - message_count: number; - }>() - .execute(); - -export const allThreadsOverviewQuery = cached(allThreadsOverviewQueryRaw); - -const overallSentMessagesQueryRaw = (recipientId: number) => - kyselyDb() - ?.selectFrom("message") - .select((eb) => eb.fn.countAll().as("messageCount")) - .where((eb) => - eb.and([ - eb("message.from_recipient_id", "=", recipientId), - eb("message.body", "is not", null), - eb("message.body", "!=", ""), - ]), - ) - .executeTakeFirst(); - -export const overallSentMessagesQuery = cached(overallSentMessagesQueryRaw); - -const dmPartnerRecipientQueryRaw = (dmId: number) => - kyselyDb() - ?.selectFrom("recipient") - .select([ - "recipient._id", - "recipient.system_joined_name", - "recipient.profile_joined_name", - "recipient.nickname_joined_name", - ]) - .innerJoin("thread", "recipient._id", "thread.recipient_id") - .where((eb) => eb.and([eb("thread._id", "=", dmId), eb("recipient._id", "!=", SELF_ID)])) - .$narrowType<{ - _id: number; - }>() - .executeTakeFirst(); - -export const dmPartnerRecipientQuery = cached(dmPartnerRecipientQueryRaw); - -const threadSentMessagesOverviewQueryRaw = (threadId: number) => - kyselyDb() - ?.selectFrom("message") - .select(["from_recipient_id", sql`datetime(date_sent / 1000, 'unixepoch')`.as("message_datetime")]) - .orderBy(["message_datetime"]) - .where((eb) => eb.and([eb("body", "is not", null), eb("body", "!=", ""), eb("thread_id", "=", threadId)])) - .execute(); - -export const threadSentMessagesOverviewQuery = cached(threadSentMessagesOverviewQueryRaw); - -const threadMostUsedWordsQueryRaw = (threadId: number, limit = 10) => - kyselyDb() - ?.withRecursive("words", (eb) => { - return eb - .selectFrom("message") - .select([ - sql`LOWER(substr(body, 1, instr(body || " ", " ") - 1))`.as("word"), - sql`(substr(body, instr(body || " ", " ") + 1))`.as("rest"), - ]) - .where((eb) => eb.and([eb("body", "is not", null), eb("thread_id", "=", threadId)])) - .unionAll((ebInner) => { - return ebInner - .selectFrom("words") - .select([ - sql`LOWER(substr(rest, 1, instr(rest || " ", " ") - 1))`.as("word"), - sql`(substr(rest, instr(rest || " ", " ") + 1))`.as("rest"), - ]) - .where("rest", "<>", ""); - }); - }) - .selectFrom("words") - .select((eb) => ["word", eb.fn.countAll().as("count")]) - .where("word", "<>", "") - .groupBy("word") - .orderBy("count desc") - .limit(limit) - .$narrowType<{ - count: number; - }>() - .execute(); - -export const threadMostUsedWordsQuery = cached(threadMostUsedWordsQueryRaw); diff --git a/src/index.tsx b/src/index.tsx index a903f1d..dbf7f34 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,9 +1,11 @@ /* @refresh reload */ import { render } from "solid-js/web"; -import { Router } from "@solidjs/router"; +import { Router, useNavigate } from "@solidjs/router"; import { MetaProvider } from "@solidjs/meta"; import App from "./App"; +import { createEffect } from "solid-js"; +import { db } from "./db"; const root = document.getElementById("root"); @@ -18,7 +20,20 @@ if (root) { () => (
- + { + const navigate = useNavigate(); + const { pathname } = props.location; + + createEffect(() => { + if (!db() && pathname !== "/") { + navigate("/"); + } + }); + + return props.children; + }} + > diff --git a/src/lib/db-cache.ts b/src/lib/db-cache.ts index 888a330..d42e206 100644 --- a/src/lib/db-cache.ts +++ b/src/lib/db-cache.ts @@ -1,5 +1,7 @@ -import { createRoot, on, createDeferred } from "solid-js"; +import { on, createSignal, createEffect, createRoot, createMemo } from "solid-js"; import { serialize, deserialize } from "seroval"; +import { createSignaledWorker } from "@solid-primitives/workers"; +import { db } from "~/db"; const DATABASE_HASH_PREFIX = "database"; @@ -30,31 +32,48 @@ const hashString = (str: string) => { const HASH_STORE_KEY = `${DATABASE_HASH_PREFIX}_hash`; -// cannot import `db` the normal way because this file is imported in ~/db.ts before the initialisation of `db` has happened createRoot(() => { - void import("~/db").then(({ db }) => { - // we use create deferred because hasing can take very long and we don't want to block the mainthread - createDeferred( - on(db, (currentDb) => { - if (currentDb) { - const newHash = hashString(new TextDecoder().decode(currentDb.export())).toString(); + const [dbHash, setDbHash] = createSignal(localStorage.getItem(HASH_STORE_KEY)); - const oldHash = localStorage.getItem(HASH_STORE_KEY); - - if (newHash !== oldHash) { - clearDbCache(); - - localStorage.setItem(HASH_STORE_KEY, newHash); - } + // offloaded because this can take a long time (>1s easily) and would block the mainthread + createSignaledWorker({ + input: db, + output: setDbHash, + func: function hashDb(currentDb: ReturnType) { + const hashString = (str: string) => { + let hash = 0, + i, + chr; + if (str.length === 0) return hash; + for (i = 0; i < str.length; i++) { + chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; // Convert to 32bit integer } - }), - ); + return hash; + }; + + if (currentDb?.export) { + return hashString(new TextDecoder().decode(currentDb.export())).toString(); + } + }, + }); + + createEffect(() => { + on(dbHash, (currentDbHash) => { + if (currentDbHash) { + clearDbCache(); + + localStorage.setItem(HASH_STORE_KEY, currentDbHash); + } + }); }); }); class LocalStorageCacheAdapter { keys = new Set(Object.keys(localStorage).filter((key) => key.startsWith(this.prefix))); prefix = "database"; + #dbLoaded = createMemo(() => !!db()); #createKey(cacheName: string, key: string): string { return `${this.prefix}-${cacheName}-${key}`; @@ -76,14 +95,24 @@ class LocalStorageCacheAdapter { } has(cacheName: string, key: string): boolean { - return this.keys.has(this.#createKey(cacheName, key)); + if (this.#dbLoaded()) { + return this.keys.has(this.#createKey(cacheName, key)); + } + + console.info("No database loaded"); + + return false; } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters get(cacheName: string, key: string): R | undefined { - const item = localStorage.getItem(this.#createKey(cacheName, key)); - if (item) { - return deserialize(item) as R; + if (this.#dbLoaded()) { + const item = localStorage.getItem(this.#createKey(cacheName, key)); + + if (item) { + return deserialize(item) as R; + } + } else { + console.info("No database loaded"); } } } diff --git a/src/lib/messages-worker.ts b/src/lib/messages-worker.ts new file mode 100644 index 0000000..d4cb339 --- /dev/null +++ b/src/lib/messages-worker.ts @@ -0,0 +1,109 @@ +import { getHourList, getMonthList, getWeekdayList } from "./date"; +import type { MessageOverview, MessageStats, Recipients } from "~/types"; + +const hourNames = getHourList(); + +const initialHourMap = [...hourNames.keys()]; + +const monthNames = getMonthList(); + +const initialMonthMap = [...monthNames.keys()]; + +const weekdayNames = getWeekdayList(); + +const initialWeekdayMap = [...weekdayNames.keys()]; + +function createMessageStatsSourcesRaw(messageOverview: MessageOverview, recipients: Recipients) { + const getDateList = (startDate: Date, endDate: Date): Date[] => { + const dateArray = new Array(); + + // end date for loop has to be one date after because we increment after adding the date in the loop + const endDateForLoop = new Date(endDate); + + endDateForLoop.setDate(endDateForLoop.getDate() + 1); + + let currentDate = startDate; + + while (currentDate <= endDateForLoop) { + dateArray.push(new Date(currentDate)); + + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + 1); + + currentDate = newDate; + } + + return dateArray; + }; + + const initialRecipientMap = () => { + return Object.fromEntries(recipients.map(({ recipientId }) => [recipientId, 0])); + }; + + const dateList = () => { + const firstDate = messageOverview?.at(0)?.messageDate; + const lastDate = messageOverview?.at(-1)?.messageDate; + if (firstDate && lastDate) { + return getDateList(firstDate, lastDate).map((date) => ({ + totalMessages: 0, + date, + ...initialRecipientMap(), + })); + } + }; + + const currentDateList = dateList(); + const currentInitialRecipientMap = initialRecipientMap(); + + const messageStats: MessageStats = { + person: { ...currentInitialRecipientMap }, + month: initialMonthMap.map(() => ({ ...currentInitialRecipientMap })), + date: currentDateList ?? [], + weekday: initialWeekdayMap.map(() => ({ ...currentInitialRecipientMap })), + daytime: initialHourMap.map(() => ({ ...currentInitialRecipientMap })), + }; + + if (currentDateList) { + const { person, month, date, weekday, daytime } = messageStats; + + for (const message of messageOverview) { + const { messageDate } = message; + + // increment overall message count of a person + person[message.fromRecipientId] += 1; + + // increment the message count of the message's month for this recipient + month[messageDate.getMonth()][message.fromRecipientId] += 1; + + // biome-ignore lint/style/noNonNullAssertion: + const dateStatsEntry = date.find(({ date }) => date.toDateString() === messageDate.toDateString())!; + + // increment the message count of the message's date for this recipient + dateStatsEntry[message.fromRecipientId] += 1; + + // increment the overall message count of the message's date + dateStatsEntry.totalMessages += 1; + + const weekdayOfDate = messageDate.getDay(); + // we index starting with monday while the `Date` object indexes starting with Sunday + const weekdayIndex = weekdayOfDate === 0 ? 6 : weekdayOfDate - 1; + + // increment the message count of the message's weekday for this recipient + weekday[weekdayIndex][message.fromRecipientId] += 1; + + // increment the message count of the message's daytime for this recipient + daytime[messageDate.getHours()][message.fromRecipientId] += 1; + } + } + + return messageStats; +} + +self.onmessage = (event: MessageEvent<{ dmId: number; messageOverview: MessageOverview; recipients: Recipients }>) => { + const result = createMessageStatsSourcesRaw(event.data.messageOverview, event.data.recipients); + + postMessage({ + dmId: event.data.dmId, + messageStatsSources: result, + }); +}; diff --git a/src/lib/messages.ts b/src/lib/messages.ts index 782581b..51c1539 100644 --- a/src/lib/messages.ts +++ b/src/lib/messages.ts @@ -1,82 +1,37 @@ -import { getDateList, getHourList, getMonthList, getWeekdayList } from "./date"; import type { MessageOverview, MessageStats, Recipients } from "~/types"; -import { isSameDay } from "date-fns"; import { cached } from "./db-cache"; +import { getHourList, getMonthList, getWeekdayList } from "./date"; +import MessageStatsWorker from "./messages-worker?worker"; export const hourNames = getHourList(); -const initialHoursMap = [...hourNames.keys()]; - export const monthNames = getMonthList(); -const initialMonthMap = [...monthNames.keys()]; - export const weekdayNames = getWeekdayList(); -const initialWeekdayMap = [...weekdayNames.keys()]; +const messageStatsWorker = new MessageStatsWorker(); -const createMessageStatsSourcesRaw = (messageOverview: MessageOverview, recipients: Recipients) => { - const initialRecipientMap = () => { - return Object.fromEntries(recipients.map(({ recipientId }) => [recipientId, 0])); - }; +export const createMessageStatsSources = cached( + (dmId: number, messageOverview: MessageOverview, recipients: Recipients) => { + messageStatsWorker.postMessage({ + dmId, + messageOverview, + recipients, + }); - const dateList = () => { - const firstDate = messageOverview?.at(0)?.messageDate; - const lastDate = messageOverview?.at(-1)?.messageDate; - if (firstDate && lastDate) { - return getDateList(firstDate, lastDate).map((date) => ({ - totalMessages: 0, - date, - ...initialRecipientMap(), - })); - } - }; + return new Promise((resolve) => { + const listener = ( + event: MessageEvent<{ + dmId: number; + messageStatsSources: MessageStats; + }>, + ) => { + if (event.data.dmId === dmId) { + resolve(event.data.messageStatsSources); + } + }; - const currentDateList = dateList(); - const currentInitialRecipientMap = initialRecipientMap(); - - const messageStats: MessageStats = { - person: { ...currentInitialRecipientMap }, - month: initialMonthMap.map(() => ({ ...currentInitialRecipientMap })), - date: currentDateList ?? [], - weekday: initialWeekdayMap.map(() => ({ ...currentInitialRecipientMap })), - daytime: initialHoursMap.map(() => ({ ...currentInitialRecipientMap })), - }; - - if (currentDateList) { - const { person, month, date, weekday, daytime } = messageStats; - - for (const message of messageOverview) { - const { messageDate } = message; - - // increment overall message count of a person - person[message.fromRecipientId] += 1; - - // increment the message count of the message's month for this recipient - month[messageDate.getMonth()][message.fromRecipientId] += 1; - - // biome-ignore lint/style/noNonNullAssertion: - const dateStatsEntry = date.find(({ date }) => isSameDay(date, messageDate))!; - - // increment the message count of the message's date for this recipient - dateStatsEntry[message.fromRecipientId] += 1; - - // increment the overall message count of the message's date - dateStatsEntry.totalMessages += 1; - - const weekdayOfDate = messageDate.getDay(); - // we index starting with monday while the `Date` object indexes starting with Sunday - const weekdayIndex = weekdayOfDate === 0 ? 6 : weekdayOfDate - 1; - - // increment the message count of the message's weekday for this recipient - weekday[weekdayIndex][message.fromRecipientId] += 1; - - // increment the message count of the message's daytime for this recipient - daytime[messageDate.getHours()][message.fromRecipientId] += 1; - } - } - - return messageStats; -}; - -export const createMessageStatsSources = cached(createMessageStatsSourcesRaw); + messageStatsWorker.addEventListener("message", listener); + }); + }, +); diff --git a/src/pages/dm/dm-id.tsx b/src/pages/dm/dm-id.tsx index e7f2e52..e10c445 100644 --- a/src/pages/dm/dm-id.tsx +++ b/src/pages/dm/dm-id.tsx @@ -1,7 +1,7 @@ -import { type Component, createMemo, createResource } from "solid-js"; -import type { RouteSectionProps } from "@solidjs/router"; +import { Suspense, type Component } from "solid-js"; +import { createAsync, type RoutePreloadFunc, type RouteSectionProps } from "@solidjs/router"; -import { dmPartnerRecipientQuery, SELF_ID, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db"; +import { dmPartnerRecipientQuery, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db-queries"; import { getNameFromRecipient } from "~/lib/get-name-from-recipient"; import { Heading } from "~/components/ui/heading"; import { Grid } from "~/components/ui/grid"; @@ -15,13 +15,13 @@ import { DmMessagesPerRecipient } from "./dm-messages-per-recipients"; import { DmMessagesPerWeekday } from "./dm-messages-per-weekday"; import type { MessageOverview } from "~/types"; import { createMessageStatsSources } from "~/lib/messages"; +import { SELF_ID } from "~/db"; +import { Flex } from "~/components/ui/flex"; -export const DmId: Component = (props) => { - const dmId = () => Number(props.params.dmid); - +const getDmIdData = (dmId: number) => { // the other person in the chat with name and id - const [dmPartner] = createResource(async () => { - const dmPartner = await dmPartnerRecipientQuery(dmId()); + const dmPartner = createAsync(async () => { + const dmPartner = await dmPartnerRecipientQuery(dmId); if (dmPartner) { return { @@ -35,8 +35,8 @@ export const DmId: Component = (props) => { } }); - const [dmMessagesOverview] = createResource(async () => { - const dmMessageOverview = await threadSentMessagesOverviewQuery(dmId()); + const dmMessagesOverview = createAsync(async () => { + const dmMessageOverview = await threadSentMessagesOverviewQuery(dmId); if (dmMessageOverview) { return dmMessageOverview.map((row) => { return { @@ -47,7 +47,7 @@ export const DmId: Component = (props) => { } }); - const [mostUsedWordCounts] = createResource(async () => threadMostUsedWordsQuery(dmId(), 300)); + const mostUsedWordCounts = createAsync(async () => threadMostUsedWordsQuery(dmId, 300)); const recipients = () => { const currentDmPartner = dmPartner(); @@ -63,45 +63,87 @@ export const DmId: Component = (props) => { } }; - const dmMessageStats = createMemo(() => { + const dmMessageStats = createAsync(async () => { const currentDmMessagesOverview = dmMessagesOverview(); const currentRecipients = recipients(); if (currentDmMessagesOverview && currentRecipients) { - return createMessageStatsSources(currentDmMessagesOverview, currentRecipients); + return await createMessageStatsSources(dmId, currentDmMessagesOverview, currentRecipients); } }); + return { + dmPartner, + dmMessagesOverview, + mostUsedWordCounts, + recipients, + dmMessageStats, + }; +}; + +export const preloadDmId: RoutePreloadFunc = (props) => { + void getDmIdData(Number(props.params.dmid)); +}; + +export const DmId: Component = (props) => { + const { dmPartner, dmMessagesOverview, mostUsedWordCounts, recipients, dmMessageStats } = getDmIdData( + Number(props.params.dmid), + ); + return ( <> Dm with {dmPartner()?.name}
DM with {dmPartner()?.name} Chat timeline - + +

Loading...

+ + } + > + +
Messages per - -
- Person - -
-
- Daytime - -
-
- Month - -
-
- Weekday - -
-
+ +

Loading...

+ + } + > + +
+ Person + +
+
+ Daytime + +
+
+ Month + +
+
+ Weekday + +
+
+
Word cloud - + +

Loading...

+ + } + > + +
); diff --git a/src/pages/dm/dm-wordcloud.tsx b/src/pages/dm/dm-wordcloud.tsx index bd24624..5ec2b1e 100644 --- a/src/pages/dm/dm-wordcloud.tsx +++ b/src/pages/dm/dm-wordcloud.tsx @@ -1,7 +1,7 @@ import type { ChartData } from "chart.js"; import { Show, type Accessor, type Component } from "solid-js"; import { WordCloudChart } from "~/components/ui/charts"; -import type { threadMostUsedWordsQuery } from "~/db"; +import type { threadMostUsedWordsQuery } from "~/db-queries"; const maxWordSize = 100; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index df3898f..3c5b066 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,10 +1,10 @@ import { createSignal, Show, type Component, type JSX } from "solid-js"; import { type RouteSectionProps, useNavigate } from "@solidjs/router"; -import { setDb, SQL } from "~/db"; import { Portal } from "solid-js/web"; import { Flex } from "~/components/ui/flex"; import { Title } from "@solidjs/meta"; +import { setDb, SQL } from "~/db"; export const Home: Component = () => { const [isLoadingDb, setIsLoadingDb] = createSignal(false); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 82c820d..37439ba 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,6 +2,7 @@ import { lazy } from "solid-js"; export { Home } from "./home"; +export { preloadDmId } from "./dm/dm-id"; export const GroupId = lazy(() => import("./group/group-id")); export const DmId = lazy(() => import("./dm/dm-id")); diff --git a/src/pages/overview/index.tsx b/src/pages/overview/index.tsx index e87bc14..b117891 100644 --- a/src/pages/overview/index.tsx +++ b/src/pages/overview/index.tsx @@ -1,11 +1,12 @@ import { type Component, createResource, Show } from "solid-js"; import type { RouteSectionProps } from "@solidjs/router"; -import { allThreadsOverviewQuery, overallSentMessagesQuery, SELF_ID } from "~/db"; +import { allThreadsOverviewQuery, overallSentMessagesQuery } from "~/db-queries"; import { OverviewTable, type RoomOverview } from "./overview-table"; import { getNameFromRecipient } from "~/lib/get-name-from-recipient"; import { Title } from "@solidjs/meta"; +import { SELF_ID } from "~/db"; export const Overview: Component = () => { const [allSelfSentMessagesCount] = createResource(() => overallSentMessagesQuery(SELF_ID)); diff --git a/src/pages/overview/overview-table.tsx b/src/pages/overview/overview-table.tsx index 5e896fb..2d72261 100644 --- a/src/pages/overview/overview-table.tsx +++ b/src/pages/overview/overview-table.tsx @@ -1,5 +1,5 @@ import { type Component, createSignal, For, Match, Show, Switch } from "solid-js"; -import { useNavigate } from "@solidjs/router"; +import { useNavigate, usePreloadRoute } from "@solidjs/router"; import { type ColumnFiltersState, @@ -191,6 +191,8 @@ interface OverviewTableProps { } export const OverviewTable = (props: OverviewTableProps) => { + const preload = usePreloadRoute(); + const [sorting, setSorting] = createSignal([ { id: "messageCount", @@ -314,6 +316,26 @@ export const OverviewTable = (props: OverviewTableProps) => { { + const threadId = row.original.threadId; + const isGroup = row.original.isGroup; + + const preloadTimeout = setTimeout(() => { + preload(`/${isGroup ? "group" : "dm"}/${threadId.toString()}`, { + preloadData: true, + }); + }, 20); + + event.currentTarget.addEventListener( + "pointerout", + () => { + clearTimeout(preloadTimeout); + }, + { + once: true, + }, + ); + }} onClick={() => { const threadId = row.original.threadId; const isGroup = row.original.isGroup; diff --git a/tsconfig.json b/tsconfig.json index 8281cfe..059106e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "isolatedModules": true, "verbatimModuleSyntax": true, "baseUrl": ".", + "lib": ["ESNext", "DOM", "WebWorker"], "paths": { "~/*": ["./src/*"] } diff --git a/vite.config.ts b/vite.config.ts index cbe9833..bf84311 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,4 +15,7 @@ export default defineConfig({ "~": path.resolve(__dirname, "./src"), }, }, + worker: { + format: "es", + }, });