@podium/client 5.1.10 → 5.2.0-next.2

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/CHANGELOG.md CHANGED
@@ -1,9 +1,30 @@
1
- ## [5.1.10](https://github.com/podium-lib/client/compare/v5.1.9...v5.1.10) (2024-09-06)
1
+ # [5.2.0-next.2](https://github.com/podium-lib/client/compare/v5.2.0-next.1...v5.2.0-next.2) (2024-09-16)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * **deps:** update dependency @podium/utils to v5.2.0 ([f7d4675](https://github.com/podium-lib/client/commit/f7d4675e6e2b9f2fba26cbd164d3e7a73b9c132e))
6
+ * improve error messages when fetching from manifest, content and fallback routes when a timeout occurs ([e188bbc](https://github.com/podium-lib/client/commit/e188bbc40d30ed2e21947dd29de76bd218907428))
7
+
8
+ # [5.2.0-next.1](https://github.com/podium-lib/client/compare/v5.1.10-next.2...v5.2.0-next.1) (2024-09-10)
9
+
10
+
11
+ ### Features
12
+
13
+ * read assets from podlets using 103 early hints ([64e4b27](https://github.com/podium-lib/client/commit/64e4b27773b87c220bcab1028ecdda82be2aa9fc))
14
+
15
+ ## [5.1.10-next.2](https://github.com/podium-lib/client/compare/v5.1.10-next.1...v5.1.10-next.2) (2024-08-26)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * use AbortController instead of AbortSignal to avoid unhandled exception ([#412](https://github.com/podium-lib/client/issues/412)) ([87f5ffe](https://github.com/podium-lib/client/commit/87f5ffe553aa49189658a9be0e19d1323878a55a))
21
+
22
+ ## [5.1.10-next.1](https://github.com/podium-lib/client/compare/v5.1.9...v5.1.10-next.1) (2024-08-22)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * use AbortSignal to ensure timeouts are respected ([08899d9](https://github.com/podium-lib/client/commit/08899d974246037cb1893a5b6d06bd6df58815e2))
7
28
 
8
29
  ## [5.1.9](https://github.com/podium-lib/client/compare/v5.1.8...v5.1.9) (2024-08-19)
9
30
 
@@ -67,6 +67,8 @@ export default class PodletClientHttpOutgoing extends PassThrough {
67
67
  #status;
68
68
  #name;
69
69
  #uri;
70
+ #js;
71
+ #css;
70
72
 
71
73
  /**
72
74
  * @constructor
@@ -120,6 +122,8 @@ export default class PodletClientHttpOutgoing extends PassThrough {
120
122
  this.#manifest = {
121
123
  // @ts-expect-error Internal property
122
124
  _fallback: '',
125
+ _js: [],
126
+ _css: [],
123
127
  };
124
128
 
125
129
  // How long before a request should time out
@@ -146,6 +150,24 @@ export default class PodletClientHttpOutgoing extends PassThrough {
146
150
  this.#redirectable = redirectable;
147
151
  }
148
152
 
153
+ get js() {
154
+ // return the internal js value or, fallback to the manifest for backwards compatibility
155
+ return this.#js || this.#manifest.js;
156
+ }
157
+
158
+ set js(value) {
159
+ this.#js = value;
160
+ }
161
+
162
+ get css() {
163
+ // return the internal css value or, fallback to the manifest for backwards compatibility
164
+ return this.#css || this.#manifest.css;
165
+ }
166
+
167
+ set css(value) {
168
+ this.#css = value;
169
+ }
170
+
149
171
  get rejectUnauthorized() {
150
172
  return this.#rejectUnauthorized;
151
173
  }
@@ -308,6 +330,10 @@ export default class PodletClientHttpOutgoing extends PassThrough {
308
330
  pushFallback() {
309
331
  // @ts-expect-error Internal property
310
332
  this.push(this.#manifest._fallback);
333
+ // @ts-expect-error Internal property
334
+ this.js = this.#manifest._js;
335
+ // @ts-expect-error Internal property
336
+ this.css = this.#manifest._css;
311
337
  this.push(null);
312
338
  this.#isFallback = true;
313
339
  }
package/lib/http.js CHANGED
@@ -1,4 +1,4 @@
1
- import { request } from 'undici';
1
+ import { request as undiciRequest } from 'undici';
2
2
 
3
3
  /**
4
4
  * @typedef {object} PodiumHttpClientRequestOptions
@@ -7,22 +7,39 @@ import { request } from 'undici';
7
7
  * @property {boolean} [rejectUnauthorized]
8
8
  * @property {boolean} [follow]
9
9
  * @property {number} [timeout]
10
- * @property {number} [bodyTimeout]
11
10
  * @property {object} [query]
12
11
  * @property {import('http').IncomingHttpHeaders} [headers]
12
+ * @property {(info: { statusCode: number; headers: Record<string, string | string[]>; }) => void} [onInfo]
13
13
  */
14
14
 
15
15
  export default class HTTP {
16
+ constructor(requestFn = undiciRequest) {
17
+ this.requestFn = requestFn;
18
+ }
19
+
16
20
  /**
17
21
  * @param {string} url
18
22
  * @param {PodiumHttpClientRequestOptions} options
19
23
  * @returns {Promise<Pick<import('undici').Dispatcher.ResponseData, 'statusCode' | 'headers' | 'body'>>}
20
24
  */
21
25
  async request(url, options) {
22
- const { statusCode, headers, body } = await request(
23
- new URL(url),
24
- options,
25
- );
26
- return { statusCode, headers, body };
26
+ const abortController = new AbortController();
27
+
28
+ const timeoutId = setTimeout(() => {
29
+ abortController.abort();
30
+ }, options.timeout || 1000);
31
+
32
+ try {
33
+ const { statusCode, headers, body } = await this.requestFn(
34
+ new URL(url),
35
+ {
36
+ ...options,
37
+ signal: abortController.signal,
38
+ },
39
+ );
40
+ return { statusCode, headers, body };
41
+ } finally {
42
+ clearTimeout(timeoutId);
43
+ }
27
44
  }
28
45
  }
@@ -9,6 +9,8 @@ import fs from 'fs';
9
9
  import * as utils from './utils.js';
10
10
  import Response from './response.js';
11
11
  import HTTP from './http.js';
12
+ import { parseLinkHeaders, filterAssets } from './utils.js';
13
+ import { AssetJs, AssetCss } from '@podium/utils';
12
14
 
13
15
  const currentDirectory = dirname(fileURLToPath(import.meta.url));
14
16
 
@@ -133,13 +135,33 @@ export default class PodletClientContentResolver {
133
135
  outgoing.contentUri,
134
136
  );
135
137
 
138
+ let hintsReceived = false;
139
+
136
140
  /** @type {import('./http.js').PodiumHttpClientRequestOptions} */
137
141
  const reqOptions = {
138
142
  rejectUnauthorized: outgoing.rejectUnauthorized,
139
- bodyTimeout: outgoing.timeout,
143
+ timeout: outgoing.timeout,
140
144
  method: 'GET',
141
145
  query: outgoing.reqOptions.query,
142
146
  headers,
147
+ onInfo({ statusCode, headers }) {
148
+ if (statusCode === 103 && !hintsReceived) {
149
+ const parsedAssetObjects = parseLinkHeaders(headers.link);
150
+
151
+ const scriptObjects = parsedAssetObjects.filter(
152
+ (asset) => asset instanceof AssetJs,
153
+ );
154
+ const styleObjects = parsedAssetObjects.filter(
155
+ (asset) => asset instanceof AssetCss,
156
+ );
157
+ // set the content js asset objects
158
+ outgoing.js = filterAssets('content', scriptObjects);
159
+ // set the content css asset objects
160
+ outgoing.css = filterAssets('content', styleObjects);
161
+
162
+ hintsReceived = true;
163
+ }
164
+ },
143
165
  };
144
166
 
145
167
  if (outgoing.redirectable) {
@@ -267,6 +289,7 @@ export default class PodletClientContentResolver {
267
289
  }),
268
290
  );
269
291
 
292
+ // @ts-ignore
270
293
  pipeline([body, outgoing], (err) => {
271
294
  if (err) {
272
295
  this.#log.warn('error while piping content stream', err);
@@ -275,18 +298,14 @@ export default class PodletClientContentResolver {
275
298
  } catch (error) {
276
299
  if (error.isBoom) throw error;
277
300
 
278
- // Network error
279
- if (outgoing.throwable) {
280
- timer({
281
- labels: {
282
- status: 'failure',
283
- },
284
- });
285
-
301
+ if (error.name === 'AbortError') {
302
+ this.#log.warn(
303
+ `request to remote resource for content was aborted due to the resource failing to respond before the configured timeout (${outgoing.timeout}ms) - resource: ${outgoing.name} - url: ${uri}`,
304
+ );
305
+ } else {
286
306
  this.#log.warn(
287
307
  `could not create network connection to remote resource when trying to request content - resource: ${outgoing.name} - url: ${uri}`,
288
308
  );
289
- throw badGateway(`Error reading content at ${uri}`, error);
290
309
  }
291
310
 
292
311
  timer({
@@ -295,9 +314,10 @@ export default class PodletClientContentResolver {
295
314
  },
296
315
  });
297
316
 
298
- this.#log.warn(
299
- `could not create network connection to remote resource when trying to request content - resource: ${outgoing.name} - url: ${uri}`,
300
- );
317
+ // Network error
318
+ if (outgoing.throwable) {
319
+ throw badGateway(`Error reading content at ${uri}`, error);
320
+ }
301
321
 
302
322
  outgoing.success = true;
303
323
 
@@ -305,8 +325,8 @@ export default class PodletClientContentResolver {
305
325
  outgoing.emit(
306
326
  'beforeStream',
307
327
  new Response({
308
- js: utils.filterAssets('fallback', outgoing.manifest.js),
309
- css: utils.filterAssets('fallback', outgoing.manifest.css),
328
+ js: utils.filterAssets('fallback', outgoing.js),
329
+ css: utils.filterAssets('fallback', outgoing.css),
310
330
  }),
311
331
  );
312
332
 
@@ -4,6 +4,8 @@ import { join, dirname } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import fs from 'fs';
6
6
  import HTTP from './http.js';
7
+ import { parseLinkHeaders, filterAssets } from './utils.js';
8
+ import { AssetJs, AssetCss } from '@podium/utils';
7
9
 
8
10
  const currentDirectory = dirname(fileURLToPath(import.meta.url));
9
11
 
@@ -94,12 +96,40 @@ export default class PodletClientFallbackResolver {
94
96
  'User-Agent': UA_STRING,
95
97
  };
96
98
 
99
+ let hintsReceived = false;
100
+
97
101
  /** @type {import('./http.js').PodiumHttpClientRequestOptions} */
98
102
  const reqOptions = {
99
103
  rejectUnauthorized: outgoing.rejectUnauthorized,
100
104
  timeout: outgoing.timeout,
101
105
  method: 'GET',
102
106
  headers,
107
+ onInfo({ statusCode, headers }) {
108
+ if (statusCode === 103 && !hintsReceived) {
109
+ const parsedAssetObjects = parseLinkHeaders(headers.link);
110
+
111
+ const scriptObjects = parsedAssetObjects.filter(
112
+ (asset) => asset instanceof AssetJs,
113
+ );
114
+ const styleObjects = parsedAssetObjects.filter(
115
+ (asset) => asset instanceof AssetCss,
116
+ );
117
+ // set the content js asset fallback objects
118
+ // @ts-expect-error internal property
119
+ outgoing.manifest._js = filterAssets(
120
+ 'fallback',
121
+ scriptObjects,
122
+ );
123
+ // set the fallback css asset fallback objects
124
+ // @ts-expect-error internal property
125
+ outgoing.manifest._css = filterAssets(
126
+ 'fallback',
127
+ styleObjects,
128
+ );
129
+
130
+ hintsReceived = true;
131
+ }
132
+ },
103
133
  };
104
134
 
105
135
  const timer = this.#histogram.timer({
@@ -151,7 +181,6 @@ export default class PodletClientFallbackResolver {
151
181
  `successfully read fallback from remote resource - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
152
182
  );
153
183
  return outgoing;
154
- // eslint-disable-next-line no-unused-vars
155
184
  } catch (error) {
156
185
  timer({
157
186
  labels: {
@@ -159,9 +188,15 @@ export default class PodletClientFallbackResolver {
159
188
  },
160
189
  });
161
190
 
162
- this.#log.warn(
163
- `could not create network connection to remote resource for fallback content - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
164
- );
191
+ if (error.name === 'AbortError') {
192
+ this.#log.warn(
193
+ `request to read fallback was aborted due to the resource failing to respond before the configured timeout (${outgoing.timeout}ms) - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
194
+ );
195
+ } else {
196
+ this.#log.warn(
197
+ `could not create network connection to remote resource for fallback - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
198
+ );
199
+ }
165
200
 
166
201
  outgoing.fallback = '';
167
202
  return outgoing;
@@ -246,7 +246,6 @@ export default class PodletClientManifestResolver {
246
246
  `successfully read manifest from remote resource - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
247
247
  );
248
248
  return outgoing;
249
- // eslint-disable-next-line no-unused-vars
250
249
  } catch (error) {
251
250
  timer({
252
251
  labels: {
@@ -254,9 +253,15 @@ export default class PodletClientManifestResolver {
254
253
  },
255
254
  });
256
255
 
257
- this.#log.warn(
258
- `could not create network connection to remote manifest - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
259
- );
256
+ if (error.name === 'AbortError') {
257
+ this.#log.warn(
258
+ `request to remote manifest was aborted due to the resource failing to respond before the configured timeout (${outgoing.timeout}ms) - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
259
+ );
260
+ } else {
261
+ this.#log.warn(
262
+ `could not create network connection to remote manifest - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
263
+ );
264
+ }
260
265
  return outgoing;
261
266
  }
262
267
  }
package/lib/resource.js CHANGED
@@ -159,7 +159,7 @@ export default class PodiumClientResource {
159
159
 
160
160
  this.#state.setInitializingState();
161
161
 
162
- const { manifest, headers, redirect, isFallback } =
162
+ const { headers, redirect, isFallback } =
163
163
  await this.#resolver.resolve(outgoing);
164
164
 
165
165
  const chunks = [];
@@ -177,11 +177,11 @@ export default class PodiumClientResource {
177
177
  content,
178
178
  css: utils.filterAssets(
179
179
  isFallback ? 'fallback' : 'content',
180
- manifest.css,
180
+ outgoing.css,
181
181
  ),
182
182
  js: utils.filterAssets(
183
183
  isFallback ? 'fallback' : 'content',
184
- manifest.js,
184
+ outgoing.js,
185
185
  ),
186
186
  redirect,
187
187
  });
package/lib/utils.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { AssetJs, AssetCss } from '@podium/utils';
2
+
1
3
  /**
2
4
  * Checks if a header object has a header.
3
5
  * Will return true if the header exist and is not an empty
@@ -71,3 +73,45 @@ export const filterAssets = (scope, assets) => {
71
73
  !asset.scope || asset.scope === scope || asset.scope === 'all',
72
74
  );
73
75
  };
76
+
77
+ // parse link headers in AssetCss and AssetJs objects
78
+ export const parseLinkHeaders = (headers) => {
79
+ const links = [];
80
+
81
+ if (!headers) return links;
82
+ headers.split(',').forEach((link) => {
83
+ const parts = link.split(';');
84
+ const [href, ...rest] = parts;
85
+ const value = href.replace(/<|>|"/g, '').trim();
86
+
87
+ const asset = { value };
88
+ for (const key of rest) {
89
+ const [keyName, keyValue] = key.split('=');
90
+ asset[keyName.trim()] = keyValue.trim();
91
+ }
92
+
93
+ if (asset['asset-type'] === 'script') {
94
+ links.push(new AssetJs(asset));
95
+ }
96
+ if (asset['asset-type'] === 'style') {
97
+ links.push(new AssetCss(asset));
98
+ }
99
+ });
100
+ return links;
101
+ };
102
+
103
+ export const toPreloadAssetObjects = (assetObjects) => {
104
+ return assetObjects.map((assetObject) => {
105
+ return new AssetCss({
106
+ type:
107
+ assetObject instanceof AssetJs
108
+ ? 'application/javascript'
109
+ : 'text/css',
110
+ crossorigin: !!assetObject.crossorigin,
111
+ rel: 'preload',
112
+ as: assetObject instanceof AssetJs ? 'script' : 'style',
113
+ media: assetObject.media || '',
114
+ value: assetObject.value.trim(),
115
+ });
116
+ });
117
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podium/client",
3
- "version": "5.1.10",
3
+ "version": "5.2.0-next.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -48,13 +48,13 @@
48
48
  "undici": "6.19.8"
49
49
  },
50
50
  "devDependencies": {
51
- "@podium/test-utils": "2.5.2",
51
+ "@podium/test-utils": "3.1.0-next.3",
52
52
  "@semantic-release/changelog": "6.0.3",
53
53
  "@semantic-release/git": "10.0.1",
54
- "@semantic-release/github": "10.1.7",
54
+ "@semantic-release/github": "10.0.6",
55
55
  "@semantic-release/npm": "12.0.1",
56
56
  "@semantic-release/release-notes-generator": "13.0.0",
57
- "@sinonjs/fake-timers": "11.3.1",
57
+ "@sinonjs/fake-timers": "11.2.2",
58
58
  "@types/readable-stream": "4.0.15",
59
59
  "benchmark": "2.1.4",
60
60
  "eslint": "9.6.0",
@@ -62,10 +62,10 @@
62
62
  "eslint-plugin-prettier": "5.1.3",
63
63
  "express": "4.19.2",
64
64
  "get-stream": "9.0.1",
65
- "globals": "15.9.0",
65
+ "globals": "15.8.0",
66
66
  "http-proxy": "1.18.1",
67
67
  "is-stream": "4.0.1",
68
- "npm-run-all2": "5.0.2",
68
+ "npm-run-all2": "5.0.0",
69
69
  "prettier": "3.3.2",
70
70
  "semantic-release": "23.1.1",
71
71
  "tap": "18.7.2",
@@ -46,6 +46,10 @@ export default class PodletClientHttpOutgoing extends PassThrough {
46
46
  * @param {import('@podium/utils').HttpIncoming} [incoming]
47
47
  */
48
48
  constructor(options?: PodiumClientHttpOutgoingOptions, reqOptions?: PodiumClientResourceOptions, incoming?: import('@podium/utils').HttpIncoming);
49
+ set js(value: any);
50
+ get js(): any;
51
+ set css(value: any);
52
+ get css(): any;
49
53
  get rejectUnauthorized(): boolean;
50
54
  get reqOptions(): {
51
55
  pathname: string;
package/types/http.d.ts CHANGED
@@ -5,11 +5,13 @@
5
5
  * @property {boolean} [rejectUnauthorized]
6
6
  * @property {boolean} [follow]
7
7
  * @property {number} [timeout]
8
- * @property {number} [bodyTimeout]
9
8
  * @property {object} [query]
10
9
  * @property {import('http').IncomingHttpHeaders} [headers]
10
+ * @property {(info: { statusCode: number; headers: Record<string, string | string[]>; }) => void} [onInfo]
11
11
  */
12
12
  export default class HTTP {
13
+ constructor(requestFn?: typeof undiciRequest);
14
+ requestFn: typeof undiciRequest;
13
15
  /**
14
16
  * @param {string} url
15
17
  * @param {PodiumHttpClientRequestOptions} options
@@ -23,7 +25,11 @@ export type PodiumHttpClientRequestOptions = {
23
25
  rejectUnauthorized?: boolean;
24
26
  follow?: boolean;
25
27
  timeout?: number;
26
- bodyTimeout?: number;
27
28
  query?: object;
28
29
  headers?: import('http').IncomingHttpHeaders;
30
+ onInfo?: (info: {
31
+ statusCode: number;
32
+ headers: Record<string, string | string[]>;
33
+ }) => void;
29
34
  };
35
+ import { request as undiciRequest } from 'undici';
package/types/utils.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  export function isHeaderDefined(headers: object, header: string): boolean;
2
2
  export function hasManifestChange(item: object): boolean;
3
3
  export function validateIncoming(incoming?: object): boolean;
4
- export function filterAssets<T extends import("@podium/utils").AssetJs | import("@podium/utils").AssetCss>(scope: "content" | "fallback" | "all", assets: T[]): T[];
4
+ export function filterAssets<T extends AssetJs | AssetCss>(scope: "content" | "fallback" | "all", assets: T[]): T[];
5
+ export function parseLinkHeaders(headers: any): any[];
6
+ export function toPreloadAssetObjects(assetObjects: any): any;
7
+ import { AssetJs } from '@podium/utils';
8
+ import { AssetCss } from '@podium/utils';