{"version":3,"file":"index.mjs","names":[],"sources":["../src/cache-annotation.ts","../src/cache-store.ts","../src/cache-middleware.ts"],"sourcesContent":["import { defineAnnotation } from '@prisma-next/framework-components/runtime';\n\n/**\n * Payload accepted when calling the `cacheAnnotation` handle.\n *\n * - `ttl` — Time-to-live for the cached entry, in milliseconds. When\n *   omitted, the cache middleware passes the query through untouched —\n *   presence of the annotation alone is not sufficient to enable caching.\n *   This makes the cache strictly opt-in per query.\n * - `skip` — When `true`, the cache middleware passes the query through\n *   untouched even if a `ttl` is set. Useful for selectively bypassing\n *   the cache on a per-call basis without removing the annotation\n *   entirely (e.g. a \"force refresh\" knob in user code).\n * - `key` — Per-query override of the cache key. When supplied, replaces\n *   the default `RuntimeMiddlewareContext.contentHash(exec)` digest.\n *   The supplied string is stored as-is — the cache middleware does\n *   **not** rehash it, so the caller is responsible for ensuring the\n *   string is bounded in size and free of sensitive data they do not\n *   want flowing into logs / Redis `KEYS` / persistence dumps.\n */\nexport interface CachePayload {\n  readonly ttl?: number;\n  readonly skip?: boolean;\n  readonly key?: string;\n}\n\n/**\n * Read-only annotation handle for the cache middleware.\n *\n * Declared with `applicableTo: ['read']`. Write terminals supply\n * `K = 'write'` to the type-level `ValidAnnotations<'write', As>` gate\n * (and the runtime `assertAnnotationsApplicable(annotations, 'write', ...)`\n * check); the join `K extends Kinds` fails for this annotation, making\n * \"cache a mutation\" structurally impossible without an `as any` cast\n * bypass at both type *and* runtime levels.\n *\n * Stored under namespace `'cache'` in `plan.meta.annotations`. The cache\n * middleware reads it via `cacheAnnotation.read(plan)`.\n *\n * @example\n * ```typescript\n * import { cacheAnnotation } from '@prisma-next/middleware-cache';\n *\n * // ORM read terminal — accepts the read-only annotation via the meta callback.\n * const user = await db.User.first(\n *   { id },\n *   (meta) => meta.annotate(cacheAnnotation({ ttl: 60_000 })),\n * );\n *\n * // SQL DSL select builder — chainable.\n * const plan = db.sql\n *   .from(tables.user)\n *   .annotate(cacheAnnotation({ ttl: 60_000 }))\n *   .select({ id: tables.user.columns.id })\n *   .build();\n * ```\n */\nexport const cacheAnnotation = defineAnnotation<CachePayload>()({\n  namespace: 'cache',\n  applicableTo: ['read'],\n});\n","/**\n * A cached set of rows produced by a single execution.\n *\n * - `rows` are stored raw (undecoded). The SQL runtime's `decodeRow` pass\n *   wraps the orchestrator output, so intercepted rows go through the\n *   same codec decoding as driver rows on the way to the consumer. The\n *   cache stores wire-format values; decoding happens once per consumer\n *   read regardless of where the rows came from.\n * - `storedAt` is the clock value at the moment the entry was committed\n *   to the store. It is informational metadata for callers (debugging,\n *   telemetry) and is **not** used by the in-memory store itself for\n *   expiry — TTL is driven by the store's own clock plus the `ttlMs`\n *   passed to `set`. Custom stores may use it differently.\n */\nexport interface CachedEntry {\n  readonly rows: readonly Record<string, unknown>[];\n  readonly storedAt: number;\n}\n\n/**\n * Pluggable cache backend used by the cache middleware.\n *\n * The default implementation is an in-memory LRU with TTL produced by\n * `createInMemoryCacheStore`. Users can supply Redis, Memcached, or any\n * other backend by implementing this interface.\n *\n * The interface is intentionally minimal:\n *\n * - `get` returns the entry if it exists and has not expired, or\n *   `undefined` otherwise. Implementations that gate on TTL should\n *   treat an expired entry as absent (return `undefined`) and may\n *   evict it as a side effect.\n * - `set` writes the entry under the key with an associated TTL in\n *   milliseconds. Implementations may evict other entries to make\n *   room (LRU, LFU, etc.) and may treat the operation as fire-and-\n *   forget at scale; the cache middleware does not rely on `set`\n *   completing before subsequent `get`s.\n *\n * Both methods are async to leave the door open for I/O-backed stores\n * (Redis, S3, etc.). The default in-memory store completes\n * synchronously and wraps the result in `Promise.resolve` for type\n * conformance.\n */\nexport interface CacheStore {\n  get(key: string): Promise<CachedEntry | undefined>;\n  set(key: string, entry: CachedEntry, ttlMs: number): Promise<void>;\n}\n\n/**\n * Options accepted by `createInMemoryCacheStore`.\n *\n * - `maxEntries` — hard cap on the number of live entries. Once the cap\n *   is exceeded, the least recently used entry is evicted. Reads and\n *   writes both count as \"uses\" for ordering purposes.\n * - `clock` — injectable time source for TTL math. Defaults to\n *   `Date.now`. Tests inject a controlled clock to verify expiry without\n *   real-time waits.\n */\nexport interface InMemoryCacheStoreOptions {\n  readonly maxEntries: number;\n  readonly clock?: () => number;\n}\n\ninterface StoredRecord {\n  readonly entry: CachedEntry;\n  readonly expiresAt: number;\n}\n\n/**\n * Default cache backend. An LRU with per-entry TTL, backed by a `Map`.\n *\n * Eviction policy:\n *\n * - On `set` of a fresh key whose insertion would push the live count\n *   above `maxEntries`, the least recently used entry is evicted.\n *   Setting an existing key updates the entry in place and refreshes its\n *   recency without changing the live count.\n * - On `get` of an existing key, recency is bumped (so the entry is no\n *   longer the LRU candidate).\n * - On `get` of an expired entry, the entry is removed from the map and\n *   `undefined` is returned. The slot becomes available for new writes\n *   without counting against `maxEntries`.\n *\n * `Map` insertion order is the LRU order: the first key is the LRU\n * candidate; the last key is the most recently used. Bumping recency is\n * a delete-then-set on the underlying map.\n *\n * The default store is **not** coherent across processes or replicas —\n * each process holds its own Map. Users who need a shared cache supply\n * their own `CacheStore` (Redis, Memcached, etc.).\n */\nexport function createInMemoryCacheStore(options: InMemoryCacheStoreOptions): CacheStore {\n  const maxEntries = options.maxEntries;\n  const clock = options.clock ?? Date.now;\n  const map = new Map<string, StoredRecord>();\n\n  function get(key: string): Promise<CachedEntry | undefined> {\n    const record = map.get(key);\n    if (record === undefined) {\n      return Promise.resolve(undefined);\n    }\n    if (clock() >= record.expiresAt) {\n      map.delete(key);\n      return Promise.resolve(undefined);\n    }\n    // Bump recency: re-insert at the end of the iteration order.\n    map.delete(key);\n    map.set(key, record);\n    return Promise.resolve(record.entry);\n  }\n\n  function set(key: string, entry: CachedEntry, ttlMs: number): Promise<void> {\n    const expiresAt = clock() + ttlMs;\n    // Re-set semantics: if the key is already present, deleting first\n    // ensures the new value lands at the end of the iteration order\n    // (most recently used) rather than retaining the old slot's\n    // position. This matters for LRU correctness when the same key is\n    // re-cached after a refresh.\n    if (map.has(key)) {\n      map.delete(key);\n    }\n    map.set(key, { entry, expiresAt });\n\n    // Evict LRU entries until the live count is within bounds. The\n    // iterator yields keys in insertion order; the first one is the\n    // oldest (LRU).\n    while (map.size > maxEntries) {\n      const oldest = map.keys().next();\n      if (oldest.done) {\n        break;\n      }\n      map.delete(oldest.value);\n    }\n\n    return Promise.resolve();\n  }\n\n  return { get, set };\n}\n","import type {\n  AfterExecuteResult,\n  CrossFamilyMiddleware,\n  ExecutionPlan,\n  RuntimeMiddlewareContext,\n} from '@prisma-next/framework-components/runtime';\nimport { type CachePayload, cacheAnnotation } from './cache-annotation';\nimport { type CacheStore, createInMemoryCacheStore } from './cache-store';\n\n/**\n * Options accepted by `createCacheMiddleware`.\n *\n * - `store` — pluggable cache backend. Defaults to an in-process LRU\n *   produced by `createInMemoryCacheStore`. Users supply Redis,\n *   Memcached, or any other backend by implementing the `CacheStore`\n *   interface.\n * - `maxEntries` — only consulted when `store` is omitted. Sets the\n *   `maxEntries` cap on the default in-memory store. Defaults to 1000.\n * - `clock` — injectable time source for `storedAt` stamping on\n *   committed entries. Defaults to `Date.now`. Tests inject a controlled\n *   clock to make commit-time observable. Note: TTL math lives inside\n *   the store, not the middleware — supplying a clock here only affects\n *   the `storedAt` field on committed `CachedEntry` values.\n */\nexport interface CacheMiddlewareOptions {\n  readonly store?: CacheStore;\n  readonly maxEntries?: number;\n  readonly clock?: () => number;\n}\n\n/**\n * Per-execution buffer correlated with the post-lowering `exec` object\n * via a private `WeakMap`. Each in-flight cache miss owns one of these.\n *\n * The plan-identity invariant required by this `WeakMap` correlation is\n * documented in the runtime subsystem doc and pinned by a regression\n * test: family runtimes produce a fresh, frozen `exec` per call (SQL\n * `executeAgainstQueryable` constructs `Object.freeze({...lowered, ...})`\n * on each invocation; Mongo lowers fresh per call). If a future plan-\n * memoization change ever recycles `exec` objects across calls, this\n * correlation would silently leak rows between concurrent executions\n * — which is exactly what the regression test catches.\n */\ninterface PendingMiss {\n  readonly key: string;\n  readonly ttlMs: number;\n  readonly buffer: Record<string, unknown>[];\n}\n\n/**\n * Default `maxEntries` for the built-in in-memory store. Bounded so a\n * runaway producer cannot exhaust process memory; users who need\n * different bounds supply a custom `CacheStore`.\n */\nconst DEFAULT_MAX_ENTRIES = 1000;\n\n/**\n * Reads the cache payload from the plan, if present and branded.\n *\n * Returns `undefined` when:\n * - the plan has no `meta.annotations`, or\n * - the `cache` namespace key is absent, or\n * - the value under `cache` is not a branded `AnnotationValue` (the\n *   `cacheAnnotation.read` defensive check covers this).\n */\nfunction readCachePayload(plan: ExecutionPlan): CachePayload | undefined {\n  return cacheAnnotation.read(plan);\n}\n\n/**\n * Computes the cache key for an execution.\n *\n * Two-tier resolution:\n *\n * 1. Per-query override: `cacheAnnotation({ key })` — the supplied\n *    string is used verbatim. Not rehashed; the user is responsible for\n *    keeping the string bounded and free of sensitive data.\n * 2. Default: `ctx.contentHash(exec)` — the family runtime owns this and\n *    returns an opaque, bounded digest (SHA-512 in the SQL and Mongo\n *    runtimes today).\n *\n * The returned string is consumed directly as the `Map<string, …>` key\n * by the underlying `CacheStore`; the cache middleware does not perform\n * any further transformation.\n */\nasync function resolveCacheKey(\n  payload: CachePayload,\n  exec: ExecutionPlan,\n  ctx: RuntimeMiddlewareContext,\n): Promise<string> {\n  if (payload.key !== undefined) {\n    return payload.key;\n  }\n  return ctx.contentHash(exec);\n}\n\n/**\n * Creates a family-agnostic caching middleware.\n *\n * The middleware uses three hooks:\n *\n * - `intercept` — on each execution, checks the cache. On a hit, returns\n *   the cached raw rows; the runtime skips `runDriver` and `onRow`\n *   (`beforeExecute` is not affected — it has already run for every\n *   middleware before any `intercept` is consulted) and yields the\n *   cached rows to the consumer (which, in the SQL runtime, sees them\n *   after the standard `decodeRow` pass — i.e. the cache stores\n *   wire-format values). On a miss, records a pending buffer keyed on\n *   the `exec` object identity and returns `undefined` (passthrough).\n * - `onRow` — on the miss path, appends each row yielded by the driver\n *   to the pending buffer.\n * - `afterExecute` — on the miss path, commits the buffer to the store\n *   if and only if `result.completed === true && result.source === 'driver'`.\n *   Failed executions and middleware-served executions never populate\n *   the cache. The pending buffer is cleared in all branches so a stale\n *   `WeakMap` entry cannot leak between executions sharing an `exec`.\n *\n * The middleware bypasses the cache entirely when:\n * - the plan has no `cache` annotation, or\n * - the annotation has `skip: true`, or\n * - the annotation has no `ttl`, or\n * - `ctx.scope !== 'runtime'` (connection / transaction scopes opt out).\n *\n * Returns a cross-family `RuntimeMiddleware` (no `familyId` /\n * `targetId`). The package depends only on\n * `@prisma-next/framework-components/runtime`; cache keys come from\n * `ctx.contentHash(exec)`, populated by the family runtime, so SQL and\n * Mongo runtimes both work out of the box.\n *\n * @example\n * ```typescript\n * import { createCacheMiddleware, cacheAnnotation } from '@prisma-next/middleware-cache';\n *\n * const db = postgres({\n *   contractJson,\n *   url: process.env['DATABASE_URL']!,\n *   middleware: [createCacheMiddleware({ maxEntries: 1000 })],\n * });\n *\n * const user = await db.User.first(\n *   { id },\n *   (meta) => meta.annotate(cacheAnnotation({ ttl: 60_000 })),\n * );\n * ```\n */\nexport function createCacheMiddleware(options?: CacheMiddlewareOptions): CrossFamilyMiddleware {\n  const store =\n    options?.store ??\n    createInMemoryCacheStore({\n      maxEntries: options?.maxEntries ?? DEFAULT_MAX_ENTRIES,\n    });\n  const clock = options?.clock ?? Date.now;\n\n  // Per-execution scratch space, keyed on the post-lowering `exec`\n  // object identity. WeakMap keeps cleanup automatic: if an execution is\n  // dropped without `afterExecute` firing (e.g. an early throw before\n  // `runWithMiddleware` even starts), the entry is GC'd alongside the\n  // exec object.\n  const pending = new WeakMap<object, PendingMiss>();\n\n  async function intercept(\n    exec: ExecutionPlan,\n    ctx: RuntimeMiddlewareContext,\n  ): Promise<{ readonly rows: Iterable<Record<string, unknown>> } | undefined> {\n    if (ctx.scope !== 'runtime') {\n      return undefined;\n    }\n\n    const payload = readCachePayload(exec);\n    if (payload === undefined) {\n      return undefined;\n    }\n    if (payload.skip === true) {\n      return undefined;\n    }\n    if (payload.ttl === undefined) {\n      return undefined;\n    }\n\n    const key = await resolveCacheKey(payload, exec, ctx);\n    const hit = await store.get(key);\n    if (hit !== undefined) {\n      ctx.log.debug?.({ event: 'middleware.cache.hit', middleware: 'cache', key });\n      // Hit path leaves no WeakMap entry — afterExecute's lookup will\n      // return undefined and short-circuit.\n      return { rows: hit.rows };\n    }\n\n    // Miss: record the pending buffer so onRow / afterExecute can\n    // commit on success. The TTL is captured here so a later mutation\n    // of the annotation (defensive) cannot change the commit window.\n    pending.set(exec, { key, ttlMs: payload.ttl, buffer: [] });\n    ctx.log.debug?.({ event: 'middleware.cache.miss', middleware: 'cache', key });\n    return undefined;\n  }\n\n  async function onRow(\n    row: Record<string, unknown>,\n    exec: ExecutionPlan,\n    _ctx: RuntimeMiddlewareContext,\n  ): Promise<void> {\n    const slot = pending.get(exec);\n    if (slot === undefined) {\n      return;\n    }\n    slot.buffer.push(row);\n  }\n\n  async function afterExecute(\n    exec: ExecutionPlan,\n    result: AfterExecuteResult,\n    ctx: RuntimeMiddlewareContext,\n  ): Promise<void> {\n    const slot = pending.get(exec);\n    if (slot === undefined) {\n      return;\n    }\n    // Always release the WeakMap entry — the exec is single-use and\n    // any state we leave behind is dead weight on the GC.\n    pending.delete(exec);\n\n    if (!result.completed || result.source !== 'driver') {\n      return;\n    }\n\n    await store.set(slot.key, { rows: slot.buffer, storedAt: clock() }, slot.ttlMs);\n    ctx.log.debug?.({ event: 'middleware.cache.store', middleware: 'cache', key: slot.key });\n  }\n\n  return {\n    name: 'cache',\n    intercept,\n    onRow,\n    afterExecute,\n  };\n}\n"],"mappings":"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"}