@cacheable/utils
Advanced tools
+903
| import { Keyv } from "keyv"; | ||
| //#region src/shorthand-time.d.ts | ||
| /** | ||
| * Converts a shorthand time string or number into milliseconds. | ||
| * The shorthand can be a string like '1s', '2m', '3h', '4d', or a number representing milliseconds. | ||
| * If the input is undefined, it returns undefined. | ||
| * If the input is a string that does not match the expected format, it throws an error. | ||
| * @param shorthand - A shorthand time string or number representing milliseconds. | ||
| * @returns The equivalent time in milliseconds or undefined. | ||
| */ | ||
| declare const shorthandToMilliseconds: (shorthand?: string | number) => number | undefined; | ||
| /** | ||
| * Converts a shorthand time string or number into a timestamp. | ||
| * If the shorthand is undefined, it returns the current date's timestamp. | ||
| * If the shorthand is a valid time format, it adds that duration to the current date's timestamp. | ||
| * @param shorthand - A shorthand time string or number representing milliseconds. | ||
| * @param fromDate - An optional Date object to calculate from. Defaults to the current date if not provided. | ||
| * @returns The timestamp in milliseconds since epoch. | ||
| */ | ||
| declare const shorthandToTime: (shorthand?: string | number, fromDate?: Date) => number; | ||
| //#endregion | ||
| //#region src/cache-tags.d.ts | ||
| /** | ||
| * Options for constructing a {@link CacheTags}. | ||
| * @typedef {Object} CacheTagsOptions | ||
| * @property {Keyv} store - The Keyv store used to persist tag versions and key snapshots. | ||
| * @property {string} [namespace] - An optional namespace that isolates this service's tags | ||
| * and keys from others sharing the same store. Defaults to `"default"`. | ||
| * @property {boolean} [enabled] - Whether the service is enabled. While disabled, every method | ||
| * is a no-op: read methods return their neutral value ({@link CacheTags.isKeyFresh} returns | ||
| * `true`, {@link CacheTags.isKeyStale} returns `false`, etc.) and writes are skipped. The | ||
| * service must be explicitly enabled to use tags. Defaults to `true`. | ||
| * @property {(error: unknown) => void} [onError] - Invoked with errors from non-blocking | ||
| * (fire-and-forget) operations, which cannot be thrown to the caller. Defaults to ignoring them. | ||
| */ | ||
| type CacheTagsOptions = { | ||
| store: Keyv; | ||
| namespace?: string; | ||
| enabled?: boolean; | ||
| onError?: (error: unknown) => void; | ||
| }; | ||
| /** | ||
| * Options for {@link CacheTags.setKeyTags}. | ||
| * @typedef {Object} SetKeyTagsOptions | ||
| * @property {number} [ttl] - Time-to-live in milliseconds for the key's tag snapshot. Should | ||
| * match the TTL of the cached value it tracks so the snapshot expires alongside it. If omitted, | ||
| * the snapshot does not expire. | ||
| * @property {boolean} [nonBlocking] - When `true`, the snapshot write is fire-and-forget: | ||
| * the call resolves immediately and failures are reported via the `onError` option. | ||
| */ | ||
| type SetKeyTagsOptions = { | ||
| ttl?: number; | ||
| nonBlocking?: boolean; | ||
| }; | ||
| /** | ||
| * Options for {@link CacheTags.removeKey} and {@link CacheTags.removeKeys}. | ||
| * @typedef {Object} RemoveKeysOptions | ||
| * @property {boolean} [nonBlocking] - When `true`, the removal is fire-and-forget: | ||
| * the call resolves immediately and failures are reported via the `onError` option. | ||
| */ | ||
| type RemoveKeysOptions = { | ||
| nonBlocking?: boolean; | ||
| }; | ||
| /** | ||
| * The metadata stored for a tagged key. It records the version of each tag at the moment the key | ||
| * was written, allowing {@link CacheTags.isKeyFresh} to detect later invalidations. | ||
| * @typedef {Object} KeyTagEntry | ||
| * @property {Record<string, number>} tags - A snapshot mapping each tag name to its version at set time. | ||
| */ | ||
| type KeyTagEntry = { | ||
| tags: Record<string, number>; | ||
| }; | ||
| /** | ||
| * Provides tag-based cache invalidation on top of any {@link Keyv} store. It is store-agnostic and | ||
| * requires no adapter changes. | ||
| * | ||
| * The service uses a lazy invalidation model rather than scanning and deleting keys. Each tag has a | ||
| * monotonically increasing version counter; {@link CacheTags.invalidateTag} simply increments | ||
| * it. When a key is tagged via {@link CacheTags.setKeyTags}, a snapshot of its tags' current | ||
| * versions is stored alongside it. {@link CacheTags.isKeyFresh} compares that snapshot against | ||
| * the live versions — if any tag has been incremented since, the key is considered stale. Stale | ||
| * entries are not deleted explicitly; they are expected to fall out of the cache via their TTL. | ||
| * | ||
| * This keeps invalidation constant-time regardless of how many keys reference a tag, at the cost of | ||
| * one additional `isKeyFresh` read per cache lookup. | ||
| * | ||
| * The service can be disabled via the `enabled` option or property so integrations pay no cost for | ||
| * untagged workloads: while disabled, every method is a no-op — reads return their neutral value | ||
| * and writes are skipped. The service must be explicitly enabled to use tags; it never enables | ||
| * itself, which keeps behavior consistent across distributed instances sharing a store. | ||
| * | ||
| * All metadata is written under a reserved prefix so it cannot collide with user keys: | ||
| * - `--cacheable--tags--:<namespace>:tag:<tag>` → integer version counter (stored without TTL). | ||
| * - `--cacheable--tags--:<namespace>:key:<key>` → the {@link KeyTagEntry} snapshot. | ||
| * | ||
| * Note: the read-version-then-write-snapshot sequence in `setKeyTags` is not atomic across | ||
| * processes. A concurrent `invalidateTag` running between the read and the write can leave a freshly | ||
| * written key referencing a stale version. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const cacheTags = new CacheTags({ store: new Keyv(), namespace: 'app' }); | ||
| * await cacheTags.setKeyTags('user:42', ['users', 'org:7'], { ttl: 3600000 }); | ||
| * await cacheTags.isKeyFresh('user:42'); // true | ||
| * await cacheTags.invalidateTag('users'); | ||
| * await cacheTags.isKeyFresh('user:42'); // false | ||
| * ``` | ||
| */ | ||
| declare class CacheTags { | ||
| private readonly _store; | ||
| private readonly _namespace; | ||
| private _enabled; | ||
| private readonly _onError?; | ||
| /** | ||
| * Creates a new tag service. | ||
| * @param {CacheTagsOptions} options - The store, optional namespace, enabled state, and | ||
| * non-blocking error handler to use. | ||
| */ | ||
| constructor(options: CacheTagsOptions); | ||
| /** | ||
| * The Keyv store backing this service. | ||
| * @returns {Keyv} The store provided to the constructor. | ||
| */ | ||
| get store(): Keyv; | ||
| /** | ||
| * The namespace isolating this service's tags and keys within the store. | ||
| * @returns {string} The configured namespace, or `"default"` if none was provided. | ||
| */ | ||
| get namespace(): string; | ||
| /** | ||
| * Whether the service is enabled. While disabled, every method is a no-op — read methods | ||
| * return their neutral value and writes are skipped — so integrations pay no extra store | ||
| * reads for untagged workloads. The service must be explicitly enabled to use tags; it never | ||
| * enables itself. | ||
| * @returns {boolean} Whether the service is enabled. | ||
| */ | ||
| get enabled(): boolean; | ||
| /** | ||
| * Sets whether the service is enabled. | ||
| * @param {boolean} enabled Whether the service is enabled. | ||
| */ | ||
| set enabled(enabled: boolean); | ||
| /** | ||
| * Builds the reserved store key under which a tag's version counter is stored. | ||
| * @param tag - The tag name. | ||
| * @returns {string} The namespaced store key for the tag's version. | ||
| */ | ||
| private tagKey; | ||
| /** | ||
| * Builds the reserved store key under which a cache key's tag snapshot is stored. | ||
| * @param key - The cache key being tagged. | ||
| * @returns {string} The namespaced store key for the key's snapshot. | ||
| */ | ||
| private keyEntryKey; | ||
| /** | ||
| * Builds the common prefix shared by every key-snapshot entry in this namespace. Used to filter | ||
| * key entries when iterating the store. | ||
| * @returns {string} The namespaced key-entry prefix. | ||
| */ | ||
| private keyPrefix; | ||
| /** | ||
| * Reads the current version of a single tag. | ||
| * @param tag - The tag name. | ||
| * @returns {Promise<number>} The tag's version, or `0` if it has never been invalidated. | ||
| */ | ||
| private getTagVersion; | ||
| /** | ||
| * Reads the current versions of multiple tags in a single batched store read. | ||
| * @param tags - The tag names to look up. | ||
| * @returns {Promise<number[]>} The versions in the same order as `tags`; entries that have never | ||
| * been invalidated resolve to `0`. Returns an empty array when `tags` is empty. | ||
| */ | ||
| private getTagVersions; | ||
| /** | ||
| * Reports a fire-and-forget failure to the `onError` handler, if one was provided. | ||
| * @param error - The error raised by the non-blocking operation. | ||
| */ | ||
| private handleNonBlockingError; | ||
| /** | ||
| * Reads the version snapshot of each tag and writes the key's tag snapshot to the store. | ||
| * @param key - The cache key to tag. | ||
| * @param tags - The tags to associate with the key. | ||
| * @param ttl - Time-to-live in milliseconds for the snapshot. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been written. | ||
| */ | ||
| private writeKeyTags; | ||
| /** | ||
| * Associates a cache key with a set of tags by recording a snapshot of each tag's current | ||
| * version. Call this whenever you write a fresh value to the cache. Duplicate tags are ignored. | ||
| * No-op while the service is disabled. | ||
| * @param key - The cache key to tag. | ||
| * @param tags - The tags to associate with the key. | ||
| * @param {SetKeyTagsOptions} [options] - Optional settings, such as a `ttl` for the snapshot or | ||
| * `nonBlocking` to fire-and-forget the write. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been written, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| setKeyTags(key: string, tags: string[], options?: SetKeyTagsOptions): Promise<void>; | ||
| /** | ||
| * Removes a key's tag snapshot. After this, {@link CacheTags.isKeyFresh} returns `false` | ||
| * for the key. Use when the cached value itself is deleted. No-op while the service is | ||
| * disabled. | ||
| * @param key - The cache key whose snapshot should be removed. | ||
| * @param {RemoveKeysOptions} [options] - Optional settings, such as `nonBlocking` to | ||
| * fire-and-forget the removal. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been deleted, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| removeKey(key: string, options?: RemoveKeysOptions): Promise<void>; | ||
| /** | ||
| * Removes multiple keys' tag snapshots in a single batched store delete. After this, | ||
| * {@link CacheTags.isKeyFresh} returns `false` for each key. An empty list is a no-op, as is | ||
| * the entire call while the service is disabled. | ||
| * @param keys - The cache keys whose snapshots should be removed. | ||
| * @param {RemoveKeysOptions} [options] - Optional settings, such as `nonBlocking` to | ||
| * fire-and-forget the removal. | ||
| * @returns {Promise<void>} Resolves once the snapshots have been deleted, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| removeKeys(keys: string[], options?: RemoveKeysOptions): Promise<void>; | ||
| /** | ||
| * Determines whether a key's cached value can still be trusted. A key is fresh only when a | ||
| * snapshot exists for it and every tag in that snapshot still has the version it had at set time. | ||
| * A key with no tags is trivially fresh. Call this before returning a value from your cache. | ||
| * Always returns `true` while the service is disabled. | ||
| * @param key - The cache key to check. | ||
| * @returns {Promise<boolean>} `true` if the key is still fresh; `false` if it is unknown or any of | ||
| * its tags has been invalidated since the snapshot was taken. | ||
| */ | ||
| isKeyFresh(key: string): Promise<boolean>; | ||
| /** | ||
| * Determines whether a key's cached value is known to be stale due to tag invalidation. This is | ||
| * the complement of {@link CacheTags.isKeyFresh} for tagged keys, but treats keys without a | ||
| * snapshot as not stale — making it safe to call for every cache lookup, including keys that were | ||
| * never tagged. Always returns `false` while the service is disabled. | ||
| * @param key - The cache key to check. | ||
| * @returns {Promise<boolean>} `true` only when a snapshot exists for the key and at least one of | ||
| * its tags has been invalidated since the snapshot was taken; `false` otherwise (including when | ||
| * the key has no snapshot). | ||
| */ | ||
| isKeyStale(key: string): Promise<boolean>; | ||
| /** | ||
| * Determines which of the given keys are known to be stale due to tag invalidation, using two | ||
| * batched store reads regardless of how many keys are checked: one for the snapshots and one for | ||
| * the union of their tag versions. Keys without a snapshot are not considered stale. Returns an | ||
| * empty array while the service is disabled. | ||
| * @param keys - The cache keys to check. | ||
| * @returns {Promise<string[]>} The subset of `keys` whose snapshot references at least one tag | ||
| * that has been invalidated since the snapshot was taken. | ||
| */ | ||
| getStaleKeys(keys: string[]): Promise<string[]>; | ||
| /** | ||
| * Returns the tags currently associated with a key. Returns `undefined` while the service is | ||
| * disabled. | ||
| * @param key - The cache key to look up. | ||
| * @returns {Promise<string[] | undefined>} The tag names from the key's snapshot, or `undefined` | ||
| * if the key has no snapshot. | ||
| */ | ||
| getTags(key: string): Promise<string[] | undefined>; | ||
| /** | ||
| * Returns all cache keys whose snapshot references the given tag. This scans every key entry in | ||
| * the namespace via the Keyv iterator, making it an `O(N)` operation intended for debugging and | ||
| * tests rather than hot paths. Returns an empty array if the underlying store exposes no iterator | ||
| * or while the service is disabled. | ||
| * @param tag - The tag to search for. | ||
| * @returns {Promise<string[]>} The cache keys (with the reserved prefix stripped) referencing the tag. | ||
| */ | ||
| getKeysByTag(tag: string): Promise<string[]>; | ||
| /** | ||
| * Invalidates a single tag by incrementing its version counter. Every key whose snapshot | ||
| * references this tag becomes stale immediately. Runs in constant time regardless of how many | ||
| * keys reference the tag. No-op while the service is disabled. | ||
| * @param tag - The tag to invalidate. | ||
| * @returns {Promise<string[]>} A single-element array containing the invalidated tag, or an | ||
| * empty array while the service is disabled. | ||
| */ | ||
| invalidateTag(tag: string): Promise<string[]>; | ||
| /** | ||
| * Invalidates multiple tags by incrementing each of their version counters in a single batched | ||
| * store write. Duplicate tags are bumped once. An empty list is a no-op, as is the entire call | ||
| * while the service is disabled. | ||
| * @param tags - The tags to invalidate. | ||
| * @returns {Promise<string[]>} The `tags` argument as provided (including any duplicates), or | ||
| * an empty array while the service is disabled. | ||
| */ | ||
| invalidateTags(tags: string[]): Promise<string[]>; | ||
| } | ||
| //#endregion | ||
| //#region src/cacheable-item-types.d.ts | ||
| /** | ||
| * CacheableItem | ||
| * @typedef {Object} CacheableItem | ||
| * @property {string} key - The key of the cacheable item | ||
| * @property {any} value - The value of the cacheable item | ||
| * @property {number|string} [ttl] - Time to Live - If you set a number it is miliseconds, if you set a string it is a human-readable | ||
| * format such as `1s` for 1 second or `1h` for 1 hour. Setting undefined means that it will use the default time-to-live. If both are | ||
| * undefined then it will not have a time-to-live. | ||
| */ | ||
| type CacheableItem = { | ||
| key: string; | ||
| value: any; | ||
| ttl?: number | string; | ||
| }; | ||
| /** | ||
| * CacheableStoreItem | ||
| * @typedef {Object} CacheableStoreItem | ||
| * @property {string} key - The key of the cacheable store item | ||
| * @property {any} value - The value of the cacheable store item | ||
| * @property {number} [expires] - The expiration time in milliseconds since epoch. If not set, the item does not expire. | ||
| */ | ||
| type CacheableStoreItem = { | ||
| key: string; | ||
| value: any; | ||
| expires?: number; | ||
| }; | ||
| //#endregion | ||
| //#region src/coalesce-async.d.ts | ||
| /** | ||
| * Enqueue a promise for the group identified by `key`. | ||
| * | ||
| * All requests received for the same key while a request for that key | ||
| * is already being executed will wait. Once the running request settles | ||
| * then all the waiting requests in the group will settle, too. | ||
| * This minimizes how many times the function itself runs at the same time. | ||
| * This function resolves or rejects according to the given function argument. | ||
| * | ||
| * @url https://github.com/douglascayers/promise-coalesce | ||
| */ | ||
| declare function coalesceAsync<T>( | ||
| /** | ||
| * Any identifier to group requests together. | ||
| */ | ||
| key: string, | ||
| /** | ||
| * The function to run. | ||
| */ | ||
| fnc: () => T | PromiseLike<T>): Promise<T>; | ||
| //#endregion | ||
| //#region src/hash.d.ts | ||
| declare enum HashAlgorithm { | ||
| SHA256 = "SHA-256", | ||
| SHA384 = "SHA-384", | ||
| SHA512 = "SHA-512", | ||
| DJB2 = "djb2", | ||
| FNV1 = "fnv1", | ||
| MURMER = "murmer", | ||
| CRC32 = "crc32" | ||
| } | ||
| type HashOptions = { | ||
| algorithm?: HashAlgorithm; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type HashToNumberOptions = HashOptions & { | ||
| min?: number; | ||
| max?: number; | ||
| hashLength?: number; | ||
| }; | ||
| /** | ||
| * Hashes an object asynchronously using the specified cryptographic algorithm. | ||
| * This method should be used for cryptographic algorithms (SHA-256, SHA-384, SHA-512). | ||
| * For non-cryptographic algorithms, use hashSync() for better performance. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use | ||
| * @returns {Promise<string>} The hash of the object | ||
| */ | ||
| declare function hash(object: any, options?: HashOptions): Promise<string>; | ||
| /** | ||
| * Hashes an object synchronously using the specified non-cryptographic algorithm. | ||
| * This method should be used for non-cryptographic algorithms (DJB2, FNV1, MURMER, CRC32). | ||
| * For cryptographic algorithms, use hash() instead. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use | ||
| * @returns {string} The hash of the object | ||
| */ | ||
| declare function hashSync(object: any, options?: HashOptions): string; | ||
| /** | ||
| * Hashes an object asynchronously and converts it to a number within a specified range. | ||
| * This method should be used for cryptographic algorithms (SHA-256, SHA-384, SHA-512). | ||
| * For non-cryptographic algorithms, use hashToNumberSync() for better performance. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use including min/max range | ||
| * @returns {Promise<number>} A number within the specified range | ||
| */ | ||
| declare function hashToNumber(object: any, options?: HashToNumberOptions): Promise<number>; | ||
| /** | ||
| * Hashes an object synchronously and converts it to a number within a specified range. | ||
| * This method should be used for non-cryptographic algorithms (DJB2, FNV1, MURMER, CRC32). | ||
| * For cryptographic algorithms, use hashToNumber() instead. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use including min/max range | ||
| * @returns {number} A number within the specified range | ||
| */ | ||
| declare function hashToNumberSync(object: any, options?: HashToNumberOptions): number; | ||
| //#endregion | ||
| //#region src/is-keyv-instance.d.ts | ||
| declare function isKeyvInstance(keyv: any): boolean; | ||
| //#endregion | ||
| //#region src/is-object.d.ts | ||
| declare function isObject<T = Record<string, unknown>>(value: unknown): value is T; | ||
| //#endregion | ||
| //#region src/less-than.d.ts | ||
| declare function lessThan(number1?: number, number2?: number): boolean; | ||
| //#endregion | ||
| //#region src/ttl.d.ts | ||
| /** | ||
| * A per-store time-to-live override. Each field is a normal TTL (a number in milliseconds or a | ||
| * human-readable shorthand such as `1s`, `1m`, `1h`, `1d`) applied to that specific store. Fields | ||
| * left undefined fall back to that store's own default TTL resolution. | ||
| */ | ||
| type PerStoreTtl = { | ||
| /** | ||
| * The time-to-live to use for the primary store. | ||
| */ | ||
| primary?: number | string; | ||
| /** | ||
| * The time-to-live to use for the secondary store. | ||
| */ | ||
| secondary?: number | string; | ||
| }; | ||
| /** | ||
| * Normalizes a TTL input into per-store milliseconds. When given an object it resolves the | ||
| * `primary` and `secondary` fields independently; when given a number or shorthand string it | ||
| * applies the same value to both stores. Undefined fields (or undefined input) resolve to | ||
| * `undefined` so the caller can fall back to its own default TTL. | ||
| * @param ttl - The TTL input: a number (ms), a shorthand string, or a {@link PerStoreTtl} object. | ||
| * @returns {{ primary?: number; secondary?: number }} The resolved per-store TTLs in milliseconds. | ||
| */ | ||
| declare function resolvePerStoreTtl(ttl?: number | string | PerStoreTtl): { | ||
| primary?: number; | ||
| secondary?: number; | ||
| }; | ||
| /** | ||
| * Converts a exspires value to a TTL value. | ||
| * @param expires - The expires value to convert. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if the expires value is not valid. | ||
| */ | ||
| declare function getTtlFromExpires(expires: number | undefined): number | undefined; | ||
| /** | ||
| * Get the TTL value from the cacheableTtl, primaryTtl, and secondaryTtl values. | ||
| * @param cacheableTtl - The cacheableTtl value to use. | ||
| * @param primaryTtl - The primaryTtl value to use. | ||
| * @param secondaryTtl - The secondaryTtl value to use. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if all values are undefined. | ||
| */ | ||
| declare function getCascadingTtl(cacheableTtl?: number | string, primaryTtl?: number, secondaryTtl?: number): number | undefined; | ||
| /** | ||
| * Calculate the TTL value from the expires value. If the ttl is undefined, it will be set to the expires value. If the | ||
| * expires value is undefined, it will be set to the ttl value. If both values are defined, the smaller of the two will be used. | ||
| * @param ttl | ||
| * @param expires | ||
| * @returns | ||
| */ | ||
| declare function calculateTtlFromExpiration(ttl: number | undefined, expires: number | undefined): number | undefined; | ||
| //#endregion | ||
| //#region src/memoize.d.ts | ||
| type CacheInstance = { | ||
| get: (key: string) => Promise<any | undefined>; | ||
| has: (key: string) => Promise<boolean>; | ||
| set: (key: string, value: any, ttl?: number | string | PerStoreTtl) => Promise<void>; | ||
| on: (event: string, listener: (...args: any[]) => void) => void; | ||
| emit: (event: string, ...args: any[]) => boolean; | ||
| }; | ||
| type CacheSyncInstance = { | ||
| get: (key: string) => any | undefined; | ||
| has: (key: string) => boolean; | ||
| set: (key: string, value: any, ttl?: number | string) => void; | ||
| on: (event: string, listener: (...args: any[]) => void) => void; | ||
| emit: (event: string, ...args: any[]) => boolean; | ||
| }; | ||
| type GetOrSetKey = string | ((options?: GetOrSetOptions) => string); | ||
| type GetOrSetThrowErrorsContext = "function" | "store"; | ||
| type GetOrSetFunctionOptions = { | ||
| ttl?: number | string; | ||
| cacheErrors?: boolean; | ||
| /** Whether or not to throw errors: | ||
| * - `false` (default) - do not throw any errors | ||
| * - `true` - throw any error | ||
| * - `"function"` - only throw errors that occur in the provided function / setter | ||
| * - `"store"` - only throw errors that occur when getting/setting the cache | ||
| */ | ||
| throwErrors?: boolean | GetOrSetThrowErrorsContext; | ||
| /** | ||
| * If set, this will bypass the instances nonBlocking setting for the get call. | ||
| * @type {boolean} | ||
| */ | ||
| nonBlocking?: boolean; | ||
| }; | ||
| type GetOrSetOptions = Omit<GetOrSetFunctionOptions, "ttl"> & { | ||
| ttl?: number | string | PerStoreTtl; | ||
| cacheId?: string; | ||
| cache: CacheInstance; | ||
| }; | ||
| /** | ||
| * Options for {@link getOrSetSync}, the synchronous counterpart to {@link GetOrSetOptions}. It | ||
| * targets a {@link CacheSyncInstance} and its `ttl` is always a single value (a number in | ||
| * milliseconds or a shorthand string), never a per-store object. The inherited `nonBlocking` | ||
| * option has no effect on a single, synchronous store. | ||
| */ | ||
| type GetOrSetSyncOptions = GetOrSetFunctionOptions & { | ||
| cache: CacheSyncInstance; | ||
| }; | ||
| /** | ||
| * A cache key for {@link getOrSetSync}: either a string or a function that derives the key from the | ||
| * resolved {@link GetOrSetSyncOptions}. | ||
| */ | ||
| type GetOrSetSyncKey = string | ((options?: GetOrSetSyncOptions) => string); | ||
| type CreateWrapKey = (function_: AnyFunction, arguments_: any[], options?: WrapFunctionOptions) => string; | ||
| type WrapFunctionOptions = { | ||
| ttl?: number | string; | ||
| keyPrefix?: string; | ||
| createKey?: CreateWrapKey; | ||
| cacheErrors?: boolean; | ||
| cacheId?: string; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type WrapOptions = Omit<WrapFunctionOptions, "ttl"> & { | ||
| ttl?: number | string | PerStoreTtl; | ||
| cache: CacheInstance; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type WrapSyncOptions = WrapFunctionOptions & { | ||
| cache: CacheSyncInstance; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type AnyFunction = (...arguments_: any[]) => any; | ||
| declare function wrapSync<T>(function_: AnyFunction, options: WrapSyncOptions): AnyFunction; | ||
| declare function getOrSet<T>(key: GetOrSetKey, function_: () => Promise<T>, options: GetOrSetOptions): Promise<T | undefined>; | ||
| /** | ||
| * Synchronous counterpart to {@link getOrSet}. Reads `key` from the cache and, on a miss, computes | ||
| * the value with `function_`, stores it, and returns it. | ||
| * | ||
| * Unlike {@link getOrSet} there is no request coalescing: synchronous code runs to completion | ||
| * without interleaving, so concurrent callers cannot stampede the setter the way they can with an | ||
| * async cache. | ||
| * | ||
| * Error handling mirrors {@link getOrSet}: errors are emitted on the cache's `error` event, can be | ||
| * cached when `cacheErrors` is set, and can be rethrown selectively via `throwErrors` (`true` for | ||
| * any error, `"function"` for setter errors, `"store"` for cache read/write errors). | ||
| * | ||
| * @param key - The cache key, or a function that derives it from the resolved options. | ||
| * @param function_ - The setter invoked on a cache miss to compute the value. | ||
| * @param options - The {@link GetOrSetSyncOptions} including the target synchronous cache. | ||
| * @returns The cached or freshly computed value, or `undefined`. | ||
| */ | ||
| declare function getOrSetSync<T>(key: GetOrSetSyncKey, function_: () => T, options: GetOrSetSyncOptions): T | undefined; | ||
| declare function wrap<T>(function_: AnyFunction, options: WrapOptions): AnyFunction; | ||
| type CreateWrapKeyOptions = { | ||
| keyPrefix?: string; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| declare function createWrapKey(function_: AnyFunction, arguments_: any[], options?: CreateWrapKeyOptions): string; | ||
| //#endregion | ||
| //#region src/run-if-fn.d.ts | ||
| type Function_<P, T> = (...arguments_: P[]) => T; | ||
| declare function runIfFn<T, P>(valueOrFunction: T | Function_<P, T>, ...arguments_: P[]): T; | ||
| //#endregion | ||
| //#region src/sleep.d.ts | ||
| declare const sleep: (ms: number) => Promise<unknown>; | ||
| //#endregion | ||
| //#region src/stats.d.ts | ||
| /** | ||
| * A counter field that can be incremented or decremented via the unified | ||
| * {@link Stats.increment} / {@link Stats.decrement} API or an event map. | ||
| */ | ||
| type StatField = "hits" | "misses" | "gets" | "sets" | "deletes" | "clears" | "count"; | ||
| /** | ||
| * A duck-typed event emitter. This intentionally matches both `Hookified` | ||
| * (used by `cacheable`, `node-cache`, `memory`, `flat-cache`) and Node's | ||
| * built-in `EventEmitter` (used by `cache-manager`, `cacheable-request`) | ||
| * without adding a hard dependency on either. | ||
| */ | ||
| type StatsEmitter = { | ||
| on(event: string, listener: (...args: any[]) => void): unknown; | ||
| off?(event: string, listener: (...args: any[]) => void): unknown; | ||
| removeListener?(event: string, listener: (...args: any[]) => void): unknown; | ||
| }; | ||
| /** | ||
| * A custom handler invoked when a subscribed event fires. It receives the | ||
| * {@link Stats} instance and the raw event arguments (which may be positional, | ||
| * e.g. node-cache emits `(key, value)`). | ||
| */ | ||
| type StatsEventHandler = (stats: Stats, ...args: any[]) => void; | ||
| /** | ||
| * Maps an event name to the stat update it should perform: a single field to | ||
| * increment, an array of fields to increment, or a custom handler. | ||
| */ | ||
| type StatsEventMap = Record<string, StatField | StatField[] | StatsEventHandler>; | ||
| /** | ||
| * A counter field that can be recorded per key via {@link Stats.recordKey}. | ||
| * This is the subset of {@link StatField} that makes sense for a single key | ||
| * (`clears` and `count` are cache-wide). | ||
| */ | ||
| type KeyStatField = "hits" | "misses" | "gets" | "sets" | "deletes"; | ||
| /** | ||
| * Per-key statistics returned by {@link Stats.mostUsedKeys}, | ||
| * {@link Stats.leastUsedKeys}, and {@link Stats.keyStats}. | ||
| */ | ||
| type StatsKeyEntry = { | ||
| key: string; /** Total recorded operations for this key (sum of all fields). */ | ||
| count: number; | ||
| hits: number; | ||
| misses: number; | ||
| gets: number; | ||
| sets: number; | ||
| deletes: number; /** `hits / (hits + misses)` for this key, or `0` when there have been no lookups. */ | ||
| hitRate: number; | ||
| }; | ||
| /** | ||
| * A plain-object snapshot of a {@link Stats} instance, suitable for logging, | ||
| * metrics, or serialization. Returned by {@link Stats.toJSON}. | ||
| */ | ||
| type StatsSnapshot = { | ||
| enabled: boolean; | ||
| hits: number; | ||
| misses: number; | ||
| gets: number; | ||
| sets: number; | ||
| deletes: number; | ||
| clears: number; | ||
| vsize: number; | ||
| ksize: number; | ||
| count: number; | ||
| hitRate: number; | ||
| missRate: number; /** Number of unique keys currently tracked (0 when key tracking is off). */ | ||
| trackedKeys: number; | ||
| lastUpdated?: number; | ||
| lastReset?: number; | ||
| }; | ||
| type StatsOptions = { | ||
| /** Whether the stats are enabled. Defaults to `false`. */enabled?: boolean; /** Optionally subscribe to an emitter immediately on construction. */ | ||
| emitter?: StatsEmitter; /** The event map to use. Required when `emitter` is provided. */ | ||
| eventMap?: StatsEventMap; /** Track per-key statistics via {@link Stats.recordKey}. Defaults to `false`. */ | ||
| trackKeys?: boolean; | ||
| /** | ||
| * Safety cap on the number of unique keys tracked. When exceeded, the | ||
| * lowest-count keys are pruned, which keeps {@link Stats.mostUsedKeys} | ||
| * approximately accurate but makes {@link Stats.leastUsedKeys} unreliable. | ||
| * Unbounded when unset. | ||
| */ | ||
| maxTrackedKeys?: number; | ||
| }; | ||
| /** | ||
| * Event map for `@cacheable/node-cache` instances. node-cache emits with | ||
| * positional arguments (e.g. `set(key, value)`), and emits each lifecycle | ||
| * event exactly once, so the counts map cleanly. `flush` clears the cache data | ||
| * and `flush_stats` resets the stats counters, mirroring node-cache's | ||
| * `flushAll()` / `flushStats()` lifecycle. | ||
| * | ||
| * Presets for `cacheable` and `cache-manager` are intentionally not provided: | ||
| * their event streams emit per-store probes (and, for cache-manager, do not | ||
| * emit an event on a normal miss), so a simple map cannot faithfully reproduce | ||
| * their imperative stats. Wire those up with a custom map or imperative calls. | ||
| */ | ||
| declare const nodeCacheStatsEventMap: StatsEventMap; | ||
| /** | ||
| * Raw per-key counters stored in {@link Stats.trackedKeys}: the | ||
| * `hits`/`misses`/`gets`/`sets`/`deletes` totals for a single cache key. | ||
| */ | ||
| type KeyCounters = Record<KeyStatField, number>; | ||
| declare class Stats { | ||
| private _counters; | ||
| private _vsize; | ||
| private _ksize; | ||
| private _enabled; | ||
| private _lastUpdated; | ||
| private _lastReset; | ||
| private _subscriptions; | ||
| /** Backing store for the public {@link trackedKeys} read-only view. */ | ||
| private _trackedKeys; | ||
| private _trackKeys; | ||
| private _maxTrackedKeys; | ||
| constructor(options?: StatsOptions); | ||
| /** | ||
| * @returns {boolean} - Whether the stats are enabled | ||
| */ | ||
| get enabled(): boolean; | ||
| /** | ||
| * @param {boolean} enabled - Whether to enable the stats | ||
| */ | ||
| set enabled(enabled: boolean); | ||
| /** | ||
| * @returns {boolean} - Whether per-key statistics are tracked | ||
| */ | ||
| get trackKeys(): boolean; | ||
| /** | ||
| * @param {boolean} trackKeys - Whether to track per-key statistics | ||
| */ | ||
| set trackKeys(trackKeys: boolean); | ||
| /** | ||
| * @returns {number | undefined} - The cap on unique keys tracked, or | ||
| * `undefined` when unbounded | ||
| */ | ||
| get maxTrackedKeys(): number | undefined; | ||
| /** | ||
| * @param {number | undefined} maxTrackedKeys - The cap on unique keys | ||
| * tracked. Set `undefined` for unbounded. | ||
| */ | ||
| set maxTrackedKeys(maxTrackedKeys: number | undefined); | ||
| /** | ||
| * Per-key statistics, keyed by cache key, holding each key's raw | ||
| * `hits`/`misses`/`gets`/`sets`/`deletes` counters. Populated by | ||
| * {@link recordKey} when {@link trackKeys} is enabled; read `trackedKeys.size` | ||
| * for the number of unique keys currently tracked. The returned map is a | ||
| * read-only view — mutate per-key stats via {@link recordKey} / | ||
| * {@link clearKeys} / {@link reset}. | ||
| * @returns {ReadonlyMap<string, Readonly<KeyCounters>>} | ||
| * @readonly | ||
| */ | ||
| get trackedKeys(): ReadonlyMap<string, Readonly<KeyCounters>>; | ||
| /** | ||
| * @returns {number} - The number of hits | ||
| * @readonly | ||
| */ | ||
| get hits(): number; | ||
| /** | ||
| * @returns {number} - The number of misses | ||
| * @readonly | ||
| */ | ||
| get misses(): number; | ||
| /** | ||
| * @returns {number} - The number of gets | ||
| * @readonly | ||
| */ | ||
| get gets(): number; | ||
| /** | ||
| * @returns {number} - The number of sets | ||
| * @readonly | ||
| */ | ||
| get sets(): number; | ||
| /** | ||
| * @returns {number} - The number of deletes | ||
| * @readonly | ||
| */ | ||
| get deletes(): number; | ||
| /** | ||
| * @returns {number} - The number of clears | ||
| * @readonly | ||
| */ | ||
| get clears(): number; | ||
| /** | ||
| * @returns {number} - The vsize (value size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get vsize(): number; | ||
| /** | ||
| * @returns {number} - The ksize (key size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get ksize(): number; | ||
| /** | ||
| * @returns {number} - The count of the cache instance | ||
| * @readonly | ||
| */ | ||
| get count(): number; | ||
| /** | ||
| * The ratio of hits to total lookups (hits + misses). Returns `0` when there | ||
| * have been no lookups. | ||
| * @returns {number} - A value between 0 and 1 | ||
| * @readonly | ||
| */ | ||
| get hitRate(): number; | ||
| /** | ||
| * The ratio of misses to total lookups (hits + misses). Returns `0` when | ||
| * there have been no lookups. | ||
| * @returns {number} - A value between 0 and 1 | ||
| * @readonly | ||
| */ | ||
| get missRate(): number; | ||
| /** | ||
| * The timestamp (ms since epoch) of the last mutation while enabled, or | ||
| * `undefined` if there have been none since the last reset. | ||
| * @returns {number | undefined} | ||
| * @readonly | ||
| */ | ||
| get lastUpdated(): number | undefined; | ||
| /** | ||
| * The timestamp (ms since epoch) of the last {@link reset}/{@link clear}, or | ||
| * `undefined` if it has never been reset. | ||
| * @returns {number | undefined} | ||
| * @readonly | ||
| */ | ||
| get lastReset(): number | undefined; | ||
| /** | ||
| * Increment a counter field by `amount` (default `1`). No-op when disabled. | ||
| * @param {StatField} field - The counter to increment | ||
| * @param {number} amount - The amount to add (default 1) | ||
| */ | ||
| increment(field: StatField, amount?: number): void; | ||
| /** | ||
| * Decrement a counter field by `amount` (default `1`). No-op when disabled. | ||
| * @param {StatField} field - The counter to decrement | ||
| * @param {number} amount - The amount to subtract (default 1) | ||
| */ | ||
| decrement(field: StatField, amount?: number): void; | ||
| incrementHits(amount?: number): void; | ||
| incrementMisses(amount?: number): void; | ||
| incrementGets(amount?: number): void; | ||
| incrementSets(amount?: number): void; | ||
| incrementDeletes(amount?: number): void; | ||
| incrementClears(amount?: number): void; | ||
| incrementVSize(value: any): void; | ||
| decreaseVSize(value: any): void; | ||
| incrementKSize(key: string): void; | ||
| decreaseKSize(key: string): void; | ||
| incrementCount(amount?: number): void; | ||
| decreaseCount(amount?: number): void; | ||
| setCount(count: number): void; | ||
| roughSizeOfString(value: string): number; | ||
| roughSizeOfObject(object: any): number; | ||
| /** | ||
| * Enable stat tracking. Equivalent to setting {@link enabled} to `true`. | ||
| */ | ||
| enable(): void; | ||
| /** | ||
| * Disable stat tracking. Equivalent to setting {@link enabled} to `false`. | ||
| */ | ||
| disable(): void; | ||
| /** | ||
| * Reset all counters to zero and record the reset timestamp. Alias of | ||
| * {@link reset}. | ||
| */ | ||
| clear(): void; | ||
| reset(): void; | ||
| resetStoreValues(): void; | ||
| /** | ||
| * @returns {StatsSnapshot} - A plain-object snapshot of the current stats, | ||
| * including computed `hitRate`/`missRate` and timestamps. | ||
| */ | ||
| toJSON(): StatsSnapshot; | ||
| /** | ||
| * @returns {StatsSnapshot} - A plain-object snapshot of the current stats. | ||
| * Alias of {@link toJSON}. | ||
| */ | ||
| snapshot(): StatsSnapshot; | ||
| /** | ||
| * Record an operation against a specific key for per-key statistics. No-op | ||
| * unless both {@link enabled} and {@link trackKeys} are `true`. | ||
| * @param {string} key - The cache key the operation touched | ||
| * @param {KeyStatField} field - The per-key counter to increment | ||
| * @param {number} amount - The amount to add (default 1) | ||
| */ | ||
| recordKey(key: string, field: KeyStatField, amount?: number): void; | ||
| /** | ||
| * The most-used keys, sorted descending. Sorts by total recorded operations, | ||
| * or by a single field when `field` is provided. Ties order by key. | ||
| * @param {number} limit - Maximum entries to return (default 100) | ||
| * @param {KeyStatField} [field] - Optionally rank by one counter (e.g. "hits") | ||
| * @returns {StatsKeyEntry[]} | ||
| */ | ||
| mostUsedKeys(limit?: number, field?: KeyStatField): StatsKeyEntry[]; | ||
| /** | ||
| * The least-used keys, sorted ascending. Sorts by total recorded operations, | ||
| * or by a single field when `field` is provided. Ties order by key. Note: | ||
| * only keys that have been recorded at least once can be ranked, and when | ||
| * {@link maxTrackedKeys} pruning has occurred the true least-used keys may | ||
| * have been evicted. | ||
| * @param {number} limit - Maximum entries to return (default 100) | ||
| * @param {KeyStatField} [field] - Optionally rank by one counter (e.g. "gets") | ||
| * @returns {StatsKeyEntry[]} | ||
| */ | ||
| leastUsedKeys(limit?: number, field?: KeyStatField): StatsKeyEntry[]; | ||
| /** | ||
| * @param {string} key - The key to look up | ||
| * @returns {StatsKeyEntry | undefined} - The per-key statistics, or | ||
| * `undefined` if the key has not been recorded | ||
| */ | ||
| keyStats(key: string): StatsKeyEntry | undefined; | ||
| /** | ||
| * Clear all per-key statistics without touching the aggregate counters. | ||
| */ | ||
| clearKeys(): void; | ||
| private totalOf; | ||
| private toKeyEntry; | ||
| private sortedKeyEntries; | ||
| /** | ||
| * When over {@link maxTrackedKeys}, prune the lowest-count keys down to 90% | ||
| * of the cap (batched so the sort cost amortizes across inserts). The key | ||
| * that was just recorded is never pruned. | ||
| */ | ||
| private pruneTrackedKeys; | ||
| /** | ||
| * Subscribe to an emitter so that matching events automatically update the | ||
| * stats. Counting is gated by {@link enabled}, so you may subscribe first and | ||
| * toggle enablement later. Call {@link unsubscribe} to detach. | ||
| * @param {StatsEmitter} emitter - The emitter to listen on | ||
| * @param {StatsEventMap} eventMap - The event-to-stat mapping (e.g. | ||
| * {@link nodeCacheStatsEventMap} or a custom map) | ||
| */ | ||
| subscribe(emitter: StatsEmitter, eventMap: StatsEventMap): void; | ||
| /** | ||
| * Detach listeners previously attached via {@link subscribe}. When `emitter` | ||
| * is provided, only that emitter's listeners are removed; otherwise all are. | ||
| * @param {StatsEmitter} [emitter] - The emitter to detach from | ||
| */ | ||
| unsubscribe(emitter?: StatsEmitter): void; | ||
| private applyEvent; | ||
| private touch; | ||
| } | ||
| //#endregion | ||
| export { type AnyFunction, type CacheInstance, type CacheSyncInstance, CacheTags, type CacheTagsOptions, type CacheableItem, type CacheableStoreItem, type CreateWrapKey, type CreateWrapKeyOptions, type GetOrSetFunctionOptions, type GetOrSetKey, type GetOrSetOptions, type GetOrSetSyncKey, type GetOrSetSyncOptions, HashAlgorithm, type HashOptions, type HashToNumberOptions, type KeyCounters, type KeyStatField, type KeyTagEntry, type PerStoreTtl, type RemoveKeysOptions, type SetKeyTagsOptions, type StatField, Stats, type StatsEmitter, type StatsEventHandler, type StatsEventMap, type StatsKeyEntry, type StatsOptions, type StatsSnapshot, type WrapFunctionOptions, type WrapOptions, type WrapSyncOptions, calculateTtlFromExpiration, coalesceAsync, createWrapKey, getCascadingTtl, getOrSet, getOrSetSync, getTtlFromExpires, hash, hashSync, hashToNumber, hashToNumberSync, isKeyvInstance, isObject, lessThan, nodeCacheStatsEventMap, resolvePerStoreTtl, runIfFn, shorthandToMilliseconds, shorthandToTime, sleep, wrap, wrapSync }; |
+1355
| import { Hashery } from "hashery"; | ||
| import { Keyv } from "keyv"; | ||
| //#region src/shorthand-time.ts | ||
| /** | ||
| * Converts a shorthand time string or number into milliseconds. | ||
| * The shorthand can be a string like '1s', '2m', '3h', '4d', or a number representing milliseconds. | ||
| * If the input is undefined, it returns undefined. | ||
| * If the input is a string that does not match the expected format, it throws an error. | ||
| * @param shorthand - A shorthand time string or number representing milliseconds. | ||
| * @returns The equivalent time in milliseconds or undefined. | ||
| */ | ||
| const shorthandToMilliseconds = (shorthand) => { | ||
| let milliseconds; | ||
| if (shorthand === void 0) return; | ||
| if (typeof shorthand === "number") milliseconds = shorthand; | ||
| else { | ||
| if (typeof shorthand !== "string") return; | ||
| shorthand = shorthand.trim(); | ||
| if (Number.isNaN(Number(shorthand))) { | ||
| const match = /^([\d.]+)\s*(ms|s|m|h|hr|d)$/i.exec(shorthand); | ||
| if (!match) throw new Error(`Unsupported time format: "${shorthand}". Use 'ms', 's', 'm', 'h', 'hr', or 'd'.`); | ||
| const [, value, unit] = match; | ||
| const numericValue = Number.parseFloat(value); | ||
| switch (unit.toLowerCase()) { | ||
| case "ms": | ||
| milliseconds = numericValue; | ||
| break; | ||
| case "s": | ||
| milliseconds = numericValue * 1e3; | ||
| break; | ||
| case "m": | ||
| milliseconds = numericValue * 1e3 * 60; | ||
| break; | ||
| case "h": | ||
| milliseconds = numericValue * 1e3 * 60 * 60; | ||
| break; | ||
| case "hr": | ||
| milliseconds = numericValue * 1e3 * 60 * 60; | ||
| break; | ||
| case "d": | ||
| milliseconds = numericValue * 1e3 * 60 * 60 * 24; | ||
| break; | ||
| /* v8 ignore next -- @preserve */ | ||
| default: milliseconds = Number(shorthand); | ||
| } | ||
| } else milliseconds = Number(shorthand); | ||
| } | ||
| return milliseconds; | ||
| }; | ||
| /** | ||
| * Converts a shorthand time string or number into a timestamp. | ||
| * If the shorthand is undefined, it returns the current date's timestamp. | ||
| * If the shorthand is a valid time format, it adds that duration to the current date's timestamp. | ||
| * @param shorthand - A shorthand time string or number representing milliseconds. | ||
| * @param fromDate - An optional Date object to calculate from. Defaults to the current date if not provided. | ||
| * @returns The timestamp in milliseconds since epoch. | ||
| */ | ||
| const shorthandToTime = (shorthand, fromDate) => { | ||
| fromDate ??= /* @__PURE__ */ new Date(); | ||
| const milliseconds = shorthandToMilliseconds(shorthand); | ||
| if (milliseconds === void 0) return fromDate.getTime(); | ||
| return fromDate.getTime() + milliseconds; | ||
| }; | ||
| //#endregion | ||
| //#region src/cache-tags.ts | ||
| /** | ||
| * Prefix applied to every store key written by the service so its metadata cannot collide with | ||
| * user-supplied cache keys. | ||
| */ | ||
| const RESERVED_PREFIX = "--cacheable--tags--"; | ||
| /** Namespace used when none is supplied to the constructor. */ | ||
| const DEFAULT_NAMESPACE = "default"; | ||
| /** | ||
| * Provides tag-based cache invalidation on top of any {@link Keyv} store. It is store-agnostic and | ||
| * requires no adapter changes. | ||
| * | ||
| * The service uses a lazy invalidation model rather than scanning and deleting keys. Each tag has a | ||
| * monotonically increasing version counter; {@link CacheTags.invalidateTag} simply increments | ||
| * it. When a key is tagged via {@link CacheTags.setKeyTags}, a snapshot of its tags' current | ||
| * versions is stored alongside it. {@link CacheTags.isKeyFresh} compares that snapshot against | ||
| * the live versions — if any tag has been incremented since, the key is considered stale. Stale | ||
| * entries are not deleted explicitly; they are expected to fall out of the cache via their TTL. | ||
| * | ||
| * This keeps invalidation constant-time regardless of how many keys reference a tag, at the cost of | ||
| * one additional `isKeyFresh` read per cache lookup. | ||
| * | ||
| * The service can be disabled via the `enabled` option or property so integrations pay no cost for | ||
| * untagged workloads: while disabled, every method is a no-op — reads return their neutral value | ||
| * and writes are skipped. The service must be explicitly enabled to use tags; it never enables | ||
| * itself, which keeps behavior consistent across distributed instances sharing a store. | ||
| * | ||
| * All metadata is written under a reserved prefix so it cannot collide with user keys: | ||
| * - `--cacheable--tags--:<namespace>:tag:<tag>` → integer version counter (stored without TTL). | ||
| * - `--cacheable--tags--:<namespace>:key:<key>` → the {@link KeyTagEntry} snapshot. | ||
| * | ||
| * Note: the read-version-then-write-snapshot sequence in `setKeyTags` is not atomic across | ||
| * processes. A concurrent `invalidateTag` running between the read and the write can leave a freshly | ||
| * written key referencing a stale version. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const cacheTags = new CacheTags({ store: new Keyv(), namespace: 'app' }); | ||
| * await cacheTags.setKeyTags('user:42', ['users', 'org:7'], { ttl: 3600000 }); | ||
| * await cacheTags.isKeyFresh('user:42'); // true | ||
| * await cacheTags.invalidateTag('users'); | ||
| * await cacheTags.isKeyFresh('user:42'); // false | ||
| * ``` | ||
| */ | ||
| var CacheTags = class { | ||
| _store; | ||
| _namespace; | ||
| _enabled; | ||
| _onError; | ||
| /** | ||
| * Creates a new tag service. | ||
| * @param {CacheTagsOptions} options - The store, optional namespace, enabled state, and | ||
| * non-blocking error handler to use. | ||
| */ | ||
| constructor(options) { | ||
| this._store = options.store; | ||
| this._namespace = options.namespace ?? DEFAULT_NAMESPACE; | ||
| this._enabled = options.enabled ?? true; | ||
| this._onError = options.onError; | ||
| } | ||
| /** | ||
| * The Keyv store backing this service. | ||
| * @returns {Keyv} The store provided to the constructor. | ||
| */ | ||
| get store() { | ||
| return this._store; | ||
| } | ||
| /** | ||
| * The namespace isolating this service's tags and keys within the store. | ||
| * @returns {string} The configured namespace, or `"default"` if none was provided. | ||
| */ | ||
| get namespace() { | ||
| return this._namespace; | ||
| } | ||
| /** | ||
| * Whether the service is enabled. While disabled, every method is a no-op — read methods | ||
| * return their neutral value and writes are skipped — so integrations pay no extra store | ||
| * reads for untagged workloads. The service must be explicitly enabled to use tags; it never | ||
| * enables itself. | ||
| * @returns {boolean} Whether the service is enabled. | ||
| */ | ||
| get enabled() { | ||
| return this._enabled; | ||
| } | ||
| /** | ||
| * Sets whether the service is enabled. | ||
| * @param {boolean} enabled Whether the service is enabled. | ||
| */ | ||
| set enabled(enabled) { | ||
| this._enabled = enabled; | ||
| } | ||
| /** | ||
| * Builds the reserved store key under which a tag's version counter is stored. | ||
| * @param tag - The tag name. | ||
| * @returns {string} The namespaced store key for the tag's version. | ||
| */ | ||
| tagKey(tag) { | ||
| return `${RESERVED_PREFIX}:${this._namespace}:tag:${tag}`; | ||
| } | ||
| /** | ||
| * Builds the reserved store key under which a cache key's tag snapshot is stored. | ||
| * @param key - The cache key being tagged. | ||
| * @returns {string} The namespaced store key for the key's snapshot. | ||
| */ | ||
| keyEntryKey(key) { | ||
| return `${RESERVED_PREFIX}:${this._namespace}:key:${key}`; | ||
| } | ||
| /** | ||
| * Builds the common prefix shared by every key-snapshot entry in this namespace. Used to filter | ||
| * key entries when iterating the store. | ||
| * @returns {string} The namespaced key-entry prefix. | ||
| */ | ||
| keyPrefix() { | ||
| return `${RESERVED_PREFIX}:${this._namespace}:key:`; | ||
| } | ||
| /** | ||
| * Reads the current version of a single tag. | ||
| * @param tag - The tag name. | ||
| * @returns {Promise<number>} The tag's version, or `0` if it has never been invalidated. | ||
| */ | ||
| async getTagVersion(tag) { | ||
| const version = await this._store.get(this.tagKey(tag)); | ||
| return typeof version === "number" ? version : 0; | ||
| } | ||
| /** | ||
| * Reads the current versions of multiple tags in a single batched store read. | ||
| * @param tags - The tag names to look up. | ||
| * @returns {Promise<number[]>} The versions in the same order as `tags`; entries that have never | ||
| * been invalidated resolve to `0`. Returns an empty array when `tags` is empty. | ||
| */ | ||
| async getTagVersions(tags) { | ||
| if (tags.length === 0) return []; | ||
| const tagKeys = tags.map((tag) => this.tagKey(tag)); | ||
| const raw = await this._store.get(tagKeys); | ||
| return tags.map((_, i) => { | ||
| const value = raw?.[i]; | ||
| return typeof value === "number" ? value : 0; | ||
| }); | ||
| } | ||
| /** | ||
| * Reports a fire-and-forget failure to the `onError` handler, if one was provided. | ||
| * @param error - The error raised by the non-blocking operation. | ||
| */ | ||
| handleNonBlockingError(error) { | ||
| this._onError?.(error); | ||
| } | ||
| /** | ||
| * Reads the version snapshot of each tag and writes the key's tag snapshot to the store. | ||
| * @param key - The cache key to tag. | ||
| * @param tags - The tags to associate with the key. | ||
| * @param ttl - Time-to-live in milliseconds for the snapshot. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been written. | ||
| */ | ||
| async writeKeyTags(key, tags, ttl) { | ||
| const uniqueTags = [...new Set(tags)]; | ||
| const versions = await this.getTagVersions(uniqueTags); | ||
| const snapshot = {}; | ||
| for (let i = 0; i < uniqueTags.length; i++) snapshot[uniqueTags[i]] = versions[i]; | ||
| const entry = { tags: snapshot }; | ||
| await this._store.set(this.keyEntryKey(key), entry, ttl); | ||
| } | ||
| /** | ||
| * Associates a cache key with a set of tags by recording a snapshot of each tag's current | ||
| * version. Call this whenever you write a fresh value to the cache. Duplicate tags are ignored. | ||
| * No-op while the service is disabled. | ||
| * @param key - The cache key to tag. | ||
| * @param tags - The tags to associate with the key. | ||
| * @param {SetKeyTagsOptions} [options] - Optional settings, such as a `ttl` for the snapshot or | ||
| * `nonBlocking` to fire-and-forget the write. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been written, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| async setKeyTags(key, tags, options) { | ||
| if (!this._enabled) return; | ||
| const work = this.writeKeyTags(key, tags, options?.ttl); | ||
| if (options?.nonBlocking) { | ||
| work.catch((error) => { | ||
| this.handleNonBlockingError(error); | ||
| }); | ||
| return; | ||
| } | ||
| await work; | ||
| } | ||
| /** | ||
| * Removes a key's tag snapshot. After this, {@link CacheTags.isKeyFresh} returns `false` | ||
| * for the key. Use when the cached value itself is deleted. No-op while the service is | ||
| * disabled. | ||
| * @param key - The cache key whose snapshot should be removed. | ||
| * @param {RemoveKeysOptions} [options] - Optional settings, such as `nonBlocking` to | ||
| * fire-and-forget the removal. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been deleted, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| async removeKey(key, options) { | ||
| await this.removeKeys([key], options); | ||
| } | ||
| /** | ||
| * Removes multiple keys' tag snapshots in a single batched store delete. After this, | ||
| * {@link CacheTags.isKeyFresh} returns `false` for each key. An empty list is a no-op, as is | ||
| * the entire call while the service is disabled. | ||
| * @param keys - The cache keys whose snapshots should be removed. | ||
| * @param {RemoveKeysOptions} [options] - Optional settings, such as `nonBlocking` to | ||
| * fire-and-forget the removal. | ||
| * @returns {Promise<void>} Resolves once the snapshots have been deleted, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| async removeKeys(keys, options) { | ||
| if (!this._enabled || keys.length === 0) return; | ||
| const entryKeys = keys.map((key) => this.keyEntryKey(key)); | ||
| const work = this._store.deleteMany(entryKeys); | ||
| if (options?.nonBlocking) { | ||
| work.catch((error) => { | ||
| this.handleNonBlockingError(error); | ||
| }); | ||
| return; | ||
| } | ||
| await work; | ||
| } | ||
| /** | ||
| * Determines whether a key's cached value can still be trusted. A key is fresh only when a | ||
| * snapshot exists for it and every tag in that snapshot still has the version it had at set time. | ||
| * A key with no tags is trivially fresh. Call this before returning a value from your cache. | ||
| * Always returns `true` while the service is disabled. | ||
| * @param key - The cache key to check. | ||
| * @returns {Promise<boolean>} `true` if the key is still fresh; `false` if it is unknown or any of | ||
| * its tags has been invalidated since the snapshot was taken. | ||
| */ | ||
| async isKeyFresh(key) { | ||
| if (!this._enabled) return true; | ||
| const entry = await this._store.get(this.keyEntryKey(key)); | ||
| if (!entry?.tags) return false; | ||
| const tags = Object.keys(entry.tags); | ||
| const currentVersions = await this.getTagVersions(tags); | ||
| for (let i = 0; i < tags.length; i++) if (currentVersions[i] !== entry.tags[tags[i]]) return false; | ||
| return true; | ||
| } | ||
| /** | ||
| * Determines whether a key's cached value is known to be stale due to tag invalidation. This is | ||
| * the complement of {@link CacheTags.isKeyFresh} for tagged keys, but treats keys without a | ||
| * snapshot as not stale — making it safe to call for every cache lookup, including keys that were | ||
| * never tagged. Always returns `false` while the service is disabled. | ||
| * @param key - The cache key to check. | ||
| * @returns {Promise<boolean>} `true` only when a snapshot exists for the key and at least one of | ||
| * its tags has been invalidated since the snapshot was taken; `false` otherwise (including when | ||
| * the key has no snapshot). | ||
| */ | ||
| async isKeyStale(key) { | ||
| if (!this._enabled) return false; | ||
| return (await this.getStaleKeys([key])).length > 0; | ||
| } | ||
| /** | ||
| * Determines which of the given keys are known to be stale due to tag invalidation, using two | ||
| * batched store reads regardless of how many keys are checked: one for the snapshots and one for | ||
| * the union of their tag versions. Keys without a snapshot are not considered stale. Returns an | ||
| * empty array while the service is disabled. | ||
| * @param keys - The cache keys to check. | ||
| * @returns {Promise<string[]>} The subset of `keys` whose snapshot references at least one tag | ||
| * that has been invalidated since the snapshot was taken. | ||
| */ | ||
| async getStaleKeys(keys) { | ||
| if (!this._enabled || keys.length === 0) return []; | ||
| const entryKeys = keys.map((key) => this.keyEntryKey(key)); | ||
| const entries = await this._store.get(entryKeys); | ||
| const tagSet = /* @__PURE__ */ new Set(); | ||
| for (const entry of entries) if (entry?.tags) for (const tag of Object.keys(entry.tags)) tagSet.add(tag); | ||
| const tags = [...tagSet]; | ||
| const versions = await this.getTagVersions(tags); | ||
| const currentVersions = /* @__PURE__ */ new Map(); | ||
| for (let i = 0; i < tags.length; i++) currentVersions.set(tags[i], versions[i]); | ||
| const staleKeys = []; | ||
| for (const [i, entry] of entries.entries()) { | ||
| if (!entry?.tags) continue; | ||
| for (const [tag, version] of Object.entries(entry.tags)) if (currentVersions.get(tag) !== version) { | ||
| staleKeys.push(keys[i]); | ||
| break; | ||
| } | ||
| } | ||
| return staleKeys; | ||
| } | ||
| /** | ||
| * Returns the tags currently associated with a key. Returns `undefined` while the service is | ||
| * disabled. | ||
| * @param key - The cache key to look up. | ||
| * @returns {Promise<string[] | undefined>} The tag names from the key's snapshot, or `undefined` | ||
| * if the key has no snapshot. | ||
| */ | ||
| async getTags(key) { | ||
| if (!this._enabled) return; | ||
| const entry = await this._store.get(this.keyEntryKey(key)); | ||
| if (!entry?.tags) return; | ||
| return Object.keys(entry.tags); | ||
| } | ||
| /** | ||
| * Returns all cache keys whose snapshot references the given tag. This scans every key entry in | ||
| * the namespace via the Keyv iterator, making it an `O(N)` operation intended for debugging and | ||
| * tests rather than hot paths. Returns an empty array if the underlying store exposes no iterator | ||
| * or while the service is disabled. | ||
| * @param tag - The tag to search for. | ||
| * @returns {Promise<string[]>} The cache keys (with the reserved prefix stripped) referencing the tag. | ||
| */ | ||
| async getKeysByTag(tag) { | ||
| const result = []; | ||
| if (!this._enabled) return result; | ||
| const prefix = this.keyPrefix(); | ||
| const iterator = this._store.iterator?.(this._store.namespace); | ||
| if (!iterator) return result; | ||
| for await (const [storedKey, value] of iterator) { | ||
| if (typeof storedKey !== "string" || !storedKey.startsWith(prefix)) continue; | ||
| const entry = value; | ||
| if (entry?.tags && Object.hasOwn(entry.tags, tag)) result.push(storedKey.slice(prefix.length)); | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Invalidates a single tag by incrementing its version counter. Every key whose snapshot | ||
| * references this tag becomes stale immediately. Runs in constant time regardless of how many | ||
| * keys reference the tag. No-op while the service is disabled. | ||
| * @param tag - The tag to invalidate. | ||
| * @returns {Promise<string[]>} A single-element array containing the invalidated tag, or an | ||
| * empty array while the service is disabled. | ||
| */ | ||
| async invalidateTag(tag) { | ||
| if (!this._enabled) return []; | ||
| const current = await this.getTagVersion(tag); | ||
| await this._store.set(this.tagKey(tag), current + 1); | ||
| return [tag]; | ||
| } | ||
| /** | ||
| * Invalidates multiple tags by incrementing each of their version counters in a single batched | ||
| * store write. Duplicate tags are bumped once. An empty list is a no-op, as is the entire call | ||
| * while the service is disabled. | ||
| * @param tags - The tags to invalidate. | ||
| * @returns {Promise<string[]>} The `tags` argument as provided (including any duplicates), or | ||
| * an empty array while the service is disabled. | ||
| */ | ||
| async invalidateTags(tags) { | ||
| if (!this._enabled) return []; | ||
| const uniqueTags = [...new Set(tags)]; | ||
| if (uniqueTags.length === 0) return tags; | ||
| const versions = await this.getTagVersions(uniqueTags); | ||
| const kvPairs = []; | ||
| for (let i = 0; i < uniqueTags.length; i++) kvPairs.push({ | ||
| key: this.tagKey(uniqueTags[i]), | ||
| value: versions[i] + 1 | ||
| }); | ||
| await this._store.setMany(kvPairs); | ||
| return tags; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/coalesce-async.ts | ||
| const callbacks = /* @__PURE__ */ new Map(); | ||
| function hasKey(key) { | ||
| return callbacks.has(key); | ||
| } | ||
| function addKey(key) { | ||
| callbacks.set(key, []); | ||
| } | ||
| function removeKey(key) { | ||
| callbacks.delete(key); | ||
| } | ||
| function addCallbackToKey(key, callback) { | ||
| const stash = getCallbacksByKey(key); | ||
| stash.push(callback); | ||
| callbacks.set(key, stash); | ||
| } | ||
| function getCallbacksByKey(key) { | ||
| /* v8 ignore next -- @preserve */ | ||
| return callbacks.get(key) ?? []; | ||
| } | ||
| async function enqueue(key) { | ||
| return new Promise((resolve, reject) => { | ||
| addCallbackToKey(key, { | ||
| resolve, | ||
| reject | ||
| }); | ||
| }); | ||
| } | ||
| function dequeue(key) { | ||
| const stash = getCallbacksByKey(key); | ||
| removeKey(key); | ||
| return stash; | ||
| } | ||
| function coalesce(options) { | ||
| const { key, error, result } = options; | ||
| for (const callback of dequeue(key)) | ||
| /* c8 ignore next 1 */ | ||
| if (error) | ||
| /* c8 ignore next 3 */ | ||
| callback.reject(error); | ||
| else callback.resolve(result); | ||
| } | ||
| /** | ||
| * Enqueue a promise for the group identified by `key`. | ||
| * | ||
| * All requests received for the same key while a request for that key | ||
| * is already being executed will wait. Once the running request settles | ||
| * then all the waiting requests in the group will settle, too. | ||
| * This minimizes how many times the function itself runs at the same time. | ||
| * This function resolves or rejects according to the given function argument. | ||
| * | ||
| * @url https://github.com/douglascayers/promise-coalesce | ||
| */ | ||
| async function coalesceAsync(key, fnc) { | ||
| if (!hasKey(key)) { | ||
| addKey(key); | ||
| try { | ||
| const result = await Promise.resolve(fnc()); | ||
| coalesce({ | ||
| key, | ||
| result | ||
| }); | ||
| return result; | ||
| } catch (error) { | ||
| /* c8 ignore next 5 */ | ||
| coalesce({ | ||
| key, | ||
| error | ||
| }); | ||
| throw error; | ||
| } | ||
| } | ||
| return enqueue(key); | ||
| } | ||
| //#endregion | ||
| //#region src/hash.ts | ||
| let HashAlgorithm = /* @__PURE__ */ function(HashAlgorithm) { | ||
| HashAlgorithm["SHA256"] = "SHA-256"; | ||
| HashAlgorithm["SHA384"] = "SHA-384"; | ||
| HashAlgorithm["SHA512"] = "SHA-512"; | ||
| HashAlgorithm["DJB2"] = "djb2"; | ||
| HashAlgorithm["FNV1"] = "fnv1"; | ||
| HashAlgorithm["MURMER"] = "murmer"; | ||
| HashAlgorithm["CRC32"] = "crc32"; | ||
| return HashAlgorithm; | ||
| }({}); | ||
| /** | ||
| * Hashes an object asynchronously using the specified cryptographic algorithm. | ||
| * This method should be used for cryptographic algorithms (SHA-256, SHA-384, SHA-512). | ||
| * For non-cryptographic algorithms, use hashSync() for better performance. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use | ||
| * @returns {Promise<string>} The hash of the object | ||
| */ | ||
| async function hash(object, options = { | ||
| algorithm: "SHA-256", | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const algorithm = options?.algorithm ?? "SHA-256"; | ||
| const objectString = (options?.serialize ?? JSON.stringify)(object); | ||
| return new Hashery().toHash(objectString, { algorithm }); | ||
| } | ||
| /** | ||
| * Hashes an object synchronously using the specified non-cryptographic algorithm. | ||
| * This method should be used for non-cryptographic algorithms (DJB2, FNV1, MURMER, CRC32). | ||
| * For cryptographic algorithms, use hash() instead. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use | ||
| * @returns {string} The hash of the object | ||
| */ | ||
| function hashSync(object, options = { | ||
| algorithm: "djb2", | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const algorithm = options?.algorithm ?? "djb2"; | ||
| const objectString = (options?.serialize ?? JSON.stringify)(object); | ||
| return new Hashery().toHashSync(objectString, { algorithm }); | ||
| } | ||
| /** | ||
| * Hashes an object asynchronously and converts it to a number within a specified range. | ||
| * This method should be used for cryptographic algorithms (SHA-256, SHA-384, SHA-512). | ||
| * For non-cryptographic algorithms, use hashToNumberSync() for better performance. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use including min/max range | ||
| * @returns {Promise<number>} A number within the specified range | ||
| */ | ||
| async function hashToNumber(object, options = { | ||
| min: 0, | ||
| max: 10, | ||
| algorithm: "SHA-256", | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const min = options?.min ?? 0; | ||
| const max = options?.max ?? 10; | ||
| const algorithm = options?.algorithm ?? "SHA-256"; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const hashLength = options?.hashLength ?? 16; | ||
| if (min >= max) throw new Error(`Invalid range: min (${min}) must be less than max (${max})`); | ||
| const objectString = serialize(object); | ||
| return new Hashery().toNumber(objectString, { | ||
| algorithm, | ||
| min, | ||
| max, | ||
| hashLength | ||
| }); | ||
| } | ||
| /** | ||
| * Hashes an object synchronously and converts it to a number within a specified range. | ||
| * This method should be used for non-cryptographic algorithms (DJB2, FNV1, MURMER, CRC32). | ||
| * For cryptographic algorithms, use hashToNumber() instead. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use including min/max range | ||
| * @returns {number} A number within the specified range | ||
| */ | ||
| function hashToNumberSync(object, options = { | ||
| min: 0, | ||
| max: 10, | ||
| algorithm: "djb2", | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const min = options?.min ?? 0; | ||
| const max = options?.max ?? 10; | ||
| const algorithm = options?.algorithm ?? "djb2"; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const hashLength = options?.hashLength ?? 16; | ||
| if (min >= max) throw new Error(`Invalid range: min (${min}) must be less than max (${max})`); | ||
| const objectString = serialize(object); | ||
| return new Hashery().toNumberSync(objectString, { | ||
| algorithm, | ||
| min, | ||
| max, | ||
| hashLength | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/is-keyv-instance.ts | ||
| function isKeyvInstance(keyv) { | ||
| if (keyv === null || keyv === void 0) return false; | ||
| if (keyv instanceof Keyv) return true; | ||
| return [ | ||
| "generateIterator", | ||
| "get", | ||
| "getMany", | ||
| "set", | ||
| "setMany", | ||
| "delete", | ||
| "deleteMany", | ||
| "has", | ||
| "hasMany", | ||
| "clear", | ||
| "disconnect", | ||
| "serialize", | ||
| "deserialize" | ||
| ].every((method) => typeof keyv[method] === "function"); | ||
| } | ||
| //#endregion | ||
| //#region src/is-object.ts | ||
| function isObject(value) { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| //#endregion | ||
| //#region src/less-than.ts | ||
| function lessThan(number1, number2) { | ||
| return typeof number1 === "number" && typeof number2 === "number" ? number1 < number2 : false; | ||
| } | ||
| //#endregion | ||
| //#region src/memoize.ts | ||
| function wrapSync(function_, options) { | ||
| const { ttl, keyPrefix, cache, serialize } = options; | ||
| return (...arguments_) => { | ||
| let cacheKey = createWrapKey(function_, arguments_, { | ||
| keyPrefix, | ||
| serialize | ||
| }); | ||
| if (options.createKey) cacheKey = options.createKey(function_, arguments_, options); | ||
| let value = cache.get(cacheKey); | ||
| if (value === void 0) try { | ||
| value = function_(...arguments_); | ||
| cache.set(cacheKey, value, ttl); | ||
| } catch (error) { | ||
| cache.emit("error", error); | ||
| if (options.cacheErrors) cache.set(cacheKey, error, ttl); | ||
| } | ||
| return value; | ||
| }; | ||
| } | ||
| async function getOrSet(key, function_, options) { | ||
| const keyString = typeof key === "function" ? key(options) : key; | ||
| let value; | ||
| try { | ||
| value = await options.cache.get(keyString); | ||
| } catch (error) { | ||
| options.cache.emit("error", error); | ||
| if (options.throwErrors === true || options.throwErrors === "store") throw error; | ||
| } | ||
| if (value === void 0) value = await coalesceAsync(`${options.cacheId ?? "default"}::${keyString}`, async () => { | ||
| let result; | ||
| try { | ||
| try { | ||
| result = await function_(); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope(error, "function"); | ||
| } | ||
| try { | ||
| await options.cache.set(keyString, result, options.ttl); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope(error, "store"); | ||
| } | ||
| return result; | ||
| } catch (caught) { | ||
| const errorType = caught instanceof ErrorEnvelope ? caught.context : void 0; | ||
| const error = caught instanceof ErrorEnvelope ? caught.error : caught; | ||
| options.cache.emit("error", error); | ||
| if (options.cacheErrors && errorType === "function") try { | ||
| await options.cache.set(keyString, error, options.ttl); | ||
| } catch (storeError) { | ||
| options.cache.emit("error", storeError); | ||
| } | ||
| if (options.throwErrors === true || options.throwErrors === errorType) throw error; | ||
| } | ||
| return result; | ||
| }); | ||
| return value; | ||
| } | ||
| /** | ||
| * Synchronous counterpart to {@link getOrSet}. Reads `key` from the cache and, on a miss, computes | ||
| * the value with `function_`, stores it, and returns it. | ||
| * | ||
| * Unlike {@link getOrSet} there is no request coalescing: synchronous code runs to completion | ||
| * without interleaving, so concurrent callers cannot stampede the setter the way they can with an | ||
| * async cache. | ||
| * | ||
| * Error handling mirrors {@link getOrSet}: errors are emitted on the cache's `error` event, can be | ||
| * cached when `cacheErrors` is set, and can be rethrown selectively via `throwErrors` (`true` for | ||
| * any error, `"function"` for setter errors, `"store"` for cache read/write errors). | ||
| * | ||
| * @param key - The cache key, or a function that derives it from the resolved options. | ||
| * @param function_ - The setter invoked on a cache miss to compute the value. | ||
| * @param options - The {@link GetOrSetSyncOptions} including the target synchronous cache. | ||
| * @returns The cached or freshly computed value, or `undefined`. | ||
| */ | ||
| function getOrSetSync(key, function_, options) { | ||
| const keyString = typeof key === "function" ? key(options) : key; | ||
| let value; | ||
| try { | ||
| value = options.cache.get(keyString); | ||
| } catch (error) { | ||
| options.cache.emit("error", error); | ||
| if (options.throwErrors === true || options.throwErrors === "store") throw error; | ||
| } | ||
| if (value === void 0) try { | ||
| try { | ||
| value = function_(); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope(error, "function"); | ||
| } | ||
| try { | ||
| options.cache.set(keyString, value, options.ttl); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope(error, "store"); | ||
| } | ||
| } catch (caught) { | ||
| const errorType = caught instanceof ErrorEnvelope ? caught.context : void 0; | ||
| const error = caught instanceof ErrorEnvelope ? caught.error : caught; | ||
| options.cache.emit("error", error); | ||
| if (options.cacheErrors && errorType === "function") try { | ||
| options.cache.set(keyString, error, options.ttl); | ||
| } catch (storeError) { | ||
| options.cache.emit("error", storeError); | ||
| } | ||
| if (options.throwErrors === true || options.throwErrors === errorType) throw error; | ||
| } | ||
| return value; | ||
| } | ||
| function wrap(function_, options) { | ||
| const { keyPrefix, serialize } = options; | ||
| return async (...arguments_) => { | ||
| let cacheKey = createWrapKey(function_, arguments_, { | ||
| keyPrefix, | ||
| serialize | ||
| }); | ||
| if (options.createKey) cacheKey = options.createKey(function_, arguments_, options); | ||
| return getOrSet(cacheKey, async () => function_(...arguments_), options); | ||
| }; | ||
| } | ||
| function createWrapKey(function_, arguments_, options) { | ||
| const { keyPrefix, serialize } = options || {}; | ||
| if (!keyPrefix) return `${function_.name}::${hashSync(arguments_, { serialize })}`; | ||
| return `${keyPrefix}::${function_.name}::${hashSync(arguments_, { serialize })}`; | ||
| } | ||
| var ErrorEnvelope = class { | ||
| error; | ||
| context; | ||
| constructor(error, context) { | ||
| this.error = error; | ||
| this.context = context; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/run-if-fn.ts | ||
| function runIfFn(valueOrFunction, ...arguments_) { | ||
| return typeof valueOrFunction === "function" ? valueOrFunction(...arguments_) : valueOrFunction; | ||
| } | ||
| //#endregion | ||
| //#region src/sleep.ts | ||
| const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | ||
| //#endregion | ||
| //#region src/stats.ts | ||
| /** | ||
| * Event map for `@cacheable/node-cache` instances. node-cache emits with | ||
| * positional arguments (e.g. `set(key, value)`), and emits each lifecycle | ||
| * event exactly once, so the counts map cleanly. `flush` clears the cache data | ||
| * and `flush_stats` resets the stats counters, mirroring node-cache's | ||
| * `flushAll()` / `flushStats()` lifecycle. | ||
| * | ||
| * Presets for `cacheable` and `cache-manager` are intentionally not provided: | ||
| * their event streams emit per-store probes (and, for cache-manager, do not | ||
| * emit an event on a normal miss), so a simple map cannot faithfully reproduce | ||
| * their imperative stats. Wire those up with a custom map or imperative calls. | ||
| */ | ||
| const nodeCacheStatsEventMap = { | ||
| set: (stats, key) => { | ||
| stats.increment("sets"); | ||
| if (typeof key === "string" || typeof key === "number") stats.recordKey(String(key), "sets"); | ||
| }, | ||
| del: (stats, key) => { | ||
| stats.increment("deletes"); | ||
| if (typeof key === "string" || typeof key === "number") stats.recordKey(String(key), "deletes"); | ||
| }, | ||
| flush: "clears", | ||
| flush_stats: (stats) => { | ||
| stats.reset(); | ||
| } | ||
| }; | ||
| var Stats = class { | ||
| _counters = { | ||
| hits: 0, | ||
| misses: 0, | ||
| gets: 0, | ||
| sets: 0, | ||
| deletes: 0, | ||
| clears: 0, | ||
| count: 0 | ||
| }; | ||
| _vsize = 0; | ||
| _ksize = 0; | ||
| _enabled = false; | ||
| _lastUpdated; | ||
| _lastReset; | ||
| _subscriptions = []; | ||
| /** Backing store for the public {@link trackedKeys} read-only view. */ | ||
| _trackedKeys = /* @__PURE__ */ new Map(); | ||
| _trackKeys = false; | ||
| _maxTrackedKeys; | ||
| constructor(options) { | ||
| if (options?.enabled) this._enabled = options.enabled; | ||
| if (options?.trackKeys) this._trackKeys = options.trackKeys; | ||
| if (options?.maxTrackedKeys !== void 0) this._maxTrackedKeys = options.maxTrackedKeys; | ||
| if (options?.emitter && options?.eventMap) this.subscribe(options.emitter, options.eventMap); | ||
| } | ||
| /** | ||
| * @returns {boolean} - Whether the stats are enabled | ||
| */ | ||
| get enabled() { | ||
| return this._enabled; | ||
| } | ||
| /** | ||
| * @param {boolean} enabled - Whether to enable the stats | ||
| */ | ||
| set enabled(enabled) { | ||
| this._enabled = enabled; | ||
| } | ||
| /** | ||
| * @returns {boolean} - Whether per-key statistics are tracked | ||
| */ | ||
| get trackKeys() { | ||
| return this._trackKeys; | ||
| } | ||
| /** | ||
| * @param {boolean} trackKeys - Whether to track per-key statistics | ||
| */ | ||
| set trackKeys(trackKeys) { | ||
| this._trackKeys = trackKeys; | ||
| } | ||
| /** | ||
| * @returns {number | undefined} - The cap on unique keys tracked, or | ||
| * `undefined` when unbounded | ||
| */ | ||
| get maxTrackedKeys() { | ||
| return this._maxTrackedKeys; | ||
| } | ||
| /** | ||
| * @param {number | undefined} maxTrackedKeys - The cap on unique keys | ||
| * tracked. Set `undefined` for unbounded. | ||
| */ | ||
| set maxTrackedKeys(maxTrackedKeys) { | ||
| this._maxTrackedKeys = maxTrackedKeys; | ||
| } | ||
| /** | ||
| * Per-key statistics, keyed by cache key, holding each key's raw | ||
| * `hits`/`misses`/`gets`/`sets`/`deletes` counters. Populated by | ||
| * {@link recordKey} when {@link trackKeys} is enabled; read `trackedKeys.size` | ||
| * for the number of unique keys currently tracked. The returned map is a | ||
| * read-only view — mutate per-key stats via {@link recordKey} / | ||
| * {@link clearKeys} / {@link reset}. | ||
| * @returns {ReadonlyMap<string, Readonly<KeyCounters>>} | ||
| * @readonly | ||
| */ | ||
| get trackedKeys() { | ||
| return this._trackedKeys; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of hits | ||
| * @readonly | ||
| */ | ||
| get hits() { | ||
| return this._counters.hits; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of misses | ||
| * @readonly | ||
| */ | ||
| get misses() { | ||
| return this._counters.misses; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of gets | ||
| * @readonly | ||
| */ | ||
| get gets() { | ||
| return this._counters.gets; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of sets | ||
| * @readonly | ||
| */ | ||
| get sets() { | ||
| return this._counters.sets; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of deletes | ||
| * @readonly | ||
| */ | ||
| get deletes() { | ||
| return this._counters.deletes; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of clears | ||
| * @readonly | ||
| */ | ||
| get clears() { | ||
| return this._counters.clears; | ||
| } | ||
| /** | ||
| * @returns {number} - The vsize (value size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get vsize() { | ||
| return this._vsize; | ||
| } | ||
| /** | ||
| * @returns {number} - The ksize (key size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get ksize() { | ||
| return this._ksize; | ||
| } | ||
| /** | ||
| * @returns {number} - The count of the cache instance | ||
| * @readonly | ||
| */ | ||
| get count() { | ||
| return this._counters.count; | ||
| } | ||
| /** | ||
| * The ratio of hits to total lookups (hits + misses). Returns `0` when there | ||
| * have been no lookups. | ||
| * @returns {number} - A value between 0 and 1 | ||
| * @readonly | ||
| */ | ||
| get hitRate() { | ||
| const total = this._counters.hits + this._counters.misses; | ||
| return total === 0 ? 0 : this._counters.hits / total; | ||
| } | ||
| /** | ||
| * The ratio of misses to total lookups (hits + misses). Returns `0` when | ||
| * there have been no lookups. | ||
| * @returns {number} - A value between 0 and 1 | ||
| * @readonly | ||
| */ | ||
| get missRate() { | ||
| const total = this._counters.hits + this._counters.misses; | ||
| return total === 0 ? 0 : this._counters.misses / total; | ||
| } | ||
| /** | ||
| * The timestamp (ms since epoch) of the last mutation while enabled, or | ||
| * `undefined` if there have been none since the last reset. | ||
| * @returns {number | undefined} | ||
| * @readonly | ||
| */ | ||
| get lastUpdated() { | ||
| return this._lastUpdated; | ||
| } | ||
| /** | ||
| * The timestamp (ms since epoch) of the last {@link reset}/{@link clear}, or | ||
| * `undefined` if it has never been reset. | ||
| * @returns {number | undefined} | ||
| * @readonly | ||
| */ | ||
| get lastReset() { | ||
| return this._lastReset; | ||
| } | ||
| /** | ||
| * Increment a counter field by `amount` (default `1`). No-op when disabled. | ||
| * @param {StatField} field - The counter to increment | ||
| * @param {number} amount - The amount to add (default 1) | ||
| */ | ||
| increment(field, amount = 1) { | ||
| if (!this._enabled) return; | ||
| this._counters[field] += amount; | ||
| this.touch(); | ||
| } | ||
| /** | ||
| * Decrement a counter field by `amount` (default `1`). No-op when disabled. | ||
| * @param {StatField} field - The counter to decrement | ||
| * @param {number} amount - The amount to subtract (default 1) | ||
| */ | ||
| decrement(field, amount = 1) { | ||
| if (!this._enabled) return; | ||
| this._counters[field] -= amount; | ||
| this.touch(); | ||
| } | ||
| incrementHits(amount = 1) { | ||
| this.increment("hits", amount); | ||
| } | ||
| incrementMisses(amount = 1) { | ||
| this.increment("misses", amount); | ||
| } | ||
| incrementGets(amount = 1) { | ||
| this.increment("gets", amount); | ||
| } | ||
| incrementSets(amount = 1) { | ||
| this.increment("sets", amount); | ||
| } | ||
| incrementDeletes(amount = 1) { | ||
| this.increment("deletes", amount); | ||
| } | ||
| incrementClears(amount = 1) { | ||
| this.increment("clears", amount); | ||
| } | ||
| incrementVSize(value) { | ||
| if (!this._enabled) return; | ||
| this._vsize += this.roughSizeOfObject(value); | ||
| this.touch(); | ||
| } | ||
| decreaseVSize(value) { | ||
| if (!this._enabled) return; | ||
| this._vsize = Math.max(0, this._vsize - this.roughSizeOfObject(value)); | ||
| this.touch(); | ||
| } | ||
| incrementKSize(key) { | ||
| if (!this._enabled) return; | ||
| this._ksize += this.roughSizeOfString(key); | ||
| this.touch(); | ||
| } | ||
| decreaseKSize(key) { | ||
| if (!this._enabled) return; | ||
| this._ksize = Math.max(0, this._ksize - this.roughSizeOfString(key)); | ||
| this.touch(); | ||
| } | ||
| incrementCount(amount = 1) { | ||
| this.increment("count", amount); | ||
| } | ||
| decreaseCount(amount = 1) { | ||
| if (!this._enabled) return; | ||
| this._counters.count = Math.max(0, this._counters.count - amount); | ||
| this.touch(); | ||
| } | ||
| setCount(count) { | ||
| if (!this._enabled) return; | ||
| this._counters.count = count; | ||
| this.touch(); | ||
| } | ||
| roughSizeOfString(value) { | ||
| return value.length * 2; | ||
| } | ||
| roughSizeOfObject(object) { | ||
| const objectList = []; | ||
| const stack = [object]; | ||
| let bytes = 0; | ||
| while (stack.length > 0) { | ||
| const value = stack.pop(); | ||
| if (typeof value === "boolean") bytes += 4; | ||
| else if (typeof value === "string") bytes += value.length * 2; | ||
| else if (typeof value === "number") bytes += 8; | ||
| else { | ||
| if (value === null || value === void 0) { | ||
| bytes += 4; | ||
| continue; | ||
| } | ||
| if (objectList.includes(value)) continue; | ||
| objectList.push(value); | ||
| for (const key in value) { | ||
| bytes += key.length * 2; | ||
| stack.push(value[key]); | ||
| } | ||
| } | ||
| } | ||
| return bytes; | ||
| } | ||
| /** | ||
| * Enable stat tracking. Equivalent to setting {@link enabled} to `true`. | ||
| */ | ||
| enable() { | ||
| this._enabled = true; | ||
| } | ||
| /** | ||
| * Disable stat tracking. Equivalent to setting {@link enabled} to `false`. | ||
| */ | ||
| disable() { | ||
| this._enabled = false; | ||
| } | ||
| /** | ||
| * Reset all counters to zero and record the reset timestamp. Alias of | ||
| * {@link reset}. | ||
| */ | ||
| clear() { | ||
| this.reset(); | ||
| } | ||
| reset() { | ||
| this._counters = { | ||
| hits: 0, | ||
| misses: 0, | ||
| gets: 0, | ||
| sets: 0, | ||
| deletes: 0, | ||
| clears: 0, | ||
| count: 0 | ||
| }; | ||
| this._vsize = 0; | ||
| this._ksize = 0; | ||
| this._trackedKeys.clear(); | ||
| this._lastReset = Date.now(); | ||
| this._lastUpdated = void 0; | ||
| } | ||
| resetStoreValues() { | ||
| this._vsize = 0; | ||
| this._ksize = 0; | ||
| this._counters.count = 0; | ||
| } | ||
| /** | ||
| * @returns {StatsSnapshot} - A plain-object snapshot of the current stats, | ||
| * including computed `hitRate`/`missRate` and timestamps. | ||
| */ | ||
| toJSON() { | ||
| return { | ||
| enabled: this._enabled, | ||
| hits: this._counters.hits, | ||
| misses: this._counters.misses, | ||
| gets: this._counters.gets, | ||
| sets: this._counters.sets, | ||
| deletes: this._counters.deletes, | ||
| clears: this._counters.clears, | ||
| vsize: this._vsize, | ||
| ksize: this._ksize, | ||
| count: this._counters.count, | ||
| hitRate: this.hitRate, | ||
| missRate: this.missRate, | ||
| trackedKeys: this._trackedKeys.size, | ||
| lastUpdated: this._lastUpdated, | ||
| lastReset: this._lastReset | ||
| }; | ||
| } | ||
| /** | ||
| * @returns {StatsSnapshot} - A plain-object snapshot of the current stats. | ||
| * Alias of {@link toJSON}. | ||
| */ | ||
| snapshot() { | ||
| return this.toJSON(); | ||
| } | ||
| /** | ||
| * Record an operation against a specific key for per-key statistics. No-op | ||
| * unless both {@link enabled} and {@link trackKeys} are `true`. | ||
| * @param {string} key - The cache key the operation touched | ||
| * @param {KeyStatField} field - The per-key counter to increment | ||
| * @param {number} amount - The amount to add (default 1) | ||
| */ | ||
| recordKey(key, field, amount = 1) { | ||
| if (!this._enabled || !this._trackKeys) return; | ||
| let counters = this._trackedKeys.get(key); | ||
| if (!counters) { | ||
| counters = { | ||
| hits: 0, | ||
| misses: 0, | ||
| gets: 0, | ||
| sets: 0, | ||
| deletes: 0 | ||
| }; | ||
| this._trackedKeys.set(key, counters); | ||
| this.pruneTrackedKeys(key); | ||
| } | ||
| counters[field] += amount; | ||
| this.touch(); | ||
| } | ||
| /** | ||
| * The most-used keys, sorted descending. Sorts by total recorded operations, | ||
| * or by a single field when `field` is provided. Ties order by key. | ||
| * @param {number} limit - Maximum entries to return (default 100) | ||
| * @param {KeyStatField} [field] - Optionally rank by one counter (e.g. "hits") | ||
| * @returns {StatsKeyEntry[]} | ||
| */ | ||
| mostUsedKeys(limit = 100, field) { | ||
| return this.sortedKeyEntries(field, "desc").slice(0, limit); | ||
| } | ||
| /** | ||
| * The least-used keys, sorted ascending. Sorts by total recorded operations, | ||
| * or by a single field when `field` is provided. Ties order by key. Note: | ||
| * only keys that have been recorded at least once can be ranked, and when | ||
| * {@link maxTrackedKeys} pruning has occurred the true least-used keys may | ||
| * have been evicted. | ||
| * @param {number} limit - Maximum entries to return (default 100) | ||
| * @param {KeyStatField} [field] - Optionally rank by one counter (e.g. "gets") | ||
| * @returns {StatsKeyEntry[]} | ||
| */ | ||
| leastUsedKeys(limit = 100, field) { | ||
| return this.sortedKeyEntries(field, "asc").slice(0, limit); | ||
| } | ||
| /** | ||
| * @param {string} key - The key to look up | ||
| * @returns {StatsKeyEntry | undefined} - The per-key statistics, or | ||
| * `undefined` if the key has not been recorded | ||
| */ | ||
| keyStats(key) { | ||
| const counters = this._trackedKeys.get(key); | ||
| return counters ? this.toKeyEntry(key, counters) : void 0; | ||
| } | ||
| /** | ||
| * Clear all per-key statistics without touching the aggregate counters. | ||
| */ | ||
| clearKeys() { | ||
| this._trackedKeys.clear(); | ||
| } | ||
| totalOf(counters) { | ||
| return counters.hits + counters.misses + counters.gets + counters.sets + counters.deletes; | ||
| } | ||
| toKeyEntry(key, counters) { | ||
| const lookups = counters.hits + counters.misses; | ||
| return { | ||
| key, | ||
| count: this.totalOf(counters), | ||
| hits: counters.hits, | ||
| misses: counters.misses, | ||
| gets: counters.gets, | ||
| sets: counters.sets, | ||
| deletes: counters.deletes, | ||
| hitRate: lookups === 0 ? 0 : counters.hits / lookups | ||
| }; | ||
| } | ||
| sortedKeyEntries(field, direction) { | ||
| const entries = []; | ||
| for (const [key, counters] of this._trackedKeys) entries.push(this.toKeyEntry(key, counters)); | ||
| const sign = direction === "asc" ? 1 : -1; | ||
| entries.sort((a, b) => { | ||
| const valueA = field ? a[field] : a.count; | ||
| const valueB = field ? b[field] : b.count; | ||
| if (valueA !== valueB) return (valueA - valueB) * sign; | ||
| return a.key < b.key ? -1 : 1; | ||
| }); | ||
| return entries; | ||
| } | ||
| /** | ||
| * When over {@link maxTrackedKeys}, prune the lowest-count keys down to 90% | ||
| * of the cap (batched so the sort cost amortizes across inserts). The key | ||
| * that was just recorded is never pruned. | ||
| */ | ||
| pruneTrackedKeys(protectedKey) { | ||
| if (this._maxTrackedKeys === void 0 || this._trackedKeys.size <= this._maxTrackedKeys) return; | ||
| const target = Math.max(1, Math.floor(this._maxTrackedKeys * .9)); | ||
| const sorted = [...this._trackedKeys.entries()].sort((a, b) => this.totalOf(a[1]) - this.totalOf(b[1])); | ||
| for (const [key] of sorted) { | ||
| if (this._trackedKeys.size <= target) break; | ||
| if (key === protectedKey) continue; | ||
| this._trackedKeys.delete(key); | ||
| } | ||
| } | ||
| /** | ||
| * Subscribe to an emitter so that matching events automatically update the | ||
| * stats. Counting is gated by {@link enabled}, so you may subscribe first and | ||
| * toggle enablement later. Call {@link unsubscribe} to detach. | ||
| * @param {StatsEmitter} emitter - The emitter to listen on | ||
| * @param {StatsEventMap} eventMap - The event-to-stat mapping (e.g. | ||
| * {@link nodeCacheStatsEventMap} or a custom map) | ||
| */ | ||
| subscribe(emitter, eventMap) { | ||
| for (const [event, action] of Object.entries(eventMap)) { | ||
| const listener = (...args) => { | ||
| this.applyEvent(action, args); | ||
| }; | ||
| emitter.on(event, listener); | ||
| this._subscriptions.push({ | ||
| emitter, | ||
| event, | ||
| listener | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Detach listeners previously attached via {@link subscribe}. When `emitter` | ||
| * is provided, only that emitter's listeners are removed; otherwise all are. | ||
| * @param {StatsEmitter} [emitter] - The emitter to detach from | ||
| */ | ||
| unsubscribe(emitter) { | ||
| const remaining = []; | ||
| for (const sub of this._subscriptions) { | ||
| if (emitter && sub.emitter !== emitter) { | ||
| remaining.push(sub); | ||
| continue; | ||
| } | ||
| (sub.emitter.off ?? sub.emitter.removeListener)?.call(sub.emitter, sub.event, sub.listener); | ||
| } | ||
| this._subscriptions = remaining; | ||
| } | ||
| applyEvent(action, args) { | ||
| if (!this._enabled) return; | ||
| if (typeof action === "function") { | ||
| action(this, ...args); | ||
| return; | ||
| } | ||
| if (Array.isArray(action)) { | ||
| for (const field of action) this.increment(field); | ||
| return; | ||
| } | ||
| this.increment(action); | ||
| } | ||
| touch() { | ||
| this._lastUpdated = Date.now(); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/ttl.ts | ||
| /** | ||
| * Normalizes a TTL input into per-store milliseconds. When given an object it resolves the | ||
| * `primary` and `secondary` fields independently; when given a number or shorthand string it | ||
| * applies the same value to both stores. Undefined fields (or undefined input) resolve to | ||
| * `undefined` so the caller can fall back to its own default TTL. | ||
| * @param ttl - The TTL input: a number (ms), a shorthand string, or a {@link PerStoreTtl} object. | ||
| * @returns {{ primary?: number; secondary?: number }} The resolved per-store TTLs in milliseconds. | ||
| */ | ||
| function resolvePerStoreTtl(ttl) { | ||
| if (ttl === void 0 || ttl === null) return { | ||
| primary: void 0, | ||
| secondary: void 0 | ||
| }; | ||
| if (typeof ttl === "object") return { | ||
| primary: shorthandToMilliseconds(ttl.primary), | ||
| secondary: shorthandToMilliseconds(ttl.secondary) | ||
| }; | ||
| const milliseconds = shorthandToMilliseconds(ttl); | ||
| return { | ||
| primary: milliseconds, | ||
| secondary: milliseconds | ||
| }; | ||
| } | ||
| /** | ||
| * Converts a exspires value to a TTL value. | ||
| * @param expires - The expires value to convert. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if the expires value is not valid. | ||
| */ | ||
| function getTtlFromExpires(expires) { | ||
| if (expires === void 0 || expires === null) return; | ||
| const now = Date.now(); | ||
| if (expires < now) return; | ||
| return expires - now; | ||
| } | ||
| /** | ||
| * Get the TTL value from the cacheableTtl, primaryTtl, and secondaryTtl values. | ||
| * @param cacheableTtl - The cacheableTtl value to use. | ||
| * @param primaryTtl - The primaryTtl value to use. | ||
| * @param secondaryTtl - The secondaryTtl value to use. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if all values are undefined. | ||
| */ | ||
| function getCascadingTtl(cacheableTtl, primaryTtl, secondaryTtl) { | ||
| return secondaryTtl ?? primaryTtl ?? shorthandToMilliseconds(cacheableTtl); | ||
| } | ||
| /** | ||
| * Calculate the TTL value from the expires value. If the ttl is undefined, it will be set to the expires value. If the | ||
| * expires value is undefined, it will be set to the ttl value. If both values are defined, the smaller of the two will be used. | ||
| * @param ttl | ||
| * @param expires | ||
| * @returns | ||
| */ | ||
| function calculateTtlFromExpiration(ttl, expires) { | ||
| const ttlFromExpires = getTtlFromExpires(expires); | ||
| const expiresFromTtl = ttl ? Date.now() + ttl : void 0; | ||
| if (ttlFromExpires === void 0) return ttl; | ||
| if (expiresFromTtl === void 0) return ttlFromExpires; | ||
| if (expires && expires > expiresFromTtl) return ttl; | ||
| return ttlFromExpires; | ||
| } | ||
| //#endregion | ||
| export { CacheTags, HashAlgorithm, Stats, calculateTtlFromExpiration, coalesceAsync, createWrapKey, getCascadingTtl, getOrSet, getOrSetSync, getTtlFromExpires, hash, hashSync, hashToNumber, hashToNumberSync, isKeyvInstance, isObject, lessThan, nodeCacheStatsEventMap, resolvePerStoreTtl, runIfFn, shorthandToMilliseconds, shorthandToTime, sleep, wrap, wrapSync }; |
+1322
-619
@@ -1,677 +0,1380 @@ | ||
| "use strict"; | ||
| var __defProp = Object.defineProperty; | ||
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
| var __getOwnPropNames = Object.getOwnPropertyNames; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __export = (target, all) => { | ||
| for (var name in all) | ||
| __defProp(target, name, { get: all[name], enumerable: true }); | ||
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| let hashery = require("hashery"); | ||
| let keyv = require("keyv"); | ||
| //#region src/shorthand-time.ts | ||
| /** | ||
| * Converts a shorthand time string or number into milliseconds. | ||
| * The shorthand can be a string like '1s', '2m', '3h', '4d', or a number representing milliseconds. | ||
| * If the input is undefined, it returns undefined. | ||
| * If the input is a string that does not match the expected format, it throws an error. | ||
| * @param shorthand - A shorthand time string or number representing milliseconds. | ||
| * @returns The equivalent time in milliseconds or undefined. | ||
| */ | ||
| const shorthandToMilliseconds = (shorthand) => { | ||
| let milliseconds; | ||
| if (shorthand === void 0) return; | ||
| if (typeof shorthand === "number") milliseconds = shorthand; | ||
| else { | ||
| if (typeof shorthand !== "string") return; | ||
| shorthand = shorthand.trim(); | ||
| if (Number.isNaN(Number(shorthand))) { | ||
| const match = /^([\d.]+)\s*(ms|s|m|h|hr|d)$/i.exec(shorthand); | ||
| if (!match) throw new Error(`Unsupported time format: "${shorthand}". Use 'ms', 's', 'm', 'h', 'hr', or 'd'.`); | ||
| const [, value, unit] = match; | ||
| const numericValue = Number.parseFloat(value); | ||
| switch (unit.toLowerCase()) { | ||
| case "ms": | ||
| milliseconds = numericValue; | ||
| break; | ||
| case "s": | ||
| milliseconds = numericValue * 1e3; | ||
| break; | ||
| case "m": | ||
| milliseconds = numericValue * 1e3 * 60; | ||
| break; | ||
| case "h": | ||
| milliseconds = numericValue * 1e3 * 60 * 60; | ||
| break; | ||
| case "hr": | ||
| milliseconds = numericValue * 1e3 * 60 * 60; | ||
| break; | ||
| case "d": | ||
| milliseconds = numericValue * 1e3 * 60 * 60 * 24; | ||
| break; | ||
| /* v8 ignore next -- @preserve */ | ||
| default: milliseconds = Number(shorthand); | ||
| } | ||
| } else milliseconds = Number(shorthand); | ||
| } | ||
| return milliseconds; | ||
| }; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") { | ||
| for (let key of __getOwnPropNames(from)) | ||
| if (!__hasOwnProp.call(to, key) && key !== except) | ||
| __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
| } | ||
| return to; | ||
| /** | ||
| * Converts a shorthand time string or number into a timestamp. | ||
| * If the shorthand is undefined, it returns the current date's timestamp. | ||
| * If the shorthand is a valid time format, it adds that duration to the current date's timestamp. | ||
| * @param shorthand - A shorthand time string or number representing milliseconds. | ||
| * @param fromDate - An optional Date object to calculate from. Defaults to the current date if not provided. | ||
| * @returns The timestamp in milliseconds since epoch. | ||
| */ | ||
| const shorthandToTime = (shorthand, fromDate) => { | ||
| fromDate ??= /* @__PURE__ */ new Date(); | ||
| const milliseconds = shorthandToMilliseconds(shorthand); | ||
| if (milliseconds === void 0) return fromDate.getTime(); | ||
| return fromDate.getTime() + milliseconds; | ||
| }; | ||
| var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
| // src/index.ts | ||
| var index_exports = {}; | ||
| __export(index_exports, { | ||
| HashAlgorithm: () => HashAlgorithm, | ||
| Stats: () => Stats, | ||
| calculateTtlFromExpiration: () => calculateTtlFromExpiration, | ||
| coalesceAsync: () => coalesceAsync, | ||
| createWrapKey: () => createWrapKey, | ||
| getCascadingTtl: () => getCascadingTtl, | ||
| getOrSet: () => getOrSet, | ||
| getTtlFromExpires: () => getTtlFromExpires, | ||
| hash: () => hash, | ||
| hashSync: () => hashSync, | ||
| hashToNumber: () => hashToNumber, | ||
| hashToNumberSync: () => hashToNumberSync, | ||
| isKeyvInstance: () => isKeyvInstance, | ||
| isObject: () => isObject, | ||
| lessThan: () => lessThan, | ||
| runIfFn: () => runIfFn, | ||
| shorthandToMilliseconds: () => shorthandToMilliseconds, | ||
| shorthandToTime: () => shorthandToTime, | ||
| sleep: () => sleep, | ||
| wrap: () => wrap, | ||
| wrapSync: () => wrapSync | ||
| }); | ||
| module.exports = __toCommonJS(index_exports); | ||
| // src/shorthand-time.ts | ||
| var shorthandToMilliseconds = (shorthand) => { | ||
| let milliseconds; | ||
| if (shorthand === void 0) { | ||
| return void 0; | ||
| } | ||
| if (typeof shorthand === "number") { | ||
| milliseconds = shorthand; | ||
| } else { | ||
| if (typeof shorthand !== "string") { | ||
| return void 0; | ||
| } | ||
| shorthand = shorthand.trim(); | ||
| if (Number.isNaN(Number(shorthand))) { | ||
| const match = /^([\d.]+)\s*(ms|s|m|h|hr|d)$/i.exec(shorthand); | ||
| if (!match) { | ||
| throw new Error( | ||
| `Unsupported time format: "${shorthand}". Use 'ms', 's', 'm', 'h', 'hr', or 'd'.` | ||
| ); | ||
| } | ||
| const [, value, unit] = match; | ||
| const numericValue = Number.parseFloat(value); | ||
| const unitLower = unit.toLowerCase(); | ||
| switch (unitLower) { | ||
| case "ms": { | ||
| milliseconds = numericValue; | ||
| break; | ||
| } | ||
| case "s": { | ||
| milliseconds = numericValue * 1e3; | ||
| break; | ||
| } | ||
| case "m": { | ||
| milliseconds = numericValue * 1e3 * 60; | ||
| break; | ||
| } | ||
| case "h": { | ||
| milliseconds = numericValue * 1e3 * 60 * 60; | ||
| break; | ||
| } | ||
| case "hr": { | ||
| milliseconds = numericValue * 1e3 * 60 * 60; | ||
| break; | ||
| } | ||
| case "d": { | ||
| milliseconds = numericValue * 1e3 * 60 * 60 * 24; | ||
| break; | ||
| } | ||
| /* v8 ignore next -- @preserve */ | ||
| default: { | ||
| milliseconds = Number(shorthand); | ||
| } | ||
| } | ||
| } else { | ||
| milliseconds = Number(shorthand); | ||
| } | ||
| } | ||
| return milliseconds; | ||
| //#endregion | ||
| //#region src/cache-tags.ts | ||
| /** | ||
| * Prefix applied to every store key written by the service so its metadata cannot collide with | ||
| * user-supplied cache keys. | ||
| */ | ||
| const RESERVED_PREFIX = "--cacheable--tags--"; | ||
| /** Namespace used when none is supplied to the constructor. */ | ||
| const DEFAULT_NAMESPACE = "default"; | ||
| /** | ||
| * Provides tag-based cache invalidation on top of any {@link Keyv} store. It is store-agnostic and | ||
| * requires no adapter changes. | ||
| * | ||
| * The service uses a lazy invalidation model rather than scanning and deleting keys. Each tag has a | ||
| * monotonically increasing version counter; {@link CacheTags.invalidateTag} simply increments | ||
| * it. When a key is tagged via {@link CacheTags.setKeyTags}, a snapshot of its tags' current | ||
| * versions is stored alongside it. {@link CacheTags.isKeyFresh} compares that snapshot against | ||
| * the live versions — if any tag has been incremented since, the key is considered stale. Stale | ||
| * entries are not deleted explicitly; they are expected to fall out of the cache via their TTL. | ||
| * | ||
| * This keeps invalidation constant-time regardless of how many keys reference a tag, at the cost of | ||
| * one additional `isKeyFresh` read per cache lookup. | ||
| * | ||
| * The service can be disabled via the `enabled` option or property so integrations pay no cost for | ||
| * untagged workloads: while disabled, every method is a no-op — reads return their neutral value | ||
| * and writes are skipped. The service must be explicitly enabled to use tags; it never enables | ||
| * itself, which keeps behavior consistent across distributed instances sharing a store. | ||
| * | ||
| * All metadata is written under a reserved prefix so it cannot collide with user keys: | ||
| * - `--cacheable--tags--:<namespace>:tag:<tag>` → integer version counter (stored without TTL). | ||
| * - `--cacheable--tags--:<namespace>:key:<key>` → the {@link KeyTagEntry} snapshot. | ||
| * | ||
| * Note: the read-version-then-write-snapshot sequence in `setKeyTags` is not atomic across | ||
| * processes. A concurrent `invalidateTag` running between the read and the write can leave a freshly | ||
| * written key referencing a stale version. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const cacheTags = new CacheTags({ store: new Keyv(), namespace: 'app' }); | ||
| * await cacheTags.setKeyTags('user:42', ['users', 'org:7'], { ttl: 3600000 }); | ||
| * await cacheTags.isKeyFresh('user:42'); // true | ||
| * await cacheTags.invalidateTag('users'); | ||
| * await cacheTags.isKeyFresh('user:42'); // false | ||
| * ``` | ||
| */ | ||
| var CacheTags = class { | ||
| _store; | ||
| _namespace; | ||
| _enabled; | ||
| _onError; | ||
| /** | ||
| * Creates a new tag service. | ||
| * @param {CacheTagsOptions} options - The store, optional namespace, enabled state, and | ||
| * non-blocking error handler to use. | ||
| */ | ||
| constructor(options) { | ||
| this._store = options.store; | ||
| this._namespace = options.namespace ?? DEFAULT_NAMESPACE; | ||
| this._enabled = options.enabled ?? true; | ||
| this._onError = options.onError; | ||
| } | ||
| /** | ||
| * The Keyv store backing this service. | ||
| * @returns {Keyv} The store provided to the constructor. | ||
| */ | ||
| get store() { | ||
| return this._store; | ||
| } | ||
| /** | ||
| * The namespace isolating this service's tags and keys within the store. | ||
| * @returns {string} The configured namespace, or `"default"` if none was provided. | ||
| */ | ||
| get namespace() { | ||
| return this._namespace; | ||
| } | ||
| /** | ||
| * Whether the service is enabled. While disabled, every method is a no-op — read methods | ||
| * return their neutral value and writes are skipped — so integrations pay no extra store | ||
| * reads for untagged workloads. The service must be explicitly enabled to use tags; it never | ||
| * enables itself. | ||
| * @returns {boolean} Whether the service is enabled. | ||
| */ | ||
| get enabled() { | ||
| return this._enabled; | ||
| } | ||
| /** | ||
| * Sets whether the service is enabled. | ||
| * @param {boolean} enabled Whether the service is enabled. | ||
| */ | ||
| set enabled(enabled) { | ||
| this._enabled = enabled; | ||
| } | ||
| /** | ||
| * Builds the reserved store key under which a tag's version counter is stored. | ||
| * @param tag - The tag name. | ||
| * @returns {string} The namespaced store key for the tag's version. | ||
| */ | ||
| tagKey(tag) { | ||
| return `${RESERVED_PREFIX}:${this._namespace}:tag:${tag}`; | ||
| } | ||
| /** | ||
| * Builds the reserved store key under which a cache key's tag snapshot is stored. | ||
| * @param key - The cache key being tagged. | ||
| * @returns {string} The namespaced store key for the key's snapshot. | ||
| */ | ||
| keyEntryKey(key) { | ||
| return `${RESERVED_PREFIX}:${this._namespace}:key:${key}`; | ||
| } | ||
| /** | ||
| * Builds the common prefix shared by every key-snapshot entry in this namespace. Used to filter | ||
| * key entries when iterating the store. | ||
| * @returns {string} The namespaced key-entry prefix. | ||
| */ | ||
| keyPrefix() { | ||
| return `${RESERVED_PREFIX}:${this._namespace}:key:`; | ||
| } | ||
| /** | ||
| * Reads the current version of a single tag. | ||
| * @param tag - The tag name. | ||
| * @returns {Promise<number>} The tag's version, or `0` if it has never been invalidated. | ||
| */ | ||
| async getTagVersion(tag) { | ||
| const version = await this._store.get(this.tagKey(tag)); | ||
| return typeof version === "number" ? version : 0; | ||
| } | ||
| /** | ||
| * Reads the current versions of multiple tags in a single batched store read. | ||
| * @param tags - The tag names to look up. | ||
| * @returns {Promise<number[]>} The versions in the same order as `tags`; entries that have never | ||
| * been invalidated resolve to `0`. Returns an empty array when `tags` is empty. | ||
| */ | ||
| async getTagVersions(tags) { | ||
| if (tags.length === 0) return []; | ||
| const tagKeys = tags.map((tag) => this.tagKey(tag)); | ||
| const raw = await this._store.get(tagKeys); | ||
| return tags.map((_, i) => { | ||
| const value = raw?.[i]; | ||
| return typeof value === "number" ? value : 0; | ||
| }); | ||
| } | ||
| /** | ||
| * Reports a fire-and-forget failure to the `onError` handler, if one was provided. | ||
| * @param error - The error raised by the non-blocking operation. | ||
| */ | ||
| handleNonBlockingError(error) { | ||
| this._onError?.(error); | ||
| } | ||
| /** | ||
| * Reads the version snapshot of each tag and writes the key's tag snapshot to the store. | ||
| * @param key - The cache key to tag. | ||
| * @param tags - The tags to associate with the key. | ||
| * @param ttl - Time-to-live in milliseconds for the snapshot. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been written. | ||
| */ | ||
| async writeKeyTags(key, tags, ttl) { | ||
| const uniqueTags = [...new Set(tags)]; | ||
| const versions = await this.getTagVersions(uniqueTags); | ||
| const snapshot = {}; | ||
| for (let i = 0; i < uniqueTags.length; i++) snapshot[uniqueTags[i]] = versions[i]; | ||
| const entry = { tags: snapshot }; | ||
| await this._store.set(this.keyEntryKey(key), entry, ttl); | ||
| } | ||
| /** | ||
| * Associates a cache key with a set of tags by recording a snapshot of each tag's current | ||
| * version. Call this whenever you write a fresh value to the cache. Duplicate tags are ignored. | ||
| * No-op while the service is disabled. | ||
| * @param key - The cache key to tag. | ||
| * @param tags - The tags to associate with the key. | ||
| * @param {SetKeyTagsOptions} [options] - Optional settings, such as a `ttl` for the snapshot or | ||
| * `nonBlocking` to fire-and-forget the write. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been written, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| async setKeyTags(key, tags, options) { | ||
| if (!this._enabled) return; | ||
| const work = this.writeKeyTags(key, tags, options?.ttl); | ||
| if (options?.nonBlocking) { | ||
| work.catch((error) => { | ||
| this.handleNonBlockingError(error); | ||
| }); | ||
| return; | ||
| } | ||
| await work; | ||
| } | ||
| /** | ||
| * Removes a key's tag snapshot. After this, {@link CacheTags.isKeyFresh} returns `false` | ||
| * for the key. Use when the cached value itself is deleted. No-op while the service is | ||
| * disabled. | ||
| * @param key - The cache key whose snapshot should be removed. | ||
| * @param {RemoveKeysOptions} [options] - Optional settings, such as `nonBlocking` to | ||
| * fire-and-forget the removal. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been deleted, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| async removeKey(key, options) { | ||
| await this.removeKeys([key], options); | ||
| } | ||
| /** | ||
| * Removes multiple keys' tag snapshots in a single batched store delete. After this, | ||
| * {@link CacheTags.isKeyFresh} returns `false` for each key. An empty list is a no-op, as is | ||
| * the entire call while the service is disabled. | ||
| * @param keys - The cache keys whose snapshots should be removed. | ||
| * @param {RemoveKeysOptions} [options] - Optional settings, such as `nonBlocking` to | ||
| * fire-and-forget the removal. | ||
| * @returns {Promise<void>} Resolves once the snapshots have been deleted, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| async removeKeys(keys, options) { | ||
| if (!this._enabled || keys.length === 0) return; | ||
| const entryKeys = keys.map((key) => this.keyEntryKey(key)); | ||
| const work = this._store.deleteMany(entryKeys); | ||
| if (options?.nonBlocking) { | ||
| work.catch((error) => { | ||
| this.handleNonBlockingError(error); | ||
| }); | ||
| return; | ||
| } | ||
| await work; | ||
| } | ||
| /** | ||
| * Determines whether a key's cached value can still be trusted. A key is fresh only when a | ||
| * snapshot exists for it and every tag in that snapshot still has the version it had at set time. | ||
| * A key with no tags is trivially fresh. Call this before returning a value from your cache. | ||
| * Always returns `true` while the service is disabled. | ||
| * @param key - The cache key to check. | ||
| * @returns {Promise<boolean>} `true` if the key is still fresh; `false` if it is unknown or any of | ||
| * its tags has been invalidated since the snapshot was taken. | ||
| */ | ||
| async isKeyFresh(key) { | ||
| if (!this._enabled) return true; | ||
| const entry = await this._store.get(this.keyEntryKey(key)); | ||
| if (!entry?.tags) return false; | ||
| const tags = Object.keys(entry.tags); | ||
| const currentVersions = await this.getTagVersions(tags); | ||
| for (let i = 0; i < tags.length; i++) if (currentVersions[i] !== entry.tags[tags[i]]) return false; | ||
| return true; | ||
| } | ||
| /** | ||
| * Determines whether a key's cached value is known to be stale due to tag invalidation. This is | ||
| * the complement of {@link CacheTags.isKeyFresh} for tagged keys, but treats keys without a | ||
| * snapshot as not stale — making it safe to call for every cache lookup, including keys that were | ||
| * never tagged. Always returns `false` while the service is disabled. | ||
| * @param key - The cache key to check. | ||
| * @returns {Promise<boolean>} `true` only when a snapshot exists for the key and at least one of | ||
| * its tags has been invalidated since the snapshot was taken; `false` otherwise (including when | ||
| * the key has no snapshot). | ||
| */ | ||
| async isKeyStale(key) { | ||
| if (!this._enabled) return false; | ||
| return (await this.getStaleKeys([key])).length > 0; | ||
| } | ||
| /** | ||
| * Determines which of the given keys are known to be stale due to tag invalidation, using two | ||
| * batched store reads regardless of how many keys are checked: one for the snapshots and one for | ||
| * the union of their tag versions. Keys without a snapshot are not considered stale. Returns an | ||
| * empty array while the service is disabled. | ||
| * @param keys - The cache keys to check. | ||
| * @returns {Promise<string[]>} The subset of `keys` whose snapshot references at least one tag | ||
| * that has been invalidated since the snapshot was taken. | ||
| */ | ||
| async getStaleKeys(keys) { | ||
| if (!this._enabled || keys.length === 0) return []; | ||
| const entryKeys = keys.map((key) => this.keyEntryKey(key)); | ||
| const entries = await this._store.get(entryKeys); | ||
| const tagSet = /* @__PURE__ */ new Set(); | ||
| for (const entry of entries) if (entry?.tags) for (const tag of Object.keys(entry.tags)) tagSet.add(tag); | ||
| const tags = [...tagSet]; | ||
| const versions = await this.getTagVersions(tags); | ||
| const currentVersions = /* @__PURE__ */ new Map(); | ||
| for (let i = 0; i < tags.length; i++) currentVersions.set(tags[i], versions[i]); | ||
| const staleKeys = []; | ||
| for (const [i, entry] of entries.entries()) { | ||
| if (!entry?.tags) continue; | ||
| for (const [tag, version] of Object.entries(entry.tags)) if (currentVersions.get(tag) !== version) { | ||
| staleKeys.push(keys[i]); | ||
| break; | ||
| } | ||
| } | ||
| return staleKeys; | ||
| } | ||
| /** | ||
| * Returns the tags currently associated with a key. Returns `undefined` while the service is | ||
| * disabled. | ||
| * @param key - The cache key to look up. | ||
| * @returns {Promise<string[] | undefined>} The tag names from the key's snapshot, or `undefined` | ||
| * if the key has no snapshot. | ||
| */ | ||
| async getTags(key) { | ||
| if (!this._enabled) return; | ||
| const entry = await this._store.get(this.keyEntryKey(key)); | ||
| if (!entry?.tags) return; | ||
| return Object.keys(entry.tags); | ||
| } | ||
| /** | ||
| * Returns all cache keys whose snapshot references the given tag. This scans every key entry in | ||
| * the namespace via the Keyv iterator, making it an `O(N)` operation intended for debugging and | ||
| * tests rather than hot paths. Returns an empty array if the underlying store exposes no iterator | ||
| * or while the service is disabled. | ||
| * @param tag - The tag to search for. | ||
| * @returns {Promise<string[]>} The cache keys (with the reserved prefix stripped) referencing the tag. | ||
| */ | ||
| async getKeysByTag(tag) { | ||
| const result = []; | ||
| if (!this._enabled) return result; | ||
| const prefix = this.keyPrefix(); | ||
| const iterator = this._store.iterator?.(this._store.namespace); | ||
| if (!iterator) return result; | ||
| for await (const [storedKey, value] of iterator) { | ||
| if (typeof storedKey !== "string" || !storedKey.startsWith(prefix)) continue; | ||
| const entry = value; | ||
| if (entry?.tags && Object.hasOwn(entry.tags, tag)) result.push(storedKey.slice(prefix.length)); | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Invalidates a single tag by incrementing its version counter. Every key whose snapshot | ||
| * references this tag becomes stale immediately. Runs in constant time regardless of how many | ||
| * keys reference the tag. No-op while the service is disabled. | ||
| * @param tag - The tag to invalidate. | ||
| * @returns {Promise<string[]>} A single-element array containing the invalidated tag, or an | ||
| * empty array while the service is disabled. | ||
| */ | ||
| async invalidateTag(tag) { | ||
| if (!this._enabled) return []; | ||
| const current = await this.getTagVersion(tag); | ||
| await this._store.set(this.tagKey(tag), current + 1); | ||
| return [tag]; | ||
| } | ||
| /** | ||
| * Invalidates multiple tags by incrementing each of their version counters in a single batched | ||
| * store write. Duplicate tags are bumped once. An empty list is a no-op, as is the entire call | ||
| * while the service is disabled. | ||
| * @param tags - The tags to invalidate. | ||
| * @returns {Promise<string[]>} The `tags` argument as provided (including any duplicates), or | ||
| * an empty array while the service is disabled. | ||
| */ | ||
| async invalidateTags(tags) { | ||
| if (!this._enabled) return []; | ||
| const uniqueTags = [...new Set(tags)]; | ||
| if (uniqueTags.length === 0) return tags; | ||
| const versions = await this.getTagVersions(uniqueTags); | ||
| const kvPairs = []; | ||
| for (let i = 0; i < uniqueTags.length; i++) kvPairs.push({ | ||
| key: this.tagKey(uniqueTags[i]), | ||
| value: versions[i] + 1 | ||
| }); | ||
| await this._store.setMany(kvPairs); | ||
| return tags; | ||
| } | ||
| }; | ||
| var shorthandToTime = (shorthand, fromDate) => { | ||
| fromDate ??= /* @__PURE__ */ new Date(); | ||
| const milliseconds = shorthandToMilliseconds(shorthand); | ||
| if (milliseconds === void 0) { | ||
| return fromDate.getTime(); | ||
| } | ||
| return fromDate.getTime() + milliseconds; | ||
| }; | ||
| // src/coalesce-async.ts | ||
| var callbacks = /* @__PURE__ */ new Map(); | ||
| //#endregion | ||
| //#region src/coalesce-async.ts | ||
| const callbacks = /* @__PURE__ */ new Map(); | ||
| function hasKey(key) { | ||
| return callbacks.has(key); | ||
| return callbacks.has(key); | ||
| } | ||
| function addKey(key) { | ||
| callbacks.set(key, []); | ||
| callbacks.set(key, []); | ||
| } | ||
| function removeKey(key) { | ||
| callbacks.delete(key); | ||
| callbacks.delete(key); | ||
| } | ||
| function addCallbackToKey(key, callback) { | ||
| const stash = getCallbacksByKey(key); | ||
| stash.push(callback); | ||
| callbacks.set(key, stash); | ||
| const stash = getCallbacksByKey(key); | ||
| stash.push(callback); | ||
| callbacks.set(key, stash); | ||
| } | ||
| function getCallbacksByKey(key) { | ||
| return callbacks.get(key) ?? []; | ||
| /* v8 ignore next -- @preserve */ | ||
| return callbacks.get(key) ?? []; | ||
| } | ||
| async function enqueue(key) { | ||
| return new Promise((resolve, reject) => { | ||
| const callback = { resolve, reject }; | ||
| addCallbackToKey(key, callback); | ||
| }); | ||
| return new Promise((resolve, reject) => { | ||
| addCallbackToKey(key, { | ||
| resolve, | ||
| reject | ||
| }); | ||
| }); | ||
| } | ||
| function dequeue(key) { | ||
| const stash = getCallbacksByKey(key); | ||
| removeKey(key); | ||
| return stash; | ||
| const stash = getCallbacksByKey(key); | ||
| removeKey(key); | ||
| return stash; | ||
| } | ||
| function coalesce(options) { | ||
| const { key, error, result } = options; | ||
| for (const callback of dequeue(key)) { | ||
| if (error) { | ||
| callback.reject(error); | ||
| } else { | ||
| callback.resolve(result); | ||
| } | ||
| } | ||
| const { key, error, result } = options; | ||
| for (const callback of dequeue(key)) | ||
| /* c8 ignore next 1 */ | ||
| if (error) | ||
| /* c8 ignore next 3 */ | ||
| callback.reject(error); | ||
| else callback.resolve(result); | ||
| } | ||
| /** | ||
| * Enqueue a promise for the group identified by `key`. | ||
| * | ||
| * All requests received for the same key while a request for that key | ||
| * is already being executed will wait. Once the running request settles | ||
| * then all the waiting requests in the group will settle, too. | ||
| * This minimizes how many times the function itself runs at the same time. | ||
| * This function resolves or rejects according to the given function argument. | ||
| * | ||
| * @url https://github.com/douglascayers/promise-coalesce | ||
| */ | ||
| async function coalesceAsync(key, fnc) { | ||
| if (!hasKey(key)) { | ||
| addKey(key); | ||
| try { | ||
| const result = await Promise.resolve(fnc()); | ||
| coalesce({ key, result }); | ||
| return result; | ||
| } catch (error) { | ||
| coalesce({ key, error }); | ||
| throw error; | ||
| } | ||
| } | ||
| return enqueue(key); | ||
| if (!hasKey(key)) { | ||
| addKey(key); | ||
| try { | ||
| const result = await Promise.resolve(fnc()); | ||
| coalesce({ | ||
| key, | ||
| result | ||
| }); | ||
| return result; | ||
| } catch (error) { | ||
| /* c8 ignore next 5 */ | ||
| coalesce({ | ||
| key, | ||
| error | ||
| }); | ||
| throw error; | ||
| } | ||
| } | ||
| return enqueue(key); | ||
| } | ||
| // src/hash.ts | ||
| var import_hashery = require("hashery"); | ||
| var HashAlgorithm = /* @__PURE__ */ ((HashAlgorithm2) => { | ||
| HashAlgorithm2["SHA256"] = "SHA-256"; | ||
| HashAlgorithm2["SHA384"] = "SHA-384"; | ||
| HashAlgorithm2["SHA512"] = "SHA-512"; | ||
| HashAlgorithm2["DJB2"] = "djb2"; | ||
| HashAlgorithm2["FNV1"] = "fnv1"; | ||
| HashAlgorithm2["MURMER"] = "murmer"; | ||
| HashAlgorithm2["CRC32"] = "crc32"; | ||
| return HashAlgorithm2; | ||
| })(HashAlgorithm || {}); | ||
| //#endregion | ||
| //#region src/hash.ts | ||
| let HashAlgorithm = /* @__PURE__ */ function(HashAlgorithm) { | ||
| HashAlgorithm["SHA256"] = "SHA-256"; | ||
| HashAlgorithm["SHA384"] = "SHA-384"; | ||
| HashAlgorithm["SHA512"] = "SHA-512"; | ||
| HashAlgorithm["DJB2"] = "djb2"; | ||
| HashAlgorithm["FNV1"] = "fnv1"; | ||
| HashAlgorithm["MURMER"] = "murmer"; | ||
| HashAlgorithm["CRC32"] = "crc32"; | ||
| return HashAlgorithm; | ||
| }({}); | ||
| /** | ||
| * Hashes an object asynchronously using the specified cryptographic algorithm. | ||
| * This method should be used for cryptographic algorithms (SHA-256, SHA-384, SHA-512). | ||
| * For non-cryptographic algorithms, use hashSync() for better performance. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use | ||
| * @returns {Promise<string>} The hash of the object | ||
| */ | ||
| async function hash(object, options = { | ||
| algorithm: "SHA-256" /* SHA256 */, | ||
| serialize: JSON.stringify | ||
| algorithm: "SHA-256", | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const algorithm = options?.algorithm ?? "SHA-256" /* SHA256 */; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const objectString = serialize(object); | ||
| const hashery = new import_hashery.Hashery(); | ||
| return hashery.toHash(objectString, { algorithm }); | ||
| const algorithm = options?.algorithm ?? "SHA-256"; | ||
| const objectString = (options?.serialize ?? JSON.stringify)(object); | ||
| return new hashery.Hashery().toHash(objectString, { algorithm }); | ||
| } | ||
| /** | ||
| * Hashes an object synchronously using the specified non-cryptographic algorithm. | ||
| * This method should be used for non-cryptographic algorithms (DJB2, FNV1, MURMER, CRC32). | ||
| * For cryptographic algorithms, use hash() instead. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use | ||
| * @returns {string} The hash of the object | ||
| */ | ||
| function hashSync(object, options = { | ||
| algorithm: "djb2" /* DJB2 */, | ||
| serialize: JSON.stringify | ||
| algorithm: "djb2", | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const algorithm = options?.algorithm ?? "djb2" /* DJB2 */; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const objectString = serialize(object); | ||
| const hashery = new import_hashery.Hashery(); | ||
| return hashery.toHashSync(objectString, { algorithm }); | ||
| const algorithm = options?.algorithm ?? "djb2"; | ||
| const objectString = (options?.serialize ?? JSON.stringify)(object); | ||
| return new hashery.Hashery().toHashSync(objectString, { algorithm }); | ||
| } | ||
| /** | ||
| * Hashes an object asynchronously and converts it to a number within a specified range. | ||
| * This method should be used for cryptographic algorithms (SHA-256, SHA-384, SHA-512). | ||
| * For non-cryptographic algorithms, use hashToNumberSync() for better performance. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use including min/max range | ||
| * @returns {Promise<number>} A number within the specified range | ||
| */ | ||
| async function hashToNumber(object, options = { | ||
| min: 0, | ||
| max: 10, | ||
| algorithm: "SHA-256" /* SHA256 */, | ||
| serialize: JSON.stringify | ||
| min: 0, | ||
| max: 10, | ||
| algorithm: "SHA-256", | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const min = options?.min ?? 0; | ||
| const max = options?.max ?? 10; | ||
| const algorithm = options?.algorithm ?? "SHA-256" /* SHA256 */; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const hashLength = options?.hashLength ?? 16; | ||
| if (min >= max) { | ||
| throw new Error( | ||
| `Invalid range: min (${min}) must be less than max (${max})` | ||
| ); | ||
| } | ||
| const objectString = serialize(object); | ||
| const hashery = new import_hashery.Hashery(); | ||
| return hashery.toNumber(objectString, { | ||
| algorithm, | ||
| min, | ||
| max, | ||
| hashLength | ||
| }); | ||
| const min = options?.min ?? 0; | ||
| const max = options?.max ?? 10; | ||
| const algorithm = options?.algorithm ?? "SHA-256"; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const hashLength = options?.hashLength ?? 16; | ||
| if (min >= max) throw new Error(`Invalid range: min (${min}) must be less than max (${max})`); | ||
| const objectString = serialize(object); | ||
| return new hashery.Hashery().toNumber(objectString, { | ||
| algorithm, | ||
| min, | ||
| max, | ||
| hashLength | ||
| }); | ||
| } | ||
| /** | ||
| * Hashes an object synchronously and converts it to a number within a specified range. | ||
| * This method should be used for non-cryptographic algorithms (DJB2, FNV1, MURMER, CRC32). | ||
| * For cryptographic algorithms, use hashToNumber() instead. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use including min/max range | ||
| * @returns {number} A number within the specified range | ||
| */ | ||
| function hashToNumberSync(object, options = { | ||
| min: 0, | ||
| max: 10, | ||
| algorithm: "djb2" /* DJB2 */, | ||
| serialize: JSON.stringify | ||
| min: 0, | ||
| max: 10, | ||
| algorithm: "djb2", | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const min = options?.min ?? 0; | ||
| const max = options?.max ?? 10; | ||
| const algorithm = options?.algorithm ?? "djb2" /* DJB2 */; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const hashLength = options?.hashLength ?? 16; | ||
| if (min >= max) { | ||
| throw new Error( | ||
| `Invalid range: min (${min}) must be less than max (${max})` | ||
| ); | ||
| } | ||
| const objectString = serialize(object); | ||
| const hashery = new import_hashery.Hashery(); | ||
| return hashery.toNumberSync(objectString, { | ||
| algorithm, | ||
| min, | ||
| max, | ||
| hashLength | ||
| }); | ||
| const min = options?.min ?? 0; | ||
| const max = options?.max ?? 10; | ||
| const algorithm = options?.algorithm ?? "djb2"; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const hashLength = options?.hashLength ?? 16; | ||
| if (min >= max) throw new Error(`Invalid range: min (${min}) must be less than max (${max})`); | ||
| const objectString = serialize(object); | ||
| return new hashery.Hashery().toNumberSync(objectString, { | ||
| algorithm, | ||
| min, | ||
| max, | ||
| hashLength | ||
| }); | ||
| } | ||
| // src/is-keyv-instance.ts | ||
| var import_keyv = require("keyv"); | ||
| function isKeyvInstance(keyv) { | ||
| if (keyv === null || keyv === void 0) { | ||
| return false; | ||
| } | ||
| if (keyv instanceof import_keyv.Keyv) { | ||
| return true; | ||
| } | ||
| const keyvMethods = [ | ||
| "generateIterator", | ||
| "get", | ||
| "getMany", | ||
| "set", | ||
| "setMany", | ||
| "delete", | ||
| "deleteMany", | ||
| "has", | ||
| "hasMany", | ||
| "clear", | ||
| "disconnect", | ||
| "serialize", | ||
| "deserialize" | ||
| ]; | ||
| return keyvMethods.every((method) => typeof keyv[method] === "function"); | ||
| //#endregion | ||
| //#region src/is-keyv-instance.ts | ||
| function isKeyvInstance(keyv$1) { | ||
| if (keyv$1 === null || keyv$1 === void 0) return false; | ||
| if (keyv$1 instanceof keyv.Keyv) return true; | ||
| return [ | ||
| "generateIterator", | ||
| "get", | ||
| "getMany", | ||
| "set", | ||
| "setMany", | ||
| "delete", | ||
| "deleteMany", | ||
| "has", | ||
| "hasMany", | ||
| "clear", | ||
| "disconnect", | ||
| "serialize", | ||
| "deserialize" | ||
| ].every((method) => typeof keyv$1[method] === "function"); | ||
| } | ||
| // src/is-object.ts | ||
| //#endregion | ||
| //#region src/is-object.ts | ||
| function isObject(value) { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| // src/less-than.ts | ||
| //#endregion | ||
| //#region src/less-than.ts | ||
| function lessThan(number1, number2) { | ||
| return typeof number1 === "number" && typeof number2 === "number" ? number1 < number2 : false; | ||
| return typeof number1 === "number" && typeof number2 === "number" ? number1 < number2 : false; | ||
| } | ||
| // src/memoize.ts | ||
| //#endregion | ||
| //#region src/memoize.ts | ||
| function wrapSync(function_, options) { | ||
| const { ttl, keyPrefix, cache, serialize } = options; | ||
| return (...arguments_) => { | ||
| let cacheKey = createWrapKey(function_, arguments_, { | ||
| keyPrefix, | ||
| serialize | ||
| }); | ||
| if (options.createKey) { | ||
| cacheKey = options.createKey(function_, arguments_, options); | ||
| } | ||
| let value = cache.get(cacheKey); | ||
| if (value === void 0) { | ||
| try { | ||
| value = function_(...arguments_); | ||
| cache.set(cacheKey, value, ttl); | ||
| } catch (error) { | ||
| cache.emit("error", error); | ||
| if (options.cacheErrors) { | ||
| cache.set(cacheKey, error, ttl); | ||
| } | ||
| } | ||
| } | ||
| return value; | ||
| }; | ||
| const { ttl, keyPrefix, cache, serialize } = options; | ||
| return (...arguments_) => { | ||
| let cacheKey = createWrapKey(function_, arguments_, { | ||
| keyPrefix, | ||
| serialize | ||
| }); | ||
| if (options.createKey) cacheKey = options.createKey(function_, arguments_, options); | ||
| let value = cache.get(cacheKey); | ||
| if (value === void 0) try { | ||
| value = function_(...arguments_); | ||
| cache.set(cacheKey, value, ttl); | ||
| } catch (error) { | ||
| cache.emit("error", error); | ||
| if (options.cacheErrors) cache.set(cacheKey, error, ttl); | ||
| } | ||
| return value; | ||
| }; | ||
| } | ||
| async function getOrSet(key, function_, options) { | ||
| const keyString = typeof key === "function" ? key(options) : key; | ||
| let value; | ||
| try { | ||
| value = await options.cache.get(keyString); | ||
| } catch (error) { | ||
| options.cache.emit("error", error); | ||
| if (options.throwErrors === true || options.throwErrors === "store") { | ||
| throw error; | ||
| } | ||
| } | ||
| if (value === void 0) { | ||
| const cacheId = options.cacheId ?? "default"; | ||
| const coalesceKey = `${cacheId}::${keyString}`; | ||
| value = await coalesceAsync(coalesceKey, async () => { | ||
| let result; | ||
| try { | ||
| try { | ||
| result = await function_(); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope( | ||
| error, | ||
| "function" | ||
| ); | ||
| } | ||
| try { | ||
| await options.cache.set(keyString, result, options.ttl); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope(error, "store"); | ||
| } | ||
| return result; | ||
| } catch (caught) { | ||
| const errorType = caught instanceof ErrorEnvelope ? caught.context : ( | ||
| /* c8 ignore next 1 */ | ||
| void 0 | ||
| ); | ||
| const error = caught instanceof ErrorEnvelope ? caught.error : caught; | ||
| options.cache.emit("error", error); | ||
| if (options.cacheErrors) { | ||
| await options.cache.set(keyString, error, options.ttl); | ||
| } | ||
| if (options.throwErrors === true || options.throwErrors === errorType) { | ||
| throw error; | ||
| } | ||
| } | ||
| return result; | ||
| }); | ||
| } | ||
| return value; | ||
| const keyString = typeof key === "function" ? key(options) : key; | ||
| let value; | ||
| try { | ||
| value = await options.cache.get(keyString); | ||
| } catch (error) { | ||
| options.cache.emit("error", error); | ||
| if (options.throwErrors === true || options.throwErrors === "store") throw error; | ||
| } | ||
| if (value === void 0) value = await coalesceAsync(`${options.cacheId ?? "default"}::${keyString}`, async () => { | ||
| let result; | ||
| try { | ||
| try { | ||
| result = await function_(); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope(error, "function"); | ||
| } | ||
| try { | ||
| await options.cache.set(keyString, result, options.ttl); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope(error, "store"); | ||
| } | ||
| return result; | ||
| } catch (caught) { | ||
| const errorType = caught instanceof ErrorEnvelope ? caught.context : void 0; | ||
| const error = caught instanceof ErrorEnvelope ? caught.error : caught; | ||
| options.cache.emit("error", error); | ||
| if (options.cacheErrors && errorType === "function") try { | ||
| await options.cache.set(keyString, error, options.ttl); | ||
| } catch (storeError) { | ||
| options.cache.emit("error", storeError); | ||
| } | ||
| if (options.throwErrors === true || options.throwErrors === errorType) throw error; | ||
| } | ||
| return result; | ||
| }); | ||
| return value; | ||
| } | ||
| /** | ||
| * Synchronous counterpart to {@link getOrSet}. Reads `key` from the cache and, on a miss, computes | ||
| * the value with `function_`, stores it, and returns it. | ||
| * | ||
| * Unlike {@link getOrSet} there is no request coalescing: synchronous code runs to completion | ||
| * without interleaving, so concurrent callers cannot stampede the setter the way they can with an | ||
| * async cache. | ||
| * | ||
| * Error handling mirrors {@link getOrSet}: errors are emitted on the cache's `error` event, can be | ||
| * cached when `cacheErrors` is set, and can be rethrown selectively via `throwErrors` (`true` for | ||
| * any error, `"function"` for setter errors, `"store"` for cache read/write errors). | ||
| * | ||
| * @param key - The cache key, or a function that derives it from the resolved options. | ||
| * @param function_ - The setter invoked on a cache miss to compute the value. | ||
| * @param options - The {@link GetOrSetSyncOptions} including the target synchronous cache. | ||
| * @returns The cached or freshly computed value, or `undefined`. | ||
| */ | ||
| function getOrSetSync(key, function_, options) { | ||
| const keyString = typeof key === "function" ? key(options) : key; | ||
| let value; | ||
| try { | ||
| value = options.cache.get(keyString); | ||
| } catch (error) { | ||
| options.cache.emit("error", error); | ||
| if (options.throwErrors === true || options.throwErrors === "store") throw error; | ||
| } | ||
| if (value === void 0) try { | ||
| try { | ||
| value = function_(); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope(error, "function"); | ||
| } | ||
| try { | ||
| options.cache.set(keyString, value, options.ttl); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope(error, "store"); | ||
| } | ||
| } catch (caught) { | ||
| const errorType = caught instanceof ErrorEnvelope ? caught.context : void 0; | ||
| const error = caught instanceof ErrorEnvelope ? caught.error : caught; | ||
| options.cache.emit("error", error); | ||
| if (options.cacheErrors && errorType === "function") try { | ||
| options.cache.set(keyString, error, options.ttl); | ||
| } catch (storeError) { | ||
| options.cache.emit("error", storeError); | ||
| } | ||
| if (options.throwErrors === true || options.throwErrors === errorType) throw error; | ||
| } | ||
| return value; | ||
| } | ||
| function wrap(function_, options) { | ||
| const { keyPrefix, serialize } = options; | ||
| return async (...arguments_) => { | ||
| let cacheKey = createWrapKey(function_, arguments_, { | ||
| keyPrefix, | ||
| serialize | ||
| }); | ||
| if (options.createKey) { | ||
| cacheKey = options.createKey(function_, arguments_, options); | ||
| } | ||
| return getOrSet( | ||
| cacheKey, | ||
| async () => function_(...arguments_), | ||
| options | ||
| ); | ||
| }; | ||
| const { keyPrefix, serialize } = options; | ||
| return async (...arguments_) => { | ||
| let cacheKey = createWrapKey(function_, arguments_, { | ||
| keyPrefix, | ||
| serialize | ||
| }); | ||
| if (options.createKey) cacheKey = options.createKey(function_, arguments_, options); | ||
| return getOrSet(cacheKey, async () => function_(...arguments_), options); | ||
| }; | ||
| } | ||
| function createWrapKey(function_, arguments_, options) { | ||
| const { keyPrefix, serialize } = options || {}; | ||
| if (!keyPrefix) { | ||
| return `${function_.name}::${hashSync(arguments_, { serialize })}`; | ||
| } | ||
| return `${keyPrefix}::${function_.name}::${hashSync(arguments_, { serialize })}`; | ||
| const { keyPrefix, serialize } = options || {}; | ||
| if (!keyPrefix) return `${function_.name}::${hashSync(arguments_, { serialize })}`; | ||
| return `${keyPrefix}::${function_.name}::${hashSync(arguments_, { serialize })}`; | ||
| } | ||
| var ErrorEnvelope = class { | ||
| constructor(error, context) { | ||
| this.error = error; | ||
| this.context = context; | ||
| } | ||
| error; | ||
| context; | ||
| constructor(error, context) { | ||
| this.error = error; | ||
| this.context = context; | ||
| } | ||
| }; | ||
| // src/run-if-fn.ts | ||
| //#endregion | ||
| //#region src/run-if-fn.ts | ||
| function runIfFn(valueOrFunction, ...arguments_) { | ||
| return typeof valueOrFunction === "function" ? valueOrFunction(...arguments_) : valueOrFunction; | ||
| return typeof valueOrFunction === "function" ? valueOrFunction(...arguments_) : valueOrFunction; | ||
| } | ||
| // src/sleep.ts | ||
| var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | ||
| // src/stats.ts | ||
| //#endregion | ||
| //#region src/sleep.ts | ||
| const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | ||
| //#endregion | ||
| //#region src/stats.ts | ||
| /** | ||
| * Event map for `@cacheable/node-cache` instances. node-cache emits with | ||
| * positional arguments (e.g. `set(key, value)`), and emits each lifecycle | ||
| * event exactly once, so the counts map cleanly. `flush` clears the cache data | ||
| * and `flush_stats` resets the stats counters, mirroring node-cache's | ||
| * `flushAll()` / `flushStats()` lifecycle. | ||
| * | ||
| * Presets for `cacheable` and `cache-manager` are intentionally not provided: | ||
| * their event streams emit per-store probes (and, for cache-manager, do not | ||
| * emit an event on a normal miss), so a simple map cannot faithfully reproduce | ||
| * their imperative stats. Wire those up with a custom map or imperative calls. | ||
| */ | ||
| const nodeCacheStatsEventMap = { | ||
| set: (stats, key) => { | ||
| stats.increment("sets"); | ||
| if (typeof key === "string" || typeof key === "number") stats.recordKey(String(key), "sets"); | ||
| }, | ||
| del: (stats, key) => { | ||
| stats.increment("deletes"); | ||
| if (typeof key === "string" || typeof key === "number") stats.recordKey(String(key), "deletes"); | ||
| }, | ||
| flush: "clears", | ||
| flush_stats: (stats) => { | ||
| stats.reset(); | ||
| } | ||
| }; | ||
| var Stats = class { | ||
| _hits = 0; | ||
| _misses = 0; | ||
| _gets = 0; | ||
| _sets = 0; | ||
| _deletes = 0; | ||
| _clears = 0; | ||
| _vsize = 0; | ||
| _ksize = 0; | ||
| _count = 0; | ||
| _enabled = false; | ||
| constructor(options) { | ||
| if (options?.enabled) { | ||
| this._enabled = options.enabled; | ||
| } | ||
| } | ||
| /** | ||
| * @returns {boolean} - Whether the stats are enabled | ||
| */ | ||
| get enabled() { | ||
| return this._enabled; | ||
| } | ||
| /** | ||
| * @param {boolean} enabled - Whether to enable the stats | ||
| */ | ||
| set enabled(enabled) { | ||
| this._enabled = enabled; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of hits | ||
| * @readonly | ||
| */ | ||
| get hits() { | ||
| return this._hits; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of misses | ||
| * @readonly | ||
| */ | ||
| get misses() { | ||
| return this._misses; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of gets | ||
| * @readonly | ||
| */ | ||
| get gets() { | ||
| return this._gets; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of sets | ||
| * @readonly | ||
| */ | ||
| get sets() { | ||
| return this._sets; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of deletes | ||
| * @readonly | ||
| */ | ||
| get deletes() { | ||
| return this._deletes; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of clears | ||
| * @readonly | ||
| */ | ||
| get clears() { | ||
| return this._clears; | ||
| } | ||
| /** | ||
| * @returns {number} - The vsize (value size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get vsize() { | ||
| return this._vsize; | ||
| } | ||
| /** | ||
| * @returns {number} - The ksize (key size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get ksize() { | ||
| return this._ksize; | ||
| } | ||
| /** | ||
| * @returns {number} - The count of the cache instance | ||
| * @readonly | ||
| */ | ||
| get count() { | ||
| return this._count; | ||
| } | ||
| incrementHits() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._hits++; | ||
| } | ||
| incrementMisses() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._misses++; | ||
| } | ||
| incrementGets() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._gets++; | ||
| } | ||
| incrementSets() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._sets++; | ||
| } | ||
| incrementDeletes() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._deletes++; | ||
| } | ||
| incrementClears() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._clears++; | ||
| } | ||
| incrementVSize(value) { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._vsize += this.roughSizeOfObject(value); | ||
| } | ||
| decreaseVSize(value) { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._vsize -= this.roughSizeOfObject(value); | ||
| } | ||
| incrementKSize(key) { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._ksize += this.roughSizeOfString(key); | ||
| } | ||
| decreaseKSize(key) { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._ksize -= this.roughSizeOfString(key); | ||
| } | ||
| incrementCount() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._count++; | ||
| } | ||
| decreaseCount() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._count--; | ||
| } | ||
| setCount(count) { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._count = count; | ||
| } | ||
| roughSizeOfString(value) { | ||
| return value.length * 2; | ||
| } | ||
| roughSizeOfObject(object) { | ||
| const objectList = []; | ||
| const stack = [object]; | ||
| let bytes = 0; | ||
| while (stack.length > 0) { | ||
| const value = stack.pop(); | ||
| if (typeof value === "boolean") { | ||
| bytes += 4; | ||
| } else if (typeof value === "string") { | ||
| bytes += value.length * 2; | ||
| } else if (typeof value === "number") { | ||
| bytes += 8; | ||
| } else { | ||
| if (value === null || value === void 0) { | ||
| bytes += 4; | ||
| continue; | ||
| } | ||
| if (objectList.includes(value)) { | ||
| continue; | ||
| } | ||
| objectList.push(value); | ||
| for (const key in value) { | ||
| bytes += key.length * 2; | ||
| stack.push(value[key]); | ||
| } | ||
| } | ||
| } | ||
| return bytes; | ||
| } | ||
| reset() { | ||
| this._hits = 0; | ||
| this._misses = 0; | ||
| this._gets = 0; | ||
| this._sets = 0; | ||
| this._deletes = 0; | ||
| this._clears = 0; | ||
| this._vsize = 0; | ||
| this._ksize = 0; | ||
| this._count = 0; | ||
| } | ||
| resetStoreValues() { | ||
| this._vsize = 0; | ||
| this._ksize = 0; | ||
| this._count = 0; | ||
| } | ||
| _counters = { | ||
| hits: 0, | ||
| misses: 0, | ||
| gets: 0, | ||
| sets: 0, | ||
| deletes: 0, | ||
| clears: 0, | ||
| count: 0 | ||
| }; | ||
| _vsize = 0; | ||
| _ksize = 0; | ||
| _enabled = false; | ||
| _lastUpdated; | ||
| _lastReset; | ||
| _subscriptions = []; | ||
| /** Backing store for the public {@link trackedKeys} read-only view. */ | ||
| _trackedKeys = /* @__PURE__ */ new Map(); | ||
| _trackKeys = false; | ||
| _maxTrackedKeys; | ||
| constructor(options) { | ||
| if (options?.enabled) this._enabled = options.enabled; | ||
| if (options?.trackKeys) this._trackKeys = options.trackKeys; | ||
| if (options?.maxTrackedKeys !== void 0) this._maxTrackedKeys = options.maxTrackedKeys; | ||
| if (options?.emitter && options?.eventMap) this.subscribe(options.emitter, options.eventMap); | ||
| } | ||
| /** | ||
| * @returns {boolean} - Whether the stats are enabled | ||
| */ | ||
| get enabled() { | ||
| return this._enabled; | ||
| } | ||
| /** | ||
| * @param {boolean} enabled - Whether to enable the stats | ||
| */ | ||
| set enabled(enabled) { | ||
| this._enabled = enabled; | ||
| } | ||
| /** | ||
| * @returns {boolean} - Whether per-key statistics are tracked | ||
| */ | ||
| get trackKeys() { | ||
| return this._trackKeys; | ||
| } | ||
| /** | ||
| * @param {boolean} trackKeys - Whether to track per-key statistics | ||
| */ | ||
| set trackKeys(trackKeys) { | ||
| this._trackKeys = trackKeys; | ||
| } | ||
| /** | ||
| * @returns {number | undefined} - The cap on unique keys tracked, or | ||
| * `undefined` when unbounded | ||
| */ | ||
| get maxTrackedKeys() { | ||
| return this._maxTrackedKeys; | ||
| } | ||
| /** | ||
| * @param {number | undefined} maxTrackedKeys - The cap on unique keys | ||
| * tracked. Set `undefined` for unbounded. | ||
| */ | ||
| set maxTrackedKeys(maxTrackedKeys) { | ||
| this._maxTrackedKeys = maxTrackedKeys; | ||
| } | ||
| /** | ||
| * Per-key statistics, keyed by cache key, holding each key's raw | ||
| * `hits`/`misses`/`gets`/`sets`/`deletes` counters. Populated by | ||
| * {@link recordKey} when {@link trackKeys} is enabled; read `trackedKeys.size` | ||
| * for the number of unique keys currently tracked. The returned map is a | ||
| * read-only view — mutate per-key stats via {@link recordKey} / | ||
| * {@link clearKeys} / {@link reset}. | ||
| * @returns {ReadonlyMap<string, Readonly<KeyCounters>>} | ||
| * @readonly | ||
| */ | ||
| get trackedKeys() { | ||
| return this._trackedKeys; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of hits | ||
| * @readonly | ||
| */ | ||
| get hits() { | ||
| return this._counters.hits; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of misses | ||
| * @readonly | ||
| */ | ||
| get misses() { | ||
| return this._counters.misses; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of gets | ||
| * @readonly | ||
| */ | ||
| get gets() { | ||
| return this._counters.gets; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of sets | ||
| * @readonly | ||
| */ | ||
| get sets() { | ||
| return this._counters.sets; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of deletes | ||
| * @readonly | ||
| */ | ||
| get deletes() { | ||
| return this._counters.deletes; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of clears | ||
| * @readonly | ||
| */ | ||
| get clears() { | ||
| return this._counters.clears; | ||
| } | ||
| /** | ||
| * @returns {number} - The vsize (value size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get vsize() { | ||
| return this._vsize; | ||
| } | ||
| /** | ||
| * @returns {number} - The ksize (key size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get ksize() { | ||
| return this._ksize; | ||
| } | ||
| /** | ||
| * @returns {number} - The count of the cache instance | ||
| * @readonly | ||
| */ | ||
| get count() { | ||
| return this._counters.count; | ||
| } | ||
| /** | ||
| * The ratio of hits to total lookups (hits + misses). Returns `0` when there | ||
| * have been no lookups. | ||
| * @returns {number} - A value between 0 and 1 | ||
| * @readonly | ||
| */ | ||
| get hitRate() { | ||
| const total = this._counters.hits + this._counters.misses; | ||
| return total === 0 ? 0 : this._counters.hits / total; | ||
| } | ||
| /** | ||
| * The ratio of misses to total lookups (hits + misses). Returns `0` when | ||
| * there have been no lookups. | ||
| * @returns {number} - A value between 0 and 1 | ||
| * @readonly | ||
| */ | ||
| get missRate() { | ||
| const total = this._counters.hits + this._counters.misses; | ||
| return total === 0 ? 0 : this._counters.misses / total; | ||
| } | ||
| /** | ||
| * The timestamp (ms since epoch) of the last mutation while enabled, or | ||
| * `undefined` if there have been none since the last reset. | ||
| * @returns {number | undefined} | ||
| * @readonly | ||
| */ | ||
| get lastUpdated() { | ||
| return this._lastUpdated; | ||
| } | ||
| /** | ||
| * The timestamp (ms since epoch) of the last {@link reset}/{@link clear}, or | ||
| * `undefined` if it has never been reset. | ||
| * @returns {number | undefined} | ||
| * @readonly | ||
| */ | ||
| get lastReset() { | ||
| return this._lastReset; | ||
| } | ||
| /** | ||
| * Increment a counter field by `amount` (default `1`). No-op when disabled. | ||
| * @param {StatField} field - The counter to increment | ||
| * @param {number} amount - The amount to add (default 1) | ||
| */ | ||
| increment(field, amount = 1) { | ||
| if (!this._enabled) return; | ||
| this._counters[field] += amount; | ||
| this.touch(); | ||
| } | ||
| /** | ||
| * Decrement a counter field by `amount` (default `1`). No-op when disabled. | ||
| * @param {StatField} field - The counter to decrement | ||
| * @param {number} amount - The amount to subtract (default 1) | ||
| */ | ||
| decrement(field, amount = 1) { | ||
| if (!this._enabled) return; | ||
| this._counters[field] -= amount; | ||
| this.touch(); | ||
| } | ||
| incrementHits(amount = 1) { | ||
| this.increment("hits", amount); | ||
| } | ||
| incrementMisses(amount = 1) { | ||
| this.increment("misses", amount); | ||
| } | ||
| incrementGets(amount = 1) { | ||
| this.increment("gets", amount); | ||
| } | ||
| incrementSets(amount = 1) { | ||
| this.increment("sets", amount); | ||
| } | ||
| incrementDeletes(amount = 1) { | ||
| this.increment("deletes", amount); | ||
| } | ||
| incrementClears(amount = 1) { | ||
| this.increment("clears", amount); | ||
| } | ||
| incrementVSize(value) { | ||
| if (!this._enabled) return; | ||
| this._vsize += this.roughSizeOfObject(value); | ||
| this.touch(); | ||
| } | ||
| decreaseVSize(value) { | ||
| if (!this._enabled) return; | ||
| this._vsize = Math.max(0, this._vsize - this.roughSizeOfObject(value)); | ||
| this.touch(); | ||
| } | ||
| incrementKSize(key) { | ||
| if (!this._enabled) return; | ||
| this._ksize += this.roughSizeOfString(key); | ||
| this.touch(); | ||
| } | ||
| decreaseKSize(key) { | ||
| if (!this._enabled) return; | ||
| this._ksize = Math.max(0, this._ksize - this.roughSizeOfString(key)); | ||
| this.touch(); | ||
| } | ||
| incrementCount(amount = 1) { | ||
| this.increment("count", amount); | ||
| } | ||
| decreaseCount(amount = 1) { | ||
| if (!this._enabled) return; | ||
| this._counters.count = Math.max(0, this._counters.count - amount); | ||
| this.touch(); | ||
| } | ||
| setCount(count) { | ||
| if (!this._enabled) return; | ||
| this._counters.count = count; | ||
| this.touch(); | ||
| } | ||
| roughSizeOfString(value) { | ||
| return value.length * 2; | ||
| } | ||
| roughSizeOfObject(object) { | ||
| const objectList = []; | ||
| const stack = [object]; | ||
| let bytes = 0; | ||
| while (stack.length > 0) { | ||
| const value = stack.pop(); | ||
| if (typeof value === "boolean") bytes += 4; | ||
| else if (typeof value === "string") bytes += value.length * 2; | ||
| else if (typeof value === "number") bytes += 8; | ||
| else { | ||
| if (value === null || value === void 0) { | ||
| bytes += 4; | ||
| continue; | ||
| } | ||
| if (objectList.includes(value)) continue; | ||
| objectList.push(value); | ||
| for (const key in value) { | ||
| bytes += key.length * 2; | ||
| stack.push(value[key]); | ||
| } | ||
| } | ||
| } | ||
| return bytes; | ||
| } | ||
| /** | ||
| * Enable stat tracking. Equivalent to setting {@link enabled} to `true`. | ||
| */ | ||
| enable() { | ||
| this._enabled = true; | ||
| } | ||
| /** | ||
| * Disable stat tracking. Equivalent to setting {@link enabled} to `false`. | ||
| */ | ||
| disable() { | ||
| this._enabled = false; | ||
| } | ||
| /** | ||
| * Reset all counters to zero and record the reset timestamp. Alias of | ||
| * {@link reset}. | ||
| */ | ||
| clear() { | ||
| this.reset(); | ||
| } | ||
| reset() { | ||
| this._counters = { | ||
| hits: 0, | ||
| misses: 0, | ||
| gets: 0, | ||
| sets: 0, | ||
| deletes: 0, | ||
| clears: 0, | ||
| count: 0 | ||
| }; | ||
| this._vsize = 0; | ||
| this._ksize = 0; | ||
| this._trackedKeys.clear(); | ||
| this._lastReset = Date.now(); | ||
| this._lastUpdated = void 0; | ||
| } | ||
| resetStoreValues() { | ||
| this._vsize = 0; | ||
| this._ksize = 0; | ||
| this._counters.count = 0; | ||
| } | ||
| /** | ||
| * @returns {StatsSnapshot} - A plain-object snapshot of the current stats, | ||
| * including computed `hitRate`/`missRate` and timestamps. | ||
| */ | ||
| toJSON() { | ||
| return { | ||
| enabled: this._enabled, | ||
| hits: this._counters.hits, | ||
| misses: this._counters.misses, | ||
| gets: this._counters.gets, | ||
| sets: this._counters.sets, | ||
| deletes: this._counters.deletes, | ||
| clears: this._counters.clears, | ||
| vsize: this._vsize, | ||
| ksize: this._ksize, | ||
| count: this._counters.count, | ||
| hitRate: this.hitRate, | ||
| missRate: this.missRate, | ||
| trackedKeys: this._trackedKeys.size, | ||
| lastUpdated: this._lastUpdated, | ||
| lastReset: this._lastReset | ||
| }; | ||
| } | ||
| /** | ||
| * @returns {StatsSnapshot} - A plain-object snapshot of the current stats. | ||
| * Alias of {@link toJSON}. | ||
| */ | ||
| snapshot() { | ||
| return this.toJSON(); | ||
| } | ||
| /** | ||
| * Record an operation against a specific key for per-key statistics. No-op | ||
| * unless both {@link enabled} and {@link trackKeys} are `true`. | ||
| * @param {string} key - The cache key the operation touched | ||
| * @param {KeyStatField} field - The per-key counter to increment | ||
| * @param {number} amount - The amount to add (default 1) | ||
| */ | ||
| recordKey(key, field, amount = 1) { | ||
| if (!this._enabled || !this._trackKeys) return; | ||
| let counters = this._trackedKeys.get(key); | ||
| if (!counters) { | ||
| counters = { | ||
| hits: 0, | ||
| misses: 0, | ||
| gets: 0, | ||
| sets: 0, | ||
| deletes: 0 | ||
| }; | ||
| this._trackedKeys.set(key, counters); | ||
| this.pruneTrackedKeys(key); | ||
| } | ||
| counters[field] += amount; | ||
| this.touch(); | ||
| } | ||
| /** | ||
| * The most-used keys, sorted descending. Sorts by total recorded operations, | ||
| * or by a single field when `field` is provided. Ties order by key. | ||
| * @param {number} limit - Maximum entries to return (default 100) | ||
| * @param {KeyStatField} [field] - Optionally rank by one counter (e.g. "hits") | ||
| * @returns {StatsKeyEntry[]} | ||
| */ | ||
| mostUsedKeys(limit = 100, field) { | ||
| return this.sortedKeyEntries(field, "desc").slice(0, limit); | ||
| } | ||
| /** | ||
| * The least-used keys, sorted ascending. Sorts by total recorded operations, | ||
| * or by a single field when `field` is provided. Ties order by key. Note: | ||
| * only keys that have been recorded at least once can be ranked, and when | ||
| * {@link maxTrackedKeys} pruning has occurred the true least-used keys may | ||
| * have been evicted. | ||
| * @param {number} limit - Maximum entries to return (default 100) | ||
| * @param {KeyStatField} [field] - Optionally rank by one counter (e.g. "gets") | ||
| * @returns {StatsKeyEntry[]} | ||
| */ | ||
| leastUsedKeys(limit = 100, field) { | ||
| return this.sortedKeyEntries(field, "asc").slice(0, limit); | ||
| } | ||
| /** | ||
| * @param {string} key - The key to look up | ||
| * @returns {StatsKeyEntry | undefined} - The per-key statistics, or | ||
| * `undefined` if the key has not been recorded | ||
| */ | ||
| keyStats(key) { | ||
| const counters = this._trackedKeys.get(key); | ||
| return counters ? this.toKeyEntry(key, counters) : void 0; | ||
| } | ||
| /** | ||
| * Clear all per-key statistics without touching the aggregate counters. | ||
| */ | ||
| clearKeys() { | ||
| this._trackedKeys.clear(); | ||
| } | ||
| totalOf(counters) { | ||
| return counters.hits + counters.misses + counters.gets + counters.sets + counters.deletes; | ||
| } | ||
| toKeyEntry(key, counters) { | ||
| const lookups = counters.hits + counters.misses; | ||
| return { | ||
| key, | ||
| count: this.totalOf(counters), | ||
| hits: counters.hits, | ||
| misses: counters.misses, | ||
| gets: counters.gets, | ||
| sets: counters.sets, | ||
| deletes: counters.deletes, | ||
| hitRate: lookups === 0 ? 0 : counters.hits / lookups | ||
| }; | ||
| } | ||
| sortedKeyEntries(field, direction) { | ||
| const entries = []; | ||
| for (const [key, counters] of this._trackedKeys) entries.push(this.toKeyEntry(key, counters)); | ||
| const sign = direction === "asc" ? 1 : -1; | ||
| entries.sort((a, b) => { | ||
| const valueA = field ? a[field] : a.count; | ||
| const valueB = field ? b[field] : b.count; | ||
| if (valueA !== valueB) return (valueA - valueB) * sign; | ||
| return a.key < b.key ? -1 : 1; | ||
| }); | ||
| return entries; | ||
| } | ||
| /** | ||
| * When over {@link maxTrackedKeys}, prune the lowest-count keys down to 90% | ||
| * of the cap (batched so the sort cost amortizes across inserts). The key | ||
| * that was just recorded is never pruned. | ||
| */ | ||
| pruneTrackedKeys(protectedKey) { | ||
| if (this._maxTrackedKeys === void 0 || this._trackedKeys.size <= this._maxTrackedKeys) return; | ||
| const target = Math.max(1, Math.floor(this._maxTrackedKeys * .9)); | ||
| const sorted = [...this._trackedKeys.entries()].sort((a, b) => this.totalOf(a[1]) - this.totalOf(b[1])); | ||
| for (const [key] of sorted) { | ||
| if (this._trackedKeys.size <= target) break; | ||
| if (key === protectedKey) continue; | ||
| this._trackedKeys.delete(key); | ||
| } | ||
| } | ||
| /** | ||
| * Subscribe to an emitter so that matching events automatically update the | ||
| * stats. Counting is gated by {@link enabled}, so you may subscribe first and | ||
| * toggle enablement later. Call {@link unsubscribe} to detach. | ||
| * @param {StatsEmitter} emitter - The emitter to listen on | ||
| * @param {StatsEventMap} eventMap - The event-to-stat mapping (e.g. | ||
| * {@link nodeCacheStatsEventMap} or a custom map) | ||
| */ | ||
| subscribe(emitter, eventMap) { | ||
| for (const [event, action] of Object.entries(eventMap)) { | ||
| const listener = (...args) => { | ||
| this.applyEvent(action, args); | ||
| }; | ||
| emitter.on(event, listener); | ||
| this._subscriptions.push({ | ||
| emitter, | ||
| event, | ||
| listener | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Detach listeners previously attached via {@link subscribe}. When `emitter` | ||
| * is provided, only that emitter's listeners are removed; otherwise all are. | ||
| * @param {StatsEmitter} [emitter] - The emitter to detach from | ||
| */ | ||
| unsubscribe(emitter) { | ||
| const remaining = []; | ||
| for (const sub of this._subscriptions) { | ||
| if (emitter && sub.emitter !== emitter) { | ||
| remaining.push(sub); | ||
| continue; | ||
| } | ||
| (sub.emitter.off ?? sub.emitter.removeListener)?.call(sub.emitter, sub.event, sub.listener); | ||
| } | ||
| this._subscriptions = remaining; | ||
| } | ||
| applyEvent(action, args) { | ||
| if (!this._enabled) return; | ||
| if (typeof action === "function") { | ||
| action(this, ...args); | ||
| return; | ||
| } | ||
| if (Array.isArray(action)) { | ||
| for (const field of action) this.increment(field); | ||
| return; | ||
| } | ||
| this.increment(action); | ||
| } | ||
| touch() { | ||
| this._lastUpdated = Date.now(); | ||
| } | ||
| }; | ||
| // src/ttl.ts | ||
| //#endregion | ||
| //#region src/ttl.ts | ||
| /** | ||
| * Normalizes a TTL input into per-store milliseconds. When given an object it resolves the | ||
| * `primary` and `secondary` fields independently; when given a number or shorthand string it | ||
| * applies the same value to both stores. Undefined fields (or undefined input) resolve to | ||
| * `undefined` so the caller can fall back to its own default TTL. | ||
| * @param ttl - The TTL input: a number (ms), a shorthand string, or a {@link PerStoreTtl} object. | ||
| * @returns {{ primary?: number; secondary?: number }} The resolved per-store TTLs in milliseconds. | ||
| */ | ||
| function resolvePerStoreTtl(ttl) { | ||
| if (ttl === void 0 || ttl === null) return { | ||
| primary: void 0, | ||
| secondary: void 0 | ||
| }; | ||
| if (typeof ttl === "object") return { | ||
| primary: shorthandToMilliseconds(ttl.primary), | ||
| secondary: shorthandToMilliseconds(ttl.secondary) | ||
| }; | ||
| const milliseconds = shorthandToMilliseconds(ttl); | ||
| return { | ||
| primary: milliseconds, | ||
| secondary: milliseconds | ||
| }; | ||
| } | ||
| /** | ||
| * Converts a exspires value to a TTL value. | ||
| * @param expires - The expires value to convert. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if the expires value is not valid. | ||
| */ | ||
| function getTtlFromExpires(expires) { | ||
| if (expires === void 0 || expires === null) { | ||
| return void 0; | ||
| } | ||
| const now = Date.now(); | ||
| if (expires < now) { | ||
| return void 0; | ||
| } | ||
| return expires - now; | ||
| if (expires === void 0 || expires === null) return; | ||
| const now = Date.now(); | ||
| if (expires < now) return; | ||
| return expires - now; | ||
| } | ||
| /** | ||
| * Get the TTL value from the cacheableTtl, primaryTtl, and secondaryTtl values. | ||
| * @param cacheableTtl - The cacheableTtl value to use. | ||
| * @param primaryTtl - The primaryTtl value to use. | ||
| * @param secondaryTtl - The secondaryTtl value to use. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if all values are undefined. | ||
| */ | ||
| function getCascadingTtl(cacheableTtl, primaryTtl, secondaryTtl) { | ||
| return secondaryTtl ?? primaryTtl ?? shorthandToMilliseconds(cacheableTtl); | ||
| return secondaryTtl ?? primaryTtl ?? shorthandToMilliseconds(cacheableTtl); | ||
| } | ||
| /** | ||
| * Calculate the TTL value from the expires value. If the ttl is undefined, it will be set to the expires value. If the | ||
| * expires value is undefined, it will be set to the ttl value. If both values are defined, the smaller of the two will be used. | ||
| * @param ttl | ||
| * @param expires | ||
| * @returns | ||
| */ | ||
| function calculateTtlFromExpiration(ttl, expires) { | ||
| const ttlFromExpires = getTtlFromExpires(expires); | ||
| const expiresFromTtl = ttl ? Date.now() + ttl : void 0; | ||
| if (ttlFromExpires === void 0) { | ||
| return ttl; | ||
| } | ||
| if (expiresFromTtl === void 0) { | ||
| return ttlFromExpires; | ||
| } | ||
| if (expires && expires > expiresFromTtl) { | ||
| return ttl; | ||
| } | ||
| return ttlFromExpires; | ||
| const ttlFromExpires = getTtlFromExpires(expires); | ||
| const expiresFromTtl = ttl ? Date.now() + ttl : void 0; | ||
| if (ttlFromExpires === void 0) return ttl; | ||
| if (expiresFromTtl === void 0) return ttlFromExpires; | ||
| if (expires && expires > expiresFromTtl) return ttl; | ||
| return ttlFromExpires; | ||
| } | ||
| // Annotate the CommonJS export names for ESM import in node: | ||
| 0 && (module.exports = { | ||
| HashAlgorithm, | ||
| Stats, | ||
| calculateTtlFromExpiration, | ||
| coalesceAsync, | ||
| createWrapKey, | ||
| getCascadingTtl, | ||
| getOrSet, | ||
| getTtlFromExpires, | ||
| hash, | ||
| hashSync, | ||
| hashToNumber, | ||
| hashToNumberSync, | ||
| isKeyvInstance, | ||
| isObject, | ||
| lessThan, | ||
| runIfFn, | ||
| shorthandToMilliseconds, | ||
| shorthandToTime, | ||
| sleep, | ||
| wrap, | ||
| wrapSync | ||
| }); | ||
| /* v8 ignore next -- @preserve */ | ||
| //#endregion | ||
| exports.CacheTags = CacheTags; | ||
| exports.HashAlgorithm = HashAlgorithm; | ||
| exports.Stats = Stats; | ||
| exports.calculateTtlFromExpiration = calculateTtlFromExpiration; | ||
| exports.coalesceAsync = coalesceAsync; | ||
| exports.createWrapKey = createWrapKey; | ||
| exports.getCascadingTtl = getCascadingTtl; | ||
| exports.getOrSet = getOrSet; | ||
| exports.getOrSetSync = getOrSetSync; | ||
| exports.getTtlFromExpires = getTtlFromExpires; | ||
| exports.hash = hash; | ||
| exports.hashSync = hashSync; | ||
| exports.hashToNumber = hashToNumber; | ||
| exports.hashToNumberSync = hashToNumberSync; | ||
| exports.isKeyvInstance = isKeyvInstance; | ||
| exports.isObject = isObject; | ||
| exports.lessThan = lessThan; | ||
| exports.nodeCacheStatsEventMap = nodeCacheStatsEventMap; | ||
| exports.resolvePerStoreTtl = resolvePerStoreTtl; | ||
| exports.runIfFn = runIfFn; | ||
| exports.shorthandToMilliseconds = shorthandToMilliseconds; | ||
| exports.shorthandToTime = shorthandToTime; | ||
| exports.sleep = sleep; | ||
| exports.wrap = wrap; | ||
| exports.wrapSync = wrapSync; |
+769
-173
@@ -0,1 +1,4 @@ | ||
| import { Keyv } from "keyv"; | ||
| //#region src/shorthand-time.d.ts | ||
| /** | ||
@@ -19,4 +22,272 @@ * Converts a shorthand time string or number into milliseconds. | ||
| declare const shorthandToTime: (shorthand?: string | number, fromDate?: Date) => number; | ||
| //#endregion | ||
| //#region src/cache-tags.d.ts | ||
| /** | ||
| * Options for constructing a {@link CacheTags}. | ||
| * @typedef {Object} CacheTagsOptions | ||
| * @property {Keyv} store - The Keyv store used to persist tag versions and key snapshots. | ||
| * @property {string} [namespace] - An optional namespace that isolates this service's tags | ||
| * and keys from others sharing the same store. Defaults to `"default"`. | ||
| * @property {boolean} [enabled] - Whether the service is enabled. While disabled, every method | ||
| * is a no-op: read methods return their neutral value ({@link CacheTags.isKeyFresh} returns | ||
| * `true`, {@link CacheTags.isKeyStale} returns `false`, etc.) and writes are skipped. The | ||
| * service must be explicitly enabled to use tags. Defaults to `true`. | ||
| * @property {(error: unknown) => void} [onError] - Invoked with errors from non-blocking | ||
| * (fire-and-forget) operations, which cannot be thrown to the caller. Defaults to ignoring them. | ||
| */ | ||
| type CacheTagsOptions = { | ||
| store: Keyv; | ||
| namespace?: string; | ||
| enabled?: boolean; | ||
| onError?: (error: unknown) => void; | ||
| }; | ||
| /** | ||
| * Options for {@link CacheTags.setKeyTags}. | ||
| * @typedef {Object} SetKeyTagsOptions | ||
| * @property {number} [ttl] - Time-to-live in milliseconds for the key's tag snapshot. Should | ||
| * match the TTL of the cached value it tracks so the snapshot expires alongside it. If omitted, | ||
| * the snapshot does not expire. | ||
| * @property {boolean} [nonBlocking] - When `true`, the snapshot write is fire-and-forget: | ||
| * the call resolves immediately and failures are reported via the `onError` option. | ||
| */ | ||
| type SetKeyTagsOptions = { | ||
| ttl?: number; | ||
| nonBlocking?: boolean; | ||
| }; | ||
| /** | ||
| * Options for {@link CacheTags.removeKey} and {@link CacheTags.removeKeys}. | ||
| * @typedef {Object} RemoveKeysOptions | ||
| * @property {boolean} [nonBlocking] - When `true`, the removal is fire-and-forget: | ||
| * the call resolves immediately and failures are reported via the `onError` option. | ||
| */ | ||
| type RemoveKeysOptions = { | ||
| nonBlocking?: boolean; | ||
| }; | ||
| /** | ||
| * The metadata stored for a tagged key. It records the version of each tag at the moment the key | ||
| * was written, allowing {@link CacheTags.isKeyFresh} to detect later invalidations. | ||
| * @typedef {Object} KeyTagEntry | ||
| * @property {Record<string, number>} tags - A snapshot mapping each tag name to its version at set time. | ||
| */ | ||
| type KeyTagEntry = { | ||
| tags: Record<string, number>; | ||
| }; | ||
| /** | ||
| * Provides tag-based cache invalidation on top of any {@link Keyv} store. It is store-agnostic and | ||
| * requires no adapter changes. | ||
| * | ||
| * The service uses a lazy invalidation model rather than scanning and deleting keys. Each tag has a | ||
| * monotonically increasing version counter; {@link CacheTags.invalidateTag} simply increments | ||
| * it. When a key is tagged via {@link CacheTags.setKeyTags}, a snapshot of its tags' current | ||
| * versions is stored alongside it. {@link CacheTags.isKeyFresh} compares that snapshot against | ||
| * the live versions — if any tag has been incremented since, the key is considered stale. Stale | ||
| * entries are not deleted explicitly; they are expected to fall out of the cache via their TTL. | ||
| * | ||
| * This keeps invalidation constant-time regardless of how many keys reference a tag, at the cost of | ||
| * one additional `isKeyFresh` read per cache lookup. | ||
| * | ||
| * The service can be disabled via the `enabled` option or property so integrations pay no cost for | ||
| * untagged workloads: while disabled, every method is a no-op — reads return their neutral value | ||
| * and writes are skipped. The service must be explicitly enabled to use tags; it never enables | ||
| * itself, which keeps behavior consistent across distributed instances sharing a store. | ||
| * | ||
| * All metadata is written under a reserved prefix so it cannot collide with user keys: | ||
| * - `--cacheable--tags--:<namespace>:tag:<tag>` → integer version counter (stored without TTL). | ||
| * - `--cacheable--tags--:<namespace>:key:<key>` → the {@link KeyTagEntry} snapshot. | ||
| * | ||
| * Note: the read-version-then-write-snapshot sequence in `setKeyTags` is not atomic across | ||
| * processes. A concurrent `invalidateTag` running between the read and the write can leave a freshly | ||
| * written key referencing a stale version. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const cacheTags = new CacheTags({ store: new Keyv(), namespace: 'app' }); | ||
| * await cacheTags.setKeyTags('user:42', ['users', 'org:7'], { ttl: 3600000 }); | ||
| * await cacheTags.isKeyFresh('user:42'); // true | ||
| * await cacheTags.invalidateTag('users'); | ||
| * await cacheTags.isKeyFresh('user:42'); // false | ||
| * ``` | ||
| */ | ||
| declare class CacheTags { | ||
| private readonly _store; | ||
| private readonly _namespace; | ||
| private _enabled; | ||
| private readonly _onError?; | ||
| /** | ||
| * Creates a new tag service. | ||
| * @param {CacheTagsOptions} options - The store, optional namespace, enabled state, and | ||
| * non-blocking error handler to use. | ||
| */ | ||
| constructor(options: CacheTagsOptions); | ||
| /** | ||
| * The Keyv store backing this service. | ||
| * @returns {Keyv} The store provided to the constructor. | ||
| */ | ||
| get store(): Keyv; | ||
| /** | ||
| * The namespace isolating this service's tags and keys within the store. | ||
| * @returns {string} The configured namespace, or `"default"` if none was provided. | ||
| */ | ||
| get namespace(): string; | ||
| /** | ||
| * Whether the service is enabled. While disabled, every method is a no-op — read methods | ||
| * return their neutral value and writes are skipped — so integrations pay no extra store | ||
| * reads for untagged workloads. The service must be explicitly enabled to use tags; it never | ||
| * enables itself. | ||
| * @returns {boolean} Whether the service is enabled. | ||
| */ | ||
| get enabled(): boolean; | ||
| /** | ||
| * Sets whether the service is enabled. | ||
| * @param {boolean} enabled Whether the service is enabled. | ||
| */ | ||
| set enabled(enabled: boolean); | ||
| /** | ||
| * Builds the reserved store key under which a tag's version counter is stored. | ||
| * @param tag - The tag name. | ||
| * @returns {string} The namespaced store key for the tag's version. | ||
| */ | ||
| private tagKey; | ||
| /** | ||
| * Builds the reserved store key under which a cache key's tag snapshot is stored. | ||
| * @param key - The cache key being tagged. | ||
| * @returns {string} The namespaced store key for the key's snapshot. | ||
| */ | ||
| private keyEntryKey; | ||
| /** | ||
| * Builds the common prefix shared by every key-snapshot entry in this namespace. Used to filter | ||
| * key entries when iterating the store. | ||
| * @returns {string} The namespaced key-entry prefix. | ||
| */ | ||
| private keyPrefix; | ||
| /** | ||
| * Reads the current version of a single tag. | ||
| * @param tag - The tag name. | ||
| * @returns {Promise<number>} The tag's version, or `0` if it has never been invalidated. | ||
| */ | ||
| private getTagVersion; | ||
| /** | ||
| * Reads the current versions of multiple tags in a single batched store read. | ||
| * @param tags - The tag names to look up. | ||
| * @returns {Promise<number[]>} The versions in the same order as `tags`; entries that have never | ||
| * been invalidated resolve to `0`. Returns an empty array when `tags` is empty. | ||
| */ | ||
| private getTagVersions; | ||
| /** | ||
| * Reports a fire-and-forget failure to the `onError` handler, if one was provided. | ||
| * @param error - The error raised by the non-blocking operation. | ||
| */ | ||
| private handleNonBlockingError; | ||
| /** | ||
| * Reads the version snapshot of each tag and writes the key's tag snapshot to the store. | ||
| * @param key - The cache key to tag. | ||
| * @param tags - The tags to associate with the key. | ||
| * @param ttl - Time-to-live in milliseconds for the snapshot. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been written. | ||
| */ | ||
| private writeKeyTags; | ||
| /** | ||
| * Associates a cache key with a set of tags by recording a snapshot of each tag's current | ||
| * version. Call this whenever you write a fresh value to the cache. Duplicate tags are ignored. | ||
| * No-op while the service is disabled. | ||
| * @param key - The cache key to tag. | ||
| * @param tags - The tags to associate with the key. | ||
| * @param {SetKeyTagsOptions} [options] - Optional settings, such as a `ttl` for the snapshot or | ||
| * `nonBlocking` to fire-and-forget the write. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been written, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| setKeyTags(key: string, tags: string[], options?: SetKeyTagsOptions): Promise<void>; | ||
| /** | ||
| * Removes a key's tag snapshot. After this, {@link CacheTags.isKeyFresh} returns `false` | ||
| * for the key. Use when the cached value itself is deleted. No-op while the service is | ||
| * disabled. | ||
| * @param key - The cache key whose snapshot should be removed. | ||
| * @param {RemoveKeysOptions} [options] - Optional settings, such as `nonBlocking` to | ||
| * fire-and-forget the removal. | ||
| * @returns {Promise<void>} Resolves once the snapshot has been deleted, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| removeKey(key: string, options?: RemoveKeysOptions): Promise<void>; | ||
| /** | ||
| * Removes multiple keys' tag snapshots in a single batched store delete. After this, | ||
| * {@link CacheTags.isKeyFresh} returns `false` for each key. An empty list is a no-op, as is | ||
| * the entire call while the service is disabled. | ||
| * @param keys - The cache keys whose snapshots should be removed. | ||
| * @param {RemoveKeysOptions} [options] - Optional settings, such as `nonBlocking` to | ||
| * fire-and-forget the removal. | ||
| * @returns {Promise<void>} Resolves once the snapshots have been deleted, or immediately when | ||
| * `nonBlocking` is set. | ||
| */ | ||
| removeKeys(keys: string[], options?: RemoveKeysOptions): Promise<void>; | ||
| /** | ||
| * Determines whether a key's cached value can still be trusted. A key is fresh only when a | ||
| * snapshot exists for it and every tag in that snapshot still has the version it had at set time. | ||
| * A key with no tags is trivially fresh. Call this before returning a value from your cache. | ||
| * Always returns `true` while the service is disabled. | ||
| * @param key - The cache key to check. | ||
| * @returns {Promise<boolean>} `true` if the key is still fresh; `false` if it is unknown or any of | ||
| * its tags has been invalidated since the snapshot was taken. | ||
| */ | ||
| isKeyFresh(key: string): Promise<boolean>; | ||
| /** | ||
| * Determines whether a key's cached value is known to be stale due to tag invalidation. This is | ||
| * the complement of {@link CacheTags.isKeyFresh} for tagged keys, but treats keys without a | ||
| * snapshot as not stale — making it safe to call for every cache lookup, including keys that were | ||
| * never tagged. Always returns `false` while the service is disabled. | ||
| * @param key - The cache key to check. | ||
| * @returns {Promise<boolean>} `true` only when a snapshot exists for the key and at least one of | ||
| * its tags has been invalidated since the snapshot was taken; `false` otherwise (including when | ||
| * the key has no snapshot). | ||
| */ | ||
| isKeyStale(key: string): Promise<boolean>; | ||
| /** | ||
| * Determines which of the given keys are known to be stale due to tag invalidation, using two | ||
| * batched store reads regardless of how many keys are checked: one for the snapshots and one for | ||
| * the union of their tag versions. Keys without a snapshot are not considered stale. Returns an | ||
| * empty array while the service is disabled. | ||
| * @param keys - The cache keys to check. | ||
| * @returns {Promise<string[]>} The subset of `keys` whose snapshot references at least one tag | ||
| * that has been invalidated since the snapshot was taken. | ||
| */ | ||
| getStaleKeys(keys: string[]): Promise<string[]>; | ||
| /** | ||
| * Returns the tags currently associated with a key. Returns `undefined` while the service is | ||
| * disabled. | ||
| * @param key - The cache key to look up. | ||
| * @returns {Promise<string[] | undefined>} The tag names from the key's snapshot, or `undefined` | ||
| * if the key has no snapshot. | ||
| */ | ||
| getTags(key: string): Promise<string[] | undefined>; | ||
| /** | ||
| * Returns all cache keys whose snapshot references the given tag. This scans every key entry in | ||
| * the namespace via the Keyv iterator, making it an `O(N)` operation intended for debugging and | ||
| * tests rather than hot paths. Returns an empty array if the underlying store exposes no iterator | ||
| * or while the service is disabled. | ||
| * @param tag - The tag to search for. | ||
| * @returns {Promise<string[]>} The cache keys (with the reserved prefix stripped) referencing the tag. | ||
| */ | ||
| getKeysByTag(tag: string): Promise<string[]>; | ||
| /** | ||
| * Invalidates a single tag by incrementing its version counter. Every key whose snapshot | ||
| * references this tag becomes stale immediately. Runs in constant time regardless of how many | ||
| * keys reference the tag. No-op while the service is disabled. | ||
| * @param tag - The tag to invalidate. | ||
| * @returns {Promise<string[]>} A single-element array containing the invalidated tag, or an | ||
| * empty array while the service is disabled. | ||
| */ | ||
| invalidateTag(tag: string): Promise<string[]>; | ||
| /** | ||
| * Invalidates multiple tags by incrementing each of their version counters in a single batched | ||
| * store write. Duplicate tags are bumped once. An empty list is a no-op, as is the entire call | ||
| * while the service is disabled. | ||
| * @param tags - The tags to invalidate. | ||
| * @returns {Promise<string[]>} The `tags` argument as provided (including any duplicates), or | ||
| * an empty array while the service is disabled. | ||
| */ | ||
| invalidateTags(tags: string[]): Promise<string[]>; | ||
| } | ||
| //#endregion | ||
| //#region src/cacheable-item-types.d.ts | ||
| /** | ||
| * CacheableItem | ||
@@ -31,5 +302,5 @@ * @typedef {Object} CacheableItem | ||
| type CacheableItem = { | ||
| key: string; | ||
| value: any; | ||
| ttl?: number | string; | ||
| key: string; | ||
| value: any; | ||
| ttl?: number | string; | ||
| }; | ||
@@ -44,7 +315,8 @@ /** | ||
| type CacheableStoreItem = { | ||
| key: string; | ||
| value: any; | ||
| expires?: number; | ||
| key: string; | ||
| value: any; | ||
| expires?: number; | ||
| }; | ||
| //#endregion | ||
| //#region src/coalesce-async.d.ts | ||
| /** | ||
@@ -65,25 +337,28 @@ * Enqueue a promise for the group identified by `key`. | ||
| */ | ||
| key: string, | ||
| key: string, | ||
| /** | ||
| * The function to run. | ||
| */ | ||
| fnc: () => T | PromiseLike<T>): Promise<T>; | ||
| //#endregion | ||
| //#region src/hash.d.ts | ||
| declare enum HashAlgorithm { | ||
| SHA256 = "SHA-256", | ||
| SHA384 = "SHA-384", | ||
| SHA512 = "SHA-512", | ||
| DJB2 = "djb2", | ||
| FNV1 = "fnv1", | ||
| MURMER = "murmer", | ||
| CRC32 = "crc32" | ||
| SHA256 = "SHA-256", | ||
| SHA384 = "SHA-384", | ||
| SHA512 = "SHA-512", | ||
| DJB2 = "djb2", | ||
| FNV1 = "fnv1", | ||
| MURMER = "murmer", | ||
| CRC32 = "crc32" | ||
| } | ||
| type HashOptions = { | ||
| algorithm?: HashAlgorithm; | ||
| serialize?: (object: any) => string; | ||
| algorithm?: HashAlgorithm; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type HashToNumberOptions = HashOptions & { | ||
| min?: number; | ||
| max?: number; | ||
| hashLength?: number; | ||
| min?: number; | ||
| max?: number; | ||
| hashLength?: number; | ||
| }; | ||
@@ -126,22 +401,77 @@ /** | ||
| declare function hashToNumberSync(object: any, options?: HashToNumberOptions): number; | ||
| //#endregion | ||
| //#region src/is-keyv-instance.d.ts | ||
| declare function isKeyvInstance(keyv: any): boolean; | ||
| //#endregion | ||
| //#region src/is-object.d.ts | ||
| declare function isObject<T = Record<string, unknown>>(value: unknown): value is T; | ||
| //#endregion | ||
| //#region src/less-than.d.ts | ||
| declare function lessThan(number1?: number, number2?: number): boolean; | ||
| //#endregion | ||
| //#region src/ttl.d.ts | ||
| /** | ||
| * A per-store time-to-live override. Each field is a normal TTL (a number in milliseconds or a | ||
| * human-readable shorthand such as `1s`, `1m`, `1h`, `1d`) applied to that specific store. Fields | ||
| * left undefined fall back to that store's own default TTL resolution. | ||
| */ | ||
| type PerStoreTtl = { | ||
| /** | ||
| * The time-to-live to use for the primary store. | ||
| */ | ||
| primary?: number | string; | ||
| /** | ||
| * The time-to-live to use for the secondary store. | ||
| */ | ||
| secondary?: number | string; | ||
| }; | ||
| /** | ||
| * Normalizes a TTL input into per-store milliseconds. When given an object it resolves the | ||
| * `primary` and `secondary` fields independently; when given a number or shorthand string it | ||
| * applies the same value to both stores. Undefined fields (or undefined input) resolve to | ||
| * `undefined` so the caller can fall back to its own default TTL. | ||
| * @param ttl - The TTL input: a number (ms), a shorthand string, or a {@link PerStoreTtl} object. | ||
| * @returns {{ primary?: number; secondary?: number }} The resolved per-store TTLs in milliseconds. | ||
| */ | ||
| declare function resolvePerStoreTtl(ttl?: number | string | PerStoreTtl): { | ||
| primary?: number; | ||
| secondary?: number; | ||
| }; | ||
| /** | ||
| * Converts a exspires value to a TTL value. | ||
| * @param expires - The expires value to convert. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if the expires value is not valid. | ||
| */ | ||
| declare function getTtlFromExpires(expires: number | undefined): number | undefined; | ||
| /** | ||
| * Get the TTL value from the cacheableTtl, primaryTtl, and secondaryTtl values. | ||
| * @param cacheableTtl - The cacheableTtl value to use. | ||
| * @param primaryTtl - The primaryTtl value to use. | ||
| * @param secondaryTtl - The secondaryTtl value to use. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if all values are undefined. | ||
| */ | ||
| declare function getCascadingTtl(cacheableTtl?: number | string, primaryTtl?: number, secondaryTtl?: number): number | undefined; | ||
| /** | ||
| * Calculate the TTL value from the expires value. If the ttl is undefined, it will be set to the expires value. If the | ||
| * expires value is undefined, it will be set to the ttl value. If both values are defined, the smaller of the two will be used. | ||
| * @param ttl | ||
| * @param expires | ||
| * @returns | ||
| */ | ||
| declare function calculateTtlFromExpiration(ttl: number | undefined, expires: number | undefined): number | undefined; | ||
| //#endregion | ||
| //#region src/memoize.d.ts | ||
| type CacheInstance = { | ||
| get: (key: string) => Promise<any | undefined>; | ||
| has: (key: string) => Promise<boolean>; | ||
| set: (key: string, value: any, ttl?: number | string) => Promise<void>; | ||
| on: (event: string, listener: (...args: any[]) => void) => void; | ||
| emit: (event: string, ...args: any[]) => boolean; | ||
| get: (key: string) => Promise<any | undefined>; | ||
| has: (key: string) => Promise<boolean>; | ||
| set: (key: string, value: any, ttl?: number | string | PerStoreTtl) => Promise<void>; | ||
| on: (event: string, listener: (...args: any[]) => void) => void; | ||
| emit: (event: string, ...args: any[]) => boolean; | ||
| }; | ||
| type CacheSyncInstance = { | ||
| get: (key: string) => any | undefined; | ||
| has: (key: string) => boolean; | ||
| set: (key: string, value: any, ttl?: number | string) => void; | ||
| on: (event: string, listener: (...args: any[]) => void) => void; | ||
| emit: (event: string, ...args: any[]) => boolean; | ||
| get: (key: string) => any | undefined; | ||
| has: (key: string) => boolean; | ||
| set: (key: string, value: any, ttl?: number | string) => void; | ||
| on: (event: string, listener: (...args: any[]) => void) => void; | ||
| emit: (event: string, ...args: any[]) => boolean; | ||
| }; | ||
@@ -151,37 +481,53 @@ type GetOrSetKey = string | ((options?: GetOrSetOptions) => string); | ||
| type GetOrSetFunctionOptions = { | ||
| ttl?: number | string; | ||
| cacheErrors?: boolean; | ||
| /** Whether or not to throw errors: | ||
| * - `false` (default) - do not throw any errors | ||
| * - `true` - throw any error | ||
| * - `"function"` - only throw errors that occur in the provided function / setter | ||
| * - `"store"` - only throw errors that occur when getting/setting the cache | ||
| */ | ||
| throwErrors?: boolean | GetOrSetThrowErrorsContext; | ||
| /** | ||
| * If set, this will bypass the instances nonBlocking setting for the get call. | ||
| * @type {boolean} | ||
| */ | ||
| nonBlocking?: boolean; | ||
| ttl?: number | string; | ||
| cacheErrors?: boolean; | ||
| /** Whether or not to throw errors: | ||
| * - `false` (default) - do not throw any errors | ||
| * - `true` - throw any error | ||
| * - `"function"` - only throw errors that occur in the provided function / setter | ||
| * - `"store"` - only throw errors that occur when getting/setting the cache | ||
| */ | ||
| throwErrors?: boolean | GetOrSetThrowErrorsContext; | ||
| /** | ||
| * If set, this will bypass the instances nonBlocking setting for the get call. | ||
| * @type {boolean} | ||
| */ | ||
| nonBlocking?: boolean; | ||
| }; | ||
| type GetOrSetOptions = GetOrSetFunctionOptions & { | ||
| cacheId?: string; | ||
| cache: CacheInstance; | ||
| type GetOrSetOptions = Omit<GetOrSetFunctionOptions, "ttl"> & { | ||
| ttl?: number | string | PerStoreTtl; | ||
| cacheId?: string; | ||
| cache: CacheInstance; | ||
| }; | ||
| /** | ||
| * Options for {@link getOrSetSync}, the synchronous counterpart to {@link GetOrSetOptions}. It | ||
| * targets a {@link CacheSyncInstance} and its `ttl` is always a single value (a number in | ||
| * milliseconds or a shorthand string), never a per-store object. The inherited `nonBlocking` | ||
| * option has no effect on a single, synchronous store. | ||
| */ | ||
| type GetOrSetSyncOptions = GetOrSetFunctionOptions & { | ||
| cache: CacheSyncInstance; | ||
| }; | ||
| /** | ||
| * A cache key for {@link getOrSetSync}: either a string or a function that derives the key from the | ||
| * resolved {@link GetOrSetSyncOptions}. | ||
| */ | ||
| type GetOrSetSyncKey = string | ((options?: GetOrSetSyncOptions) => string); | ||
| type CreateWrapKey = (function_: AnyFunction, arguments_: any[], options?: WrapFunctionOptions) => string; | ||
| type WrapFunctionOptions = { | ||
| ttl?: number | string; | ||
| keyPrefix?: string; | ||
| createKey?: CreateWrapKey; | ||
| cacheErrors?: boolean; | ||
| cacheId?: string; | ||
| serialize?: (object: any) => string; | ||
| ttl?: number | string; | ||
| keyPrefix?: string; | ||
| createKey?: CreateWrapKey; | ||
| cacheErrors?: boolean; | ||
| cacheId?: string; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type WrapOptions = WrapFunctionOptions & { | ||
| cache: CacheInstance; | ||
| serialize?: (object: any) => string; | ||
| type WrapOptions = Omit<WrapFunctionOptions, "ttl"> & { | ||
| ttl?: number | string | PerStoreTtl; | ||
| cache: CacheInstance; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type WrapSyncOptions = WrapFunctionOptions & { | ||
| cache: CacheSyncInstance; | ||
| serialize?: (object: any) => string; | ||
| cache: CacheSyncInstance; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
@@ -191,124 +537,374 @@ type AnyFunction = (...arguments_: any[]) => any; | ||
| declare function getOrSet<T>(key: GetOrSetKey, function_: () => Promise<T>, options: GetOrSetOptions): Promise<T | undefined>; | ||
| /** | ||
| * Synchronous counterpart to {@link getOrSet}. Reads `key` from the cache and, on a miss, computes | ||
| * the value with `function_`, stores it, and returns it. | ||
| * | ||
| * Unlike {@link getOrSet} there is no request coalescing: synchronous code runs to completion | ||
| * without interleaving, so concurrent callers cannot stampede the setter the way they can with an | ||
| * async cache. | ||
| * | ||
| * Error handling mirrors {@link getOrSet}: errors are emitted on the cache's `error` event, can be | ||
| * cached when `cacheErrors` is set, and can be rethrown selectively via `throwErrors` (`true` for | ||
| * any error, `"function"` for setter errors, `"store"` for cache read/write errors). | ||
| * | ||
| * @param key - The cache key, or a function that derives it from the resolved options. | ||
| * @param function_ - The setter invoked on a cache miss to compute the value. | ||
| * @param options - The {@link GetOrSetSyncOptions} including the target synchronous cache. | ||
| * @returns The cached or freshly computed value, or `undefined`. | ||
| */ | ||
| declare function getOrSetSync<T>(key: GetOrSetSyncKey, function_: () => T, options: GetOrSetSyncOptions): T | undefined; | ||
| declare function wrap<T>(function_: AnyFunction, options: WrapOptions): AnyFunction; | ||
| type CreateWrapKeyOptions = { | ||
| keyPrefix?: string; | ||
| serialize?: (object: any) => string; | ||
| keyPrefix?: string; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| declare function createWrapKey(function_: AnyFunction, arguments_: any[], options?: CreateWrapKeyOptions): string; | ||
| //#endregion | ||
| //#region src/run-if-fn.d.ts | ||
| type Function_<P, T> = (...arguments_: P[]) => T; | ||
| declare function runIfFn<T, P>(valueOrFunction: T | Function_<P, T>, ...arguments_: P[]): T; | ||
| //#endregion | ||
| //#region src/sleep.d.ts | ||
| declare const sleep: (ms: number) => Promise<unknown>; | ||
| type StatsOptions = { | ||
| enabled?: boolean; | ||
| //#endregion | ||
| //#region src/stats.d.ts | ||
| /** | ||
| * A counter field that can be incremented or decremented via the unified | ||
| * {@link Stats.increment} / {@link Stats.decrement} API or an event map. | ||
| */ | ||
| type StatField = "hits" | "misses" | "gets" | "sets" | "deletes" | "clears" | "count"; | ||
| /** | ||
| * A duck-typed event emitter. This intentionally matches both `Hookified` | ||
| * (used by `cacheable`, `node-cache`, `memory`, `flat-cache`) and Node's | ||
| * built-in `EventEmitter` (used by `cache-manager`, `cacheable-request`) | ||
| * without adding a hard dependency on either. | ||
| */ | ||
| type StatsEmitter = { | ||
| on(event: string, listener: (...args: any[]) => void): unknown; | ||
| off?(event: string, listener: (...args: any[]) => void): unknown; | ||
| removeListener?(event: string, listener: (...args: any[]) => void): unknown; | ||
| }; | ||
| declare class Stats { | ||
| private _hits; | ||
| private _misses; | ||
| private _gets; | ||
| private _sets; | ||
| private _deletes; | ||
| private _clears; | ||
| private _vsize; | ||
| private _ksize; | ||
| private _count; | ||
| private _enabled; | ||
| constructor(options?: StatsOptions); | ||
| /** | ||
| * @returns {boolean} - Whether the stats are enabled | ||
| */ | ||
| get enabled(): boolean; | ||
| /** | ||
| * @param {boolean} enabled - Whether to enable the stats | ||
| */ | ||
| set enabled(enabled: boolean); | ||
| /** | ||
| * @returns {number} - The number of hits | ||
| * @readonly | ||
| */ | ||
| get hits(): number; | ||
| /** | ||
| * @returns {number} - The number of misses | ||
| * @readonly | ||
| */ | ||
| get misses(): number; | ||
| /** | ||
| * @returns {number} - The number of gets | ||
| * @readonly | ||
| */ | ||
| get gets(): number; | ||
| /** | ||
| * @returns {number} - The number of sets | ||
| * @readonly | ||
| */ | ||
| get sets(): number; | ||
| /** | ||
| * @returns {number} - The number of deletes | ||
| * @readonly | ||
| */ | ||
| get deletes(): number; | ||
| /** | ||
| * @returns {number} - The number of clears | ||
| * @readonly | ||
| */ | ||
| get clears(): number; | ||
| /** | ||
| * @returns {number} - The vsize (value size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get vsize(): number; | ||
| /** | ||
| * @returns {number} - The ksize (key size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get ksize(): number; | ||
| /** | ||
| * @returns {number} - The count of the cache instance | ||
| * @readonly | ||
| */ | ||
| get count(): number; | ||
| incrementHits(): void; | ||
| incrementMisses(): void; | ||
| incrementGets(): void; | ||
| incrementSets(): void; | ||
| incrementDeletes(): void; | ||
| incrementClears(): void; | ||
| incrementVSize(value: any): void; | ||
| decreaseVSize(value: any): void; | ||
| incrementKSize(key: string): void; | ||
| decreaseKSize(key: string): void; | ||
| incrementCount(): void; | ||
| decreaseCount(): void; | ||
| setCount(count: number): void; | ||
| roughSizeOfString(value: string): number; | ||
| roughSizeOfObject(object: any): number; | ||
| reset(): void; | ||
| resetStoreValues(): void; | ||
| } | ||
| /** | ||
| * Converts a exspires value to a TTL value. | ||
| * @param expires - The expires value to convert. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if the expires value is not valid. | ||
| * A custom handler invoked when a subscribed event fires. It receives the | ||
| * {@link Stats} instance and the raw event arguments (which may be positional, | ||
| * e.g. node-cache emits `(key, value)`). | ||
| */ | ||
| declare function getTtlFromExpires(expires: number | undefined): number | undefined; | ||
| type StatsEventHandler = (stats: Stats, ...args: any[]) => void; | ||
| /** | ||
| * Get the TTL value from the cacheableTtl, primaryTtl, and secondaryTtl values. | ||
| * @param cacheableTtl - The cacheableTtl value to use. | ||
| * @param primaryTtl - The primaryTtl value to use. | ||
| * @param secondaryTtl - The secondaryTtl value to use. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if all values are undefined. | ||
| * Maps an event name to the stat update it should perform: a single field to | ||
| * increment, an array of fields to increment, or a custom handler. | ||
| */ | ||
| declare function getCascadingTtl(cacheableTtl?: number | string, primaryTtl?: number, secondaryTtl?: number): number | undefined; | ||
| type StatsEventMap = Record<string, StatField | StatField[] | StatsEventHandler>; | ||
| /** | ||
| * Calculate the TTL value from the expires value. If the ttl is undefined, it will be set to the expires value. If the | ||
| * expires value is undefined, it will be set to the ttl value. If both values are defined, the smaller of the two will be used. | ||
| * @param ttl | ||
| * @param expires | ||
| * @returns | ||
| * A counter field that can be recorded per key via {@link Stats.recordKey}. | ||
| * This is the subset of {@link StatField} that makes sense for a single key | ||
| * (`clears` and `count` are cache-wide). | ||
| */ | ||
| declare function calculateTtlFromExpiration(ttl: number | undefined, expires: number | undefined): number | undefined; | ||
| export { type AnyFunction, type CacheInstance, type CacheSyncInstance, type CacheableItem, type CacheableStoreItem, type CreateWrapKey, type CreateWrapKeyOptions, type GetOrSetFunctionOptions, type GetOrSetKey, type GetOrSetOptions, HashAlgorithm, type HashOptions, type HashToNumberOptions, Stats, type StatsOptions, type WrapFunctionOptions, type WrapOptions, type WrapSyncOptions, calculateTtlFromExpiration, coalesceAsync, createWrapKey, getCascadingTtl, getOrSet, getTtlFromExpires, hash, hashSync, hashToNumber, hashToNumberSync, isKeyvInstance, isObject, lessThan, runIfFn, shorthandToMilliseconds, shorthandToTime, sleep, wrap, wrapSync }; | ||
| type KeyStatField = "hits" | "misses" | "gets" | "sets" | "deletes"; | ||
| /** | ||
| * Per-key statistics returned by {@link Stats.mostUsedKeys}, | ||
| * {@link Stats.leastUsedKeys}, and {@link Stats.keyStats}. | ||
| */ | ||
| type StatsKeyEntry = { | ||
| key: string; /** Total recorded operations for this key (sum of all fields). */ | ||
| count: number; | ||
| hits: number; | ||
| misses: number; | ||
| gets: number; | ||
| sets: number; | ||
| deletes: number; /** `hits / (hits + misses)` for this key, or `0` when there have been no lookups. */ | ||
| hitRate: number; | ||
| }; | ||
| /** | ||
| * A plain-object snapshot of a {@link Stats} instance, suitable for logging, | ||
| * metrics, or serialization. Returned by {@link Stats.toJSON}. | ||
| */ | ||
| type StatsSnapshot = { | ||
| enabled: boolean; | ||
| hits: number; | ||
| misses: number; | ||
| gets: number; | ||
| sets: number; | ||
| deletes: number; | ||
| clears: number; | ||
| vsize: number; | ||
| ksize: number; | ||
| count: number; | ||
| hitRate: number; | ||
| missRate: number; /** Number of unique keys currently tracked (0 when key tracking is off). */ | ||
| trackedKeys: number; | ||
| lastUpdated?: number; | ||
| lastReset?: number; | ||
| }; | ||
| type StatsOptions = { | ||
| /** Whether the stats are enabled. Defaults to `false`. */enabled?: boolean; /** Optionally subscribe to an emitter immediately on construction. */ | ||
| emitter?: StatsEmitter; /** The event map to use. Required when `emitter` is provided. */ | ||
| eventMap?: StatsEventMap; /** Track per-key statistics via {@link Stats.recordKey}. Defaults to `false`. */ | ||
| trackKeys?: boolean; | ||
| /** | ||
| * Safety cap on the number of unique keys tracked. When exceeded, the | ||
| * lowest-count keys are pruned, which keeps {@link Stats.mostUsedKeys} | ||
| * approximately accurate but makes {@link Stats.leastUsedKeys} unreliable. | ||
| * Unbounded when unset. | ||
| */ | ||
| maxTrackedKeys?: number; | ||
| }; | ||
| /** | ||
| * Event map for `@cacheable/node-cache` instances. node-cache emits with | ||
| * positional arguments (e.g. `set(key, value)`), and emits each lifecycle | ||
| * event exactly once, so the counts map cleanly. `flush` clears the cache data | ||
| * and `flush_stats` resets the stats counters, mirroring node-cache's | ||
| * `flushAll()` / `flushStats()` lifecycle. | ||
| * | ||
| * Presets for `cacheable` and `cache-manager` are intentionally not provided: | ||
| * their event streams emit per-store probes (and, for cache-manager, do not | ||
| * emit an event on a normal miss), so a simple map cannot faithfully reproduce | ||
| * their imperative stats. Wire those up with a custom map or imperative calls. | ||
| */ | ||
| declare const nodeCacheStatsEventMap: StatsEventMap; | ||
| /** | ||
| * Raw per-key counters stored in {@link Stats.trackedKeys}: the | ||
| * `hits`/`misses`/`gets`/`sets`/`deletes` totals for a single cache key. | ||
| */ | ||
| type KeyCounters = Record<KeyStatField, number>; | ||
| declare class Stats { | ||
| private _counters; | ||
| private _vsize; | ||
| private _ksize; | ||
| private _enabled; | ||
| private _lastUpdated; | ||
| private _lastReset; | ||
| private _subscriptions; | ||
| /** Backing store for the public {@link trackedKeys} read-only view. */ | ||
| private _trackedKeys; | ||
| private _trackKeys; | ||
| private _maxTrackedKeys; | ||
| constructor(options?: StatsOptions); | ||
| /** | ||
| * @returns {boolean} - Whether the stats are enabled | ||
| */ | ||
| get enabled(): boolean; | ||
| /** | ||
| * @param {boolean} enabled - Whether to enable the stats | ||
| */ | ||
| set enabled(enabled: boolean); | ||
| /** | ||
| * @returns {boolean} - Whether per-key statistics are tracked | ||
| */ | ||
| get trackKeys(): boolean; | ||
| /** | ||
| * @param {boolean} trackKeys - Whether to track per-key statistics | ||
| */ | ||
| set trackKeys(trackKeys: boolean); | ||
| /** | ||
| * @returns {number | undefined} - The cap on unique keys tracked, or | ||
| * `undefined` when unbounded | ||
| */ | ||
| get maxTrackedKeys(): number | undefined; | ||
| /** | ||
| * @param {number | undefined} maxTrackedKeys - The cap on unique keys | ||
| * tracked. Set `undefined` for unbounded. | ||
| */ | ||
| set maxTrackedKeys(maxTrackedKeys: number | undefined); | ||
| /** | ||
| * Per-key statistics, keyed by cache key, holding each key's raw | ||
| * `hits`/`misses`/`gets`/`sets`/`deletes` counters. Populated by | ||
| * {@link recordKey} when {@link trackKeys} is enabled; read `trackedKeys.size` | ||
| * for the number of unique keys currently tracked. The returned map is a | ||
| * read-only view — mutate per-key stats via {@link recordKey} / | ||
| * {@link clearKeys} / {@link reset}. | ||
| * @returns {ReadonlyMap<string, Readonly<KeyCounters>>} | ||
| * @readonly | ||
| */ | ||
| get trackedKeys(): ReadonlyMap<string, Readonly<KeyCounters>>; | ||
| /** | ||
| * @returns {number} - The number of hits | ||
| * @readonly | ||
| */ | ||
| get hits(): number; | ||
| /** | ||
| * @returns {number} - The number of misses | ||
| * @readonly | ||
| */ | ||
| get misses(): number; | ||
| /** | ||
| * @returns {number} - The number of gets | ||
| * @readonly | ||
| */ | ||
| get gets(): number; | ||
| /** | ||
| * @returns {number} - The number of sets | ||
| * @readonly | ||
| */ | ||
| get sets(): number; | ||
| /** | ||
| * @returns {number} - The number of deletes | ||
| * @readonly | ||
| */ | ||
| get deletes(): number; | ||
| /** | ||
| * @returns {number} - The number of clears | ||
| * @readonly | ||
| */ | ||
| get clears(): number; | ||
| /** | ||
| * @returns {number} - The vsize (value size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get vsize(): number; | ||
| /** | ||
| * @returns {number} - The ksize (key size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get ksize(): number; | ||
| /** | ||
| * @returns {number} - The count of the cache instance | ||
| * @readonly | ||
| */ | ||
| get count(): number; | ||
| /** | ||
| * The ratio of hits to total lookups (hits + misses). Returns `0` when there | ||
| * have been no lookups. | ||
| * @returns {number} - A value between 0 and 1 | ||
| * @readonly | ||
| */ | ||
| get hitRate(): number; | ||
| /** | ||
| * The ratio of misses to total lookups (hits + misses). Returns `0` when | ||
| * there have been no lookups. | ||
| * @returns {number} - A value between 0 and 1 | ||
| * @readonly | ||
| */ | ||
| get missRate(): number; | ||
| /** | ||
| * The timestamp (ms since epoch) of the last mutation while enabled, or | ||
| * `undefined` if there have been none since the last reset. | ||
| * @returns {number | undefined} | ||
| * @readonly | ||
| */ | ||
| get lastUpdated(): number | undefined; | ||
| /** | ||
| * The timestamp (ms since epoch) of the last {@link reset}/{@link clear}, or | ||
| * `undefined` if it has never been reset. | ||
| * @returns {number | undefined} | ||
| * @readonly | ||
| */ | ||
| get lastReset(): number | undefined; | ||
| /** | ||
| * Increment a counter field by `amount` (default `1`). No-op when disabled. | ||
| * @param {StatField} field - The counter to increment | ||
| * @param {number} amount - The amount to add (default 1) | ||
| */ | ||
| increment(field: StatField, amount?: number): void; | ||
| /** | ||
| * Decrement a counter field by `amount` (default `1`). No-op when disabled. | ||
| * @param {StatField} field - The counter to decrement | ||
| * @param {number} amount - The amount to subtract (default 1) | ||
| */ | ||
| decrement(field: StatField, amount?: number): void; | ||
| incrementHits(amount?: number): void; | ||
| incrementMisses(amount?: number): void; | ||
| incrementGets(amount?: number): void; | ||
| incrementSets(amount?: number): void; | ||
| incrementDeletes(amount?: number): void; | ||
| incrementClears(amount?: number): void; | ||
| incrementVSize(value: any): void; | ||
| decreaseVSize(value: any): void; | ||
| incrementKSize(key: string): void; | ||
| decreaseKSize(key: string): void; | ||
| incrementCount(amount?: number): void; | ||
| decreaseCount(amount?: number): void; | ||
| setCount(count: number): void; | ||
| roughSizeOfString(value: string): number; | ||
| roughSizeOfObject(object: any): number; | ||
| /** | ||
| * Enable stat tracking. Equivalent to setting {@link enabled} to `true`. | ||
| */ | ||
| enable(): void; | ||
| /** | ||
| * Disable stat tracking. Equivalent to setting {@link enabled} to `false`. | ||
| */ | ||
| disable(): void; | ||
| /** | ||
| * Reset all counters to zero and record the reset timestamp. Alias of | ||
| * {@link reset}. | ||
| */ | ||
| clear(): void; | ||
| reset(): void; | ||
| resetStoreValues(): void; | ||
| /** | ||
| * @returns {StatsSnapshot} - A plain-object snapshot of the current stats, | ||
| * including computed `hitRate`/`missRate` and timestamps. | ||
| */ | ||
| toJSON(): StatsSnapshot; | ||
| /** | ||
| * @returns {StatsSnapshot} - A plain-object snapshot of the current stats. | ||
| * Alias of {@link toJSON}. | ||
| */ | ||
| snapshot(): StatsSnapshot; | ||
| /** | ||
| * Record an operation against a specific key for per-key statistics. No-op | ||
| * unless both {@link enabled} and {@link trackKeys} are `true`. | ||
| * @param {string} key - The cache key the operation touched | ||
| * @param {KeyStatField} field - The per-key counter to increment | ||
| * @param {number} amount - The amount to add (default 1) | ||
| */ | ||
| recordKey(key: string, field: KeyStatField, amount?: number): void; | ||
| /** | ||
| * The most-used keys, sorted descending. Sorts by total recorded operations, | ||
| * or by a single field when `field` is provided. Ties order by key. | ||
| * @param {number} limit - Maximum entries to return (default 100) | ||
| * @param {KeyStatField} [field] - Optionally rank by one counter (e.g. "hits") | ||
| * @returns {StatsKeyEntry[]} | ||
| */ | ||
| mostUsedKeys(limit?: number, field?: KeyStatField): StatsKeyEntry[]; | ||
| /** | ||
| * The least-used keys, sorted ascending. Sorts by total recorded operations, | ||
| * or by a single field when `field` is provided. Ties order by key. Note: | ||
| * only keys that have been recorded at least once can be ranked, and when | ||
| * {@link maxTrackedKeys} pruning has occurred the true least-used keys may | ||
| * have been evicted. | ||
| * @param {number} limit - Maximum entries to return (default 100) | ||
| * @param {KeyStatField} [field] - Optionally rank by one counter (e.g. "gets") | ||
| * @returns {StatsKeyEntry[]} | ||
| */ | ||
| leastUsedKeys(limit?: number, field?: KeyStatField): StatsKeyEntry[]; | ||
| /** | ||
| * @param {string} key - The key to look up | ||
| * @returns {StatsKeyEntry | undefined} - The per-key statistics, or | ||
| * `undefined` if the key has not been recorded | ||
| */ | ||
| keyStats(key: string): StatsKeyEntry | undefined; | ||
| /** | ||
| * Clear all per-key statistics without touching the aggregate counters. | ||
| */ | ||
| clearKeys(): void; | ||
| private totalOf; | ||
| private toKeyEntry; | ||
| private sortedKeyEntries; | ||
| /** | ||
| * When over {@link maxTrackedKeys}, prune the lowest-count keys down to 90% | ||
| * of the cap (batched so the sort cost amortizes across inserts). The key | ||
| * that was just recorded is never pruned. | ||
| */ | ||
| private pruneTrackedKeys; | ||
| /** | ||
| * Subscribe to an emitter so that matching events automatically update the | ||
| * stats. Counting is gated by {@link enabled}, so you may subscribe first and | ||
| * toggle enablement later. Call {@link unsubscribe} to detach. | ||
| * @param {StatsEmitter} emitter - The emitter to listen on | ||
| * @param {StatsEventMap} eventMap - The event-to-stat mapping (e.g. | ||
| * {@link nodeCacheStatsEventMap} or a custom map) | ||
| */ | ||
| subscribe(emitter: StatsEmitter, eventMap: StatsEventMap): void; | ||
| /** | ||
| * Detach listeners previously attached via {@link subscribe}. When `emitter` | ||
| * is provided, only that emitter's listeners are removed; otherwise all are. | ||
| * @param {StatsEmitter} [emitter] - The emitter to detach from | ||
| */ | ||
| unsubscribe(emitter?: StatsEmitter): void; | ||
| private applyEvent; | ||
| private touch; | ||
| } | ||
| //#endregion | ||
| export { type AnyFunction, type CacheInstance, type CacheSyncInstance, CacheTags, type CacheTagsOptions, type CacheableItem, type CacheableStoreItem, type CreateWrapKey, type CreateWrapKeyOptions, type GetOrSetFunctionOptions, type GetOrSetKey, type GetOrSetOptions, type GetOrSetSyncKey, type GetOrSetSyncOptions, HashAlgorithm, type HashOptions, type HashToNumberOptions, type KeyCounters, type KeyStatField, type KeyTagEntry, type PerStoreTtl, type RemoveKeysOptions, type SetKeyTagsOptions, type StatField, Stats, type StatsEmitter, type StatsEventHandler, type StatsEventMap, type StatsKeyEntry, type StatsOptions, type StatsSnapshot, type WrapFunctionOptions, type WrapOptions, type WrapSyncOptions, calculateTtlFromExpiration, coalesceAsync, createWrapKey, getCascadingTtl, getOrSet, getOrSetSync, getTtlFromExpires, hash, hashSync, hashToNumber, hashToNumberSync, isKeyvInstance, isObject, lessThan, nodeCacheStatsEventMap, resolvePerStoreTtl, runIfFn, shorthandToMilliseconds, shorthandToTime, sleep, wrap, wrapSync }; |
+8
-8
| { | ||
| "name": "@cacheable/utils", | ||
| "version": "2.4.1", | ||
| "version": "2.5.0", | ||
| "description": "Cacheable Utilities for Caching Libraries", | ||
| "type": "module", | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "main": "./dist/index.cjs", | ||
| "module": "./dist/index.mjs", | ||
| "types": "./dist/index.d.mts", | ||
| "exports": { | ||
| ".": { | ||
| "import": { | ||
| "types": "./dist/index.d.ts", | ||
| "default": "./dist/index.js" | ||
| "types": "./dist/index.d.mts", | ||
| "default": "./dist/index.mjs" | ||
| }, | ||
@@ -34,3 +34,3 @@ "require": { | ||
| "devDependencies": { | ||
| "tsup": "^8.5.1", | ||
| "tsdown": "^0.22.0", | ||
| "typescript": "^5.9.3" | ||
@@ -51,3 +51,3 @@ }, | ||
| "scripts": { | ||
| "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean", | ||
| "build": "rimraf ./dist && tsdown src/index.ts --format cjs,esm --dts --clean", | ||
| "lint": "biome check --write --error-on-warnings", | ||
@@ -54,0 +54,0 @@ "test": "pnpm lint && vitest run --coverage", |
+342
-8
@@ -16,6 +16,7 @@ [<img align="center" src="https://cacheable.org/logo.svg" alt="Cacheable" />](https://github.com/jaredwray/cacheable) | ||
| * Coalesce Async for Handling Multiple Promises | ||
| * Stats Helpers for Caching Statistics | ||
| * Statistics for Tracking Cache Metrics | ||
| * Sleep / Delay for Testing and Timing | ||
| * Memoization for wraping or get / set options | ||
| * Time to Live (TTL) Helpers | ||
| * Tag-Based Cache Invalidation | ||
@@ -29,3 +30,3 @@ # Table of Contents | ||
| * [Sleep Helper](#sleep-helper) | ||
| * [Stats Helpers](#stats-helpers) | ||
| * [Statistics](#statistics) | ||
| * [Time to Live (TTL) Helpers](#time-to-live-ttl-helpers) | ||
@@ -37,2 +38,3 @@ * [Run if Function Helper](#run-if-function-helper) | ||
| * [Get Or Set Memoization Function](#get-or-set-memoization-function) | ||
| * [Cache Tags](#cache-tags) | ||
| * [How to Contribute](#how-to-contribute) | ||
@@ -197,14 +199,219 @@ * [License and Copyright](#license-and-copyright) | ||
| # Stats Helpers | ||
| # Statistics | ||
| The `@cacheable/utils` package provides statistics helpers that can be used to track and analyze caching operations. These helpers can be used to gather metrics such as hit rates, miss rates, and other performance-related statistics. | ||
| The `Stats` class provides a unified, event-driven way to track caching metrics such as hits, misses, hit rate, item counts, and approximate memory usage. It can be driven two ways: | ||
| * **Imperatively** — call `increment` / `decrement` (or the named helpers) directly from your code. | ||
| * **Event-driven** — `subscribe` it to an event emitter (such as `@cacheable/node-cache` or a Node `EventEmitter`) and let matching events update the counters automatically. | ||
| Statistics are **opt-in**: a new `Stats` instance is disabled by default and ignores every update until enabled, so there is zero tracking overhead unless you ask for it. | ||
| ```typescript | ||
| import { stats } from '@cacheable/utils'; | ||
| import { Stats } from '@cacheable/utils'; | ||
| const cacheStats = stats(); | ||
| cacheStats.incrementHits(); | ||
| console.log(cacheStats.hits); // Get the hit rate of the cache | ||
| const stats = new Stats({ enabled: true }); | ||
| stats.incrementHits(); | ||
| stats.incrementMisses(); | ||
| stats.incrementGets(); | ||
| console.log(stats.hits); // 1 | ||
| console.log(stats.misses); // 1 | ||
| console.log(stats.hitRate); // 0.5 | ||
| ``` | ||
| ## Available Statistics | ||
| Every counter is exposed as a read-only property: | ||
| | Property | Type | Description | | ||
| | --- | --- | --- | | ||
| | `hits` | `number` | Number of cache hits. | | ||
| | `misses` | `number` | Number of cache misses. | | ||
| | `gets` | `number` | Number of get operations. | | ||
| | `sets` | `number` | Number of set operations. | | ||
| | `deletes` | `number` | Number of delete operations. | | ||
| | `clears` | `number` | Number of clear operations. | | ||
| | `count` | `number` | Number of items currently tracked. | | ||
| | `ksize` | `number` | Approximate size of all keys, in bytes. | | ||
| | `vsize` | `number` | Approximate size of all values, in bytes. | | ||
| ### Computed Properties | ||
| | Property | Type | Description | | ||
| | --- | --- | --- | | ||
| | `hitRate` | `number` | `hits / (hits + misses)`, or `0` when there have been no lookups. | | ||
| | `missRate` | `number` | `misses / (hits + misses)`, or `0` when there have been no lookups. | | ||
| ### Metadata | ||
| | Property | Type | Description | | ||
| | --- | --- | --- | | ||
| | `enabled` | `boolean` | Whether tracking is currently on. | | ||
| | `lastUpdated` | `number \| undefined` | Timestamp (ms since epoch) of the last update while enabled. | | ||
| | `lastReset` | `number \| undefined` | Timestamp (ms since epoch) of the last `reset()` / `clear()`. | | ||
| ## Enabling, Disabling, and Clearing | ||
| ```typescript | ||
| const stats = new Stats(); // disabled by default | ||
| stats.enable(); // start tracking (or: stats.enabled = true) | ||
| stats.incrementHits(); | ||
| console.log(stats.hits); // 1 | ||
| stats.disable(); // stop tracking (or: stats.enabled = false) | ||
| stats.incrementHits(); | ||
| console.log(stats.hits); // still 1 | ||
| stats.clear(); // reset every counter back to 0 (alias of reset()) | ||
| console.log(stats.hits); // 0 | ||
| ``` | ||
| * `reset()` / `clear()` — set every counter back to `0` and record `lastReset`. | ||
| * `resetStoreValues()` — reset only `count`, `ksize`, and `vsize`, leaving the hit/miss history intact. | ||
| ## Incrementing and Decrementing | ||
| Use the unified `increment` / `decrement` methods with any counter field, or the named helpers. All updates are ignored while disabled. | ||
| ```typescript | ||
| const stats = new Stats({ enabled: true }); | ||
| // Unified API — optional amount (defaults to 1) | ||
| stats.increment('hits'); | ||
| stats.increment('sets', 5); | ||
| stats.decrement('count', 2); | ||
| // Named helpers | ||
| stats.incrementHits(); | ||
| stats.incrementMisses(); | ||
| stats.incrementGets(); | ||
| stats.incrementSets(); | ||
| stats.incrementDeletes(); | ||
| stats.incrementClears(); | ||
| stats.incrementCount(); | ||
| stats.decreaseCount(); | ||
| // Approximate key/value sizes | ||
| stats.incrementKSize('my-key'); // adds the byte size of the key | ||
| stats.incrementVSize({ a: 1 }); // adds the byte size of the value | ||
| stats.decreaseKSize('my-key'); | ||
| stats.decreaseVSize({ a: 1 }); | ||
| stats.setCount(10); // set the item count directly | ||
| ``` | ||
| `StatField` is the union of countable fields: `'hits' | 'misses' | 'gets' | 'sets' | 'deletes' | 'clears' | 'count'`. | ||
| ## Snapshot | ||
| `toJSON()` (aliased as `snapshot()`) returns a plain object of every counter, the computed rates, and the timestamps — handy for logging or sending to a metrics system. | ||
| ```typescript | ||
| const stats = new Stats({ enabled: true }); | ||
| stats.incrementHits(3); | ||
| stats.incrementMisses(); | ||
| console.log(stats.toJSON()); | ||
| // { | ||
| // enabled: true, | ||
| // hits: 3, misses: 1, gets: 0, sets: 0, deletes: 0, clears: 0, | ||
| // vsize: 0, ksize: 0, count: 0, | ||
| // hitRate: 0.75, missRate: 0.25, | ||
| // lastUpdated: 1749513600000, lastReset: undefined | ||
| // } | ||
| ``` | ||
| ## Event-Driven Tracking | ||
| Instead of calling the increment methods yourself, you can `subscribe` a `Stats` instance to an emitter and have events update the counters automatically. The emitter is duck-typed — anything with `.on()` (plus `.off()` or `.removeListener()` to detach) works, including `Hookified`-based classes and Node's `EventEmitter`. | ||
| An **event map** describes how each event name updates the stats. A map value can be: | ||
| * a single field — `"sets"` | ||
| * an array of fields — `["hits", "gets"]` | ||
| * a custom handler — `(stats, ...args) => void` | ||
| ```typescript | ||
| import { Stats, nodeCacheStatsEventMap } from '@cacheable/utils'; | ||
| import { NodeCache } from '@cacheable/node-cache'; | ||
| const cache = new NodeCache(); | ||
| const stats = new Stats({ enabled: true }); | ||
| // nodeCacheStatsEventMap maps set -> sets, del -> deletes, flush -> clears, | ||
| // and flush_stats -> reset. | ||
| stats.subscribe(cache, nodeCacheStatsEventMap); | ||
| cache.set('key', 'value'); | ||
| console.log(stats.sets); // 1 | ||
| stats.unsubscribe(); // detach all listeners (or pass an emitter to detach just one) | ||
| ``` | ||
| You can also provide your own map for any emitter: | ||
| ```typescript | ||
| import { EventEmitter } from 'node:events'; | ||
| import { Stats } from '@cacheable/utils'; | ||
| const emitter = new EventEmitter(); | ||
| const stats = new Stats({ enabled: true }); | ||
| stats.subscribe(emitter, { | ||
| 'cache:hit': ['hits', 'gets'], | ||
| 'cache:miss': ['misses', 'gets'], | ||
| evicted: (s) => s.incrementDeletes(), | ||
| }); | ||
| emitter.emit('cache:hit', { key: 'a' }); | ||
| console.log(stats.hitRate); // 1 | ||
| ``` | ||
| You can subscribe to multiple emitters from a single `Stats` instance, and pass an emitter to `unsubscribe(emitter)` to detach just that one. Counting is gated by `enabled`, so you can subscribe first and toggle tracking on later — handlers do not run at all while disabled. | ||
| ## Per-Key Tracking (Most and Least Used Keys) | ||
| To find your hottest and coldest keys, enable per-key tracking with `trackKeys`. Each recorded key keeps its own breakdown of `hits`, `misses`, `gets`, `sets`, and `deletes`, plus a computed total `count` and per-key `hitRate`. | ||
| ```typescript | ||
| import { Stats } from '@cacheable/utils'; | ||
| const stats = new Stats({ enabled: true, trackKeys: true }); | ||
| stats.recordKey('user:1', 'hits'); | ||
| stats.recordKey('user:1', 'gets', 5); | ||
| stats.recordKey('user:2', 'misses'); | ||
| // 100 most used keys by total operations (descending) | ||
| console.log(stats.mostUsedKeys(100)); | ||
| // [ | ||
| // { key: 'user:1', count: 6, hits: 1, misses: 0, gets: 5, sets: 0, deletes: 0, hitRate: 1 }, | ||
| // { key: 'user:2', count: 1, hits: 0, misses: 1, gets: 0, sets: 0, deletes: 0, hitRate: 0 } | ||
| // ] | ||
| // 100 least used keys by total operations (ascending) | ||
| console.log(stats.leastUsedKeys(100)); | ||
| // Rank by a single counter instead of the total | ||
| console.log(stats.mostUsedKeys(100, 'hits')); | ||
| // Inspect one key, or read the (read-only) tracked-keys map directly | ||
| console.log(stats.keyStats('user:1')); | ||
| console.log(stats.trackedKeys.size); | ||
| stats.clearKeys(); // clear per-key stats only (reset() clears these too) | ||
| ``` | ||
| Both `mostUsedKeys` and `leastUsedKeys` default to 100 entries, and `trackedKeys` is included in `toJSON()` snapshots. | ||
| Per-key tracking is fed two ways, just like the aggregate counters: | ||
| * **Imperatively** — call `recordKey(key, field, amount?)` wherever you already increment stats. | ||
| * **Event-driven** — `nodeCacheStatsEventMap` automatically records keys from `set`/`del` events when `trackKeys` is on, and custom event-map handlers can call `recordKey` with whatever the payload carries. | ||
| Memory is proportional to the number of unique keys tracked, so `trackKeys` is off by default. You can also set `maxTrackedKeys` as a safety cap — when exceeded, the lowest-count keys are pruned. Note that pruning keeps `mostUsedKeys` approximately accurate but makes `leastUsedKeys` unreliable (the pruned keys *are* the least used), so leave it unset if you need exact least-used-key rankings. | ||
| > **Note:** a built-in map is provided only where a library's events map cleanly to stats. `nodeCacheStatsEventMap` is included because `@cacheable/node-cache` emits each lifecycle event exactly once. Libraries that emit per-store probes or omit events on a miss (such as `cacheable` and `cache-manager`) should be wired with a custom map or driven imperatively so the counts stay accurate. | ||
| # Time to Live (TTL) Helpers | ||
@@ -519,2 +726,129 @@ | ||
| # Cache Tags | ||
| The `CacheTags` service provides tag-based invalidation on top of any [Keyv](https://github.com/jaredwray/keyv) store. It is store-agnostic and does not require any adapter changes. | ||
| The service uses a lazy invalidation model. Instead of scanning and deleting keys, `invalidateTag` increments a per-tag version counter. Each cached key stores a snapshot of its tag versions at the time it was written, and `isKeyFresh` compares that snapshot to the current versions. If any tag version has been incremented since the snapshot was taken, the key is considered stale. Stale entries are not deleted explicitly and are expected to fall out of the cache via their TTL. | ||
| This approach keeps invalidation constant-time regardless of how many keys reference a tag. The trade-off is one additional `isKeyFresh` read per cache lookup. | ||
| ```typescript | ||
| import { Keyv } from 'keyv'; | ||
| import { CacheTags } from '@cacheable/utils'; | ||
| const store = new Keyv(); | ||
| const cacheTags = new CacheTags({ store, namespace: 'app' }); | ||
| await cacheTags.setKeyTags('user:42', ['users', 'org:7'], { ttl: 3600000 }); | ||
| console.log(await cacheTags.isKeyFresh('user:42')); // true | ||
| await cacheTags.invalidateTag('users'); | ||
| console.log(await cacheTags.isKeyFresh('user:42')); // false | ||
| ``` | ||
| The recommended pattern is to call `isKeyFresh` before trusting a value returned from your cache, and to refresh the tag snapshot whenever you write a new value: | ||
| ```typescript | ||
| import { Cacheable } from 'cacheable'; | ||
| import { Keyv } from 'keyv'; | ||
| import { CacheTags } from '@cacheable/utils'; | ||
| const cache = new Cacheable(); | ||
| const cacheTags = new CacheTags({ store: new Keyv() }); | ||
| const getUser = async (id: string) => { | ||
| const key = `user:${id}`; | ||
| if (await cacheTags.isKeyFresh(key)) { | ||
| const cached = await cache.get(key); | ||
| if (cached !== undefined) { | ||
| return cached; | ||
| } | ||
| } | ||
| const fresh = await loadUser(id); | ||
| await cache.set(key, fresh, '1h'); | ||
| await cacheTags.setKeyTags(key, ['users', `org:${fresh.orgId}`], { ttl: 3600000 }); | ||
| return fresh; | ||
| }; | ||
| ``` | ||
| You can invalidate one or many tags at a time. Both methods return the names of the tags that were bumped: | ||
| ```typescript | ||
| const bumped = await cacheTags.invalidateTags(['users', 'org:7']); | ||
| console.log(bumped); // ['users', 'org:7'] | ||
| ``` | ||
| When integrating with a cache where most keys are untagged, use `isKeyStale` instead of `isKeyFresh`. It only reports `true` when a snapshot exists for the key and one of its tags has been invalidated, so keys that were never tagged are not treated as stale: | ||
| ```typescript | ||
| console.log(await cacheTags.isKeyStale('never-tagged')); // false | ||
| await cacheTags.setKeyTags('user:42', ['users']); | ||
| console.log(await cacheTags.isKeyStale('user:42')); // false | ||
| await cacheTags.invalidateTag('users'); | ||
| console.log(await cacheTags.isKeyStale('user:42')); // true | ||
| ``` | ||
| The `getStaleKeys` method checks many keys at once using two batched store reads regardless of how many keys are passed — one for the snapshots and one for the union of their tag versions: | ||
| ```typescript | ||
| await cacheTags.setKeyTags('a', ['x']); | ||
| await cacheTags.setKeyTags('b', ['y']); | ||
| await cacheTags.invalidateTag('x'); | ||
| console.log(await cacheTags.getStaleKeys(['a', 'b', 'untagged'])); // ['a'] | ||
| ``` | ||
| The `getTags` method returns the tags currently associated with a key, or `undefined` if the key has no snapshot: | ||
| ```typescript | ||
| await cacheTags.setKeyTags('user:42', ['users', 'org:7']); | ||
| console.log(await cacheTags.getTags('user:42')); // ['users', 'org:7'] | ||
| console.log(await cacheTags.getTags('missing')); // undefined | ||
| ``` | ||
| The `removeKey` and `removeKeys` methods delete tag snapshots when the cached values themselves are deleted. `removeKeys` performs a single batched delete: | ||
| ```typescript | ||
| await cacheTags.removeKeys(['user:1', 'user:2']); | ||
| ``` | ||
| The service can be disabled via the `enabled` option or property so integrations pay no extra store reads for untagged workloads. While disabled, every method is a no-op: read methods return their neutral value (`isKeyFresh` returns `true`, `isKeyStale` returns `false`, `getStaleKeys` returns `[]`, and so on) and writes are skipped. The service never enables itself — you have to turn it on explicitly, which keeps behavior consistent across distributed instances sharing a store: | ||
| ```typescript | ||
| const cacheTags = new CacheTags({ store, enabled: false }); | ||
| console.log(await cacheTags.isKeyStale('anything')); // false, no store read | ||
| await cacheTags.setKeyTags('user:42', ['users']); // no-op while disabled | ||
| cacheTags.enabled = true; // turn it on to use tags | ||
| ``` | ||
| `setKeyTags`, `removeKey`, and `removeKeys` accept a `nonBlocking` option to fire-and-forget the store write. Failures from non-blocking operations are reported to the `onError` constructor option since they cannot be thrown to the caller: | ||
| ```typescript | ||
| const cacheTags = new CacheTags({ store, onError: (error) => console.error(error) }); | ||
| await cacheTags.setKeyTags('user:42', ['users'], { ttl: 3600000, nonBlocking: true }); | ||
| ``` | ||
| The `getKeysByTag` method returns the keys currently referencing a given tag. It iterates the Keyv namespace and is therefore an `O(N)` operation. It is intended for debugging and tests rather than hot paths. | ||
| ```typescript | ||
| await cacheTags.setKeyTags('user:1', ['users']); | ||
| await cacheTags.setKeyTags('user:2', ['users']); | ||
| const keys = await cacheTags.getKeysByTag('users'); | ||
| console.log(keys); // ['user:1', 'user:2'] | ||
| ``` | ||
| The service stores its metadata under a reserved prefix so that it cannot collide with user keys: | ||
| ``` | ||
| --cacheable--tags--:<namespace>:tag:<tagName> → integer version counter | ||
| --cacheable--tags--:<namespace>:key:<keyName> → { tags: { [tag]: versionAtSetTime } } | ||
| ``` | ||
| Tag version counters are stored without a TTL because they must outlive any key that references them. Key entries respect the `ttl` passed to `setKeyTags`, which should be set to match the TTL of the cached value it tracks. | ||
| The namespace defaults to `default` and can be set via the constructor. Two services configured with different namespaces can share the same store without seeing each other's tags or keys. | ||
| The read-version then write-snapshot sequence in `setKeyTags` is not atomic across processes. A concurrent `invalidateTag` that runs between the read and the write can leave a freshly written key referencing a stale version. An atomic Redis fast path using `MULTI` or Lua is a planned future enhancement. | ||
| # How to Contribute | ||
@@ -521,0 +855,0 @@ |
-307
| /** | ||
| * Converts a shorthand time string or number into milliseconds. | ||
| * The shorthand can be a string like '1s', '2m', '3h', '4d', or a number representing milliseconds. | ||
| * If the input is undefined, it returns undefined. | ||
| * If the input is a string that does not match the expected format, it throws an error. | ||
| * @param shorthand - A shorthand time string or number representing milliseconds. | ||
| * @returns The equivalent time in milliseconds or undefined. | ||
| */ | ||
| declare const shorthandToMilliseconds: (shorthand?: string | number) => number | undefined; | ||
| /** | ||
| * Converts a shorthand time string or number into a timestamp. | ||
| * If the shorthand is undefined, it returns the current date's timestamp. | ||
| * If the shorthand is a valid time format, it adds that duration to the current date's timestamp. | ||
| * @param shorthand - A shorthand time string or number representing milliseconds. | ||
| * @param fromDate - An optional Date object to calculate from. Defaults to the current date if not provided. | ||
| * @returns The timestamp in milliseconds since epoch. | ||
| */ | ||
| declare const shorthandToTime: (shorthand?: string | number, fromDate?: Date) => number; | ||
| /** | ||
| * CacheableItem | ||
| * @typedef {Object} CacheableItem | ||
| * @property {string} key - The key of the cacheable item | ||
| * @property {any} value - The value of the cacheable item | ||
| * @property {number|string} [ttl] - Time to Live - If you set a number it is miliseconds, if you set a string it is a human-readable | ||
| * format such as `1s` for 1 second or `1h` for 1 hour. Setting undefined means that it will use the default time-to-live. If both are | ||
| * undefined then it will not have a time-to-live. | ||
| */ | ||
| type CacheableItem = { | ||
| key: string; | ||
| value: any; | ||
| ttl?: number | string; | ||
| }; | ||
| /** | ||
| * CacheableStoreItem | ||
| * @typedef {Object} CacheableStoreItem | ||
| * @property {string} key - The key of the cacheable store item | ||
| * @property {any} value - The value of the cacheable store item | ||
| * @property {number} [expires] - The expiration time in milliseconds since epoch. If not set, the item does not expire. | ||
| */ | ||
| type CacheableStoreItem = { | ||
| key: string; | ||
| value: any; | ||
| expires?: number; | ||
| }; | ||
| /** | ||
| * Enqueue a promise for the group identified by `key`. | ||
| * | ||
| * All requests received for the same key while a request for that key | ||
| * is already being executed will wait. Once the running request settles | ||
| * then all the waiting requests in the group will settle, too. | ||
| * This minimizes how many times the function itself runs at the same time. | ||
| * This function resolves or rejects according to the given function argument. | ||
| * | ||
| * @url https://github.com/douglascayers/promise-coalesce | ||
| */ | ||
| declare function coalesceAsync<T>( | ||
| /** | ||
| * Any identifier to group requests together. | ||
| */ | ||
| key: string, | ||
| /** | ||
| * The function to run. | ||
| */ | ||
| fnc: () => T | PromiseLike<T>): Promise<T>; | ||
| declare enum HashAlgorithm { | ||
| SHA256 = "SHA-256", | ||
| SHA384 = "SHA-384", | ||
| SHA512 = "SHA-512", | ||
| DJB2 = "djb2", | ||
| FNV1 = "fnv1", | ||
| MURMER = "murmer", | ||
| CRC32 = "crc32" | ||
| } | ||
| type HashOptions = { | ||
| algorithm?: HashAlgorithm; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type HashToNumberOptions = HashOptions & { | ||
| min?: number; | ||
| max?: number; | ||
| hashLength?: number; | ||
| }; | ||
| /** | ||
| * Hashes an object asynchronously using the specified cryptographic algorithm. | ||
| * This method should be used for cryptographic algorithms (SHA-256, SHA-384, SHA-512). | ||
| * For non-cryptographic algorithms, use hashSync() for better performance. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use | ||
| * @returns {Promise<string>} The hash of the object | ||
| */ | ||
| declare function hash(object: any, options?: HashOptions): Promise<string>; | ||
| /** | ||
| * Hashes an object synchronously using the specified non-cryptographic algorithm. | ||
| * This method should be used for non-cryptographic algorithms (DJB2, FNV1, MURMER, CRC32). | ||
| * For cryptographic algorithms, use hash() instead. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use | ||
| * @returns {string} The hash of the object | ||
| */ | ||
| declare function hashSync(object: any, options?: HashOptions): string; | ||
| /** | ||
| * Hashes an object asynchronously and converts it to a number within a specified range. | ||
| * This method should be used for cryptographic algorithms (SHA-256, SHA-384, SHA-512). | ||
| * For non-cryptographic algorithms, use hashToNumberSync() for better performance. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use including min/max range | ||
| * @returns {Promise<number>} A number within the specified range | ||
| */ | ||
| declare function hashToNumber(object: any, options?: HashToNumberOptions): Promise<number>; | ||
| /** | ||
| * Hashes an object synchronously and converts it to a number within a specified range. | ||
| * This method should be used for non-cryptographic algorithms (DJB2, FNV1, MURMER, CRC32). | ||
| * For cryptographic algorithms, use hashToNumber() instead. | ||
| * @param object The object to hash | ||
| * @param options The hash options to use including min/max range | ||
| * @returns {number} A number within the specified range | ||
| */ | ||
| declare function hashToNumberSync(object: any, options?: HashToNumberOptions): number; | ||
| declare function isKeyvInstance(keyv: any): boolean; | ||
| declare function isObject<T = Record<string, unknown>>(value: unknown): value is T; | ||
| declare function lessThan(number1?: number, number2?: number): boolean; | ||
| type CacheInstance = { | ||
| get: (key: string) => Promise<any | undefined>; | ||
| has: (key: string) => Promise<boolean>; | ||
| set: (key: string, value: any, ttl?: number | string) => Promise<void>; | ||
| on: (event: string, listener: (...args: any[]) => void) => void; | ||
| emit: (event: string, ...args: any[]) => boolean; | ||
| }; | ||
| type CacheSyncInstance = { | ||
| get: (key: string) => any | undefined; | ||
| has: (key: string) => boolean; | ||
| set: (key: string, value: any, ttl?: number | string) => void; | ||
| on: (event: string, listener: (...args: any[]) => void) => void; | ||
| emit: (event: string, ...args: any[]) => boolean; | ||
| }; | ||
| type GetOrSetKey = string | ((options?: GetOrSetOptions) => string); | ||
| type GetOrSetThrowErrorsContext = "function" | "store"; | ||
| type GetOrSetFunctionOptions = { | ||
| ttl?: number | string; | ||
| cacheErrors?: boolean; | ||
| /** Whether or not to throw errors: | ||
| * - `false` (default) - do not throw any errors | ||
| * - `true` - throw any error | ||
| * - `"function"` - only throw errors that occur in the provided function / setter | ||
| * - `"store"` - only throw errors that occur when getting/setting the cache | ||
| */ | ||
| throwErrors?: boolean | GetOrSetThrowErrorsContext; | ||
| /** | ||
| * If set, this will bypass the instances nonBlocking setting for the get call. | ||
| * @type {boolean} | ||
| */ | ||
| nonBlocking?: boolean; | ||
| }; | ||
| type GetOrSetOptions = GetOrSetFunctionOptions & { | ||
| cacheId?: string; | ||
| cache: CacheInstance; | ||
| }; | ||
| type CreateWrapKey = (function_: AnyFunction, arguments_: any[], options?: WrapFunctionOptions) => string; | ||
| type WrapFunctionOptions = { | ||
| ttl?: number | string; | ||
| keyPrefix?: string; | ||
| createKey?: CreateWrapKey; | ||
| cacheErrors?: boolean; | ||
| cacheId?: string; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type WrapOptions = WrapFunctionOptions & { | ||
| cache: CacheInstance; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type WrapSyncOptions = WrapFunctionOptions & { | ||
| cache: CacheSyncInstance; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| type AnyFunction = (...arguments_: any[]) => any; | ||
| declare function wrapSync<T>(function_: AnyFunction, options: WrapSyncOptions): AnyFunction; | ||
| declare function getOrSet<T>(key: GetOrSetKey, function_: () => Promise<T>, options: GetOrSetOptions): Promise<T | undefined>; | ||
| declare function wrap<T>(function_: AnyFunction, options: WrapOptions): AnyFunction; | ||
| type CreateWrapKeyOptions = { | ||
| keyPrefix?: string; | ||
| serialize?: (object: any) => string; | ||
| }; | ||
| declare function createWrapKey(function_: AnyFunction, arguments_: any[], options?: CreateWrapKeyOptions): string; | ||
| type Function_<P, T> = (...arguments_: P[]) => T; | ||
| declare function runIfFn<T, P>(valueOrFunction: T | Function_<P, T>, ...arguments_: P[]): T; | ||
| declare const sleep: (ms: number) => Promise<unknown>; | ||
| type StatsOptions = { | ||
| enabled?: boolean; | ||
| }; | ||
| declare class Stats { | ||
| private _hits; | ||
| private _misses; | ||
| private _gets; | ||
| private _sets; | ||
| private _deletes; | ||
| private _clears; | ||
| private _vsize; | ||
| private _ksize; | ||
| private _count; | ||
| private _enabled; | ||
| constructor(options?: StatsOptions); | ||
| /** | ||
| * @returns {boolean} - Whether the stats are enabled | ||
| */ | ||
| get enabled(): boolean; | ||
| /** | ||
| * @param {boolean} enabled - Whether to enable the stats | ||
| */ | ||
| set enabled(enabled: boolean); | ||
| /** | ||
| * @returns {number} - The number of hits | ||
| * @readonly | ||
| */ | ||
| get hits(): number; | ||
| /** | ||
| * @returns {number} - The number of misses | ||
| * @readonly | ||
| */ | ||
| get misses(): number; | ||
| /** | ||
| * @returns {number} - The number of gets | ||
| * @readonly | ||
| */ | ||
| get gets(): number; | ||
| /** | ||
| * @returns {number} - The number of sets | ||
| * @readonly | ||
| */ | ||
| get sets(): number; | ||
| /** | ||
| * @returns {number} - The number of deletes | ||
| * @readonly | ||
| */ | ||
| get deletes(): number; | ||
| /** | ||
| * @returns {number} - The number of clears | ||
| * @readonly | ||
| */ | ||
| get clears(): number; | ||
| /** | ||
| * @returns {number} - The vsize (value size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get vsize(): number; | ||
| /** | ||
| * @returns {number} - The ksize (key size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get ksize(): number; | ||
| /** | ||
| * @returns {number} - The count of the cache instance | ||
| * @readonly | ||
| */ | ||
| get count(): number; | ||
| incrementHits(): void; | ||
| incrementMisses(): void; | ||
| incrementGets(): void; | ||
| incrementSets(): void; | ||
| incrementDeletes(): void; | ||
| incrementClears(): void; | ||
| incrementVSize(value: any): void; | ||
| decreaseVSize(value: any): void; | ||
| incrementKSize(key: string): void; | ||
| decreaseKSize(key: string): void; | ||
| incrementCount(): void; | ||
| decreaseCount(): void; | ||
| setCount(count: number): void; | ||
| roughSizeOfString(value: string): number; | ||
| roughSizeOfObject(object: any): number; | ||
| reset(): void; | ||
| resetStoreValues(): void; | ||
| } | ||
| /** | ||
| * Converts a exspires value to a TTL value. | ||
| * @param expires - The expires value to convert. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if the expires value is not valid. | ||
| */ | ||
| declare function getTtlFromExpires(expires: number | undefined): number | undefined; | ||
| /** | ||
| * Get the TTL value from the cacheableTtl, primaryTtl, and secondaryTtl values. | ||
| * @param cacheableTtl - The cacheableTtl value to use. | ||
| * @param primaryTtl - The primaryTtl value to use. | ||
| * @param secondaryTtl - The secondaryTtl value to use. | ||
| * @returns {number | undefined} The TTL value in milliseconds, or undefined if all values are undefined. | ||
| */ | ||
| declare function getCascadingTtl(cacheableTtl?: number | string, primaryTtl?: number, secondaryTtl?: number): number | undefined; | ||
| /** | ||
| * Calculate the TTL value from the expires value. If the ttl is undefined, it will be set to the expires value. If the | ||
| * expires value is undefined, it will be set to the ttl value. If both values are defined, the smaller of the two will be used. | ||
| * @param ttl | ||
| * @param expires | ||
| * @returns | ||
| */ | ||
| declare function calculateTtlFromExpiration(ttl: number | undefined, expires: number | undefined): number | undefined; | ||
| export { type AnyFunction, type CacheInstance, type CacheSyncInstance, type CacheableItem, type CacheableStoreItem, type CreateWrapKey, type CreateWrapKeyOptions, type GetOrSetFunctionOptions, type GetOrSetKey, type GetOrSetOptions, HashAlgorithm, type HashOptions, type HashToNumberOptions, Stats, type StatsOptions, type WrapFunctionOptions, type WrapOptions, type WrapSyncOptions, calculateTtlFromExpiration, coalesceAsync, createWrapKey, getCascadingTtl, getOrSet, getTtlFromExpires, hash, hashSync, hashToNumber, hashToNumberSync, isKeyvInstance, isObject, lessThan, runIfFn, shorthandToMilliseconds, shorthandToTime, sleep, wrap, wrapSync }; |
-630
| // src/shorthand-time.ts | ||
| var shorthandToMilliseconds = (shorthand) => { | ||
| let milliseconds; | ||
| if (shorthand === void 0) { | ||
| return void 0; | ||
| } | ||
| if (typeof shorthand === "number") { | ||
| milliseconds = shorthand; | ||
| } else { | ||
| if (typeof shorthand !== "string") { | ||
| return void 0; | ||
| } | ||
| shorthand = shorthand.trim(); | ||
| if (Number.isNaN(Number(shorthand))) { | ||
| const match = /^([\d.]+)\s*(ms|s|m|h|hr|d)$/i.exec(shorthand); | ||
| if (!match) { | ||
| throw new Error( | ||
| `Unsupported time format: "${shorthand}". Use 'ms', 's', 'm', 'h', 'hr', or 'd'.` | ||
| ); | ||
| } | ||
| const [, value, unit] = match; | ||
| const numericValue = Number.parseFloat(value); | ||
| const unitLower = unit.toLowerCase(); | ||
| switch (unitLower) { | ||
| case "ms": { | ||
| milliseconds = numericValue; | ||
| break; | ||
| } | ||
| case "s": { | ||
| milliseconds = numericValue * 1e3; | ||
| break; | ||
| } | ||
| case "m": { | ||
| milliseconds = numericValue * 1e3 * 60; | ||
| break; | ||
| } | ||
| case "h": { | ||
| milliseconds = numericValue * 1e3 * 60 * 60; | ||
| break; | ||
| } | ||
| case "hr": { | ||
| milliseconds = numericValue * 1e3 * 60 * 60; | ||
| break; | ||
| } | ||
| case "d": { | ||
| milliseconds = numericValue * 1e3 * 60 * 60 * 24; | ||
| break; | ||
| } | ||
| /* v8 ignore next -- @preserve */ | ||
| default: { | ||
| milliseconds = Number(shorthand); | ||
| } | ||
| } | ||
| } else { | ||
| milliseconds = Number(shorthand); | ||
| } | ||
| } | ||
| return milliseconds; | ||
| }; | ||
| var shorthandToTime = (shorthand, fromDate) => { | ||
| fromDate ??= /* @__PURE__ */ new Date(); | ||
| const milliseconds = shorthandToMilliseconds(shorthand); | ||
| if (milliseconds === void 0) { | ||
| return fromDate.getTime(); | ||
| } | ||
| return fromDate.getTime() + milliseconds; | ||
| }; | ||
| // src/coalesce-async.ts | ||
| var callbacks = /* @__PURE__ */ new Map(); | ||
| function hasKey(key) { | ||
| return callbacks.has(key); | ||
| } | ||
| function addKey(key) { | ||
| callbacks.set(key, []); | ||
| } | ||
| function removeKey(key) { | ||
| callbacks.delete(key); | ||
| } | ||
| function addCallbackToKey(key, callback) { | ||
| const stash = getCallbacksByKey(key); | ||
| stash.push(callback); | ||
| callbacks.set(key, stash); | ||
| } | ||
| function getCallbacksByKey(key) { | ||
| return callbacks.get(key) ?? []; | ||
| } | ||
| async function enqueue(key) { | ||
| return new Promise((resolve, reject) => { | ||
| const callback = { resolve, reject }; | ||
| addCallbackToKey(key, callback); | ||
| }); | ||
| } | ||
| function dequeue(key) { | ||
| const stash = getCallbacksByKey(key); | ||
| removeKey(key); | ||
| return stash; | ||
| } | ||
| function coalesce(options) { | ||
| const { key, error, result } = options; | ||
| for (const callback of dequeue(key)) { | ||
| if (error) { | ||
| callback.reject(error); | ||
| } else { | ||
| callback.resolve(result); | ||
| } | ||
| } | ||
| } | ||
| async function coalesceAsync(key, fnc) { | ||
| if (!hasKey(key)) { | ||
| addKey(key); | ||
| try { | ||
| const result = await Promise.resolve(fnc()); | ||
| coalesce({ key, result }); | ||
| return result; | ||
| } catch (error) { | ||
| coalesce({ key, error }); | ||
| throw error; | ||
| } | ||
| } | ||
| return enqueue(key); | ||
| } | ||
| // src/hash.ts | ||
| import { Hashery } from "hashery"; | ||
| var HashAlgorithm = /* @__PURE__ */ ((HashAlgorithm2) => { | ||
| HashAlgorithm2["SHA256"] = "SHA-256"; | ||
| HashAlgorithm2["SHA384"] = "SHA-384"; | ||
| HashAlgorithm2["SHA512"] = "SHA-512"; | ||
| HashAlgorithm2["DJB2"] = "djb2"; | ||
| HashAlgorithm2["FNV1"] = "fnv1"; | ||
| HashAlgorithm2["MURMER"] = "murmer"; | ||
| HashAlgorithm2["CRC32"] = "crc32"; | ||
| return HashAlgorithm2; | ||
| })(HashAlgorithm || {}); | ||
| async function hash(object, options = { | ||
| algorithm: "SHA-256" /* SHA256 */, | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const algorithm = options?.algorithm ?? "SHA-256" /* SHA256 */; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const objectString = serialize(object); | ||
| const hashery = new Hashery(); | ||
| return hashery.toHash(objectString, { algorithm }); | ||
| } | ||
| function hashSync(object, options = { | ||
| algorithm: "djb2" /* DJB2 */, | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const algorithm = options?.algorithm ?? "djb2" /* DJB2 */; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const objectString = serialize(object); | ||
| const hashery = new Hashery(); | ||
| return hashery.toHashSync(objectString, { algorithm }); | ||
| } | ||
| async function hashToNumber(object, options = { | ||
| min: 0, | ||
| max: 10, | ||
| algorithm: "SHA-256" /* SHA256 */, | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const min = options?.min ?? 0; | ||
| const max = options?.max ?? 10; | ||
| const algorithm = options?.algorithm ?? "SHA-256" /* SHA256 */; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const hashLength = options?.hashLength ?? 16; | ||
| if (min >= max) { | ||
| throw new Error( | ||
| `Invalid range: min (${min}) must be less than max (${max})` | ||
| ); | ||
| } | ||
| const objectString = serialize(object); | ||
| const hashery = new Hashery(); | ||
| return hashery.toNumber(objectString, { | ||
| algorithm, | ||
| min, | ||
| max, | ||
| hashLength | ||
| }); | ||
| } | ||
| function hashToNumberSync(object, options = { | ||
| min: 0, | ||
| max: 10, | ||
| algorithm: "djb2" /* DJB2 */, | ||
| serialize: JSON.stringify | ||
| }) { | ||
| const min = options?.min ?? 0; | ||
| const max = options?.max ?? 10; | ||
| const algorithm = options?.algorithm ?? "djb2" /* DJB2 */; | ||
| const serialize = options?.serialize ?? JSON.stringify; | ||
| const hashLength = options?.hashLength ?? 16; | ||
| if (min >= max) { | ||
| throw new Error( | ||
| `Invalid range: min (${min}) must be less than max (${max})` | ||
| ); | ||
| } | ||
| const objectString = serialize(object); | ||
| const hashery = new Hashery(); | ||
| return hashery.toNumberSync(objectString, { | ||
| algorithm, | ||
| min, | ||
| max, | ||
| hashLength | ||
| }); | ||
| } | ||
| // src/is-keyv-instance.ts | ||
| import { Keyv } from "keyv"; | ||
| function isKeyvInstance(keyv) { | ||
| if (keyv === null || keyv === void 0) { | ||
| return false; | ||
| } | ||
| if (keyv instanceof Keyv) { | ||
| return true; | ||
| } | ||
| const keyvMethods = [ | ||
| "generateIterator", | ||
| "get", | ||
| "getMany", | ||
| "set", | ||
| "setMany", | ||
| "delete", | ||
| "deleteMany", | ||
| "has", | ||
| "hasMany", | ||
| "clear", | ||
| "disconnect", | ||
| "serialize", | ||
| "deserialize" | ||
| ]; | ||
| return keyvMethods.every((method) => typeof keyv[method] === "function"); | ||
| } | ||
| // src/is-object.ts | ||
| function isObject(value) { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| // src/less-than.ts | ||
| function lessThan(number1, number2) { | ||
| return typeof number1 === "number" && typeof number2 === "number" ? number1 < number2 : false; | ||
| } | ||
| // src/memoize.ts | ||
| function wrapSync(function_, options) { | ||
| const { ttl, keyPrefix, cache, serialize } = options; | ||
| return (...arguments_) => { | ||
| let cacheKey = createWrapKey(function_, arguments_, { | ||
| keyPrefix, | ||
| serialize | ||
| }); | ||
| if (options.createKey) { | ||
| cacheKey = options.createKey(function_, arguments_, options); | ||
| } | ||
| let value = cache.get(cacheKey); | ||
| if (value === void 0) { | ||
| try { | ||
| value = function_(...arguments_); | ||
| cache.set(cacheKey, value, ttl); | ||
| } catch (error) { | ||
| cache.emit("error", error); | ||
| if (options.cacheErrors) { | ||
| cache.set(cacheKey, error, ttl); | ||
| } | ||
| } | ||
| } | ||
| return value; | ||
| }; | ||
| } | ||
| async function getOrSet(key, function_, options) { | ||
| const keyString = typeof key === "function" ? key(options) : key; | ||
| let value; | ||
| try { | ||
| value = await options.cache.get(keyString); | ||
| } catch (error) { | ||
| options.cache.emit("error", error); | ||
| if (options.throwErrors === true || options.throwErrors === "store") { | ||
| throw error; | ||
| } | ||
| } | ||
| if (value === void 0) { | ||
| const cacheId = options.cacheId ?? "default"; | ||
| const coalesceKey = `${cacheId}::${keyString}`; | ||
| value = await coalesceAsync(coalesceKey, async () => { | ||
| let result; | ||
| try { | ||
| try { | ||
| result = await function_(); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope( | ||
| error, | ||
| "function" | ||
| ); | ||
| } | ||
| try { | ||
| await options.cache.set(keyString, result, options.ttl); | ||
| } catch (error) { | ||
| throw new ErrorEnvelope(error, "store"); | ||
| } | ||
| return result; | ||
| } catch (caught) { | ||
| const errorType = caught instanceof ErrorEnvelope ? caught.context : ( | ||
| /* c8 ignore next 1 */ | ||
| void 0 | ||
| ); | ||
| const error = caught instanceof ErrorEnvelope ? caught.error : caught; | ||
| options.cache.emit("error", error); | ||
| if (options.cacheErrors) { | ||
| await options.cache.set(keyString, error, options.ttl); | ||
| } | ||
| if (options.throwErrors === true || options.throwErrors === errorType) { | ||
| throw error; | ||
| } | ||
| } | ||
| return result; | ||
| }); | ||
| } | ||
| return value; | ||
| } | ||
| function wrap(function_, options) { | ||
| const { keyPrefix, serialize } = options; | ||
| return async (...arguments_) => { | ||
| let cacheKey = createWrapKey(function_, arguments_, { | ||
| keyPrefix, | ||
| serialize | ||
| }); | ||
| if (options.createKey) { | ||
| cacheKey = options.createKey(function_, arguments_, options); | ||
| } | ||
| return getOrSet( | ||
| cacheKey, | ||
| async () => function_(...arguments_), | ||
| options | ||
| ); | ||
| }; | ||
| } | ||
| function createWrapKey(function_, arguments_, options) { | ||
| const { keyPrefix, serialize } = options || {}; | ||
| if (!keyPrefix) { | ||
| return `${function_.name}::${hashSync(arguments_, { serialize })}`; | ||
| } | ||
| return `${keyPrefix}::${function_.name}::${hashSync(arguments_, { serialize })}`; | ||
| } | ||
| var ErrorEnvelope = class { | ||
| constructor(error, context) { | ||
| this.error = error; | ||
| this.context = context; | ||
| } | ||
| }; | ||
| // src/run-if-fn.ts | ||
| function runIfFn(valueOrFunction, ...arguments_) { | ||
| return typeof valueOrFunction === "function" ? valueOrFunction(...arguments_) : valueOrFunction; | ||
| } | ||
| // src/sleep.ts | ||
| var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | ||
| // src/stats.ts | ||
| var Stats = class { | ||
| _hits = 0; | ||
| _misses = 0; | ||
| _gets = 0; | ||
| _sets = 0; | ||
| _deletes = 0; | ||
| _clears = 0; | ||
| _vsize = 0; | ||
| _ksize = 0; | ||
| _count = 0; | ||
| _enabled = false; | ||
| constructor(options) { | ||
| if (options?.enabled) { | ||
| this._enabled = options.enabled; | ||
| } | ||
| } | ||
| /** | ||
| * @returns {boolean} - Whether the stats are enabled | ||
| */ | ||
| get enabled() { | ||
| return this._enabled; | ||
| } | ||
| /** | ||
| * @param {boolean} enabled - Whether to enable the stats | ||
| */ | ||
| set enabled(enabled) { | ||
| this._enabled = enabled; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of hits | ||
| * @readonly | ||
| */ | ||
| get hits() { | ||
| return this._hits; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of misses | ||
| * @readonly | ||
| */ | ||
| get misses() { | ||
| return this._misses; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of gets | ||
| * @readonly | ||
| */ | ||
| get gets() { | ||
| return this._gets; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of sets | ||
| * @readonly | ||
| */ | ||
| get sets() { | ||
| return this._sets; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of deletes | ||
| * @readonly | ||
| */ | ||
| get deletes() { | ||
| return this._deletes; | ||
| } | ||
| /** | ||
| * @returns {number} - The number of clears | ||
| * @readonly | ||
| */ | ||
| get clears() { | ||
| return this._clears; | ||
| } | ||
| /** | ||
| * @returns {number} - The vsize (value size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get vsize() { | ||
| return this._vsize; | ||
| } | ||
| /** | ||
| * @returns {number} - The ksize (key size) of the cache instance | ||
| * @readonly | ||
| */ | ||
| get ksize() { | ||
| return this._ksize; | ||
| } | ||
| /** | ||
| * @returns {number} - The count of the cache instance | ||
| * @readonly | ||
| */ | ||
| get count() { | ||
| return this._count; | ||
| } | ||
| incrementHits() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._hits++; | ||
| } | ||
| incrementMisses() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._misses++; | ||
| } | ||
| incrementGets() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._gets++; | ||
| } | ||
| incrementSets() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._sets++; | ||
| } | ||
| incrementDeletes() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._deletes++; | ||
| } | ||
| incrementClears() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._clears++; | ||
| } | ||
| incrementVSize(value) { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._vsize += this.roughSizeOfObject(value); | ||
| } | ||
| decreaseVSize(value) { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._vsize -= this.roughSizeOfObject(value); | ||
| } | ||
| incrementKSize(key) { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._ksize += this.roughSizeOfString(key); | ||
| } | ||
| decreaseKSize(key) { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._ksize -= this.roughSizeOfString(key); | ||
| } | ||
| incrementCount() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._count++; | ||
| } | ||
| decreaseCount() { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._count--; | ||
| } | ||
| setCount(count) { | ||
| if (!this._enabled) { | ||
| return; | ||
| } | ||
| this._count = count; | ||
| } | ||
| roughSizeOfString(value) { | ||
| return value.length * 2; | ||
| } | ||
| roughSizeOfObject(object) { | ||
| const objectList = []; | ||
| const stack = [object]; | ||
| let bytes = 0; | ||
| while (stack.length > 0) { | ||
| const value = stack.pop(); | ||
| if (typeof value === "boolean") { | ||
| bytes += 4; | ||
| } else if (typeof value === "string") { | ||
| bytes += value.length * 2; | ||
| } else if (typeof value === "number") { | ||
| bytes += 8; | ||
| } else { | ||
| if (value === null || value === void 0) { | ||
| bytes += 4; | ||
| continue; | ||
| } | ||
| if (objectList.includes(value)) { | ||
| continue; | ||
| } | ||
| objectList.push(value); | ||
| for (const key in value) { | ||
| bytes += key.length * 2; | ||
| stack.push(value[key]); | ||
| } | ||
| } | ||
| } | ||
| return bytes; | ||
| } | ||
| reset() { | ||
| this._hits = 0; | ||
| this._misses = 0; | ||
| this._gets = 0; | ||
| this._sets = 0; | ||
| this._deletes = 0; | ||
| this._clears = 0; | ||
| this._vsize = 0; | ||
| this._ksize = 0; | ||
| this._count = 0; | ||
| } | ||
| resetStoreValues() { | ||
| this._vsize = 0; | ||
| this._ksize = 0; | ||
| this._count = 0; | ||
| } | ||
| }; | ||
| // src/ttl.ts | ||
| function getTtlFromExpires(expires) { | ||
| if (expires === void 0 || expires === null) { | ||
| return void 0; | ||
| } | ||
| const now = Date.now(); | ||
| if (expires < now) { | ||
| return void 0; | ||
| } | ||
| return expires - now; | ||
| } | ||
| function getCascadingTtl(cacheableTtl, primaryTtl, secondaryTtl) { | ||
| return secondaryTtl ?? primaryTtl ?? shorthandToMilliseconds(cacheableTtl); | ||
| } | ||
| function calculateTtlFromExpiration(ttl, expires) { | ||
| const ttlFromExpires = getTtlFromExpires(expires); | ||
| const expiresFromTtl = ttl ? Date.now() + ttl : void 0; | ||
| if (ttlFromExpires === void 0) { | ||
| return ttl; | ||
| } | ||
| if (expiresFromTtl === void 0) { | ||
| return ttlFromExpires; | ||
| } | ||
| if (expires && expires > expiresFromTtl) { | ||
| return ttl; | ||
| } | ||
| return ttlFromExpires; | ||
| } | ||
| export { | ||
| HashAlgorithm, | ||
| Stats, | ||
| calculateTtlFromExpiration, | ||
| coalesceAsync, | ||
| createWrapKey, | ||
| getCascadingTtl, | ||
| getOrSet, | ||
| getTtlFromExpires, | ||
| hash, | ||
| hashSync, | ||
| hashToNumber, | ||
| hashToNumberSync, | ||
| isKeyvInstance, | ||
| isObject, | ||
| lessThan, | ||
| runIfFn, | ||
| shorthandToMilliseconds, | ||
| shorthandToTime, | ||
| sleep, | ||
| wrap, | ||
| wrapSync | ||
| }; | ||
| /* v8 ignore next -- @preserve */ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
208385
167.52%2736
72.95%855
64.11%1
Infinity%