@zudojs/cache 1.2.2 → 1.2.4

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 (54) hide show
  1. package/README.md +21 -4
  2. package/dist/cache.d.ts +9 -3
  3. package/dist/cache.js +31 -25
  4. package/dist/constants.d.ts +12 -7
  5. package/dist/constants.js +14 -9
  6. package/dist/errors.d.ts +0 -1
  7. package/dist/errors.js +0 -1
  8. package/dist/index.d.ts +0 -1
  9. package/dist/index.js +0 -1
  10. package/dist/invalidation.d.ts +0 -1
  11. package/dist/invalidation.js +0 -1
  12. package/dist/key-builder.d.ts +0 -1
  13. package/dist/key-builder.js +9 -2
  14. package/dist/lock.d.ts +0 -1
  15. package/dist/lock.js +0 -1
  16. package/dist/memory.d.ts +0 -1
  17. package/dist/memory.js +0 -1
  18. package/dist/metrics.d.ts +0 -1
  19. package/dist/metrics.js +0 -1
  20. package/dist/serializer.d.ts +0 -1
  21. package/dist/serializer.js +0 -1
  22. package/dist/store.d.ts +0 -1
  23. package/dist/store.js +0 -1
  24. package/dist/tags.d.ts +0 -1
  25. package/dist/tags.js +0 -1
  26. package/dist/types-adapter.d.ts +8 -1
  27. package/dist/types-adapter.js +0 -1
  28. package/dist/types-config.d.ts +0 -1
  29. package/dist/types-config.js +0 -1
  30. package/dist/types-events.d.ts +0 -1
  31. package/dist/types-events.js +0 -1
  32. package/dist/types-health.d.ts +0 -1
  33. package/dist/types-health.js +0 -1
  34. package/dist/types-keys.d.ts +0 -1
  35. package/dist/types-keys.js +0 -1
  36. package/dist/types-lock.d.ts +0 -1
  37. package/dist/types-lock.js +0 -1
  38. package/dist/types-metrics.d.ts +0 -1
  39. package/dist/types-metrics.js +0 -1
  40. package/dist/types-operations.d.ts +0 -1
  41. package/dist/types-operations.js +0 -1
  42. package/dist/types-results.d.ts +0 -1
  43. package/dist/types-results.js +0 -1
  44. package/dist/types-tags.d.ts +0 -1
  45. package/dist/types-tags.js +0 -1
  46. package/dist/types-utility.d.ts +0 -1
  47. package/dist/types-utility.js +0 -1
  48. package/dist/types-values.d.ts +0 -1
  49. package/dist/types-values.js +0 -1
  50. package/dist/types.d.ts +0 -1
  51. package/dist/types.js +0 -1
  52. package/dist/utils.d.ts +9 -1
  53. package/dist/utils.js +34 -3
  54. package/package.json +5 -5
package/README.md CHANGED
@@ -45,10 +45,27 @@ const { value, cached } = await cache.getOrSet<User>(`user.${id}`, () =>
45
45
  );
