@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/dist/constants.js CHANGED
@@ -8,10 +8,13 @@
8
8
  /* -------------------------------------------------------------------------- */
9
9
  /** Default time-to-live in milliseconds (5 minutes). */
10
10
  export const DEFAULT_TTL_MS = 5 * 60 * 1000;
11
- /** Maximum supported TTL (24 hours). */
11
+ /** Maximum supported TTL (24 hours). TTLs above this are rejected. */
12
12
  export const MAX_TTL_MS = 24 * 60 * 60 * 1000;
13
- /** Minimum TTL (1 second). */
14
- export const MIN_TTL_MS = 1_000;
13
+ /**
14
+ * Minimum TTL (1 millisecond). TTLs below this (zero or negative) are
15
+ * rejected; use `ttl: null` for entries that never expire.
16
+ */
17
+ export const MIN_TTL_MS = 1;
15
18
  /* -------------------------------------------------------------------------- */
16
19
  /* Key Generation */
17
20
  /* -------------------------------------------------------------------------- */
@@ -21,8 +24,23 @@ export const DEFAULT_SEPARATOR = ":";
21
24
  export const DEFAULT_PREFIX = "zudojs";
22
25
  /** Maximum key length in characters. */
23
26
  export const MAX_KEY_LENGTH = 256;
24
- /** Pattern used to validate cache keys. */
25
- export const CACHE_KEY_PATTERN = /^[a-zA-Z0-9._\-:]+$/;
27
+ /**
28
+ * Pattern used to validate each individual cache key part (prefix,
29
+ * namespace, and raw key). Deliberately excludes the default separator
30
+ * (`:`) so callers cannot forge namespaced keys (e.g. `build("admin:x")`
31
+ * throws). Parts are additionally checked against the active separator.
32
+ */
33
+ export const CACHE_KEY_PATTERN = /^[a-zA-Z0-9._\-]+$/;
34
+ /**
35
+ * Pattern used to validate the caller-supplied *glob* segment of a key
36
+ * pattern. It is the key alphabet plus the two glob metacharacters `*` and
37
+ * `?` — nothing else has meaning for the matcher, so anything else is
38
+ * caller error. The separator is excluded so a pattern cannot escape the
39
+ * prefix/namespace scope it is composed into.
40
+ */
41
+ export const CACHE_PATTERN_PART_PATTERN = /^[a-zA-Z0-9._\-*?]+$/;
42
+ /** Maximum length of a cache tag. Tags are untrusted map keys. */
43
+ export const MAX_TAG_LENGTH = 128;
26
44
  /* -------------------------------------------------------------------------- */
27
45
  /* Lock Defaults */
28
46
  /* -------------------------------------------------------------------------- */
@@ -32,30 +50,40 @@ export const DEFAULT_LOCK_TTL_MS = 30_000;
32
50
  export const DEFAULT_LOCK_RETRY_ATTEMPTS = 3;
33
51
  /** Default delay between lock retry attempts in milliseconds. */
34
52
  export const DEFAULT_LOCK_RETRY_DELAY_MS = 100;
53
+ /**
54
+ * Divisor applied to a lock's TTL to derive its heartbeat interval, so a
55
+ * lease is renewed roughly three times per TTL window while the critical
56
+ * section runs.
57
+ */
58
+ export const LOCK_HEARTBEAT_DIVISOR = 3;
35
59
  /* -------------------------------------------------------------------------- */
36
60
  /* Metrics */
37
61
  /* -------------------------------------------------------------------------- */
38
62
  /** Maximum number of latency samples to keep per operation. */
39
63
  export const MAX_LATENCY_SAMPLES = 1_000;
40
- /** Bucket boundaries for latency histograms (ms). */
64
+ /** Maximum number of distinct keys tracked for hot-key metrics. */
65
+ export const MAX_TRACKED_KEYS = 1_024;
66
+ /** Bucket boundaries for latency histograms (ms). A +Infinity bucket is appended at query time. */
41
67
  export const LATENCY_BUCKETS = [1, 5, 10, 25, 50, 100, 250, 500, 1000];
42
68
  /* -------------------------------------------------------------------------- */
43
69
  /* Memory Adapter */
44
70
  /* -------------------------------------------------------------------------- */
45
71
  /** Default maximum number of entries for the in-memory adapter. */
46
72
  export const DEFAULT_MAX_ENTRIES = 10_000;
