tokentrace 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/app/models/page.tsx +1 -1
  3. package/app/page.tsx +72 -47
  4. package/app/projects/page.tsx +1 -1
  5. package/app/tools/page.tsx +1 -1
  6. package/components/charts/rank-bar-chart-lazy.tsx +9 -0
  7. package/components/charts/skeleton.tsx +16 -0
  8. package/components/charts/trend-section-lazy.tsx +9 -0
  9. package/components/overview/current-mix-panel.tsx +1 -1
  10. package/components/overview/section-skeletons.tsx +35 -0
  11. package/dist/runtime/agent.mjs +29 -6
  12. package/dist/runtime/db-migrate.mjs +5 -0
  13. package/dist/runtime/db-seed.mjs +5 -0
  14. package/dist/runtime/digest.mjs +25 -2
  15. package/dist/runtime/doctor.mjs +81 -19
  16. package/dist/runtime/evidence.mjs +5 -0
  17. package/dist/runtime/insights.mjs +25 -2
  18. package/dist/runtime/pricing-refresh.mjs +27 -4
  19. package/dist/runtime/repair.mjs +27 -4
  20. package/dist/runtime/report.mjs +30 -6
  21. package/dist/runtime/reset.mjs +5 -0
  22. package/dist/runtime/review.mjs +25 -2
  23. package/dist/runtime/scan.mjs +152 -102
  24. package/dist/runtime/status.mjs +5 -0
  25. package/next.config.mjs +28 -5
  26. package/package.json +1 -1
  27. package/scripts/doctor.ts +38 -11
  28. package/server.json +2 -2
  29. package/src/db/client.ts +5 -0
  30. package/src/db/prepared.ts +17 -0
  31. package/src/ingestion/discovery.ts +2 -5
  32. package/src/ingestion/persist.ts +12 -3
  33. package/src/ingestion/scan-adapters.ts +11 -0
  34. package/src/ingestion/scan-files.ts +35 -24
  35. package/src/ingestion/scan.ts +6 -8
  36. package/src/lib/analytics-query-helpers.ts +2 -2
  37. package/src/lib/doctor-cli.ts +45 -0
  38. package/src/lib/overview-data.ts +140 -46
  39. package/src/lib/scheduled-scan.ts +4 -4
  40. package/src/lib/source-catalog.ts +5 -3
  41. package/src/lib/unknown-cost-repair/suggestions.ts +2 -2
  42. package/src/lib/unknown-cost-repair/workbench.ts +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,64 @@ All notable changes to TokenTrace are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.17.0] - 2026-05-23
