@seta-labs/nestjs-cache-control 0.0.0-stage → 0.1.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 (38) hide show
  1. package/README.md +77 -2
  2. package/dist/api-cache-control.decorator.d.ts +2 -0
  3. package/dist/api-cache-control.decorator.js +16 -0
  4. package/dist/api-cache-control.decorator.js.map +1 -0
  5. package/dist/cache-control.constants.d.ts +5 -0
  6. package/dist/cache-control.constants.js +6 -0
  7. package/dist/cache-control.constants.js.map +1 -0
  8. package/dist/cache-control.module.d.ts +10 -0
  9. package/dist/cache-control.module.js +32 -0
  10. package/dist/cache-control.module.js.map +1 -0
  11. package/dist/cache-control.util.d.ts +18 -0
  12. package/dist/cache-control.util.js +94 -0
  13. package/dist/cache-control.util.js.map +1 -0
  14. package/dist/cache-status.interceptor.d.ts +18 -0
  15. package/dist/cache-status.interceptor.js +59 -0
  16. package/dist/cache-status.interceptor.js.map +1 -0
  17. package/dist/cache-status.util.d.ts +20 -0
  18. package/dist/cache-status.util.js +31 -0
  19. package/dist/cache-status.util.js.map +1 -0
  20. package/dist/index.d.ts +11 -0
  21. package/dist/index.js +9 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/not-cached.exception.d.ts +9 -0
  24. package/dist/not-cached.exception.js +13 -0
  25. package/dist/not-cached.exception.js.map +1 -0
  26. package/dist/request-cache-policy.decorator.d.ts +6 -0
  27. package/dist/request-cache-policy.decorator.js +14 -0
  28. package/dist/request-cache-policy.decorator.js.map +1 -0
  29. package/dist/types/cache-context.type.d.ts +14 -0
  30. package/dist/types/cache-context.type.js +2 -0
  31. package/dist/types/cache-context.type.js.map +1 -0
  32. package/dist/types/cache-control-options.type.d.ts +4 -0
  33. package/dist/types/cache-control-options.type.js +2 -0
  34. package/dist/types/cache-control-options.type.js.map +1 -0
  35. package/dist/types/cache-policy.type.d.ts +24 -0
  36. package/dist/types/cache-policy.type.js +2 -0
  37. package/dist/types/cache-policy.type.js.map +1 -0
  38. package/package.json +47 -5
