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",
|
"version": "0.1.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@js/types": "^0.2.0",
|
"@js/types": "^0.2.1",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@js/types": {
|
"node_modules/@js/types": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"resolved": "https://gitea.jbrumond.me/api/packages/js/npm/%40js%2Ftypes/-/0.2.0/types-0.2.0.tgz",
|
"resolved": "https://gitea.jbrumond.me/api/packages/js/npm/%40js%2Ftypes/-/0.2.1/types-0.2.1.tgz",
|
||||||
"integrity": "sha512-7CsWkTQjuP9+Y+5Fv1vK0o6QDshJzCB0b8hTld3bu1j+wELrwBuLT3iTcfdB8H3hLLdohZfDMCWMl0ZY+0bseg==",
|
"integrity": "sha512-+TpX2Sfl0yBHptcDnXqdPKazL+HZDb6s/ax07sJBYt14gGvgUNlf6/QbsQHwOg53T6DkXYld0VgktQiQkRQw7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"author": "James Brumond <https://jbrumond.me>",
|
"author": "James Brumond <https://jbrumond.me>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@js/types": "^0.2.0",
|
"@js/types": "^0.2.1",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
64
readme.md
64
readme.md
@ -2,3 +2,67 @@
|
|||||||
In-memory caching/memoization utilities
|
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 {
|
export * from './simple';
|
||||||
return 'hello';
|
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