46
46
  ```
47
47
 
48
- Key parts are validated individually and must match `/^[a-zA-Z0-9._-]+$/`, so
49
- use `.` rather than the `:` separator inside a key (`user.123`, not
50
- `user:123`) — `:` is reserved for the `prefix:namespace:key` structure the key
51
- builder produces.
48
+ Key parts (prefix, namespace, key) are validated individually: letters,
49
+ digits, `.`, `_`, `-` and `:`, minus the active separator. With the default
50
+ `:` separator that means no `:` inside a key — `prefix:namespace:key` is the
51
+ structure the key builder produces, so a key `a:b` would collide with key `b`
52
+ in namespace `a`, and `clear({ namespace: "a" })` would delete it. Put the
53
+ scope in `namespace` (`cache.get("dashboard", { namespace: tenantId })`), use
54
+ `.` or `-` inside a key (`user.123`, not `user:123`), or, if your keys must
55
+ contain `:` (keys built by another library, say), configure a different
56
+ separator on both the service and the memory adapter:
57
+
58
+ ```typescript
59
+ const cache = createCacheService({
60
+ adapter: createMemoryCacheAdapter({ separator: "/" }),
61
+ config: { separator: "/" },
62
+ });
63
+ await cache.set("tenant:kola:dashboard", totals); // zudojs/tenant:kola:dashboard
64
+ ```
65
+
66
+ A rejected key throws a `CacheError` (`ERR_INVALID_INPUT`, status 400) whose
67
+ `operation` names the call that rejected it (`get`, `set`, `lock_acquire`,
68
+ `clear`, …) and whose message explains the separator rule.
52
69
 
53
70
  ## Features
54
71
 
package/dist/cache.d.ts CHANGED
@@ -14,9 +14,10 @@
14
14
  * validated as an identity part everywhere it is used (including in glob
15
15
  * patterns), so it can never widen an operation beyond its own tenant.
16
16
  */
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";
17
+ import type { CacheAdapter, CacheBatchOperation, CacheBatchResult, CacheConfig, CacheDeleteResult, CacheEvent, CacheEventHandler, CacheEventSubscription, CacheGetResult, CacheHealth, CacheHealthChecker, CacheNamespace, CacheOrComputeOptions, CacheOrComputeResult, CacheSetResult, CacheStats, CacheTag, CacheTTL } from "./types.js";
18
18
  import type { CacheKeyBuilder } from "./types-keys.js";
19
19
  import { InMemoryCacheMetrics } from "./metrics.js";
20
+ import { CacheOperation } from "./errors.js";
20
21
  /** Options accepted by every namespace-scoped read operation. */
21
22
  interface NamespaceOptions {
22
23
  readonly namespace?: CacheNamespace;
@@ -44,6 +45,10 @@ export declare class CacheService implements CacheHealthChecker {
44
45
  readonly config?: CacheConfig;
45
46
  readonly keyBuilder?: CacheKeyBuilder;
46
47
  });
48
+ /**
49
+ * Reads one entry. `TValue` is an unchecked assertion — it only shapes
50
+ * the return type; nothing verifies the stored value matches it.
51
+ */
47
52
  get<TValue = unknown>(key: string, options?: NamespaceOptions): Promise<CacheGetResult<TValue>>;
48
53
  set<TValue = unknown>(key: string, value: TValue, options?: {
49
54
  readonly ttl?: CacheTTL;
@@ -150,7 +155,9 @@ export declare class CacheService implements CacheHealthChecker {
150
155
  /**
151
156
  * Runs input validation. A rejection is recorded like an adapter failure
152
157
  * (`getStats().errors`, `cache.error`) and rethrown: invalid input is a
153
- * caller error, so `failSilently` never hides it.
158
+ * caller error, so `failSilently` never hides it. The key builder does
159
+ * not know which operation asked for the key, so the rejection is
160
+ * attributed to `operation` here rather than reported as `unknown`.
154
161
  */
155
162
  private guardInput;
156
163
  private purgeTagsMatching;
@@ -163,4 +170,3 @@ export declare function createCacheService(options: {
163
170
  readonly keyBuilder?: CacheKeyBuilder;
164
171
  }): CacheService;
165
172
  export {};
166
- //# sourceMappingURL=cache.d.ts.map
package/dist/cache.js CHANGED
@@ -21,8 +21,8 @@ import { assertValidTag, createTagStore } from "./tags.js";
21
21
  import { CacheInvalidationManager, createInvalidationManager, } from "./invalidation.js";
22
22
  import { CacheLockManager, createLockManager } from "./lock.js";
23
23
  import { createCacheMetrics, InMemoryCacheMetrics } from "./metrics.js";
24
- import { CacheError, cacheDeserializationError, cacheSerializationError, isCacheError, } from "./errors.js";
25
- import { createGlobMatcher } from "./utils.js";
24
+ import { CacheError, CacheOperation, cacheDeserializationError, cacheSerializationError, isCacheError, } from "./errors.js";
25
+ import { attachCacheOperation, createGlobMatcher } from "./utils.js";
26
26
  export class CacheService {
27
27
  store;
28
28
  keyBuilder;
@@ -85,10 +85,14 @@ export class CacheService {
85
85
  });
86
86
  this.lockManager = createLockManager(options.config?.lockStore ? { store: options.config.lockStore } : {});
87
87
  }
88
+ /**
89
+ * Reads one entry. `TValue` is an unchecked assertion — it only shapes
90
+ * the return type; nothing verifies the stored value matches it.
91
+ */
88
92
  async get(key, options) {
89
93
  if (!this.enabled)
90
94
  return { hit: false, value: null };
91
- const fullKey = this.buildKey(key, options);
95
+ const fullKey = this.buildKey(key, options, CacheOperation.GET);
92
96
  try {
93
97
  const result = await this.store.get(fullKey);
94
98
  if (!result.hit || !this.serializer)
@@ -111,9 +115,9 @@ export class CacheService {
111
115
  async set(key, value, options) {
112
116
  if (!this.enabled)
113
117
  return { success: false, key, expiresAt: null };
114
- const fullKey = this.buildKey(key, options);
118
+ const fullKey = this.buildKey(key, options, CacheOperation.SET);
115
119
  if (options?.tags)
116
- this.validateTags(key, options.tags);
120
+ this.validateTags(key, options.tags, CacheOperation.SET);
117
121
  try {
118
122
  const stored = this.serializer ? this.serialize(value) : value;
119
123
  const result = await this.store.set(fullKey, stored, {
@@ -145,7 +149,7 @@ export class CacheService {
145
149
  async delete(key, options) {
146
150
  if (!this.enabled)
147
151
  return { deleted: false, key };
148
- const fullKey = this.buildKey(key, options);
152
+ const fullKey = this.buildKey(key, options, CacheOperation.DELETE);
149
153
  try {
150
154
  const result = await this.store.delete(fullKey);
151
155
  await this.tagStore.removeKey?.(fullKey);
@@ -160,7 +164,7 @@ export class CacheService {
160
164
  async has(key, options) {
161
165
  if (!this.enabled)
162
166
  return false;
163
- const fullKey = this.buildKey(key, options);
167
+ const fullKey = this.buildKey(key, options, CacheOperation.HAS);
164
168
  try {
165
169
  return await this.store.has(fullKey);
166
170
  }
@@ -184,7 +188,7 @@ export class CacheService {
184
188
  if (options?.pattern !== undefined || options?.namespace !== undefined) {
185
189
  // Validation happens outside the failSilently guard: a malformed
186
190
  // pattern is programmer error, not an adapter fault.
187
- const pattern = this.qualifyPattern(options.pattern ?? "*", options.namespace);
191
+ const pattern = this.qualifyPattern(options.pattern ?? "*", options.namespace, CacheOperation.CLEAR);
188
192
  try {
189
193
  const result = await this.store.clear({ pattern });
190
194
  await this.purgeTagsMatching(pattern);
@@ -214,7 +218,7 @@ export class CacheService {
214
218
  async ttl(key, options) {
215
219
  if (!this.enabled)
216
220
  return undefined;
217
- const fullKey = this.buildKey(key, options);
221
+ const fullKey = this.buildKey(key, options, CacheOperation.TTL);
218
222
  try {
219
223
  const remaining = await this.store.ttl?.(fullKey);
220
224
  return typeof remaining === "number" && Number.isFinite(remaining)
@@ -233,7 +237,7 @@ export class CacheService {
233
237
  async expire(key, ttl, options) {
234
238
  if (!this.enabled)
235
239
  return false;
236
- const fullKey = this.buildKey(key, options);
240
+ const fullKey = this.buildKey(key, options, CacheOperation.EXPIRE);
237
241
  try {
238
242
  return (await this.store.expire?.(fullKey, ttl)) ?? false;
239
243
  }
@@ -246,7 +250,7 @@ export class CacheService {
246
250
  async getOrSet(key, fn, options) {
247
251
  if (!this.enabled)
248
252
  return { value: await fn(), cached: false };
249
- const fullKey = this.buildKey(key, options);
253
+ const fullKey = this.buildKey(key, options, CacheOperation.GET);
250
254
  if (!options?.forceRefresh) {
251
255
  const cached = await this.get(key, options);
252
256
  if (cached.hit)
@@ -278,7 +282,7 @@ export class CacheService {
278
282
  async invalidateByTag(tags, options) {
279
283
  if (!this.enabled)
280
284
  return { cleared: 0 };
281
- this.validateTags(tags.join(","), tags);
285
+ this.validateTags(tags.join(","), tags, CacheOperation.DELETE_MANY);
282
286
  try {
283
287
  return await this.invalidation.invalidateByTag(tags, this.tagScope(options));
284
288
  }
@@ -301,7 +305,7 @@ export class CacheService {
301
305
  async invalidateByPattern(pattern, options) {
302
306
  if (!this.enabled)
303
307
  return { cleared: 0 };
304
- const qualified = this.qualifyPattern(pattern, options?.namespace);
308
+ const qualified = this.qualifyPattern(pattern, options?.namespace, CacheOperation.DELETE_MANY);
305
309
  try {
306
310
  const result = await this.invalidation.invalidateByPattern(qualified);
307
311
  await this.purgeTagsMatching(qualified);
@@ -332,7 +336,7 @@ export class CacheService {
332
336
  }
333
337
  const lockKey = this.buildKey(key, options?.namespace !== undefined
334
338
  ? { namespace: options.namespace }
335
- : undefined);
339
+ : undefined, CacheOperation.LOCK_ACQUIRE);
336
340
  return this.lockManager.withLock(lockKey, fn, {
337
341
  ...(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
338
342
  // `retryAttempts: 0` is honored (single attempt, no retries).
@@ -475,19 +479,19 @@ export class CacheService {
475
479
  assertNonEmptyNamespace(namespace);
476
480
  return namespace !== undefined ? { namespace } : {};
477
481
  }
478
- qualifyPattern(pattern, namespace) {
482
+ qualifyPattern(pattern, namespace, operation) {
479
483
  const build = this.keyBuilder.buildPattern?.bind(this.keyBuilder);
480
484
  if (!build)
481
485
  return pattern;
482
- return this.guardInput(pattern, () => build(pattern, namespace !== undefined ? { namespace } : undefined));
486
+ return this.guardInput(pattern, operation, () => build(pattern, namespace !== undefined ? { namespace } : undefined));
483
487
  }
484
488
  /** Builds a full key, counting a rejected key in the error stats. */
485
- buildKey(key, options) {
486
- return this.guardInput(key, () => this.keyBuilder.build(key, options));
489
+ buildKey(key, options, operation) {
490
+ return this.guardInput(key, operation, () => this.keyBuilder.build(key, options));
487
491
  }
488
492
  /** Validates tags, counting a rejected tag in the error stats. */
489
- validateTags(key, tags) {
490
- this.guardInput(key, () => {
493
+ validateTags(key, tags, operation) {
494
+ this.guardInput(key, operation, () => {
491
495
  for (const tag of tags)
492
496
  assertValidTag(tag);
493
497
  });
@@ -495,15 +499,18 @@ export class CacheService {
495
499
  /**
496
500
  * Runs input validation. A rejection is recorded like an adapter failure
497
501
  * (`getStats().errors`, `cache.error`) and rethrown: invalid input is a
498
- * caller error, so `failSilently` never hides it.
502
+ * caller error, so `failSilently` never hides it. The key builder does
503
+ * not know which operation asked for the key, so the rejection is
504
+ * attributed to `operation` here rather than reported as `unknown`.
499
505
  */
500
- guardInput(key, validate) {
506
+ guardInput(key, operation, validate) {
501
507
  try {
502
508
  return validate();
503
509
  }
504
510
  catch (error) {
505
- this.store.recordError(key, error);
506
- throw error;
511
+ const attributed = attachCacheOperation(error, operation);
512
+ this.store.recordError(key, attributed);
513
+ throw attributed;
507
514
  }
508
515
  }
509
516
  async purgeTagsMatching(pattern) {
@@ -540,4 +547,3 @@ export class CacheService {
540
547
  export function createCacheService(options) {
541
548
  return new CacheService(options);
542
549
  }
543
- //# sourceMappingURL=cache.js.map
@@ -19,18 +19,24 @@ export declare const DEFAULT_PREFIX = "zudojs";
19
19
  /** Maximum key length in characters. */
20
20
  export declare const MAX_KEY_LENGTH = 256;
21
21
  /**
22
- * Pattern used to validate each individual cache key part (prefix,
23
- * namespace, and raw key). Deliberately excludes the default separator
24
- * (`:`) so callers cannot forge namespaced keys (e.g. `build("admin:x")`
25
- * throws). Parts are additionally checked against the active separator.
22
+ * Alphabet of one cache key part (prefix, namespace, and raw key): letters,
23
+ * digits, `.`, `_`, `-` and `:`.
24
+ *
25
+ * The *active separator* is rejected separately, so with the default `:`
26
+ * separator a part can still never contain `:` — `build("admin:x")` would
27
+ * otherwise be indistinguishable from key `x` in namespace `admin`, and a
28
+ * namespace-scoped invalidation would reach it. `:` inside a part is only
29
+ * possible under a different separator
30
+ * (`createKeyBuilder({ separator: "/" })` gives `zudojs/tenant:1/dashboard`),
31
+ * where it cannot collide with the scope structure.
26
32
  */
27
33
  export declare const CACHE_KEY_PATTERN: RegExp;
28
34
  /**
29
35
  * Pattern used to validate the caller-supplied *glob* segment of a key
30
36
  * pattern. It is the key alphabet plus the two glob metacharacters `*` and
31
37
  * `?` — nothing else has meaning for the matcher, so anything else is
32
- * caller error. The separator is excluded so a pattern cannot escape the
33
- * prefix/namespace scope it is composed into.
38
+ * caller error. The active separator is rejected separately so a pattern
39
+ * cannot escape the prefix/namespace scope it is composed into.
34
40
  */
35
41
  export declare const CACHE_PATTERN_PART_PATTERN: RegExp;
36
42
  /** Maximum length of a cache tag. Tags are untrusted map keys. */
@@ -71,4 +77,3 @@ export declare const EXPIRED_PURGE_INTERVAL_MS = 30000;
71
77
  * Keeps `set` O(1)-ish for large object graphs at the cost of accuracy.
72
78
  */
73
79
  export declare const SIZE_ESTIMATE_NODE_BUDGET = 512;
74
- //# sourceMappingURL=constants.d.ts.map
package/dist/constants.js CHANGED
@@ -25,20 +25,26 @@ export const DEFAULT_PREFIX = "zudojs";
25
25
  /** Maximum key length in characters. */
26
26
  export const MAX_KEY_LENGTH = 256;
27
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.
28
+ * Alphabet of one cache key part (prefix, namespace, and raw key): letters,
29
+ * digits, `.`, `_`, `-` and `:`.
30
+ *
31
+ * The *active separator* is rejected separately, so with the default `:`
32
+ * separator a part can still never contain `:` — `build("admin:x")` would
33
+ * otherwise be indistinguishable from key `x` in namespace `admin`, and a
34
+ * namespace-scoped invalidation would reach it. `:` inside a part is only
35
+ * possible under a different separator
36
+ * (`createKeyBuilder({ separator: "/" })` gives `zudojs/tenant:1/dashboard`),
37
+ * where it cannot collide with the scope structure.
32
38
  */
33
- export const CACHE_KEY_PATTERN = /^[a-zA-Z0-9._\-]+$/;
39
+ export const CACHE_KEY_PATTERN = /^[a-zA-Z0-9._:\-]+$/;
34
40
  /**
35
41
  * Pattern used to validate the caller-supplied *glob* segment of a key
36
42
  * pattern. It is the key alphabet plus the two glob metacharacters `*` and
37
43
  * `?` — 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.
44
+ * caller error. The active separator is rejected separately so a pattern
45
+ * cannot escape the prefix/namespace scope it is composed into.
40
46
  */
41
- export const CACHE_PATTERN_PART_PATTERN = /^[a-zA-Z0-9._\-*?]+$/;
47
+ export const CACHE_PATTERN_PART_PATTERN = /^[a-zA-Z0-9._:\-*?]+$/;
42
48
  /** Maximum length of a cache tag. Tags are untrusted map keys. */
43
49
  export const MAX_TAG_LENGTH = 128;
44
50
  /* -------------------------------------------------------------------------- */
@@ -86,4 +92,3 @@ export const EXPIRED_PURGE_INTERVAL_MS = 30_000;
86
92
  * Keeps `set` O(1)-ish for large object graphs at the cost of accuracy.
87
93
  */
88
94
  export const SIZE_ESTIMATE_NODE_BUDGET = 512;
89
- //# sourceMappingURL=constants.js.map
package/dist/errors.d.ts CHANGED
@@ -6,4 +6,3 @@
6
6
  */
7
7
  export { CacheError, isCacheError, cacheConnectionError, cacheTimeoutError, cacheSerializationError, cacheDeserializationError, cacheInvalidKeyError, cacheAdapterNotConfiguredError, CacheOperation, } from "@zudojs/errors";
8
8
  export type { CacheErrorOptions } from "@zudojs/errors";
9
- //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js CHANGED
@@ -5,4 +5,3 @@
5
5
  * for convenience. No local error classes are created in this package.
6
6
  */
7
7
  export { CacheError, isCacheError, cacheConnectionError, cacheTimeoutError, cacheSerializationError, cacheDeserializationError, cacheInvalidKeyError, cacheAdapterNotConfiguredError, CacheOperation, } from "@zudojs/errors";
8
- //# sourceMappingURL=errors.js.map
package/dist/index.d.ts CHANGED
@@ -33,4 +33,3 @@ export { CacheInvalidationManager, createInvalidationManager, } from "./invalida
33
33
  export { InMemoryLockStore, CacheLockManager, createLockManager, defaultLockStore, } from "./lock.js";
34
34
  export { InMemoryCacheMetrics, createCacheMetrics } from "./metrics.js";
35
35
  export { CacheService, createCacheService } from "./cache.js";
36
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -40,4 +40,3 @@ export { InMemoryLockStore, CacheLockManager, createLockManager, defaultLockStor
40
40
  export { InMemoryCacheMetrics, createCacheMetrics } from "./metrics.js";
41
41
  // Cache Service
42
42
  export { CacheService, createCacheService } from "./cache.js";
43
- //# sourceMappingURL=index.js.map
@@ -72,4 +72,3 @@ export declare function createInvalidationManager(options: {
72
72
  readonly tagStore: CacheTagStore;
73
73
  readonly keyBuilder?: CacheKeyBuilder;
74
74
  }): CacheInvalidationManager;
75
- //# sourceMappingURL=invalidation.d.ts.map
@@ -110,4 +110,3 @@ export class CacheInvalidationManager {
110
110
  export function createInvalidationManager(options) {
111
111
  return new CacheInvalidationManager(options);
112
112
  }
113
- //# sourceMappingURL=invalidation.js.map
@@ -60,4 +60,3 @@ export declare function createKeyBuilder(options?: {
60
60
  }): DefaultKeyBuilder;
61
61
  /** Default key builder singleton. */
62
62
  export declare const defaultKeyBuilder: DefaultKeyBuilder;
63
- //# sourceMappingURL=key-builder.d.ts.map
@@ -104,7 +104,15 @@ export class DefaultKeyBuilder {
104
104
  });
105
105
  }
106
106
  validatePart(part, separator) {
107
- if (part.includes(separator) || !CACHE_KEY_PATTERN.test(part)) {
107
+ if (part.includes(separator)) {
108
+ throw cacheInvalidKeyError(part, `Invalid cache key part "${part}": parts must not contain the ` +
109
+ `separator "${separator}", which delimits prefix, namespace and ` +
110
+ `key (a key "a${separator}b" would collide with key "b" in ` +
111
+ `namespace "a"). Put the scope in the namespace option ` +
112
+ `(cache.get("b", { namespace: "a" })), use "." or "-" inside ` +
113
+ `the part, or configure a different separator on the key builder.`);
114
+ }
115
+ if (!CACHE_KEY_PATTERN.test(part)) {
108
116
  throw cacheInvalidKeyError(part, `Invalid cache key part "${part}": parts must match ${String(CACHE_KEY_PATTERN)} and must not contain the separator "${separator}".`);
109
117
  }
110
118
  }
@@ -120,4 +128,3 @@ export function createKeyBuilder(options) {
120
128
  }
121
129
  /** Default key builder singleton. */
122
130
  export const defaultKeyBuilder = new DefaultKeyBuilder();
123
- //# sourceMappingURL=key-builder.js.map
package/dist/lock.d.ts CHANGED
@@ -61,4 +61,3 @@ export declare function createLockManager(options?: {
61
61
  * `CacheService` instances in one process must share locks.
62
62
  */
63
63
  export declare const defaultLockStore: InMemoryLockStore;
64
- //# sourceMappingURL=lock.d.ts.map
package/dist/lock.js CHANGED
@@ -259,4 +259,3 @@ export function createLockManager(options) {
259
259
  * `CacheService` instances in one process must share locks.
260
260
  */
261
261
  export const defaultLockStore = new InMemoryLockStore();
262
- //# sourceMappingURL=lock.js.map
package/dist/memory.d.ts CHANGED
@@ -90,4 +90,3 @@ export declare function createMemoryCacheAdapter(options?: {
90
90
  readonly defaultTtl?: CacheTTL;
91
91
  readonly separator?: string;
92
92
  }): MemoryCacheAdapter;
93
- //# sourceMappingURL=memory.d.ts.map
package/dist/memory.js CHANGED
@@ -325,4 +325,3 @@ export class MemoryCacheAdapter {
325
325
  export function createMemoryCacheAdapter(options) {
326
326
  return new MemoryCacheAdapter(options);
327
327
  }
328
- //# sourceMappingURL=memory.js.map
package/dist/metrics.d.ts CHANGED
@@ -48,4 +48,3 @@ export declare class InMemoryCacheMetrics implements CacheMetrics {
48
48
  reset(): void;
49
49
  }
50
50
  export declare function createCacheMetrics(): InMemoryCacheMetrics;
51
- //# sourceMappingURL=metrics.d.ts.map
package/dist/metrics.js CHANGED
@@ -138,4 +138,3 @@ function percentile(sorted, p) {
138
138
  export function createCacheMetrics() {
139
139
  return new InMemoryCacheMetrics();
140
140
  }
141
- //# sourceMappingURL=metrics.js.map
@@ -53,4 +53,3 @@ export declare class RawCacheSerializer implements CacheSerializer<unknown, unkn
53
53
  export declare const defaultSerializer: JsonCacheSerializer;
54
54
  /** Default raw (pass-through) serializer instance. */
55
55
  export declare const rawSerializer: RawCacheSerializer;
56
- //# sourceMappingURL=serializer.d.ts.map
@@ -143,4 +143,3 @@ export class RawCacheSerializer {
143
143
  export const defaultSerializer = new JsonCacheSerializer();
144
144
  /** Default raw (pass-through) serializer instance. */
145
145
  export const rawSerializer = new RawCacheSerializer();
146
- //# sourceMappingURL=serializer.js.map
package/dist/store.d.ts CHANGED
@@ -55,4 +55,3 @@ export declare function createCacheStore(options: {
55
55
  readonly metrics?: CacheMetrics;
56
56
  readonly middlewares?: readonly CacheMiddleware[];
57
57
  }): DefaultCacheStore;
58
- //# sourceMappingURL=store.d.ts.map
package/dist/store.js CHANGED
@@ -246,4 +246,3 @@ export class DefaultCacheStore {
246
246
  export function createCacheStore(options) {
247
247
  return new DefaultCacheStore(options);
248
248
  }
249
- //# sourceMappingURL=store.js.map
package/dist/tags.d.ts CHANGED
@@ -54,4 +54,3 @@ export declare class InMemoryTagStore implements CacheTagStore {
54
54
  * Creates an in-memory tag store.
55
55
  */
56
56
  export declare function createTagStore(): InMemoryTagStore;
57
- //# sourceMappingURL=tags.d.ts.map
package/dist/tags.js CHANGED
@@ -171,4 +171,3 @@ export class InMemoryTagStore {
171
171
  export function createTagStore() {
172
172
  return new InMemoryTagStore();
173
173
  }
174
- //# sourceMappingURL=tags.js.map
@@ -13,6 +13,14 @@ export interface CacheAdapter {
13
13
  readonly name: string;
14
14
  connect?(): Promise<void>;
15
15
  disconnect?(): Promise<void>;
16
+ /**
17
+ * Reads one entry.
18
+ *
19
+ * `TValue` appears only in the return type, so it is an **unchecked
20
+ * assertion** by the caller: nothing verifies that the stored value has
21
+ * that shape. Validate a value that crosses a trust boundary (a shared
22
+ * cache, a schema that changed between deploys) before relying on it.
23
+ */
16
24
  get<TValue = unknown>(key: CacheKey): Promise<CacheGetResult<TValue>>;
17
25
  set<TValue = unknown>(key: CacheKey, value: TValue, options?: CacheSetOptions): Promise<CacheSetResult>;
18
26
  delete(key: CacheKey): Promise<CacheDeleteResult>;
@@ -35,4 +43,3 @@ export interface CacheAdapter {
35
43
  }
36
44
  export interface CacheStore extends CacheAdapter {
37
45
  }
38
- //# sourceMappingURL=types-adapter.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-adapter.js.map
@@ -64,4 +64,3 @@ export interface CacheConfig {
64
64
  * generic `ErrorCode` values, not these.
65
65
  */
66
66
  export type CacheErrorCode = "CACHE_DISABLED" | "CACHE_OPERATION_FAILED" | "CACHE_INVALID_TTL" | "CACHE_MIDDLEWARE_RESULT_MISSING" | "CACHE_LOCK_UNAVAILABLE" | "CACHE_LOCK_ACQUIRE_FAILED" | "CACHE_LOCK_LOST";
67
- //# sourceMappingURL=types-config.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-config.js.map
@@ -35,4 +35,3 @@ export type CacheEventHandler<TEvent extends CacheEvent = CacheEvent> = (event:
35
35
  export interface CacheEventSubscription {
36
36
  readonly unsubscribe: () => void;
37
37
  }
38
- //# sourceMappingURL=types-events.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-events.js.map
@@ -20,4 +20,3 @@ export interface CacheSerializer<TValue = unknown, TSerialized = unknown> {
20
20
  export interface CacheSerializationOptions {
21
21
  readonly serializer?: CacheSerializer;
22
22
  }
23
- //# sourceMappingURL=types-health.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-health.js.map
@@ -16,4 +16,3 @@ export interface CacheKeyBuilder {
16
16
  */
17
17
  buildPattern?(pattern: string, options?: CacheKeyOptions): string;
18
18
  }
19
- //# sourceMappingURL=types-keys.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-keys.js.map
@@ -27,4 +27,3 @@ export interface CacheLockStore {
27
27
  /** Releases every held lease. Intended for tests and shutdown. */
28
28
  clear?(): void;
29
29
  }
30
- //# sourceMappingURL=types-lock.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-lock.js.map
@@ -16,4 +16,3 @@ export interface CacheMiddlewareContext {
16
16
  readonly metadata?: Readonly<Record<string, unknown>>;
17
17
  }
18
18
  export type CacheMiddleware = (context: CacheMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>;
19
- //# sourceMappingURL=types-metrics.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-metrics.js.map
@@ -29,4 +29,3 @@ export interface CacheSetManyOptions {
29
29
  readonly ttl?: CacheTTL;
30
30
  readonly overwrite?: boolean;
31
31
  }
32
- //# sourceMappingURL=types-operations.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-operations.js.map
@@ -36,4 +36,3 @@ export interface CacheStats {
36
36
  readonly errors: number;
37
37
  readonly hitRate: number;
38
38
  }
39
- //# sourceMappingURL=types-results.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-results.js.map
@@ -30,4 +30,3 @@ export interface CacheTagStore {
30
30
  */
31
31
  trackedKeys?(): readonly CacheKey[] | Promise<readonly CacheKey[]>;
32
32
  }
33
- //# sourceMappingURL=types-tags.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-tags.js.map
@@ -24,4 +24,3 @@ export interface CacheBatchResult {
24
24
  readonly result?: unknown;
25
25
  readonly error?: unknown;
26
26
  }
27
- //# sourceMappingURL=types-utility.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-utility.js.map
@@ -16,4 +16,3 @@ export interface CacheEntry<TValue = unknown> {
16
16
  readonly tags?: readonly string[];
17
17
  readonly metadata?: Readonly<Record<string, unknown>>;
18
18
  }
19
- //# sourceMappingURL=types-values.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types-values.js.map
package/dist/types.d.ts CHANGED
@@ -10,4 +10,3 @@ export type { CacheHealth, CacheHealthChecker, CacheSerializer, CacheSerializati
10
10
  export type { CacheMiddleware, CacheMiddlewareContext, CacheMetrics, CacheOperation, } from "./types-metrics.js";
11
11
  export type { CacheConfig, CacheErrorCode } from "./types-config.js";
12
12
  export type { CacheBatchOperation, CacheBatchResult, CacheOrComputeOptions, CacheOrComputeResult, MaybePromise, } from "./types-utility.js";
13
- //# sourceMappingURL=types.d.ts.map
package/dist/types.js CHANGED
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types.js.map
package/dist/utils.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * TTL/pattern validation, and a monotonic clock.
6
6
  */
7
7
  import type { CacheDeleteManyResult, CacheDeleteResult, CacheKey, CacheTTL } from "./types.js";
8
+ import { CacheOperation } from "./errors.js";
8
9
  /**
9
10
  * Monotonic milliseconds since process start. Unlike `Date.now()` this is
10
11
  * unaffected by NTP steps, VM resume, or manual clock changes, so deadlines
@@ -62,4 +63,11 @@ export declare function deleteManyViaDelete(keys: readonly CacheKey[], deleteOne
62
63
  * `CACHE_INVALID_TTL` code otherwise.
63
64
  */
64
65
  export declare function assertValidTtl(ttl: CacheTTL | undefined): void;
65
- //# sourceMappingURL=utils.d.ts.map
66
+ /**
67
+ * Returns `error` with its `operation` filled in when it is a `CacheError`
68
+ * raised before the operation was known — key, namespace, pattern and tag
69
+ * validation happens in the key builder, which has no idea whether a
70
+ * `get` or a `set` asked for the key. Anything else is returned unchanged.
71
+ * The original stack is kept so the throw site stays visible.
72
+ */
73
+ export declare function attachCacheOperation(error: unknown, operation: CacheOperation): unknown;
package/dist/utils.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * TTL/pattern validation, and a monotonic clock.
6
6
  */
7
7
  import { CACHE_PATTERN_PART_PATTERN, DEFAULT_SEPARATOR, MAX_KEY_LENGTH, MAX_TTL_MS, MIN_TTL_MS, } from "./constants.js";
8
- import { CacheError, cacheInvalidKeyError } from "./errors.js";
8
+ import { CacheError, CacheOperation, cacheInvalidKeyError, isCacheError, } from "./errors.js";
9
9
  /* -------------------------------------------------------------------------- */
10
10
  /* Monotonic clock */
11
11
  /* -------------------------------------------------------------------------- */
@@ -164,7 +164,12 @@ export function assertValidPatternPart(part, separator) {
164
164
  if (part.length === 0) {
165
165
  throw cacheInvalidKeyError(part, "Cache pattern must not be empty.");
166
166
  }
167
- if (part.includes(separator) || !CACHE_PATTERN_PART_PATTERN.test(part)) {
167
+ if (part.includes(separator)) {
168
+ throw cacheInvalidKeyError(part, `Invalid cache pattern "${part}": patterns must not contain the ` +
169
+ `separator "${separator}"; scope a pattern with the namespace ` +
170
+ `option instead of writing the namespace into it.`);
171
+ }
172
+ if (!CACHE_PATTERN_PART_PATTERN.test(part)) {
168
173
  throw cacheInvalidKeyError(part, `Invalid cache pattern "${part}": patterns must match ${String(CACHE_PATTERN_PART_PATTERN)} and must not contain the separator "${separator}".`);
169
174
  }
170
175
  }
@@ -207,4 +212,30 @@ export function assertValidTtl(ttl) {
207
212
  });
208
213
  }
209
214
  }
210
- //# sourceMappingURL=utils.js.map
215
+ /**
216
+ * Returns `error` with its `operation` filled in when it is a `CacheError`
217
+ * raised before the operation was known — key, namespace, pattern and tag
218
+ * validation happens in the key builder, which has no idea whether a
219
+ * `get` or a `set` asked for the key. Anything else is returned unchanged.
220
+ * The original stack is kept so the throw site stays visible.
221
+ */
222
+ export function attachCacheOperation(error, operation) {
223
+ if (!isCacheError(error) || error.operation !== CacheOperation.UNKNOWN) {
224
+ return error;
225
+ }
226
+ const attributed = new CacheError(error.message, {
227
+ code: error.code,
228
+ category: error.category,
229
+ severity: error.severity,
230
+ operation,
231
+ key: error.key,
232
+ adapter: error.adapter,
233
+ statusCode: error.statusCode,
234
+ expose: error.expose,
235
+ isOperational: error.isOperational,
236
+ metadata: error.metadata,
237
+ cause: error.cause,
238
+ });
239
+ attributed.stack = error.stack;
240
+ return attributed;
241
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/cache",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Caching primitives, abstractions, and adapters for the Zudojs framework.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -24,10 +24,10 @@
24
24
  "!dist/.tsbuildinfo"
25
25
  ],
26
26
  "dependencies": {
27
- "@zudojs/constants": "1.1.3",
28
- "@zudojs/errors": "1.3.1",
29
- "@zudojs/serialization": "1.2.2",
30
- "@zudojs/types": "1.2.0"
27
+ "@zudojs/constants": "1.2.0",
28
+ "@zudojs/errors": "1.4.0",
29
+ "@zudojs/serialization": "1.3.0",
30
+ "@zudojs/types": "1.3.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "typescript": "7.0.2",