package/README.md CHANGED
@@ -1,3 +1,78 @@
1
- # Temporary Holding Version
1
+ # @seta-labs/nestjs-cache-control
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The request cache directives of RFC 9111 for NestJS lookups served from a
4
+ store (a table of provider answers, paid or rate-limited): the rule lives in
5
+ one tested function, each app only reads its row, asks its provider and
6
+ writes. The response says what happened in `Cache-Status` (RFC 9211) and
7
+ `Age`.
8
+
9
+ | Request `Cache-Control` | Stored row usable | No row, or past `max-age` | Writes the answer |
10
+ | ----------------------- | ------------------------ | ------------------------- | ----------------- |
11
+ | none | served (`hit`) | provider (`fwd=miss`) | yes |
12
+ | `no-cache` | provider (`fwd=request`) | provider | yes |
13
+ | `no-cache, no-store` | provider | provider | no: pure bypass |
14
+ | `no-store` | served | provider | no |
15
+ | `max-age=N` | served if ≤ N s old | provider (`fwd=stale`) | yes |
16
+ | `only-if-cached` | served | `504` `NOT_CACHED` | — |
17
+
18
+ If the provider is asked, its answer is stored unless the request says
19
+ `no-store`: a store shared by several apps keeps the freshest answer. Unknown
20
+ directives and an invalid `max-age` are ignored, as the RFC asks.
21
+
22
+ ## Use
23
+
24
+ ```ts
25
+ // app.module.ts: once, it registers the interceptor writing Cache-Status and Age
26
+ CacheControlModule.forRoot({ name: 'seta-geo' });
27
+ ```
28
+
29
+ ```ts
30
+ @Post('parcel')
31
+ @ApiCacheControl() // OpenAPI: the Cache-Control header and the 504
32
+ parcel(@Body() dto: ParcelRequestDto, @RequestCachePolicy() cache: CacheContext) {
33
+ return this.parcelService.parcel(dto, cache);
34
+ }
35
+ ```
36
+
37
+ ```ts
38
+ async parcel(request: ParcelRequestDto, cache: CacheContext): Promise<ParcelResultDto> {
39
+ const stored = await this.repository.find(keyOf(request));
40
+ const decision = cache.decide(stored?.updatedAt ?? null);
41
+ switch (decision.action) {
42
+ case 'serve':
43
+ return toParcelResultDto(stored!, true);
44
+ case 'refuse':
45
+ throw new NotCachedException();
46
+ case 'fetch': {
47
+ const answer = await this.provider.lookup(request);
48
+ const parcel = decision.store ? await this.repository.save(answer) : answer;
49
+ return toParcelResultDto(parcel, false);
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ `cache.decide(storedAt)` rules on one row and records the decision; the
56
+ interceptor turns a response's single decision into its headers. A request
57
+ deciding for several rows (a batch) gets no `Cache-Status`: one header
58
+ cannot describe its items, the body does. Routes without
59
+ `@RequestCachePolicy()` are left alone.
60
+
61
+ The headers go through the Nest HTTP adapter, so Express and Fastify behave
62
+ the same. A request that fails after deciding never says `stored`: the fetch
63
+ that threw wrote nothing. Directive arguments may be quoted strings; a comma
64
+ inside quotes never starts a directive.
65
+
66
+ | Decision | Response headers |
67
+ | ---------------------------------- | ---------------------------------------------- |
68
+ | `serve` | `Cache-Status: <name>; hit`, `Age: <seconds>` |
69
+ | `fetch` (`miss`/`request`/`stale`) | `Cache-Status: <name>; fwd=<reason>[; stored]` |
70
+ | `refuse` | `Cache-Status: <name>; detail=not-cached`, 504 |
71
+
72
+ ## API
73
+
74
+ - `CacheControlModule.forRoot({ name })`: the global `CacheStatusInterceptor`, `name` labels `Cache-Status`.
75
+ - `@RequestCachePolicy()`: the request's `CacheContext` (`policy`, `decide(storedAt)`, `decisions`).
76
+ - `@ApiCacheControl()`: documents the request header and the 504.
77
+ - `NotCachedException`: the 504 with `code: 'NOT_CACHED'`.
78
+ - `parseCacheControl(header)`, `decideCache(policy, storedAt, now?)`, `createCacheContext(policy)`, `cacheStatusHeaders(name, decision)`: the pure parts, for other transports.
@@ -0,0 +1,2 @@
1
+ /** OpenAPI of a lookup that honours `Cache-Control`: the request header and the 504. */
2
+ export declare function ApiCacheControl(): MethodDecorator & ClassDecorator;
@@ -0,0 +1,16 @@
1
+ import { applyDecorators } from '@nestjs/common';
2
+ import { ApiGatewayTimeoutResponse, ApiHeader } from '@nestjs/swagger';
3
+ /** OpenAPI of a lookup that honours `Cache-Control`: the request header and the 504. */
4
+ export function ApiCacheControl() {
5
+ return applyDecorators(ApiHeader({
6
+ name: 'Cache-Control',
7
+ required: false,
8
+ description: 'no-cache: ask the provider and store its answer; no-store: never write the store; ' +
9
+ 'max-age=N: a stored answer older than N seconds is asked again; only-if-cached: ' +
10
+ 'never ask the provider. The response says what happened in Cache-Status ' +
11
+ '(hit, fwd=miss|request|stale, stored) and, on a hit, Age.',
12
+ }), ApiGatewayTimeoutResponse({
13
+ description: 'NOT_CACHED: only-if-cached and no usable stored answer.',
14
+ }));
15
+ }
16
+ //# sourceMappingURL=api-cache-control.decorator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-cache-control.decorator.js","sourceRoot":"","sources":["../src/api-cache-control.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,yBAAyB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEvE,wFAAwF;AACxF,MAAM,UAAU,eAAe;IAC3B,OAAO,eAAe,CAClB,SAAS,CAAC;QACN,IAAI,EAAE,eAAe;QACrB,QAAQ,EAAE,KAAK;QACf,WAAW,EACP,oFAAoF;YACpF,kFAAkF;YAClF,0EAA0E;YAC1E,2DAA2D;KAClE,CAAC,EACF,yBAAyB,CAAC;QACtB,WAAW,EAAE,yDAAyD;KACzE,CAAC,CACL,CAAC;AACN,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare const CACHE_CONTROL_OPTIONS: unique symbol;
2
+ /** Where `@RequestCachePolicy()` leaves the request's context for the interceptor. */
3
+ export declare const CACHE_CONTEXT: unique symbol;
4
+ /** The `code` of the 504 a refused `only-if-cached` lookup answers. */
5
+ export declare const NOT_CACHED = "NOT_CACHED";
@@ -0,0 +1,6 @@
1
+ export const CACHE_CONTROL_OPTIONS = Symbol('CACHE_CONTROL_OPTIONS');
2
+ /** Where `@RequestCachePolicy()` leaves the request's context for the interceptor. */
3
+ export const CACHE_CONTEXT = Symbol('CACHE_CONTEXT');
4
+ /** The `code` of the 504 a refused `only-if-cached` lookup answers. */
5
+ export const NOT_CACHED = 'NOT_CACHED';
6
+ //# sourceMappingURL=cache-control.constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-control.constants.js","sourceRoot":"","sources":["../src/cache-control.constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,uBAAuB,CAAC,CAAC;AAErE,sFAAsF;AACtF,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;AAErD,uEAAuE;AACvE,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ import type { CacheControlModuleOptions } from './types/cache-control-options.type.js';
3
+ /**
4
+ * Import once in the root module: registers `CacheStatusInterceptor` for
5
+ * every route. Lookups then take `@RequestCachePolicy()` and decide with it;
6
+ * routes that do not are left alone.
7
+ */
8
+ export declare class CacheControlModule {
9
+ static forRoot(options: CacheControlModuleOptions): DynamicModule;
10
+ }
@@ -0,0 +1,32 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var CacheControlModule_1;
8
+ import { Module } from '@nestjs/common';
9
+ import { APP_INTERCEPTOR } from '@nestjs/core';
10
+ import { CACHE_CONTROL_OPTIONS } from './cache-control.constants.js';
11
+ import { CacheStatusInterceptor } from './cache-status.interceptor.js';
12
+ /**
13
+ * Import once in the root module: registers `CacheStatusInterceptor` for
14
+ * every route. Lookups then take `@RequestCachePolicy()` and decide with it;
15
+ * routes that do not are left alone.
16
+ */
17
+ let CacheControlModule = CacheControlModule_1 = class CacheControlModule {
18
+ static forRoot(options) {
19
+ return {
20
+ module: CacheControlModule_1,
21
+ providers: [
22
+ { provide: CACHE_CONTROL_OPTIONS, useValue: options },
23
+ { provide: APP_INTERCEPTOR, useClass: CacheStatusInterceptor },
24
+ ],
25
+ };
26
+ }
27
+ };
28
+ CacheControlModule = CacheControlModule_1 = __decorate([
29
+ Module({})
30
+ ], CacheControlModule);
31
+ export { CacheControlModule };
32
+ //# sourceMappingURL=cache-control.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-control.module.js","sourceRoot":"","sources":["../src/cache-control.module.ts"],"names":[],"mappings":";;;;;;;AAAA,OAAO,EAAsB,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAGvE;;;;GAIG;AAEI,IAAM,kBAAkB,0BAAxB,MAAM,kBAAkB;IAC3B,MAAM,CAAC,OAAO,CAAC,OAAkC;QAC7C,OAAO;YACH,MAAM,EAAE,oBAAkB;YAC1B,SAAS,EAAE;gBACP,EAAE,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,OAAO,EAAE;gBACrD,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,sBAAsB,EAAE;aACjE;SACJ,CAAC;IACN,CAAC;CACJ,CAAA;AAVY,kBAAkB;IAD9B,MAAM,CAAC,EAAE,CAAC;GACE,kBAAkB,CAU9B"}
@@ -0,0 +1,18 @@
1
+ import type { CacheContext } from './types/cache-context.type.js';
2
+ import type { CacheDecision, CachePolicy } from './types/cache-policy.type.js';
3
+ /**
4
+ * Reads the request directives a lookup honours from a `Cache-Control`
5
+ * header (RFC 9111 §5.2.1): `no-cache`, `no-store`, `max-age=N`,
6
+ * `only-if-cached`. Directive names are case-insensitive; unknown
7
+ * directives and an invalid `max-age` are ignored, as the RFC asks.
8
+ */
9
+ export declare function parseCacheControl(header: string | string[] | undefined): CachePolicy;
10
+ /**
11
+ * The one rule of a lookup served from a store. A stored row answers unless
12
+ * the request says `no-cache` or the row is older than its `max-age`;
13
+ * otherwise the provider is asked and its answer stored, unless `no-store`.
14
+ * `only-if-cached` never asks the provider: without a usable row it refuses.
15
+ */
16
+ export declare function decideCache(policy: CachePolicy, storedAt: Date | null, now?: number): CacheDecision;
17
+ /** A request's cache context: decides with `decideCache` and records every decision. */
18
+ export declare function createCacheContext(policy: CachePolicy, now?: () => number): CacheContext;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Reads the request directives a lookup honours from a `Cache-Control`
3
+ * header (RFC 9111 §5.2.1): `no-cache`, `no-store`, `max-age=N`,
4
+ * `only-if-cached`. Directive names are case-insensitive; unknown
5
+ * directives and an invalid `max-age` are ignored, as the RFC asks.
6
+ */
7
+ export function parseCacheControl(header) {
8
+ const text = Array.isArray(header) ? header.join(',') : (header ?? '');
9
+ const directives = new Map();
10
+ for (const part of splitDirectives(text)) {
11
+ const equals = part.indexOf('=');
12
+ const name = (equals < 0 ? part : part.slice(0, equals)).trim().toLowerCase();
13
+ const value = equals < 0 ? undefined : unquote(part.slice(equals + 1).trim());
14
+ if (name)
15
+ directives.set(name, value);
16
+ }
17
+ const maxAge = deltaSeconds(directives.get('max-age'));
18
+ return {
19
+ noCache: directives.has('no-cache'),
20
+ noStore: directives.has('no-store'),
21
+ onlyIfCached: directives.has('only-if-cached'),
22
+ ...(maxAge !== undefined && { maxAge }),
23
+ };
24
+ }
25
+ // RFC 9111 §1.2.2: a value past the largest one representable counts as 2^31.
26
+ const MAX_DELTA_SECONDS = 2_147_483_648;
27
+ /** `delta-seconds` (decimal digits only, RFC 9111 §1.2.2); anything else is no value. */
28
+ function deltaSeconds(value) {
29
+ if (value === undefined || !/^\d+$/.test(value))
30
+ return undefined;
31
+ return Math.min(Number(value), MAX_DELTA_SECONDS);
32
+ }
33
+ /**
34
+ * The one rule of a lookup served from a store. A stored row answers unless
35
+ * the request says `no-cache` or the row is older than its `max-age`;
36
+ * otherwise the provider is asked and its answer stored, unless `no-store`.
37
+ * `only-if-cached` never asks the provider: without a usable row it refuses.
38
+ */
39
+ export function decideCache(policy, storedAt, now = Date.now()) {
40
+ const ageSeconds = storedAt ? Math.max(0, Math.floor((now - storedAt.getTime()) / 1000)) : 0;
41
+ const stale = policy.maxAge !== undefined && ageSeconds > policy.maxAge;
42
+ if (storedAt && !policy.noCache && !stale)
43
+ return { action: 'serve', ageSeconds };
44
+ if (policy.onlyIfCached)
45
+ return { action: 'refuse' };
46
+ const reason = !storedAt ? 'miss' : policy.noCache ? 'request' : 'stale';
47
+ return { action: 'fetch', reason, store: !policy.noStore };
48
+ }
49
+ /**
50
+ * The directives of a header, split on the commas outside quoted strings
51
+ * (RFC 9110 §5.6.4): `ext="a,only-if-cached"` stays one unknown directive.
52
+ */
53
+ function splitDirectives(text) {
54
+ const parts = [];
55
+ let current = '';
56
+ let quoted = false;
57
+ let escaped = false;
58
+ for (const char of text) {
59
+ if (escaped)
60
+ escaped = false;
61
+ else if (quoted && char === '\\')
62
+ escaped = true;
63
+ else if (char === '"')
64
+ quoted = !quoted;
65
+ else if (char === ',' && !quoted) {
66
+ parts.push(current);
67
+ current = '';
68
+ continue;
69
+ }
70
+ current += char;
71
+ }
72
+ parts.push(current);
73
+ return parts;
74
+ }
75
+ /** A quoted-string argument without its quotes and escapes; a token as is. */
76
+ function unquote(value) {
77
+ if (!(value.length >= 2 && value.startsWith('"') && value.endsWith('"')))
78
+ return value;
79
+ return value.slice(1, -1).replace(/\\(.)/g, '$1');
80
+ }
81
+ /** A request's cache context: decides with `decideCache` and records every decision. */
82
+ export function createCacheContext(policy, now = Date.now) {
83
+ const decisions = [];
84
+ return {
85
+ policy,
86
+ decisions,
87
+ decide(storedAt) {
88
+ const decision = decideCache(policy, storedAt, now());
89
+ decisions.push(decision);
90
+ return decision;
91
+ },
92
+ };
93
+ }
94
+ //# sourceMappingURL=cache-control.util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-control.util.js","sourceRoot":"","sources":["../src/cache-control.util.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
2
+ import { HttpAdapterHost } from '@nestjs/core';
3
+ import { type Observable } from 'rxjs';
4
+ import type { CacheControlModuleOptions } from './types/cache-control-options.type.js';
5
+ /**
6
+ * Writes `Cache-Status` and `Age` from the decisions the service took, on a
7
+ * success and on an error alike (a refused `only-if-cached` is a 504).
8
+ * Headers go through the Nest HTTP adapter, so Express and Fastify behave
9
+ * the same. A failed request never says `stored`: a fetch that threw did not
10
+ * write. Routes that never read `@RequestCachePolicy()` get nothing.
11
+ */
12
+ export declare class CacheStatusInterceptor implements NestInterceptor {
13
+ private readonly options;
14
+ private readonly adapterHost;
15
+ constructor(options: CacheControlModuleOptions, adapterHost: HttpAdapterHost);
16
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
17
+ private write;
18
+ }
@@ -0,0 +1,59 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable, } from '@nestjs/common';
14
+ import { HttpAdapterHost } from '@nestjs/core';
15
+ import { catchError, tap, throwError } from 'rxjs';
16
+ import { CACHE_CONTEXT, CACHE_CONTROL_OPTIONS } from './cache-control.constants.js';
17
+ import { responseCacheHeaders } from './cache-status.util.js';
18
+ /**
19
+ * Writes `Cache-Status` and `Age` from the decisions the service took, on a
20
+ * success and on an error alike (a refused `only-if-cached` is a 504).
21
+ * Headers go through the Nest HTTP adapter, so Express and Fastify behave
22
+ * the same. A failed request never says `stored`: a fetch that threw did not
23
+ * write. Routes that never read `@RequestCachePolicy()` get nothing.
24
+ */
25
+ let CacheStatusInterceptor = class CacheStatusInterceptor {
26
+ options;
27
+ adapterHost;
28
+ constructor(options, adapterHost) {
29
+ this.options = options;
30
+ this.adapterHost = adapterHost;
31
+ }
32
+ intercept(context, next) {
33
+ return next.handle().pipe(tap(() => {
34
+ this.write(context, true);
35
+ }), catchError((error) => {
36
+ this.write(context, false);
37
+ return throwError(() => error);
38
+ }));
39
+ }
40
+ write(context, succeeded) {
41
+ const http = context.switchToHttp();
42
+ const decisions = http.getRequest()[CACHE_CONTEXT]?.decisions ?? [];
43
+ const headers = responseCacheHeaders(this.options.name, decisions, succeeded);
44
+ const { httpAdapter } = this.adapterHost;
45
+ const response = http.getResponse();
46
+ if (!headers || httpAdapter.isHeadersSent(response))
47
+ return;
48
+ httpAdapter.setHeader(response, 'Cache-Status', headers['Cache-Status']);
49
+ if (headers.Age !== undefined)
50
+ httpAdapter.setHeader(response, 'Age', headers.Age);
51
+ }
52
+ };
53
+ CacheStatusInterceptor = __decorate([
54
+ Injectable(),
55
+ __param(0, Inject(CACHE_CONTROL_OPTIONS)),
56
+ __metadata("design:paramtypes", [Object, HttpAdapterHost])
57
+ ], CacheStatusInterceptor);
58
+ export { CacheStatusInterceptor };
59
+ //# sourceMappingURL=cache-status.interceptor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-status.interceptor.js","sourceRoot":"","sources":["../src/cache-status.interceptor.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAGH,MAAM,EACN,UAAU,GAEb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAmB,GAAG,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEpE,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAQ9D;;;;;;GAMG;AAEI,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IAEqB;IAC/B;IAFrB,YACoD,OAAkC,EACjE,WAA4B;QADG,YAAO,GAAP,OAAO,CAA2B;QACjE,gBAAW,GAAX,WAAW,CAAiB;IAC9C,CAAC;IAEJ,SAAS,CAAC,OAAyB,EAAE,IAAiB;QAClD,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CACrB,GAAG,CAAC,GAAG,EAAE;YACL,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC9B,CAAC,CAAC,EACF,UAAU,CAAC,CAAC,KAAc,EAAE,EAAE;YAC1B,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC3B,OAAO,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;QACnC,CAAC,CAAC,CACL,CAAC;IACN,CAAC;IAEO,KAAK,CAAC,OAAyB,EAAE,SAAkB;QACvD,MAAM,IAAI,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;QACpC,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAgB,CAAC,aAAa,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;QAClF,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;QAC9E,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC;QACzC,MAAM,QAAQ,GAAY,IAAI,CAAC,WAAW,EAAE,CAAC;QAC7C,IAAI,CAAC,OAAO,IAAI,WAAW,CAAC,aAAa,CAAC,QAAQ,CAAC;YAAE,OAAO;QAE5D,WAAW,CAAC,SAAS,CAAC,QAAQ,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC;QACzE,IAAI,OAAO,CAAC,GAAG,KAAK,SAAS;YAAE,WAAW,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACvF,CAAC;CACJ,CAAA;AA7BY,sBAAsB;IADlC,UAAU,EAAE;IAGJ,WAAA,MAAM,CAAC,qBAAqB,CAAC,CAAA;6CACA,eAAe;GAHxC,sBAAsB,CA6BlC"}
@@ -0,0 +1,20 @@
1
+ import type { CacheDecision } from './types/cache-policy.type.js';
2
+ /** The response headers describing one decision. */
3
+ export interface CacheStatusHeaders {
4
+ 'Cache-Status': string;
5
+ Age?: string;
6
+ }
7
+ /**
8
+ * `Cache-Status` (RFC 9211) for one decision, plus `Age` (RFC 9111 §5.1) on
9
+ * a hit: `hit`; `fwd=miss|request|stale`, with `stored` only when the answer
10
+ * was written (`stored: false` for a request that failed after deciding); a
11
+ * refusal carries no `fwd` because nothing was forwarded.
12
+ */
13
+ export declare function cacheStatusHeaders(name: string, decision: CacheDecision, stored?: boolean): CacheStatusHeaders;
14
+ /**
15
+ * The headers of a whole response: those of its decision when it took one.
16
+ * Several lookups (a batch) describe their items in the body instead: one
17
+ * header cannot tell them apart. `succeeded` false (the request failed)
18
+ * drops `stored`: whatever the decision allowed, nothing was written.
19
+ */
20
+ export declare function responseCacheHeaders(name: string, decisions: readonly CacheDecision[], succeeded?: boolean): CacheStatusHeaders | undefined;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `Cache-Status` (RFC 9211) for one decision, plus `Age` (RFC 9111 §5.1) on
3
+ * a hit: `hit`; `fwd=miss|request|stale`, with `stored` only when the answer
4
+ * was written (`stored: false` for a request that failed after deciding); a
5
+ * refusal carries no `fwd` because nothing was forwarded.
6
+ */
7
+ export function cacheStatusHeaders(name, decision, stored = true) {
8
+ switch (decision.action) {
9
+ case 'serve':
10
+ return { 'Cache-Status': `${name}; hit`, Age: String(decision.ageSeconds) };
11
+ case 'fetch': {
12
+ const written = decision.store && stored ? '; stored' : '';
13
+ return { 'Cache-Status': `${name}; fwd=${decision.reason}${written}` };
14
+ }
15
+ case 'refuse':
16
+ return { 'Cache-Status': `${name}; detail=not-cached` };
17
+ }
18
+ }
19
+ /**
20
+ * The headers of a whole response: those of its decision when it took one.
21
+ * Several lookups (a batch) describe their items in the body instead: one
22
+ * header cannot tell them apart. `succeeded` false (the request failed)
23
+ * drops `stored`: whatever the decision allowed, nothing was written.
24
+ */
25
+ export function responseCacheHeaders(name, decisions, succeeded = true) {
26
+ const [decision, ...others] = decisions;
27
+ return decision && others.length === 0
28
+ ? cacheStatusHeaders(name, decision, succeeded)
29
+ : undefined;
30
+ }
31
+ //# sourceMappingURL=cache-status.util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-status.util.js","sourceRoot":"","sources":["../src/cache-status.util.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAC9B,IAAY,EACZ,QAAuB,EACvB,MAAM,GAAG,IAAI;IAEb,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,OAAO;YACR,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAChF,KAAK,OAAO,CAAC,CAAC,CAAC;YACX,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3D,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,SAAS,QAAQ,CAAC,MAAM,GAAG,OAAO,EAAE,EAAE,CAAC;QAC3E,CAAC;QACD,KAAK,QAAQ;YACT,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,qBAAqB,EAAE,CAAC;IAChE,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAChC,IAAY,EACZ,SAAmC,EACnC,SAAS,GAAG,IAAI;IAEhB,MAAM,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,GAAG,SAAS,CAAC;IACxC,OAAO,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAClC,CAAC,CAAC,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,CAAC;QAC/C,CAAC,CAAC,SAAS,CAAC;AACpB,CAAC"}
@@ -0,0 +1,11 @@
1
+ export { ApiCacheControl } from './api-cache-control.decorator.js';
2
+ export { NOT_CACHED } from './cache-control.constants.js';
3
+ export { CacheControlModule } from './cache-control.module.js';
4
+ export { createCacheContext, decideCache, parseCacheControl } from './cache-control.util.js';
5
+ export { CacheStatusInterceptor } from './cache-status.interceptor.js';
6
+ export { cacheStatusHeaders, type CacheStatusHeaders } from './cache-status.util.js';
7
+ export { NotCachedException } from './not-cached.exception.js';
8
+ export { RequestCachePolicy } from './request-cache-policy.decorator.js';
9
+ export type { CacheContext } from './types/cache-context.type.js';
10
+ export type { CacheControlModuleOptions } from './types/cache-control-options.type.js';
11
+ export type { CacheDecision, CachePolicy, FetchReason } from './types/cache-policy.type.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { ApiCacheControl } from './api-cache-control.decorator.js';
2
+ export { NOT_CACHED } from './cache-control.constants.js';
3
+ export { CacheControlModule } from './cache-control.module.js';
4
+ export { createCacheContext, decideCache, parseCacheControl } from './cache-control.util.js';
5
+ export { CacheStatusInterceptor } from './cache-status.interceptor.js';
6
+ export { cacheStatusHeaders } from './cache-status.util.js';
7
+ export { NotCachedException } from './not-cached.exception.js';
8
+ export { RequestCachePolicy } from './request-cache-policy.decorator.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC7F,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAA2B,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { GatewayTimeoutException } from '@nestjs/common';
2
+ /**
3
+ * What an `only-if-cached` lookup answers without a usable row: 504, as
4
+ * RFC 9111 §5.2.1.7 asks, with `code: NOT_CACHED` to tell it from a gateway
5
+ * that timed out.
6
+ */
7
+ export declare class NotCachedException extends GatewayTimeoutException {
8
+ constructor(message?: string);
9
+ }
@@ -0,0 +1,13 @@
1
+ import { GatewayTimeoutException } from '@nestjs/common';
2
+ import { NOT_CACHED } from './cache-control.constants.js';
3
+ /**
4
+ * What an `only-if-cached` lookup answers without a usable row: 504, as
5
+ * RFC 9111 §5.2.1.7 asks, with `code: NOT_CACHED` to tell it from a gateway
6
+ * that timed out.
7
+ */
8
+ export class NotCachedException extends GatewayTimeoutException {
9
+ constructor(message = 'Not in the store: only-if-cached forbids asking the provider.') {
10
+ super({ code: NOT_CACHED, message });
11
+ }
12
+ }
13
+ //# sourceMappingURL=not-cached.exception.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"not-cached.exception.js","sourceRoot":"","sources":["../src/not-cached.exception.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE1D;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,uBAAuB;IAC3D,YAAY,OAAO,GAAG,+DAA+D;QACjF,KAAK,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;IACzC,CAAC;CACJ"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The request's `Cache-Control` as a `CacheContext` for the service. The
3
+ * context stays on the request, where `CacheStatusInterceptor` reads the
4
+ * decisions to write `Cache-Status` and `Age`.
5
+ */
6
+ export declare const RequestCachePolicy: (...dataOrPipes: unknown[]) => ParameterDecorator;
@@ -0,0 +1,14 @@
1
+ import { createParamDecorator } from '@nestjs/common';
2
+ import { CACHE_CONTEXT } from './cache-control.constants.js';
3
+ import { createCacheContext, parseCacheControl } from './cache-control.util.js';
4
+ /**
5
+ * The request's `Cache-Control` as a `CacheContext` for the service. The
6
+ * context stays on the request, where `CacheStatusInterceptor` reads the
7
+ * decisions to write `Cache-Status` and `Age`.
8
+ */
9
+ export const RequestCachePolicy = createParamDecorator((_data, context) => {
10
+ const request = context.switchToHttp().getRequest();
11
+ request[CACHE_CONTEXT] ??= createCacheContext(parseCacheControl(request.headers['cache-control']));
12
+ return request[CACHE_CONTEXT];
13
+ });
14
+ //# sourceMappingURL=request-cache-policy.decorator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-cache-policy.decorator.js","sourceRoot":"","sources":["../src/request-cache-policy.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAyB,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAQhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAClD,CAAC,KAAc,EAAE,OAAyB,EAAgB,EAAE;IACxD,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAgB,CAAC;IAClE,OAAO,CAAC,aAAa,CAAC,KAAK,kBAAkB,CACzC,iBAAiB,CAAC,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CACtD,CAAC;IACF,OAAO,OAAO,CAAC,aAAa,CAAC,CAAC;AAClC,CAAC,CACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { CacheDecision, CachePolicy } from './cache-policy.type.js';
2
+ /**
3
+ * The cache policy of one request, handed to the service by
4
+ * `@RequestCachePolicy()`. Every `decide` is recorded: the interceptor turns
5
+ * the decisions into `Cache-Status` and `Age`, so the service never touches
6
+ * the response.
7
+ */
8
+ export interface CacheContext {
9
+ readonly policy: CachePolicy;
10
+ /** Rules on one stored row (`null` when there is none) and records the decision. */
11
+ decide(storedAt: Date | null): CacheDecision;
12
+ /** The decisions taken so far, in order: one per lookup of the request. */
13
+ readonly decisions: readonly CacheDecision[];
14
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cache-context.type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-context.type.js","sourceRoot":"","sources":["../../src/types/cache-context.type.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ export interface CacheControlModuleOptions {
2
+ /** The cache's name in `Cache-Status` (RFC 9211), e.g. the service name. */
3
+ name: string;
4
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cache-control-options.type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-control-options.type.js","sourceRoot":"","sources":["../../src/types/cache-control-options.type.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ /** The request directives of RFC 9111 a lookup honours; any other directive is ignored. */
2
+ export interface CachePolicy {
3
+ /** `no-cache`: never answer from the store without asking the provider. */
4
+ noCache: boolean;
5
+ /** `no-store`: never write the provider's answer to the store. */
6
+ noStore: boolean;
7
+ /** `only-if-cached`: never ask the provider; without a usable row, refuse. */
8
+ onlyIfCached: boolean;
9
+ /** `max-age=N`: a stored row older than N seconds is not usable. */
10
+ maxAge?: number;
11
+ }
12
+ /** Why a lookup goes to the provider: no row, a `no-cache` request, or a row past `max-age`. */
13
+ export type FetchReason = 'miss' | 'request' | 'stale';
14
+ /** What a lookup does for one stored row, as `decideCache` rules it. */
15
+ export type CacheDecision = {
16
+ action: 'serve';
17
+ ageSeconds: number;
18
+ } | {
19
+ action: 'fetch';
20
+ reason: FetchReason;
21
+ store: boolean;
22
+ } | {
23
+ action: 'refuse';
24
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cache-policy.type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-policy.type.js","sourceRoot":"","sources":["../../src/types/cache-policy.type.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
- "name": "@seta-labs/nestjs-cache-control",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@seta-labs/nestjs-cache-control",
3
+ "version": "0.1.0",
4
+ "description": "RFC 9111 request cache directives for NestJS lookups served from a store: no-cache, no-store, max-age and only-if-cached decided in one place, Cache-Status and Age on the response, a 504 for a refused lookup",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Seta-Labs/node-common.git",
9
+ "directory": "packages/nestjs-cache-control"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "test": "vitest run",
29
+ "typecheck": "tsc -p tsconfig.json --noEmit"
30
+ },
31
+ "peerDependencies": {
32
+ "@nestjs/common": "^11.0.0 || ^12.0.0",
33
+ "@nestjs/core": "^11.0.0 || ^12.0.0",
34
+ "@nestjs/swagger": "^11.0.0 || ^12.0.0",
35
+ "rxjs": "^7.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@nestjs/common": "^12.1.2",
39
+ "@nestjs/core": "^12.1.2",
40
+ "@nestjs/platform-express": "^12.1.2",
41
+ "@nestjs/swagger": "^12.0.2",
42
+ "@types/node": "^26.6.5",
43
+ "reflect-metadata": "^0.2.2",
44
+ "rxjs": "^7.8.1",
45
+ "typescript": "^6.0.3",
46
+ "vitest": "^5.0.3"
47
+ }
48
+ }