8
+
9
+ ### Performance
10
+
11
+ - **Runtime SQLite pragmas tuned for analytics.** `src/db/client.ts` now
12
+ re-applies `journal_mode=WAL` on the live connection and sets
13
+ `synchronous=NORMAL`, `temp_store=MEMORY`, `cache_size=64MB`, and
14
+ `mmap_size=256MB`. Previously only `busy_timeout` and `foreign_keys`
15
+ were set on the runtime connection.
16
+ - **Prepared-statement cache.** `src/db/prepared.ts` adds a tiny
17
+ `prepareCached(sql)` helper keyed by SQL string. The hot analytics,
18
+ unknown-cost-repair, scheduled-scan, and ingestion helpers now skip
19
+ the parse-and-plan cost on repeat queries.
20
+ - **Overview page parallelization.** The independent sub-queries in
21
+ `getOverviewData` (analytics, accounting invariants, scan diff,
22
+ default search roots, repair workbench) now run through `Promise.all`,
23
+ so the async filesystem walk overlaps with the serialized SQLite
24
+ reads.
25
+ - **Render-scoped overview memo.** `getOverviewPageData` is wrapped in
26
+ `React.cache` so any future composition that calls it twice within a
27
+ single server render tree pays the cost once.
28
+ - **Lazy-loaded Recharts.** `TrendSection` and `RankBarChart` are now
29
+ loaded via `next/dynamic({ ssr: false })` on the overview, projects,
30
+ tools, and models routes, splitting the ~80KB Recharts bundle out of
31
+ the initial JS payload. `ChartSkeleton` keeps the chart slot from
32
+ collapsing during client hydration.
33
+ - **Streaming overview with two `<Suspense>` boundaries.** The overview
34
+ page now splits into two cache-wrapped fetchers — `getOverviewPrimaryData`
35
+ (analytics-driven: pulse, summary, trend, trust, guardrails,
36
+ recommendations, mix) and `getOverviewRepairData` (workbench-driven:
37
+ review status, repair items). Each renders inside its own `<Suspense>`
38
+ so the page shell and period filter paint immediately, the primary
39
+ analytics block streams in next, and the repair lane streams in
40
+ independently when the workbench query resolves.
41
+ - **`tokentrace doctor --timings`.** New flag force-enables analytics
42
+ timing capture and emits the analytics timing report (slow queries,
43
+ threshold, sample list). Combine with `--json` for machine-readable
44
+ output. Useful for before/after measurement of performance changes
45
+ since `TOKENTRACE_ANALYTICS_TIMING` is off by default in production.
46
+ - **Scan ingestion throughput.** Adds a (path, size, mtime)-keyed
47
+ file-hash cache so rescans of unchanged files skip the `fs.readFile`
48
+ + SHA-256 step entirely. The hot scan-side `INSERT INTO scan_files`,
49
+ `INSERT INTO scan_runs`, and `hasImportedFile` lookups also route
50
+ through `prepareCached`.
51
+ - **Next bundle optimizations.** Enables `optimizePackageImports` for
52
+ `lucide-react` (~37 import sites) and `recharts` so Next 16 transforms
53
+ named imports into per-symbol imports and prunes unused-symbol weight
54
+ from the client bundle. Adds an opt-in `@next/bundle-analyzer`
55
+ integration gated on `ANALYZE=true` for follow-up audits.
56
+ - **Hot-path bug-hunt fixes.** `source-catalog.summarizeSourceCoverage`
57
+ was O(rows × entries); now uses a pre-built Map for O(1) parser tier
58
+ lookups. `ingestion/discovery.getDefaultSearchRoots` parallelizes its
59
+ `fs.access` checks via `Promise.all` instead of awaiting them
60
+ sequentially. `ingestion/persist.findProjectRoot` memoizes the
61
+ resolved project root by start directory, eliminating duplicate
62
+ filesystem walks when many imported sessions share the same source
63
+ directory.
64
+
7
65
  ## [0.16.0] - 2026-05-23
8
66
 
9
67
  ### Added
@@ -3,7 +3,7 @@ import { EmptyState } from "@/components/empty-state";
3
3
  import { ScanNowButton } from "@/components/scan-now-button";
4
4
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
5
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
6
- import { RankBarChart } from "@/components/charts/rank-bar-chart";
6
+ import { RankBarChart } from "@/components/charts/rank-bar-chart-lazy";
7
7
  import { PageHeader } from "@/components/ui/typography";
8
8
  import { getAnalyticsData } from "@/src/lib/analytics";
9
9
  import { formatCurrency, formatTokens } from "@/src/lib/format";
package/app/page.tsx CHANGED
@@ -1,12 +1,17 @@
1
+ import { Suspense } from "react";
1
2
  import Link from "next/link";
2
3
  import { ArrowRight } from "lucide-react";
3
- import { TrendSection } from "@/components/charts/trend-section";
4
+ import { TrendSection } from "@/components/charts/trend-section-lazy";
4
5
  import { OverviewCurrentMixPanel } from "@/components/overview/current-mix-panel";
5
6
  import { DataConfidenceStrip } from "@/components/overview/data-confidence-strip";
6
7
  import { FirstRunPanel } from "@/components/overview/first-run-panel";
7
8
  import { UsageGuardrailsPanel } from "@/components/overview/guardrails-panel";
8
9
  import { OverviewRecommendationsCard } from "@/components/overview/recommendations-card";
9
10
  import { OverviewReviewStatusStrip } from "@/components/overview/review-status-strip";
11
+ import {
12
+ OverviewPrimarySkeleton,
13
+ OverviewRepairSkeleton
14
+ } from "@/components/overview/section-skeletons";
10
15
  import { CostSessionsCard, TokenAccountingCard } from "@/components/overview/summary-cards";
11
16
  import { TopRepairItemsStrip } from "@/components/overview/top-repair-items-strip";
12
17
  import { OverviewTrustFooter } from "@/components/overview/trust-footer";
@@ -14,9 +19,9 @@ import { UsagePulsePanel } from "@/components/overview/usage-pulse-panel";
14
19
  import { PeriodFilter } from "@/components/period-filter";
15
20
  import { Button } from "@/components/ui/button";
16
21
  import { PageHeader } from "@/components/ui/typography";
17
- import { resolveDateRange } from "@/src/lib/date-range";
22
+ import { mergeHrefParams, type ResolvedDateRange, resolveDateRange } from "@/src/lib/date-range";
18
23
  import { runDueScheduledScan } from "@/src/lib/scheduled-scan";
