@vercube/cache 1.2.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/LICENSE +21 -0
- package/README.md +54 -0
- package/dist/index.d.mts +407 -0
- package/dist/index.mjs +461 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present - Vercube
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/vercube/vercube/refs/heads/main/.github/assets/cover.png" width="100%" alt="Vercube - Unleash your server development." />
|
|
3
|
+
<br>
|
|
4
|
+
<br>
|
|
5
|
+
|
|
6
|
+
# @vercube/cache
|
|
7
|
+
|
|
8
|
+
### Decorator driven caching for Vercube apps
|
|
9
|
+
|
|
10
|
+
[&labelColor=%23000&color=%232f2f2f>)](https://deepwiki.com/vercube/vercube)
|
|
11
|
+
&labelColor=%23000&color=%232e2e2e&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40vercube%2Fcache>)
|
|
12
|
+
&labelColor=%23000&color=%232f2f2f>)
|
|
13
|
+
&labelColor=%23000&color=%232f2f2f>)
|
|
14
|
+
|
|
15
|
+
**One decorator turns any method into a cached one - with TTL, stale-while-revalidate, call deduplication and precise invalidation. Every entry lives in a regular Vercube storage.**
|
|
16
|
+
|
|
17
|
+
[Website](https://vercube.dev) • [Documentation](https://vercube.dev/docs/getting-started)
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
## ✨ Features
|
|
22
|
+
|
|
23
|
+
- **`@Cache()` decorator** - cache any method, keyed by its arguments
|
|
24
|
+
- **Backed by `@vercube/storage`** - memory, S3 or your own driver, no glue code
|
|
25
|
+
- **Stale-while-revalidate** - serve instantly, refresh in the background
|
|
26
|
+
- **Call deduplication** - concurrent calls for the same key share one execution
|
|
27
|
+
- **Precise invalidation** - `invalidate()` and `expire()` right on the decorated method
|
|
28
|
+
- **Self-invalidating** - entries are dropped when the method body or its options change
|
|
29
|
+
- **Multi-tier** - read through a fast local storage into a shared one
|
|
30
|
+
|
|
31
|
+
## 📦 Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm add @vercube/cache @vercube/storage
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 📖 Usage
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { Cache } from '@vercube/cache';
|
|
41
|
+
|
|
42
|
+
export class UsersService {
|
|
43
|
+
@Cache({ maxAge: 300, swr: true, staleMaxAge: 900 })
|
|
44
|
+
public async getUser(id: string): Promise<User> {
|
|
45
|
+
return this.database.findUser(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Check out the full [documentation](https://vercube.dev/docs/modules/cache/overview)
|
|
51
|
+
|
|
52
|
+
## 📜 License
|
|
53
|
+
|
|
54
|
+
[MIT](https://github.com/vercube/vercube/blob/main/LICENSE)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { BaseDecorator, Container } from "@vercube/di";
|
|
2
|
+
import { Logger } from "@vercube/logger";
|
|
3
|
+
import { Storage, StorageManager } from "@vercube/storage";
|
|
4
|
+
import { CacheEntry, CacheOptions, CacheStatus, StorageInterface } from "ocache";
|
|
5
|
+
//#region src/Services/CacheStorageAdapter.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Prefix every cache key starts with. The segment that follows it (when present)
|
|
8
|
+
* is the name of the storage mounted in {@link StorageManager} that backs the entry.
|
|
9
|
+
*
|
|
10
|
+
* ocache builds keys as `<base>:<group>:<name>:<key>.json`, so the base is always
|
|
11
|
+
* the first colon separated segment and can be parsed back out on read/write.
|
|
12
|
+
*/
|
|
13
|
+
declare const CACHE_BASE_PREFIX = "/cache";
|
|
14
|
+
/**
|
|
15
|
+
* Builds the cache key base for a storage mounted in {@link StorageManager}.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} [storage] - Name of the mounted storage, `default` when omitted
|
|
18
|
+
* @returns {string} The base prefix cache keys for that storage start with
|
|
19
|
+
*/
|
|
20
|
+
declare function cacheBaseForStorage(storage?: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Extracts the mounted storage name back out of a full cache key.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} key - A cache key produced by the caching engine
|
|
25
|
+
* @returns {string} Name of the storage the key belongs to, `default` when the key carries no storage segment
|
|
26
|
+
*/
|
|
27
|
+
declare function storageNameFromCacheKey(key: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Bridges the caching engine onto the Vercube {@link StorageManager}.
|
|
30
|
+
*
|
|
31
|
+
* The cache module owns no storage of its own - every entry lives in a storage
|
|
32
|
+
* mounted in `@vercube/storage`, so a cache can be backed by memory, S3 or any
|
|
33
|
+
* other driver simply by mounting it, and the very same entries are visible
|
|
34
|
+
* through the regular storage API.
|
|
35
|
+
*
|
|
36
|
+
* Every cache key carries the name of the storage it belongs to in its base
|
|
37
|
+
* segment, so a single adapter instance serves cached functions that live in
|
|
38
|
+
* different storages - and even ones that span several of them at once.
|
|
39
|
+
*
|
|
40
|
+
* A cached function may point at a storage that has not been mounted; in that
|
|
41
|
+
* case a {@link MemoryStorage} is mounted under that name on first use so that
|
|
42
|
+
* caching works with zero configuration. Mounting the name yourself - before or
|
|
43
|
+
* after - is what swaps the backend for a real one.
|
|
44
|
+
*/
|
|
45
|
+
declare class CacheStorageAdapter implements StorageInterface {
|
|
46
|
+
/** Storage manager holding every mounted storage */
|
|
47
|
+
protected gStorageManager: StorageManager | null;
|
|
48
|
+
/** Logger instance */
|
|
49
|
+
protected gLogger: Logger | null;
|
|
50
|
+
/** In-flight auto-mounts, keyed by storage name, so concurrent calls share one storage */
|
|
51
|
+
protected fMounting: Map<string, Promise<Storage>>;
|
|
52
|
+
/**
|
|
53
|
+
* Reads a cache entry from the storage its key points at.
|
|
54
|
+
*
|
|
55
|
+
* @template T - Type of the stored entry
|
|
56
|
+
* @param {string} key - Full cache key, including the storage base prefix
|
|
57
|
+
* @returns {Promise<T | null>} The stored entry, or null when it is absent
|
|
58
|
+
*/
|
|
59
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Writes a cache entry to the storage its key points at.
|
|
62
|
+
*
|
|
63
|
+
* The caching engine signals a deletion by writing `null`, which is mapped onto
|
|
64
|
+
* the storage's delete operation so that no empty entries are left behind.
|
|
65
|
+
*
|
|
66
|
+
* @template T - Type of the value to store
|
|
67
|
+
* @param {string} key - Full cache key, including the storage base prefix
|
|
68
|
+
* @param {T} value - Entry to store, or null to remove it
|
|
69
|
+
* @param {{ ttl?: number }} [opts] - Storage hints, `ttl` in seconds
|
|
70
|
+
* @returns {Promise<void>} Resolves once the write is complete
|
|
71
|
+
*/
|
|
72
|
+
set<T = unknown>(key: string, value: T, opts?: {
|
|
73
|
+
ttl?: number;
|
|
74
|
+
}): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Resolves the mounted storage a cache key belongs to, mounting an in-memory
|
|
77
|
+
* one under that name when nothing has been mounted yet.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} key - Full cache key, including the storage base prefix
|
|
80
|
+
* @returns {Promise<Storage>} The storage backing that key
|
|
81
|
+
* @protected
|
|
82
|
+
*/
|
|
83
|
+
protected resolveStorage(key: string): Promise<Storage>;
|
|
84
|
+
/**
|
|
85
|
+
* Mounts an in-memory storage under the given name, so that a cached function
|
|
86
|
+
* pointing at a storage nobody mounted still works.
|
|
87
|
+
*
|
|
88
|
+
* @param {StorageManager} manager - Storage manager to mount into
|
|
89
|
+
* @param {string} name - Name of the storage to mount
|
|
90
|
+
* @returns {Promise<Storage>} The freshly mounted storage
|
|
91
|
+
* @protected
|
|
92
|
+
*/
|
|
93
|
+
protected mountFallback(manager: StorageManager, name: string): Promise<Storage>;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/Types/CacheTypes.d.ts
|
|
97
|
+
declare namespace CacheTypes {
|
|
98
|
+
/**
|
|
99
|
+
* Name (or ordered list of names) of storages mounted in the {@link StorageManager}
|
|
100
|
+
* that should back a cached function.
|
|
101
|
+
*
|
|
102
|
+
* When an array is given, reads try each storage in order (multi-tier: hit the fast
|
|
103
|
+
* one first, fall back to the shared one) and writes go to all of them.
|
|
104
|
+
*
|
|
105
|
+
* When omitted, the `default` storage is used.
|
|
106
|
+
*/
|
|
107
|
+
type StorageRef = string | string[];
|
|
108
|
+
/**
|
|
109
|
+
* How a cached value was served on a given call.
|
|
110
|
+
* - `hit` - a fresh cached value was returned without re-resolving
|
|
111
|
+
* - `stale` - a stale value was served while a background SWR refresh runs
|
|
112
|
+
* - `revalidated` - a prior value existed but was expired, so it was re-resolved in the foreground
|
|
113
|
+
* - `miss` - the value was resolved fresh on this call
|
|
114
|
+
*/
|
|
115
|
+
type Status = CacheStatus;
|
|
116
|
+
/**
|
|
117
|
+
* A cache entry as it is stored, wrapping the cached value with its metadata.
|
|
118
|
+
*/
|
|
119
|
+
type Entry<T = unknown> = CacheEntry<T>;
|
|
120
|
+
/**
|
|
121
|
+
* Options accepted by {@link CacheManager.cached} and the `@Cache()` decorator.
|
|
122
|
+
*
|
|
123
|
+
* This is the ocache option set with the low level `base` option replaced by the
|
|
124
|
+
* Vercube native `storage` option - `base` is derived from it so that cache keys
|
|
125
|
+
* can be routed to the right mounted storage.
|
|
126
|
+
*/
|
|
127
|
+
type Options<T = any, ArgsT extends unknown[] = any[]> = Omit<CacheOptions<T, ArgsT>, 'base'> & {
|
|
128
|
+
/** Storage (or storages, for multi-tier caching) mounted in StorageManager to keep entries in. */
|
|
129
|
+
storage?: StorageRef;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Application wide defaults applied to every cached function.
|
|
133
|
+
* Per-call options always win over these.
|
|
134
|
+
*/
|
|
135
|
+
interface Defaults {
|
|
136
|
+
/** Number of seconds an entry stays fresh. @default 1 */
|
|
137
|
+
maxAge?: number;
|
|
138
|
+
/** Serve a stale entry while refreshing it in the background. @default false */
|
|
139
|
+
swr?: boolean;
|
|
140
|
+
/** Maximum number of seconds a stale entry may be served while revalidating. */
|
|
141
|
+
staleMaxAge?: number;
|
|
142
|
+
/** Cache key group prefix. @default 'functions' */
|
|
143
|
+
group?: string;
|
|
144
|
+
/** Default storage (or storages) to keep entries in. */
|
|
145
|
+
storage?: StorageRef;
|
|
146
|
+
/** Called for every cache related error (read, write, background refresh). */
|
|
147
|
+
onError?: (error: unknown) => void;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A function wrapped with caching, augmented with on-demand revalidation helpers.
|
|
151
|
+
*/
|
|
152
|
+
type CachedFunction<T = unknown, ArgsT extends unknown[] = any[]> = {
|
|
153
|
+
(...args: ArgsT): Promise<T>;
|
|
154
|
+
/** Resolves every storage key (one per storage tier) the given arguments cache under. */
|
|
155
|
+
resolveKeys: (...args: ArgsT) => Promise<string[]>;
|
|
156
|
+
/** Removes cached entries for the given arguments from every storage tier. */
|
|
157
|
+
invalidate: (...args: ArgsT) => Promise<void>;
|
|
158
|
+
/** Marks cached entries as stale so the next access refreshes them. */
|
|
159
|
+
expire: (...args: ArgsT) => Promise<void>;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Options for the `@Cache()` decorator. Identical to {@link Options}, except that
|
|
163
|
+
* `name` defaults to `ClassName.methodName` when it is not given.
|
|
164
|
+
*/
|
|
165
|
+
type DecoratorOptions<T = any, ArgsT extends unknown[] = any[]> = Options<T, ArgsT>;
|
|
166
|
+
/**
|
|
167
|
+
* A method wrapped by the `@Cache()` decorator, as seen from the outside.
|
|
168
|
+
* Use it to type a cached method so its `.invalidate()` / `.expire()` helpers are visible.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* class UsersService {
|
|
173
|
+
* @Cache({ maxAge: 60 })
|
|
174
|
+
* public getUser!: CacheTypes.CachedMethod<[id: string], User>;
|
|
175
|
+
* }
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
type CachedMethod<ArgsT extends unknown[] = any[], T = unknown> = CachedFunction<T, ArgsT>;
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/Services/CacheManager.d.ts
|
|
182
|
+
/**
|
|
183
|
+
* Central entry point of the cache module.
|
|
184
|
+
*
|
|
185
|
+
* The CacheManager owns the wiring between the caching engine and the storages
|
|
186
|
+
* mounted in `@vercube/storage`, holds the application wide defaults and exposes
|
|
187
|
+
* the imperative API used both directly and by the `@Cache()` decorator.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* container.bind(CacheManager);
|
|
192
|
+
*
|
|
193
|
+
* const cache = container.get(CacheManager);
|
|
194
|
+
* cache.configure({ maxAge: 60, swr: true, staleMaxAge: 300, storage: 'cache' });
|
|
195
|
+
*
|
|
196
|
+
* const getUser = cache.cached((id: string) => db.users.find(id), { name: 'getUser' });
|
|
197
|
+
*
|
|
198
|
+
* await getUser('123'); // resolves and stores
|
|
199
|
+
* await getUser('123'); // served from cache
|
|
200
|
+
* await getUser.invalidate('123'); // drops the entry
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
declare class CacheManager {
|
|
204
|
+
/** Container instance */
|
|
205
|
+
protected gContainer: Container;
|
|
206
|
+
/** Logger instance */
|
|
207
|
+
protected gLogger: Logger | null;
|
|
208
|
+
/** Application wide defaults applied to every cached function */
|
|
209
|
+
protected fDefaults: CacheTypes.Defaults;
|
|
210
|
+
/** Storage adapter bridging the caching engine onto the mounted storages */
|
|
211
|
+
protected fAdapter: CacheStorageAdapter | null;
|
|
212
|
+
/**
|
|
213
|
+
* Returns the storage adapter this manager installed into the caching engine.
|
|
214
|
+
*
|
|
215
|
+
* @returns {CacheStorageAdapter | null} The adapter, or null before initialization
|
|
216
|
+
*/
|
|
217
|
+
get adapter(): CacheStorageAdapter | null;
|
|
218
|
+
/**
|
|
219
|
+
* Returns the currently configured defaults.
|
|
220
|
+
*
|
|
221
|
+
* @returns {CacheTypes.Defaults} A copy of the active defaults
|
|
222
|
+
*/
|
|
223
|
+
get defaults(): CacheTypes.Defaults;
|
|
224
|
+
/**
|
|
225
|
+
* Sets the application wide cache defaults. Values passed per cached function
|
|
226
|
+
* always win over these. Calling it repeatedly merges into the existing defaults.
|
|
227
|
+
*
|
|
228
|
+
* @param {CacheTypes.Defaults} defaults - Defaults to apply to every cached function
|
|
229
|
+
* @returns {void}
|
|
230
|
+
*/
|
|
231
|
+
configure(defaults: CacheTypes.Defaults): void;
|
|
232
|
+
/**
|
|
233
|
+
* Wraps a function with caching.
|
|
234
|
+
*
|
|
235
|
+
* The returned function keeps the original signature and adds `resolveKeys()`,
|
|
236
|
+
* `invalidate()` and `expire()` helpers keyed exactly like the cached calls, so
|
|
237
|
+
* no cache key ever has to be rebuilt by hand.
|
|
238
|
+
*
|
|
239
|
+
* @template T - Return type of the wrapped function
|
|
240
|
+
* @template ArgsT - Argument tuple of the wrapped function
|
|
241
|
+
* @param {(...args: ArgsT) => T | Promise<T>} fn - The function to cache
|
|
242
|
+
* @param {CacheTypes.Options<T, ArgsT>} [options] - Per-function cache options
|
|
243
|
+
* @returns {CacheTypes.CachedFunction<T, ArgsT>} The cached function
|
|
244
|
+
*/
|
|
245
|
+
cached<T, ArgsT extends unknown[] = any[]>(fn: (...args: ArgsT) => T | Promise<T>, options?: CacheTypes.Options<T, ArgsT>): CacheTypes.CachedFunction<T, ArgsT>;
|
|
246
|
+
/**
|
|
247
|
+
* Removes cached entries for the given arguments from every storage tier.
|
|
248
|
+
*
|
|
249
|
+
* Pass the same options (`name`, `group`, `storage`, `getKey`) the entry was
|
|
250
|
+
* cached with so the very same keys are resolved.
|
|
251
|
+
*
|
|
252
|
+
* @template ArgsT - Argument tuple the entry was cached under
|
|
253
|
+
* @param {CacheTypes.Options<any, ArgsT>} options - Options identifying the cached function
|
|
254
|
+
* @param {ArgsT} args - Arguments identifying the entry
|
|
255
|
+
* @returns {Promise<void>} Resolves once every tier has been cleared
|
|
256
|
+
*/
|
|
257
|
+
invalidate<ArgsT extends unknown[] = any[]>(options: CacheTypes.Options<any, ArgsT>, ...args: ArgsT): Promise<void>;
|
|
258
|
+
/**
|
|
259
|
+
* Marks cached entries as stale without removing them.
|
|
260
|
+
*
|
|
261
|
+
* With `swr` enabled the stale value keeps being served (within `staleMaxAge`)
|
|
262
|
+
* while the next access refreshes it in the background; without it, the next
|
|
263
|
+
* call re-resolves before returning.
|
|
264
|
+
*
|
|
265
|
+
* @template ArgsT - Argument tuple the entry was cached under
|
|
266
|
+
* @param {CacheTypes.Options<any, ArgsT>} options - Options identifying the cached function
|
|
267
|
+
* @param {ArgsT} args - Arguments identifying the entry
|
|
268
|
+
* @returns {Promise<void>} Resolves once every tier has been marked
|
|
269
|
+
*/
|
|
270
|
+
expire<ArgsT extends unknown[] = any[]>(options: CacheTypes.Options<any, ArgsT>, ...args: ArgsT): Promise<void>;
|
|
271
|
+
/**
|
|
272
|
+
* Resolves every storage key (one per storage tier) the given arguments cache under.
|
|
273
|
+
* Useful for debugging and for cache inspection tooling.
|
|
274
|
+
*
|
|
275
|
+
* @template ArgsT - Argument tuple the entry was cached under
|
|
276
|
+
* @param {CacheTypes.Options<any, ArgsT>} options - Options identifying the cached function
|
|
277
|
+
* @param {ArgsT} args - Arguments identifying the entry
|
|
278
|
+
* @returns {Promise<string[]>} The resolved storage keys
|
|
279
|
+
*/
|
|
280
|
+
resolveKeys<ArgsT extends unknown[] = any[]>(options: CacheTypes.Options<any, ArgsT>, ...args: ArgsT): Promise<string[]>;
|
|
281
|
+
/**
|
|
282
|
+
* Merges the configured defaults into per-function options and translates the
|
|
283
|
+
* Vercube `storage` option into the key base the storage adapter routes on.
|
|
284
|
+
*
|
|
285
|
+
* @template T - Return type of the cached function
|
|
286
|
+
* @template ArgsT - Argument tuple of the cached function
|
|
287
|
+
* @param {CacheTypes.Options<T, ArgsT>} options - Per-function cache options
|
|
288
|
+
* @returns {CacheOptions<T, ArgsT>} Options understood by the caching engine
|
|
289
|
+
* @protected
|
|
290
|
+
*/
|
|
291
|
+
protected resolveOptions<T, ArgsT extends unknown[]>(options: CacheTypes.Options<T, ArgsT>): CacheOptions<T, ArgsT>;
|
|
292
|
+
/**
|
|
293
|
+
* Drops keys whose value is `undefined`, so that an explicitly passed
|
|
294
|
+
* `undefined` never shadows a configured default with a missing value.
|
|
295
|
+
*
|
|
296
|
+
* @template T - Shape of the option object
|
|
297
|
+
* @param {T} options - Options to clean up
|
|
298
|
+
* @returns {Partial<T>} The options without undefined values
|
|
299
|
+
* @protected
|
|
300
|
+
*/
|
|
301
|
+
protected stripUndefined<T extends object>(options: T): Partial<T>;
|
|
302
|
+
/**
|
|
303
|
+
* Installs the storage adapter into the caching engine.
|
|
304
|
+
* Called automatically with the `@Init()` decorator.
|
|
305
|
+
*
|
|
306
|
+
* @returns {void}
|
|
307
|
+
* @protected
|
|
308
|
+
*/
|
|
309
|
+
protected init(): void;
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/Decorators/Cache.d.ts
|
|
313
|
+
/**
|
|
314
|
+
* Decorator implementation that replaces the decorated method with a cached
|
|
315
|
+
* version of itself.
|
|
316
|
+
*
|
|
317
|
+
* The wrapper is built lazily on the first call so that storages mounted after
|
|
318
|
+
* the container has been flushed - and defaults configured during bootstrap -
|
|
319
|
+
* are still picked up.
|
|
320
|
+
*/
|
|
321
|
+
declare class CacheDecorator extends BaseDecorator<CacheTypes.DecoratorOptions> {
|
|
322
|
+
/** Cache manager owning the caching engine wiring */
|
|
323
|
+
protected gCacheManager: CacheManager;
|
|
324
|
+
/** The untouched method, kept so it can be restored on destroy */
|
|
325
|
+
protected fOriginalMethod: ((...args: unknown[]) => unknown) | null;
|
|
326
|
+
/** Whether the method lived on the instance itself rather than on the prototype */
|
|
327
|
+
protected fOwnMethod: boolean;
|
|
328
|
+
/**
|
|
329
|
+
* Replaces the decorated method with its cached counterpart.
|
|
330
|
+
*
|
|
331
|
+
* @returns {void}
|
|
332
|
+
*/
|
|
333
|
+
created(): void;
|
|
334
|
+
/**
|
|
335
|
+
* Drops the cached wrapper and puts the original method back in place.
|
|
336
|
+
*
|
|
337
|
+
* A method defined on the instance itself (a class field holding a function)
|
|
338
|
+
* is written back, while a prototype method is simply uncovered by removing
|
|
339
|
+
* the wrapper the decorator added to the instance.
|
|
340
|
+
*
|
|
341
|
+
* @returns {void}
|
|
342
|
+
*/
|
|
343
|
+
destroyed(): void;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Caches the result of the decorated method.
|
|
347
|
+
*
|
|
348
|
+
* The cache key is derived from the method's arguments, so every distinct set of
|
|
349
|
+
* arguments gets its own entry. Concurrent calls for the same key are coalesced
|
|
350
|
+
* into a single execution, and entries are automatically dropped when the method
|
|
351
|
+
* body or its cache options change.
|
|
352
|
+
*
|
|
353
|
+
* The decorated method gains three helpers, keyed exactly like the cached calls:
|
|
354
|
+
* `resolveKeys(...args)`, `invalidate(...args)` and `expire(...args)`.
|
|
355
|
+
*
|
|
356
|
+
* @param {CacheTypes.DecoratorOptions} [options] - Cache options for this method
|
|
357
|
+
* @returns {Function} The method decorator
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```ts
|
|
361
|
+
* class UsersService {
|
|
362
|
+
* @Cache({ maxAge: 60, swr: true, staleMaxAge: 300, storage: 'redis' })
|
|
363
|
+
* public async getUser(id: string): Promise<User> {
|
|
364
|
+
* return this.database.findUser(id);
|
|
365
|
+
* }
|
|
366
|
+
* }
|
|
367
|
+
*
|
|
368
|
+
* await usersService.getUser('123');
|
|
369
|
+
* await (usersService.getUser as CacheTypes.CachedMethod<[string], User>).invalidate('123');
|
|
370
|
+
* ```
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```ts
|
|
374
|
+
* // arguments that are not safely hashable (Request, streams, class instances)
|
|
375
|
+
* // should be projected into an explicit key
|
|
376
|
+
* class ReportsController {
|
|
377
|
+
* @Cache({ maxAge: 300, getKey: (range: DateRange) => `${range.from}-${range.to}` })
|
|
378
|
+
* public async report(range: DateRange): Promise<Report> {
|
|
379
|
+
* return this.reports.build(range);
|
|
380
|
+
* }
|
|
381
|
+
* }
|
|
382
|
+
* ```
|
|
383
|
+
*/
|
|
384
|
+
declare function Cache(options?: CacheTypes.DecoratorOptions): Function;
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/Errors/CacheError.d.ts
|
|
387
|
+
/**
|
|
388
|
+
* Custom error class for cache-related errors.
|
|
389
|
+
* Wraps underlying cache/storage errors with standardized error messages.
|
|
390
|
+
*/
|
|
391
|
+
declare class CacheError extends Error {
|
|
392
|
+
/**
|
|
393
|
+
* The original error that caused this cache error
|
|
394
|
+
*/
|
|
395
|
+
readonly cause?: Error;
|
|
396
|
+
/**
|
|
397
|
+
* The cache operation that failed
|
|
398
|
+
*/
|
|
399
|
+
readonly operation: string;
|
|
400
|
+
/**
|
|
401
|
+
* Additional metadata about the error (non-sensitive)
|
|
402
|
+
*/
|
|
403
|
+
readonly metadata?: Record<string, unknown>;
|
|
404
|
+
constructor(message: string, operation: string, cause?: Error, metadata?: Record<string, unknown>);
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
export { CACHE_BASE_PREFIX, Cache, CacheDecorator, CacheError, CacheManager, CacheStorageAdapter, CacheTypes, cacheBaseForStorage, storageNameFromCacheKey };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { BaseDecorator, Container, Init, Inject, InjectOptional, createDecorator } from "@vercube/di";
|
|
2
|
+
import { hash } from "ohash";
|
|
3
|
+
import { Logger } from "@vercube/logger";
|
|
4
|
+
import { StorageManager } from "@vercube/storage";
|
|
5
|
+
import { defineCachedFunction, expireCache, invalidateCache, resolveCacheKeys, setStorage, useStorage } from "ocache";
|
|
6
|
+
import { MemoryStorage } from "@vercube/storage/drivers/MemoryStorage";
|
|
7
|
+
//#region src/Errors/CacheError.ts
|
|
8
|
+
/**
|
|
9
|
+
* Custom error class for cache-related errors.
|
|
10
|
+
* Wraps underlying cache/storage errors with standardized error messages.
|
|
11
|
+
*/
|
|
12
|
+
var CacheError = class CacheError extends Error {
|
|
13
|
+
/**
|
|
14
|
+
* The original error that caused this cache error
|
|
15
|
+
*/
|
|
16
|
+
cause;
|
|
17
|
+
/**
|
|
18
|
+
* The cache operation that failed
|
|
19
|
+
*/
|
|
20
|
+
operation;
|
|
21
|
+
/**
|
|
22
|
+
* Additional metadata about the error (non-sensitive)
|
|
23
|
+
*/
|
|
24
|
+
metadata;
|
|
25
|
+
constructor(message, operation, cause, metadata) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "CacheError";
|
|
28
|
+
this.operation = operation;
|
|
29
|
+
this.cause = cause;
|
|
30
|
+
this.metadata = metadata;
|
|
31
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, CacheError);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region \0@oxc-project+runtime@0.140.0/helpers/esm/decorate.js
|
|
36
|
+
function __decorate(decorators, target, key, desc) {
|
|
37
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
38
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
39
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
40
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/Services/CacheStorageAdapter.ts
|
|
44
|
+
/**
|
|
45
|
+
* Prefix every cache key starts with. The segment that follows it (when present)
|
|
46
|
+
* is the name of the storage mounted in {@link StorageManager} that backs the entry.
|
|
47
|
+
*
|
|
48
|
+
* ocache builds keys as `<base>:<group>:<name>:<key>.json`, so the base is always
|
|
49
|
+
* the first colon separated segment and can be parsed back out on read/write.
|
|
50
|
+
*/
|
|
51
|
+
const CACHE_BASE_PREFIX = "/cache";
|
|
52
|
+
/**
|
|
53
|
+
* Builds the cache key base for a storage mounted in {@link StorageManager}.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} [storage] - Name of the mounted storage, `default` when omitted
|
|
56
|
+
* @returns {string} The base prefix cache keys for that storage start with
|
|
57
|
+
*/
|
|
58
|
+
function cacheBaseForStorage(storage) {
|
|
59
|
+
if (storage?.includes(":")) throw new CacheError("Storage name used for caching must not contain a colon", "cacheBaseForStorage", void 0, { storage });
|
|
60
|
+
return storage && storage !== "default" ? `${CACHE_BASE_PREFIX}/${storage}` : CACHE_BASE_PREFIX;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Extracts the mounted storage name back out of a full cache key.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} key - A cache key produced by the caching engine
|
|
66
|
+
* @returns {string} Name of the storage the key belongs to, `default` when the key carries no storage segment
|
|
67
|
+
*/
|
|
68
|
+
function storageNameFromCacheKey(key) {
|
|
69
|
+
const separatorIndex = key.indexOf(":");
|
|
70
|
+
const base = separatorIndex === -1 ? key : key.slice(0, separatorIndex);
|
|
71
|
+
if (!base.startsWith(`/cache/`)) return "default";
|
|
72
|
+
return base.slice(7) || "default";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Bridges the caching engine onto the Vercube {@link StorageManager}.
|
|
76
|
+
*
|
|
77
|
+
* The cache module owns no storage of its own - every entry lives in a storage
|
|
78
|
+
* mounted in `@vercube/storage`, so a cache can be backed by memory, S3 or any
|
|
79
|
+
* other driver simply by mounting it, and the very same entries are visible
|
|
80
|
+
* through the regular storage API.
|
|
81
|
+
*
|
|
82
|
+
* Every cache key carries the name of the storage it belongs to in its base
|
|
83
|
+
* segment, so a single adapter instance serves cached functions that live in
|
|
84
|
+
* different storages - and even ones that span several of them at once.
|
|
85
|
+
*
|
|
86
|
+
* A cached function may point at a storage that has not been mounted; in that
|
|
87
|
+
* case a {@link MemoryStorage} is mounted under that name on first use so that
|
|
88
|
+
* caching works with zero configuration. Mounting the name yourself - before or
|
|
89
|
+
* after - is what swaps the backend for a real one.
|
|
90
|
+
*/
|
|
91
|
+
var CacheStorageAdapter = class {
|
|
92
|
+
/** Storage manager holding every mounted storage */
|
|
93
|
+
gStorageManager;
|
|
94
|
+
/** Logger instance */
|
|
95
|
+
gLogger;
|
|
96
|
+
/** In-flight auto-mounts, keyed by storage name, so concurrent calls share one storage */
|
|
97
|
+
fMounting = /* @__PURE__ */ new Map();
|
|
98
|
+
/**
|
|
99
|
+
* Reads a cache entry from the storage its key points at.
|
|
100
|
+
*
|
|
101
|
+
* @template T - Type of the stored entry
|
|
102
|
+
* @param {string} key - Full cache key, including the storage base prefix
|
|
103
|
+
* @returns {Promise<T | null>} The stored entry, or null when it is absent
|
|
104
|
+
*/
|
|
105
|
+
async get(key) {
|
|
106
|
+
return await (await this.resolveStorage(key)).getItem(key) ?? null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Writes a cache entry to the storage its key points at.
|
|
110
|
+
*
|
|
111
|
+
* The caching engine signals a deletion by writing `null`, which is mapped onto
|
|
112
|
+
* the storage's delete operation so that no empty entries are left behind.
|
|
113
|
+
*
|
|
114
|
+
* @template T - Type of the value to store
|
|
115
|
+
* @param {string} key - Full cache key, including the storage base prefix
|
|
116
|
+
* @param {T} value - Entry to store, or null to remove it
|
|
117
|
+
* @param {{ ttl?: number }} [opts] - Storage hints, `ttl` in seconds
|
|
118
|
+
* @returns {Promise<void>} Resolves once the write is complete
|
|
119
|
+
*/
|
|
120
|
+
async set(key, value, opts) {
|
|
121
|
+
const storage = await this.resolveStorage(key);
|
|
122
|
+
if (value === null || value === void 0) {
|
|
123
|
+
await storage.deleteItem(key);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await storage.setItem(key, value, opts);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolves the mounted storage a cache key belongs to, mounting an in-memory
|
|
130
|
+
* one under that name when nothing has been mounted yet.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} key - Full cache key, including the storage base prefix
|
|
133
|
+
* @returns {Promise<Storage>} The storage backing that key
|
|
134
|
+
* @protected
|
|
135
|
+
*/
|
|
136
|
+
async resolveStorage(key) {
|
|
137
|
+
const manager = this.gStorageManager;
|
|
138
|
+
if (!manager) throw new CacheError("StorageManager is not registered in the container - bind it so cached entries have a storage to live in", "resolveStorage", void 0, { key });
|
|
139
|
+
const name = storageNameFromCacheKey(key);
|
|
140
|
+
return manager.getStorage(name) ?? await this.mountFallback(manager, name);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Mounts an in-memory storage under the given name, so that a cached function
|
|
144
|
+
* pointing at a storage nobody mounted still works.
|
|
145
|
+
*
|
|
146
|
+
* @param {StorageManager} manager - Storage manager to mount into
|
|
147
|
+
* @param {string} name - Name of the storage to mount
|
|
148
|
+
* @returns {Promise<Storage>} The freshly mounted storage
|
|
149
|
+
* @protected
|
|
150
|
+
*/
|
|
151
|
+
async mountFallback(manager, name) {
|
|
152
|
+
const pending = this.fMounting.get(name);
|
|
153
|
+
if (pending) return pending;
|
|
154
|
+
const mounting = (async () => {
|
|
155
|
+
await manager.mount({
|
|
156
|
+
name,
|
|
157
|
+
storage: MemoryStorage
|
|
158
|
+
});
|
|
159
|
+
const storage = manager.getStorage(name);
|
|
160
|
+
if (!storage) throw new CacheError("Unable to mount a fallback cache storage", "mountFallback", void 0, { storage: name });
|
|
161
|
+
await storage.initialize(void 0);
|
|
162
|
+
this.gLogger?.warn(`Vercube/CacheStorageAdapter::storage "${name}" was not mounted, an in-memory one has been mounted automatically`);
|
|
163
|
+
return storage;
|
|
164
|
+
})();
|
|
165
|
+
this.fMounting.set(name, mounting);
|
|
166
|
+
mounting.catch(() => void 0).finally(() => {
|
|
167
|
+
if (this.fMounting.get(name) === mounting) this.fMounting.delete(name);
|
|
168
|
+
});
|
|
169
|
+
return mounting;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
__decorate([InjectOptional(StorageManager)], CacheStorageAdapter.prototype, "gStorageManager", void 0);
|
|
173
|
+
__decorate([InjectOptional(Logger)], CacheStorageAdapter.prototype, "gLogger", void 0);
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/Services/CacheManager.ts
|
|
176
|
+
/**
|
|
177
|
+
* Central entry point of the cache module.
|
|
178
|
+
*
|
|
179
|
+
* The CacheManager owns the wiring between the caching engine and the storages
|
|
180
|
+
* mounted in `@vercube/storage`, holds the application wide defaults and exposes
|
|
181
|
+
* the imperative API used both directly and by the `@Cache()` decorator.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* container.bind(CacheManager);
|
|
186
|
+
*
|
|
187
|
+
* const cache = container.get(CacheManager);
|
|
188
|
+
* cache.configure({ maxAge: 60, swr: true, staleMaxAge: 300, storage: 'cache' });
|
|
189
|
+
*
|
|
190
|
+
* const getUser = cache.cached((id: string) => db.users.find(id), { name: 'getUser' });
|
|
191
|
+
*
|
|
192
|
+
* await getUser('123'); // resolves and stores
|
|
193
|
+
* await getUser('123'); // served from cache
|
|
194
|
+
* await getUser.invalidate('123'); // drops the entry
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
var CacheManager = class {
|
|
198
|
+
/** Container instance */
|
|
199
|
+
gContainer;
|
|
200
|
+
/** Logger instance */
|
|
201
|
+
gLogger;
|
|
202
|
+
/** Application wide defaults applied to every cached function */
|
|
203
|
+
fDefaults = {};
|
|
204
|
+
/** Storage adapter bridging the caching engine onto the mounted storages */
|
|
205
|
+
fAdapter = null;
|
|
206
|
+
/**
|
|
207
|
+
* Returns the storage adapter this manager installed into the caching engine.
|
|
208
|
+
*
|
|
209
|
+
* @returns {CacheStorageAdapter | null} The adapter, or null before initialization
|
|
210
|
+
*/
|
|
211
|
+
get adapter() {
|
|
212
|
+
return this.fAdapter;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Returns the currently configured defaults.
|
|
216
|
+
*
|
|
217
|
+
* @returns {CacheTypes.Defaults} A copy of the active defaults
|
|
218
|
+
*/
|
|
219
|
+
get defaults() {
|
|
220
|
+
return { ...this.fDefaults };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Sets the application wide cache defaults. Values passed per cached function
|
|
224
|
+
* always win over these. Calling it repeatedly merges into the existing defaults.
|
|
225
|
+
*
|
|
226
|
+
* @param {CacheTypes.Defaults} defaults - Defaults to apply to every cached function
|
|
227
|
+
* @returns {void}
|
|
228
|
+
*/
|
|
229
|
+
configure(defaults) {
|
|
230
|
+
this.fDefaults = {
|
|
231
|
+
...this.fDefaults,
|
|
232
|
+
...this.stripUndefined(defaults)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Wraps a function with caching.
|
|
237
|
+
*
|
|
238
|
+
* The returned function keeps the original signature and adds `resolveKeys()`,
|
|
239
|
+
* `invalidate()` and `expire()` helpers keyed exactly like the cached calls, so
|
|
240
|
+
* no cache key ever has to be rebuilt by hand.
|
|
241
|
+
*
|
|
242
|
+
* @template T - Return type of the wrapped function
|
|
243
|
+
* @template ArgsT - Argument tuple of the wrapped function
|
|
244
|
+
* @param {(...args: ArgsT) => T | Promise<T>} fn - The function to cache
|
|
245
|
+
* @param {CacheTypes.Options<T, ArgsT>} [options] - Per-function cache options
|
|
246
|
+
* @returns {CacheTypes.CachedFunction<T, ArgsT>} The cached function
|
|
247
|
+
*/
|
|
248
|
+
cached(fn, options = {}) {
|
|
249
|
+
if (typeof fn !== "function") throw new CacheError("Cached target must be a function", "cached", void 0, { received: typeof fn });
|
|
250
|
+
return defineCachedFunction(fn, this.resolveOptions(options));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Removes cached entries for the given arguments from every storage tier.
|
|
254
|
+
*
|
|
255
|
+
* Pass the same options (`name`, `group`, `storage`, `getKey`) the entry was
|
|
256
|
+
* cached with so the very same keys are resolved.
|
|
257
|
+
*
|
|
258
|
+
* @template ArgsT - Argument tuple the entry was cached under
|
|
259
|
+
* @param {CacheTypes.Options<any, ArgsT>} options - Options identifying the cached function
|
|
260
|
+
* @param {ArgsT} args - Arguments identifying the entry
|
|
261
|
+
* @returns {Promise<void>} Resolves once every tier has been cleared
|
|
262
|
+
*/
|
|
263
|
+
async invalidate(options, ...args) {
|
|
264
|
+
await invalidateCache({
|
|
265
|
+
options: this.resolveOptions(options),
|
|
266
|
+
args
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Marks cached entries as stale without removing them.
|
|
271
|
+
*
|
|
272
|
+
* With `swr` enabled the stale value keeps being served (within `staleMaxAge`)
|
|
273
|
+
* while the next access refreshes it in the background; without it, the next
|
|
274
|
+
* call re-resolves before returning.
|
|
275
|
+
*
|
|
276
|
+
* @template ArgsT - Argument tuple the entry was cached under
|
|
277
|
+
* @param {CacheTypes.Options<any, ArgsT>} options - Options identifying the cached function
|
|
278
|
+
* @param {ArgsT} args - Arguments identifying the entry
|
|
279
|
+
* @returns {Promise<void>} Resolves once every tier has been marked
|
|
280
|
+
*/
|
|
281
|
+
async expire(options, ...args) {
|
|
282
|
+
await expireCache({
|
|
283
|
+
options: this.resolveOptions(options),
|
|
284
|
+
args
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Resolves every storage key (one per storage tier) the given arguments cache under.
|
|
289
|
+
* Useful for debugging and for cache inspection tooling.
|
|
290
|
+
*
|
|
291
|
+
* @template ArgsT - Argument tuple the entry was cached under
|
|
292
|
+
* @param {CacheTypes.Options<any, ArgsT>} options - Options identifying the cached function
|
|
293
|
+
* @param {ArgsT} args - Arguments identifying the entry
|
|
294
|
+
* @returns {Promise<string[]>} The resolved storage keys
|
|
295
|
+
*/
|
|
296
|
+
async resolveKeys(options, ...args) {
|
|
297
|
+
return resolveCacheKeys({
|
|
298
|
+
options: this.resolveOptions(options),
|
|
299
|
+
args
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Merges the configured defaults into per-function options and translates the
|
|
304
|
+
* Vercube `storage` option into the key base the storage adapter routes on.
|
|
305
|
+
*
|
|
306
|
+
* @template T - Return type of the cached function
|
|
307
|
+
* @template ArgsT - Argument tuple of the cached function
|
|
308
|
+
* @param {CacheTypes.Options<T, ArgsT>} options - Per-function cache options
|
|
309
|
+
* @returns {CacheOptions<T, ArgsT>} Options understood by the caching engine
|
|
310
|
+
* @protected
|
|
311
|
+
*/
|
|
312
|
+
resolveOptions(options) {
|
|
313
|
+
const { storage, ...rest } = {
|
|
314
|
+
...this.fDefaults,
|
|
315
|
+
...this.stripUndefined(options)
|
|
316
|
+
};
|
|
317
|
+
const storages = Array.isArray(storage) ? storage : [storage];
|
|
318
|
+
if (storages.length === 0) throw new CacheError("At least one storage must be given when caching", "resolveOptions", void 0, { name: rest.name });
|
|
319
|
+
const base = storages.map((name) => cacheBaseForStorage(name));
|
|
320
|
+
return {
|
|
321
|
+
...rest,
|
|
322
|
+
base: base.length === 1 ? base[0] : base
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Drops keys whose value is `undefined`, so that an explicitly passed
|
|
327
|
+
* `undefined` never shadows a configured default with a missing value.
|
|
328
|
+
*
|
|
329
|
+
* @template T - Shape of the option object
|
|
330
|
+
* @param {T} options - Options to clean up
|
|
331
|
+
* @returns {Partial<T>} The options without undefined values
|
|
332
|
+
* @protected
|
|
333
|
+
*/
|
|
334
|
+
stripUndefined(options) {
|
|
335
|
+
return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== void 0));
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Installs the storage adapter into the caching engine.
|
|
339
|
+
* Called automatically with the `@Init()` decorator.
|
|
340
|
+
*
|
|
341
|
+
* @returns {void}
|
|
342
|
+
* @protected
|
|
343
|
+
*/
|
|
344
|
+
init() {
|
|
345
|
+
if (!this.gContainer.getOptional(StorageManager)) this.gLogger?.warn("Vercube/CacheManager::StorageManager is not registered - bind it so cached entries have a storage to live in");
|
|
346
|
+
if (useStorage() instanceof CacheStorageAdapter) this.gLogger?.warn("Vercube/CacheManager::another CacheManager is already installed in this process - cached functions from every container will now route through this one");
|
|
347
|
+
this.fAdapter = this.gContainer.resolve(CacheStorageAdapter);
|
|
348
|
+
setStorage(this.fAdapter);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
__decorate([Inject(Container)], CacheManager.prototype, "gContainer", void 0);
|
|
352
|
+
__decorate([InjectOptional(Logger)], CacheManager.prototype, "gLogger", void 0);
|
|
353
|
+
__decorate([Init()], CacheManager.prototype, "init", null);
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/Decorators/Cache.ts
|
|
356
|
+
/**
|
|
357
|
+
* Decorator implementation that replaces the decorated method with a cached
|
|
358
|
+
* version of itself.
|
|
359
|
+
*
|
|
360
|
+
* The wrapper is built lazily on the first call so that storages mounted after
|
|
361
|
+
* the container has been flushed - and defaults configured during bootstrap -
|
|
362
|
+
* are still picked up.
|
|
363
|
+
*/
|
|
364
|
+
var CacheDecorator = class extends BaseDecorator {
|
|
365
|
+
/** Cache manager owning the caching engine wiring */
|
|
366
|
+
gCacheManager;
|
|
367
|
+
/** The untouched method, kept so it can be restored on destroy */
|
|
368
|
+
fOriginalMethod = null;
|
|
369
|
+
/** Whether the method lived on the instance itself rather than on the prototype */
|
|
370
|
+
fOwnMethod = false;
|
|
371
|
+
/**
|
|
372
|
+
* Replaces the decorated method with its cached counterpart.
|
|
373
|
+
*
|
|
374
|
+
* @returns {void}
|
|
375
|
+
*/
|
|
376
|
+
created() {
|
|
377
|
+
const original = this.instance[this.propertyName];
|
|
378
|
+
if (typeof original !== "function") throw new CacheError("@Cache() can only be applied to methods", "decorate", void 0, {
|
|
379
|
+
property: this.propertyName,
|
|
380
|
+
received: typeof original
|
|
381
|
+
});
|
|
382
|
+
this.fOriginalMethod = original;
|
|
383
|
+
this.fOwnMethod = Object.hasOwn(this.instance, this.propertyName);
|
|
384
|
+
const options = {
|
|
385
|
+
name: `${this.instance?.constructor?.name ?? "anonymous"}.${this.propertyName}`,
|
|
386
|
+
integrity: hash([original, this.options ?? {}]),
|
|
387
|
+
...this.options
|
|
388
|
+
};
|
|
389
|
+
let cached = null;
|
|
390
|
+
const resolveCached = () => {
|
|
391
|
+
cached ??= this.gCacheManager.cached((...args) => original.apply(this.instance, args), options);
|
|
392
|
+
return cached;
|
|
393
|
+
};
|
|
394
|
+
const wrapper = (...args) => resolveCached()(...args);
|
|
395
|
+
wrapper.resolveKeys = (...args) => resolveCached().resolveKeys(...args);
|
|
396
|
+
wrapper.invalidate = (...args) => resolveCached().invalidate(...args);
|
|
397
|
+
wrapper.expire = (...args) => resolveCached().expire(...args);
|
|
398
|
+
this.instance[this.propertyName] = wrapper;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Drops the cached wrapper and puts the original method back in place.
|
|
402
|
+
*
|
|
403
|
+
* A method defined on the instance itself (a class field holding a function)
|
|
404
|
+
* is written back, while a prototype method is simply uncovered by removing
|
|
405
|
+
* the wrapper the decorator added to the instance.
|
|
406
|
+
*
|
|
407
|
+
* @returns {void}
|
|
408
|
+
*/
|
|
409
|
+
destroyed() {
|
|
410
|
+
if (!this.fOriginalMethod) return;
|
|
411
|
+
if (this.fOwnMethod) this.instance[this.propertyName] = this.fOriginalMethod;
|
|
412
|
+
else delete this.instance[this.propertyName];
|
|
413
|
+
this.fOriginalMethod = null;
|
|
414
|
+
this.fOwnMethod = false;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
__decorate([Inject(CacheManager)], CacheDecorator.prototype, "gCacheManager", void 0);
|
|
418
|
+
/**
|
|
419
|
+
* Caches the result of the decorated method.
|
|
420
|
+
*
|
|
421
|
+
* The cache key is derived from the method's arguments, so every distinct set of
|
|
422
|
+
* arguments gets its own entry. Concurrent calls for the same key are coalesced
|
|
423
|
+
* into a single execution, and entries are automatically dropped when the method
|
|
424
|
+
* body or its cache options change.
|
|
425
|
+
*
|
|
426
|
+
* The decorated method gains three helpers, keyed exactly like the cached calls:
|
|
427
|
+
* `resolveKeys(...args)`, `invalidate(...args)` and `expire(...args)`.
|
|
428
|
+
*
|
|
429
|
+
* @param {CacheTypes.DecoratorOptions} [options] - Cache options for this method
|
|
430
|
+
* @returns {Function} The method decorator
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* ```ts
|
|
434
|
+
* class UsersService {
|
|
435
|
+
* @Cache({ maxAge: 60, swr: true, staleMaxAge: 300, storage: 'redis' })
|
|
436
|
+
* public async getUser(id: string): Promise<User> {
|
|
437
|
+
* return this.database.findUser(id);
|
|
438
|
+
* }
|
|
439
|
+
* }
|
|
440
|
+
*
|
|
441
|
+
* await usersService.getUser('123');
|
|
442
|
+
* await (usersService.getUser as CacheTypes.CachedMethod<[string], User>).invalidate('123');
|
|
443
|
+
* ```
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```ts
|
|
447
|
+
* // arguments that are not safely hashable (Request, streams, class instances)
|
|
448
|
+
* // should be projected into an explicit key
|
|
449
|
+
* class ReportsController {
|
|
450
|
+
* @Cache({ maxAge: 300, getKey: (range: DateRange) => `${range.from}-${range.to}` })
|
|
451
|
+
* public async report(range: DateRange): Promise<Report> {
|
|
452
|
+
* return this.reports.build(range);
|
|
453
|
+
* }
|
|
454
|
+
* }
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
function Cache(options = {}) {
|
|
458
|
+
return createDecorator(CacheDecorator, options);
|
|
459
|
+
}
|
|
460
|
+
//#endregion
|
|
461
|
+
export { CACHE_BASE_PREFIX, Cache, CacheDecorator, CacheError, CacheManager, CacheStorageAdapter, cacheBaseForStorage, storageNameFromCacheKey };
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vercube/cache",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Cache module for Vercube framework",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/vercube/vercube.git",
|
|
8
|
+
"directory": "packages/cache"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.mjs",
|
|
14
|
+
"module": "./dist/index.mjs",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/index.mjs",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@vercube/core": "1.2.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"ocache": "0.2.0",
|
|
29
|
+
"ohash": "2.0.12",
|
|
30
|
+
"@vercube/di": "1.2.0",
|
|
31
|
+
"@vercube/storage": "1.2.0",
|
|
32
|
+
"@vercube/logger": "1.2.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsdown --config ../../tsdown.config.ts --config-loader=unrun"
|
|
39
|
+
}
|
|
40
|
+
}
|