@zudojs/cache 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +90 -14
- package/dist/cache.d.ts +107 -18
- package/dist/cache.js +393 -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 +21 -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/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
|
-
/**
|
|
14
|
-
|
|
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
|
-
/**
|
|
25
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
export const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
export const
|
|
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,
|
|
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
|
-
*
|
|
17
|
-
*
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
*
|
|
17
|
-
*
|
|
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,
|
|
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,
|
|
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
|
package/dist/invalidation.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
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[]
|
|
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
|
package/dist/invalidation.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
63
|
-
const result = await this.adapter.delete(key
|
|
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
|
|
71
|
-
|
|
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
|
-
|
|
89
|
-
this.tagStore.clear();
|
|
90
|
-
}
|
|
100
|
+
this.tagStore.clear?.();
|
|
91
101
|
return result;
|
|
92
102
|
}
|
|
93
103
|
}
|
package/dist/key-builder.d.ts
CHANGED
|
@@ -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.
|
package/dist/key-builder.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|