better code: improved expiringCache

Massimo Melina committed Dec 24, 2025 at 14:20 UTC f2a0946bb7a8ad67f7de5d8fea519d058798024a
2 files changed +20 -9
src/expiringCache.ts
+17 -6
@@ -1,13 +1,24 @@
1 export function expiringCache<T, K=string>(ttlMs: number) {
2 + if (!ttlMs)
3 + throw Error('invalid TTL')
4 const o = new Map<K,T>()
5 return Object.assign(o, {
4 - try(k: K, creator: () => T): T {
6 + // creator can return undefined if the value should not be cached. `invalidate` is useful in case you have some custom logic for it, other than ttl.
7 + try(k: K, creator: (invalidate: () => void) => T): T {
8 let ret = o.get(k)
6 - if (ret === undefined) {
7 - ret = creator()
8 - o.set(k, ret)
9 - // in case of async, wait for it to be done before starting the timer
10 - Promise.resolve(ret).finally(() => setTimeout(() => o.delete(k), ttlMs)).catch(() => {})
9 + if (ret === undefined) { // undefined = missing, as we don't accept this value in our cache
10 + ret = creator(invalidate)
11 + if (ret !== undefined) {
12 + o.set(k, ret)
13 + Promise.resolve(ret).then(v => {
14 + if (v === undefined) // even in a promise, we'll consider undefined as a request to cancel the caching
15 + invalidate()
16 + }, () => {}) // avoid js warning
17 + .finally(() => setTimeout(invalidate, ttlMs)) // wait for async (in case) before starting the timer
18 + }
19 + function invalidate() {
20 + o.delete(k)
21 + }
22 }
23 return ret
24 },
src/lang.ts
+3 -3
@@ -1,5 +1,5 @@
1 import Koa from 'koa'
2 -import { CFG, hasProp, onlyTruthy, tryJson } from './misc'
2 +import { CFG, Dict, hasProp, onlyTruthy, tryJson } from './misc'
3 import { expiringCache } from './expiringCache'
4 import { readFile } from 'fs/promises'
5 import { defineConfig } from './config'
@@ -21,7 +21,7 @@ export function file2code(fn: string) {
21 return fn.replace(PREFIX, '').replace(SUFFIX, '')
22 }
23
24 -const cache = expiringCache(3_000) // 3 seconds for both a good dx and acceptable performance
24 +const cache = expiringCache<Dict>(3_000) // 3 seconds for both a good dx and acceptable performance
25 export async function getLangData(ctxOrLangCsv: Koa.Context | string) {
26 if (typeof ctxOrLangCsv !== 'string') {
27 const ctx = ctxOrLangCsv
@@ -33,7 +33,7 @@ export async function getLangData(ctxOrLangCsv: Koa.Context | string) {
33 const csv = ctxOrLangCsv.toLowerCase()
34 return cache.try(csv, async () => {
35 const langs = csv.split(',')
36 - const ret: any = {}
36 + const ret: Dict = {}
37 let i = 0
38 while (i < langs.length) {
39 let k = langs[i] || '' // shut up ts