@travetto/web 6.0.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,6 +68,8 @@ import { BaseWebMessage } from './message.ts';
68
68
 
69
69
  export interface WebResponseContext {
70
70
  httpStatusCode?: number;
71
+ isPrivate?: boolean;
72
+ cacheableAge?: number;
71
73
  }
72
74
 
73
75
  /**
@@ -112,13 +114,13 @@ class SimpleController {
112
114
  Once the controller is declared, each method of the controller is a candidate for being an endpoint. By design, everything is asynchronous, and so async/await is natively supported.
113
115
 
114
116
  The most common pattern is to register HTTP-driven endpoints. The HTTP methods that are currently supported:
115
- * [@Get](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L43)
116
- * [@Post](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L50)
117
- * [@Put](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L57)
118
- * [@Delete](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L70)
119
- * [@Patch](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L64)
120
- * [@Head](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L76)
121
- * [@Options](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L82)
117
+ * [@Get](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L42)
118
+ * [@Post](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L49)
119
+ * [@Put](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L56)
120
+ * [@Delete](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L69)
121
+ * [@Patch](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L63)
122
+ * [@Head](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L75)
123
+ * [@Options](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L81)
122
124
 
123
125
  Similar to the Controller, each endpoint decorator handles the following config:
124
126
  * `title` - The definition of the endpoint
@@ -249,7 +251,7 @@ class ContextController {
249
251
  }
250
252
  ```
251
253
 
252
- **Note**: When referencing the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L61) values, the contract for idempotency needs to be carefully inspected, if expected. You can see in the example above that the [CacheControl](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/common.ts#L55) decorator is used to ensure that the response is not cached.
254
+ **Note**: When referencing the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L61) values, the contract for idempotency needs to be carefully inspected, if expected. You can see in the example above that the [CacheControl](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/common.ts#L48) decorator is used to ensure that the response is not cached.
253
255
 
254
256
  ### Validating Inputs
255
257
  The module provides high level access for [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.") support, via decorators, for validating and typing request inputs.
@@ -377,7 +379,7 @@ Out of the box, the web framework comes with a few interceptors, and more are co
377
379
  1. terminal - Handles once request and response are finished building - [LoggingInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/logging.ts#L28), [RespondInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/respond.ts#L12)
378
380
  1. pre-request - Prepares the request for running - [TrustProxyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/trust-proxy.ts#L23)
379
381
  1. request - Handles inbound request, validation, and body preparation - [DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L53), [AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#L34), [BodyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body.ts#L57), [CookieInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookie.ts#L60)
380
- 1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L50), [CorsInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cors.ts#L51), [EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#L34), [ResponseCacheInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/response-cache.ts#L30)
382
+ 1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L50), [CorsInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cors.ts#L51), [EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#L43), [CacheControlInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cache-control.ts#L23)
381
383
  1. application - Lives outside of the general request/response behavior, [Web Auth](https://github.com/travetto/travetto/tree/main/module/auth-web#readme "Web authentication integration support for the Travetto framework") uses this for login and logout flows.
382
384
 
383
385
  ### Packaged Interceptors
@@ -514,7 +516,7 @@ export class WebBodyConfig {
514
516
  /**
515
517
  * Max body size limit
516
518
  */
517
- limit: `${number}${'mb' | 'kb' | 'gb' | 'b' | ''}` = '1mb';
519
+ limit: ByteSizeInput = '1mb';
518
520
  /**
519
521
  * How to interpret different content types
520
522
  */