47
- /** Default maximum memory budget in bytes (50 MB). */
73
+ /**
74
+ * Default maximum memory budget in bytes (50 MB) for the in-memory adapter.
75
+ * Entry sizes are estimated (see `MemoryCacheAdapter`), so the budget is
76
+ * approximate; it exists to bound worst-case retention, not to be exact.
77
+ */
48
78
  export const DEFAULT_MAX_MEMORY_BYTES = 50 * 1024 * 1024;
49
- /* -------------------------------------------------------------------------- */
50
- /* Batch Operations */
51
- /* -------------------------------------------------------------------------- */
52
- /** Maximum number of keys in a single batch operation. */
53
- export const MAX_BATCH_SIZE = 100;
54
- /* -------------------------------------------------------------------------- */
55
- /* Patterns */
56
- /* -------------------------------------------------------------------------- */
57
- /** Glob pattern for matching all keys. */
58
- export const MATCH_ALL_PATTERN = "*";
59
- /** Regex pattern that matches valid namespace characters. */
60
- export const NAMESPACE_PATTERN = /^[a-zA-Z0-9._\-]+$/;
79
+ /**
80
+ * How often (at most) the in-memory adapter opportunistically purges expired
81
+ * entries on the overwrite path, where no eviction is otherwise triggered.
82
+ */
83
+ export const EXPIRED_PURGE_INTERVAL_MS = 30_000;
84
+ /**
85
+ * Maximum number of value nodes visited when estimating an entry's size.
86
+ * Keeps `set` O(1)-ish for large object graphs at the cost of accuracy.
87
+ */
88
+ export const SIZE_ESTIMATE_NODE_BUDGET = 512;
61
89
  //# sourceMappingURL=constants.js.map
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * @zudojs/cache
3
3
  *
4
4
  * Cache abstraction layer with memory adapter, tag-based
5
- * invalidation, distributed locking, and metrics.
5
+ * invalidation, in-process locking, events, middleware, and metrics.
6
6
  *
7
7
  * @example
8
8
  * ```ts
@@ -13,21 +13,22 @@
13
13
  * config: { defaultTtl: 60_000 },
14
14
  * });
15
15
  *
16
- * await cache.set("user:123", { name: "Alice" }, { tags: ["users"] });
17
- * const result = await cache.get("user:123");
16
+ * // Key parts must not contain the separator, so use "." inside a key.
17
+ * await cache.set("user.123", { name: "Alice" }, { tags: ["users"] });
18
+ * const { hit, value } = await cache.get<{ name: string }>("user.123");
18
19
  * ```
19
20
  */
20
- export type { CacheKey, CacheNamespace, CacheKeyParts, CacheKeyOptions, CacheValue, SerializableCacheValue, CacheTTL, CacheExpiration, CacheExpirationInfo, CacheSetOptions, CacheGetOptions, CacheDeleteOptions, CacheHasOptions, CacheClearOptions, CacheKeysOptions, CacheGetManyOptions, CacheSetManyOptions, CacheDeleteManyOptions, CacheEntry, CacheEntryMetadata, CacheGetResult, CacheSetResult, CacheDeleteResult, CacheDeleteManyResult, CacheClearResult, CacheStats, CacheAdapter, CacheStore, CacheAdapterFactory, CacheConfig, CacheEventType, CacheTag, CacheTagOptions, CacheTagStore, CacheLockOptions, CacheLock, CacheLockStore, CacheHealth, CacheHealthChecker, CacheSerializer, CacheMetrics, CacheOperation, CacheMiddlewareContext, CacheMiddleware, CacheSerializationOptions, CacheErrorCode, CacheOrComputeOptions, CacheOrComputeResult, CacheBatchOperation, CacheBatchResult, MaybePromise, CacheResult, } from "./types.js";
21
+ export type { CacheKey, CacheNamespace, CacheKeyOptions, CacheTTL, CacheExpiration, CacheSetOptions, CacheClearOptions, CacheKeysOptions, CacheSetManyOptions, CacheEntry, CacheGetResult, CacheSetResult, CacheDeleteResult, CacheDeleteManyResult, CacheClearResult, CacheStats, CacheAdapter, CacheStore, CacheConfig, CacheEventType, CacheTag, CacheTagOptions, CacheTagStore, CacheLockOptions, CacheLock, CacheLockStore, CacheHealth, CacheHealthChecker, CacheSerializer, CacheMetrics, CacheMiddlewareContext, CacheMiddleware, CacheSerializationOptions, CacheErrorCode, CacheOrComputeOptions, CacheOrComputeResult, CacheBatchOperation, CacheBatchResult, MaybePromise, } from "./types.js";
21
22
  export type { BaseCacheEvent, CacheHitEvent, CacheMissEvent, CacheSetEvent, CacheDeleteEvent, CacheClearEvent, CacheErrorEvent, CacheEvent, CacheEventHandler, CacheEventSubscription, } from "./types-events.js";