19
- import { getOverviewPageData } from "@/src/lib/overview-data";
24
+ import { getOverviewPrimaryData, getOverviewRepairData } from "@/src/lib/overview-data";
20
25
 
21
26
  export const dynamic = "force-dynamic";
22
27
 
@@ -24,51 +29,24 @@ type OverviewPageProps = {
24
29
  searchParams?: Promise<Record<string, string | string[] | undefined>>;
25
30
  };
26
31
 
27
- export default async function OverviewPage({ searchParams }: OverviewPageProps) {
28
- void runDueScheduledScan().catch(() => undefined);
29
- const params = (await searchParams) ?? {};
30
- const range = resolveDateRange(params);
31
- const overview = await getOverviewPageData(range);
32
+ async function OverviewPrimarySection({ range }: { range: ResolvedDateRange }) {
32
33
  const {
33
34
  data,
34
35
  trust,
35
- accountingReport,
36
- postSessionReview,
37
- rangeLinkParams,
38
36
  evidenceLinks,
39
- doctorReport,
40
- repairWorkbench,
41
- repairFocusHref,
42
- unknownCostEvidenceHref,
43
37
  firstRunStatus,
44
38
  summary,
45
- trendDefaultWindow
46
- } = overview;
39
+ trendDefaultWindow,
40
+ unknownCostEvidenceHref,
41
+ rangeLinkParams
42
+ } = await getOverviewPrimaryData(range);
43
+ const repairFocusHref = mergeHrefParams("/repair", rangeLinkParams);
47
44
 
48
45
  return (
49
46
  <div className="space-y-8">
50
- <PageHeader
51
- title="Overview"
52
- description="Local token, cost, model, and session analytics across AI CLI tools."
53
- actions={
54
- <Button asChild>
55
- <Link href="/settings#scan-controls">
56
- Configure scan <ArrowRight className="h-4 w-4" />
57
- </Link>
58
- </Button>
59
- }
60
- />
61
-
62
- <PeriodFilter range={range} />
63
-
64
- {summary.interactions === 0 ? (
65
- <FirstRunPanel status={firstRunStatus} />
66
- ) : null}
67
-
47
+ {summary.interactions === 0 ? <FirstRunPanel status={firstRunStatus} /> : null}
68
48
  <UsagePulsePanel comparison={data.comparison} />
69
-
70
49
  <DataConfidenceStrip confidence={data.dataConfidence} />
71
-
72
50
  <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-6">
73
51
  <TokenAccountingCard
74
52
  summary={summary}
@@ -84,11 +62,38 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
84
62
  sessionsHref={evidenceLinks.sessions}
85
63
  />
86
64
  </div>
87
-
88
65
  <OverviewTrustFooter health={trust.health} pricedModelCount={trust.pricedModelCount} />
89
-
90
66
  <TrendSection data={data.trends} defaultWindow={trendDefaultWindow} />
67
+ <UsageGuardrailsPanel progress={data.usageGuardrails} />
68
+ <OverviewRecommendationsCard recommendations={data.recommendations} />
69
+ <OverviewCurrentMixPanel
70
+ tools={data.tools}
71
+ mostUsedTool={summary.mostUsedTool}
72
+ mostUsedModel={summary.mostUsedModel}
73
+ />
74
+ </div>
75
+ );
76
+ }
77
+
78
+ async function OverviewRepairSection({ range }: { range: ResolvedDateRange }) {
79
+ const {
80
+ accountingReport,
81
+ postSessionReview,
82
+ doctorReport,
83
+ repairWorkbench,
84
+ repairFocusHref,
85
+ evidenceLinks,
86
+ summary,
87
+ trust,
88
+ rangeLinkParams
89
+ } = await getOverviewRepairData(range);
91
90
 
91
+ if (summary.interactions === 0 && repairWorkbench.groups.length === 0) {
92
+ return null;
93
+ }
94
+
95
+ return (
96
+ <div className="space-y-8">
92
97
  {summary.interactions > 0 ? (
93
98
  <OverviewReviewStatusStrip
94
99
  report={doctorReport}
@@ -102,7 +107,6 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
102
107
  cachedEvidenceHref={evidenceLinks["cached-tokens"]}
103
108
  />
104
109
  ) : null}
105
-
106
110
  {repairWorkbench.groups.length ? (
107
111
  <TopRepairItemsStrip
108
112
  groups={repairWorkbench.groups}
@@ -110,17 +114,38 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
110
114
  rangeLinkParams={rangeLinkParams}
111
115
  />
112
116
  ) : null}
117
+ </div>
118
+ );
119
+ }
113
120
 
