import type { ReadonlyDeep } from "type-fest"; import type { PublicInterface } from "type-party"; import type Cache from "../Cache.js"; import type { CacheSpec, SpecForId } from "../types/00_CacheSpec.js"; import type { AnyParams, AnyValidators, ConsumerDirectives, Logger, RequestPairedProducer } from "../types/index.js"; import { type PartialConsumerRequest } from "./requestPairedProducerUtils.js"; /** * Represents the outcome of a cache lookup operation. */ export type CacheResultOutcome = /** * A cached value was returned without contacting producer. (Value was fresh * or within consumer's tolerance for stale values without revalidation.) */ "hit" /** * A stale cached value was returned while revalidating in background */ | "stale_while_revalidate" /** * The consumer requested to bypass the cache by providing directives that * could _never_ be satisfied with cached data (e.g., `maxAge: 0`). This is * arguably a type of miss, but is distinguised from other misses. */ | "bypass" /** * No cached value was suitable; the producer was contacted. E.g., the cache * had no stored value, or the stored value's age exceeded consumer's max-age. */ | "miss" /** * Request was not cacheable; producer was contacted directly. */ | "uncacheable"; export type WrapProducerOptions = { /** * A name for this cache, used for identifying it in diagnostics/monitoring. * This name is included in messages published to the diagnostics channel. */ cacheName?: string; /** * A function returning whether a given request can have its response cached. * Defaults to assuming all requests are cacheable (i.e., always returning * true). Of course, producers can indicate in their individual responses that * the response is not cachable (e.g., through the `maxAge: 0, storeFor: 0` * directives), but this function allows the cache to always pass-through * whole classes of requests. E.g., an HTTP cache built with this would return * false for all requests where a `method` request parameter is POST. */ isCacheable?(this: void, id: string, params: ReadonlyDeep>): boolean; /** * Controls whether the function returned by `wrapProducer`/`wrapBulkProducer` * will fall back to calling the produer if its attempt to read from the cache * results in an error, or whether it will throw. Normally, falling back to * calling the producer is desirable (so that brief unavailability of the * cache doesn't effect the application), and this is the default. However, * this must be considered carefully: calling through to the producer on every * request can _dramatically_ increase the load it's under -- e.g., if the * cache hit rate was even 95% (which is very low for many applications), then * calling the producer unconditionally will increase the load its under by * 20x! I.e., instead of 1 in 20 requests hitting the producer, all 20 will. * If the producer is/uses a shared resource, and it doesn't have good load * shedding or autoscaling mechanisms, and the requests to it that are going * through this cache aren't its most important work, then sending all the * requests to the producer could lead to cascading failures and/or prevent it * from serving more important requests. In that case, having the function * returned by `wrappedProducer` throw might be more desirable. */ onCacheReadFailure?: "throw" | "call-producer"; /** * If multiple, identical requests (i.e., calls to the function returned by * `wrapProducer`/`wrapBulkProducer`) are made that overlap in time (i.e., one * has started, but not yet finished, at the time another starts), and * multiple of these requests would be forwarded to the wrapped producer * [because there's no cached value to satisfy them], these requests can be * "deduplicated", so that only one request (which'll still be a bulk request * in the case of wrapBulkProducer) is made to the underlying producer, and * its response is used for all the overlapping requests. This setting * controls the maximum number of seconds that are allowed to have elapsed * between the current request and the first of the overlapping requests, if * this deduplication is to occur. I.e., if a request occurred greater than * `collapseOverlappingRequestsTime` seconds after the earliest, identical, * overlapping request, it will not be merged with the prior one, and instead * a new request will go to the producer */ collapseOverlappingRequestsTime?: number; /** * A custom logger to use (optional). */ logger?: Logger; }; /** * Fundamentally, this function takes a function that returns values (likely * without the help of a cache), and returns another function that's a drop-in * replacement for the first, except that it tries to lookup and reuse prior * results from a cache, before calling the underlying user-provided function. * * Note that any supplemental resources returned by the producer will be * cached but not returned to the caller. * * The wrapped function is generic over the specific id of an incoming request * so that the result's content type is narrowed (when `Spec` is a union of * cache key shapes) to the variants compatible with that id. * * ## AbortSignal support * * The returned function accepts an optional `{ signal }` parameter. The signal * is propagated differently depending on the request path: * * - **Uncacheable requests** (per `isCacheable`): the signal is forwarded * directly to the producer, so the producer can use it to abort its own work * (e.g., cancel an outgoing fetch). These calls are never collapsed. * * - **Cacheable requests**: the signal is forwarded to `cache.get()`, so the * store read can be aborted. If the signal fires before the cache read * completes, the function throws without ever contacting the producer. * Once the cache read resolves and the producer must be called, the signal is * **not** forwarded to the producer — because that call goes through the * `collapsedTaskCreator`, which may be sharing the same underlying producer * call with other callers who have not aborted. However, the caller's wait * for the producer result is **raced** against the signal, so the caller can * bail out immediately without waiting for the producer to finish. * * Critically, bailing out does NOT prevent the producer's result from being * stored: `callProducerAndStore` always fires a (non-awaited) `cache.store()` * after the producer resolves, so the work is never wasted. The trade-off is * that the producer itself cannot observe the signal to cancel its own * in-progress work (e.g., an outgoing HTTP request). Supporting that would * require either (a) aborting the shared task only when *all* callers have * aborted, which adds significant complexity, or (b) giving up request * collapsing for callers that pass a signal, which would defeat its purpose. * * @param cache - An instance of the cache class. This is where values returned * by the producer (see below) will actually be stored. * * @param options - See `WrapProducerOptions` for details. * * @param producer - The function that's actually responsible for returning the * result that will be sent to the user and/or stored in the cache. It acts as * the origin or "producer" for the cache. This function is passed the request * (id and params) along with the caller's cache directives, which may be * needed in case this producer function is itself backed by a cache, and it * needs to decide whether to contact its origin. */ export default function wrapProducer(cache: PublicInterface>, options: WrapProducerOptions | undefined, producer: RequestPairedProducer): { (req: PartialConsumerRequest, options?: { signal?: AbortSignal; }): Promise, Validators, Params>>; cache: PublicInterface>; }; export declare function isRequestingCacheBypass(dirs: ReadonlyDeep): boolean; //# sourceMappingURL=wrapProducer.d.ts.map