@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 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
@@ -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
+ }