114
- <UsageGuardrailsPanel progress={data.usageGuardrails} />
115
-
116
- <OverviewRecommendationsCard recommendations={data.recommendations} />
121
+ export default async function OverviewPage({ searchParams }: OverviewPageProps) {
122
+ void runDueScheduledScan().catch(() => undefined);
123
+ const params = (await searchParams) ?? {};
124
+ const range = resolveDateRange(params);
117
125
 
118
- <OverviewCurrentMixPanel
119
- tools={data.tools}
120
- mostUsedTool={summary.mostUsedTool}
121
- mostUsedModel={summary.mostUsedModel}
126
+ return (
127
+ <div className="space-y-8">
128
+ <PageHeader
129
+ title="Overview"
130
+ description="Local token, cost, model, and session analytics across AI CLI tools."
131
+ actions={
132
+ <Button asChild>
133
+ <Link href="/settings#scan-controls">
134
+ Configure scan <ArrowRight className="h-4 w-4" />
135
+ </Link>
136
+ </Button>
137
+ }
122
138
  />
123
139
 
140
+ <PeriodFilter range={range} />
141
+
142
+ <Suspense fallback={<OverviewPrimarySkeleton />}>
143
+ <OverviewPrimarySection range={range} />
144
+ </Suspense>
145
+
146
+ <Suspense fallback={<OverviewRepairSkeleton />}>
147
+ <OverviewRepairSection range={range} />
148
+ </Suspense>
124
149
  </div>
125
150
  );
126
151
  }
@@ -1,5 +1,5 @@
1
1
  import Link from "next/link";
2
- import { RankBarChart } from "@/components/charts/rank-bar-chart";
2
+ import { RankBarChart } from "@/components/charts/rank-bar-chart-lazy";
3
3
  import { TrendChart } from "@/components/charts/trend-chart";
4
4
  import { EmptyState } from "@/components/empty-state";
5
5
  import { ScanNowButton } from "@/components/scan-now-button";
@@ -1,4 +1,4 @@
1
- import { RankBarChart } from "@/components/charts/rank-bar-chart";
1
+ import { RankBarChart } from "@/components/charts/rank-bar-chart-lazy";
2
2
  import { EmptyState } from "@/components/empty-state";
3
3
  import { ScanNowButton } from "@/components/scan-now-button";
