● Zion Boggan zionboggan.com ↗

Fix load hang: WAL + read-write DB connection (was delete-journal + readonly -> writer blocked readers); render-cap heavy pages (register/votes/bills/member); disable heavy 30min watcher

7dd3ab5   Zion Boggan committed on Jun 26, 2026 (1 month ago)
app/bills/page.tsx +3 -2
@@ -29,12 +29,13 @@ export default function BillsPage({
const congressRaw = asString(searchParams.congress);
const congressNum = parseInt(congressRaw ?? "", 10);
- const bills = getBills({
+ const allBills = getBills({
policyArea,
q,
congress: Number.isFinite(congressNum) ? congressNum : undefined,
sort: "updated",
});
+ const bills = allBills.slice(0, 100); // render cap; full count in eyebrow
const policyAreas = getPolicyAreaLabels();
const congresses = getBillCongresses();
@@ -130,7 +131,7 @@ export default function BillsPage({
<div className="mt-8">
<SectionHeader
- eyebrow={`${bills.length} bill${bills.length === 1 ? "" : "s"}`}
+ eyebrow={`${allBills.length} bill${allBills.length === 1 ? "" : "s"} · showing ${bills.length}`}
title={policyArea ? policyArea : "On record"}
/>
{bills.length === 0 ? (
app/register/page.tsx +8 -2
@@ -33,8 +33,14 @@ const ACTION_LABEL: Record<FeedItem["kind"], string> = {
apportionment: "Apportionments",
};
+// Render cap: the full ranked feed can be tens of thousands of rows; render
+// the top slice (it's already rank-ordered) so the page stays fast. Counts/
+// stats below still use the full set.
+const FEED_RENDER_LIMIT = 150;
+
export default async function RegisterPage() {
const items = getFeedItems();
+ const shown = items.slice(0, FEED_RENDER_LIMIT);
const awards = getAwards();
const changes = getChangeEvents(12);
const topTopics = getCategories()
@@ -45,7 +51,7 @@ export default async function RegisterPage() {
return (
<>
- <RegisterHero seed={items} />
+ <RegisterHero seed={shown.slice(0, 12)} />
<div className="mx-auto w-full max-w-register px-4 py-12 sm:px-6">
<SectionHeader
@@ -80,7 +86,7 @@ export default async function RegisterPage() {
</tr>
</thead>
<tbody>
- {items.map((r) => (
+ {shown.map((r) => (
<tr
key={r.id}
className="border-b border-line font-mono text-[12px] transition-colors last:border-0 hover:bg-graphite/60"
app/votes/page.tsx +2 -1
@@ -16,6 +16,7 @@ export const dynamic = "force-dynamic";
export default function VotesPage() {
const votes = getVotes();
+ const shown = votes.slice(0, 100); // render cap; full count shown in eyebrow
return (
<div className="mx-auto w-full max-w-register px-4 py-10 sm:px-6">
@@ -54,7 +55,7 @@ export default function VotesPage() {
</tr>
</thead>
<tbody>
- {votes.map((v) => {
+ {shown.map((v) => {
const variant = classifyStatus(v.result);
return (
<tr
lib/db.ts +11 -3
@@ -16,16 +16,24 @@ export const DB_PATH =
let _db: Database.Database | null = null;
-/** Single shared read-only connection. */
+/**
+ * Single shared connection. Opened read-WRITE (not readonly) on purpose:
+ * the DB is in WAL mode (so ingest can write while we read without blocking),
+ * and a read-only SQLite handle cannot operate on a WAL database because WAL
+ * requires writing the shared-memory index. This layer only ever issues
+ * SELECTs, so it behaves as a WAL *reader* - it never writes data. A
+ * busy_timeout means a read waits briefly for a checkpoint instead of the
+ * synchronous driver hanging the Node event loop.
+ */
function db(): Database.Database {
if (!_db) {
_db = new Database(DB_PATH, {
- readonly: true,
+ readonly: false,
fileMustExist: false,
});
- // Small perf wins; we only read.
try {
_db.pragma("journal_mode = WAL");
+ _db.pragma("busy_timeout = 4000");
} catch {
/* ignore */
}
lib/feed.ts +1 -1
@@ -1043,7 +1043,7 @@ export function getMember(rawId: string): MemberDetail | null {
JOIN votes v ON v.id = vp.vote_id
WHERE vp.member_id = :mid
ORDER BY v.date DESC, v.id DESC
- LIMIT 5000`,
+ LIMIT 1500`,
{ mid: summary.numericId },
(v) => ({
voteId: `vote:${v.id}`,