generated from templates/typescript-library
add 3 caching implementations for various use cases
This commit is contained in:
parent
5acbfd4555
commit
13a64c0d2d
8
package-lock.json
generated
8
package-lock.json
generated
@ -9,14 +9,14 @@
|
||||
"version": "0.1.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@js/types": "^0.2.0",
|
||||
"@js/types": "^0.2.1",
|
||||
"typescript": "^5.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@js/types": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://gitea.jbrumond.me/api/packages/js/npm/%40js%2Ftypes/-/0.2.0/types-0.2.0.tgz",
|
||||
"integrity": "sha512-7CsWkTQjuP9+Y+5Fv1vK0o6QDshJzCB0b8hTld3bu1j+wELrwBuLT3iTcfdB8H3hLLdohZfDMCWMl0ZY+0bseg==",
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://gitea.jbrumond.me/api/packages/js/npm/%40js%2Ftypes/-/0.2.1/types-0.2.1.tgz",
|
||||
"integrity": "sha512-+TpX2Sfl0yBHptcDnXqdPKazL+HZDb6s/ax07sJBYt14gGvgUNlf6/QbsQHwOg53T6DkXYld0VgktQiQkRQw7w==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
|
@ -18,7 +18,7 @@
|
||||
"author": "James Brumond <https://jbrumond.me>",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@js/types": "^0.2.0",
|
||||
"@js/types": "^0.2.1",
|
||||
"typescript": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
64
readme.md
64
readme.md
@ -2,3 +2,67 @@
|
||||
In-memory caching/memoization utilities
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install --save @js/memo
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { memo } from '@js/memo';
|
||||
|
||||
function get_data_from_slow_source(name: string) : string {
|
||||
// ....
|
||||
}
|
||||
|
||||
const get_data_cached = memo({
|
||||
func: get_data_from_slow_source,
|
||||
ttl: 30_000,
|
||||
});
|
||||
```
|
||||
|
||||
### Custom cache key generation
|
||||
|
||||
```ts
|
||||
import { memo } from '@js/memo';
|
||||
|
||||
interface SomeObject {
|
||||
id: string;
|
||||
foo: string;
|
||||
bar: number;
|
||||
baz: {
|
||||
qux: string;
|
||||
};
|
||||
}
|
||||
|
||||
function get_data_from_slow_source(obj: SomeObject) : string {
|
||||
// ....
|
||||
}
|
||||
|
||||
const get_data_cached = memo({
|
||||
func: get_data_from_slow_source,
|
||||
ttl: 30_000,
|
||||
|
||||
// Pull just the `id` field from the first parameter to use
|
||||
// as the cache key
|
||||
key([ obj ]) {
|
||||
return obj.id;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Simple functions with no params
|
||||
|
||||
```ts
|
||||
|
||||
import { memo_simple } from '@js/memo';
|
||||
|
||||
function get_data_from_slow_source() : string {
|
||||
// ....
|
||||
}
|
||||
|
||||
const get_data_cached = memo_simple(30_000, get_data_from_slow_source);
|
||||
```
|
||||
|
238
src/async.ts
Normal file
238
src/async.ts
Normal file
@ -0,0 +1,238 @@
|
||||
|
||||
import type { AsyncFunc, AsyncResult, Params } from '@js/types';
|
||||
|
||||
export interface MemoAsyncOptions<T extends AsyncFunc> {
|
||||
/**
|
||||
* The function to call and cache results from
|
||||
*/
|
||||
func: T;
|
||||
|
||||
/**
|
||||
* Time-to-live for records in the cache. After this amount of time,
|
||||
* a value is considered "stale", and by default, will be evicted
|
||||
* from the cache.
|
||||
*
|
||||
* This value can be a number (ms), or a function which receives the
|
||||
* params and result of the call and returns a number for each record.
|
||||
*/
|
||||
ttl: number | MemoAsyncTTLFunc<T>;
|
||||
|
||||
/**
|
||||
* Custom function for generating a cache key. If not provided, a
|
||||
* built-in default will be used instead. (At the moment, that
|
||||
* default is `JSON.stringify()`, which is not guranteed to be
|
||||
* stable or particularly fast; Providing a better implementation
|
||||
* for most functions is recommended)
|
||||
*/
|
||||
key?: MemoAsyncKeyFunc<T>;
|
||||
|
||||
/**
|
||||
* Callback to run when evicting a record from the cache (usually
|
||||
* because it expired)
|
||||
*/
|
||||
on_evict?: MemoAsyncOnEvict<T>;
|
||||
|
||||
/**
|
||||
* If provided and non-zero, the cache will keep values in storage
|
||||
* for this long (ms) past their expiration times. During this window,
|
||||
* a subsequent request for the value will result in a new call to the
|
||||
* source to refresh the cache, but the stale value in cache will still
|
||||
* be served immediately to reduce cache misses
|
||||
*/
|
||||
stale_ttl?: number;
|
||||
|
||||
/**
|
||||
* If provided and non-zero, outstanding (not yet fulfilled) promises
|
||||
* will be cached and returned to subsequent callers for the given
|
||||
* amount of time (ms). If the promise does not resolve before the TTL
|
||||
* passes, it will be evicted from the cache to allow for another attempt.
|
||||
* If the promise rejects, it will be dropped from the cache. If the
|
||||
* promise resolves, the actual result will then be stored
|
||||
*/
|
||||
promise_ttl?: number;
|
||||
}
|
||||
|
||||
export interface MemoAsyncKeyFunc<T extends AsyncFunc> {
|
||||
(params: Params<T>) : string;
|
||||
}
|
||||
|
||||
export interface MemoAsyncTTLFunc<T extends AsyncFunc> {
|
||||
(params: Params<T>, result: ReturnType<T>) : number;
|
||||
}
|
||||
|
||||
export interface MemoAsyncOnEvict<T extends AsyncFunc> {
|
||||
(evicted: MemoAsyncStoredResult<T>) : any;
|
||||
}
|
||||
|
||||
export interface MemoAsyncStoredResult<T extends AsyncFunc> {
|
||||
key: string;
|
||||
created: number;
|
||||
resolved: number;
|
||||
expires: number;
|
||||
promise: ReturnType<T>;
|
||||
result: AsyncResult<T>;
|
||||
hits: number;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
export function memo<T extends AsyncFunc>(opts: MemoAsyncOptions<T>) : T {
|
||||
const get_key = opts.key ?? default_key;
|
||||
const storage: Record<string, MemoAsyncStoredResult<T>> = Object.create(null);
|
||||
const refreshing: Record<string, MemoAsyncStoredResult<T>> = Object.create(null);
|
||||
|
||||
function memoized(...params: Params<T>) : ReturnType<T> {
|
||||
const key = get_key(params);
|
||||
|
||||
if (storage[key]) {
|
||||
storage[key].hits++;
|
||||
|
||||
// If the record is stale (but still stored because of `stale_ttl`), start
|
||||
// a new refresh call in the background
|
||||
if (Date.now() >= storage[key].expires) {
|
||||
refresh_stale(storage[key], params);
|
||||
}
|
||||
|
||||
return storage[key].result;
|
||||
}
|
||||
|
||||
const promise = opts.func(...params) as ReturnType<T>;
|
||||
store_record(key, params, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function evict(record: MemoAsyncStoredResult<T>) {
|
||||
if (storage[record.key] === record) {
|
||||
delete storage[record.key];
|
||||
|
||||
if (refreshing[record.key]?.timer) {
|
||||
// If another call is already working on refreshing this record and
|
||||
// it has an active `promise_ttl` timer, promote it into the main
|
||||
// storage now
|
||||
storage[record.key] = refreshing[record.key];
|
||||
delete refreshing[record.key];
|
||||
}
|
||||
|
||||
if (opts.on_evict) {
|
||||
opts.on_evict(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refresh_stale(old_record: MemoAsyncStoredResult<T>, params: Params<T>) {
|
||||
const record: MemoAsyncStoredResult<T> = {
|
||||
key: old_record.key,
|
||||
created: Date.now(),
|
||||
resolved: null,
|
||||
expires: null,
|
||||
promise: opts.func(...params) as ReturnType<T>,
|
||||
result: null,
|
||||
hits: 0,
|
||||
timer: null,
|
||||
};
|
||||
|
||||
refreshing[record.key] = record;
|
||||
|
||||
if (opts.promise_ttl) {
|
||||
record.timer = setTimeout(() => on_timeout(record), opts.promise_ttl);
|
||||
}
|
||||
|
||||
record.promise.then(
|
||||
(result) => on_refresh_resolve(record, params, result),
|
||||
(reason) => on_reject(record, reason)
|
||||
);
|
||||
}
|
||||
|
||||
function store_record(key: string, params: Params<T>, promise: ReturnType<T>) {
|
||||
const record: MemoAsyncStoredResult<T> = {
|
||||
key,
|
||||
created: Date.now(),
|
||||
resolved: null,
|
||||
expires: null,
|
||||
promise,
|
||||
result: null,
|
||||
hits: 0,
|
||||
timer: null,
|
||||
};
|
||||
|
||||
if (opts.promise_ttl) {
|
||||
storage[key] = record;
|
||||
record.timer = setTimeout(() => on_timeout(record), opts.promise_ttl);
|
||||
}
|
||||
|
||||
promise.then(
|
||||
(result) => on_resolve(record, params, result),
|
||||
(reason) => on_reject(record, reason)
|
||||
);
|
||||
}
|
||||
|
||||
function on_timeout(record: MemoAsyncStoredResult<T>) {
|
||||
record.timer = null;
|
||||
|
||||
if (storage[record.key] === record) {
|
||||
delete storage[record.key];
|
||||
}
|
||||
}
|
||||
|
||||
function on_resolve(record: MemoAsyncStoredResult<T>, params: Params<T>, result: AsyncResult<T>) {
|
||||
if (record.timer) {
|
||||
clearTimeout(record.timer);
|
||||
}
|
||||
|
||||
const ttl = typeof opts.ttl === 'function'
|
||||
? opts.ttl(params, result)
|
||||
: opts.ttl;
|
||||
|
||||
const evict_after = opts.stale_ttl
|
||||
? ttl + opts.stale_ttl
|
||||
: ttl;
|
||||
|
||||
record.result = result;
|
||||
record.resolved = Date.now();
|
||||
record.expires = record.resolved + ttl;
|
||||
record.timer = setTimeout(() => evict(record), evict_after);
|
||||
storage[record.key] = record;
|
||||
}
|
||||
|
||||
function on_refresh_resolve(record: MemoAsyncStoredResult<T>, params: Params<T>, result: AsyncResult<T>) {
|
||||
if (record.timer) {
|
||||
clearTimeout(record.timer);
|
||||
}
|
||||
|
||||
const ttl = typeof opts.ttl === 'function'
|
||||
? opts.ttl(params, result)
|
||||
: opts.ttl;
|
||||
|
||||
const evict_after = opts.stale_ttl
|
||||
? ttl + opts.stale_ttl
|
||||
: ttl;
|
||||
|
||||
record.result = result;
|
||||
record.resolved = Date.now();
|
||||
record.expires = record.resolved + ttl;
|
||||
record.timer = setTimeout(() => evict(record), evict_after);
|
||||
|
||||
if (storage[record.key]?.timer) {
|
||||
clearTimeout(storage[record.key].timer);
|
||||
evict(storage[record.key]);
|
||||
}
|
||||
|
||||
storage[record.key] = record;
|
||||
}
|
||||
|
||||
function on_reject(record: MemoAsyncStoredResult<T>, reason: any) {
|
||||
if (record.timer) {
|
||||
// If a timeout timer was set due to `promise_ttl`, clear it now
|
||||
clearTimeout(record.timer);
|
||||
}
|
||||
|
||||
if (storage[record.key] === record) {
|
||||
delete storage[record.key];
|
||||
}
|
||||
}
|
||||
|
||||
return memoized as unknown as T;
|
||||
}
|
||||
|
||||
function default_key(...params: any[]) : string {
|
||||
return JSON.stringify(params);
|
||||
}
|
108
src/full.ts
Normal file
108
src/full.ts
Normal file
@ -0,0 +1,108 @@
|
||||
|
||||
import type { Func, Params } from '@js/types';
|
||||
|
||||
export interface MemoOptions<T extends Func> {
|
||||
/**
|
||||
* The function to call and cache results from
|
||||
*/
|
||||
func: T;
|
||||
|
||||
/**
|
||||
* Time-to-live for records in the cache. After this amount of time,
|
||||
* a value is considered "stale", and by default, will be evicted
|
||||
* from the cache.
|
||||
*
|
||||
* This value can be a number (ms), or a function which receives the
|
||||
* params and result of the call and returns a number for each record.
|
||||
*/
|
||||
ttl: number | MemoTTLFunc<T>;
|
||||
|
||||
/**
|
||||
* Custom function for generating a cache key. If not provided, a
|
||||
* built-in default will be used instead. (At the moment, that
|
||||
* default is `JSON.stringify()`, which is not guranteed to be
|
||||
* stable or particularly fast; Providing a better implementation
|
||||
* for most functions is recommended)
|
||||
*/
|
||||
key?: MemoKeyFunc<T>;
|
||||
|
||||
/**
|
||||
* Callback to run when evicting a record from the cache (usually
|
||||
* because it expired)
|
||||
*/
|
||||
on_evict?: MemoOnEvict<T>;
|
||||
}
|
||||
|
||||
export interface MemoKeyFunc<T extends Func> {
|
||||
(params: Params<T>) : string;
|
||||
}
|
||||
|
||||
export interface MemoTTLFunc<T extends Func> {
|
||||
(params: Params<T>, result: ReturnType<T>) : number;
|
||||
}
|
||||
|
||||
export interface MemoOnEvict<T extends Func> {
|
||||
(evicted: MemoStoredResult<T>) : any;
|
||||
}
|
||||
|
||||
export interface MemoStoredResult<T extends Func> {
|
||||
key: string;
|
||||
created: number;
|
||||
expires: number;
|
||||
result: ReturnType<T>;
|
||||
hits: number;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
export function memo<T extends Func>(opts: MemoOptions<T>) : T {
|
||||
const get_key = opts.key ?? default_key;
|
||||
const storage: Record<string, MemoStoredResult<T>> = Object.create(null);
|
||||
|
||||
function memoized(...args: Params<T>) : ReturnType<T> {
|
||||
const key = get_key(args);
|
||||
|
||||
if (storage[key]) {
|
||||
storage[key].hits++;
|
||||
return storage[key].result;
|
||||
}
|
||||
|
||||
const result = opts.func(...args);
|
||||
store_record(key, args, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function evict(key: string) {
|
||||
if (storage[key]) {
|
||||
const evicted = storage[key];
|
||||
delete storage[key];
|
||||
|
||||
if (opts.on_evict) {
|
||||
opts.on_evict(evicted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function store_record(key: string, params: Params<T>, result: ReturnType<T>) {
|
||||
const record: MemoStoredResult<T> = {
|
||||
key,
|
||||
created: Date.now(),
|
||||
expires: null,
|
||||
result,
|
||||
hits: 0,
|
||||
timer: null,
|
||||
};
|
||||
|
||||
const ttl = typeof opts.ttl === 'function'
|
||||
? opts.ttl(params, result)
|
||||
: opts.ttl;
|
||||
|
||||
record.expires = record.created + ttl;
|
||||
record.timer = setTimeout(() => evict(record.key), ttl);
|
||||
}
|
||||
|
||||
return memoized as unknown as T;
|
||||
}
|
||||
|
||||
function default_key(...params: any[]) : string {
|
||||
return JSON.stringify(params);
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
|
||||
export function hello() : string {
|
||||
return 'hello';
|
||||
}
|
||||
export * from './simple';
|
||||
export * from './full';
|
||||
|
16
src/simple.ts
Normal file
16
src/simple.ts
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
export function memo_simple<T>(ttl: number, func: () => T) : () => T {
|
||||
let value: T;
|
||||
let expires: number;
|
||||
|
||||
return function memoized() : T {
|
||||
const now = Date.now();
|
||||
|
||||
if (value == null || expires < now) {
|
||||
value = func();
|
||||
expires = now + ttl;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user