4
4
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+ import { ChartSkeleton } from "@/components/charts/skeleton";
5
+
6
+ export const RankBarChart = dynamic(
7
+ () => import("./rank-bar-chart").then((m) => m.RankBarChart),
8
+ { ssr: false, loading: () => <ChartSkeleton heightClass="h-60" label="Loading chart…" /> }
9
+ );
@@ -0,0 +1,16 @@
1
+ type ChartSkeletonProps = {
2
+ heightClass?: string;
3
+ label?: string;
4
+ };
5
+
6
+ export function ChartSkeleton({ heightClass = "h-64", label = "Loading chart…" }: ChartSkeletonProps) {
7
+ return (
8
+ <div
9
+ className={`flex items-center justify-center rounded-md border border-dashed border-muted-foreground/30 bg-muted/30 text-xs text-muted-foreground ${heightClass}`}
10
+ role="status"
11
+ aria-live="polite"
12
+ >
13
+ {label}
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+ import { ChartSkeleton } from "@/components/charts/skeleton";
5
+
6
+ export const TrendSection = dynamic(
7
+ () => import("./trend-section").then((m) => m.TrendSection),
8
+ { ssr: false, loading: () => <ChartSkeleton heightClass="h-72" label="Loading trend chart…" /> }
9
+ );
@@ -1,5 +1,5 @@
1
1
  import Link from "next/link";
2
- import { RankBarChart } from "@/components/charts/rank-bar-chart";
2
+ import { RankBarChart } from "@/components/charts/rank-bar-chart-lazy";
3
3
  import { Badge } from "@/components/ui/badge";
4
4
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
5
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
@@ -0,0 +1,35 @@
1
+ type SectionSkeletonProps = {
2
+ heightClass?: string;
3
+ label: string;
4
+ };
5
+
6
+ export function OverviewSectionSkeleton({ heightClass = "h-48", label }: SectionSkeletonProps) {
7
+ return (
8
+ <div
9
+ className={`flex items-center justify-center rounded-md border border-dashed border-muted-foreground/30 bg-muted/30 text-xs text-muted-foreground ${heightClass}`}
10
+ role="status"
11
+ aria-live="polite"
12
+ >
13
+ {label}
14
+ </div>
15
+ );
16
+ }
17
+
18
+ export function OverviewPrimarySkeleton() {
19
+ return (
20
+ <div className="space-y-6">
21
+ <OverviewSectionSkeleton heightClass="h-20" label="Loading usage pulse…" />
22
+ <OverviewSectionSkeleton heightClass="h-36" label="Loading summary cards…" />
23
+ <OverviewSectionSkeleton heightClass="h-72" label="Loading trend chart…" />
24
+ </div>
25
+ );
26
+ }
27
+
28
+ export function OverviewRepairSkeleton() {
29
+ return (
30
+ <div className="space-y-6">
31
+ <OverviewSectionSkeleton heightClass="h-24" label="Loading review status…" />
32
+ <OverviewSectionSkeleton heightClass="h-40" label="Loading repair items…" />
33
+ </div>
34
+ );
35
+ }
@@ -655,6 +655,11 @@ var init_client = __esm({
655
655
  dbPath = process.env.TOKENTRACE_DB ?? databaseUrlPath(process.env.DATABASE_URL) ?? defaultDbPath;
656
656
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
657
657
  sqlite = new Database(dbPath);
658
+ sqlite.pragma("journal_mode = WAL");
659
+ sqlite.pragma("synchronous = NORMAL");
660
+ sqlite.pragma("temp_store = MEMORY");
661
+ sqlite.pragma("cache_size = -65536");
662
+ sqlite.pragma("mmap_size = 268435456");
658
663
  sqlite.pragma("busy_timeout = 10000");
659
664
  sqlite.pragma("foreign_keys = ON");
660
665
  registerSqliteFunctions(sqlite);
@@ -765,6 +770,24 @@ var init_agent_actions = __esm({
765
770
  }
766
771
  });
767
772
 
773
+ // src/db/prepared.ts
774
+ function prepareCached(sql2) {
775
+ let stmt = cache.get(sql2);
776
+ if (!stmt) {
777
+ stmt = sqlite.prepare(sql2);
778
+ cache.set(sql2, stmt);
779
+ }
780
+ return stmt;
781
+ }
782
+ var cache;
783
+ var init_prepared = __esm({
784
+ "src/db/prepared.ts"() {
785
+ "use strict";
786
+ init_client();
787
+ cache = /* @__PURE__ */ new Map();
788
+ }
789
+ });
790
+
768
791
  // src/lib/analytics-query-helpers.ts
769
792
  function number(value) {
770
793
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
@@ -822,7 +845,7 @@ function parseJson(value, fallback) {
822
845
  }
823
846
  }
824
847
  function rows(sql2, ...params) {
825
- return sqlite.prepare(sql2).all(...params);
848
+ return prepareCached(sql2).all(...params);
826
849
  }
827
850
  function withQuery(path3, params) {
828
851
  const query = new URLSearchParams();
@@ -877,7 +900,7 @@ function timestampJoinCondition(filters = {}, alias = "i") {
877
900
  var init_analytics_query_helpers = __esm({
878
901
  "src/lib/analytics-query-helpers.ts"() {
879
902
  "use strict";
880
- init_client();
903
+ init_prepared();
881
904
  }
882
905
  });
883
906
 
@@ -3419,7 +3442,7 @@ var init_repair_actions = __esm({
3419
3442
 
3420
3443
  // src/lib/unknown-cost-repair/suggestions.ts
3421
3444
  function rows2(sql2, ...params) {
3422
- return sqlite.prepare(sql2).all(...params);
3445
+ return prepareCached(sql2).all(...params);
3423
3446
  }
3424
3447
  function buildPricedModelLookup() {
3425
3448
  const pricedRows = rows2(
@@ -3493,7 +3516,7 @@ function aliasSuggestion({
3493
3516
  var init_suggestions = __esm({
3494
3517
  "src/lib/unknown-cost-repair/suggestions.ts"() {
3495
3518
  "use strict";
3496
- init_client();
3519
+ init_prepared();
3497
3520
  init_model_aliases();
3498
3521
  }
3499
3522
  });
@@ -3503,7 +3526,7 @@ function number3(value) {
3503
3526
  return Number(value ?? 0);
3504
3527
  }
3505
3528
  function rows3(sql2, ...params) {
3506
- return sqlite.prepare(sql2).all(...params);
3529
+ return prepareCached(sql2).all(...params);
3507
3530
  }
3508
3531
  function interactionDateFilter(filters = {}, alias = "i") {
3509
3532
  const clauses = [];
@@ -3691,7 +3714,7 @@ function buildUnknownCostRepairWorkbench(filters = {}, options = {}) {
3691
3714
  var init_workbench = __esm({
3692
3715
  "src/lib/unknown-cost-repair/workbench.ts"() {
3693
3716
  "use strict";
3694
- init_client();
3717
+ init_prepared();
3695
3718
  init_repair_actions();
3696
3719
  init_keys();
3697
3720
  init_reviews();
@@ -628,6 +628,11 @@ function databaseUrlPath(value) {
628
628
  var dbPath = process.env.TOKENTRACE_DB ?? databaseUrlPath(process.env.DATABASE_URL) ?? defaultDbPath;
629
629
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
630
630
  var sqlite = new Database(dbPath);
631
+ sqlite.pragma("journal_mode = WAL");
632
+ sqlite.pragma("synchronous = NORMAL");
633
+ sqlite.pragma("temp_store = MEMORY");
634
+ sqlite.pragma("cache_size = -65536");
635
+ sqlite.pragma("mmap_size = 268435456");
631
636
  sqlite.pragma("busy_timeout = 10000");
632
637
  sqlite.pragma("foreign_keys = ON");
633
638
  registerSqliteFunctions(sqlite);
@@ -628,6 +628,11 @@ function databaseUrlPath(value) {
628
628
  var dbPath = process.env.TOKENTRACE_DB ?? databaseUrlPath(process.env.DATABASE_URL) ?? defaultDbPath;
629
629
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
630
630
  var sqlite = new Database(dbPath);
631
+ sqlite.pragma("journal_mode = WAL");
632
+ sqlite.pragma("synchronous = NORMAL");
633
+ sqlite.pragma("temp_store = MEMORY");
634
+ sqlite.pragma("cache_size = -65536");
635
+ sqlite.pragma("mmap_size = 268435456");
631
636
  sqlite.pragma("busy_timeout = 10000");
632
637
  sqlite.pragma("foreign_keys = ON");
633
638
  registerSqliteFunctions(sqlite);
@@ -655,6 +655,11 @@ var init_client = __esm({
655
655
  dbPath = process.env.TOKENTRACE_DB ?? databaseUrlPath(process.env.DATABASE_URL) ?? defaultDbPath;
656
656
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
657
657
  sqlite = new Database(dbPath);
658
+ sqlite.pragma("journal_mode = WAL");
659
+ sqlite.pragma("synchronous = NORMAL");
660
+ sqlite.pragma("temp_store = MEMORY");
661
+ sqlite.pragma("cache_size = -65536");
662
+ sqlite.pragma("mmap_size = 268435456");
658
663
  sqlite.pragma("busy_timeout = 10000");
659
664
  sqlite.pragma("foreign_keys = ON");
660
665
  registerSqliteFunctions(sqlite);
@@ -663,6 +668,24 @@ var init_client = __esm({
663
668
  }
664
669
  });
665
670
 
671
+ // src/db/prepared.ts
672
+ function prepareCached(sql2) {
673
+ let stmt = cache.get(sql2);
674
+ if (!stmt) {
675
+ stmt = sqlite.prepare(sql2);
676
+ cache.set(sql2, stmt);
677
+ }
678
+ return stmt;
679
+ }
680
+ var cache;
681
+ var init_prepared = __esm({
682
+ "src/db/prepared.ts"() {
683
+ "use strict";
684
+ init_client();
685
+ cache = /* @__PURE__ */ new Map();
686
+ }
687
+ });
688
+
666
689
  // src/lib/analytics-query-helpers.ts
667
690
  function number(value) {
668
691
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
@@ -720,7 +743,7 @@ function parseJson(value, fallback) {
720
743
  }
721
744
  }
722
745
  function rows(sql2, ...params) {
723
- return sqlite.prepare(sql2).all(...params);
746
+ return prepareCached(sql2).all(...params);
724
747
  }
725
748
  function withQuery(path2, params) {
726
749
  const query = new URLSearchParams();
@@ -775,7 +798,7 @@ function timestampJoinCondition(filters = {}, alias = "i") {
775
798
  var init_analytics_query_helpers = __esm({
776
799
  "src/lib/analytics-query-helpers.ts"() {
777
800
  "use strict";
778
- init_client();
801
+ init_prepared();
779
802
  }
780
803
  });
781
804