22
23
  export type { CacheKeyBuilder } from "./types-keys.js";
23
- export { DEFAULT_TTL_MS, MAX_TTL_MS, MIN_TTL_MS, DEFAULT_SEPARATOR, DEFAULT_PREFIX, MAX_KEY_LENGTH, CACHE_KEY_PATTERN, DEFAULT_LOCK_TTL_MS, DEFAULT_LOCK_RETRY_ATTEMPTS, DEFAULT_LOCK_RETRY_DELAY_MS, MAX_LATENCY_SAMPLES, LATENCY_BUCKETS, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_MEMORY_BYTES, MAX_BATCH_SIZE, MATCH_ALL_PATTERN, NAMESPACE_PATTERN, } from "./constants.js";
24
- export { CacheError, isCacheError, cacheConnectionError, cacheTimeoutError, cacheSerializationError, cacheDeserializationError, cacheInvalidKeyError, cacheAdapterNotConfiguredError, } from "./errors.js";
24
+ export { DEFAULT_TTL_MS, MAX_TTL_MS, MIN_TTL_MS, DEFAULT_SEPARATOR, DEFAULT_PREFIX, MAX_KEY_LENGTH, CACHE_KEY_PATTERN, DEFAULT_LOCK_TTL_MS, DEFAULT_LOCK_RETRY_ATTEMPTS, DEFAULT_LOCK_RETRY_DELAY_MS, MAX_LATENCY_SAMPLES, MAX_TRACKED_KEYS, LATENCY_BUCKETS, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_MEMORY_BYTES, EXPIRED_PURGE_INTERVAL_MS, CACHE_PATTERN_PART_PATTERN, MAX_TAG_LENGTH, } from "./constants.js";
25
+ export { CacheError, isCacheError, cacheConnectionError, cacheTimeoutError, cacheSerializationError, cacheDeserializationError, cacheInvalidKeyError, cacheAdapterNotConfiguredError, CacheOperation, } from "./errors.js";
25
26
  export type { CacheErrorOptions } from "./errors.js";
26
- export { JsonCacheSerializer, RawCacheSerializer, defaultSerializer, rawSerializer, getSerializer, } from "./serializer.js";
27
+ export { JsonCacheSerializer, RawCacheSerializer, defaultSerializer, rawSerializer, stripUnsafeKeys, } from "./serializer.js";
27
28
  export { DefaultKeyBuilder, createKeyBuilder, defaultKeyBuilder, } from "./key-builder.js";
28
29
  export { DefaultCacheStore, createCacheStore } from "./store.js";
29
- export { MemoryCacheAdapter, createMemoryCacheAdapter } from "./memory.js";
30
- export { InMemoryTagStore, createTagStore } from "./tags.js";
30
+ export { MemoryCacheAdapter, createMemoryCacheAdapter, estimateValueBytes, } from "./memory.js";
31
+ export { InMemoryTagStore, createTagStore, assertValidTag } from "./tags.js";
31
32
  export { CacheInvalidationManager, createInvalidationManager, } from "./invalidation.js";
32
33
  export { InMemoryLockStore, CacheLockManager, createLockManager, defaultLockStore, } from "./lock.js";
33
34
  export { InMemoryCacheMetrics, createCacheMetrics } from "./metrics.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * @zudojs/cache
3
3
  *
4
4
  * Cache abstraction layer with memory adapter, tag-based
5
- * invalidation, distributed locking, and metrics.
5
+ * invalidation, in-process locking, events, middleware, and metrics.
6
6
  *
7
7
  * @example
8
8
  * ```ts
@@ -13,24 +13,25 @@
13
13
  * config: { defaultTtl: 60_000 },
14
14
  * });
15
15
  *
16
- * await cache.set("user:123", { name: "Alice" }, { tags: ["users"] });
17
- * const result = await cache.get("user:123");
16
+ * // Key parts must not contain the separator, so use "." inside a key.
17
+ * await cache.set("user.123", { name: "Alice" }, { tags: ["users"] });
18
+ * const { hit, value } = await cache.get<{ name: string }>("user.123");
18
19
  * ```
19
20
  */
20
21
  // Constants
