Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/web/app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getPapersConfig } from "@papers/config"
import { pingDatabase } from "@papers/db"
import { getCacheStats, pingDatabase } from "@papers/db"
import { NextResponse } from "next/server"

export const dynamic = "force-dynamic"
Expand All @@ -26,6 +26,7 @@ export async function GET() {
status: allOk ? "ok" : "degraded",
demoMode,
checks,
cache: getCacheStats(),
},
{ status: allOk ? 200 : 503 },
)
Expand Down
127 changes: 1 addition & 126 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@papers/config": "0.1.0",
"@papers/db": "0.1.0",
"better-auth": "^1.5.6",
"drizzle-orm": "^0.44.5",
"drizzle-orm": "^0.45.1",
"pg": "^8.16.3"
}
}
49 changes: 49 additions & 0 deletions packages/db/src/cache-counter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { AsyncLocalStorage } from "node:async_hooks"
import type { DemoState } from "./demo-store"

export type CacheStats = {
hits: number
misses: number
}

type CacheStore = {
hits: number
misses: number
state: DemoState | null
}

const storage = new AsyncLocalStorage<CacheStore>()

export function runWithCacheCounter<T>(fn: () => T): T {
return storage.run({ hits: 0, misses: 0, state: null }, fn)
}

export function getCacheStats(): CacheStats | null {
const store = storage.getStore()
if (!store) return null
return { hits: store.hits, misses: store.misses }
}

export function getCachedState(): DemoState | null {
return storage.getStore()?.state ?? null
}

export function setCachedState(state: DemoState): void {
const store = storage.getStore()
if (store) store.state = state
}

export function recordHit(): void {
const store = storage.getStore()
if (store) store.hits++
}

export function recordMiss(): void {
const store = storage.getStore()
if (store) store.misses++
}

export function invalidateCachedState(): void {
const store = storage.getStore()
if (store) store.state = null
}
17 changes: 17 additions & 0 deletions packages/db/src/demo-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ import type {
User,
} from "@papers/contracts"
import { serializePublicPaper, slugify } from "@papers/contracts"
import {
getCachedState,
invalidateCachedState,
recordHit,
recordMiss,
setCachedState,
} from "./cache-counter"

export type DemoState = {
users: User[]
Expand Down Expand Up @@ -745,6 +752,13 @@ function getStorePath(): string {
}

export async function readDemoState(): Promise<DemoState> {
const cached = getCachedState()
if (cached) {
recordHit()
return cached
}
recordMiss()

const filePath = getStorePath()
await mkdir(path.dirname(filePath), { recursive: true })

Expand Down Expand Up @@ -783,17 +797,20 @@ export async function readDemoState(): Promise<DemoState> {
: initial.groupReadingListItems,
}
await writeFile(filePath, JSON.stringify(normalized, null, 2))
setCachedState(normalized)
return normalized
} catch {
const initial = createInitialState()
await writeFile(filePath, JSON.stringify(initial, null, 2))
setCachedState(initial)
return initial
}
}

export async function writeDemoState(state: DemoState): Promise<void> {
await mkdir(path.dirname(getStorePath()), { recursive: true })
await writeFile(getStorePath(), JSON.stringify(state, null, 2))
invalidateCachedState()
}

export function getPublicComments(comments: Comment[], paper: Paper): Comment[] {
Expand Down
3 changes: 3 additions & 0 deletions packages/db/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ import {
readDemoState,
writeDemoState,
} from "./demo-store"

export { type CacheStats, getCacheStats, runWithCacheCounter } from "./cache-counter"

import {
authAccounts,
authSessions,
Expand Down
61 changes: 61 additions & 0 deletions packages/db/test/cache-counter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest"
import {
getCachedState,
getCacheStats,
invalidateCachedState,
recordHit,
recordMiss,
runWithCacheCounter,
setCachedState,
} from "../src/cache-counter"

describe("cache counter", () => {
it("returns null outside a request scope", () => {
expect(getCacheStats()).toBeNull()
})

it("tracks hits and misses within a scope", async () => {
await runWithCacheCounter(async () => {
recordMiss()
recordHit()
recordHit()

const stats = getCacheStats()
expect(stats).toEqual({ hits: 2, misses: 1 })
})
})

it("isolates counters between scopes", async () => {
await runWithCacheCounter(async () => {
recordMiss()
expect(getCacheStats()).toEqual({ hits: 0, misses: 1 })
})

await runWithCacheCounter(async () => {
recordMiss()
recordHit()
expect(getCacheStats()).toEqual({ hits: 1, misses: 1 })
})
})

it("caches and retrieves state within a scope", async () => {
await runWithCacheCounter(async () => {
expect(getCachedState()).toBeNull()

const fakeState = { users: [] } as never
setCachedState(fakeState)
expect(getCachedState()).toBe(fakeState)
})
})

it("invalidates cached state within a scope", async () => {
await runWithCacheCounter(async () => {
const fakeState = { users: [] } as never
setCachedState(fakeState)
expect(getCachedState()).toBe(fakeState)

invalidateCachedState()
expect(getCachedState()).toBeNull()
})
})
})