@travetto/web 7.1.4 → 8.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,7 +88,7 @@ export class WebResponse<B = unknown> extends BaseWebMessage<B, WebResponseConte
88
88
  }
89
89
  ```
90
90
 
91
- These objects do not represent the underlying sockets provided by various http servers, but in fact are simple wrappers that track the flow through the call stack of the various [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)s and the [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14) handler. One of the biggest departures here, is that the response is not an entity that is passed around from call-site to call-site, but is is solely a return-value. This doesn't mean the return value has to be static and pre-allocated, on the contrary streams are still supported. The difference here is that the streams/asynchronous values will be consumed until the response is sent back to the user. The [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L44) is a good reference for transforming a [WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3) that can either be a stream or a fixed value.
91
+ These objects do not represent the underlying sockets provided by various http servers, but in fact are simple wrappers that track the flow through the call stack of the various [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)s and the [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14) handler. One of the biggest departures here, is that the response is not an entity that is passed around from call-site to call-site, but is is solely a return-value. This doesn't mean the return value has to be static and pre-allocated, on the contrary streams are still supported. The difference here is that the streams/asynchronous values will be consumed until the response is sent back to the user. The [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L51) is a good reference for transforming a [WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3) that can either be a stream or a fixed value.
92
92
 
93
93
  ## Defining a Controller
94
94
  To start, we must define a [@Controller](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/controller.ts#L12), which is only allowed on classes. Controllers can be configured with:
@@ -145,7 +145,7 @@ class SimpleController {
145
145
  @Get('/')
146
146
  async simpleGet() {
147
147
  let data: Data | undefined;
148
- //
148
+ // Do work
149
149
  return data;
150
150
  }
151
151
  }
@@ -378,8 +378,8 @@ Out of the box, the web framework comes with a few interceptors, and more are co
378
378
  1. global - Intended to run outside of the request flow - [AsyncContextInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/context.ts#L13)
379
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)
380
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)
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#L33), [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)
382
- 1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L44), [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
+ 1. request - Handles inbound request, validation, and body preparation - [DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L51), [AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#L33), [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#L61)
382
+ 1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L51), [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#L41), [CacheControlInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cache-control.ts#L23)
383
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.
384
384
 
385
385
  ### Packaged Interceptors
@@ -445,7 +445,7 @@ export class AcceptConfig {
445
445
  ```
446
446
 
447
447
  #### DecompressInterceptor
448
- [DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L53) handles decompressing the inbound request, if supported. This relies upon HTTP standards for content encoding, and negotiating the appropriate decompression scheme.
448
+ [DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L51) handles decompressing the inbound request, if supported. This relies upon HTTP standards for content encoding, and negotiating the appropriate decompression scheme.
449
449
 
450
450
  **Code: Decompress Config**
451
451
  ```typescript
@@ -462,7 +462,7 @@ export class DecompressConfig {
462
462
  ```
463
463
 
464
464
  #### CookieInterceptor
