avancement planning
This commit is contained in:
+108
-30
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* @module LRUCache
|
||||
*/
|
||||
export type Perf = {
|
||||
now: () => number;
|
||||
};
|
||||
import type { Perf } from './perf.js';
|
||||
export type { Perf } from './perf.js';
|
||||
declare const TYPE: unique symbol;
|
||||
export type PosInt = number & {
|
||||
[TYPE]: 'Positive Integer';
|
||||
@@ -119,8 +118,16 @@ export declare namespace LRUCache {
|
||||
*
|
||||
* The `status` option should be a plain JavaScript object. The following
|
||||
* fields will be set on it appropriately, depending on the situation.
|
||||
*
|
||||
* These objects are also the context objects passed to listeners on the
|
||||
* `lru-cache:metrics` diagnostic channel, and the `lru-cache` tracing
|
||||
* channels, in platforms that support them.
|
||||
*/
|
||||
interface Status<V> {
|
||||
interface Status<K, V, FC = unknown> {
|
||||
/**
|
||||
* The operation being performed
|
||||
*/
|
||||
op?: 'get' | 'set' | 'memo' | 'fetch' | 'delete' | 'has' | 'peek';
|
||||
/**
|
||||
* The status of a set() operation.
|
||||
*
|
||||
@@ -129,7 +136,37 @@ export declare namespace LRUCache {
|
||||
* - replace: the item was in the cache, and replaced
|
||||
* - miss: the item was not added to the cache for some reason
|
||||
*/
|
||||
set?: 'add' | 'update' | 'replace' | 'miss';
|
||||
set?: 'add' | 'update' | 'replace' | 'miss' | 'deleted';
|
||||
/**
|
||||
* The status of a delete() operation.
|
||||
*/
|
||||
delete?: LRUCache.DisposeReason;
|
||||
/**
|
||||
* The result of a peek() operation
|
||||
*
|
||||
* - hit: the item was found and returned
|
||||
* - stale: the item is in the cache, but past its ttl and not returned
|
||||
* - miss: item not in the cache
|
||||
*/
|
||||
peek?: 'hit' | 'miss' | 'stale';
|
||||
/**
|
||||
* The status of a memo() operation.
|
||||
*
|
||||
* - 'hit': the item was found in the cache and returned
|
||||
* - 'miss': the `memoMethod` function was called
|
||||
*/
|
||||
memo?: 'hit' | 'miss';
|
||||
/**
|
||||
* The `context` option provided to a memo or fetch operation
|
||||
*
|
||||
* In practice, of course, this will be the same type as the `FC`
|
||||
* fetch context param used to instantiate the LRUCache, but the
|
||||
* convolutions of threading that through would get quite complicated,
|
||||
* and preclude forcing/forbidding the passing of a `context` param
|
||||
* where it is/isn't expected, which is more valuable for error
|
||||
* prevention.
|
||||
*/
|
||||
context?: unknown;
|
||||
/**
|
||||
* the ttl stored for the item, or undefined if ttls are not used.
|
||||
*/
|
||||
@@ -160,8 +197,15 @@ export declare namespace LRUCache {
|
||||
*/
|
||||
maxEntrySizeExceeded?: true;
|
||||
/**
|
||||
* The old value, specified in the case of `set:'update'` or
|
||||
* `set:'replace'`
|
||||
* The key that was set or retrieved
|
||||
*/
|
||||
key?: K;
|
||||
/**
|
||||
* The value that was set
|
||||
*/
|
||||
value?: V;
|
||||
/**
|
||||
* The old value, specified in the case of `set:'replace'`
|
||||
*/
|
||||
oldValue?: V;
|
||||
/**
|
||||
@@ -187,6 +231,10 @@ export declare namespace LRUCache {
|
||||
* {@link FetchOptions.forceRefresh} was specified.
|
||||
*/
|
||||
fetch?: 'get' | 'inflight' | 'miss' | 'hit' | 'stale' | 'refresh';
|
||||
/**
|
||||
* `forceRefresh` option was used for either a fetch or memo operation
|
||||
*/
|
||||
forceRefresh?: boolean;
|
||||
/**
|
||||
* The {@link OptionsBase.fetchMethod} was called
|
||||
*/
|
||||
@@ -208,7 +256,7 @@ export declare namespace LRUCache {
|
||||
fetchAborted?: true;
|
||||
/**
|
||||
* The abort signal received was ignored, and the fetch was allowed to
|
||||
* continue.
|
||||
* continue in the background.
|
||||
*/
|
||||
fetchAbortIgnored?: true;
|
||||
/**
|
||||
@@ -224,15 +272,27 @@ export declare namespace LRUCache {
|
||||
*
|
||||
* - fetching: The item is currently being fetched. If a previous value
|
||||
* is present and allowed, that will be returned.
|
||||
* - stale: The item is in the cache, and is stale.
|
||||
* - stale: The item is in the cache, and is stale. If it was returned,
|
||||
* then the `returnedStale` flag will be set.
|
||||
* - stale-fetching: The value is being fetched in the background, but is
|
||||
* currently stale. If the stale value was returned, then the
|
||||
* `returnedStale` flag will be set.
|
||||
* - hit: the item is in the cache
|
||||
* - miss: the item is not in the cache
|
||||
*/
|
||||
get?: 'stale' | 'hit' | 'miss';
|
||||
get?: 'stale' | 'hit' | 'miss' | 'fetching' | 'stale-fetching';
|
||||
/**
|
||||
* A fetch or get operation returned a stale value.
|
||||
*/
|
||||
returnedStale?: true;
|
||||
/**
|
||||
* A tracingChannel trace was started for this operation
|
||||
*/
|
||||
trace?: boolean;
|
||||
/**
|
||||
* A reference to the cache instance associated with this operation
|
||||
*/
|
||||
cache?: LRUCache<K & {}, V & {}, FC>;
|
||||
}
|
||||
/**
|
||||
* options which override the options set in the LRUCache constructor
|
||||
@@ -250,7 +310,7 @@ export declare namespace LRUCache {
|
||||
* the fetchMethod is called.
|
||||
*/
|
||||
interface FetcherFetchOptions<K, V, FC = unknown> extends Pick<OptionsBase<K, V, FC>, 'allowStale' | 'updateAgeOnGet' | 'noDeleteOnStaleGet' | 'sizeCalculation' | 'ttl' | 'noDisposeOnSet' | 'noUpdateTTL' | 'noDeleteOnFetchRejection' | 'allowStaleOnFetchRejection' | 'ignoreFetchAbort' | 'allowStaleOnFetchAbort'> {
|
||||
status?: Status<V>;
|
||||
status?: Status<K, V, FC>;
|
||||
size?: Size;
|
||||
}
|
||||
/**
|
||||
@@ -272,7 +332,7 @@ export declare namespace LRUCache {
|
||||
*/
|
||||
context?: FC;
|
||||
signal?: AbortSignal;
|
||||
status?: Status<V>;
|
||||
status?: Status<K, V, FC>;
|
||||
}
|
||||
/**
|
||||
* Options provided to {@link LRUCache#fetch} when the FC type is something
|
||||
@@ -285,7 +345,7 @@ export declare namespace LRUCache {
|
||||
* Options provided to {@link LRUCache#fetch} when the FC type is
|
||||
* `undefined` or `void`
|
||||
*/
|
||||
interface FetchOptionsNoContext<K, V> extends FetchOptions<K, V, undefined> {
|
||||
interface FetchOptionsNoContext<K, V, FC extends undefined | void = undefined> extends FetchOptions<K, V, FC> {
|
||||
context?: undefined;
|
||||
}
|
||||
interface MemoOptions<K, V, FC = unknown> extends Pick<OptionsBase<K, V, FC>, 'allowStale' | 'updateAgeOnGet' | 'noDeleteOnStaleGet' | 'sizeCalculation' | 'ttl' | 'noDisposeOnSet' | 'noUpdateTTL' | 'noDeleteOnFetchRejection' | 'allowStaleOnFetchRejection' | 'ignoreFetchAbort' | 'allowStaleOnFetchAbort'> {
|
||||
@@ -303,7 +363,7 @@ export declare namespace LRUCache {
|
||||
* be required.
|
||||
*/
|
||||
context?: FC;
|
||||
status?: Status<V>;
|
||||
status?: Status<K, V, FC>;
|
||||
}
|
||||
/**
|
||||
* Options provided to {@link LRUCache#memo} when the FC type is something
|
||||
@@ -316,7 +376,7 @@ export declare namespace LRUCache {
|
||||
* Options provided to {@link LRUCache#memo} when the FC type is
|
||||
* `undefined` or `void`
|
||||
*/
|
||||
interface MemoOptionsNoContext<K, V> extends MemoOptions<K, V, undefined> {
|
||||
interface MemoOptionsNoContext<K, V, FC extends undefined | void = undefined> extends MemoOptions<K, V, FC> {
|
||||
context?: undefined;
|
||||
}
|
||||
/**
|
||||
@@ -345,7 +405,7 @@ export declare namespace LRUCache {
|
||||
* the memoMethod is called.
|
||||
*/
|
||||
interface MemoizerMemoOptions<K, V, FC = unknown> extends Pick<OptionsBase<K, V, FC>, 'allowStale' | 'updateAgeOnGet' | 'noDeleteOnStaleGet' | 'sizeCalculation' | 'ttl' | 'noDisposeOnSet' | 'noUpdateTTL'> {
|
||||
status?: Status<V>;
|
||||
status?: Status<K, V, FC>;
|
||||
size?: Size;
|
||||
start?: Milliseconds;
|
||||
}
|
||||
@@ -353,18 +413,19 @@ export declare namespace LRUCache {
|
||||
* Options that may be passed to the {@link LRUCache#has} method.
|
||||
*/
|
||||
interface HasOptions<K, V, FC> extends Pick<OptionsBase<K, V, FC>, 'updateAgeOnHas'> {
|
||||
status?: Status<V>;
|
||||
status?: Status<K, V, FC>;
|
||||
}
|
||||
/**
|
||||
* Options that may be passed to the {@link LRUCache#get} method.
|
||||
*/
|
||||
interface GetOptions<K, V, FC> extends Pick<OptionsBase<K, V, FC>, 'allowStale' | 'updateAgeOnGet' | 'noDeleteOnStaleGet'> {
|
||||
status?: Status<V>;
|
||||
status?: Status<K, V, FC>;
|
||||
}
|
||||
/**
|
||||
* Options that may be passed to the {@link LRUCache#peek} method.
|
||||
*/
|
||||
interface PeekOptions<K, V, FC> extends Pick<OptionsBase<K, V, FC>, 'allowStale'> {
|
||||
status?: Status<K, V, FC>;
|
||||
}
|
||||
/**
|
||||
* Options that may be passed to the {@link LRUCache#set} method.
|
||||
@@ -385,7 +446,7 @@ export declare namespace LRUCache {
|
||||
* method is in use.
|
||||
*/
|
||||
start?: Milliseconds;
|
||||
status?: Status<V>;
|
||||
status?: Status<K, V, FC>;
|
||||
}
|
||||
/**
|
||||
* The type signature for the {@link OptionsBase.fetchMethod} option.
|
||||
@@ -638,6 +699,20 @@ export declare namespace LRUCache {
|
||||
* though for most cases, only minimally.
|
||||
*/
|
||||
maxSize?: Size;
|
||||
/**
|
||||
* The effective size for background fetch promises.
|
||||
*
|
||||
* This has no effect unless `maxSize` and `sizeCalculation` are used,
|
||||
* and a {@link LRUCache.OptionsBase.fetchMethod} is provided to
|
||||
* support {@link LRUCache#fetch}.
|
||||
*
|
||||
* If a stale value is present in the cache, then the effective size of
|
||||
* the background fetch is the size of the stale item it will eventually
|
||||
* replace. If not, then this value is used as its effective size.
|
||||
*
|
||||
* @default 1
|
||||
*/
|
||||
backgroundFetchSize?: number;
|
||||
/**
|
||||
* The maximum allowed size for any single item in the cache.
|
||||
*
|
||||
@@ -940,6 +1015,8 @@ export declare class LRUCache<K extends {}, V extends {}, FC = unknown> {
|
||||
* {@link LRUCache.OptionsBase.ignoreFetchAbort}
|
||||
*/
|
||||
ignoreFetchAbort: boolean;
|
||||
/** {@link LRUCache.OptionsBase.backgroundFetchSize} */
|
||||
backgroundFetchSize: number;
|
||||
/**
|
||||
* Do not call this method unless you need to inspect the
|
||||
* inner workings of the cache. If anything returned by this
|
||||
@@ -952,6 +1029,7 @@ export declare class LRUCache<K extends {}, V extends {}, FC = unknown> {
|
||||
static unsafeExposeInternals<K extends {}, V extends {}, FC extends unknown = unknown>(c: LRUCache<K, V, FC>): {
|
||||
starts: ZeroArray | undefined;
|
||||
ttls: ZeroArray | undefined;
|
||||
autopurgeTimers: (NodeJS.Timeout | undefined)[] | undefined;
|
||||
sizes: ZeroArray | undefined;
|
||||
keyMap: Map<K, number>;
|
||||
keyList: (K | undefined)[];
|
||||
@@ -961,8 +1039,8 @@ export declare class LRUCache<K extends {}, V extends {}, FC = unknown> {
|
||||
readonly head: Index;
|
||||
readonly tail: Index;
|
||||
free: StackLike;
|
||||
isBackgroundFetch: (p: any) => p is BackgroundFetch<V>;
|
||||
backgroundFetch: (k: K, index: number | undefined, options: LRUCache.FetchOptions<K, V, FC>, context: any) => BackgroundFetch<V>;
|
||||
isBackgroundFetch: (p: unknown) => p is BackgroundFetch<V>;
|
||||
backgroundFetch: (k: K, index: number | undefined, options: LRUCache.FetchOptions<K, V, FC>, context: unknown) => BackgroundFetch<V>;
|
||||
moveToTail: (index: number) => void;
|
||||
indexes: (options?: {
|
||||
allowStale: boolean;
|
||||
@@ -1074,12 +1152,12 @@ export declare class LRUCache<K extends {}, V extends {}, FC = unknown> {
|
||||
*
|
||||
* Does not update age or recenty of use, or iterate over stale values.
|
||||
*/
|
||||
forEach(fn: (v: V, k: K, self: LRUCache<K, V, FC>) => any, thisp?: any): void;
|
||||
forEach(fn: (v: V, k: K, self: LRUCache<K, V, FC>) => unknown, thisp?: unknown): void;
|
||||
/**
|
||||
* The same as {@link LRUCache.forEach} but items are iterated over in
|
||||
* reverse order. (ie, less recently used items are iterated over first.)
|
||||
*/
|
||||
rforEach(fn: (v: V, k: K, self: LRUCache<K, V, FC>) => any, thisp?: any): void;
|
||||
rforEach(fn: (v: V, k: K, self: LRUCache<K, V, FC>) => unknown, thisp?: unknown): void;
|
||||
/**
|
||||
* Delete any stale entries. Returns true if anything was removed,
|
||||
* false otherwise.
|
||||
@@ -1152,7 +1230,7 @@ export declare class LRUCache<K extends {}, V extends {}, FC = unknown> {
|
||||
* If the value is `undefined`, then this is an alias for
|
||||
* `cache.delete(key)`. `undefined` is never stored in the cache.
|
||||
*/
|
||||
set(k: K, v: V | BackgroundFetch<V> | undefined, setOptions?: LRUCache.SetOptions<K, V, FC>): this;
|
||||
set(k: K, v: V | undefined, setOptions?: LRUCache.SetOptions<K, V, FC>): this;
|
||||
/**
|
||||
* Evict the least recently used item, returning its value or
|
||||
* `undefined` if cache is empty.
|
||||
@@ -1268,8 +1346,8 @@ export declare class LRUCache<K extends {}, V extends {}, FC = unknown> {
|
||||
* the same time, because they're both waiting on the same
|
||||
* underlying fetchMethod response.
|
||||
*/
|
||||
fetch(k: K, fetchOptions: unknown extends FC ? LRUCache.FetchOptions<K, V, FC> : FC extends undefined | void ? LRUCache.FetchOptionsNoContext<K, V> : LRUCache.FetchOptionsWithContext<K, V, FC>): Promise<undefined | V>;
|
||||
fetch(k: unknown extends FC ? K : FC extends undefined | void ? K : never, fetchOptions?: unknown extends FC ? LRUCache.FetchOptions<K, V, FC> : FC extends undefined | void ? LRUCache.FetchOptionsNoContext<K, V> : never): Promise<undefined | V>;
|
||||
fetch(k: K, fetchOptions: unknown extends FC ? LRUCache.FetchOptions<K, V, FC> : FC extends undefined | void ? LRUCache.FetchOptionsNoContext<K, V, FC> : LRUCache.FetchOptionsWithContext<K, V, FC>): Promise<undefined | V>;
|
||||
fetch(k: unknown extends FC ? K : FC extends undefined | void ? K : never, fetchOptions?: unknown extends FC ? LRUCache.FetchOptions<K, V, FC> : FC extends undefined | void ? LRUCache.FetchOptionsNoContext<K, V, FC> : never): Promise<undefined | V>;
|
||||
/**
|
||||
* In some cases, `cache.fetch()` may resolve to `undefined`, either because
|
||||
* a {@link LRUCache.OptionsBase#fetchMethod} was not provided (turning
|
||||
@@ -1283,8 +1361,8 @@ export declare class LRUCache<K extends {}, V extends {}, FC = unknown> {
|
||||
* cumbersome, but testing for `undefined` can also be annoying, this method
|
||||
* can be used, which will reject if `this.fetch()` resolves to undefined.
|
||||
*/
|
||||
forceFetch(k: K, fetchOptions: unknown extends FC ? LRUCache.FetchOptions<K, V, FC> : FC extends undefined | void ? LRUCache.FetchOptionsNoContext<K, V> : LRUCache.FetchOptionsWithContext<K, V, FC>): Promise<V>;
|
||||
forceFetch(k: unknown extends FC ? K : FC extends undefined | void ? K : never, fetchOptions?: unknown extends FC ? LRUCache.FetchOptions<K, V, FC> : FC extends undefined | void ? LRUCache.FetchOptionsNoContext<K, V> : never): Promise<V>;
|
||||
forceFetch(k: K, fetchOptions: unknown extends FC ? LRUCache.FetchOptions<K, V, FC> : FC extends undefined | void ? LRUCache.FetchOptionsNoContext<K, V, FC> : LRUCache.FetchOptionsWithContext<K, V, FC>): Promise<V>;
|
||||
forceFetch(k: unknown extends FC ? K : FC extends undefined | void ? K : never, fetchOptions?: unknown extends FC ? LRUCache.FetchOptions<K, V, FC> : FC extends undefined | void ? LRUCache.FetchOptionsNoContext<K, V, FC> : never): Promise<V>;
|
||||
/**
|
||||
* If the key is found in the cache, then this is equivalent to
|
||||
* {@link LRUCache#get}. If not, in the cache, then calculate the value using
|
||||
@@ -1299,8 +1377,8 @@ export declare class LRUCache<K extends {}, V extends {}, FC = unknown> {
|
||||
* relevant in the course of fetching the data. It is only relevant for the
|
||||
* course of a single `memo()` operation, and discarded afterwards.
|
||||
*/
|
||||
memo(k: K, memoOptions: unknown extends FC ? LRUCache.MemoOptions<K, V, FC> : FC extends undefined | void ? LRUCache.MemoOptionsNoContext<K, V> : LRUCache.MemoOptionsWithContext<K, V, FC>): V;
|
||||
memo(k: unknown extends FC ? K : FC extends undefined | void ? K : never, memoOptions?: unknown extends FC ? LRUCache.MemoOptions<K, V, FC> : FC extends undefined | void ? LRUCache.MemoOptionsNoContext<K, V> : never): V;
|
||||
memo(k: K, memoOptions: unknown extends FC ? LRUCache.MemoOptions<K, V, FC> : FC extends undefined | void ? LRUCache.MemoOptionsNoContext<K, V, FC> : LRUCache.MemoOptionsWithContext<K, V, FC>): V;
|
||||
memo(k: unknown extends FC ? K : FC extends undefined | void ? K : never, memoOptions?: unknown extends FC ? LRUCache.MemoOptions<K, V, FC> : FC extends undefined | void ? LRUCache.MemoOptionsNoContext<K, V, FC> : never): V;
|
||||
/**
|
||||
* Return a value from the cache. Will update the recency of the cache
|
||||
* entry found.
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+366
-208
@@ -1,75 +1,27 @@
|
||||
/**
|
||||
* @module LRUCache
|
||||
*/
|
||||
const defaultPerf = (typeof performance === 'object' &&
|
||||
performance &&
|
||||
typeof performance.now === 'function') ?
|
||||
performance
|
||||
: Date;
|
||||
import { metrics, tracing } from './diagnostics-channel.js';
|
||||
import { defaultPerf } from './perf.js';
|
||||
const hasSubscribers = () => metrics.hasSubscribers || tracing.hasSubscribers;
|
||||
const warned = new Set();
|
||||
/* c8 ignore start */
|
||||
const PROCESS = (typeof process === 'object' && !!process ?
|
||||
process
|
||||
: {});
|
||||
/* c8 ignore start */
|
||||
const emitWarning = (msg, type, code, fn) => {
|
||||
typeof PROCESS.emitWarning === 'function' ?
|
||||
PROCESS.emitWarning(msg, type, code, fn)
|
||||
: console.error(`[${code}] ${type}: ${msg}`);
|
||||
};
|
||||
let AC = globalThis.AbortController;
|
||||
let AS = globalThis.AbortSignal;
|
||||
/* c8 ignore start */
|
||||
if (typeof AC === 'undefined') {
|
||||
//@ts-ignore
|
||||
AS = class AbortSignal {
|
||||
onabort;
|
||||
_onabort = [];
|
||||
reason;
|
||||
aborted = false;
|
||||
addEventListener(_, fn) {
|
||||
this._onabort.push(fn);
|
||||
}
|
||||
};
|
||||
//@ts-ignore
|
||||
AC = class AbortController {
|
||||
constructor() {
|
||||
warnACPolyfill();
|
||||
}
|
||||
signal = new AS();
|
||||
abort(reason) {
|
||||
if (this.signal.aborted)
|
||||
return;
|
||||
//@ts-ignore
|
||||
this.signal.reason = reason;
|
||||
//@ts-ignore
|
||||
this.signal.aborted = true;
|
||||
//@ts-ignore
|
||||
for (const fn of this.signal._onabort) {
|
||||
fn(reason);
|
||||
}
|
||||
this.signal.onabort?.(reason);
|
||||
}
|
||||
};
|
||||
let printACPolyfillWarning = PROCESS.env?.LRU_CACHE_IGNORE_AC_WARNING !== '1';
|
||||
const warnACPolyfill = () => {
|
||||
if (!printACPolyfillWarning)
|
||||
return;
|
||||
printACPolyfillWarning = false;
|
||||
emitWarning('AbortController is not defined. If using lru-cache in ' +
|
||||
'node 14, load an AbortController polyfill from the ' +
|
||||
'`node-abort-controller` package. A minimal polyfill is ' +
|
||||
'provided for use by LRUCache.fetch(), but it should not be ' +
|
||||
'relied upon in other contexts (eg, passing it to other APIs that ' +
|
||||
'use AbortController/AbortSignal might have undesirable effects). ' +
|
||||
'You may disable this with LRU_CACHE_IGNORE_AC_WARNING=1 in the env.', 'NO_ABORT_CONTROLLER', 'ENOTSUP', warnACPolyfill);
|
||||
};
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
const emitWarning = (msg, type, code, fn) => {
|
||||
if (typeof PROCESS.emitWarning === 'function') {
|
||||
PROCESS.emitWarning(msg, type, code, fn);
|
||||
}
|
||||
else {
|
||||
//oxlint-disable-next-line no-console
|
||||
console.error(`[${code}] ${type}: ${msg}`);
|
||||
}
|
||||
};
|
||||
const shouldWarn = (code) => !warned.has(code);
|
||||
const TYPE = Symbol('type');
|
||||
const isPosInt = (n) => n && n === Math.floor(n) && n > 0 && isFinite(n);
|
||||
/* c8 ignore start */
|
||||
const isPosInt = (n) => !!n && n === Math.floor(n) && n > 0 && isFinite(n);
|
||||
// This is a little bit ridiculous, tbh.
|
||||
// The maximum array length is 2^32-1 or thereabouts on most JS impls.
|
||||
// And well before that point, you're caching the entire world, I mean,
|
||||
@@ -78,6 +30,7 @@ const isPosInt = (n) => n && n === Math.floor(n) && n > 0 && isFinite(n);
|
||||
// zeroes at init time is brutal when you get that big.
|
||||
// But why not be complete?
|
||||
// Maybe in the future, these limits will have expanded.
|
||||
/* c8 ignore start */
|
||||
const getUintArray = (max) => !isPosInt(max) ? null
|
||||
: max <= Math.pow(2, 8) ? Uint8Array
|
||||
: max <= Math.pow(2, 16) ? Uint16Array
|
||||
@@ -92,7 +45,9 @@ class ZeroArray extends Array {
|
||||
}
|
||||
}
|
||||
class Stack {
|
||||
/* c8 ignore start - not sure why this is showing up uncovered?? */
|
||||
heap;
|
||||
/* c8 ignore stop */
|
||||
length;
|
||||
// private constructor
|
||||
static #constructing = false;
|
||||
@@ -212,6 +167,8 @@ export class LRUCache {
|
||||
* {@link LRUCache.OptionsBase.ignoreFetchAbort}
|
||||
*/
|
||||
ignoreFetchAbort;
|
||||
/** {@link LRUCache.OptionsBase.backgroundFetchSize} */
|
||||
backgroundFetchSize;
|
||||
// computed properties
|
||||
#size;
|
||||
#calculatedSize;
|
||||
@@ -227,6 +184,7 @@ export class LRUCache {
|
||||
#sizes;
|
||||
#starts;
|
||||
#ttls;
|
||||
#autopurgeTimers;
|
||||
#hasDispose;
|
||||
#hasFetchMethod;
|
||||
#hasDisposeAfter;
|
||||
@@ -245,6 +203,7 @@ export class LRUCache {
|
||||
// properties
|
||||
starts: c.#starts,
|
||||
ttls: c.#ttls,
|
||||
autopurgeTimers: c.#autopurgeTimers,
|
||||
sizes: c.#sizes,
|
||||
keyMap: c.#keyMap,
|
||||
keyList: c.#keyList,
|
||||
@@ -320,7 +279,8 @@ export class LRUCache {
|
||||
return this.#disposeAfter;
|
||||
}
|
||||
constructor(options) {
|
||||
const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, perf, } = options;
|
||||
const { max = 0, ttl, ttlResolution = 1, ttlAutopurge, updateAgeOnGet, updateAgeOnHas, allowStale, dispose, onInsert, disposeAfter, noDisposeOnSet, noUpdateTTL, maxSize = 0, maxEntrySize = 0, sizeCalculation, fetchMethod, memoMethod, noDeleteOnFetchRejection, noDeleteOnStaleGet, allowStaleOnFetchRejection, allowStaleOnFetchAbort, ignoreFetchAbort, backgroundFetchSize = 1, perf, } = options;
|
||||
this.backgroundFetchSize = backgroundFetchSize;
|
||||
if (perf !== undefined) {
|
||||
if (typeof perf?.now !== 'function') {
|
||||
throw new TypeError('perf option must have a now() method if specified');
|
||||
@@ -346,20 +306,18 @@ export class LRUCache {
|
||||
throw new TypeError('sizeCalculation set to non-function');
|
||||
}
|
||||
}
|
||||
if (memoMethod !== undefined &&
|
||||
typeof memoMethod !== 'function') {
|
||||
if (memoMethod !== undefined && typeof memoMethod !== 'function') {
|
||||
throw new TypeError('memoMethod must be a function if defined');
|
||||
}
|
||||
this.#memoMethod = memoMethod;
|
||||
if (fetchMethod !== undefined &&
|
||||
typeof fetchMethod !== 'function') {
|
||||
if (fetchMethod !== undefined && typeof fetchMethod !== 'function') {
|
||||
throw new TypeError('fetchMethod must be a function if specified');
|
||||
}
|
||||
this.#fetchMethod = fetchMethod;
|
||||
this.#hasFetchMethod = !!fetchMethod;
|
||||
this.#keyMap = new Map();
|
||||
this.#keyList = new Array(max).fill(undefined);
|
||||
this.#valList = new Array(max).fill(undefined);
|
||||
this.#keyList = Array.from({ length: max }).fill(undefined);
|
||||
this.#valList = Array.from({ length: max }).fill(undefined);
|
||||
this.#next = new UintArray(max);
|
||||
this.#prev = new UintArray(max);
|
||||
this.#head = 0;
|
||||
@@ -407,9 +365,7 @@ export class LRUCache {
|
||||
this.updateAgeOnGet = !!updateAgeOnGet;
|
||||
this.updateAgeOnHas = !!updateAgeOnHas;
|
||||
this.ttlResolution =
|
||||
isPosInt(ttlResolution) || ttlResolution === 0 ?
|
||||
ttlResolution
|
||||
: 1;
|
||||
isPosInt(ttlResolution) || ttlResolution === 0 ? ttlResolution : 1;
|
||||
this.ttlAutopurge = !!ttlAutopurge;
|
||||
this.ttl = ttl || 0;
|
||||
if (this.ttl) {
|
||||
@@ -444,33 +400,56 @@ export class LRUCache {
|
||||
const starts = new ZeroArray(this.#max);
|
||||
this.#ttls = ttls;
|
||||
this.#starts = starts;
|
||||
const purgeTimers = this.ttlAutopurge ?
|
||||
Array.from({
|
||||
length: this.#max,
|
||||
})
|
||||
: undefined;
|
||||
this.#autopurgeTimers = purgeTimers;
|
||||
this.#setItemTTL = (index, ttl, start = this.#perf.now()) => {
|
||||
starts[index] = ttl !== 0 ? start : 0;
|
||||
ttls[index] = ttl;
|
||||
if (ttl !== 0 && this.ttlAutopurge) {
|
||||
const t = setTimeout(() => {
|
||||
if (this.#isStale(index)) {
|
||||
this.#delete(this.#keyList[index], 'expire');
|
||||
}
|
||||
}, ttl + 1);
|
||||
// unref() not supported on all platforms
|
||||
/* c8 ignore start */
|
||||
if (t.unref) {
|
||||
t.unref();
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
}
|
||||
setPurgetTimer(index, ttl);
|
||||
};
|
||||
this.#updateItemAge = index => {
|
||||
starts[index] = ttls[index] !== 0 ? this.#perf.now() : 0;
|
||||
setPurgetTimer(index, ttls[index]);
|
||||
};
|
||||
// clear out the purge timer if we're setting TTL to 0, and
|
||||
// previously had a ttl purge timer running, so it doesn't
|
||||
// fire unnecessarily. Don't need to do this if we're not doing
|
||||
// autopurge.
|
||||
const setPurgetTimer = !this.ttlAutopurge ?
|
||||
() => { }
|
||||
: (index, ttl) => {
|
||||
if (purgeTimers?.[index]) {
|
||||
clearTimeout(purgeTimers[index]);
|
||||
purgeTimers[index] = undefined;
|
||||
}
|
||||
if (ttl && ttl !== 0 && purgeTimers) {
|
||||
const t = setTimeout(() => {
|
||||
if (this.#isStale(index)) {
|
||||
this.#delete(this.#keyList[index], 'expire');
|
||||
}
|
||||
}, ttl + 1);
|
||||
// unref() not supported on all platforms
|
||||
/* c8 ignore start */
|
||||
if (t.unref) {
|
||||
t.unref();
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
purgeTimers[index] = t;
|
||||
}
|
||||
};
|
||||
this.#statusTTL = (status, index) => {
|
||||
if (ttls[index]) {
|
||||
const ttl = ttls[index];
|
||||
const start = starts[index];
|
||||
/* c8 ignore next */
|
||||
if (!ttl || !start)
|
||||
/* c8 ignore start */
|
||||
if (!ttl || !start) {
|
||||
return;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
status.ttl = ttl;
|
||||
status.start = start;
|
||||
status.now = cachedNow || getNow();
|
||||
@@ -529,12 +508,15 @@ export class LRUCache {
|
||||
sizes[index] = 0;
|
||||
};
|
||||
this.#requireSize = (k, v, size, sizeCalculation) => {
|
||||
// provisionally accept background fetches.
|
||||
// actual value size will be checked when they return.
|
||||
if (this.#isBackgroundFetch(v)) {
|
||||
return 0;
|
||||
}
|
||||
if (!isPosInt(size)) {
|
||||
// provisionally accept background fetches.
|
||||
// actual value size will be checked when they return.
|
||||
if (this.#isBackgroundFetch(v)) {
|
||||
// NB: this cannot occur if v.__staleWhileFetching is set,
|
||||
// because in that case, it would take on the size of the
|
||||
// existing entry that it temporarily replaces.
|
||||
return this.backgroundFetchSize;
|
||||
}
|
||||
if (sizeCalculation) {
|
||||
if (typeof sizeCalculation !== 'function') {
|
||||
throw new TypeError('sizeCalculation must be a function');
|
||||
@@ -577,10 +559,7 @@ export class LRUCache {
|
||||
};
|
||||
*#indexes({ allowStale = this.allowStale } = {}) {
|
||||
if (this.#size) {
|
||||
for (let i = this.#tail; true;) {
|
||||
if (!this.#isValidIndex(i)) {
|
||||
break;
|
||||
}
|
||||
for (let i = this.#tail; this.#isValidIndex(i);) {
|
||||
if (allowStale || !this.#isStale(i)) {
|
||||
yield i;
|
||||
}
|
||||
@@ -595,10 +574,7 @@ export class LRUCache {
|
||||
}
|
||||
*#rindexes({ allowStale = this.allowStale } = {}) {
|
||||
if (this.#size) {
|
||||
for (let i = this.#head; true;) {
|
||||
if (!this.#isValidIndex(i)) {
|
||||
break;
|
||||
}
|
||||
for (let i = this.#head; this.#isValidIndex(i);) {
|
||||
if (allowStale || !this.#isStale(i)) {
|
||||
yield i;
|
||||
}
|
||||
@@ -650,8 +626,7 @@ export class LRUCache {
|
||||
*keys() {
|
||||
for (const i of this.#indexes()) {
|
||||
const k = this.#keyList[i];
|
||||
if (k !== undefined &&
|
||||
!this.#isBackgroundFetch(this.#valList[i])) {
|
||||
if (k !== undefined && !this.#isBackgroundFetch(this.#valList[i])) {
|
||||
yield k;
|
||||
}
|
||||
}
|
||||
@@ -665,8 +640,7 @@ export class LRUCache {
|
||||
*rkeys() {
|
||||
for (const i of this.#rindexes()) {
|
||||
const k = this.#keyList[i];
|
||||
if (k !== undefined &&
|
||||
!this.#isBackgroundFetch(this.#valList[i])) {
|
||||
if (k !== undefined && !this.#isBackgroundFetch(this.#valList[i])) {
|
||||
yield k;
|
||||
}
|
||||
}
|
||||
@@ -678,8 +652,7 @@ export class LRUCache {
|
||||
*values() {
|
||||
for (const i of this.#indexes()) {
|
||||
const v = this.#valList[i];
|
||||
if (v !== undefined &&
|
||||
!this.#isBackgroundFetch(this.#valList[i])) {
|
||||
if (v !== undefined && !this.#isBackgroundFetch(this.#valList[i])) {
|
||||
yield this.#valList[i];
|
||||
}
|
||||
}
|
||||
@@ -693,8 +666,7 @@ export class LRUCache {
|
||||
*rvalues() {
|
||||
for (const i of this.#rindexes()) {
|
||||
const v = this.#valList[i];
|
||||
if (v !== undefined &&
|
||||
!this.#isBackgroundFetch(this.#valList[i])) {
|
||||
if (v !== undefined && !this.#isBackgroundFetch(this.#valList[i])) {
|
||||
yield this.#valList[i];
|
||||
}
|
||||
}
|
||||
@@ -723,7 +695,7 @@ export class LRUCache {
|
||||
if (value === undefined)
|
||||
continue;
|
||||
if (fn(value, this.#keyList[i], this)) {
|
||||
return this.get(this.#keyList[i], getOptions);
|
||||
return this.#get(this.#keyList[i], getOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -796,7 +768,7 @@ export class LRUCache {
|
||||
const value = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v;
|
||||
if (value === undefined)
|
||||
return undefined;
|
||||
/* c8 ignore end */
|
||||
/* c8 ignore stop */
|
||||
const entry = { value };
|
||||
if (this.#ttls && this.#starts) {
|
||||
const ttl = this.#ttls[i];
|
||||
@@ -870,7 +842,7 @@ export class LRUCache {
|
||||
const age = Date.now() - entry.start;
|
||||
entry.start = this.#perf.now() - age;
|
||||
}
|
||||
this.set(key, entry.value, entry);
|
||||
this.#set(key, entry.value, entry);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -904,22 +876,43 @@ export class LRUCache {
|
||||
* `cache.delete(key)`. `undefined` is never stored in the cache.
|
||||
*/
|
||||
set(k, v, setOptions = {}) {
|
||||
const { status = metrics.hasSubscribers ? {} : undefined } = setOptions;
|
||||
setOptions.status = status;
|
||||
if (status) {
|
||||
status.op = 'set';
|
||||
status.key = k;
|
||||
if (v !== undefined)
|
||||
status.value = v;
|
||||
status.cache = this;
|
||||
}
|
||||
const result = this.#set(k, v, setOptions);
|
||||
if (status && metrics.hasSubscribers) {
|
||||
metrics.publish(status);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#set(k, v, setOptions, bf) {
|
||||
const { ttl = this.ttl, start, noDisposeOnSet = this.noDisposeOnSet, sizeCalculation = this.sizeCalculation, status, } = setOptions;
|
||||
const isBF = this.#isBackgroundFetch(v);
|
||||
if (v === undefined) {
|
||||
if (status)
|
||||
status.set = 'deleted';
|
||||
this.delete(k);
|
||||
return this;
|
||||
}
|
||||
const { ttl = this.ttl, start, noDisposeOnSet = this.noDisposeOnSet, sizeCalculation = this.sizeCalculation, status, } = setOptions;
|
||||
let { noUpdateTTL = this.noUpdateTTL } = setOptions;
|
||||
const size = this.#requireSize(k, v, setOptions.size || 0, sizeCalculation);
|
||||
if (status && !isBF)
|
||||
status.value = v;
|
||||
const size = this.#requireSize(k, v, setOptions.size || 0, sizeCalculation, status);
|
||||
// if the item doesn't fit, don't do anything
|
||||
// NB: maxEntrySize set to maxSize by default
|
||||
if (this.maxEntrySize && size > this.maxEntrySize) {
|
||||
// have to delete, in case something is there already.
|
||||
this.#delete(k, 'set');
|
||||
if (status) {
|
||||
status.set = 'miss';
|
||||
status.maxEntrySizeExceeded = true;
|
||||
}
|
||||
// have to delete, in case something is there already.
|
||||
this.#delete(k, 'set');
|
||||
return this;
|
||||
}
|
||||
let index = this.#size === 0 ? undefined : this.#keyMap.get(k);
|
||||
@@ -940,52 +933,68 @@ export class LRUCache {
|
||||
if (status)
|
||||
status.set = 'add';
|
||||
noUpdateTTL = false;
|
||||
if (this.#hasOnInsert) {
|
||||
if (this.#hasOnInsert && !isBF) {
|
||||
this.#onInsert?.(v, k, 'add');
|
||||
}
|
||||
}
|
||||
else {
|
||||
// update
|
||||
// might be updating a background fetch!
|
||||
this.#moveToTail(index);
|
||||
const oldVal = this.#valList[index];
|
||||
if (v !== oldVal) {
|
||||
if (this.#hasFetchMethod && this.#isBackgroundFetch(oldVal)) {
|
||||
oldVal.__abortController.abort(new Error('replaced'));
|
||||
const { __staleWhileFetching: s } = oldVal;
|
||||
if (s !== undefined && !noDisposeOnSet) {
|
||||
if (!noDisposeOnSet) {
|
||||
if (this.#isBackgroundFetch(oldVal)) {
|
||||
if (oldVal !== bf) {
|
||||
// setting over a background fetch, not merely resolving it.
|
||||
oldVal.__abortController.abort(new Error('replaced'));
|
||||
}
|
||||
const { __staleWhileFetching: s } = oldVal;
|
||||
if (s !== undefined && s !== v) {
|
||||
if (this.#hasDispose) {
|
||||
this.#dispose?.(s, k, 'set');
|
||||
}
|
||||
if (this.#hasDisposeAfter) {
|
||||
this.#disposed?.push([s, k, 'set']);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (this.#hasDispose) {
|
||||
this.#dispose?.(s, k, 'set');
|
||||
this.#dispose?.(oldVal, k, 'set');
|
||||
}
|
||||
if (this.#hasDisposeAfter) {
|
||||
this.#disposed?.push([s, k, 'set']);
|
||||
this.#disposed?.push([oldVal, k, 'set']);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!noDisposeOnSet) {
|
||||
if (this.#hasDispose) {
|
||||
this.#dispose?.(oldVal, k, 'set');
|
||||
}
|
||||
if (this.#hasDisposeAfter) {
|
||||
this.#disposed?.push([oldVal, k, 'set']);
|
||||
}
|
||||
}
|
||||
this.#removeItemSize(index);
|
||||
this.#addItemSize(index, size, status);
|
||||
this.#valList[index] = v;
|
||||
if (status) {
|
||||
status.set = 'replace';
|
||||
if (!isBF) {
|
||||
const oldValue = oldVal && this.#isBackgroundFetch(oldVal) ?
|
||||
oldVal.__staleWhileFetching
|
||||
: oldVal;
|
||||
if (oldValue !== undefined)
|
||||
status.oldValue = oldValue;
|
||||
const setType = oldValue === undefined ? 'add'
|
||||
: v !== oldValue ? 'replace'
|
||||
: 'update';
|
||||
if (status) {
|
||||
status.set = setType;
|
||||
if (oldValue !== undefined)
|
||||
status.oldValue = oldValue;
|
||||
}
|
||||
if (this.#hasOnInsert) {
|
||||
this.onInsert?.(v, k, setType);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (status) {
|
||||
status.set = 'update';
|
||||
}
|
||||
if (this.#hasOnInsert) {
|
||||
this.onInsert?.(v, k, v === oldVal ? 'update' : 'replace');
|
||||
else if (!isBF) {
|
||||
if (status) {
|
||||
status.set = 'update';
|
||||
}
|
||||
if (this.#hasOnInsert) {
|
||||
this.onInsert?.(v, k, 'update');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ttl !== 0 && !this.#ttls) {
|
||||
@@ -1040,18 +1049,25 @@ export class LRUCache {
|
||||
const head = this.#head;
|
||||
const k = this.#keyList[head];
|
||||
const v = this.#valList[head];
|
||||
if (this.#hasFetchMethod && this.#isBackgroundFetch(v)) {
|
||||
const isBF = this.#isBackgroundFetch(v);
|
||||
if (isBF) {
|
||||
v.__abortController.abort(new Error('evicted'));
|
||||
}
|
||||
else if (this.#hasDispose || this.#hasDisposeAfter) {
|
||||
const oldValue = isBF ? v.__staleWhileFetching : v;
|
||||
if ((this.#hasDispose || this.#hasDisposeAfter) &&
|
||||
oldValue !== undefined) {
|
||||
if (this.#hasDispose) {
|
||||
this.#dispose?.(v, k, 'evict');
|
||||
this.#dispose?.(oldValue, k, 'evict');
|
||||
}
|
||||
if (this.#hasDisposeAfter) {
|
||||
this.#disposed?.push([v, k, 'evict']);
|
||||
this.#disposed?.push([oldValue, k, 'evict']);
|
||||
}
|
||||
}
|
||||
this.#removeItemSize(head);
|
||||
if (this.#autopurgeTimers?.[head]) {
|
||||
clearTimeout(this.#autopurgeTimers[head]);
|
||||
this.#autopurgeTimers[head] = undefined;
|
||||
}
|
||||
// if we aren't about to use the index, then null these out
|
||||
if (free) {
|
||||
this.#keyList[head] = undefined;
|
||||
@@ -1086,6 +1102,19 @@ export class LRUCache {
|
||||
* {@link LRUCache.OptionsBase.updateAgeOnHas} is set.
|
||||
*/
|
||||
has(k, hasOptions = {}) {
|
||||
const { status = metrics.hasSubscribers ? {} : undefined } = hasOptions;
|
||||
hasOptions.status = status;
|
||||
if (status) {
|
||||
status.op = 'has';
|
||||
status.key = k;
|
||||
status.cache = this;
|
||||
}
|
||||
const result = this.#has(k, hasOptions);
|
||||
if (metrics.hasSubscribers)
|
||||
metrics.publish(status);
|
||||
return result;
|
||||
}
|
||||
#has(k, hasOptions = {}) {
|
||||
const { updateAgeOnHas = this.updateAgeOnHas, status } = hasOptions;
|
||||
const index = this.#keyMap.get(k);
|
||||
if (index !== undefined) {
|
||||
@@ -1122,22 +1151,46 @@ export class LRUCache {
|
||||
* {@link LRUCache.OptionsBase.allowStale} is set.
|
||||
*/
|
||||
peek(k, peekOptions = {}) {
|
||||
const { allowStale = this.allowStale } = peekOptions;
|
||||
const { status = hasSubscribers() ? {} : undefined } = peekOptions;
|
||||
if (status) {
|
||||
status.op = 'peek';
|
||||
status.key = k;
|
||||
status.cache = this;
|
||||
}
|
||||
peekOptions.status = status;
|
||||
const result = this.#peek(k, peekOptions);
|
||||
if (metrics.hasSubscribers) {
|
||||
metrics.publish(status);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#peek(k, peekOptions) {
|
||||
const { status, allowStale = this.allowStale } = peekOptions;
|
||||
const index = this.#keyMap.get(k);
|
||||
if (index === undefined ||
|
||||
(!allowStale && this.#isStale(index))) {
|
||||
return;
|
||||
if (index === undefined || (!allowStale && this.#isStale(index))) {
|
||||
if (status)
|
||||
status.peek = index === undefined ? 'miss' : 'stale';
|
||||
return undefined;
|
||||
}
|
||||
const v = this.#valList[index];
|
||||
// either stale and allowed, or forcing a refresh of non-stale value
|
||||
return this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v;
|
||||
const val = this.#isBackgroundFetch(v) ? v.__staleWhileFetching : v;
|
||||
if (status) {
|
||||
if (val !== undefined) {
|
||||
status.peek = 'hit';
|
||||
status.value = val;
|
||||
}
|
||||
else {
|
||||
status.peek = 'miss';
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
#backgroundFetch(k, index, options, context) {
|
||||
const v = index === undefined ? undefined : this.#valList[index];
|
||||
if (this.#isBackgroundFetch(v)) {
|
||||
return v;
|
||||
}
|
||||
const ac = new AC();
|
||||
const ac = new AbortController();
|
||||
const { signal } = options;
|
||||
// when/if our AC signals, then stop listening to theirs.
|
||||
signal?.addEventListener('abort', () => ac.abort(signal.reason), {
|
||||
@@ -1151,6 +1204,8 @@ export class LRUCache {
|
||||
const cb = (v, updateCache = false) => {
|
||||
const { aborted } = ac.signal;
|
||||
const ignoreAbort = options.ignoreFetchAbort && v !== undefined;
|
||||
const proceed = options.ignoreFetchAbort ||
|
||||
!!(options.allowStaleOnFetchAbort && v !== undefined);
|
||||
if (options.status) {
|
||||
if (aborted && !updateCache) {
|
||||
options.status.fetchAborted = true;
|
||||
@@ -1163,7 +1218,7 @@ export class LRUCache {
|
||||
}
|
||||
}
|
||||
if (aborted && !ignoreAbort && !updateCache) {
|
||||
return fetchFail(ac.signal.reason);
|
||||
return fetchFail(ac.signal.reason, proceed);
|
||||
}
|
||||
// either we didn't abort, and are still here, or we did, and ignored
|
||||
const bf = p;
|
||||
@@ -1171,7 +1226,7 @@ export class LRUCache {
|
||||
// cache and ignore the abort, or if it's still pending on this specific
|
||||
// background request, then write it to the cache.
|
||||
const vl = this.#valList[index];
|
||||
if (vl === p || ignoreAbort && updateCache && vl === undefined) {
|
||||
if (vl === p || (vl === undefined && ignoreAbort && updateCache)) {
|
||||
if (v === undefined) {
|
||||
if (bf.__staleWhileFetching !== undefined) {
|
||||
this.#valList[index] = bf.__staleWhileFetching;
|
||||
@@ -1183,7 +1238,7 @@ export class LRUCache {
|
||||
else {
|
||||
if (options.status)
|
||||
options.status.fetchUpdated = true;
|
||||
this.set(k, v, fetchOpts.options);
|
||||
this.#set(k, v, fetchOpts.options, bf);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
@@ -1193,9 +1248,10 @@ export class LRUCache {
|
||||
options.status.fetchRejected = true;
|
||||
options.status.fetchError = er;
|
||||
}
|
||||
return fetchFail(er);
|
||||
// do not pass go, do not collect $200
|
||||
return fetchFail(er, false);
|
||||
};
|
||||
const fetchFail = (er) => {
|
||||
const fetchFail = (er, proceed) => {
|
||||
const { aborted } = ac.signal;
|
||||
const allowStaleAborted = aborted && options.allowStaleOnFetchAbort;
|
||||
const allowStale = allowStaleAborted || options.allowStaleOnFetchRejection;
|
||||
@@ -1204,7 +1260,7 @@ export class LRUCache {
|
||||
if (this.#valList[index] === p) {
|
||||
// if we allow stale on fetch rejections, then we need to ensure that
|
||||
// the stale value is not removed from the cache when the fetch fails.
|
||||
const del = !noDelete || bf.__staleWhileFetching === undefined;
|
||||
const del = !noDelete || (!proceed && bf.__staleWhileFetching === undefined);
|
||||
if (del) {
|
||||
this.#delete(k, 'fetch');
|
||||
}
|
||||
@@ -1228,15 +1284,11 @@ export class LRUCache {
|
||||
};
|
||||
const pcall = (res, rej) => {
|
||||
const fmp = this.#fetchMethod?.(k, v, fetchOpts);
|
||||
if (fmp && fmp instanceof Promise) {
|
||||
fmp.then(v => res(v === undefined ? undefined : v), rej);
|
||||
}
|
||||
// ignored, we go until we finish, regardless.
|
||||
// defer check until we are actually aborting,
|
||||
// so fetchMethod can override.
|
||||
ac.signal.addEventListener('abort', () => {
|
||||
if (!options.ignoreFetchAbort ||
|
||||
options.allowStaleOnFetchAbort) {
|
||||
if (!options.ignoreFetchAbort || options.allowStaleOnFetchAbort) {
|
||||
res(undefined);
|
||||
// when it eventually resolves, update the cache.
|
||||
if (options.allowStaleOnFetchAbort) {
|
||||
@@ -1244,6 +1296,12 @@ export class LRUCache {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (fmp && fmp instanceof Promise) {
|
||||
fmp.then(v => res(v === undefined ? undefined : v), rej);
|
||||
}
|
||||
else if (fmp !== undefined) {
|
||||
res(fmp);
|
||||
}
|
||||
};
|
||||
if (options.status)
|
||||
options.status.fetchDispatched = true;
|
||||
@@ -1255,10 +1313,14 @@ export class LRUCache {
|
||||
});
|
||||
if (index === undefined) {
|
||||
// internal, don't expose status.
|
||||
this.set(k, bf, { ...fetchOpts.options, status: undefined });
|
||||
this.#set(k, bf, { ...fetchOpts.options, status: undefined });
|
||||
index = this.#keyMap.get(k);
|
||||
}
|
||||
else {
|
||||
// do not call #set, because we do not want to adjust its place
|
||||
// in the lru queue, as it has not yet been "used". Also, we don't
|
||||
// need to worry about evicting for size, because a background fetch
|
||||
// over a stale value is treated as the same size as its stale value.
|
||||
this.#valList[index] = bf;
|
||||
}
|
||||
return bf;
|
||||
@@ -1270,9 +1332,23 @@ export class LRUCache {
|
||||
return (!!b &&
|
||||
b instanceof Promise &&
|
||||
b.hasOwnProperty('__staleWhileFetching') &&
|
||||
b.__abortController instanceof AC);
|
||||
b.__abortController instanceof AbortController);
|
||||
}
|
||||
async fetch(k, fetchOptions = {}) {
|
||||
fetch(k, fetchOptions = {}) {
|
||||
const ths = tracing.hasSubscribers;
|
||||
const { status = hasSubscribers() ? {} : undefined } = fetchOptions;
|
||||
fetchOptions.status = status;
|
||||
if (status && fetchOptions.context) {
|
||||
status.context = fetchOptions.context;
|
||||
}
|
||||
const p = this.#fetch(k, fetchOptions);
|
||||
if (status && ths) {
|
||||
status.trace = true;
|
||||
tracing.tracePromise(() => p, status).catch(() => { });
|
||||
}
|
||||
return p;
|
||||
}
|
||||
async #fetch(k, fetchOptions = {}) {
|
||||
const {
|
||||
// get options
|
||||
allowStale = this.allowStale, updateAgeOnGet = this.updateAgeOnGet, noDeleteOnStaleGet = this.noDeleteOnStaleGet,
|
||||
@@ -1280,10 +1356,17 @@ export class LRUCache {
|
||||
ttl = this.ttl, noDisposeOnSet = this.noDisposeOnSet, size = 0, sizeCalculation = this.sizeCalculation, noUpdateTTL = this.noUpdateTTL,
|
||||
// fetch exclusive options
|
||||
noDeleteOnFetchRejection = this.noDeleteOnFetchRejection, allowStaleOnFetchRejection = this.allowStaleOnFetchRejection, ignoreFetchAbort = this.ignoreFetchAbort, allowStaleOnFetchAbort = this.allowStaleOnFetchAbort, context, forceRefresh = false, status, signal, } = fetchOptions;
|
||||
if (status) {
|
||||
status.op = 'fetch';
|
||||
status.key = k;
|
||||
if (forceRefresh)
|
||||
status.forceRefresh = true;
|
||||
status.cache = this;
|
||||
}
|
||||
if (!this.#hasFetchMethod) {
|
||||
if (status)
|
||||
status.fetch = 'get';
|
||||
return this.get(k, {
|
||||
return this.#get(k, {
|
||||
allowStale,
|
||||
updateAgeOnGet,
|
||||
noDeleteOnStaleGet,
|
||||
@@ -1352,26 +1435,68 @@ export class LRUCache {
|
||||
return staleVal ? p.__staleWhileFetching : (p.__returned = p);
|
||||
}
|
||||
}
|
||||
async forceFetch(k, fetchOptions = {}) {
|
||||
const v = await this.fetch(k, fetchOptions);
|
||||
forceFetch(k, fetchOptions = {}) {
|
||||
const ths = tracing.hasSubscribers;
|
||||
const { status = hasSubscribers() ? {} : undefined } = fetchOptions;
|
||||
fetchOptions.status = status;
|
||||
if (status && fetchOptions.context) {
|
||||
status.context = fetchOptions.context;
|
||||
}
|
||||
const p = this.#forceFetch(k, fetchOptions);
|
||||
if (status && ths) {
|
||||
status.trace = true;
|
||||
tracing.tracePromise(() => p, status).catch(() => { });
|
||||
}
|
||||
return p;
|
||||
}
|
||||
async #forceFetch(k, fetchOptions = {}) {
|
||||
const v = await this.#fetch(k, fetchOptions);
|
||||
if (v === undefined)
|
||||
throw new Error('fetch() returned undefined');
|
||||
return v;
|
||||
}
|
||||
memo(k, memoOptions = {}) {
|
||||
const { status = metrics.hasSubscribers ? {} : undefined } = memoOptions;
|
||||
memoOptions.status = status;
|
||||
if (status) {
|
||||
status.op = 'memo';
|
||||
status.key = k;
|
||||
if (memoOptions.context) {
|
||||
status.context = memoOptions.context;
|
||||
}
|
||||
status.cache = this;
|
||||
}
|
||||
const result = this.#memo(k, memoOptions);
|
||||
if (status)
|
||||
status.value = result;
|
||||
if (metrics.hasSubscribers)
|
||||
metrics.publish(status);
|
||||
return result;
|
||||
}
|
||||
#memo(k, memoOptions = {}) {
|
||||
const memoMethod = this.#memoMethod;
|
||||
if (!memoMethod) {
|
||||
throw new Error('no memoMethod provided to constructor');
|
||||
}
|
||||
const { context, forceRefresh, ...options } = memoOptions;
|
||||
const v = this.get(k, options);
|
||||
if (!forceRefresh && v !== undefined)
|
||||
const { context, status, forceRefresh, ...options } = memoOptions;
|
||||
if (status && forceRefresh)
|
||||
status.forceRefresh = true;
|
||||
const v = this.#get(k, options);
|
||||
const refresh = forceRefresh || v === undefined;
|
||||
if (status) {
|
||||
status.memo = refresh ? 'miss' : 'hit';
|
||||
if (!refresh)
|
||||
status.value = v;
|
||||
}
|
||||
if (!refresh)
|
||||
return v;
|
||||
const vv = memoMethod(k, v, {
|
||||
options,
|
||||
context,
|
||||
});
|
||||
this.set(k, vv, options);
|
||||
if (status)
|
||||
status.value = vv;
|
||||
this.#set(k, vv, options);
|
||||
return vv;
|
||||
}
|
||||
/**
|
||||
@@ -1381,55 +1506,71 @@ export class LRUCache {
|
||||
* If the key is not found, get() will return `undefined`.
|
||||
*/
|
||||
get(k, getOptions = {}) {
|
||||
const { status = metrics.hasSubscribers ? {} : undefined } = getOptions;
|
||||
getOptions.status = status;
|
||||
if (status) {
|
||||
status.op = 'get';
|
||||
status.key = k;
|
||||
status.cache = this;
|
||||
}
|
||||
const result = this.#get(k, getOptions);
|
||||
if (status) {
|
||||
if (result !== undefined)
|
||||
status.value = result;
|
||||
if (metrics.hasSubscribers)
|
||||
metrics.publish(status);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#get(k, getOptions = {}) {
|
||||
const { allowStale = this.allowStale, updateAgeOnGet = this.updateAgeOnGet, noDeleteOnStaleGet = this.noDeleteOnStaleGet, status, } = getOptions;
|
||||
const index = this.#keyMap.get(k);
|
||||
if (index !== undefined) {
|
||||
const value = this.#valList[index];
|
||||
const fetching = this.#isBackgroundFetch(value);
|
||||
if (index === undefined) {
|
||||
if (status)
|
||||
this.#statusTTL(status, index);
|
||||
if (this.#isStale(index)) {
|
||||
status.get = 'miss';
|
||||
return undefined;
|
||||
}
|
||||
const value = this.#valList[index];
|
||||
const fetching = this.#isBackgroundFetch(value);
|
||||
if (status)
|
||||
this.#statusTTL(status, index);
|
||||
if (this.#isStale(index)) {
|
||||
// delete only if not an in-flight background fetch
|
||||
if (!fetching) {
|
||||
if (!noDeleteOnStaleGet) {
|
||||
this.#delete(k, 'expire');
|
||||
}
|
||||
if (status)
|
||||
status.get = 'stale';
|
||||
// delete only if not an in-flight background fetch
|
||||
if (!fetching) {
|
||||
if (!noDeleteOnStaleGet) {
|
||||
this.#delete(k, 'expire');
|
||||
}
|
||||
if (status && allowStale)
|
||||
if (allowStale) {
|
||||
if (status)
|
||||
status.returnedStale = true;
|
||||
return allowStale ? value : undefined;
|
||||
}
|
||||
else {
|
||||
if (status &&
|
||||
allowStale &&
|
||||
value.__staleWhileFetching !== undefined) {
|
||||
status.returnedStale = true;
|
||||
}
|
||||
return allowStale ? value.__staleWhileFetching : undefined;
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
else {
|
||||
if (status)
|
||||
status.get = 'stale-fetching';
|
||||
if (allowStale && value.__staleWhileFetching !== undefined) {
|
||||
if (status)
|
||||
status.get = 'hit';
|
||||
// if we're currently fetching it, we don't actually have it yet
|
||||
// it's not stale, which means this isn't a staleWhileRefetching.
|
||||
// If it's not stale, and fetching, AND has a __staleWhileFetching
|
||||
// value, then that means the user fetched with {forceRefresh:true},
|
||||
// so it's safe to return that value.
|
||||
if (fetching) {
|
||||
return value.__staleWhileFetching;
|
||||
}
|
||||
this.#moveToTail(index);
|
||||
if (updateAgeOnGet) {
|
||||
this.#updateItemAge(index);
|
||||
}
|
||||
return value;
|
||||
status.returnedStale = true;
|
||||
return value.__staleWhileFetching;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
else if (status) {
|
||||
status.get = 'miss';
|
||||
// not stale
|
||||
if (status)
|
||||
status.get = fetching ? 'fetching' : 'hit';
|
||||
// if we're currently fetching it, we don't actually have it yet
|
||||
// it's not stale, which means this isn't a staleWhileRefetching.
|
||||
// If it's not stale, and fetching, AND has a __staleWhileFetching
|
||||
// value, then that means the user fetched with {forceRefresh:true},
|
||||
// so it's safe to return that value.
|
||||
this.#moveToTail(index);
|
||||
if (updateAgeOnGet) {
|
||||
this.#updateItemAge(index);
|
||||
}
|
||||
return fetching ? value.__staleWhileFetching : value;
|
||||
}
|
||||
#connect(p, n) {
|
||||
this.#prev[n] = p;
|
||||
@@ -1464,10 +1605,22 @@ export class LRUCache {
|
||||
return this.#delete(k, 'delete');
|
||||
}
|
||||
#delete(k, reason) {
|
||||
if (metrics.hasSubscribers) {
|
||||
metrics.publish({
|
||||
op: 'delete',
|
||||
delete: reason,
|
||||
key: k,
|
||||
cache: this,
|
||||
});
|
||||
}
|
||||
let deleted = false;
|
||||
if (this.#size !== 0) {
|
||||
const index = this.#keyMap.get(k);
|
||||
if (index !== undefined) {
|
||||
if (this.#autopurgeTimers?.[index]) {
|
||||
clearTimeout(this.#autopurgeTimers?.[index]);
|
||||
this.#autopurgeTimers[index] = undefined;
|
||||
}
|
||||
deleted = true;
|
||||
if (this.#size === 1) {
|
||||
this.#clear(reason);
|
||||
@@ -1538,11 +1691,16 @@ export class LRUCache {
|
||||
}
|
||||
}
|
||||
this.#keyMap.clear();
|
||||
this.#valList.fill(undefined);
|
||||
void this.#valList.fill(undefined);
|
||||
this.#keyList.fill(undefined);
|
||||
if (this.#ttls && this.#starts) {
|
||||
this.#ttls.fill(0);
|
||||
this.#starts.fill(0);
|
||||
for (const t of this.#autopurgeTimers ?? []) {
|
||||
if (t !== undefined)
|
||||
clearTimeout(t);
|
||||
}
|
||||
this.#autopurgeTimers?.fill(undefined);
|
||||
}
|
||||
if (this.#sizes) {
|
||||
this.#sizes.fill(0);
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user