21
- export { DEFAULT_TTL_MS, MAX_TTL_MS, MIN_TTL_MS, DEFAULT_SEPARATOR, DEFAULT_PREFIX, MAX_KEY_LENGTH, CACHE_KEY_PATTERN, DEFAULT_LOCK_TTL_MS, DEFAULT_LOCK_RETRY_ATTEMPTS, DEFAULT_LOCK_RETRY_DELAY_MS, MAX_LATENCY_SAMPLES, LATENCY_BUCKETS, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_MEMORY_BYTES, MAX_BATCH_SIZE, MATCH_ALL_PATTERN, NAMESPACE_PATTERN, } from "./constants.js";
22
+ export { DEFAULT_TTL_MS, MAX_TTL_MS, MIN_TTL_MS, DEFAULT_SEPARATOR, DEFAULT_PREFIX, MAX_KEY_LENGTH, CACHE_KEY_PATTERN, DEFAULT_LOCK_TTL_MS, DEFAULT_LOCK_RETRY_ATTEMPTS, DEFAULT_LOCK_RETRY_DELAY_MS, MAX_LATENCY_SAMPLES, MAX_TRACKED_KEYS, LATENCY_BUCKETS, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_MEMORY_BYTES, EXPIRED_PURGE_INTERVAL_MS, CACHE_PATTERN_PART_PATTERN, MAX_TAG_LENGTH, } from "./constants.js";
22
23
  // Errors (re-exported from @zudojs/errors)
23
- export { CacheError, isCacheError, cacheConnectionError, cacheTimeoutError, cacheSerializationError, cacheDeserializationError, cacheInvalidKeyError, cacheAdapterNotConfiguredError, } from "./errors.js";
24
+ export { CacheError, isCacheError, cacheConnectionError, cacheTimeoutError, cacheSerializationError, cacheDeserializationError, cacheInvalidKeyError, cacheAdapterNotConfiguredError, CacheOperation, } from "./errors.js";
24
25
  // Serializer
25
- export { JsonCacheSerializer, RawCacheSerializer, defaultSerializer, rawSerializer, getSerializer, } from "./serializer.js";
26
+ export { JsonCacheSerializer, RawCacheSerializer, defaultSerializer, rawSerializer, stripUnsafeKeys, } from "./serializer.js";
26
27
  // Key Builder
27
28
  export { DefaultKeyBuilder, createKeyBuilder, defaultKeyBuilder, } from "./key-builder.js";
28
29
  // Store
29
30
  export { DefaultCacheStore, createCacheStore } from "./store.js";
30
31
  // Memory Adapter
31
- export { MemoryCacheAdapter, createMemoryCacheAdapter } from "./memory.js";
32
+ export { MemoryCacheAdapter, createMemoryCacheAdapter, estimateValueBytes, } from "./memory.js";
32
33
  // Tags
33
- export { InMemoryTagStore, createTagStore } from "./tags.js";
34
+ export { InMemoryTagStore, createTagStore, assertValidTag } from "./tags.js";
34
35
  // Invalidation
35
36
  export { CacheInvalidationManager, createInvalidationManager, } from "./invalidation.js";
36
37
  // Lock
@@ -4,8 +4,14 @@
4
4
  * Coordinates cache invalidation across tags, patterns, and keys.
5
5
  * Works with both the tag store and the cache adapter to ensure
6
6
  * consistent invalidation.
7
+ *
8
+ * The adapter passed in may be an instrumented `CacheStore` (as done by
9
+ * `CacheService`), in which case deletions flow through metrics/events.
10
+ * Adapters operate on fully-qualified keys and glob patterns only;
11
+ * namespace scoping is translated into patterns via the key builder.
7
12
  */
8
- import type { CacheAdapter, CacheClearResult, CacheKey, CacheNamespace, CacheTag, CacheTagStore } from "./types.js";
13
+ import type { CacheAdapter, CacheClearResult, CacheKey, CacheNamespace, CacheTag, CacheTagOptions, CacheTagStore } from "./types.js";
14
+ import type { CacheKeyBuilder } from "./types-keys.js";
9
15
  /**
10
16
  * Coordinates cache invalidation across multiple strategies:
11
17
  * - Tag-based invalidation
@@ -15,41 +21,41 @@ import type { CacheAdapter, CacheClearResult, CacheKey, CacheNamespace, CacheTag
15
21
  export declare class CacheInvalidationManager {
16
22
  private readonly adapter;
17
23
  private readonly tagStore;
24
+ private readonly keyBuilder?;
18
25
  constructor(options: {
19
26
  readonly adapter: CacheAdapter;
20
27
  readonly tagStore: CacheTagStore;
28
+ /** Used to translate namespaces into fully-qualified key patterns. */
29
+ readonly keyBuilder?: CacheKeyBuilder;
21
30
  });
