@gera2ld/async-memo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +104 -0
- package/dist/index.test.d.ts +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# @gera2ld/async-memo
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
|
+
[![bundle][bundle-src]][bundle-href]
|
|
6
|
+
[![JSDocs][jsdocs-src]][jsdocs-href]
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
### Quick Start
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { asyncMemo } from '@gera2ld/async-memo';
|
|
14
|
+
|
|
15
|
+
const myApi = asyncMemo(myApiCall, {
|
|
16
|
+
resolver: (params) => [groupKey, cacheKey],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Call from anywhere
|
|
20
|
+
async function someAction() {
|
|
21
|
+
const response = await myApi(params);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get the current value anytime
|
|
25
|
+
function getValueSync() {
|
|
26
|
+
return myApi.get(params);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get context to avoid passing args around
|
|
30
|
+
function otherPlace() {
|
|
31
|
+
const ctx = myApi.context(params);
|
|
32
|
+
ctx.get(); // sync get
|
|
33
|
+
ctx.reload(); // refresh data
|
|
34
|
+
ctx.isSettled(); // check loading state
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Each `groupKey` has its own cache. The cache is invalidated when the `cacheKey` changes.
|
|
39
|
+
|
|
40
|
+
### Options
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
interface CacheOptions<T extends unknown[]> {
|
|
44
|
+
/** Convert args into cacheGroup and cacheKey.
|
|
45
|
+
* By default the function is only evaluated once. */
|
|
46
|
+
resolver: (...args: T) => string | [cacheGroup: string, cacheKey: string];
|
|
47
|
+
/** Whether stale value should be returned. Default: false */
|
|
48
|
+
mustRevalidate: boolean;
|
|
49
|
+
/** Time to live in ms, -1 for always. Default: -1 */
|
|
50
|
+
ttl: number;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Use Cases
|
|
55
|
+
|
|
56
|
+
#### Load once globally
|
|
57
|
+
|
|
58
|
+
This is the default behavior.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const loadOnceGlobally = asyncMemo(api);
|
|
62
|
+
// or
|
|
63
|
+
const loadOnceGlobally = asyncMemo(api, {
|
|
64
|
+
resolver: () => '',
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Load on param change
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const loadOnParamChange = asyncMemo(api, {
|
|
72
|
+
resolver: (params) => ['', JSON.stringify(params)],
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Cache data for multiple tabs
|
|
77
|
+
|
|
78
|
+
The data for each tab will be cached in a different group, with the parameters as its cache key.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const loadOnParamChange = asyncMemo(api, {
|
|
82
|
+
resolver: (params) => [params.tab, JSON.stringify(params)],
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Custom Cache Store
|
|
87
|
+
|
|
88
|
+
For frameworks like MobX or Vue, you can provide a custom cache store:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { createAsyncMemo } from '@gera2ld/async-memo';
|
|
92
|
+
|
|
93
|
+
function createCache<T>() {
|
|
94
|
+
// ... your cache implementation
|
|
95
|
+
return {
|
|
96
|
+
get(cacheKey: string) { ... },
|
|
97
|
+
set(cacheKey: string, data?: T) { ... },
|
|
98
|
+
clear() { ... },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const asyncMemo = createAsyncMemo(createCache);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary>MobX example</summary>
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { observable } from 'mobx';
|
|
110
|
+
|
|
111
|
+
function createCache<T>() {
|
|
112
|
+
const target = observable<{ value: Record<string, T | undefined> }>(
|
|
113
|
+
{ value: {} },
|
|
114
|
+
{ value: observable.ref },
|
|
115
|
+
);
|
|
116
|
+
return {
|
|
117
|
+
get(cacheKey: string) {
|
|
118
|
+
return target.value[cacheKey];
|
|
119
|
+
},
|
|
120
|
+
set(cacheKey: string, data?: T) {
|
|
121
|
+
target.value = { ...target.value, [cacheKey]: data };
|
|
122
|
+
},
|
|
123
|
+
clear() {
|
|
124
|
+
target.value = {};
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
</details>
|
|
131
|
+
|
|
132
|
+
<details>
|
|
133
|
+
<summary>Vue example</summary>
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { ref } from 'vue';
|
|
137
|
+
|
|
138
|
+
function createCache<T>() {
|
|
139
|
+
const target = ref<Record<string, T | undefined>>({});
|
|
140
|
+
return {
|
|
141
|
+
get(cacheKey: string) {
|
|
142
|
+
return target.value[cacheKey];
|
|
143
|
+
},
|
|
144
|
+
set(cacheKey: string, data?: T) {
|
|
145
|
+
target.value = { ...target.value, [cacheKey]: data };
|
|
146
|
+
},
|
|
147
|
+
clear() {
|
|
148
|
+
target.value = {};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
</details>
|
|
155
|
+
|
|
156
|
+
[npm-version-src]: https://img.shields.io/npm/v/@gera2ld/async-memo?style=flat&colorA=18181B&colorB=F0DB4F
|
|
157
|
+
[npm-version-href]: https://npmjs.com/package/@gera2ld/async-memo
|
|
158
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/@gera2ld/async-memo?style=flat&colorA=18181B&colorB=F0DB4F
|
|
159
|
+
[npm-downloads-href]: https://npmjs.com/package/@gera2ld/async-memo
|
|
160
|
+
[bundle-src]: https://img.shields.io/bundlephobia/minzip/@gera2ld/async-memo?style=flat&colorA=18181B&colorB=F0DB4F
|
|
161
|
+
[bundle-href]: https://bundlephobia.com/result?p=@gera2ld/async-memo
|
|
162
|
+
[jsdocs-src]: https://img.shields.io/badge/jsDocs.io-reference-18181B?style=flat&colorA=18181B&colorB=F0DB4F
|
|
163
|
+
[jsdocs-href]: https://www.jsdocs.io/package/@gera2ld/async-memo
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface CacheContext<U> {
|
|
2
|
+
call(): Promise<U>;
|
|
3
|
+
/** Return the currently cached value immediately. */
|
|
4
|
+
get(): U | undefined;
|
|
5
|
+
/** Override the cache with the specified value. */
|
|
6
|
+
set(value: U, ttl?: number): void;
|
|
7
|
+
/** Delete the currently cached value. */
|
|
8
|
+
delete(): void;
|
|
9
|
+
/** Invalidate the current cached value and send a new request without deleting the old value. */
|
|
10
|
+
reload(): Promise<U>;
|
|
11
|
+
/** Whether the latest request is settled. */
|
|
12
|
+
isSettled(): boolean;
|
|
13
|
+
/** Whether the value returned by `get` is fresh. */
|
|
14
|
+
isFresh(): boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface CachedFunction<T extends unknown[], U, S extends CacheStorage> {
|
|
17
|
+
(...args: T): Promise<U>;
|
|
18
|
+
/** Return the currently cached value immediately. */
|
|
19
|
+
get(...args: T): U | undefined;
|
|
20
|
+
/** Delete the currently cached value. */
|
|
21
|
+
delete(...args: T): void;
|
|
22
|
+
/** Invalidate the current cached value and send a new request without deleting the old value. */
|
|
23
|
+
reload(...args: T): Promise<U>;
|
|
24
|
+
/** Whether the latest request is settled. */
|
|
25
|
+
isSettled(...args: T): boolean;
|
|
26
|
+
/** Whether the value returned by `get` is fresh. */
|
|
27
|
+
isFresh(...args: T): boolean;
|
|
28
|
+
/** Clear cache. */
|
|
29
|
+
clear(): void;
|
|
30
|
+
/** Get the cache context so we don't need to pass `args` around. */
|
|
31
|
+
context(...args: T): CacheContext<U>;
|
|
32
|
+
/** The cache storage, only used for testing purpose. */
|
|
33
|
+
cache: S;
|
|
34
|
+
}
|
|
35
|
+
type CachePrimitiveKey = string | number | undefined;
|
|
36
|
+
export interface CacheOptions<T extends unknown[]> {
|
|
37
|
+
/**
|
|
38
|
+
* Convert args into `cacheGroup` and `cacheKey`.
|
|
39
|
+
* If `cacheKey` is not provided, `cacheGroup` will be used.
|
|
40
|
+
* By default the args are ignored and the function is only evaluated once.
|
|
41
|
+
*
|
|
42
|
+
* Each `cacheGroup` stores a cached value, `cacheKey` determines whether the value is stale and needs to be reloaded.
|
|
43
|
+
*
|
|
44
|
+
* @returns `cacheGroup` or `[cacheGroup, cacheKey]`.
|
|
45
|
+
*/
|
|
46
|
+
resolver: (...args: T) => CachePrimitiveKey | [cacheGroup: CachePrimitiveKey, cacheKey: CachePrimitiveKey];
|
|
47
|
+
/**
|
|
48
|
+
* Whether stale value should be returned.
|
|
49
|
+
*/
|
|
50
|
+
mustRevalidate: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Time to live, -1 for always. Default as `-1`.
|
|
53
|
+
*/
|
|
54
|
+
ttl: number;
|
|
55
|
+
}
|
|
56
|
+
export interface CacheData<U = unknown> {
|
|
57
|
+
key: string;
|
|
58
|
+
promise: Promise<U>;
|
|
59
|
+
settled: boolean;
|
|
60
|
+
value?: U;
|
|
61
|
+
expiresAt: number;
|
|
62
|
+
}
|
|
63
|
+
export interface CacheStorage<U = unknown> {
|
|
64
|
+
get(cacheGroup: string): CacheData<U> | undefined;
|
|
65
|
+
set(cacheGroup: string, data?: CacheData<U>): void;
|
|
66
|
+
clear(): void;
|
|
67
|
+
}
|
|
68
|
+
export declare function createAsyncMemoStorage(): Map<string, CacheData<unknown>>;
|
|
69
|
+
export declare function createAsyncMemo<S extends CacheStorage = ReturnType<typeof createAsyncMemoStorage>>(cacheFactory?: () => S): <U, T extends unknown[]>(fn: (...args: T) => Promise<U>, options?: Partial<CacheOptions<T>>) => CachedFunction<T, U, S>;
|
|
70
|
+
export declare const asyncMemo: <U, T extends unknown[]>(fn: (...args: T) => Promise<U>, options?: Partial<CacheOptions<T>> | undefined) => CachedFunction<T, U, Map<string, CacheData<unknown>>>;
|
|
71
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const K = {
|
|
2
|
+
mustRevalidate: !1,
|
|
3
|
+
resolver: () => "",
|
|
4
|
+
ttl: -1
|
|
5
|
+
};
|
|
6
|
+
function O() {
|
|
7
|
+
return /* @__PURE__ */ new Map();
|
|
8
|
+
}
|
|
9
|
+
function P(x) {
|
|
10
|
+
return (y, D) => {
|
|
11
|
+
const n = (x || O)(), { mustRevalidate: M, resolver: w, ttl: a } = {
|
|
12
|
+
...K,
|
|
13
|
+
...D
|
|
14
|
+
}, F = (...e) => {
|
|
15
|
+
const s = w(...e);
|
|
16
|
+
return (Array.isArray(s) ? s : [s, s]).map((r) => `${r ?? ""}`);
|
|
17
|
+
}, o = (e) => (...s) => {
|
|
18
|
+
const t = F(...s);
|
|
19
|
+
return e(t, s);
|
|
20
|
+
}, i = ([e, s]) => {
|
|
21
|
+
const t = n.get(e);
|
|
22
|
+
return t?.key === s && t.settled;
|
|
23
|
+
}, l = ([e, s]) => {
|
|
24
|
+
const t = n.get(e);
|
|
25
|
+
return t?.key === s && t.settled && (t.expiresAt < 0 || t.expiresAt > Date.now());
|
|
26
|
+
}, h = ([e, s]) => {
|
|
27
|
+
const t = n.get(e);
|
|
28
|
+
if (t && (!M || l([e, s])))
|
|
29
|
+
return t.value;
|
|
30
|
+
}, v = ([e, s], t, r = a) => {
|
|
31
|
+
const c = r < 0 ? r : Date.now() + r;
|
|
32
|
+
n.set(e, {
|
|
33
|
+
key: s,
|
|
34
|
+
...n.get(e),
|
|
35
|
+
promise: t == null ? Promise.reject() : Promise.resolve(t),
|
|
36
|
+
value: t,
|
|
37
|
+
expiresAt: c,
|
|
38
|
+
settled: !0
|
|
39
|
+
});
|
|
40
|
+
}, f = ([e]) => {
|
|
41
|
+
n.set(e);
|
|
42
|
+
}, S = () => {
|
|
43
|
+
n.clear();
|
|
44
|
+
}, d = ([e, s], t) => {
|
|
45
|
+
const r = n.get(e), c = y(...t), u = {
|
|
46
|
+
...r,
|
|
47
|
+
key: s,
|
|
48
|
+
promise: c,
|
|
49
|
+
// Set to -1 until the promise is either resolved or rejected
|
|
50
|
+
expiresAt: -1,
|
|
51
|
+
settled: !1
|
|
52
|
+
};
|
|
53
|
+
n.set(e, u);
|
|
54
|
+
const p = (m, C) => {
|
|
55
|
+
if (n.get(e) !== u)
|
|
56
|
+
return;
|
|
57
|
+
let g;
|
|
58
|
+
m ? g = 0 : g = a < 0 ? a : Date.now() + a, n.set(e, {
|
|
59
|
+
...u,
|
|
60
|
+
value: C,
|
|
61
|
+
expiresAt: g,
|
|
62
|
+
settled: !0
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
return c.then(
|
|
66
|
+
(m) => {
|
|
67
|
+
p(!1, m);
|
|
68
|
+
},
|
|
69
|
+
() => {
|
|
70
|
+
p(!0);
|
|
71
|
+
}
|
|
72
|
+
), c;
|
|
73
|
+
}, A = (e, s) => {
|
|
74
|
+
const [t, r] = e, c = n.get(t);
|
|
75
|
+
return c?.key === r && (l(e) || !i(e)) ? c.promise : d(e, s);
|
|
76
|
+
}, j = (e, s) => ({
|
|
77
|
+
call: () => A(e, s),
|
|
78
|
+
get: () => h(e),
|
|
79
|
+
set: (t, r = -1) => {
|
|
80
|
+
v(e, t, r);
|
|
81
|
+
},
|
|
82
|
+
delete: () => f(e),
|
|
83
|
+
reload: () => d(e, s),
|
|
84
|
+
isSettled: () => i(e),
|
|
85
|
+
isFresh: () => l(e)
|
|
86
|
+
});
|
|
87
|
+
return Object.assign(o(A), {
|
|
88
|
+
get: o(h),
|
|
89
|
+
delete: o(f),
|
|
90
|
+
reload: o(d),
|
|
91
|
+
isSettled: o(i),
|
|
92
|
+
isFresh: o(l),
|
|
93
|
+
context: o(j),
|
|
94
|
+
clear: S,
|
|
95
|
+
cache: n
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const _ = P();
|
|
100
|
+
export {
|
|
101
|
+
_ as asyncMemo,
|
|
102
|
+
P as createAsyncMemo,
|
|
103
|
+
O as createAsyncMemoStorage
|
|
104
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gera2ld/async-memo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cache asynchronous functions where they should be",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"registry": "https://registry.npmjs.org/"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"api",
|
|
24
|
+
"cache",
|
|
25
|
+
"async"
|
|
26
|
+
],
|
|
27
|
+
"author": "Gerald <code@gera2ld.space>",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/gera2ld/async-memo.git"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.3.3",
|
|
35
|
+
"vite": "^7.3.1",
|
|
36
|
+
"vitest": "^4.0.18"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build:js": "vite build",
|
|
40
|
+
"build:types": "tsc",
|
|
41
|
+
"build": "pnpm run /^build:/",
|
|
42
|
+
"test": "vitest --run"
|
|
43
|
+
}
|
|
44
|
+
}
|