{"version":3,"file":"cached-registry.mjs","names":[],"sources":["../../src/cache/cached-registry.ts"],"sourcesContent":["import type { Dependency, Maintainer, Package, URLBuilder, Version } from \"../core/types.ts\";\nimport { Registry } from \"../core/registry.ts\";\nimport { getStorage, computeIntegrity } from \"./storage.ts\";\nimport {\n  readLockfile,\n  writeLockfile,\n  cacheKey,\n  getFreshEntry,\n  setEntry,\n  DEFAULT_TTL,\n} from \"./lockfile.ts\";\nimport type { EntryType, LockfileEntry } from \"./lockfile.ts\";\nimport type { Storage } from \"unstorage\";\n\n/**\n * CachedRegistry wraps any Registry and adds caching backed by unstorage\n * with a lockfile for freshness tracking.\n *\n * Pass a custom `storage` to use a non-default driver (e.g. Cloudflare KV).\n * If omitted, uses the globally configured storage (see `configureStorage()`).\n */\nexport class CachedRegistry extends Registry {\n  readonly inner: Registry;\n  readonly storage: Storage;\n  private readonly lockfileStorage: Storage;\n  private readonly inflight = new Map<string, Promise<unknown>>();\n\n  constructor(inner: Registry, storage?: Storage) {\n    super();\n    this.inner = inner;\n    this.storage = storage ?? getStorage();\n    this.lockfileStorage = storage ?? getStorage();\n  }\n\n  ecosystem(): string {\n    return this.inner.ecosystem();\n  }\n\n  urls(): URLBuilder {\n    return this.inner.urls();\n  }\n\n  async fetchPackage(name: string, signal?: AbortSignal): Promise<Package> {\n    return this.cached<Package>(\n      name,\n      \"package\",\n      undefined,\n      () => this.inner.fetchPackage(name, signal),\n      signal,\n      (value) => ({ latestVersion: value.latestVersion }),\n    );\n  }\n\n  async fetchVersions(name: string, signal?: AbortSignal): Promise<Version[]> {\n    const versions = await this.cached<Version[]>(\n      name,\n      \"versions\",\n      undefined,\n      () => this.inner.fetchVersions(name, signal),\n      signal,\n    );\n    // JSON round-trip through storage turns Date objects into ISO strings.\n    // Revive them so consumers can safely call .getTime() / .toISOString().\n    return versions.map((v) => ({\n      ...v,\n      publishedAt: v.publishedAt ? new Date(v.publishedAt as unknown as string) : null,\n    }));\n  }\n\n  async fetchDependencies(\n    name: string,\n    version: string,\n    signal?: AbortSignal,\n  ): Promise<Dependency[]> {\n    return this.cached<Dependency[]>(\n      name,\n      \"dependencies\",\n      version,\n      () => this.inner.fetchDependencies(name, version, signal),\n      signal,\n    );\n  }\n\n  async fetchMaintainers(name: string, signal?: AbortSignal): Promise<Maintainer[]> {\n    return this.cached<Maintainer[]>(\n      name,\n      \"maintainers\",\n      undefined,\n      () => this.inner.fetchMaintainers(name, signal),\n      signal,\n    );\n  }\n\n  /**\n   * Read a fresh cached value or fetch and persist its replacement.\n   *\n   * @param name - Package name.\n   * @param type - Cached data type.\n   * @param version - Package version when the cache entry is versioned.\n   * @param fetcher - Upstream operation used after a cache miss.\n   * @param signal - Optional cancellation signal.\n   * @param extraMeta - Optional lockfile metadata derived from the fetched value.\n   * @returns {Promise<T>} The cached or freshly fetched value.\n   */\n  private async cached<T>(\n    name: string,\n    type: EntryType,\n    version: string | undefined,\n    fetcher: () => Promise<T>,\n    signal: AbortSignal | undefined,\n    extraMeta?: (value: T) => Record<string, unknown>,\n  ): Promise<T> {\n    const eco = this.inner.ecosystem();\n    const key = cacheKey(eco, name, type, version);\n    const storageKey = key;\n\n    // 1. Check lockfile\n    const lockfile = await readLockfile(this.lockfileStorage);\n    const entry = getFreshEntry(lockfile, key);\n\n    if (entry) {\n      // 2. Read from storage\n      const cached = await this.storage.getItem<T>(storageKey);\n      if (cached !== null) {\n        // Verify integrity\n        const currentHash = computeIntegrity(cached);\n        if (currentHash === entry.integrity) {\n          return cached;\n        }\n        // Integrity mismatch - refetch\n      }\n    }\n\n    // 3. Coalesce concurrent fetches for the same key (single-flight).\n    // Skip dedup when caller provides an AbortSignal to avoid sharing\n    // cancellation context across unrelated callers.\n    if (!signal) {\n      const pending = this.inflight.get(key);\n      if (pending) {\n        return pending as Promise<T>;\n      }\n    }\n\n    const flight = (async () => {\n      const value = await fetcher();\n\n      // Persist to cache (best-effort - storage failures must not discard fetched data)\n      const meta = extraMeta ? extraMeta(value) : {};\n      const newEntry: LockfileEntry = {\n        key,\n        type,\n        fetchedAt: Date.now(),\n        ttl: DEFAULT_TTL[type],\n        integrity: computeIntegrity(value),\n        ...(\"latestVersion\" in meta ? { latestVersion: meta[\"latestVersion\"] as string } : {}),\n      };\n      try {\n        await this.storage.setItem(storageKey, value as Record<string, unknown>);\n        // Re-read lockfile before writing to avoid overwriting entries\n        // added by concurrent calls for different keys.\n        const freshLockfile = await readLockfile(this.lockfileStorage);\n        setEntry(freshLockfile, newEntry);\n        await writeLockfile(freshLockfile, this.lockfileStorage);\n      } catch {\n        // Storage I/O failed (disk full, permissions, remote driver timeout, etc.).\n        // The data was already fetched successfully - return it anyway.\n      }\n\n      return value;\n    })();\n\n    if (!signal) {\n      this.inflight.set(key, flight);\n    }\n    try {\n      return await flight;\n    } finally {\n      if (!signal) {\n        this.inflight.delete(key);\n      }\n    }\n  }\n}\n"],"mappings":";;;;;;;;EAqBA,MAAa;EACF,KAAA,QAAA;EACA,KAAA,UAAA,WAAA,WAAA;EACQ,KAAA,kBAAA,WAAA,WAAA;CACjB;CAEA,YAAY;EACV,OAAM,KAAA,MAAA,UAAA;CACN;CACA,OAAK;EACL,OAAK,KAAA,MAAA,KAAkB;CACzB;CAEA,MAAA,aAAoB,MAAA,QAAA;EAClB,OAAO,KAAK,OAAM,MAAA,WAAU,KAAA,SAAA,KAAA,MAAA,aAAA,MAAA,MAAA,GAAA,SAAA,WAAA,EAAA,eAAA,MAAA,cAAA,EAAA;CAC9B;CAEA,MAAA,cAAmB,MAAA,QAAA;EACjB,QAAO,MAAK,KAAM,OAAK,MAAA,YAAA,KAAA,SAAA,KAAA,MAAA,cAAA,MAAA,MAAA,GAAA,MAAA,EAAA,CAAA,KAAA,OAAA;GACzB,GAAA;GAEA,aAAM,EAAA,cAA2B,IAAwC,KAAA,EAAA,WAAA,IAAA;EACvE,EAAA;CAQF;CAEA,MAAM,kBAAc,MAAc,SAA0C,QAAA;EAU1E,OAAA,KAAO,OATqB,MAAA,gBAE1B,eACA,KACM,MAAK,kBAAoB,MAAM,SACrC,MAIa,GAAC,MAAK;CACnB;CACA,MAAA,iBAAe,MAAA,QAAkB;EACnC,OAAE,KAAA,OAAA,MAAA,eAAA,KAAA,SAAA,KAAA,MAAA,iBAAA,MAAA,MAAA,GAAA,MAAA;CACJ;CAOE,MAAA,OAAY,MAAA,MACV,SACA,SACA,QAAA,WACW;EAGf,MAAA,MAAA,KAAA,MAAA,UAAA;EAEA,MAAM,MAAA,SAAA,KAAiB,MAAc,MAA6C,OAAA;EAChF,MAAA,aACE;EAMJ,MAAA,WAAA,MAAA,aAAA,KAAA,eAAA;;;;;;;;;;;;EAaA,MAAc,UACZ,YAEA;GAKA,MAAM,QAAM,MAAK,QAAM;GACvB,MAAM,OAAM,YAAc,UAAM,KAAM,IAAO,CAAA;GAC7C,MAAM,WAAA;IAGN;IACA;IAEA,WAAW,KAAA,IAAA;IAET,KAAM,YAAS;IACf,WAAI,iBAGF,KAAA;IADoB,GAAA,mBAAiB,OACvB,EAAA,eAAY,KACxB,iBAAO,IAAA,CAAA;GACT;GAGJ,IAAA;IAKA,MAAK,KAAQ,QAAA,QAAA,YAAA,KAAA;IACX,MAAM,gBAAe,MAAA,aAAgB,KAAA,eAAA;IACrC,SAAI,eACK,QAAA;IAEX,MAAA,cAAA,eAAA,KAAA,eAAA;GAEA,QAAM,CAAA;GACJ,OAAM;EAGN,EAAA,CAAA;EACA,IAAA,CAAA,QAAM,KAA0B,SAAA,IAAA,KAAA,MAAA;EAC9B,IAAA;GACA,OAAA,MAAA;EACA,UAAA;GACA,IAAA,CAAA,QAAK,KAAY,SAAA,OAAA,GAAA;EACjB;CACA;AACF;AAEE,SAAA"}