22
31
  /**
23
- * Invalidates all cache entries associated with the given tags.
24
- * Returns the number of keys that were affected.
32
+ * Invalidates all cache entries associated with the given tags, within
33
+ * the given tag scope (namespace). Keys are de-duplicated across tags and
34
+ * only actual successful deletions are counted; keys that already expired
35
+ * or were evicted (dead tag mappings) are tolerated and simply skipped.
25
36
  */
26
- invalidateByTag(tags: readonly CacheTag[], options?: {
27
- readonly namespace?: CacheNamespace;
28
- }): Promise<CacheClearResult>;
37
+ invalidateByTag(tags: readonly CacheTag[], options?: CacheTagOptions): Promise<CacheClearResult>;
29
38
  /**
30
39
  * Invalidates all cache entries matching the given glob pattern.
40
+ * The pattern is matched against fully-qualified keys as-is; use
41
+ * `CacheService.invalidateByPattern` for prefix/namespace-aware patterns.
31
42
  */
32
- invalidateByPattern(pattern: string, options?: {
33
- readonly namespace?: CacheNamespace;
34
- }): Promise<CacheClearResult>;
43
+ invalidateByPattern(pattern: string): Promise<CacheClearResult>;
35
44
  /**
36
- * Invalidates all cache entries in the given namespace.
45
+ * Invalidates all cache entries in the given namespace by building a
46
+ * key pattern (`prefix:namespace:*`) instead of wiping the whole cache.
37
47
  */
38
48
  invalidateByNamespace(namespace: CacheNamespace): Promise<CacheClearResult>;
39
49
  /**
40
50
  * Invalidates a specific cache key.
41
51
  */
