/** * 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} load 从真源全量拉取并归一为单一形态 T * @property {() => Promise} version 版本令牌;返回值变化即视为配置失效 * @property {number} ttlMs 新鲜窗口毫秒数;窗口内不探测 version、不 load * @property {() => number} now 注入时钟 * * @template T * @typedef {Object} VersionedSource * @property {() => Promise} get 返回当前有效配置(必要时按版本失效后重载) * @property {() => void} invalidate 清空缓存,强制下次 get 走 load */ /** * @template T * @param {VersionedSourceDeps} deps * @returns {VersionedSource} */ export function createVersionedSource({ load, version, ttlMs, now }) { /** @type {T | null} */ let cached = null /** @type {string | null} */ let cachedVersion = null let checkedAt = 0 /** @type {Promise | null} */ let inflight = null /** @returns {Promise} */ 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 } }