@travetto/web 7.0.0-rc.2 → 7.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,6 @@
1
- import { asConstructable, castKey, castTo, Class, Runtime, TypedObject } from '@travetto/runtime';
1
+ import { asConstructable, castKey, castTo, Class, TypedObject } from '@travetto/runtime';
2
2
  import { BindUtil, SchemaParameterConfig, SchemaRegistryIndex, SchemaValidator, ValidationResultError } from '@travetto/schema';
3
3
  import { DependencyRegistryIndex } from '@travetto/di';
4
- import { RetargettingProxy } from '@travetto/registry';
5
4
 
6
5
  import { WebChainedFilter, WebChainedContext, WebFilter } from '../types/filter.ts';
7
6
  import { WebResponse } from '../types/response.ts';
@@ -65,12 +64,12 @@ export class EndpointUtil {
65
64
 
66
65
  const inputByClass = Map.groupBy(
67
66
  [...controller?.interceptorConfigs ?? [], ...endpoint.interceptorConfigs ?? []],
68
- x => x[0]
67
+ entry => entry[0]
69
68
  );
70
69
 
71
70
  const configs = new Map<Class, unknown>(interceptors.map(inst => {
72
71
  const cls = asConstructable<WebInterceptor>(inst).constructor;
73
- const inputs = (inputByClass.get(cls) ?? []).map(x => x[1]);
72
+ const inputs = (inputByClass.get(cls) ?? []).map(entry => entry[1]);
74
73
  const config = Object.assign({}, inst.config, ...inputs);
75
74
  return [cls, inst.finalizeConfig?.({ config, endpoint }, castTo(inputs)) ?? config];
76
75
  }));
@@ -96,8 +95,8 @@ export class EndpointUtil {
96
95
  case 'header': return isArray ? request.headers.getList(name) : request.headers.get(name);
97
96
  case 'query': {
98
97
  const withQuery: typeof request & { [WebQueryExpandedSymbol]?: Record<string, unknown> } = request;
99
- const q = withQuery[WebQueryExpandedSymbol] ??= BindUtil.expandPaths(request.context.httpQuery ?? {});
100
- return q[name];
98
+ const query = withQuery[WebQueryExpandedSymbol] ??= BindUtil.expandPaths(request.context.httpQuery ?? {});
99
+ return query[name];
101
100
  }
102
101
  }
103
102
  }
@@ -114,33 +113,32 @@ export class EndpointUtil {
114
113
  } else if (param.location === 'query') {
115
114
  // TODO: Revisit this logic?
116
115
  const withQuery: typeof request & { [WebQueryExpandedSymbol]?: Record<string, unknown> } = request;
117
- const q = withQuery[WebQueryExpandedSymbol] ??= BindUtil.expandPaths(request.context.httpQuery ?? {});
116
+ const query = withQuery[WebQueryExpandedSymbol] ??= BindUtil.expandPaths(request.context.httpQuery ?? {});
118
117
  if (param.prefix) { // Has a prefix provided
119
- return q[param.prefix];
118
+ return query[param.prefix];
120
119
  } else if (input.type.Ⲑid) { // Is a full type
121
- return q;
120
+ return query;
122
121
  }
123
122
  }
124
123
 
125
- let res = this.extractParameterValue(request, param, input.name!.toString(), input.array) ?? undefined;
126
- for (let i = 0; res === undefined && input.aliases && i < input.aliases.length; i += 1) {
127
- res = this.extractParameterValue(request, param, input.aliases[i], input.array) ?? undefined;
124
+ let result = this.extractParameterValue(request, param, input.name!, input.array) ?? undefined;
125
+ for (let i = 0; result === undefined && input.aliases && i < input.aliases.length; i += 1) {
126
+ result = this.extractParameterValue(request, param, input.aliases[i], input.array) ?? undefined;
128
127
  }
129
- return res;
128
+ return result;
130
129
  }
131
130
 
132
131
  /**
133
132
  * Extract all parameters for a given endpoint/request/response combo
134
133
  * @param endpoint The endpoint to extract for
135
- * @param req The request
136
- * @param res The response
134
+ * @param request The request
137
135
  */