42
- invalidateKey(key: CacheKey, options?: {
43
- readonly namespace?: CacheNamespace;
44
- }): Promise<{
52
+ invalidateKey(key: CacheKey): Promise<{
45
53
  readonly deleted: boolean;
46
54
  }>;
47
55
  /**
48
56
  * Invalidates multiple cache keys at once.
49
57
  */
50
- invalidateKeys(keys: readonly CacheKey[], options?: {
51
- readonly namespace?: CacheNamespace;
52
- }): Promise<{
58
+ invalidateKeys(keys: readonly CacheKey[]): Promise<{
53
59
  readonly deleted: number;
54
60
  readonly keys: readonly CacheKey[];
55
61
  }>;
@@ -64,5 +70,6 @@ export declare class CacheInvalidationManager {
64
70
  export declare function createInvalidationManager(options: {
65
71
  readonly adapter: CacheAdapter;
66
72
  readonly tagStore: CacheTagStore;
73
+ readonly keyBuilder?: CacheKeyBuilder;
67
74
  }): CacheInvalidationManager;
68
75
  //# sourceMappingURL=invalidation.d.ts.map
@@ -4,7 +4,14 @@
4
4
  * Coordinates cache invalidation across tags, patterns, and keys.
5
5
  * Works with both the tag store and the cache adapter to ensure
6
6
  * consistent invalidation.
7
+ *
8
+ * The adapter passed in may be an instrumented `CacheStore` (as done by
9
+ * `CacheService`), in which case deletions flow through metrics/events.
10
+ * Adapters operate on fully-qualified keys and glob patterns only;
11
+ * namespace scoping is translated into patterns via the key builder.
7
12
  */
13
+ import { DEFAULT_SEPARATOR } from "./constants.js";
14
+ import { deleteManyViaDelete } from "./utils.js";
8
15
  /* -------------------------------------------------------------------------- */
9
16
  /* Invalidation Manager */
10
17
  /* -------------------------------------------------------------------------- */
@@ -17,67 +24,72 @@
17
24
  export class CacheInvalidationManager {
18
25
  adapter;
19
26
  tagStore;
27
+ keyBuilder;
20
28
  constructor(options) {
21
29
  this.adapter = options.adapter;
22
30
  this.tagStore = options.tagStore;
31
+ this.keyBuilder = options.keyBuilder;
23
32
  }
24
33
  /* ---- Tag Invalidation ---- */
25
34
  /**
26
- * Invalidates all cache entries associated with the given tags.
27
- * Returns the number of keys that were affected.
35
+ * Invalidates all cache entries associated with the given tags, within
36
+ * the given tag scope (namespace). Keys are de-duplicated across tags and
37
+ * only actual successful deletions are counted; keys that already expired
38
+ * or were evicted (dead tag mappings) are tolerated and simply skipped.
28
39
  */
29
40
  async invalidateByTag(tags, options) {
30
- let totalCleared = 0;
41
+ const keys = new Set();
42
+ for (const tag of tags) {
43
+ for (const key of await this.tagStore.getKeys(tag, options))
44
+ keys.add(key);
45
+ }
46
+ let cleared = 0;
47
+ for (const key of keys) {
48
+ const result = await this.adapter.delete(key);
49
+ if (result.deleted)
50
+ cleared++;
51
+ // The key is gone from the cache, so every tag mapping it still has
52
+ // (in any scope) is dead too.
53
+ this.tagStore.removeKey?.(key);
54
+ }
31
55
  for (const tag of tags) {
32
- const keys = await this.tagStore.getKeys(tag, options);
33
- for (const key of keys) {
34
- await this.adapter.delete(key, options);
35
- }
36
- const result = await this.tagStore.invalidate(tag, options);
37
- totalCleared += result.cleared;
56
+ await this.tagStore.invalidate(tag, options);
38
57
  }
39
- return { cleared: totalCleared };
58
+ return { cleared };
40
59
  }
41
60
  /* ---- Pattern Invalidation ---- */
42
61
  /**
43
62
  * Invalidates all cache entries matching the given glob pattern.
63
+ * The pattern is matched against fully-qualified keys as-is; use
64
+ * `CacheService.invalidateByPattern` for prefix/namespace-aware patterns.
44
65
  */
45
- async invalidateByPattern(pattern, options) {
46
- return this.adapter.clear({
47
- ...options,
48
- pattern,
49
- });
66
+ async invalidateByPattern(pattern) {
67
+ return this.adapter.clear({ pattern });
50
68
  }
51
69
  /* ---- Namespace Invalidation ---- */
52
70
  /**
53
- * Invalidates all cache entries in the given namespace.
71
+ * Invalidates all cache entries in the given namespace by building a
72
+ * key pattern (`prefix:namespace:*`) instead of wiping the whole cache.
54
73
  */
55
74
  async invalidateByNamespace(namespace) {
56
- return this.adapter.clear({ namespace });
75
+ const pattern = this.keyBuilder?.buildPattern?.("*", { namespace }) ??
76
+ `${namespace}${DEFAULT_SEPARATOR}*`;
77
+ return this.adapter.clear({ pattern });
57
78
  }
58
79
  /* ---- Direct Key Invalidation ---- */
59
80
  /**
60
81
  * Invalidates a specific cache key.
61
82
  */
62
- async invalidateKey(key, options) {
63
- const result = await this.adapter.delete(key, options);
83
+ async invalidateKey(key) {
84
+ const result = await this.adapter.delete(key);
64
85
  return { deleted: result.deleted };
65
86
  }
66
87
  /* ---- Bulk Invalidation ---- */
67
88
  /**
68
89
  * Invalidates multiple cache keys at once.
69
90
  */
70
- async invalidateKeys(keys, options) {
71
- let deleted = 0;
72
- const deletedKeys = [];
73
- for (const key of keys) {
74
- const result = await this.adapter.delete(key, options);
75
- if (result.deleted) {
76
- deleted++;
77
- deletedKeys.push(key);
78
- }
79
- }
80
- return { deleted, keys: deletedKeys };
91
+ async invalidateKeys(keys) {
92
+ return deleteManyViaDelete(keys, (key) => this.adapter.delete(key));
81
93
  }
82
94
  /* ---- Full Flush ---- */
83
95
  /**
@@ -85,9 +97,7 @@ export class CacheInvalidationManager {
85
97
  */
86
98
  async flushAll() {
87
99
  const result = await this.adapter.clear();
88
- if (typeof this.tagStore.clear === "function") {
89
- this.tagStore.clear();
90
- }
100
+ this.tagStore.clear?.();
91
101
  return result;
92
102
  }
93
103
  }
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * Builds fully qualified cache keys from key parts, namespaces,
5
5
  * and prefixes. Ensures keys are well-formed and consistently formatted.
6
+ *
7
+ * Each part (prefix, namespace, raw key) is validated individually with a
8
+ * pattern that excludes the separator character, so raw keys cannot forge
9
+ * namespaced/prefixed keys (e.g. `build("admin:x")` throws).
6
10
  */
7
11
  import type { CacheKey, CacheKeyOptions, CacheNamespace } from "./types.js";
8
12
  import type { CacheKeyBuilder } from "./types-keys.js";
@@ -21,7 +25,18 @@ export declare class DefaultKeyBuilder implements CacheKeyBuilder {
21
25
  readonly namespace?: CacheNamespace;
22
26
  });
23
27
  build(key: string, options?: CacheKeyOptions): CacheKey;
28
+ /**
29
+ * Builds a fully-qualified glob pattern.
30
+ *
31
+ * Prefix and namespace are *identity* parts — they are the scope boundary
32
+ * a pattern operation must stay inside — so they are validated exactly as
33
+ * `build()` validates them. A namespace of `"*"` is rejected rather than
34
+ * silently widening the pattern to every namespace. Only the trailing
35
+ * pattern segment may contain `*` and `?`.
36
+ */
37
+ buildPattern(pattern: string, options?: CacheKeyOptions): string;
24
38
  namespace(namespace: CacheNamespace): CacheKeyBuilder;
39
+ private validatePart;
25
40
  }
26
41
  /**
27
42
  * Creates a new `DefaultKeyBuilder` with the given options.
@@ -3,9 +3,14 @@
3
3
  *
4
4
  * Builds fully qualified cache keys from key parts, namespaces,
5
5
  * and prefixes. Ensures keys are well-formed and consistently formatted.
6
+ *
7
+ * Each part (prefix, namespace, raw key) is validated individually with a
8
+ * pattern that excludes the separator character, so raw keys cannot forge
9
+ * namespaced/prefixed keys (e.g. `build("admin:x")` throws).
6
10
  */
7
11
  import { CACHE_KEY_PATTERN, DEFAULT_PREFIX, DEFAULT_SEPARATOR, MAX_KEY_LENGTH, } from "./constants.js";
8
12
  import { cacheInvalidKeyError } from "./errors.js";
13
+ import { assertValidPatternPart } from "./utils.js";
9
14
  /* -------------------------------------------------------------------------- */
10
15
  /* Default Key Builder */
11
16
  /* -------------------------------------------------------------------------- */
@@ -27,6 +32,9 @@ export class DefaultKeyBuilder {
27
32
  const separator = options?.separator ?? this.globalSeparator;
28
33
  const namespace = options?.namespace ?? this.currentNamespace;
29
34
  const prefix = options?.prefix ?? this.globalPrefix;
35
+ if (key.length === 0) {
36
+ throw cacheInvalidKeyError(key, "Cache key must not be empty.");
37
+ }
30
38
  const parts = [];
31
39
  if (prefix) {
32
40
  parts.push(prefix);
@@ -35,15 +43,42 @@ export class DefaultKeyBuilder {
35
43
  parts.push(namespace);
36
44
  }
37
45
  parts.push(key);
46
+ for (const part of parts) {
47
+ this.validatePart(part, separator);
48
+ }
38
49
  const fullKey = parts.join(separator);
39
50
  if (fullKey.length > MAX_KEY_LENGTH) {
40
51
  throw cacheInvalidKeyError(fullKey, `Cache key exceeds maximum length of ${MAX_KEY_LENGTH} characters.`);
41
52
  }
42
- if (!CACHE_KEY_PATTERN.test(fullKey)) {
43
- throw cacheInvalidKeyError(fullKey);
44
- }
45
53
  return fullKey;
46
54
  }
55
+ /**
56
+ * Builds a fully-qualified glob pattern.
57
+ *
58
+ * Prefix and namespace are *identity* parts — they are the scope boundary
59
+ * a pattern operation must stay inside — so they are validated exactly as
60
+ * `build()` validates them. A namespace of `"*"` is rejected rather than
61
+ * silently widening the pattern to every namespace. Only the trailing
62
+ * pattern segment may contain `*` and `?`.
63
+ */
64
+ buildPattern(pattern, options) {
65
+ const separator = options?.separator ?? this.globalSeparator;
66
+ const namespace = options?.namespace ?? this.currentNamespace;
67
+ const prefix = options?.prefix ?? this.globalPrefix;
68
+ const identityParts = [];
69
+ if (prefix)
70
+ identityParts.push(prefix);
71
+ if (namespace)
72
+ identityParts.push(namespace);
73
+ for (const part of identityParts)
74
+ this.validatePart(part, separator);
75
+ assertValidPatternPart(pattern, separator);
76
+ const fullPattern = [...identityParts, pattern].join(separator);
77
+ if (fullPattern.length > MAX_KEY_LENGTH) {
78
+ throw cacheInvalidKeyError(fullPattern, `Cache pattern exceeds maximum length of ${MAX_KEY_LENGTH} characters.`);
79
+ }
80
+ return fullPattern;
81
+ }
47
82
  namespace(namespace) {
48
83
  return new DefaultKeyBuilder({
49
84
  prefix: this.globalPrefix,
@@ -51,6 +86,11 @@ export class DefaultKeyBuilder {
51
86
  namespace,
52
87
  });
53
88
  }
89
+ validatePart(part, separator) {
90
+ if (part.includes(separator) || !CACHE_KEY_PATTERN.test(part)) {
91
+ throw cacheInvalidKeyError(part, `Invalid cache key part "${part}": parts must match ${String(CACHE_KEY_PATTERN)} and must not contain the separator "${separator}".`);
92
+ }
93
+ }
54
94
  }
55
95
  /* -------------------------------------------------------------------------- */
56
96
  /* Factory */
package/dist/lock.d.ts CHANGED
@@ -1,11 +1,24 @@
1
1
  /**
2
2
  * @zudojs/cache — Lock Manager
3
- * Distributed lock manager for preventing concurrent cache operations.
3
+ *
4
+ * In-process lock manager with a pluggable `CacheLockStore`, for preventing
5
+ * concurrent cache operations. The bundled `InMemoryLockStore` holds leases
6
+ * in a single `Map` in a single process — it is NOT distributed. Pass a
7
+ * shared store (the exported `defaultLockStore` to share across services in
8
+ * one process, or a Redis-backed `CacheLockStore` to share across processes)
9
+ * via `CacheConfig.lockStore` when wider mutual exclusion is required.
10
+ *
11
+ * Leases are measured on a monotonic clock and are renewed by a heartbeat
12
+ * while the critical section runs, so a slow `fn()` does not silently lose
13
+ * its lock to expiry. If a lease is lost anyway, `withLock` aborts the
14
+ * signal it passed to `fn` and throws rather than reporting success.
4
15
  */
5
16
  import type { CacheLock, CacheLockOptions, CacheLockStore } from "./types.js";
6
17
  export declare class InMemoryLockStore implements CacheLockStore {
7
18
  private readonly locks;
8
19
  acquire(key: string, options?: CacheLockOptions): Promise<CacheLock | null>;
20
+ /** Removes all expired locks. Called opportunistically on each acquire. */
21
+ sweepExpired(now?: number): void;
9
22
  get size(): number;
10
23
  clear(): void;
11
24
  }
@@ -18,13 +31,34 @@ export declare class CacheLockManager {
18
31
  readonly retryAttempts?: number;
19
32
  readonly retryDelayMs?: number;
20
33
  });
34
+ /**
35
+ * Acquires a lock, retrying on contention.
36
+ *
37
+ * Returns `null` for ordinary contention (someone else holds it) and
38
+ * throws only when the *last* attempt failed with a store error — the two
39
+ * outcomes decide differently (retry vs. fail the request), so they are
40
+ * never conflated.
41
+ */
21
42
  acquire(key: string, options?: CacheLockOptions): Promise<CacheLock | null>;
22
- withLock<T>(key: string, fn: () => Promise<T>, options?: CacheLockOptions): Promise<T>;
43
+ /**
44
+ * Runs `fn` while holding the lock.
45
+ *
46
+ * The lease is renewed on an interval of roughly `ttl / 3` for as long as
47
+ * `fn` runs, so a critical section longer than the TTL does not silently
48
+ * lose mutual exclusion. If renewal or release reports that the lease is
49
+ * no longer ours, the `AbortSignal` passed to `fn` fires and the call
50
+ * throws — losing a lease is never reported as success.
51
+ */
52
+ withLock<T>(key: string, fn: (signal: AbortSignal) => Promise<T>, options?: CacheLockOptions): Promise<T>;
23
53
  }
24
54
  export declare function createLockManager(options?: {
25
55
  readonly store?: CacheLockStore;
26
56
  readonly retryAttempts?: number;
27
57
  readonly retryDelayMs?: number;
28
58
  }): CacheLockManager;
59
+ /**
60
+ * Process-wide lock store. Pass it as `CacheConfig.lockStore` when several
61
+ * `CacheService` instances in one process must share locks.
62
+ */
29
63
  export declare const defaultLockStore: InMemoryLockStore;
30
64
  //# sourceMappingURL=lock.d.ts.map