{"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":";;;;;;;;;AAqBA,IAAa,iBAAb,cAAoC,SAAS;CAC3C;CACA;CACA;CACA,2BAA4B,IAAI,KAA+B;CAE/D,YAAY,OAAiB,SAAmB;AAC9C,SAAO;AACP,OAAK,QAAQ;AACb,OAAK,UAAU,WAAW,YAAY;AACtC,OAAK,kBAAkB,WAAW,YAAY;;CAGhD,YAAoB;AAClB,SAAO,KAAK,MAAM,WAAW;;CAG/B,OAAmB;AACjB,SAAO,KAAK,MAAM,MAAM;;CAG1B,MAAM,aAAa,MAAc,QAAwC;AACvE,SAAO,KAAK,OACV,MACA,WACA,KAAA,SACM,KAAK,MAAM,aAAa,MAAM,OAAO,EAC3C,SACC,WAAW,EAAE,eAAe,MAAM,eAAe,EACnD;;CAGH,MAAM,cAAc,MAAc,QAA0C;AAU1E,UATiB,MAAM,KAAK,OAC1B,MACA,YACA,KAAA,SACM,KAAK,MAAM,cAAc,MAAM,OAAO,EAC5C,OACD,EAGe,KAAK,OAAO;GAC1B,GAAG;GACH,aAAa,EAAE,cAAc,IAAI,KAAK,EAAE,YAAiC,GAAG;GAC7E,EAAE;;CAGL,MAAM,kBACJ,MACA,SACA,QACuB;AACvB,SAAO,KAAK,OACV,MACA,gBACA,eACM,KAAK,MAAM,kBAAkB,MAAM,SAAS,OAAO,EACzD,OACD;;CAGH,MAAM,iBAAiB,MAAc,QAA6C;AAChF,SAAO,KAAK,OACV,MACA,eACA,KAAA,SACM,KAAK,MAAM,iBAAiB,MAAM,OAAO,EAC/C,OACD;;;;;;;;;;;;;CAcH,MAAc,OACZ,MACA,MACA,SACA,SACA,QACA,WACY;EAEZ,MAAM,MAAM,SADA,KAAK,MAAM,WAAW,EACR,MAAM,MAAM,QAAQ;EAC9C,MAAM,aAAa;EAInB,MAAM,QAAQ,cADG,MAAM,aAAa,KAAK,gBAAgB,EACnB,IAAI;AAE1C,MAAI,OAAO;GAET,MAAM,SAAS,MAAM,KAAK,QAAQ,QAAW,WAAW;AACxD,OAAI,WAAW;QAEO,iBAAiB,OAAO,KACxB,MAAM,UACxB,QAAO;;;AASb,MAAI,CAAC,QAAQ;GACX,MAAM,UAAU,KAAK,SAAS,IAAI,IAAI;AACtC,OAAI,QACF,QAAO;;EAIX,MAAM,UAAU,YAAY;GAC1B,MAAM,QAAQ,MAAM,SAAS;GAG7B,MAAM,OAAO,YAAY,UAAU,MAAM,GAAG,EAAE;GAC9C,MAAM,WAA0B;IAC9B;IACA;IACA,WAAW,KAAK,KAAK;IACrB,KAAK,YAAY;IACjB,WAAW,iBAAiB,MAAM;IAClC,GAAI,mBAAmB,OAAO,EAAE,eAAe,KAAK,kBAA4B,GAAG,EAAA;IACpF;AACD,OAAI;AACF,UAAM,KAAK,QAAQ,QAAQ,YAAY,MAAiC;IAGxE,MAAM,gBAAgB,MAAM,aAAa,KAAK,gBAAgB;AAC9D,aAAS,eAAe,SAAS;AACjC,UAAM,cAAc,eAAe,KAAK,gBAAgB;WAClD;AAKR,UAAO;MACL;AAEJ,MAAI,CAAC,OACH,MAAK,SAAS,IAAI,KAAK,OAAO;AAEhC,MAAI;AACF,UAAO,MAAM;YACL;AACR,OAAI,CAAC,OACH,MAAK,SAAS,OAAO,IAAI"}