@podium/client 5.1.10 → 5.2.0-next.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/CHANGELOG.md CHANGED
@@ -1,9 +1,23 @@
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.1](https://github.com/podium-lib/client/compare/v5.1.10-next.2...v5.2.0-next.1) (2024-09-10)
2
+
3
+
4
+ ### Features
5
+
6
+ * read assets from podlets using 103 early hints ([64e4b27](https://github.com/podium-lib/client/commit/64e4b27773b87c220bcab1028ecdda82be2aa9fc))
7
+
8
+ ## [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)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * 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))
14
+
15
+ ## [5.1.10-next.1](https://github.com/podium-lib/client/compare/v5.1.9...v5.1.10-next.1) (2024-08-22)
2
16
 
3
17
 
4
18
  ### Bug Fixes
5
19
 
6
- * **deps:** update dependency @podium/utils to v5.2.0 ([f7d4675](https://github.com/podium-lib/client/commit/f7d4675e6e2b9f2fba26cbd164d3e7a73b9c132e))
20
+ * use AbortSignal to ensure timeouts are respected ([08899d9](https://github.com/podium-lib/client/commit/08899d974246037cb1893a5b6d06bd6df58815e2))
7
21
 
8
22
  ## [5.1.9](https://github.com/podium-lib/client/compare/v5.1.8...v5.1.9) (2024-08-19)
9
23
 
@@ -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);
@@ -305,8 +328,8 @@ export default class PodletClientContentResolver {
305
328
  outgoing.emit(
306
329
  'beforeStream',
307
330
  new Response({
308
- js: utils.filterAssets('fallback', outgoing.manifest.js),
309
- css: utils.filterAssets('fallback', outgoing.manifest.css),
331
+ js: utils.filterAssets('fallback', outgoing.js),
332
+ css: utils.filterAssets('fallback', outgoing.css),
310
333
  }),
311
334
  );
312
335
 
@@ -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({
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.1",
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';