@zudojs/cache 0.1.0 → 1.0.1
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 +90 -14
- package/dist/cache.d.ts +107 -18
- package/dist/cache.js +399 -33
- package/dist/constants.d.ts +46 -12
- package/dist/constants.js +47 -19
- package/dist/index.d.ts +10 -9
- package/dist/index.js +9 -8
- package/dist/invalidation.d.ts +23 -16
- package/dist/invalidation.js +43 -33
- package/dist/key-builder.d.ts +15 -0
- package/dist/key-builder.js +43 -3
- package/dist/lock.d.ts +36 -2
- package/dist/lock.js +160 -30
- package/dist/memory.d.ts +63 -7
- package/dist/memory.js +223 -39
- package/dist/metrics.d.ts +10 -0
- package/dist/metrics.js +49 -4
- package/dist/serializer.d.ts +19 -10
- package/dist/serializer.js +88 -21
- package/dist/store.d.ts +21 -11
- package/dist/store.js +125 -65
- package/dist/tags.d.ts +32 -12
- package/dist/tags.js +113 -33
- package/dist/types-adapter.d.ts +23 -9
- package/dist/types-config.d.ts +46 -9
- package/dist/types-health.d.ts +5 -2
- package/dist/types-keys.d.ts +8 -4
- package/dist/types-lock.d.ts +8 -0
- package/dist/types-operations.d.ts +13 -20
- package/dist/types-results.d.ts +7 -1
- package/dist/types-tags.d.ts +9 -0
- package/dist/types-utility.d.ts +6 -10
- package/dist/types-values.d.ts +9 -14
- package/dist/types.d.ts +6 -6
- package/dist/utils.d.ts +65 -0
- package/dist/utils.js +210 -0
- package/package.json +25 -14
- package/dist/.tsbuildinfo +0 -1
- package/dist/cache.d.ts.map +0 -1
- package/dist/cache.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invalidation.d.ts.map +0 -1
- package/dist/invalidation.js.map +0 -1
- package/dist/key-builder.d.ts.map +0 -1
- package/dist/key-builder.js.map +0 -1
- package/dist/lock.d.ts.map +0 -1
- package/dist/lock.js.map +0 -1
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js.map +0 -1
- package/dist/metrics.d.ts.map +0 -1
- package/dist/metrics.js.map +0 -1
- package/dist/serializer.d.ts.map +0 -1
- package/dist/serializer.js.map +0 -1
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js.map +0 -1
- package/dist/tags.d.ts.map +0 -1
- package/dist/tags.js.map +0 -1
- package/dist/types-adapter.d.ts.map +0 -1
- package/dist/types-adapter.js.map +0 -1
- package/dist/types-config.d.ts.map +0 -1
- package/dist/types-config.js.map +0 -1
- package/dist/types-events.d.ts.map +0 -1
- package/dist/types-events.js.map +0 -1
- package/dist/types-health.d.ts.map +0 -1
- package/dist/types-health.js.map +0 -1
- package/dist/types-keys.d.ts.map +0 -1
- package/dist/types-keys.js.map +0 -1
- package/dist/types-lock.d.ts.map +0 -1
- package/dist/types-lock.js.map +0 -1
- package/dist/types-metrics.d.ts.map +0 -1
- package/dist/types-metrics.js.map +0 -1
- package/dist/types-operations.d.ts.map +0 -1
- package/dist/types-operations.js.map +0 -1
- package/dist/types-results.d.ts.map +0 -1
- package/dist/types-results.js.map +0 -1
- package/dist/types-tags.d.ts.map +0 -1
- package/dist/types-tags.js.map +0 -1
- package/dist/types-utility.d.ts.map +0 -1
- package/dist/types-utility.js.map +0 -1
- package/dist/types-values.d.ts.map +0 -1
- package/dist/types-values.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zudojs Contributors
|
|
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zudojs/cache
|
|
2
2
|
|
|
3
|
-
Cache abstraction with memory adapter, tags, locking, and metrics for Zudojs applications.
|
|
3
|
+
Cache abstraction with a memory adapter, namespaced tags, locking, events, middleware, and metrics for Zudojs applications.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -11,27 +11,103 @@ npm install @zudojs/cache
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import {
|
|
14
|
+
import { createCacheService, createMemoryCacheAdapter } from "@zudojs/cache";
|
|
15
15
|
|
|
16
|
-
const cache =
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const cache = createCacheService({
|
|
17
|
+
adapter: createMemoryCacheAdapter({ maxEntries: 1000 }),
|
|
18
|
+
config: { defaultTtl: 60_000 },
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
await cache.set("user:123", user);
|
|
21
|
+
interface User {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
25
24
|
}
|
|
25
|
+
|
|
26
|
+
async function getUser(id: string): Promise<User> {
|
|
27
|
+
// `get` returns a result wrapper, not the value: check `hit`, read `value`.
|
|
28
|
+
const cached = await cache.get<User>(`user.${id}`);
|
|
29
|
+
if (cached.hit) return cached.value!;
|
|
30
|
+
|
|
31
|
+
const user = await fetchUserFromDb(id);
|
|
32
|
+
await cache.set(`user.${id}`, user, { tags: ["users"] });
|
|
33
|
+
return user;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Or let the cache do it, with stampede protection:
|
|
37
|
+
const { value, cached } = await cache.getOrSet<User>(`user.${id}`, () =>
|
|
38
|
+
fetchUserFromDb(id),
|
|
39
|
+
);
|
|
26
40
|
```
|
|
27
41
|
|
|
42
|
+
Key parts are validated individually and must match `/^[a-zA-Z0-9._-]+$/`, so
|
|
43
|
+
use `.` rather than the `:` separator inside a key (`user.123`, not
|
|
44
|
+
`user:123`) — `:` is reserved for the `prefix:namespace:key` structure the key
|
|
45
|
+
builder produces.
|
|
46
|
+
|
|
28
47
|
## Features
|
|
29
48
|
|
|
30
|
-
- Pluggable cache adapters (memory
|
|
31
|
-
- TTL and
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
- Cache
|
|
49
|
+
- Pluggable cache adapters (memory built in; the `CacheAdapter` contract fits Redis and friends)
|
|
50
|
+
- TTL and entry-count eviction, plus an approximate memory budget (`maxBytes`), with LRU ordering
|
|
51
|
+
- Namespaced cache tags for bulk invalidation
|
|
52
|
+
- Namespaced in-process locking with lease renewal (pluggable `CacheLockStore` for distributed backends)
|
|
53
|
+
- Cache events, middleware, hit ratios, latency percentiles and hot keys
|
|
54
|
+
|
|
55
|
+
## Multi-tenancy
|
|
56
|
+
|
|
57
|
+
`namespace` is a scope boundary, not a pattern. It is validated everywhere it
|
|
58
|
+
is used — including inside glob patterns — so an untrusted namespace can never
|
|
59
|
+
widen an operation:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
await cache.set("u1", user, { namespace: tenantId, tags: ["users"] });
|
|
63
|
+
|
|
64
|
+
// Only this tenant's entries — a tenantId of "*" is rejected, not honoured.
|
|
65
|
+
await cache.clear({ namespace: tenantId });
|
|
66
|
+
|
|
67
|
+
// Only this tenant's tagged entries.
|
|
68
|
+
await cache.invalidateByTag(["users"], { namespace: tenantId });
|
|
69
|
+
|
|
70
|
+
// Locks are namespaced and key-builder-qualified too.
|
|
71
|
+
await cache.withLock("import", runImport, { namespace: tenantId });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
In glob patterns, `*` matches within a single key segment and never crosses
|
|
75
|
+
the `:` separator; `**` as a whole segment spans namespaces deliberately.
|
|
76
|
+
|
|
77
|
+
## Observability
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const subscription = cache.subscribe("cache.miss", (event) => {
|
|
81
|
+
metrics.increment("cache.miss", { key: event.key });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
cache.getStats(); // { hits, misses, sets, deletes, errors, hitRate }
|
|
85
|
+
cache.getLatencyStats(CacheOperation.GET); // p50 / p95 / p99
|
|
86
|
+
cache.getHotKeys(10);
|
|
87
|
+
await cache.size();
|
|
88
|
+
|
|
89
|
+
subscription.unsubscribe();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Middlewares wrap every adapter operation and are configured on the service:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const cache = createCacheService({
|
|
96
|
+
adapter: createMemoryCacheAdapter(),
|
|
97
|
+
config: {
|
|
98
|
+
middlewares: [
|
|
99
|
+
async (ctx, next) => {
|
|
100
|
+
const span = tracer.start(ctx.operation);
|
|
101
|
+
try {
|
|
102
|
+
return await next(); // always return next()'s result
|
|
103
|
+
} finally {
|
|
104
|
+
span.end();
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
35
111
|
|
|
36
112
|
## Use Cases
|
|
37
113
|
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/cache — Cache Service
|
|
3
|
-
* High-level cache service combining adapter,
|
|
3
|
+
* High-level cache service combining adapter, key builder, tags,
|
|
4
|
+
* invalidation, locking, metrics, middleware, events, and an optional
|
|
5
|
+
* serializer.
|
|
6
|
+
*
|
|
7
|
+
* Serialization: when `config.serializer` is provided, values are
|
|
8
|
+
* serialized on set and deserialized on get, so cached values are
|
|
9
|
+
* structural copies. Without a serializer the memory adapter stores
|
|
10
|
+
* values by reference — mutations of a cached object are visible to
|
|
11
|
+
* later readers.
|
|
12
|
+
*
|
|
13
|
+
* Scoping: keys, tags and locks are all namespace-scoped. A namespace is
|
|
14
|
+
* validated as an identity part everywhere it is used (including in glob
|
|
15
|
+
* patterns), so it can never widen an operation beyond its own tenant.
|
|
4
16
|
*/
|
|
5
|
-
import type { CacheAdapter, CacheConfig, CacheDeleteResult, CacheGetResult, CacheHealth, CacheHealthChecker, CacheOrComputeOptions, CacheOrComputeResult, CacheSetResult, CacheStats, CacheTag } from "./types.js";
|
|
17
|
+
import type { CacheAdapter, CacheBatchOperation, CacheBatchResult, CacheConfig, CacheDeleteResult, CacheEvent, CacheEventHandler, CacheEventSubscription, CacheGetResult, CacheHealth, CacheHealthChecker, CacheNamespace, CacheOperation, CacheOrComputeOptions, CacheOrComputeResult, CacheSetResult, CacheStats, CacheTag, CacheTTL } from "./types.js";
|
|
6
18
|
import type { CacheKeyBuilder } from "./types-keys.js";
|
|
19
|
+
import { InMemoryCacheMetrics } from "./metrics.js";
|
|
20
|
+
/** Options accepted by every namespace-scoped read operation. */
|
|
21
|
+
interface NamespaceOptions {
|
|
22
|
+
readonly namespace?: CacheNamespace;
|
|
23
|
+
}
|
|
7
24
|
export declare class CacheService implements CacheHealthChecker {
|
|
8
25
|
private readonly store;
|
|
9
26
|
private readonly keyBuilder;
|
|
@@ -11,52 +28,124 @@ export declare class CacheService implements CacheHealthChecker {
|
|
|
11
28
|
private readonly invalidation;
|
|
12
29
|
private readonly lockManager;
|
|
13
30
|
private readonly metrics;
|
|
31
|
+
private readonly serializer;
|
|
14
32
|
private readonly defaultTtl;
|
|
15
33
|
private readonly enabled;
|
|
34
|
+
private readonly failSilently;
|
|
35
|
+
/** Namespace configured for this service; the default scope for keys, tags and locks. */
|
|
36
|
+
private readonly namespace;
|
|
37
|
+
private readonly separator;
|
|
38
|
+
/** In-flight getOrSet computations, keyed by full key (stampede protection). */
|
|
39
|
+
private readonly inFlight;
|
|
16
40
|
constructor(options: {
|
|
17
41
|
readonly adapter: CacheAdapter;
|
|
18
42
|
readonly config?: CacheConfig;
|
|
19
43
|
readonly keyBuilder?: CacheKeyBuilder;
|
|
20
44
|
});
|
|
21
|
-
get<TValue = unknown>(key: string, options?:
|
|
22
|
-
readonly namespace?: string;
|
|
23
|
-
}): Promise<CacheGetResult<TValue>>;
|
|
45
|
+
get<TValue = unknown>(key: string, options?: NamespaceOptions): Promise<CacheGetResult<TValue>>;
|
|
24
46
|
set<TValue = unknown>(key: string, value: TValue, options?: {
|
|
25
|
-
readonly ttl?:
|
|
47
|
+
readonly ttl?: CacheTTL;
|
|
26
48
|
readonly tags?: readonly CacheTag[];
|
|
27
|
-
readonly namespace?:
|
|
49
|
+
readonly namespace?: CacheNamespace;
|
|
50
|
+
readonly overwrite?: boolean;
|
|
51
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
28
52
|
}): Promise<CacheSetResult>;
|
|
29
|
-
delete(key: string, options?:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
delete(key: string, options?: NamespaceOptions): Promise<CacheDeleteResult>;
|
|
54
|
+
has(key: string, options?: NamespaceOptions): Promise<boolean>;
|
|
55
|
+
/**
|
|
56
|
+
* Clears cache entries.
|
|
57
|
+
* - No options: clears everything (and flushes the tag store).
|
|
58
|
+
* - `namespace` and/or `pattern`: builds a fully-qualified pattern via
|
|
59
|
+
* the key builder (prefix + namespace + pattern) so only matching
|
|
60
|
+
* entries are removed. Both parts are validated: a `namespace` of `"*"`
|
|
61
|
+
* is rejected rather than escaping its own scope.
|
|
62
|
+
*/
|
|
35
63
|
clear(options?: {
|
|
36
|
-
readonly namespace?:
|
|
64
|
+
readonly namespace?: CacheNamespace;
|
|
37
65
|
readonly pattern?: string;
|
|
38
66
|
}): Promise<{
|
|
39
67
|
readonly cleared: number;
|
|
40
68
|
}>;
|
|
69
|
+
/** Remaining TTL for a key (undefined = missing, null = never expires). */
|
|
70
|
+
ttl(key: string, options?: NamespaceOptions): Promise<number | null | undefined>;
|
|
71
|
+
/** Updates the TTL of an existing key. Returns false when unsupported or missing. */
|
|
72
|
+
expire(key: string, ttl: CacheTTL, options?: NamespaceOptions): Promise<boolean>;
|
|
41
73
|
getOrSet<TValue>(key: string, fn: () => Promise<TValue>, options?: CacheOrComputeOptions): Promise<CacheOrComputeResult<TValue>>;
|
|
42
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Invalidates every entry tagged with any of `tags`, within this
|
|
76
|
+
* service's namespace (or `options.namespace`). Tags registered under a
|
|
77
|
+
* different namespace are untouched.
|
|
78
|
+
*/
|
|
79
|
+
invalidateByTag(tags: readonly CacheTag[], options?: NamespaceOptions): Promise<{
|
|
43
80
|
readonly cleared: number;
|
|
44
81
|
}>;
|
|
45
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Invalidates entries matching a service-level glob pattern. The
|
|
84
|
+
* pattern is qualified with the key builder's prefix (and namespace,
|
|
85
|
+
* if configured), so `invalidateByPattern("user.*")` matches keys this
|
|
86
|
+
* service wrote via `set("user.1", ...)`.
|
|
87
|
+
*
|
|
88
|
+
* `*` never crosses the key separator, so a pattern cannot reach into a
|
|
89
|
+
* namespace the caller did not name. Use `**` as the pattern to span
|
|
90
|
+
* whole namespaces deliberately.
|
|
91
|
+
*/
|
|
92
|
+
invalidateByPattern(pattern: string, options?: NamespaceOptions): Promise<{
|
|
46
93
|
readonly cleared: number;
|
|
47
94
|
}>;
|
|
48
|
-
|
|
49
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Runs `fn` under a namespace-scoped, fully-qualified lock.
|
|
97
|
+
*
|
|
98
|
+
* The lock name goes through the key builder, so it is prefixed and
|
|
99
|
+
* namespaced exactly like a cache key and validated the same way — two
|
|
100
|
+
* tenants using the same lock name do not collide.
|
|
101
|
+
*
|
|
102
|
+
* The lease is renewed while `fn` runs and a lost lease throws (see
|
|
103
|
+
* `CacheLockManager.withLock`). `failSilently` deliberately does not apply:
|
|
104
|
+
* running a critical section without exclusion is never a safe
|
|
105
|
+
* degradation, and neither is running it while the cache is disabled.
|
|
106
|
+
*/
|
|
107
|
+
withLock<T>(key: string, fn: (signal: AbortSignal) => Promise<T>, options?: {
|
|
108
|
+
readonly ttl?: CacheTTL;
|
|
50
109
|
readonly retryAttempts?: number;
|
|
110
|
+
readonly namespace?: CacheNamespace;
|
|
51
111
|
}): Promise<T>;
|
|
112
|
+
/**
|
|
113
|
+
* Applies a sequence of operations, one at a time, returning one result
|
|
114
|
+
* per operation in submission order. A failing operation does not stop
|
|
115
|
+
* the batch; its error is reported on its own result.
|
|
116
|
+
*/
|
|
117
|
+
batch(operations: readonly CacheBatchOperation[], options?: NamespaceOptions): Promise<readonly CacheBatchResult[]>;
|
|
52
118
|
getStats(): CacheStats | null;
|
|
119
|
+
/** Number of live entries, when the underlying adapter can report it. */
|
|
120
|
+
size(): Promise<number | undefined>;
|
|
121
|
+
/** Latency percentiles for one operation. */
|
|
122
|
+
getLatencyStats(operation: CacheOperation): ReturnType<InMemoryCacheMetrics["getLatencyStats"]> | null;
|
|
123
|
+
/** Latency histogram for one operation. */
|
|
124
|
+
getLatencyHistogram(operation: CacheOperation): ReturnType<InMemoryCacheMetrics["getLatencyHistogram"]> | null;
|
|
125
|
+
/** The most-read keys currently tracked, hottest first. */
|
|
126
|
+
getHotKeys(topN?: number): ReturnType<InMemoryCacheMetrics["getHotKeys"]> | null;
|
|
127
|
+
/** Resets all collected metrics. */
|
|
128
|
+
resetStats(): void;
|
|
129
|
+
/**
|
|
130
|
+
* Subscribes to cache events (`cache.hit`, `cache.miss`, `cache.set`,
|
|
131
|
+
* `cache.delete`, `cache.clear`, `cache.error`), or to `"*"` for all.
|
|
132
|
+
*/
|
|
133
|
+
subscribe(eventType: CacheEvent["type"] | "*", handler: CacheEventHandler): CacheEventSubscription;
|
|
53
134
|
healthCheck(): Promise<CacheHealth>;
|
|
54
135
|
connect(): Promise<void>;
|
|
55
136
|
disconnect(): Promise<void>;
|
|
137
|
+
private applyBatchOperation;
|
|
138
|
+
/** The namespace scope applied to tag registrations and lookups. */
|
|
139
|
+
private tagScope;
|
|
140
|
+
private qualifyPattern;
|
|
141
|
+
private purgeTagsMatching;
|
|
142
|
+
private serialize;
|
|
143
|
+
private deserialize;
|
|
56
144
|
}
|
|
57
145
|
export declare function createCacheService(options: {
|
|
58
146
|
readonly adapter: CacheAdapter;
|
|
59
147
|
readonly config?: CacheConfig;
|
|
60
148
|
readonly keyBuilder?: CacheKeyBuilder;
|
|
61
149
|
}): CacheService;
|
|
150
|
+
export {};
|
|
62
151
|
//# sourceMappingURL=cache.d.ts.map
|