138
136
  static async extractParameters(endpoint: EndpointConfig, request: WebRequest): Promise<unknown[]> {
139
137
  const cls = endpoint.class;
140
138
  const vals = WebCommonUtil.getRequestParams(request);
141
139
  const { parameters } = SchemaRegistryIndex.get(cls).getMethod(endpoint.methodName);
142
- const combined = parameters.map((cfg) =>
143
- ({ schema: cfg, param: endpoint.parameters[cfg.index], value: vals?.[cfg.index] }));
140
+ const combined = parameters.map((config) =>
141
+ ({ schema: config, param: endpoint.parameters[config.index], value: vals?.[config.index] }));
144
142
 
145
143
  try {
146
144
  const extracted = combined.map(({ param, schema, value }) =>
@@ -149,20 +147,20 @@ export class EndpointUtil {
149
147
  this.extractParameter(request, param, schema)
150
148
  );
151
149
  const params = BindUtil.coerceMethodParams(cls, endpoint.methodName, extracted);
152
- await SchemaValidator.validateMethod(cls, endpoint.methodName, params, endpoint.parameters.map(x => x.prefix));
150
+ await SchemaValidator.validateMethod(cls, endpoint.methodName, params, endpoint.parameters.map(paramConfig => paramConfig.prefix));
153
151
  return params;
154
- } catch (err) {
155
- if (err instanceof ValidationResultError) {
156
- for (const el of err.details?.errors ?? []) {
157
- if (el.kind === 'required') {
158
- const config = combined.find(x => x.schema.name === el.path);
152
+ } catch (error) {
153
+ if (error instanceof ValidationResultError) {
154
+ for (const validationError of error.details?.errors ?? []) {
155
+ if (validationError.kind === 'required') {
156
+ const config = combined.find(paramConfig => paramConfig.schema.name === validationError.path);
159
157
  if (config) {
160
- el.message = `Missing ${config.param.location} value: ${config.schema.name}`;
158
+ validationError.message = `Missing ${config.param.location} value: ${config.schema.name}`;
161
159
  }
162
160
  }
163
161
  }
164
162
  }
165
- throw err;
163
+ throw error;
166
164
  }
167
165
  }
168
166
 
@@ -176,7 +174,7 @@ export class EndpointUtil {
176
174
  const headers = endpoint.finalizedResponseHeaders;
177
175
  let response: WebResponse;
178
176
  if (body instanceof WebResponse) {
179
- for (const [k, v] of headers) { body.headers.setIfAbsent(k, v); }
177
+ for (const [key, value] of headers) { body.headers.setIfAbsent(key, value); }
180
178
  // Rewrite context
181
179
  Object.assign(body.context, { ...endpoint.responseContext, ...body.context });
182
180
  response = body;
@@ -184,8 +182,8 @@ export class EndpointUtil {
184
182
  response = new WebResponse({ body, headers, context: { ...endpoint.responseContext } });
185
183
  }
186
184
  return endpoint.responseFinalizer?.(response) ?? response;
187
- } catch (err) {
188
- throw WebCommonUtil.catchResponse(err);
185
+ } catch (error) {
186
+ throw WebCommonUtil.catchResponse(error);
189
187
  }
190
188
  }
191
189
 
@@ -203,7 +201,7 @@ export class EndpointUtil {
203
201
 
204
202
  // Filter interceptors if needed
205
203
  for (const filter of [controller?.interceptorExclude, endpoint.interceptorExclude]) {
206
- interceptors = filter ? interceptors.filter(x => !filter(x)) : interceptors;
204
+ interceptors = filter ? interceptors.filter(interceptor => !filter(interceptor)) : interceptors;
207
205
  }
208
206
 
209
207
  const interceptorFilters =
@@ -214,7 +212,7 @@ export class EndpointUtil {
214
212
  const endpointFilters = [
215
213
  ...(controller?.filters ?? []).map(fn => fn.bind(controller?.instance)),
216
214
  ...(endpoint.filters ?? []).map(fn => fn.bind(endpoint.instance)),
217
- ...(endpoint.parameters.filter(cfg => cfg.resolve).map(fn => fn.resolve!))
215
+ ...(endpoint.parameters.filter(config => config.resolve).map(fn => fn.resolve!))
218
216
  ]
219
217
  .map(fn => ({ filter: fn }));
220
218
 
@@ -231,8 +229,8 @@ export class EndpointUtil {
231
229
  /**
232
230
  * Get bound endpoints, honoring the conditional status
233
231
  */
234
- static async getBoundEndpoints(c: Class): Promise<EndpointConfig[]> {
235
- const config = ControllerRegistryIndex.getConfig(c);
232
+ static async getBoundEndpoints(cls: Class): Promise<EndpointConfig[]> {
233
+ const config = ControllerRegistryIndex.getConfig(cls);
236
234
 
237
235
  // Skip registering conditional controllers
238
236
  if (config.conditional && !await config.conditional()) {
@@ -241,21 +239,17 @@ export class EndpointUtil {
241
239
 
242
240
  config.instance = await DependencyRegistryIndex.getInstance(config.class);
243
241
 
244
- if (Runtime.dynamic) {
245
- config.instance = RetargettingProxy.unwrap(config.instance);
246
- }
247
-
248
242
  // Filter out conditional endpoints
249
243
  const endpoints = (await Promise.all(
250
- config.endpoints.map(ep => Promise.resolve(ep.conditional?.() ?? true).then(v => v ? ep : undefined))
251
- )).filter(x => !!x);
244
+ config.endpoints.map(endpoint => Promise.resolve(endpoint.conditional?.() ?? true).then(value => value ? endpoint : undefined))
245
+ )).filter(endpoint => !!endpoint);
252
246
 
253
247
  if (!endpoints.length) {
254
248
  return [];
255
249
  }
256
250
 
257
- for (const ep of endpoints) {
258
- ep.instance = config.instance;
251
+ for (const endpoint of endpoints) {
252
+ endpoint.instance = config.instance;
259
253
  }
260
254
 
261
255
  return endpoints;
@@ -266,12 +260,12 @@ export class EndpointUtil {
266
260
  */
267
261
  static orderEndpoints(endpoints: EndpointConfig[]): EndpointConfig[] {
268
262
  return endpoints
269
- .map(ep => {
270
- const parts = ep.path.replace(/^[/]|[/]$/g, '').split('/');
271
- return [ep, parts.map(x => /[*]/.test(x) ? 1 : /:/.test(x) ? 2 : 3)] as const;
263
+ .map(endpoint => {
264
+ const parts = endpoint.path.replace(/^[/]|[/]$/g, '').split('/');
265
+ return [endpoint, parts.map(part => /[*]/.test(part) ? 1 : /:/.test(part) ? 2 : 3)] as const;
272
266
  })
273
267
  .toSorted((a, b) => this.#compareEndpoints(a[1], b[1]) || a[0].path.localeCompare(b[0].path))
274
- .map(([ep,]) => ep);
268
+ .map(([endpoint,]) => endpoint);
275
269
  }
276
270
 
277
271
 
@@ -279,25 +273,25 @@ export class EndpointUtil {
279
273
  * Order interceptors
280
274
  */
281
275
  static orderInterceptors(instances: WebInterceptor[]): WebInterceptor[] {
282
- const cats = WEB_INTERCEPTOR_CATEGORIES.map(x => ({
283
- key: x,
284
- start: castTo<Class<WebInterceptor>>({ name: `${x}Start` }),
285
- end: castTo<Class<WebInterceptor>>({ name: `${x}End` }),
276
+ const categoryList = WEB_INTERCEPTOR_CATEGORIES.map(category => ({
277
+ key: category,
278
+ start: castTo<Class<WebInterceptor>>({ name: `${category}Start` }),
279
+ end: castTo<Class<WebInterceptor>>({ name: `${category}End` }),
286
280
  }));
287
281
 
288
- const categoryMapping = TypedObject.fromEntries(cats.map(x => [x.key, x]));
282
+ const categoryMapping = TypedObject.fromEntries(categoryList.map(category => [category.key, category]));
289
283
 
290
- const ordered = instances.map(x => {
291
- const group = categoryMapping[x.category];
292
- const after = [...x.dependsOn ?? [], group.start];
293
- const before = [...x.runsBefore ?? [], group.end];
294
- return ({ key: x.constructor, before, after, target: x, placeholder: false });
284
+ const ordered = instances.map(category => {
285
+ const group = categoryMapping[category.category];
286
+ const after = [...category.dependsOn ?? [], group.start];
287
+ const before = [...category.runsBefore ?? [], group.end];
288
+ return ({ key: category.constructor, before, after, target: category, placeholder: false });
295
289
  });
296
290
 
297
291
  // Add category sets into the ordering
298
292
  let i = 0;
299
- for (const cat of cats) {
300
- const prevEnd = cats[i - 1]?.end ? [cats[i - 1].end] : [];
293
+ for (const cat of categoryList) {
294
+ const prevEnd = categoryList[i - 1]?.end ? [categoryList[i - 1].end] : [];
301
295
  ordered.push(
302
296
  { key: cat.start, before: [cat.end], after: prevEnd, placeholder: true, target: undefined! },
303
297
  { key: cat.end, before: [], after: [cat.start], placeholder: true, target: undefined! }
@@ -306,7 +300,7 @@ export class EndpointUtil {
306
300
  }
307
301
 
308
302
  return WebCommonUtil.ordered(ordered)
309
- .filter(x => !x.placeholder) // Drop out the placeholders
310
- .map(x => x.target);
303
+ .filter(category => !category.placeholder) // Drop out the placeholders
304
+ .map(category => category.target);
311
305
  }
312
306
  }
@@ -19,8 +19,8 @@ export class WebHeaderUtil {
19
19
  * Parse cookie header
20
20
  */
21
21
  static parseCookieHeader(header: string): Cookie[] {
22
- const val = header.trim();
23
- return !val ? [] : val.split(SPLIT_SEMI).map(item => {
22
+ const text = header.trim();
23
+ return !text ? [] : text.split(SPLIT_SEMI).map(item => {
24
24
  const [name, value] = item.split(SPLIT_EQ);
25
25
  return { name, value };
26
26
  });
@@ -32,17 +32,17 @@ export class WebHeaderUtil {
32
32
  static parseSetCookieHeader(header: string): Cookie {
33
33
  const parts = header.split(SPLIT_SEMI);
34
34
  const [name, value] = parts[0].split(SPLIT_EQ);
35
- const c: Cookie = { name, value };
36
- for (const p of parts.slice(1)) {
37
- const [k, pv = ''] = p.toLowerCase().split(SPLIT_EQ);
38
- const v = pv.charCodeAt(0) === QUOTE ? pv.slice(1, -1) : pv;
39
- if (k === 'expires') {
40
- c[k] = new Date(v);
35
+ const result: Cookie = { name, value };
36
+ for (const part of parts.slice(1)) {
37
+ const [key, partValue = ''] = part.toLowerCase().split(SPLIT_EQ);
38
+ const cleanedValue = partValue.charCodeAt(0) === QUOTE ? partValue.slice(1, -1) : partValue;
39
+ if (key === 'expires') {
40
+ result[key] = new Date(cleanedValue);
41
41
  } else {
42
- c[castKey(k)] = castTo(v || true);
42
+ result[castKey(key)] = castTo(cleanedValue || true);
43
43
  }
44
44
  }
45
- return c;
45
+ return result;
46
46
  }
47
47
 
48
48
  /**
@@ -53,20 +53,20 @@ export class WebHeaderUtil {
53
53
  if (!input) {
54
54
  return { value: '', parameters: {} };
55
55
  }
56
- const [rv, ...parts] = input.split(SPLIT_SEMI);
56
+ const [rawValue, ...parts] = input.split(SPLIT_SEMI);
57
57
  const item: WebParsedHeader = { value: '', parameters: {} };
58
- const value = rv.charCodeAt(0) === QUOTE ? rv.slice(1, -1) : rv;
58
+ const value = rawValue.charCodeAt(0) === QUOTE ? rawValue.slice(1, -1) : rawValue;
59
59
  if (value.includes('=')) {
60
60
  parts.unshift(value);
61
61
  } else {
62
62
  item.value = value;
63
63
  }
64
64
  for (const part of parts) {
65
- const [k, pv = ''] = part.split(SPLIT_EQ);
66
- const v = (pv.charCodeAt(0) === QUOTE) ? pv.slice(1, -1) : pv;
67
- item.parameters[k] = v;
68
- if (k === 'q') {
69
- item.q = parseFloat(v);
65
+ const [key, partValue = ''] = part.split(SPLIT_EQ);
66
+ const cleanedValue = (partValue.charCodeAt(0) === QUOTE) ? partValue.slice(1, -1) : partValue;
67
+ item.parameters[key] = cleanedValue;
68
+ if (key === 'q') {
69
+ item.q = parseFloat(cleanedValue);
70
70
  }
71
71
  }
72
72
  return item;
@@ -76,24 +76,24 @@ export class WebHeaderUtil {
76
76
  * Parse full header
77
77
  */
78
78
  static parseHeader(input: string): WebParsedHeader[] {
79
- const v = input.trim();
79
+ const value = input.trim();
80
80
  if (!input) { return []; }
81
- return v.split(SPLIT_COMMA).map(x => this.parseHeaderSegment(x));
81
+ return value.split(SPLIT_COMMA).map(part => this.parseHeaderSegment(part));
82
82
  }
83
83
 
84
84
  /**
85
85
  * Build cookie suffix
86
86
  */
87
- static buildCookieSuffix(c: Cookie): string[] {
87
+ static buildCookieSuffix(cookie: Cookie): string[] {
88
88
  const parts = [];
89
- if (c.path) { parts.push(`path=${c.path}`); }
90
- if (c.expires) { parts.push(`expires=${c.expires.toUTCString()}`); }
91
- if (c.domain) { parts.push(`domain=${c.domain}`); }
92
- if (c.priority) { parts.push(`priority=${c.priority.toLowerCase()}`); }
93
- if (c.sameSite) { parts.push(`samesite=${c.sameSite.toLowerCase()}`); }
94
- if (c.secure) { parts.push('secure'); }
95
- if (c.httponly) { parts.push('httponly'); }
96
- if (c.partitioned) { parts.push('partitioned'); }
89
+ if (cookie.path) { parts.push(`path=${cookie.path}`); }
90
+ if (cookie.expires) { parts.push(`expires=${cookie.expires.toUTCString()}`); }
91
+ if (cookie.domain) { parts.push(`domain=${cookie.domain}`); }
92
+ if (cookie.priority) { parts.push(`priority=${cookie.priority.toLowerCase()}`); }
93
+ if (cookie.sameSite) { parts.push(`samesite=${cookie.sameSite.toLowerCase()}`); }
94
+ if (cookie.secure) { parts.push('secure'); }
95
+ if (cookie.httponly) { parts.push('httponly'); }
96
+ if (cookie.partitioned) { parts.push('partitioned'); }
97
97
  return parts;
98
98
  }
99
99
 
@@ -104,7 +104,10 @@ export class WebHeaderUtil {
104
104
  if (header === '*' || header === '*/*') {
105
105
  return values[0];
106
106
  }
107
- const sorted = this.parseHeader(header.toLowerCase()).filter(x => (x.q ?? 1) > 0).toSorted((a, b) => (b.q ?? 1) - (a.q ?? 1));
107
+ const sorted = this.parseHeader(header.toLowerCase())
108
+ .filter(item => (item.q ?? 1) > 0)
109
+ .toSorted((a, b) => (b.q ?? 1) - (a.q ?? 1));
110
+
108
111
  const set = new Set(values);
109
112
  for (const { value } of sorted) {
110
113
  const vk: K = castKey(value);
@@ -127,7 +130,7 @@ export class WebHeaderUtil {
127
130
  const { parameters } = this.parseHeaderSegment(headers.get('Range'));
128
131
  if ('bytes' in parameters) {
129
132
  const [start, end] = parameters.bytes.split('-')
130
- .map(x => x ? parseInt(x, 10) : undefined);
133
+ .map(value => value ? parseInt(value, 10) : undefined);
131
134
  if (start !== undefined) {
132
135
  return { start, end: end ?? (start + chunkSize) };
133
136
  }
@@ -137,20 +140,20 @@ export class WebHeaderUtil {
137
140
  /**
138
141
  * Check freshness of the response using request and response headers.
139
142
  */
140
- static isFresh(req: WebHeaders, res: WebHeaders): boolean {
141
- const cacheControl = req.get('Cache-Control');
143
+ static isFresh(request: WebHeaders, response: WebHeaders): boolean {
144
+ const cacheControl = request.get('Cache-Control');
142
145
  if (cacheControl?.includes('no-cache')) {
143
146
  return false;
144
147
  }
145
148
 
146
- const noneMatch = req.get('If-None-Match');
149
+ const noneMatch = request.get('If-None-Match');
147
150
  if (noneMatch) {
148
- const etag = res.get('ETag');
149
- const validTag = (v: string): boolean => v === etag || v === `W/${etag}` || `W/${v}` === etag;
151
+ const etag = response.get('ETag');
152
+ const validTag = (value: string): boolean => value === etag || value === `W/${etag}` || `W/${value}` === etag;
150
153
  return noneMatch === '*' || (!!etag && noneMatch.split(SPLIT_COMMA).some(validTag));
151
154
  } else {
152
- const modifiedSince = req.get('If-Modified-Since');
153
- const lastModified = res.get('Last-Modified');
155
+ const modifiedSince = request.get('If-Modified-Since');
156
+ const lastModified = response.get('Last-Modified');
154
157
  if (!modifiedSince || !lastModified) {
155
158
  return false;
156
159
  }
@@ -30,7 +30,7 @@ export class KeyGrip {
30
30
  .createHmac(this.#algorithm, key ?? this.#keys[0])
31
31
  .update(data)
32
32
  .digest(this.#encoding)
33
- .replace(/[/+=]/g, x => CHAR_MAPPING[castKey(x)]);
33
+ .replace(/[/+=]/g, ch => CHAR_MAPPING[castKey(ch)]);
34
34
  }
35
35
 
36
36
  verify(data: string, digest: string): boolean {
package/src/util/net.ts CHANGED
@@ -8,25 +8,25 @@ import { ExecUtil } from '@travetto/runtime';
8
8
  export class NetUtil {
9
9
 
10
10
  /** Is an error an address in use error */
11
- static isPortUsedError(err: unknown): err is Error & { port: number } {
12
- return !!err && err instanceof Error && err.message.includes('EADDRINUSE');
11
+ static isPortUsedError(error: unknown): error is Error & { port: number } {
12
+ return !!error && error instanceof Error && error.message.includes('EADDRINUSE');
13
13
  }
14
14
 
15
15
  /** Get the port process id */
16
16
  static async getPortProcessId(port: number): Promise<number | undefined> {
17
- const proc = spawn('lsof', ['-t', '-i', `tcp:${port}`]);
18
- const result = await ExecUtil.getResult(proc, { catch: true });
19
- const [pid] = result.stdout.trim().split(/\n/g);
20
- if (pid && +pid > 0) {
21
- return +pid;
17
+ const subProcess = spawn('lsof', ['-t', '-i', `tcp:${port}`]);
18
+ const result = await ExecUtil.getResult(subProcess, { catch: true });
19
+ const [processId] = result.stdout.trim().split(/\n/g);
20
+ if (processId && +processId > 0) {
21
+ return +processId;
22
22
  }
23
23
  }
24
24
 
25
25
  /** Free port if in use */
26
26
  static async freePort(port: number): Promise<void> {
27
- const pid = await this.getPortProcessId(port);
28
- if (pid) {
29
- process.kill(pid);
27
+ const processId = await this.getPortProcessId(port);
28
+ if (processId) {
29
+ process.kill(processId);
30
30
  }
31
31
  }
32
32
 
@@ -54,19 +54,19 @@ export class NetUtil {
54
54
  */
55
55
  static getLocalAddress(): string {
56
56
  const useIPv4 = !![...Object.values(os.networkInterfaces())]
57
- .find(interfaces => interfaces?.find(nic => nic.family === 'IPv4'));
57
+ .find(interfaces => interfaces?.find(item => item.family === 'IPv4'));
58
58
 
59
59
  return useIPv4 ? '0.0.0.0' : '::';
60
60
  }
61
61
 
62
62
  /**
63
63
  * Free a port if it is in use, typically used to resolve port conflicts.
64
- * @param err The error that may indicate a port conflict
64
+ * @param error The error that may indicate a port conflict
65
65
  * @returns Returns true if the port was freed, false if not handled
66
66
  */
67
- static async freePortOnConflict(err: unknown): Promise<boolean> {
68
- if (NetUtil.isPortUsedError(err) && typeof err.port === 'number') {
69
- await NetUtil.freePort(err.port);
67
+ static async freePortOnConflict(error: unknown): Promise<boolean> {
68
+ if (NetUtil.isPortUsedError(error) && typeof error.port === 'number') {
69
+ await NetUtil.freePort(error.port);
70
70
  return true;
71
71
  } else {
72
72
  return false;
@@ -1,7 +1,7 @@
1
1
  import { buffer } from 'node:stream/consumers';
2
2
  import { Readable } from 'node:stream';
3
3
 
4
- import { AppError, BinaryUtil, castTo } from '@travetto/runtime';
4
+ import { AppError, BinaryUtil, castTo, JSONUtil } from '@travetto/runtime';
5
5
  import { BindUtil } from '@travetto/schema';
6
6
 
7
7
  import { WebResponse } from '../../src/types/response.ts';
@@ -51,7 +51,7 @@ export class WebTestDispatchUtil {
51
51
 
52
52
  if (text) {
53
53
  switch (response.headers.get('Content-Type')) {
54
- case 'application/json': result = JSON.parse(castTo(text)); break;
54
+ case 'application/json': result = JSONUtil.parseSafe(castTo(text)); break;
55
55
  case 'text/plain': result = text; break;
56
56
  }
57
57
  }
@@ -2,7 +2,7 @@ import { Registry } from '@travetto/registry';
2
2
  import { castTo, Class } from '@travetto/runtime';
3
3
  import { AfterAll, BeforeAll } from '@travetto/test';
4
4
  import { DependencyRegistryIndex, Injectable } from '@travetto/di';
5
- import { ConfigSource, ConfigSpec } from '@travetto/config';
5
+ import { ConfigSource, ConfigPayload } from '@travetto/config';
6
6
  import { Schema } from '@travetto/schema';
7
7
 
8
8
  import { WebDispatcher } from '../../../src/types/dispatch.ts';
@@ -12,7 +12,7 @@ import { WebMessageInit } from '../../../src/types/message.ts';
12
12
 
13
13
  @Injectable()
14
14
  export class WebTestConfig implements ConfigSource {
15
- async get(): Promise<ConfigSpec> {
15
+ async get(): Promise<ConfigPayload> {
16
16
  return {
17
17
  data: {
18
18
  web: {
@@ -1,19 +1,13 @@
1
1
  import assert from 'node:assert';
2
2
 
3
- import { Test, Suite, BeforeAll } from '@travetto/test';
4
- import { Registry } from '@travetto/registry';
3
+ import { Test, Suite } from '@travetto/test';
5
4
 
6
5
  import { BaseWebSuite } from './base.ts';
7
- import { TestController } from './controller.ts';
6
+ import './controller.ts'; // Ensure imported
8
7
 
9
8
  @Suite()
10
9
  export abstract class StandardWebServerSuite extends BaseWebSuite {
11
10
 
12
- @BeforeAll()
13
- async init() {
14
- Registry.process([{ type: 'added', curr: TestController }]);
15
- }
16
-
17
11
  @Test()
18
12
  async getJSON() {
19
13
  const response = await this.request({ context: { httpMethod: 'GET', path: '/test/json' } });