{"version":3,"file":"cache_service_resolver.cjs","sources":["../../../src/private/utils/cache_service_resolver.ts"],"sourcesContent":["/**\n * Resolves the cache service used for relation caching via a precedence chain\n * (per-model `relationsCacheService` option → process-wide default → none) and\n * exposes the package-level default registration setter.\n *\n * @module @nhtio/lucid-resourceful/private/utils/cache_service_resolver\n */\n\nimport type { LucidModel } from '@adonisjs/lucid/types/model'\nimport type { CacheProvider, RawCommonOptions, Duration } from 'bentocache/types'\n\n/**\n * A cache TTL/duration as understood by the underlying cache service.\n *\n * Aliased to BentoCache's own `Duration` (`number | string | null`) so the\n * relation cache never drifts from the option types it forwards to.\n *\n * @public\n */\nexport type CacheDuration = Duration\n\n/**\n * Per-relation (and model-level) cache entry options.\n *\n * The subset of BentoCache's `RawCommonOptions` that the relation cache forwards\n * to `set` (and may merge for `getOrSet`). Sourced directly from BentoCache via\n * `Pick`, so each option's type is the cache implementation's own — there is no\n * hand-rolled duplicate to keep in sync.\n *\n * @public\n */\nexport type RelationCacheEntryOptions = Partial<\n  Pick<\n    RawCommonOptions,\n    | 'ttl'\n    | 'grace'\n    | 'graceBackoff'\n    | 'suppressL2Errors'\n    | 'timeout'\n    | 'hardTimeout'\n    | 'lockTimeout'\n  >\n>\n\n/**\n * The cache service the relation cache talks to.\n *\n * The narrow slice of BentoCache's `CacheProvider` the relation cache actually\n * uses (`get`/`set`/`getOrSet`/`delete`/`deleteByTag`), taken via `Pick` so the\n * method signatures track BentoCache exactly. A `BentoCache` instance (e.g.\n * `app.container.make('cache')`) satisfies this directly, while the narrow surface\n * keeps test doubles small and documents precisely what the package depends on.\n *\n * @public\n */\nexport type CacheServiceLike = Pick<\n  CacheProvider,\n  'get' | 'set' | 'getOrSet' | 'delete' | 'deleteByTag'\n>\n\n/**\n * Resolver supplied by the host that returns the cache service for a model, or\n * `null` to opt that model out of relation caching.\n *\n * @public\n */\nexport type RelationsCacheServiceResolver = () =>\n  | CacheServiceLike\n  | null\n  | Promise<CacheServiceLike | null>\n\n/**\n * Module-scoped holder for the process-wide default cache resolver. Set once at\n * boot by the host (e.g. `core`) and read through the precedence chain.\n * @internal\n */\nlet defaultRelationsCacheServiceResolver: RelationsCacheServiceResolver | null = null\n\n/**\n * Module-scoped set of related-model identity names that are cache TARGETS — the\n * related-model names referenced by some model's `relationsCache` config (e.g.\n * `Currency`, `PaymentServiceProvider`), NOT the models that merely *declare* a\n * `relationsCache`. Used by the write-through eviction hook to skip `deleteByTag`\n * for rows that can never be a cached relation target.\n *\n * Populated PROCESS-GLOBALLY: the host calls `registerRelationCacheTargetsForModel`\n * at boot for every model that declares a `relationsCache` (and, redundantly,\n * `CachingPreloader`'s constructor registers on `.query()`/`.load()`). Boot\n * registration is essential — without it a process that never queries the\n * declaring parent (e.g. a rates-sync worker that only writes `Currency`) would\n * not know `Currency` is a target, would skip `deleteByTag`, and would leave a\n * stale entry in a shared L2 for other processes until the TTL elapsed.\n * @internal\n */\nconst relationCacheTargetNames = new Set<string>()\n\n/**\n * Registers a related-model identity name as a relation-cache TARGET. Idempotent.\n *\n * @param name - The related model's canonical identity name (`$resourcefulName`\n *   or class name).\n * @public\n */\nexport const registerRelationCacheTarget = (name: string): void => {\n  relationCacheTargetNames.add(name)\n}\n\n/**\n * Reports whether a model identity name is a registered relation-cache TARGET,\n * i.e. some model caches a 1:1 relation pointing at this model.\n *\n * @param name - The model's canonical identity name.\n * @returns `true` when the name is a registered cache target.\n * @public\n */\nexport const isRelationCacheTarget = (name: string): boolean => {\n  return relationCacheTargetNames.has(name)\n}\n\n/**\n * Clears the registered relation-cache target set. Test-only hook: the set is\n * module-scoped and accumulates across a process, which can mask regressions\n * between tests. Hosts must NOT call this at runtime.\n *\n * @public\n */\nexport const resetRelationCacheTargets = (): void => {\n  relationCacheTargetNames.clear()\n}\n\n/**\n * Per-model memoization of the resolved cache instance. Only resolved instances\n * are stored — never a premature `null` — so an unresolved lookup is retried\n * rather than permanently cached as a miss.\n * @internal\n */\nconst resolvedCacheServiceByModel = new WeakMap<LucidModel, CacheServiceLike>()\n\n/**\n * Registers (or clears) the process-wide default relation cache resolver.\n *\n * The host application calls this once at boot. Passing `null` clears the\n * default (used by tests). Models without their own `relationsCacheService`\n * fall back to this resolver via the precedence chain.\n *\n * @param resolver - The default resolver, or `null` to clear it.\n * @public\n */\nexport const setDefaultRelationsCacheService = (\n  resolver: RelationsCacheServiceResolver | null\n): void => {\n  defaultRelationsCacheServiceResolver = resolver\n  // A new default may resolve where a previous lookup found nothing, so the\n  // memoization (which only ever stored resolved instances) does not need\n  // clearing; unresolved models simply retry on their next access.\n}\n\n/**\n * Reads the currently-registered default resolver (used internally and by the\n * write-through eviction hook). Exposed for completeness; prefer\n * `resolveRelationsCacheService`.\n *\n * @returns The default resolver, or `null` when none is registered.\n * @internal\n */\nexport const getDefaultRelationsCacheService = (): RelationsCacheServiceResolver | null =>\n  defaultRelationsCacheServiceResolver\n\n/**\n * Resolves the cache service for a model via the precedence chain:\n * per-model resolver → process-wide default → `null` (uncached).\n *\n * Returns `null` only when the selected resolver itself returns `null` (an\n * intentional opt-out). If the selected resolver throws or fails to acquire the\n * cache, the error propagates — callers on the read path must fail loud, while\n * the write-through hook guards this call to stay best-effort (ADR-007).\n * Resolved instances are memoized per model; a premature `null` is never cached.\n *\n * @param model - The Lucid model whose cache service should be resolved.\n * @param perModelResolver - The model's own `relationsCacheService` option, if any.\n * @returns The resolved cache service, or `null` when caching is opted out.\n * @public\n */\nexport const resolveRelationsCacheService = async (\n  model: LucidModel,\n  perModelResolver?: RelationsCacheServiceResolver\n): Promise<CacheServiceLike | null> => {\n  const memoized = resolvedCacheServiceByModel.get(model)\n  if (memoized) {\n    return memoized\n  }\n\n  const selectedResolver = perModelResolver ?? defaultRelationsCacheServiceResolver\n  if (!selectedResolver) {\n    return null\n  }\n\n  const resolved = await selectedResolver()\n  if (!resolved) {\n    return null\n  }\n\n  resolvedCacheServiceByModel.set(model, resolved)\n  return resolved\n}\n"],"names":[],"mappings":";;AA4EA,IAAI,uCAA6E;AAkBjF,MAAM,+CAA+B,IAAA;AAS9B,MAAM,8BAA8B,CAAC,SAAuB;AACjE,2BAAyB,IAAI,IAAI;AACnC;AAUO,MAAM,wBAAwB,CAAC,SAA0B;AAC9D,SAAO,yBAAyB,IAAI,IAAI;AAC1C;AASO,MAAM,4BAA4B,MAAY;AACnD,2BAAyB,MAAA;AAC3B;AAQA,MAAM,kDAAkC,QAAA;AAYjC,MAAM,kCAAkC,CAC7C,aACS;AACT,yCAAuC;AAIzC;AAUO,MAAM,kCAAkC,MAC7C;AAiBK,MAAM,+BAA+B,OAC1C,OACA,qBACqC;AACrC,QAAM,WAAW,4BAA4B,IAAI,KAAK;AACtD,MAAI,UAAU;AACZ,WAAO;AAAA,EACT;AAEA,QAAM,mBAAmB,oBAAoB;AAC7C,MAAI,CAAC,kBAAkB;AACrB,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,MAAM,iBAAA;AACvB,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AAEA,8BAA4B,IAAI,OAAO,QAAQ;AAC/C,SAAO;AACT;;;;;;;"}