@zudojs/cache 0.0.1 → 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.
Files changed (88) hide show
  1. package/README.md +90 -14
  2. package/dist/cache.d.ts +107 -18
  3. package/dist/cache.js +393 -33
  4. package/dist/constants.d.ts +46 -12
  5. package/dist/constants.js +47 -19
  6. package/dist/index.d.ts +10 -9
  7. package/dist/index.js +9 -8
  8. package/dist/invalidation.d.ts +23 -16
  9. package/dist/invalidation.js +43 -33
  10. package/dist/key-builder.d.ts +15 -0
  11. package/dist/key-builder.js +43 -3
  12. package/dist/lock.d.ts +36 -2
  13. package/dist/lock.js +160 -30
  14. package/dist/memory.d.ts +63 -7
  15. package/dist/memory.js +223 -39
  16. package/dist/metrics.d.ts +10 -0
  17. package/dist/metrics.js +49 -4
  18. package/dist/serializer.d.ts +19 -10
  19. package/dist/serializer.js +88 -21
  20. package/dist/store.d.ts +21 -11
  21. package/dist/store.js +125 -65
  22. package/dist/tags.d.ts +32 -12
  23. package/dist/tags.js +113 -33
  24. package/dist/types-adapter.d.ts +23 -9
  25. package/dist/types-config.d.ts +46 -9
  26. package/dist/types-health.d.ts +5 -2
  27. package/dist/types-keys.d.ts +8 -4
  28. package/dist/types-lock.d.ts +8 -0
  29. package/dist/types-operations.d.ts +13 -20
  30. package/dist/types-results.d.ts +7 -1
  31. package/dist/types-tags.d.ts +9 -0
  32. package/dist/types-utility.d.ts +6 -10
  33. package/dist/types-values.d.ts +9 -14
  34. package/dist/types.d.ts +6 -6
  35. package/dist/utils.d.ts +65 -0
  36. package/dist/utils.js +210 -0
  37. package/package.json +14 -7
  38. package/dist/.tsbuildinfo +0 -1
  39. package/dist/cache.d.ts.map +0 -1
  40. package/dist/cache.js.map +0 -1
  41. package/dist/constants.d.ts.map +0 -1
  42. package/dist/constants.js.map +0 -1
  43. package/dist/errors.d.ts.map +0 -1
  44. package/dist/errors.js.map +0 -1
  45. package/dist/index.d.ts.map +0 -1
  46. package/dist/index.js.map +0 -1
  47. package/dist/invalidation.d.ts.map +0 -1
  48. package/dist/invalidation.js.map +0 -1
  49. package/dist/key-builder.d.ts.map +0 -1
  50. package/dist/key-builder.js.map +0 -1
  51. package/dist/lock.d.ts.map +0 -1
  52. package/dist/lock.js.map +0 -1
  53. package/dist/memory.d.ts.map +0 -1
  54. package/dist/memory.js.map +0 -1
  55. package/dist/metrics.d.ts.map +0 -1
  56. package/dist/metrics.js.map +0 -1
  57. package/dist/serializer.d.ts.map +0 -1
  58. package/dist/serializer.js.map +0 -1
  59. package/dist/store.d.ts.map +0 -1
  60. package/dist/store.js.map +0 -1
  61. package/dist/tags.d.ts.map +0 -1
  62. package/dist/tags.js.map +0 -1
  63. package/dist/types-adapter.d.ts.map +0 -1
  64. package/dist/types-adapter.js.map +0 -1
  65. package/dist/types-config.d.ts.map +0 -1
  66. package/dist/types-config.js.map +0 -1
  67. package/dist/types-events.d.ts.map +0 -1
  68. package/dist/types-events.js.map +0 -1
  69. package/dist/types-health.d.ts.map +0 -1
  70. package/dist/types-health.js.map +0 -1
  71. package/dist/types-keys.d.ts.map +0 -1
  72. package/dist/types-keys.js.map +0 -1
  73. package/dist/types-lock.d.ts.map +0 -1
  74. package/dist/types-lock.js.map +0 -1
  75. package/dist/types-metrics.d.ts.map +0 -1
  76. package/dist/types-metrics.js.map +0 -1
  77. package/dist/types-operations.d.ts.map +0 -1
  78. package/dist/types-operations.js.map +0 -1
  79. package/dist/types-results.d.ts.map +0 -1
  80. package/dist/types-results.js.map +0 -1
  81. package/dist/types-tags.d.ts.map +0 -1
  82. package/dist/types-tags.js.map +0 -1
  83. package/dist/types-utility.d.ts.map +0 -1
  84. package/dist/types-utility.js.map +0 -1
  85. package/dist/types-values.d.ts.map +0 -1
  86. package/dist/types-values.js.map +0 -1
  87. package/dist/types.d.ts.map +0 -1
  88. package/dist/types.js.map +0 -1
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 { createMemoryCache } from "@zudojs/cache";
14
+ import { createCacheService, createMemoryCacheAdapter } from "@zudojs/cache";
15
15
 
16
- const cache = createMemoryCache({
17
- ttl: 60000,
18
- maxSize: 1000,
16
+ const cache = createCacheService({
17
+ adapter: createMemoryCacheAdapter({ maxEntries: 1000 }),
18
+ config: { defaultTtl: 60_000 },
19
19
  });
20
20
 
21
- const user = await cache.get("user:123");
22
- if (!user) {
23
- user = await fetchUserFromDb("123");
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, Redis, etc.)
31
- - TTL and size-based eviction
32
- - Cache tags for bulk invalidation
33
- - Distributed locking
34
- - Cache metrics and hit ratios
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, serializer, key builder, tags, invalidation, locking, and metrics.
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?: number;
47
+ readonly ttl?: CacheTTL;
26
48
  readonly tags?: readonly CacheTag[];
27
- readonly namespace?: string;
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
- readonly namespace?: string;
31
- }): Promise<CacheDeleteResult>;
32
- has(key: string, options?: {
33
- readonly namespace?: string;
34
- }): Promise<boolean>;
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?: string;
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
- invalidateByTag(tags: readonly CacheTag[]): Promise<{
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
- invalidateByPattern(pattern: string): Promise<{
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
- withLock<T>(key: string, fn: () => Promise<T>, options?: {
49
- readonly ttl?: number;
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