@cqrs-ddd/pipeline-cache 0.5.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 (40) hide show
  1. package/COMMERCIAL_LICENSE.txt +34 -0
  2. package/LICENSE +661 -0
  3. package/README.md +37 -0
  4. package/dist/adapters/cache-manager.adapter.d.ts +21 -0
  5. package/dist/adapters/cache-manager.adapter.d.ts.map +1 -0
  6. package/dist/adapters/cache-manager.adapter.js +52 -0
  7. package/dist/adapters/cache-manager.adapter.js.map +1 -0
  8. package/dist/cache.behavior.d.ts +83 -0
  9. package/dist/cache.behavior.d.ts.map +1 -0
  10. package/dist/cache.behavior.js +213 -0
  11. package/dist/cache.behavior.js.map +1 -0
  12. package/dist/errors/missing-partition.error.d.ts +18 -0
  13. package/dist/errors/missing-partition.error.d.ts.map +1 -0
  14. package/dist/errors/missing-partition.error.js +19 -0
  15. package/dist/errors/missing-partition.error.js.map +1 -0
  16. package/dist/helpers/build-attributes.d.ts +37 -0
  17. package/dist/helpers/build-attributes.d.ts.map +1 -0
  18. package/dist/helpers/build-attributes.js +43 -0
  19. package/dist/helpers/build-attributes.js.map +1 -0
  20. package/dist/helpers/cache-factory.d.ts +36 -0
  21. package/dist/helpers/cache-factory.d.ts.map +1 -0
  22. package/dist/helpers/cache-factory.js +147 -0
  23. package/dist/helpers/cache-factory.js.map +1 -0
  24. package/dist/helpers/cache-key.d.ts +81 -0
  25. package/dist/helpers/cache-key.d.ts.map +1 -0
  26. package/dist/helpers/cache-key.js +91 -0
  27. package/dist/helpers/cache-key.js.map +1 -0
  28. package/dist/helpers/cache.intent.d.ts +31 -0
  29. package/dist/helpers/cache.intent.d.ts.map +1 -0
  30. package/dist/helpers/cache.intent.js +26 -0
  31. package/dist/helpers/cache.intent.js.map +1 -0
  32. package/dist/index.d.ts +9 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +9 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/interfaces/cache-options.interface.d.ts +111 -0
  37. package/dist/interfaces/cache-options.interface.d.ts.map +1 -0
  38. package/dist/interfaces/cache-options.interface.js +3 -0
  39. package/dist/interfaces/cache-options.interface.js.map +1 -0
  40. package/package.json +89 -0
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @cqrs-ddd/pipeline-cache
2
+
3
+ Result caching for `@cqrs-ddd/pipeline` on [cache-manager](https://github.com/jaredwray/cacheable)
4
+ and Keyv stores (memory, Redis, Memcache, SQLite, PostgreSQL, tiered). `buildCache()`
5
+ creates the cache from a store configuration; `createPartitionedCacheKeyFactory()`
6
+ builds keys partitioned by tenant, principal and permission scope, because a cache hit
7
+ skips the function and every check inside it.
8
+
9
+ **Documentation:** [guide](https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-cache/) · [API reference](https://aristoteliss.github.io/ddd-cqrs/api/cqrs-ddd/pipeline-cache/) · [all packages](https://aristoteliss.github.io/ddd-cqrs/)
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pnpm add @cqrs-ddd/pipeline-cache @cqrs-ddd/pipeline cache-manager keyv
15
+ ```
16
+
17
+ Add `@keyv/redis`, `@keyv/memcache`, `@keyv/sqlite` or `@keyv/postgres` for those stores.
18
+ Requires Node.js 22.12 or later. Published as an ES module; a CommonJS application loads
19
+ it with `require()`.
20
+
21
+ ## Example
22
+
23
+ ```ts
24
+ const pipeline = createPipeline({
25
+ behaviors: [new CacheBehavior(buildCache({ store: { type: 'redis', url: redisUrl } }), { ttl: 30_000 })],
26
+ });
27
+
28
+ export const getPrice = pipeline.wrap(
29
+ { name: 'getPrice', kind: 'query' },
30
+ cache({ key: createPartitionedCacheKeyFactory({ principal: currentUserId, requireScope: false }) }),
31
+ )(async (sku: string) => prices.find(sku));
32
+ ```
33
+
34
+ ## License
35
+
36
+ Dual-licensed under **AGPLv3** and a **Commercial License**. See `LICENSE` and
37
+ `COMMERCIAL_LICENSE.txt`.
@@ -0,0 +1,21 @@
1
+ import type { Cache } from 'cache-manager';
2
+ /**
3
+ * Common interface for cache interactions within the pipeline.
4
+ */
5
+ export interface IPipelineCache {
6
+ get(key: string): Promise<unknown>;
7
+ set(key: string, value: unknown, ttl?: number): Promise<unknown>;
8
+ }
9
+ /**
10
+ * Adapter wrapping a cache-manager Cache instance to reliably expose backend
11
+ * store errors that cache-manager would otherwise swallow in get().
12
+ */
13
+ export declare class CacheManagerAdapter {
14
+ private readonly cache;
15
+ private readonly operation;
16
+ constructor(cache: Cache);
17
+ private captureError;
18
+ get(key: string): Promise<unknown>;
19
+ set(key: string, value: unknown, ttl?: number): Promise<unknown>;
20
+ }
21
+ //# sourceMappingURL=cache-manager.adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-manager.adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/cache-manager.adapter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAClE;AAED;;;GAGG;AACH,qBAAa,mBAAmB;IASlB,OAAO,CAAC,QAAQ,CAAC,KAAK;IANlC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAIrB;IAEL,YAA6B,KAAK,EAAE,KAAK,EASxC;IAED,OAAO,CAAC,YAAY;IAcd,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOvC;IAEK,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOrE;CACF"}
@@ -0,0 +1,52 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ /**
4
+ * Adapter wrapping a cache-manager Cache instance to reliably expose backend
5
+ * store errors that cache-manager would otherwise swallow in get().
6
+ */
7
+ export class CacheManagerAdapter {
8
+ cache;
9
+ // cache-manager emits errors in the originating operation's async context.
10
+ // A key alone cannot identify concurrent reads, writes, or fallback lookups.
11
+ operation = new AsyncLocalStorage();
12
+ constructor(cache) {
13
+ this.cache = cache;
14
+ // A caller-supplied store may swallow backend errors, which then read as a
15
+ // miss; these listeners surface what it reports.
16
+ const emitter = cache;
17
+ emitter.on?.('get', (event) => this.captureError('get', event));
18
+ emitter.on?.('set', (event) => this.captureError('set', event));
19
+ emitter.on?.('error', (event) => this.captureError(undefined, event));
20
+ }
21
+ captureError(kind, event) {
22
+ const operation = this.operation.getStore();
23
+ if (!operation || (kind && operation.kind !== kind))
24
+ return;
25
+ if (event &&
26
+ typeof event === 'object' &&
27
+ 'key' in event &&
28
+ event.key === operation.key &&
29
+ 'error' in event) {
30
+ operation.error = event.error;
31
+ }
32
+ }
33
+ async get(key) {
34
+ return this.operation.run({ key, kind: 'get' }, async () => {
35
+ const value = await this.cache.get(key);
36
+ const error = this.operation.getStore()?.error;
37
+ if (value === undefined && error !== undefined)
38
+ throw error;
39
+ return value;
40
+ });
41
+ }
42
+ async set(key, value, ttl) {
43
+ return this.operation.run({ key, kind: 'set' }, async () => {
44
+ const result = await this.cache.set(key, value, ttl);
45
+ const error = this.operation.getStore()?.error;
46
+ if (error !== undefined)
47
+ throw error;
48
+ return result;
49
+ });
50
+ }
51
+ }
52
+ //# sourceMappingURL=cache-manager.adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-manager.adapter.js","sourceRoot":"","sources":["../../src/adapters/cache-manager.adapter.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAWrD;;;GAGG;AACH,MAAM,OAAO,mBAAmB;IASD,KAAK;IARlC,2EAA2E;IAC3E,6EAA6E;IAC5D,SAAS,GAAG,IAAI,iBAAiB,EAI9C,CAAC;IAEL,YAA6B,KAAY;qBAAZ,KAAK;QAChC,2EAA2E;QAC3E,iDAAiD;QACjD,MAAM,OAAO,GAAG,KAEf,CAAC;QACF,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAChE,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAChE,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;IACxE,CAAC;IAEO,YAAY,CAAC,IAA+B,EAAE,KAAc;QAClE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;QAC5C,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,IAAI,SAAS,CAAC,IAAI,KAAK,IAAI,CAAC;YAAE,OAAO;QAC5D,IACE,KAAK;YACL,OAAO,KAAK,KAAK,QAAQ;YACzB,KAAK,IAAI,KAAK;YACd,KAAK,CAAC,GAAG,KAAK,SAAS,CAAC,GAAG;YAC3B,OAAO,IAAI,KAAK,EAChB,CAAC;YACD,SAAS,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QAChC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE;YACzD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACxC,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,KAAK,CAAC;YAC/C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS;gBAAE,MAAM,KAAK,CAAC;YAC5D,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAc,EAAE,GAAY;QACjD,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE;YACzD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,KAAK,CAAC;YAC/C,IAAI,KAAK,KAAK,SAAS;gBAAE,MAAM,KAAK,CAAC;YACrC,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,83 @@
1
+ import { type IPipelineBehavior, type IPipelineBehaviorContract, type IPipelineBehaviorOptionsResolver, type IPipelineContext, type NextDelegate, PIPELINE_BEHAVIOR_CONTRACT, type PipelineItemToken, type PipelineLogger } from '@cqrs-ddd/pipeline';
2
+ import type { Cache } from 'cache-manager';
3
+ import { type IPipelineCache } from './adapters/cache-manager.adapter.js';
4
+ import type { CacheBehaviorOptions } from './interfaces/cache-options.interface.js';
5
+ /**
6
+ * Unique symbol key set on `context.items` recording whether the request was served from cache (`true` on hit).
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const isHit = context.items.get(CACHE_HIT_ITEM) === true;
11
+ * ```
12
+ */
13
+ export declare const CACHE_HIT_ITEM: unique symbol;
14
+ /**
15
+ * Typed token for {@link CACHE_HIT_ITEM}: the cache hit flag. Reads and writes the same
16
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
17
+ */
18
+ export declare const CACHE_HIT_ITEM_TOKEN: PipelineItemToken<boolean>;
19
+ /**
20
+ * Unique symbol key set on `context.items` recording the resolved cache key string.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const key = context.items.get(CACHE_KEY_ITEM) as string | undefined;
25
+ * ```
26
+ */
27
+ export declare const CACHE_KEY_ITEM: unique symbol;
28
+ /**
29
+ * Typed token for {@link CACHE_KEY_ITEM}: the resolved cache key. Reads and writes the same
30
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
31
+ */
32
+ export declare const CACHE_KEY_ITEM_TOKEN: PipelineItemToken<string>;
33
+ /**
34
+ * Pipeline behavior that transparently caches handler results with
35
+ * `cache-manager` (v7) on top of Keyv.
36
+ *
37
+ * Resolution of the effective options for a handler:
38
+ * 1. Application-wide defaults, the second constructor argument.
39
+ * 2. Per-operation options from the `cache({ ... })` entry,
40
+ * shallow-merged on top of the defaults (handler keys win).
41
+ *
42
+ * A `key` factory is required — see {@link createPartitionedCacheKeyFactory}. Only
43
+ * `query` requests are cached by default; commands and events pass through
44
+ * untouched. On a cache miss, `null` / `undefined` results are not written.
45
+ * Cache hits return the value from the single explicit lookup. This behavior
46
+ * intentionally does not use `cache-manager.wrap()` because its background
47
+ * refresh callback would re-enter every downstream pipeline behavior.
48
+ * Cached results are JSON values. A miss returns the JSON form of the handler
49
+ * result that it stores (a `Date` becomes an ISO string and a class instance a
50
+ * plain object), so a hit and a miss return the same shape. A result that cannot
51
+ * be JSON-serialized is returned unchanged and not cached.
52
+ *
53
+ * Store errors fail open by default: read failures bypass caching for that
54
+ * execution, and write failures return the successful handler result. Set
55
+ * `failOpen: false` to propagate store failures instead.
56
+ *
57
+ * @example Principal/permission-scoped query cache
58
+ * ```ts
59
+ * const userCacheKey = createPartitionedCacheKeyFactory({
60
+ * principal: (ctx) => ctx.items.get('userId') as string | undefined,
61
+ * scope: (ctx) => ctx.items.get('capabilityVersion') as string | undefined,
62
+ * });
63
+ *
64
+ * class GetUsersHandler {
65
+ * @pipeline.wrap({ kind: 'query' }, [CacheBehavior, { key: userCacheKey, ttl: 30_000 }])
66
+ * async handle(query: GetUsersQuery) {}
67
+ * }
68
+ * ```
69
+ */
70
+ export declare class CacheBehavior implements IPipelineBehavior, IPipelineBehaviorOptionsResolver<CacheBehaviorOptions> {
71
+ static readonly [PIPELINE_BEHAVIOR_CONTRACT]: IPipelineBehaviorContract;
72
+ private readonly logger;
73
+ private readonly defaults;
74
+ private readonly cacheAdapter;
75
+ constructor(cache: Cache | IPipelineCache, defaults?: CacheBehaviorOptions, logger?: PipelineLogger);
76
+ handle(context: IPipelineContext, next: NextDelegate): Promise<unknown>;
77
+ /** Logs a store failure and either bypasses it or propagates it. */
78
+ private handleStoreError;
79
+ private diagnose;
80
+ /** Shallow-merges pipeline-level options over application defaults. */
81
+ resolveEffectiveOptions(options?: CacheBehaviorOptions): CacheBehaviorOptions;
82
+ }
83
+ //# sourceMappingURL=cache.behavior.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.behavior.d.ts","sourceRoot":"","sources":["../src/cache.behavior.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,0BAA0B,EAI1B,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAEpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAEpF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC,OAAO,CACG,CAAC;AAEhE;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC,MAAM,CACG,CAAC;AAY/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,qBAAa,aACX,YACE,iBAAiB,EACjB,gCAAgC,CAAC,oBAAoB,CAAC;IAExD,MAAM,CAAC,QAAQ,CAAC,CAAC,0BAA0B,CAAC,EAAE,yBAAyB,CAkDrE;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiB;IAE9C,YACE,KAAK,EAAE,KAAK,GAAG,cAAc,EAC7B,QAAQ,CAAC,EAAE,oBAAoB,EAC/B,MAAM,CAAC,EAAE,cAAc,EAcxB;IAEK,MAAM,CACV,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,OAAO,CAAC,CAyDlB;IAED,oEAAoE;IACpE,OAAO,CAAC,gBAAgB;IA0BxB,OAAO,CAAC,QAAQ;IAQhB,uEAAuE;IACvE,uBAAuB,CACrB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,oBAAoB,CAGtB;CACF"}
@@ -0,0 +1,213 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { createPipelineItem, PIPELINE_BEHAVIOR_CONTRACT, setPipelineItem, } from '@cqrs-ddd/pipeline';
3
+ import { CacheManagerAdapter, } from './adapters/cache-manager.adapter.js';
4
+ /**
5
+ * Unique symbol key set on `context.items` recording whether the request was served from cache (`true` on hit).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const isHit = context.items.get(CACHE_HIT_ITEM) === true;
10
+ * ```
11
+ */
12
+ export const CACHE_HIT_ITEM = Symbol('CACHE_HIT_ITEM');
13
+ /**
14
+ * Typed token for {@link CACHE_HIT_ITEM}: the cache hit flag. Reads and writes the same
15
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
16
+ */
17
+ export const CACHE_HIT_ITEM_TOKEN = createPipelineItem('CACHE_HIT_ITEM', CACHE_HIT_ITEM);
18
+ /**
19
+ * Unique symbol key set on `context.items` recording the resolved cache key string.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const key = context.items.get(CACHE_KEY_ITEM) as string | undefined;
24
+ * ```
25
+ */
26
+ export const CACHE_KEY_ITEM = Symbol('CACHE_KEY_ITEM');
27
+ /**
28
+ * Typed token for {@link CACHE_KEY_ITEM}: the resolved cache key. Reads and writes the same
29
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
30
+ */
31
+ export const CACHE_KEY_ITEM_TOKEN = createPipelineItem('CACHE_KEY_ITEM', CACHE_KEY_ITEM);
32
+ const DEFAULT_KINDS = ['query'];
33
+ /** Whether this request kind is configured for caching. */
34
+ function inScope(options, requestKind) {
35
+ return (options?.kinds ?? DEFAULT_KINDS).includes(requestKind);
36
+ }
37
+ /**
38
+ * Pipeline behavior that transparently caches handler results with
39
+ * `cache-manager` (v7) on top of Keyv.
40
+ *
41
+ * Resolution of the effective options for a handler:
42
+ * 1. Application-wide defaults, the second constructor argument.
43
+ * 2. Per-operation options from the `cache({ ... })` entry,
44
+ * shallow-merged on top of the defaults (handler keys win).
45
+ *
46
+ * A `key` factory is required — see {@link createPartitionedCacheKeyFactory}. Only
47
+ * `query` requests are cached by default; commands and events pass through
48
+ * untouched. On a cache miss, `null` / `undefined` results are not written.
49
+ * Cache hits return the value from the single explicit lookup. This behavior
50
+ * intentionally does not use `cache-manager.wrap()` because its background
51
+ * refresh callback would re-enter every downstream pipeline behavior.
52
+ * Cached results are JSON values. A miss returns the JSON form of the handler
53
+ * result that it stores (a `Date` becomes an ISO string and a class instance a
54
+ * plain object), so a hit and a miss return the same shape. A result that cannot
55
+ * be JSON-serialized is returned unchanged and not cached.
56
+ *
57
+ * Store errors fail open by default: read failures bypass caching for that
58
+ * execution, and write failures return the successful handler result. Set
59
+ * `failOpen: false` to propagate store failures instead.
60
+ *
61
+ * @example Principal/permission-scoped query cache
62
+ * ```ts
63
+ * const userCacheKey = createPartitionedCacheKeyFactory({
64
+ * principal: (ctx) => ctx.items.get('userId') as string | undefined,
65
+ * scope: (ctx) => ctx.items.get('capabilityVersion') as string | undefined,
66
+ * });
67
+ *
68
+ * class GetUsersHandler {
69
+ * @pipeline.wrap({ kind: 'query' }, [CacheBehavior, { key: userCacheKey, ttl: 30_000 }])
70
+ * async handle(query: GetUsersQuery) {}
71
+ * }
72
+ * ```
73
+ */
74
+ export class CacheBehavior {
75
+ static [PIPELINE_BEHAVIOR_CONTRACT] = {
76
+ order: (context) => {
77
+ const options = context.effectiveOptions;
78
+ if (!inScope(options, context.requestKind)) {
79
+ return undefined;
80
+ }
81
+ return {
82
+ after: ['@cqrs-ddd/pipeline-casl:CaslBehavior', 'CaslBehavior'],
83
+ };
84
+ },
85
+ validate: (context) => {
86
+ const options = context.effectiveOptions;
87
+ if (!inScope(options, context.requestKind)) {
88
+ return undefined;
89
+ }
90
+ if (!options?.key) {
91
+ return [
92
+ {
93
+ handlerName: context.handlerName,
94
+ behaviorName: CacheBehavior.name,
95
+ message: 'Active CacheBehavior requires an explicit `key` factory',
96
+ fix: 'Provide a key factory via createPartitionedCacheKeyFactory(...) in ' +
97
+ 'the cache({ key }) entry or the CacheBehavior constructor defaults.',
98
+ },
99
+ ];
100
+ }
101
+ if (typeof options.key !== 'function') {
102
+ return [
103
+ {
104
+ handlerName: context.handlerName,
105
+ behaviorName: CacheBehavior.name,
106
+ message: `CacheBehavior key factory must be a callable function, received ${typeof options.key}`,
107
+ fix: 'Pass a valid function (ctx) => string as the key option of the cache({ key }) entry.',
108
+ },
109
+ ];
110
+ }
111
+ return undefined;
112
+ },
113
+ };
114
+ logger;
115
+ defaults;
116
+ cacheAdapter;
117
+ constructor(cache, defaults, logger) {
118
+ if (!cache || typeof cache.get !== 'function') {
119
+ throw new TypeError('CacheBehavior requires a cache, such as buildCache({ store }) of @cqrs-ddd/pipeline-cache.');
120
+ }
121
+ this.defaults = defaults ?? {};
122
+ this.cacheAdapter =
123
+ cache instanceof CacheManagerAdapter
124
+ ? cache
125
+ : new CacheManagerAdapter(cache);
126
+ this.logger = logger ?? console;
127
+ }
128
+ async handle(context, next) {
129
+ const options = this.resolveEffectiveOptions(context.getBehaviorOptions(CacheBehavior));
130
+ if (!inScope(options, context.requestKind))
131
+ return next();
132
+ if (options.condition && !options.condition(context))
133
+ return next();
134
+ if (!options.key) {
135
+ throw new TypeError(`CacheBehavior on ${context.handlerName} requires an explicit \`key\` factory. ` +
136
+ 'A cache hit returns without running the handler, so the key must partition ' +
137
+ 'every dimension that can change the authorized response: tenant, principal, ' +
138
+ 'permission scope and request payload. Use createPartitionedCacheKeyFactory(...).');
139
+ }
140
+ const key = options.key(context);
141
+ setPipelineItem(context, CACHE_KEY_ITEM_TOKEN, key);
142
+ let cached;
143
+ let canWrite = true;
144
+ try {
145
+ cached = await this.cacheAdapter.get(key);
146
+ }
147
+ catch (error) {
148
+ this.handleStoreError('read', context, key, error, options);
149
+ canWrite = false;
150
+ }
151
+ if (cached !== undefined && cached !== null) {
152
+ setPipelineItem(context, CACHE_HIT_ITEM_TOKEN, true);
153
+ this.diagnose('debug', `Cache hit for ${context.requestName} (${key})`);
154
+ return cached;
155
+ }
156
+ setPipelineItem(context, CACHE_HIT_ITEM_TOKEN, false);
157
+ this.diagnose('debug', `Cache miss for ${context.requestName} (${key})`);
158
+ const result = await next();
159
+ if (result === undefined || result === null)
160
+ return result;
161
+ const cacheable = toJsonForm(result);
162
+ if (cacheable === undefined) {
163
+ this.diagnose('warn', `Result of ${context.requestName} is not JSON-serializable; not cached (key: ${key})`);
164
+ return result;
165
+ }
166
+ if (canWrite) {
167
+ try {
168
+ await this.cacheAdapter.set(key, cacheable, options.ttl);
169
+ }
170
+ catch (error) {
171
+ this.handleStoreError('write', context, key, error, options);
172
+ }
173
+ }
174
+ return cacheable;
175
+ }
176
+ /** Logs a store failure and either bypasses it or propagates it. */
177
+ handleStoreError(operation, context, key, error, options) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ if (options.failOpen ?? true) {
180
+ this.diagnose('warn', `Cache ${operation} error for ${context.requestName} ` +
181
+ `(key: ${key}); failing open: ${message}`);
182
+ return;
183
+ }
184
+ this.diagnose('error', `Cache ${operation} error for ${context.requestName} ` +
185
+ `(key: ${key}); failing closed: ${message}`);
186
+ throw error;
187
+ }
188
+ diagnose(level, message) {
189
+ try {
190
+ this.logger[level]?.(message, CacheBehavior.name);
191
+ }
192
+ catch {
193
+ // Diagnostics must not replace the handler result or cache-store error.
194
+ }
195
+ }
196
+ /** Shallow-merges pipeline-level options over application defaults. */
197
+ resolveEffectiveOptions(options) {
198
+ if (!options)
199
+ return this.defaults;
200
+ return { ...this.defaults, ...options };
201
+ }
202
+ }
203
+ /** JSON round-trip of a cacheable result, or `undefined` when it has no JSON form. */
204
+ function toJsonForm(value) {
205
+ try {
206
+ const json = JSON.stringify(value);
207
+ return json === undefined ? undefined : JSON.parse(json);
208
+ }
209
+ catch {
210
+ return undefined;
211
+ }
212
+ }
213
+ //# sourceMappingURL=cache.behavior.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.behavior.js","sourceRoot":"","sources":["../src/cache.behavior.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { MissingPartitionError } from '@cqrs-ddd/pipeline';
2
+ /** Which dimension of the cache key could not be resolved. */
3
+ export type CachePartitionDimension = 'tenant' | 'principal' | 'scope';
4
+ /**
5
+ * Raised before the cache is consulted when a dimension required by the
6
+ * configured key factory cannot be resolved.
7
+ *
8
+ * A cache hit returns without executing the handler,
9
+ * so it also skips whatever entity-level authorization and field filtering that
10
+ * handler performs. A key missing its tenant or principal segment therefore does
11
+ * not merely lose isolation — it can replay one caller's authorized response to
12
+ * another.
13
+ */
14
+ export declare class MissingCachePartitionError extends MissingPartitionError<CachePartitionDimension> {
15
+ readonly name = "MissingCachePartitionError";
16
+ constructor(requestName: string, dimension: CachePartitionDimension, remedy: string);
17
+ }
18
+ //# sourceMappingURL=missing-partition.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-partition.error.d.ts","sourceRoot":"","sources":["../../src/errors/missing-partition.error.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,8DAA8D;AAC9D,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAEvE;;;;;;;;;GASG;AACH,qBAAa,0BAA2B,SAAQ,qBAAqB,CAAC,uBAAuB,CAAC;IAC5F,SAAkB,IAAI,gCAAgC;IAEtD,YACE,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,uBAAuB,EAClC,MAAM,EAAE,MAAM,EAGf;CACF"}
@@ -0,0 +1,19 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { MissingPartitionError } from '@cqrs-ddd/pipeline';
3
+ /**
4
+ * Raised before the cache is consulted when a dimension required by the
5
+ * configured key factory cannot be resolved.
6
+ *
7
+ * A cache hit returns without executing the handler,
8
+ * so it also skips whatever entity-level authorization and field filtering that
9
+ * handler performs. A key missing its tenant or principal segment therefore does
10
+ * not merely lose isolation — it can replay one caller's authorized response to
11
+ * another.
12
+ */
13
+ export class MissingCachePartitionError extends MissingPartitionError {
14
+ name = 'MissingCachePartitionError';
15
+ constructor(requestName, dimension, remedy) {
16
+ super('Cache', requestName, dimension, remedy);
17
+ }
18
+ }
19
+ //# sourceMappingURL=missing-partition.error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-partition.error.js","sourceRoot":"","sources":["../../src/errors/missing-partition.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAK3D;;;;;;;;;GASG;AACH,MAAM,OAAO,0BAA2B,SAAQ,qBAA8C;IAC1E,IAAI,GAAG,4BAA4B,CAAC;IAEtD,YACE,WAAmB,EACnB,SAAkC,EAClC,MAAc;QAEd,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IACjD,CAAC;CACF"}
@@ -0,0 +1,37 @@
1
+ import { type IPipelineContext } from '@cqrs-ddd/pipeline';
2
+ /**
3
+ * The cache decision of one execution as flat attributes:
4
+ * `{ 'cache.hit': boolean }` once `CacheBehavior` looked the response up, and
5
+ * `{}` when it did not run, so a request without caching never reports a
6
+ * fabricated miss. The cache key is never included: it carries the tenant and
7
+ * the principal, and is unbounded as a value.
8
+ *
9
+ * The result is plain data and needs no telemetry package: put it on a span, a
10
+ * log line, an audit record or a metric label (bounded values only).
11
+ *
12
+ * @param context - The pipeline execution, read after `CacheBehavior` ran.
13
+ * @returns The attributes; empty when the behavior did not run.
14
+ *
15
+ * @example Span attributes
16
+ * With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
17
+ * ```ts
18
+ * createPipeline({
19
+ * globalBehaviors: {
20
+ * before: [TraceBehavior, [AttributesBehavior, { factories: [buildCacheAttributes] }]],
21
+ * },
22
+ * });
23
+ * ```
24
+ *
25
+ * @example A log line from a custom behavior, after the chain ran
26
+ * ```ts
27
+ * async handle(context: IPipelineContext, next: NextDelegate) {
28
+ * try {
29
+ * return await next();
30
+ * } finally {
31
+ * this.logger.log({ request: context.requestName, ...buildCacheAttributes(context) });
32
+ * }
33
+ * }
34
+ * ```
35
+ */
36
+ export declare function buildCacheAttributes(context: IPipelineContext): Record<string, boolean>;
37
+ //# sourceMappingURL=build-attributes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-attributes.d.ts","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAG5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIzB"}
@@ -0,0 +1,43 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { getPipelineItem, hasPipelineItem, } from '@cqrs-ddd/pipeline';
3
+ import { CACHE_HIT_ITEM_TOKEN } from '../cache.behavior.js';
4
+ /**
5
+ * The cache decision of one execution as flat attributes:
6
+ * `{ 'cache.hit': boolean }` once `CacheBehavior` looked the response up, and
7
+ * `{}` when it did not run, so a request without caching never reports a
8
+ * fabricated miss. The cache key is never included: it carries the tenant and
9
+ * the principal, and is unbounded as a value.
10
+ *
11
+ * The result is plain data and needs no telemetry package: put it on a span, a
12
+ * log line, an audit record or a metric label (bounded values only).
13
+ *
14
+ * @param context - The pipeline execution, read after `CacheBehavior` ran.
15
+ * @returns The attributes; empty when the behavior did not run.
16
+ *
17
+ * @example Span attributes
18
+ * With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
19
+ * ```ts
20
+ * createPipeline({
21
+ * globalBehaviors: {
22
+ * before: [TraceBehavior, [AttributesBehavior, { factories: [buildCacheAttributes] }]],
23
+ * },
24
+ * });
25
+ * ```
26
+ *
27
+ * @example A log line from a custom behavior, after the chain ran
28
+ * ```ts
29
+ * async handle(context: IPipelineContext, next: NextDelegate) {
30
+ * try {
31
+ * return await next();
32
+ * } finally {
33
+ * this.logger.log({ request: context.requestName, ...buildCacheAttributes(context) });
34
+ * }
35
+ * }
36
+ * ```
37
+ */
38
+ export function buildCacheAttributes(context) {
39
+ return hasPipelineItem(context, CACHE_HIT_ITEM_TOKEN)
40
+ ? { 'cache.hit': getPipelineItem(context, CACHE_HIT_ITEM_TOKEN) === true }
41
+ : {};
42
+ }
43
+ //# sourceMappingURL=build-attributes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-attributes.js","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EACL,eAAe,EACf,eAAe,GAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAyB;IAEzB,OAAO,eAAe,CAAC,OAAO,EAAE,oBAAoB,CAAC;QACnD,CAAC,CAAC,EAAE,WAAW,EAAE,eAAe,CAAC,OAAO,EAAE,oBAAoB,CAAC,KAAK,IAAI,EAAE;QAC1E,CAAC,CAAC,EAAE,CAAC;AACT,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { type Cache } from 'cache-manager';
2
+ import { Keyv } from 'keyv';
3
+ import type { BuildCacheOptions, CacheStoreConfig } from '../interfaces/cache-options.interface.js';
4
+ /**
5
+ * Build a single package-owned `Keyv` instance from a declarative store
6
+ * configuration. Package-owned stores enable `throwOnErrors` so
7
+ * {@link CacheBehavior} can apply its own `failOpen` / fail-closed policy rather
8
+ * than having Keyv silently consume backend failures first.
9
+ */
10
+ export declare function buildKeyv(config: CacheStoreConfig): Keyv;
11
+ /**
12
+ * Resolve the {@link BuildCacheOptions} into a ready-to-use `cache-manager`
13
+ * {@link Cache}. A pre-built `cache` wins, followed by pre-built `stores`,
14
+ * followed by declarative `store` configuration, falling back to an in-memory
15
+ * store when nothing is provided.
16
+ *
17
+ * Package-created stores use `throwOnErrors: true` so cache failures reach
18
+ * `CacheBehavior` and its configured failure policy. Caller-owned `cache` and
19
+ * `stores` are **not mutated**: if an application intentionally shares a Keyv
20
+ * instance with another subsystem, this package must not change that object's
21
+ * error semantics globally.
22
+ *
23
+ * @example Fully package-owned Redis cache
24
+ * ```ts
25
+ * const cache = buildCache({ store: { type: 'redis', url: redisUrl } });
26
+ * ```
27
+ *
28
+ * @example Caller-owned shared store — configuration remains caller-controlled
29
+ * ```ts
30
+ * const shared = new Keyv({ store: sharedRedis, throwOnErrors: false });
31
+ * const cache = buildCache({ stores: [shared] });
32
+ * // buildCache() will not mutate shared.throwOnErrors.
33
+ * ```
34
+ */
35
+ export declare function buildCache(options: BuildCacheOptions): Cache;
36
+ //# sourceMappingURL=cache-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-factory.d.ts","sourceRoot":"","sources":["../../src/helpers/cache-factory.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,KAAK,EAAe,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,IAAI,EAAyB,MAAM,MAAM,CAAC;AACnD,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAEjB,MAAM,0CAA0C,CAAC;AAqGlD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAexD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,KAAK,CAsB5D"}