optimization: avoid rebuilding language data too often
Massimo Melina committed
Dec 9, 2024 at 19:47 UTC
698cbdaad4530c5053eba0a24e98ea0227e61d92
2 files changed
+55
-31
src/expiringCache.ts
new
+16
@@ -0,0 +1,16 @@
1
+export function expiringCache<T, K=string>(ttl: number) {
2
+ const o = new Map<K,T>()
3
+ return Object.assign(o, {
4
+ try(k: K, creator: () => T): T {
5
+ let ret = o.get(k)
6
+ if (ret === undefined) {
7
+ ret = creator()
8
+ o.set(k, ret)
9
+ Promise.resolve(ret).then(() =>
10
+ setTimeout(() => o.delete(k), ttl))
11
+ }
12
+ return ret
13
+ },
14
+ })
15
+}
16
+
src/lang.ts
+39
-31
@@ -1,5 +1,6 @@
1
import Koa from 'koa'
2
-import { CFG, hasProp, onlyTruthy, tryJson, wantArray } from './misc'
2
+import { CFG, hasProp, onlyTruthy, tryJson } from './misc'
3
+import { expiringCache } from './expiringCache'
4
import { readFile } from 'fs/promises'
5
import { defineConfig } from './config'
6
import { watchLoad } from './watchLoad'
@@ -20,39 +21,46 @@ export function file2code(fn: string) {
21
return fn.replace(PREFIX, '').replace(SUFFIX, '')
22
}
23
23
-export async function getLangData(ctx: Koa.Context) {
24
- const param = String(ctx.query.lang || '')
25
- if (!param && forceLangData)
26
- return forceLangData
27
- const ret: any = {}
28
- const csv = param || ctx.get('Accept-Language') || ''
29
- const langs = wantArray(csv.split(',').map(x => x.toLowerCase()))
30
- let i = 0
31
- while (i < langs.length) {
32
- let k = langs[i] || '' // shut up ts
33
- if (!k || k === EMBEDDED_LANGUAGE) break
34
- const fn = code2file(k)
35
- try { ret[k] = JSON.parse(await readFile(fn, 'utf8')) }
36
- catch {
37
- if (hasProp(EMBEDDED_TRANSLATIONS, k))
38
- ret[k] = EMBEDDED_TRANSLATIONS[k]
39
- else {
40
- do { k = k.substring(0, k.lastIndexOf('-'))
41
- } while (k && langs.includes(k))
42
- if (k) {
43
- langs[i] = k // overwrite and retry
44
- continue
24
+const cache = expiringCache(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
28
+ const param = String(ctx.query.lang || '')
29
+ if (!param && forceLangData)
30
+ return forceLangData
31
+ ctxOrLangCsv = param || ctx.get('Accept-Language') || ''
32
+ }
33
+ const csv = ctxOrLangCsv.toLowerCase()
34
+ return cache.try(csv, async () => {
35
+ const langs = csv.split(',')
36
+ const ret: any = {}
37
+ let i = 0
38
+ while (i < langs.length) {
39
+ let k = langs[i] || '' // shut up ts
40
+ if (!k || k === EMBEDDED_LANGUAGE) break
41
+ const fn = code2file(k)
42
+ try { ret[k] = JSON.parse(await readFile(fn, 'utf8')) } // allow external files to override embedded translations
43
+ catch {
44
+ if (hasProp(EMBEDDED_TRANSLATIONS, k))
45
+ ret[k] = EMBEDDED_TRANSLATIONS[k]
46
+ else {
47
+ do { k = k.substring(0, k.lastIndexOf('-'))
48
+ } while (k && langs.includes(k))
49
+ if (k) {
50
+ langs[i] = k // overwrite and retry
51
+ continue
52
+ }
53
}
54
}
47
- }
48
- const fromPlugins = onlyTruthy(await Promise.all(mapPlugins(async pl =>
49
- tryJson(await readFile(join(pl.folder, fn), 'utf8').catch(() => ''))?.translate )))
50
- if (fromPlugins.length)
51
- _.defaults((ret[k] ||= {}).translate ||= {}, ...fromPlugins) // be sure we have an entry for k
55
+ const fromPlugins = onlyTruthy(await Promise.all(mapPlugins(async pl =>
56
+ tryJson(await readFile(join(pl.folder, fn), 'utf8').catch(() => ''))?.translate )))
57
+ if (fromPlugins.length)
58
+ _.defaults((ret[k] ||= {}).translate ||= {}, ...fromPlugins) // be sure we have an entry for k
59
53
- i++
54
- }
55
- return ret
60
+ i++
61
+ }
62
+ return ret
63
+ })
64
}
65
66
let forceLangData: any