versioned-source.js
2.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* versioned-source.js —— 「DB 表 + 版本号 + TTL 新鲜窗口」的通用缓存机制。
*
* 不认识 DB、redis、行结构:`load` 每次返回归一后的单一形态 `T`,`version` 返回版本令牌,
* `now` 为注入时钟。三者是本模块与外界的全部接触面;构造期不调用它们(惰性),首个 `get()`
* 才触发,故 `import '@drama/config-source'` 零 IO。
*
* 失效沿单一路径逐级下探:新鲜窗口内直接命中 → 窗口外比对版本令牌 → 令牌变化才 `load`。
*
* @template T
* @typedef {Object} VersionedSourceDeps
* @property {() => Promise<T>} load 从真源全量拉取并归一为单一形态 T
* @property {() => Promise<string>} version 版本令牌;返回值变化即视为配置失效
* @property {number} ttlMs 新鲜窗口毫秒数;窗口内不探测 version、不 load
* @property {() => number} now 注入时钟
*
* @template T
* @typedef {Object} VersionedSource
* @property {() => Promise<T>} get 返回当前有效配置(必要时按版本失效后重载)
* @property {() => void} invalidate 清空缓存,强制下次 get 走 load
*/
/**
* @template T
* @param {VersionedSourceDeps<T>} deps
* @returns {VersionedSource<T>}
*/
export function createVersionedSource({ load, version, ttlMs, now }) {
/** @type {T | null} */
let cached = null
/** @type {string | null} */
let cachedVersion = null
let checkedAt = 0
/** @type {Promise<T> | null} */
let inflight = null
/** @returns {Promise<T>} */
async function get() {
if (cached !== null && now() - checkedAt < ttlMs) return cached
const v = await version()
if (cached !== null && v === cachedVersion) {
checkedAt = now()
return cached
}
if (inflight) return inflight
inflight = (async () => {
try {
const fresh = await load()
cached = fresh
cachedVersion = v
checkedAt = now()
return fresh
} finally {
inflight = null
}
})()
return inflight
}
function invalidate() {
cached = null
cachedVersion = null
}
return { get, invalidate }
}