← Public packages
@kentcdodds/audible
Personal Audible library app — browser re-auth, library, wishlist, NAS archive stub.
src/nas-status-cache-policy.ts
109 lines · 2.8 KB · TypeScript/** Pure SWR policy helpers for NAS status cache (no kody:runtime). */
export const NAS_SOFT_TTL_MS = 60 * 1000
export const NAS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000
export type NasStatusCacheEntry = {
onNas: boolean
nasFilename: string
checkedAt: number
title: string
asin?: string
}
export type NasStatusCacheStore = {
version: 1
byTitle: Record<string, NasStatusCacheEntry>
byAsin: Record<string, string>
}
export type NasCacheLookup = {
entry: NasStatusCacheEntry
ageMs: number
fresh: boolean
stale: boolean
expired: boolean
}
export function emptyNasStatusCacheStore(): NasStatusCacheStore {
return { version: 1, byTitle: {}, byAsin: {} }
}
export function titleCacheKey(title: string): string {
return title.trim().toLowerCase()
}
export function classifyCacheAge(
checkedAt: number,
now = Date.now(),
softTtlMs = NAS_SOFT_TTL_MS,
maxAgeMs = NAS_MAX_AGE_MS,
): { ageMs: number; fresh: boolean; stale: boolean; expired: boolean } {
const ageMs = Math.max(0, now - checkedAt)
const fresh = ageMs <= softTtlMs
const expired = ageMs > maxAgeMs
const stale = !fresh && !expired
return { ageMs, fresh, stale, expired }
}
export function coerceNasStatusCacheStore(raw: unknown): NasStatusCacheStore {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
return emptyNasStatusCacheStore()
}
const obj = raw as Partial<NasStatusCacheStore>
if (obj.version !== 1) return emptyNasStatusCacheStore()
return {
version: 1,
byTitle:
obj.byTitle && typeof obj.byTitle === 'object' && !Array.isArray(obj.byTitle)
? (obj.byTitle as Record<string, NasStatusCacheEntry>)
: {},
byAsin:
obj.byAsin && typeof obj.byAsin === 'object' && !Array.isArray(obj.byAsin)
? (obj.byAsin as Record<string, string>)
: {},
}
}
export function lookupNasStatusCache(
store: NasStatusCacheStore,
row: { asin?: string; title: string },
now = Date.now(),
): NasCacheLookup | null {
const titleKey = titleCacheKey(row.title)
let entry = store.byTitle[titleKey]
if (!entry && row.asin) {
const mapped = store.byAsin[row.asin.trim()]
if (mapped) entry = store.byTitle[mapped]
}
if (!entry || typeof entry.checkedAt !== 'number') return null
const age = classifyCacheAge(entry.checkedAt, now)
if (age.expired) return null
return { entry, ...age }
}
export function putNasStatusCacheEntry(
store: NasStatusCacheStore,
input: {
title: string
asin?: string
nasFilename: string
onNas: boolean
checkedAt?: number
},
): NasStatusCacheStore {
const title = input.title.trim()
if (!title) return store
const key = titleCacheKey(title)
const asin = input.asin?.trim() || undefined
const entry: NasStatusCacheEntry = {
title,
asin,
nasFilename: input.nasFilename || '',
onNas: Boolean(input.onNas),
checkedAt: input.checkedAt ?? Date.now(),
}
store.byTitle[key] = entry
if (asin) store.byAsin[asin] = key
return store
}