465
- [CookieInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookie.ts#L60) is responsible for processing inbound cookie headers and populating the appropriate data on the request, as well as sending the appropriate response data
465
+ [CookieInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookie.ts#L61) is responsible for processing inbound cookie headers and populating the appropriate data on the request, as well as sending the appropriate response data
466
466
 
467
467
  **Code: Cookies Config**
468
468
  ```typescript
@@ -532,7 +532,7 @@ export class WebBodyConfig {
532
532
  ```
533
533
 
534
534
  #### CompressInterceptor
535
- [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L44) by default, will compress all valid outbound responses over a certain size, or for streams will cache every response. This relies on Node's [Buffer](https://nodejs.org/api/zlib.html) support for compression.
535
+ [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L51) by default, will compress all valid outbound responses over a certain size, or for streams will cache every response. This relies on Node's [Buffer](https://nodejs.org/api/zlib.html) support for compression.
536
536
 
537
537
  **Code: Compress Config**
538
538
  ```typescript
@@ -544,7 +544,7 @@ export class CompressConfig {
544
544
  /**
545
545
  * Raw encoding options
546
546
  */
547
- raw?: (ZlibOptions & BrotliOptions) | undefined;
547
+ raw?: (zlib.ZlibOptions & zlib.BrotliOptions) | undefined;
548
548
  /**
549
549
  * Supported encodings
550
550
  */
@@ -553,7 +553,7 @@ export class CompressConfig {
553
553
  ```
554
554
 
555
555
  #### EtagInterceptor
556
- [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.
556
+ [EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#L41) 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.
557
557
 
558
558
  **Code: ETag Config**
559
559
  ```typescript
@@ -740,16 +740,18 @@ Cookies are a unique element, within the framework, as they sit on the request a
740
740
  **Code: CookieJar contract**
741
741
  ```typescript
742
742
  export class CookieJar {
743
- constructor({ keys, ...options }: CookieJarOptions = {});
744
- import(cookies: Cookie[]): this;
743
+ constructor(options: CookieJarOptions = {}, keys?: string[] | KeyGrip);
744
+ get shouldSign(): boolean;
745
+ async export(cookie: Cookie, response?: boolean): Promise<string[]>;
746
+ async import(cookies: Cookie[]): Promise<void>;
745
747
  has(name: string, options: CookieGetOptions = {}): boolean;
746
748
  get(name: string, options: CookieGetOptions = {}): string | undefined;
747
749
  set(cookie: Cookie): void;
748
750
  getAll(): Cookie[];
749
- importCookieHeader(header: string | null | undefined): this;
750
- importSetCookieHeader(headers: string[] | null | undefined): this;
751
- exportCookieHeader(): string;
752
- exportSetCookieHeader(): string[];
751
+ importCookieHeader(header: string | null | undefined): Promise<void>;
752
+ importSetCookieHeader(headers: string[] | null | undefined): Promise<void>;
753
+ exportCookieHeader(): Promise<string>;
754
+ exportSetCookieHeader(): Promise<string[]>;
753
755
  }
754
756
  ```
755
757
 
package/__index__.ts CHANGED
@@ -7,6 +7,7 @@ export * from './src/types/error.ts';
7
7
  export * from './src/types/cookie.ts';
8
8
  export * from './src/types/interceptor.ts';
9
9
  export * from './src/types/headers.ts';
10
+ export * from './src/types/message.ts';
10
11
 
11
12
  export * from './src/context.ts';
12
13
  export * from './src/config.ts';
@@ -43,4 +44,4 @@ export * from './src/util/header.ts';
43
44
  export * from './src/util/cookie.ts';
44
45
  export * from './src/util/common.ts';
45
46
  export * from './src/util/keygrip.ts';
46
- export * from './src/util/net.ts';
47
+ export * from './src/util/net.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/web",
3
- "version": "7.1.4",
3
+ "version": "8.0.0-alpha.0",
4
4
  "type": "module",
5
5
  "description": "Declarative support for creating Web Applications",
6
6
  "keywords": [
@@ -26,18 +26,18 @@
26
26
  "directory": "module/web"
27
27
  },
28
28
  "dependencies": {
29
- "@travetto/config": "^7.1.4",
30
- "@travetto/context": "^7.1.4",
31
- "@travetto/di": "^7.1.4",
32
- "@travetto/registry": "^7.1.4",
33
- "@travetto/runtime": "^7.1.4",
34
- "@travetto/schema": "^7.1.4",
35
- "find-my-way": "^9.4.0"
29
+ "@travetto/config": "^8.0.0-alpha.0",
30
+ "@travetto/context": "^8.0.0-alpha.0",
31
+ "@travetto/di": "^8.0.0-alpha.0",
32
+ "@travetto/registry": "^8.0.0-alpha.0",
33
+ "@travetto/runtime": "^8.0.0-alpha.0",
34
+ "@travetto/schema": "^8.0.0-alpha.0",
35
+ "find-my-way": "^9.5.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@travetto/cli": "^7.1.4",
39
- "@travetto/test": "^7.1.4",
40
- "@travetto/transformer": "^7.1.3"
38
+ "@travetto/cli": "^8.0.0-alpha.0",
39
+ "@travetto/test": "^8.0.0-alpha.0",
40
+ "@travetto/transformer": "^8.0.0-alpha.0"
41
41
  },
42
42
  "peerDependenciesMeta": {
43
43
  "@travetto/transformer": {
package/src/context.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AsyncContextValue, type AsyncContext } from '@travetto/context';
2
2
  import { Inject, Injectable } from '@travetto/di';
3
- import { AppError, castTo, type Class } from '@travetto/runtime';
3
+ import { RuntimeError, castTo, type Class } from '@travetto/runtime';
4
4
 
5
5
  import { WebRequest } from './types/request.ts';
6
6
 
@@ -38,7 +38,7 @@ export class WebAsyncContext {
38
38
  getSource<T>(cls: Class<T>): () => T {
39
39
  const item = this.#byType.get(cls.Ⲑid);
40
40
  if (!item) {
41
- throw new AppError('Unknown type for web context');
41
+ throw new RuntimeError('Unknown type for web context');
42
42
  }
43
43
  return castTo(item);
44
44
  }
@@ -1,4 +1,4 @@
1
- import { type Class, type ClassInstance, getClass, type RetainPrimitiveFields, type TimeSpan, TimeUtil } from '@travetto/runtime';
1
+ import { type Class, type ClassInstance, getClass, type RetainIntrinsicFields, type TimeSpan, TimeUtil } from '@travetto/runtime';
2
2
 
3
3
  import { ControllerRegistryIndex } from '../registry/registry-index.ts';
4
4
  import type { EndpointConfig, ControllerConfig, EndpointDecorator, EndpointFunctionDescriptor } from '../registry/types.ts';
@@ -49,7 +49,7 @@ export function CacheControl(input: TimeSpan | number | CacheControlInput, extra
49
49
  const { cacheableAge, isPrivate } = input;
50
50
  return register({
51
51
  responseContext: {
52
- ...(cacheableAge !== undefined ? { cacheableAge: TimeUtil.asSeconds(cacheableAge) } : {}),
52
+ ...(cacheableAge !== undefined ? { cacheableAge: TimeUtil.duration(cacheableAge, 's') } : {}),
53
53
  ...isPrivate !== undefined ? { isPrivate } : {}
54
54
  }
55
55
  });
@@ -74,7 +74,7 @@ export function Accepts(types: [string, ...string[]]): EndpointDecorator {
74
74
  */
75
75
  export const ConfigureInterceptor = <T extends WebInterceptor>(
76
76
  cls: Class<T>,
77
- config: Partial<RetainPrimitiveFields<T['config']>>,
77
+ config: Partial<RetainIntrinsicFields<T['config']>>,
78
78
  extra?: Partial<EndpointConfig & ControllerConfig>
79
79
  ): EndpointDecorator =>
80
80
  ControllerRegistryIndex.createInterceptorConfigDecorator(cls, config, extra);
@@ -61,7 +61,7 @@ export class AcceptInterceptor implements WebInterceptor<AcceptConfig> {
61
61
  this.validate(request, config);
62
62
  return response = await next();
63
63
  } catch (error) {
64
- throw response = await WebCommonUtil.catchResponse(error);
64
+ throw response = WebCommonUtil.catchResponse(error);
65
65
  } finally {
66
66
  response?.headers.setIfAbsent('Accept', config.types.join(','));
67
67
  }
@@ -78,7 +78,7 @@ export class BodyInterceptor implements WebInterceptor<WebBodyConfig> {
78
78
  async filter({ request, config, next }: WebChainedContext<WebBodyConfig>): Promise<WebResponse> {
79
79
  const input = request.body;
80
80
 
81
- if (!WebBodyUtil.isRaw(input)) {
81
+ if (!WebBodyUtil.isRawBinary(input)) {
82
82
  return next();
83
83
  }
84
84
 
@@ -1,8 +1,9 @@
1
- import { buffer } from 'node:stream/consumers';
2
- import { type BrotliOptions, constants, createBrotliCompress, createDeflate, createGzip, type ZlibOptions } from 'node:zlib';
1
+ import zlib from 'node:zlib';
2
+ import util from 'node:util';
3
3
 
4
4
  import { Injectable, Inject } from '@travetto/di';
5
5
  import { Config } from '@travetto/config';
6
+ import { BinaryUtil } from '@travetto/runtime';
6
7
 
7
8
  import type { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
8
9
  import type { WebInterceptorCategory } from '../types/core.ts';
@@ -13,13 +14,19 @@ import { WebError } from '../types/error.ts';
13
14
  import { WebBodyUtil } from '../util/body.ts';
14
15
  import { WebHeaderUtil } from '../util/header.ts';
15
16
 
16
- const COMPRESSORS = {
17
- gzip: createGzip,
18
- deflate: createDeflate,
19
- br: createBrotliCompress,
17
+ const STREAM_COMPRESSORS = {
18
+ gzip: zlib.createGzip,
19
+ deflate: zlib.createDeflate,
20
+ br: zlib.createBrotliCompress,
20
21
  };
21
22
 
22
- type WebCompressEncoding = keyof typeof COMPRESSORS | 'identity';
23
+ const ARRAY_COMPRESSORS = {
24
+ gzip: util.promisify(zlib.gzip),
25
+ deflate: util.promisify(zlib.deflate),
26
+ br: util.promisify(zlib.brotliCompress),
27
+ };
28
+
29
+ type WebCompressEncoding = keyof typeof ARRAY_COMPRESSORS | 'identity';
23
30
 
24
31
  @Config('web.compress')
25
32
  export class CompressConfig {
@@ -30,7 +37,7 @@ export class CompressConfig {
30
37
  /**
31
38
  * Raw encoding options
32
39
  */
33
- raw?: (ZlibOptions & BrotliOptions) | undefined;
40
+ raw?: (zlib.ZlibOptions & zlib.BrotliOptions) | undefined;
34
41
  /**
35
42
  * Supported encodings
36
43
  */
@@ -63,38 +70,38 @@ export class CompressInterceptor implements WebInterceptor {
63
70
  }
64
71
 
65
72
  const accepts = request.headers.get('Accept-Encoding');
66
- const type = WebHeaderUtil.negotiateHeader(accepts ?? '*', supportedEncodings);
73
+ const encoding = WebHeaderUtil.negotiateHeader(accepts ?? '*', supportedEncodings);
67
74
 
68
- if (!type) {
75
+ if (!encoding) {
69
76
  throw WebError.for(`Please accept one of: ${supportedEncodings.join(', ')}. ${accepts} is not supported`, 406);
70
77
  }
71
78
 
72
- if (type === 'identity') {
79
+ if (encoding === 'identity') {
73
80
  return response;
74
81
  }
75
82
 
76
83
  const binaryResponse = new WebResponse({ context: response.context, ...WebBodyUtil.toBinaryMessage(response) });
77
- const chunkSize = raw.chunkSize ?? constants.Z_DEFAULT_CHUNK;
78
- const len = Buffer.isBuffer(binaryResponse.body) ? binaryResponse.body.byteLength : undefined;
84
+ const chunkSize = raw.chunkSize ?? zlib.constants.Z_DEFAULT_CHUNK;
85
+ const len = BinaryUtil.isBinaryArray(binaryResponse.body) ? binaryResponse.body.byteLength : undefined;
79
86
 
80
87
  if (len !== undefined && len >= 0 && len < chunkSize || !binaryResponse.body) {
81
88
  return binaryResponse;
82
89
  }
83
90
 
84
- const options = type === 'br' ? { params: { [constants.BROTLI_PARAM_QUALITY]: 4, ...raw.params }, ...raw } : { ...raw };
85
- const stream = COMPRESSORS[type](options);
91
+ const options = encoding === 'br' ? { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4, ...raw.params }, ...raw } : { ...raw };
86
92
 
87
93
  // If we are compressing
88
- binaryResponse.headers.set('Content-Encoding', type);
94
+ binaryResponse.headers.set('Content-Encoding', encoding);
89
95
 
90
- if (Buffer.isBuffer(binaryResponse.body)) {
91
- stream.end(binaryResponse.body);
92
- const out = await buffer(stream);
96
+ if (BinaryUtil.isBinaryArray(binaryResponse.body)) {
97
+ const compressor = ARRAY_COMPRESSORS[encoding];
98
+ const out = await compressor(await BinaryUtil.toBuffer(binaryResponse.body), options);
93
99
  binaryResponse.body = out;
94
100
  binaryResponse.headers.set('Content-Length', `${out.byteLength}`);
95
101
  } else {
96
- binaryResponse.body.pipe(stream);
97
- binaryResponse.body = stream;
102
+ const compressedStream = STREAM_COMPRESSORS[encoding](options);
103
+ void BinaryUtil.pipeline(binaryResponse.body, compressedStream);
104
+ binaryResponse.body = compressedStream;
98
105
  binaryResponse.headers.delete('Content-Length');
99
106
  }
100
107
 
@@ -10,6 +10,7 @@ import type { WebInterceptorCategory } from '../types/core.ts';
10
10
 
11
11
  import type { WebConfig } from '../config.ts';
12
12
  import type { Cookie, CookieSetOptions } from '../types/cookie.ts';
13
+ import { KeyGrip } from '../util/keygrip.ts';
13
14
  import { CookieJar } from '../util/cookie.ts';
14
15
  import type { WebAsyncContext } from '../context.ts';
15
16
 
@@ -61,6 +62,8 @@ export class CookieInterceptor implements WebInterceptor<CookieConfig> {
61
62
 
62
63
  #cookieJar = new AsyncContextValue<CookieJar>(this);
63
64
 
65
+ keyGrip: KeyGrip;
66
+
64
67
  category: WebInterceptorCategory = 'request';
65
68
 
66
69
  @Inject()
@@ -77,13 +80,13 @@ export class CookieInterceptor implements WebInterceptor<CookieConfig> {
77
80
 
78
81
  postConstruct(): void {
79
82
  this.webAsyncContext.registerSource(CookieJar, () => this.#cookieJar.get());
83
+ this.keyGrip ??= new KeyGrip(this.config.keys ?? []);
80
84
  }
81
85
 
82
86
  finalizeConfig({ config }: WebInterceptorContext<CookieConfig>): CookieConfig {
83
87
  const url = new URL(this.webConfig.baseUrl ?? 'x://localhost');
84
88
  config.secure ??= url.protocol === 'https:';
85
89
  config.domain ??= url.hostname;
86
- config.signed ??= !!config.keys?.length;
87
90
  return config;
88
91
  }
89
92
 
@@ -92,11 +95,12 @@ export class CookieInterceptor implements WebInterceptor<CookieConfig> {
92
95
  }
93
96
 
94
97
  async filter({ request, config, next }: WebChainedContext<CookieConfig>): Promise<WebResponse> {
95
- const jar = new CookieJar(config).importCookieHeader(request.headers.get('Cookie'));
98
+ const jar = new CookieJar(config, this.keyGrip);
99
+ await jar.importCookieHeader(request.headers.get('Cookie'));
96
100
  this.#cookieJar.set(jar);
97
101
 
98
102
  const response = await next();
99
- for (const cookie of jar.exportSetCookieHeader()) { response.headers.append('Set-Cookie', cookie); }
103
+ for (const cookie of await jar.exportSetCookieHeader()) { response.headers.append('Set-Cookie', cookie); }
100
104
  return response;
101
105
  }
102
106
  }
@@ -1,10 +1,10 @@
1
- import type { Readable } from 'node:stream';
2
1
  import zlib from 'node:zlib';
3
2
  import util from 'node:util';
3
+ import { pipeline } from 'node:stream/promises';
4
4
 
5
5
  import { Injectable, Inject } from '@travetto/di';
6
6
  import { Config } from '@travetto/config';
7
- import { castTo } from '@travetto/runtime';
7
+ import { BinaryUtil, castTo, type BinaryType } from '@travetto/runtime';
8
8
 
9
9
  import type { WebChainedContext } from '../types/filter.ts';
10
10
  import type { WebResponse } from '../types/response.ts';
@@ -18,18 +18,16 @@ import { WebError } from '../types/error.ts';
18
18
  const STREAM_DECOMPRESSORS = {
19
19
  gzip: zlib.createGunzip,
20
20
  deflate: zlib.createInflate,
21
- br: zlib.createBrotliDecompress,
22
- identity: (): Readable => null!
21
+ br: zlib.createBrotliDecompress
23
22
  };
24
23
 
25
- const BUFFER_DECOMPRESSORS = {
24
+ const ARRAY_DECOMPRESSORS = {
26
25
  gzip: util.promisify(zlib.gunzip),
27
26
  deflate: util.promisify(zlib.inflate),
28
- br: util.promisify(zlib.brotliDecompress),
29
- identity: (): Readable => null!
27
+ br: util.promisify(zlib.brotliDecompress)
30
28
  };
31
29
 
32
- type WebDecompressEncoding = keyof typeof BUFFER_DECOMPRESSORS;
30
+ type WebDecompressEncoding = (keyof typeof ARRAY_DECOMPRESSORS) | 'identity';
33
31
 
34
32
  /**
35
33
  * Web body parse configuration
@@ -52,8 +50,8 @@ export class DecompressConfig {
52
50
  @Injectable()
53
51
  export class DecompressInterceptor implements WebInterceptor<DecompressConfig> {
54
52
 
55
- static async decompress(headers: WebHeaders, input: Buffer | Readable, config: DecompressConfig): Promise<typeof input> {
56
- const encoding: WebDecompressEncoding | 'identity' = castTo(headers.getList('Content-Encoding')?.[0]) ?? 'identity';
53
+ static async decompress(headers: WebHeaders, input: BinaryType, config: DecompressConfig): Promise<BinaryType> {
54
+ const encoding: WebDecompressEncoding = castTo(headers.getList('Content-Encoding')?.[0]) ?? 'identity';
57
55
 
58
56
  if (!config.supportedEncodings.includes(encoding)) {
59
57
  throw WebError.for(`Unsupported Content-Encoding: ${encoding}`, 415);
@@ -63,10 +61,14 @@ export class DecompressInterceptor implements WebInterceptor<DecompressConfig> {
63
61
  return input;
64
62
  }
65
63
 
66
- if (Buffer.isBuffer(input)) {
67
- return BUFFER_DECOMPRESSORS[encoding](input);
64
+ if (BinaryUtil.isBinaryArray(input)) {
65
+ return ARRAY_DECOMPRESSORS[encoding](await BinaryUtil.toBinaryArray(input));
66
+ } else if (BinaryUtil.isBinaryStream(input)) {
67
+ const output = STREAM_DECOMPRESSORS[encoding]();
68
+ void pipeline(input, output);
69
+ return output;
68
70
  } else {
69
- return input.pipe(STREAM_DECOMPRESSORS[encoding]());
71
+ throw WebError.for('Unable to decompress body: unsupported type', 400);
70
72
  }
71
73
  }
72
74
 
@@ -81,9 +83,9 @@ export class DecompressInterceptor implements WebInterceptor<DecompressConfig> {
81
83
  }
82
84
 
83
85
  async filter({ request, config, next }: WebChainedContext<DecompressConfig>): Promise<WebResponse> {
84
- if (WebBodyUtil.isRaw(request.body)) {
86
+ if (WebBodyUtil.isRawBinary(request.body)) {
85
87
  const updatedBody = await DecompressInterceptor.decompress(request.headers, request.body, config);
86
- request.body = WebBodyUtil.markRaw(updatedBody);
88
+ request.body = WebBodyUtil.markRawBinary(updatedBody);
87
89
  }
88
90
  return next();
89
91
  }
@@ -1,9 +1,7 @@
1
- import crypto from 'node:crypto';
2
-
3
1
  import { Injectable, Inject } from '@travetto/di';
4
2
  import { Config } from '@travetto/config';
5
3
  import { Ignore } from '@travetto/schema';
6
- import { BinaryUtil } from '@travetto/runtime';
4
+ import { BinaryMetadataUtil, BinaryUtil, type BinaryArray } from '@travetto/runtime';
7
5
 
8
6
  import type { WebChainedContext } from '../types/filter.ts';
9
7
  import { WebResponse } from '../types/response.ts';
@@ -48,14 +46,10 @@ export class EtagInterceptor implements WebInterceptor {
48
46
  @Inject()
49
47
  config: EtagConfig;
50
48
 
51
- computeTag(body: Buffer): string {
49
+ computeTag(body: BinaryArray): string {
52
50
  return body.byteLength === 0 ?
53
51
  '2jmj7l5rSw0yVb/vlWAYkK/YBwk' :
54
- crypto
55
- .createHash('sha1')
56
- .update(body.toString('utf8'), 'utf8')
57
- .digest('base64')
58
- .substring(0, 27);
52
+ BinaryMetadataUtil.hash(body, { length: 27, hashAlgorithm: 'sha1', outputEncoding: 'base64' });
59
53
  }
60
54
 
61
55
  addTag(ctx: WebChainedContext<EtagConfig>, response: WebResponse): WebResponse {
@@ -65,7 +59,7 @@ export class EtagInterceptor implements WebInterceptor {
65
59
 
66
60
  if (
67
61
  (statusCode >= 300 && statusCode !== 304) || // Ignore redirects
68
- BinaryUtil.isReadableStream(response.body) // Ignore streams (unknown length)
62
+ BinaryUtil.isBinaryStream(response.body) // Ignore streams (unknown length)
69
63
  ) {
70
64
  return response;
71
65
  }
@@ -73,12 +67,12 @@ export class EtagInterceptor implements WebInterceptor {
73
67
  const binaryResponse = new WebResponse({ ...response, ...WebBodyUtil.toBinaryMessage(response) });
74
68
 
75
69
  const body = binaryResponse.body;
76
- if (!Buffer.isBuffer(body)) {
70
+ if (!BinaryUtil.isBinaryArray(body)) {
77
71
  return binaryResponse;
78
72
  }
79
73
 
80
74
  const minSize = ctx.config._minimumSize ??= WebCommonUtil.parseByteSize(ctx.config.minimumSize);
81
- if (body.length < minSize) {
75
+ if (body.byteLength < minSize) {
82
76
  return binaryResponse;
83
77
  }
84
78
 
@@ -1,5 +1,5 @@
1
1
  import type { RegistryAdapter } from '@travetto/registry';
2
- import { AppError, asFull, castTo, type Class, type RetainPrimitiveFields, safeAssign } from '@travetto/runtime';
2
+ import { RuntimeError, asFull, castTo, type Class, type RetainIntrinsicFields, safeAssign } from '@travetto/runtime';
3
3
  import { WebHeaders } from '@travetto/web';
4
4
  import { type SchemaParameterConfig, SchemaRegistryIndex } from '@travetto/schema';
5
5
 
@@ -60,7 +60,7 @@ function computeParameterLocation(endpoint: EndpointConfig, param: SchemaParamet
60
60
  if (!SchemaRegistryIndex.has(param.type)) {
61
61
  if ((param.type === String || param.type === Number) && name && endpoint.path.includes(`:${name}`)) {
62
62
  return 'path';
63
- } else if (param.type === Blob || param.type === File || param.type === ArrayBuffer || param.type === Uint8Array) {
63
+ } else if (param.binary) {
64
64
  return 'body';
65
65
  }
66
66
  return 'query';
@@ -100,7 +100,7 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
100
100
  registerEndpoint(method: string, ...data: Partial<EndpointConfig>[]): EndpointConfig {
101
101
  this.register();
102
102
 
103
- if (!this.#endpoints.has(method)) {
103
+ const resolved = this.#endpoints.getOrInsertComputed(method, () => {
104
104
  const endpointConfig = asFull<EndpointConfig>({
105
105
  path: '/',
106
106
  fullPath: '/',
@@ -117,11 +117,11 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
117
117
  responseFinalizer: undefined
118
118
  });
119
119
  this.#config.endpoints.push(endpointConfig);
120
- this.#endpoints.set(method, endpointConfig);
121
- }
120
+ return endpointConfig;
121
+ });
122
122
 
123
- combineEndpointConfigs(this.#config, this.#endpoints.get(method)!, ...data);
124
- return this.#endpoints.get(method)!;
123
+ combineEndpointConfigs(this.#config, resolved, ...data);
124
+ return resolved;
125
125
  }
126
126
 
127
127
  registerEndpointParameter(method: string, idx: number, ...config: Partial<EndpointParameterConfig>[]): EndpointParameterConfig {
@@ -156,14 +156,14 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
156
156
  getEndpointConfig(method: string): EndpointConfig {
157
157
  const endpoint = this.#endpoints.get(method);
158
158
  if (!endpoint) {
159
- throw new AppError(`Endpoint not registered: ${String(method)} on ${this.#cls.name}`);
159
+ throw new RuntimeError(`Endpoint not registered: ${String(method)} on ${this.#cls.name}`);
160
160
  }
161
161
  return endpoint;
162
162
  }
163
163
 
164
164
  registerInterceptorConfig<T extends WebInterceptor>(
165
165
  cls: Class<T>,
166
- config: Partial<RetainPrimitiveFields<T['config']>>,
166
+ config: Partial<RetainIntrinsicFields<T['config']>>,
167
167
  extra?: Partial<EndpointConfig & ControllerConfig>
168
168
  ): ControllerConfig {
169
169
  return this.register({ interceptorConfigs: [[cls, castTo(config)]], ...extra });
@@ -172,7 +172,7 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
172
172
  registerEndpointInterceptorConfig<T extends WebInterceptor>(
173
173
  property: string,
174
174
  cls: Class<T>,
175
- config: Partial<RetainPrimitiveFields<T['config']>>,
175
+ config: Partial<RetainIntrinsicFields<T['config']>>,
176
176
  extra?: Partial<EndpointConfig>
177
177
  ): EndpointConfig {
178
178
  return this.registerEndpoint(property, { interceptorConfigs: [[cls, castTo(config)]], ...extra });
@@ -1,5 +1,5 @@
1
1
  import { type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
2
- import { type Class, type ClassInstance, getClass, isClass, type RetainPrimitiveFields } from '@travetto/runtime';
2
+ import { type Class, type ClassInstance, getClass, isClass, type RetainIntrinsicFields } from '@travetto/runtime';
3
3
  import { DependencyRegistryIndex } from '@travetto/di';
4
4
  import { SchemaRegistryIndex } from '@travetto/schema';
5
5
 
@@ -39,7 +39,7 @@ export class ControllerRegistryIndex implements RegistryIndex {
39
39
  */
40
40
  static createInterceptorConfigDecorator<T extends WebInterceptor>(
41
41
  cls: Class<T>,
42
- config: Partial<RetainPrimitiveFields<T['config']>>,
42
+ config: Partial<RetainIntrinsicFields<T['config']>>,
43
43
  extra?: Partial<EndpointConfig & ControllerConfig>
44
44
  ): EndpointDecorator {
45
45
  return (instanceOrCls: Class | ClassInstance, property?: string): void => {
@@ -1,6 +1,6 @@
1
1
  import router from 'find-my-way';
2
2
 
3
- import { AppError } from '@travetto/runtime';
3
+ import { RuntimeError } from '@travetto/runtime';
4
4
  import { Inject, Injectable } from '@travetto/di';
5
5
 
6
6
  import type { EndpointConfig } from '../registry/types.ts';
@@ -44,7 +44,7 @@ export class StandardWebRouter extends BaseWebRouter {
44
44
  const endpoint = this.#cache.get(handler!);
45
45
  if (!endpoint) {
46
46
  return new WebResponse({
47
- body: new AppError(`Unknown endpoint ${httpMethod} ${request.context.path}`, { category: 'notfound' }),
47
+ body: new RuntimeError(`Unknown endpoint ${httpMethod} ${request.context.path}`, { category: 'notfound' }),
48
48
  });
49
49
  }
50
50
  Object.assign(request.context, { pathParams: params });
@@ -1,9 +1,9 @@
1
- import { type AnyMap, AppError, type ErrorCategory } from '@travetto/runtime';
1
+ import { type AnyMap, RuntimeError, type ErrorCategory } from '@travetto/runtime';
2
2
 
3
3
  /**
4
4
  * Web Error
5
5
  */
6
- export class WebError extends AppError<{ statusCode?: number }> {
6
+ export class WebError extends RuntimeError<{ statusCode?: number }> {
7
7
  static for(message: string, code: number, details?: AnyMap, category: ErrorCategory = 'data'): WebError {
8
8
  return new WebError(message, { category, details: { ...details, statusCode: code } });
9
9
  }
@@ -1,9 +1,6 @@
1
- import type { Readable } from 'node:stream';
2
1
  import { castTo } from '@travetto/runtime';
3
2
  import { WebHeaders, type WebHeadersInit } from './headers.ts';
4
3
 
5
- export type WebBinaryBody = Readable | Buffer;
6
-
7
4
  export interface WebMessageInit<B = unknown, C = unknown> {
8
5
  context?: C;
9
6
  headers?: WebHeadersInit;