@bscotch/utility
Advanced tools
Comparing version 6.5.0 to 6.6.0
@@ -23,2 +23,3 @@ /** | ||
export * from './lib/paths.js'; | ||
export * from './lib/sequentializer.js'; | ||
export * from './lib/strings.js'; | ||
@@ -25,0 +26,0 @@ export * from './lib/trace.js'; |
@@ -23,2 +23,3 @@ /** | ||
export * from './lib/paths.js'; | ||
export * from './lib/sequentializer.js'; | ||
export * from './lib/strings.js'; | ||
@@ -25,0 +26,0 @@ export * from './lib/trace.js'; |
/** | ||
* Utilities for decorator functions. | ||
*/ | ||
import { ok } from './assert.js'; | ||
import { assert } from './types.js'; | ||
export function createDecorator( | ||
@@ -100,3 +100,3 @@ /** | ||
const func = value[functionName]; | ||
ok(typeof func === 'function', `Expected ${functionName} to be a function`); | ||
assert(typeof func === 'function', `Expected ${functionName} to be a function`); | ||
// @ts-ignore | ||
@@ -103,0 +103,0 @@ return value; |
@@ -9,13 +9,34 @@ import type { Decorator } from './decorator.js'; | ||
export declare const memoize: Decorator<any, any>; | ||
export declare function MaxAge(seconds: number, staleWhileRerunSeconds?: number): Decorator<any, any>; | ||
/** | ||
* A decorator for caching the result of a method call, including | ||
* getter methods. | ||
* | ||
* @param keyGen - An optional key-generating function can be | ||
* provided to generate | ||
* different keys based on the arguments provided in the function | ||
* call, allowing caching of multiple | ||
* outputs. Defaults to JSON-stringifying the arguments array. | ||
*/ | ||
export declare function Memoize(keyGen?: (args: any[]) => string): Decorator; | ||
export declare function Memoize(options?: { | ||
/** | ||
* An optional key-generating function can be | ||
* provided to generate | ||
* different keys based on the arguments provided in the function | ||
* call, allowing caching of multiple | ||
* outputs. Defaults to JSON-stringifying the arguments array. | ||
* @default JSON.stringify | ||
*/ | ||
keyGen?: (args: any[]) => string; | ||
ttl?: { | ||
/** | ||
* If the cache is older than this (in seconds), | ||
* the fresh value will be generated, cached, and | ||
* returned. | ||
*/ | ||
maxAge: number; | ||
/** | ||
* If the cache is older than `maxAge` but | ||
* less than `maxAge + staleWhileRevalidate`, | ||
* return the cached value while re-running | ||
* the function to update the cache for the | ||
* next request. | ||
*/ | ||
staleWhileRevalidate?: number; | ||
}; | ||
}): Decorator; | ||
//# sourceMappingURL=memoize.d.ts.map |
@@ -9,13 +9,12 @@ import { createDecorator } from './decorator.js'; | ||
export const memoize = Memoize(); | ||
export function MaxAge(seconds, staleWhileRerunSeconds = seconds / 10) { | ||
return Memoize({ | ||
ttl: { maxAge: seconds, staleWhileRevalidate: staleWhileRerunSeconds }, | ||
}); | ||
} | ||
/** | ||
* A decorator for caching the result of a method call, including | ||
* getter methods. | ||
* | ||
* @param keyGen - An optional key-generating function can be | ||
* provided to generate | ||
* different keys based on the arguments provided in the function | ||
* call, allowing caching of multiple | ||
* outputs. Defaults to JSON-stringifying the arguments array. | ||
*/ | ||
export function Memoize(keyGen = JSON.stringify) { | ||
export function Memoize(options) { | ||
return createDecorator('cache', (context) => { | ||
@@ -36,3 +35,3 @@ if (context.type === 'class') { | ||
context.descriptor[methodName] = function (...args) { | ||
return memoizedValue(this, { ...context, arguments: args }, valueGenerator, keyGen); | ||
return memoizedValue(this, { ...context, arguments: args }, valueGenerator, options?.keyGen || JSON.stringify, options?.ttl); | ||
}; | ||
@@ -39,0 +38,0 @@ } |
@@ -42,3 +42,6 @@ import type { Constructor } from 'type-fest'; | ||
*/ | ||
export declare function memoizedValue<V extends CacheValue>(calledBy: any, decorated: Decorated | DecoratedExecutionContext, valueGenerator: (...args: any[]) => V, keyGenerator?: (args: any[]) => CacheKey): V; | ||
export declare function memoizedValue<V extends CacheValue>(calledBy: any, decorated: Decorated | DecoratedExecutionContext, valueGenerator: (...args: any[]) => V, keyGenerator?: (args: any[]) => CacheKey, ttl?: { | ||
maxAge: number; | ||
staleWhileRevalidate?: number; | ||
}): V; | ||
export declare function clearMemoized(this: any, context: Decorated, scope: string | Constructor<any>): void; | ||
@@ -52,3 +55,27 @@ type AnyClass = Constructor<any>; | ||
type CacheKey = string | undefined; | ||
type CacheValue = any; | ||
type CacheValue = { | ||
/** The cached value */ | ||
value: any; | ||
/** | ||
* When using TTL caching with stale results | ||
* while revalidating, this holds onto the | ||
* promise containing the next cache value | ||
* (triggered by the first post-expirty call) | ||
* until it resolves. In the interim, the prior | ||
* value is returned by the cache. | ||
*/ | ||
nextValue?: Promise<any>; | ||
/** | ||
* Timestamp of when the value expires. | ||
* If undefined, the value never expires. | ||
*/ | ||
expiresAt?: Date; | ||
/** | ||
* If this and `expiresOn` are both defined, | ||
* after `expiresOn` any subsequent cache access | ||
* will trigger a refresh but still serve the | ||
* stale value until this date. | ||
*/ | ||
staleWhileRevalidateUntil?: Date; | ||
}; | ||
/** | ||
@@ -55,0 +82,0 @@ * The decorator "Target" is always the class constructor |
import { useTracer } from './trace.js'; | ||
const trace = useTracer('@bscotch:Memoize'); | ||
function isExpired(value) { | ||
return value.expiresAt && value.expiresAt <= new Date(); | ||
} | ||
function canUseStaleWhileRevalidate(value) { | ||
return (value.staleWhileRevalidateUntil && | ||
value.staleWhileRevalidateUntil >= new Date()); | ||
} | ||
function cacheHas(cache, key) { | ||
if (cache.has(key)) { | ||
// See if it has expired | ||
const value = cache.get(key); | ||
if (isExpired(value) && !canUseStaleWhileRevalidate(value)) { | ||
// Expired | ||
cache.delete(key); | ||
} | ||
else { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
/** | ||
@@ -8,9 +29,16 @@ * Get a memoized value if it already exists, otherwise | ||
*/ | ||
export function memoizedValue(calledBy, decorated, valueGenerator, keyGenerator) { | ||
export function memoizedValue(calledBy, decorated, valueGenerator, keyGenerator, ttl) { | ||
const args = 'arguments' in decorated ? decorated.arguments : []; | ||
const cacheKey = keyGenerator?.(args); | ||
const cache = memoizer.forProperty(decorated.classConstructor, calledBy, decorated.propertyKey); | ||
if (cache.has(cacheKey)) { | ||
let usingStaleValue = false; | ||
if (cacheHas(cache, cacheKey)) { | ||
const cached = cache.get(cacheKey); | ||
trace(`used cached for [${decorated.className}]:${decorated.propertyKey}`); | ||
return cache.get(cacheKey); | ||
if (isExpired(cached) && canUseStaleWhileRevalidate(cached)) { | ||
usingStaleValue = true; | ||
} | ||
else { | ||
return cached.value; | ||
} | ||
} | ||
@@ -20,5 +48,34 @@ trace(`missed cache for [${decorated.className}]:${decorated.propertyKey}`); | ||
trace(`using property function: %s`, decorated.propertyKey); | ||
const value = valueGenerator.bind(calledBy)(...args); | ||
return cache.set(cacheKey, value).get(cacheKey); | ||
const cached = cache.get(cacheKey); | ||
// If using stale, only re-run if we aren't already | ||
// waiting for a re-run to complete. | ||
if (usingStaleValue && cached?.nextValue) { | ||
return cached.value; | ||
} | ||
const nextValue = valueGenerator.bind(calledBy)(...args); | ||
if (cached && usingStaleValue && nextValue instanceof Promise) { | ||
// Then set it to the next value, with a THEN | ||
// that replaces the value upon resolution. | ||
cached.nextValue = nextValue; | ||
nextValue.finally(() => { | ||
cache.set(cacheKey, toCachedValue(nextValue, ttl)); | ||
}); | ||
return cached.value; | ||
} | ||
cache.set(cacheKey, toCachedValue(nextValue, ttl)); | ||
return nextValue; | ||
} | ||
function toCachedValue(value, ttl) { | ||
const expiresAt = ttl?.maxAge | ||
? new Date(Date.now() + ttl.maxAge * 1000) | ||
: undefined; | ||
const staleWhileRevalidateUntil = expiresAt && ttl?.staleWhileRevalidate | ||
? new Date(expiresAt.getTime() + ttl.staleWhileRevalidate * 1000) | ||
: undefined; | ||
return { | ||
value, | ||
expiresAt, | ||
staleWhileRevalidateUntil, | ||
}; | ||
} | ||
export function clearMemoized(context, scope) { | ||
@@ -25,0 +82,0 @@ const isInstanceScope = scope === undefined; |
import type { Defined, EmptyArray, NonEmptyArray, NotNullish, Nullish } from '../types/utility.js'; | ||
export declare class BscotchUtilError extends Error { | ||
constructor(message: string); | ||
constructor(message: string, asserter?: (...args: any[]) => any); | ||
} | ||
export declare function assert(claim: any, message: string): asserts claim; | ||
export declare function assert(claim: unknown, message: string, stackStart?: (...args: any[]) => any): asserts claim; | ||
export declare function isBoolean(thing: any): thing is boolean; | ||
@@ -7,0 +7,0 @@ export declare function isDate(thing: any): thing is Date; |
export class BscotchUtilError extends Error { | ||
constructor(message) { | ||
constructor(message, asserter) { | ||
super(message); | ||
this.name = 'BscotchUtilError'; | ||
Error.captureStackTrace(this, this.constructor); | ||
Error.captureStackTrace(this, asserter || this.constructor); | ||
} | ||
} | ||
export function assert(claim, message) { | ||
export function assert(claim, message, stackStart) { | ||
if (!claim) { | ||
throw new BscotchUtilError(message); | ||
throw new BscotchUtilError(message, stackStart || assert); | ||
} | ||
@@ -23,3 +23,3 @@ } | ||
export function assertValidDate(date) { | ||
assert(isValidDate(date), `${date} is not a valid date`); | ||
assert(isValidDate(date), `${date} is not a valid date`, assertValidDate); | ||
} | ||
@@ -26,0 +26,0 @@ export function isArray(thing) { |
@@ -14,2 +14,3 @@ /** Get a promise that resolves in some number of milliseconds. */ | ||
export declare const waitForTick: typeof resolveInNextTick; | ||
export declare function waitForTicks(ticks?: number): Promise<void>; | ||
//# sourceMappingURL=wait.d.ts.map |
@@ -20,2 +20,7 @@ /** Get a promise that resolves in some number of milliseconds. */ | ||
export const waitForTick = resolveInNextTick; | ||
export async function waitForTicks(ticks = 1) { | ||
for (let t = 0; t < ticks; t++) { | ||
await waitForTick(); | ||
} | ||
} | ||
//# sourceMappingURL=wait.js.map |
{ | ||
"name": "@bscotch/utility", | ||
"version": "6.5.0", | ||
"version": "6.6.0", | ||
"description": "Bscotch Utilities: Methods for common Node.js needs.", | ||
@@ -30,2 +30,6 @@ "keywords": [ | ||
}, | ||
"./sequentialize": { | ||
"types": "./dist/lib/sequentialize.d.ts", | ||
"import": "./dist/lib/sequentialize.js" | ||
}, | ||
"./json-pointer": { | ||
@@ -50,3 +54,3 @@ "types": "./dist/lib/jsonPointer.d.ts", | ||
"fs-extra": "10.1.0", | ||
"mocha": "^10.0.0", | ||
"mocha": "^10.1.0", | ||
"rimraf": "^3.0.2", | ||
@@ -64,4 +68,4 @@ "type-fest": "^3.0.0", | ||
"test:dev": "mocha --config ../../config/.mocharc.cjs --forbid-only=false --parallel=false --timeout=9999999999", | ||
"watch": "tsc --build --watch" | ||
"watch": "tsc-x --build --watch" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
246737
3549