versioned-source.js 2.16 KB
/**
 * 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 }
}