@@ -555,7 +557,7 @@ export class CompressConfig {
555
557
  ```
556
558
 
557
559
  #### EtagInterceptor
558
- [EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#L34) by default, will tag all cacheable HTTP responses, when the response value/length is known. Streams, and other async data sources do not have a pre-defined length, and so are ineligible for etagging.
560
+ [EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#L43) by default, will tag all cacheable HTTP responses, when the response value/length is known. Streams, and other async data sources do not have a pre-defined length, and so are ineligible for etagging.
559
561
 
560
562
  **Code: ETag Config**
561
563
  ```typescript
@@ -568,9 +570,16 @@ export class EtagConfig {
568
570
  * Should we generate a weak etag
569
571
  */
570
572
  weak?: boolean;
573
+ /**
574
+ * Threshold for tagging avoids tagging small responses
575
+ */
576
+ minimumSize: ByteSizeInput = '10kb';
571
577
 
572
578
  @Ignore()
573
579
  cacheable?: boolean;
580
+
581
+ @Ignore()
582
+ _minimumSize: number | undefined;
574
583
  }
575
584
  ```
576
585
 
@@ -611,8 +620,12 @@ export class CorsConfig {
611
620
  }
612
621
  ```
613
622
 
614
- #### ResponseCacheInterceptor
615
- [ResponseCacheInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/response-cache.ts#L30) by default, disables caching for all GET requests if the response does not include caching headers. This can be managed by setting `web.getCache.applies: <boolean>` in your config. This interceptor applies by default.
623
+ #### CacheControlInterceptor
624
+ [CacheControlInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cache-control.ts#L23) by default, enforces whatever caching policy is established on a given endpoint using the [CacheControl](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/common.ts#L48) decorator. Additionally, the interceptor retains knowledge if it is running on a private endpoint, or not. If the endpoint is deemed private it affects the caching header accordingly. If the endpoint directly returns a `Cache-Control` header, that takes precedence and all other logic is ignored.
625
+
626
+ This can be managed by setting `web.cache.applies: <boolean>` in your config.
627
+
628
+ **Note**: The [Web Auth](https://github.com/travetto/travetto/tree/main/module/auth-web#readme "Web authentication integration support for the Travetto framework") module will mark endpoints as private if they require authentication.
616
629
 
617
630
  ### Configuring Interceptors
618
631
  All framework-provided interceptors, follow the same patterns for general configuration. This falls into three areas:
package/__index__.ts CHANGED
@@ -31,7 +31,7 @@ export * from './src/interceptor/compress.ts';
31
31
  export * from './src/interceptor/context.ts';
32
32
  export * from './src/interceptor/decompress.ts';
33
33
  export * from './src/interceptor/etag.ts';
34
- export * from './src/interceptor/response-cache.ts';
34
+ export * from './src/interceptor/cache-control.ts';
35
35
  export * from './src/interceptor/logging.ts';
36
36
  export * from './src/interceptor/respond.ts';
37
37
  export * from './src/interceptor/trust-proxy.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/web",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
4
4
  "description": "Declarative support creating for Web Applications",
5
5
  "keywords": [
6
6
  "web",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@travetto/cli": "^6.0.0",
44
- "@travetto/test": "^6.0.0",
44
+ "@travetto/test": "^6.0.1",
45
45
  "@travetto/transformer": "^6.0.0"
46
46
  },
47
47
  "peerDependenciesMeta": {
@@ -1,10 +1,9 @@
1
- import { asConstructable, castTo, Class, TimeSpan } from '@travetto/runtime';
1
+ import { asConstructable, castTo, Class, TimeSpan, TimeUtil } from '@travetto/runtime';
2
2
 
3
3
  import { ControllerRegistry } from '../registry/controller.ts';
4
4
  import { EndpointConfig, ControllerConfig, DescribableConfig, EndpointDecorator, EndpointFunctionDescriptor } from '../registry/types.ts';
5
5
  import { AcceptInterceptor } from '../interceptor/accept.ts';
6
6
  import { WebInterceptor } from '../types/interceptor.ts';
7
- import { WebCommonUtil, CacheControlFlag } from '../util/common.ts';
8
7
 
9
8
  function register(config: Partial<EndpointConfig | ControllerConfig>): EndpointDecorator {
10
9
  return function <T>(target: T | Class<T>, property?: string, descriptor?: EndpointFunctionDescriptor) {
@@ -40,27 +39,25 @@ export function SetHeaders(headers: EndpointConfig['responseHeaders']): Endpoint
40
39
  */
41
40
  export function Produces(mime: string): EndpointDecorator { return SetHeaders({ 'Content-Type': mime }); }
42
41
 
43
- /**
44
- * Specifies if endpoint should be conditional
45
- */
46
- export function ConditionalRegister(handler: () => (boolean | Promise<boolean>)): EndpointDecorator {
47
- return register({ conditional: handler });
48
- }
42
+ type CacheControlInput = { cacheableAge?: number | TimeSpan, isPrivate?: boolean };
49
43
 
50
44
  /**
51
45
  * Set the max-age of a response based on the config
52
46
  * @param value The value for the duration
53
- * @param unit The unit of measurement
54
47
  */
55
- export function CacheControl(value: number | TimeSpan, flags: CacheControlFlag[] = []): EndpointDecorator {
56
- return SetHeaders({ 'Cache-Control': WebCommonUtil.getCacheControlValue(value, flags) });
48
+ export function CacheControl(input: TimeSpan | number | CacheControlInput, extra?: Omit<CacheControlInput, 'cacheableAge'>): EndpointDecorator {
49
+ if (typeof input === 'string' || typeof input === 'number') {
50
+ input = { ...extra, cacheableAge: input };
51
+ }
52
+ const { cacheableAge, isPrivate } = input;
53
+ return register({
54
+ responseContext: {
55
+ ...(cacheableAge !== undefined ? { cacheableAge: TimeUtil.asSeconds(cacheableAge) } : {}),
56
+ ...isPrivate !== undefined ? { isPrivate } : {}
57
+ }
58
+ });
57
59
  }
58
60
 
59
- /**
60
- * Disable cache control, ensuring endpoint will not cache
61
- */
62
- export const DisableCacheControl = (): EndpointDecorator => CacheControl(0);
63
-
64
61
  /**
65
62
  * Define an endpoint to support specific input types
66
63
  * @param types The list of mime types to allow/deny
@@ -79,6 +76,13 @@ export function Accepts(types: [string, ...string[]]): EndpointDecorator {
79
76
  export const ConfigureInterceptor =
80
77
  ControllerRegistry.createInterceptorConfigDecorator.bind(ControllerRegistry);
81
78
 
79
+ /**
80
+ * Specifies if endpoint should be conditional
81
+ */
82
+ export function ConditionalRegister(handler: () => (boolean | Promise<boolean>)): EndpointDecorator {
83
+ return register({ conditional: handler });
84
+ }
85
+
82
86
  /**
83
87
  * Registers an interceptor exclusion filter
84
88
  */
@@ -20,7 +20,6 @@ export function Endpoint(config: EndpointDecConfig): EndpointFunctionDecorator {
20
20
  };
21
21
  }
22
22
 
23
-
24
23
  function HttpEndpoint(method: HttpMethod, path: string): EndpointFunctionDecorator {
25
24
  const { body: allowsBody, cacheable, emptyStatusCode } = HTTP_METHODS[method];
26
25
  return Endpoint({
@@ -9,7 +9,7 @@ import { WebInterceptorCategory } from '../types/core.ts';
9
9
  import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
10
10
 
11
11
  import { WebBodyUtil } from '../util/body.ts';
12
- import { WebCommonUtil } from '../util/common.ts';
12
+ import { ByteSizeInput, WebCommonUtil } from '../util/common.ts';
13
13
 
14
14
  import { AcceptInterceptor } from './accept.ts';
15
15
  import { DecompressInterceptor } from './decompress.ts';
@@ -35,7 +35,7 @@ export class WebBodyConfig {
35
35
  /**
36
36
  * Max body size limit
37
37
  */
38
- limit: `${number}${'mb' | 'kb' | 'gb' | 'b' | ''}` = '1mb';
38
+ limit: ByteSizeInput = '1mb';
39
39
  /**
40
40
  * How to interpret different content types
41
41
  */
@@ -0,0 +1,58 @@
1
+ import { Injectable, Inject } from '@travetto/di';
2
+ import { Config } from '@travetto/config';
3
+
4
+ import { WebChainedContext } from '../types/filter.ts';
5
+ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
6
+ import { WebInterceptorCategory } from '../types/core.ts';
7
+ import { WebResponse } from '../types/response.ts';
8
+
9
+ import { EtagInterceptor } from './etag.ts';
10
+
11
+ @Config('web.cache')
12
+ export class CacheControlConfig {
13
+ /**
14
+ * Generate response cache headers
15
+ */
16
+ applies = true;
17
+ }
18
+
19
+ /**
20
+ * Determines if we should cache all get requests
21
+ */
22
+ @Injectable()
23
+ export class CacheControlInterceptor implements WebInterceptor {
24
+
25
+ category: WebInterceptorCategory = 'response';
26
+ dependsOn = [EtagInterceptor];
27
+
28
+ @Inject()
29
+ config: CacheControlConfig;
30
+
31
+ applies({ config, endpoint }: WebInterceptorContext<CacheControlConfig>): boolean {
32
+ return config.applies && endpoint.cacheable;
33
+ }
34
+
35
+ async filter({ next }: WebChainedContext<CacheControlConfig>): Promise<WebResponse> {
36
+ const response = await next();
37
+ if (!response.headers.has('Cache-Control')) {
38
+ const parts: string[] = [];
39
+ if (response.context.isPrivate !== undefined) {
40
+ parts.push(response.context.isPrivate ? 'private' : 'public');
41
+ }
42
+ if (response.context.cacheableAge !== undefined) {
43
+ parts.push(
44
+ ...(response.context.cacheableAge <= 0 ?
45
+ ['no-store', 'max-age=0'] :
46
+ [`max-age=${response.context.cacheableAge}`]
47
+ )
48
+ );
49
+ } else if (response.context.isPrivate) { // If private, but no age, don't store
50
+ parts.push('no-store');
51
+ }
52
+ if (parts.length) {
53
+ response.headers.set('Cache-Control', parts.join(','));
54
+ }
55
+ }
56
+ return response;
57
+ }
58
+ }
@@ -4,6 +4,7 @@ import fresh from 'fresh';
4
4
  import { Injectable, Inject } from '@travetto/di';
5
5
  import { Config } from '@travetto/config';
6
6
  import { Ignore } from '@travetto/schema';
7
+ import { BinaryUtil } from '@travetto/runtime';
7
8
 
8
9
  import { WebChainedContext } from '../types/filter.ts';
9
10
  import { WebResponse } from '../types/response.ts';
@@ -11,6 +12,7 @@ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
11
12
  import { WebInterceptorCategory } from '../types/core.ts';
12
13
  import { CompressInterceptor } from './compress.ts';
13
14
  import { WebBodyUtil } from '../util/body.ts';
15
+ import { ByteSizeInput, WebCommonUtil } from '../util/common.ts';
14
16
 
15
17
  @Config('web.etag')
16
18
  export class EtagConfig {
@@ -22,9 +24,16 @@ export class EtagConfig {
22
24
  * Should we generate a weak etag
23
25
  */
24
26
  weak?: boolean;
27
+ /**
28
+ * Threshold for tagging avoids tagging small responses
29
+ */
30
+ minimumSize: ByteSizeInput = '10kb';
25
31
 
26
32
  @Ignore()
27
33
  cacheable?: boolean;
34
+
35
+ @Ignore()
36
+ _minimumSize: number | undefined;
28
37
  }
29
38
 
30
39
  /**
@@ -52,21 +61,32 @@ export class EtagInterceptor implements WebInterceptor {
52
61
  addTag(ctx: WebChainedContext<EtagConfig>, response: WebResponse): WebResponse {
53
62
  const { request } = ctx;
54
63
 
55
- const statusCode = response.context.httpStatusCode;
64
+ const statusCode = response.context.httpStatusCode ?? 200;
56
65
 
57
- if (statusCode && (statusCode >= 300 && statusCode !== 304)) {
66
+ if (
67
+ (statusCode >= 300 && statusCode !== 304) || // Ignore redirects
68
+ BinaryUtil.isReadableStream(response.body) // Ignore streams (unknown length)
69
+ ) {
58
70
  return response;
59
71
  }
60
72
 
61
73
  const binaryResponse = new WebResponse({ ...response, ...WebBodyUtil.toBinaryMessage(response) });
62
- if (!Buffer.isBuffer(binaryResponse.body)) {
74
+
75
+ const body = binaryResponse.body;
76
+ if (!Buffer.isBuffer(body)) {
77
+ return binaryResponse;
78
+ }
79
+
80
+ const minSize = ctx.config._minimumSize ??= WebCommonUtil.parseByteSize(ctx.config.minimumSize);
81
+ if (body.length < minSize) {
63
82
  return binaryResponse;
64
83
  }
65
84
 
66
- const tag = this.computeTag(binaryResponse.body);
85
+ const tag = this.computeTag(body);
67
86
  binaryResponse.headers.set('ETag', `${ctx.config.weak ? 'W/' : ''}"${tag}"`);
68
87
 
69
- if (ctx.config.cacheable &&
88
+ if (
89
+ ctx.config.cacheable &&
70
90
  fresh({
71
91
  'if-modified-since': request.headers.get('If-Modified-Since')!,
72
92
  'if-none-match': request.headers.get('If-None-Match')!,
@@ -76,7 +96,12 @@ export class EtagInterceptor implements WebInterceptor {
76
96
  'last-modified': binaryResponse.headers.get('Last-Modified')!
77
97
  })
78
98
  ) {
79
- return new WebResponse({ context: { httpStatusCode: 304 } });
99
+ // Remove length for the 304
100
+ binaryResponse.headers.delete('Content-Length');
101
+ return new WebResponse({
102
+ context: { ...response.context, httpStatusCode: 304 },
103
+ headers: binaryResponse.headers
104
+ });
80
105
  }
81
106
 
82
107
  return binaryResponse;
@@ -50,6 +50,7 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
50
50
  externalName: cls.name.replace(/(Controller|Web|Service)$/, ''),
51
51
  endpoints: [],
52
52
  contextParams: {},
53
+ responseHeaders: {}
53
54
  };
54
55
  }
55
56
 
@@ -68,7 +69,8 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
68
69
  interceptorConfigs: [],
69
70
  name: endpoint.name,
70
71
  endpoint,
71
- responseHeaderMap: new WebHeaders(),
72
+ responseHeaders: {},
73
+ finalizedResponseHeaders: new WebHeaders(),
72
74
  responseFinalizer: undefined
73
75
  };
74
76
 
@@ -199,7 +201,7 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
199
201
  * @param src Root describable (controller, endpoint)
200
202
  * @param dest Target (controller, endpoint)
201
203
  */
202
- mergeDescribable(src: Partial<ControllerConfig | EndpointConfig>, dest: Partial<ControllerConfig | EndpointConfig>): void {
204
+ mergeCommon(src: Partial<ControllerConfig | EndpointConfig>, dest: Partial<ControllerConfig | EndpointConfig>): void {
203
205
  dest.filters = [...(dest.filters ?? []), ...(src.filters ?? [])];
204
206
  dest.interceptorConfigs = [...(dest.interceptorConfigs ?? []), ...(src.interceptorConfigs ?? [])];
205
207
  dest.interceptorExclude = dest.interceptorExclude ?? src.interceptorExclude;
@@ -207,6 +209,7 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
207
209
  dest.description = src.description || dest.description;
208
210
  dest.documented = src.documented ?? dest.documented;
209
211
  dest.responseHeaders = { ...src.responseHeaders, ...dest.responseHeaders };
212
+ dest.responseContext = { ...src.responseContext, ...dest.responseContext };
210
213
  }
211
214
 
212
215
  /**
@@ -231,7 +234,7 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
231
234
  srcConf.path = `/${srcConf.path}`;
232
235
  }
233
236
 
234
- this.mergeDescribable(config, srcConf);
237
+ this.mergeCommon(config, srcConf);
235
238
 
236
239
  return descriptor;
237
240
  }
@@ -252,7 +255,7 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
252
255
  srcConf.contextParams = { ...srcConf.contextParams, ...config.contextParams };
253
256
 
254
257
 
255
- this.mergeDescribable(config, srcConf);
258
+ this.mergeCommon(config, srcConf);
256
259
  }
257
260
 
258
261
  /**
@@ -266,7 +269,8 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
266
269
  this.#endpointsById.set(ep.id, ep);
267
270
  // Store full path from base for use in other contexts
268
271
  ep.fullPath = `/${final.basePath}/${ep.path}`.replace(/[/]{1,4}/g, '/').replace(/(.)[/]$/, (_, a) => a);
269
- ep.responseHeaderMap = new WebHeaders({ ...final.responseHeaders ?? {}, ...ep.responseHeaders ?? {} });
272
+ ep.finalizedResponseHeaders = new WebHeaders({ ...final.responseHeaders, ...ep.responseHeaders });
273
+ ep.responseContext = { ...final.responseContext, ...ep.responseContext };
270
274
  }
271
275
 
272
276
  if (this.has(final.basePath)) {
@@ -5,7 +5,7 @@ import type { WebInterceptor } from '../types/interceptor.ts';
5
5
  import type { WebChainedFilter, WebFilter } from '../types/filter.ts';
6
6
  import { HttpMethod } from '../types/core.ts';
7
7
  import { WebHeaders } from '../types/headers.ts';
8
- import { WebResponse } from '../types/response.ts';
8
+ import { WebResponse, WebResponseContext } from '../types/response.ts';
9
9
  import { WebRequest } from '../types/request.ts';
10
10
 
11
11
  export type EndpointFunction = TypedFunction<Any, unknown>;
@@ -78,6 +78,10 @@ interface CoreConfig {
78
78
  * Response headers
79
79
  */
80
80
  responseHeaders?: Record<string, string>;
81
+ /**
82
+ * Partial response context
83
+ */
84
+ responseContext?: Partial<WebResponseContext>;
81
85
  }
82
86
 
83
87
  /**
@@ -167,14 +171,14 @@ export interface EndpointConfig extends CoreConfig, DescribableConfig {
167
171
  * Full path including controller
168
172
  */
169
173
  fullPath: string;
170
- /**
171
- * Response header map
172
- */
173
- responseHeaderMap: WebHeaders;
174
174
  /**
175
175
  * Response finalizer
176
176
  */
177
177
  responseFinalizer?: (res: WebResponse) => WebResponse;
178
+ /**
179
+ * Response headers finalized
180
+ */
181
+ finalizedResponseHeaders: WebHeaders;
178
182
  }
179
183
 
180
184
  /**
@@ -2,6 +2,8 @@ import { BaseWebMessage } from './message.ts';
2
2
 
3
3
  export interface WebResponseContext {
4
4
  httpStatusCode?: number;
5
+ isPrivate?: boolean;
6
+ cacheableAge?: number;
5
7
  }
6
8
 
7
9
  /**
@@ -1,17 +1,15 @@
1
- import { AppError, ErrorCategory, TimeSpan, TimeUtil } from '@travetto/runtime';
1
+ import { AppError, ErrorCategory } from '@travetto/runtime';
2
2
 
3
3
  import { WebResponse } from '../types/response.ts';
4
4
  import { WebRequest } from '../types/request.ts';
5
+ import { WebError } from '../types/error.ts';
5
6
 
6
7
  type List<T> = T[] | readonly T[];
7
8
  type OrderedState<T> = { after?: List<T>, before?: List<T>, key: T };
8
9
 
9
10
  const WebRequestParamsSymbol = Symbol();
10
11
 
11
- export type CacheControlFlag =
12
- 'must-revalidate' | 'public' | 'private' | 'no-cache' |
13
- 'no-store' | 'no-transform' | 'proxy-revalidate' | 'immutable' |
14
- 'must-understand' | 'stale-if-error' | 'stale-while-revalidate';
12
+ export type ByteSizeInput = `${number}${'mb' | 'kb' | 'gb' | 'b' | ''}` | number;
15
13
 
16
14
  /**
17
15
  * Mapping from error category to standard http error codes
@@ -103,7 +101,7 @@ export class WebCommonUtil {
103
101
  new AppError(err.message, { details: err }) :
104
102
  new AppError(`${err}`);
105
103
 
106
- const error: Error & { category?: ErrorCategory, details?: { statusCode: number } } = body;
104
+ const error: Error & Partial<WebError> = body;
107
105
  const statusCode = error.details?.statusCode ?? ERROR_CATEGORY_STATUS[error.category!] ?? 500;
108
106
 
109
107
  return new WebResponse({ body, context: { httpStatusCode: statusCode } });
@@ -123,19 +121,13 @@ export class WebCommonUtil {
123
121
  request[WebRequestParamsSymbol] ??= params;
124
122
  }
125
123
 
126
- /**
127
- * Get a cache control value
128
- */
129
- static getCacheControlValue(value: number | TimeSpan, flags: CacheControlFlag[] = []): string {
130
- const delta = TimeUtil.asSeconds(value);
131
- const finalFlags = delta === 0 ? ['no-cache'] : flags;
132
- return [...finalFlags, `max-age=${delta}`].join(',');
133
- }
134
-
135
124
  /**
136
125
  * Parse byte size
137
126
  */
138
- static parseByteSize(input: `${number}${'mb' | 'kb' | 'gb' | 'b' | ''}`): number {
127
+ static parseByteSize(input: ByteSizeInput): number {
128
+ if (typeof input === 'number') {
129
+ return input;
130
+ }
139
131
  const [, num, unit] = input.toLowerCase().split(/(\d+)/);
140
132
  return parseInt(num, 10) * (this.#unitMapping[unit] ?? 1);
141
133
  }
@@ -143,10 +143,15 @@ export class EndpointUtil {
143
143
  try {
144
144
  const params = await this.extractParameters(endpoint, request);
145
145
  const body = await endpoint.endpoint.apply(endpoint.instance, params);
146
- const headers = endpoint.responseHeaderMap;
147
- const response = body instanceof WebResponse ? body : new WebResponse({ body, headers });
148
- if (response === body) {
149
- for (const [k, v] of headers) { response.headers.setIfAbsent(k, v); }
146
+ const headers = endpoint.finalizedResponseHeaders;
147
+ let response: WebResponse;
148
+ if (body instanceof WebResponse) {
149
+ for (const [k, v] of headers) { body.headers.setIfAbsent(k, v); }
150
+ // Rewrite context
151
+ Object.assign(body.context, { ...endpoint.responseContext, ...body.context });
152
+ response = body;
153
+ } else {
154
+ response = new WebResponse({ body, headers, context: { ...endpoint.responseContext } });
150
155
  }
151
156
  return endpoint.responseFinalizer?.(response) ?? response;
152
157
  } catch (err) {
@@ -1,47 +0,0 @@
1
- import { Injectable, Inject } from '@travetto/di';
2
- import { Config } from '@travetto/config';
3
-
4
- import { WebChainedContext } from '../types/filter.ts';
5
- import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
6
- import { WebInterceptorCategory } from '../types/core.ts';
7
- import { WebResponse } from '../types/response.ts';
8
-
9
- import { WebCommonUtil } from '../util/common.ts';
10
-
11
- import { EtagInterceptor } from './etag.ts';
12
-
13
- @Config('web.cache')
14
- export class ResponseCacheConfig {
15
- /**
16
- * Generate response cache headers
17
- */
18
- applies = true;
19
-
20
- /**
21
- * Determines how we cache
22
- */
23
- mode: 'allow' | 'deny' = 'deny';
24
- }
25
-
26
- /**
27
- * Determines if we should cache all get requests
28
- */
29
- @Injectable()
30
- export class ResponseCacheInterceptor implements WebInterceptor {
31
-
32
- category: WebInterceptorCategory = 'response';
33
- dependsOn = [EtagInterceptor];
34
-
35
- @Inject()
36
- config: ResponseCacheConfig;
37
-
38
- applies({ config, endpoint }: WebInterceptorContext<ResponseCacheConfig>): boolean {
39
- return !!endpoint.cacheable && config.applies && config.mode === 'deny';
40
- }
41
-
42
- async filter({ next }: WebChainedContext<ResponseCacheConfig>): Promise<WebResponse> {
43
- const response = await next();
44
- response.headers.setIfAbsent('Cache-Control', WebCommonUtil.getCacheControlValue(0));
45
- return response;
46
- }
47
- }