Buckets:
dieumercimvemba/mediastore / scripts /whatsapp-bridge /node_modules /@cacheable /utils /dist /index.mjs
| 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 }; | |
Xet Storage Details
- Size:
- 45.9 kB
- Xet hash:
- 2058d44de72b33093cdb5b1bf0590867c72561773cb39fae1753c9d21ec7dc0b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.