@podium/client 5.2.0-next.2 → 5.2.0-next.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [5.2.0-next.4](https://github.com/podium-lib/client/compare/v5.2.0-next.3...v5.2.0-next.4) (2024-09-24)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * update @podium/utils to support hints asset collection ([fe97c44](https://github.com/podium-lib/client/commit/fe97c44bc6d29d1e45d335235cd3b1ef6eeafeaa))
7
+
8
+
9
+ ### Features
10
+
11
+ * keep track of which resources have emitted early hints and emit complete event once all resources have emitted ([7cf916a](https://github.com/podium-lib/client/commit/7cf916ab286a3c6cb8fbfdae46634b58f2be256f))
12
+
13
+ # [5.2.0-next.3](https://github.com/podium-lib/client/compare/v5.2.0-next.2...v5.2.0-next.3) (2024-09-20)
14
+
15
+
16
+ ### Features
17
+
18
+ * write early hints to browser ([42513a3](https://github.com/podium-lib/client/commit/42513a38f5304648f7b2fb915995c66dc1dd3594))
19
+
1
20
  # [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
21
 
3
22
 
package/README.md CHANGED
@@ -152,6 +152,7 @@ The following values can be provided:
152
152
  - `throwable` - {Boolean} - Defines whether an error should be thrown if a failure occurs during the process of fetching a podium component. Defaults to `false` - Optional.
153
153
  - `excludeBy` - {Object} - Lets you define a set of rules where a `fetch` call will not be resolved if it matches. - Optional.
154
154
  - `includeBy` - {Object} - Inverse of `excludeBy`. Setting both at the same time will throw. - Optional.
155
+ - `earlyHints` - {boolean} - Can be used to disable early hints from being sent to the browser for this resource, see [HTTP Status 103 Early Hints](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103).
155
156
 
156
157
  ##### `excludeBy` and `includeBy`
157
158
 
package/lib/client.js CHANGED
@@ -62,6 +62,7 @@ const MAX_AGE = Infinity;
62
62
  * @property {boolean} [redirectable=false] Set to `true` to allow podlet to respond with a redirect. You need to look for the redirect response from the podlet and return a redirect response to the browser yourself.
63
63
  * @property {import('./resource.js').RequestFilterOptions} [excludeBy] Used by `fetch` to conditionally skip fetching the podlet content based on values on the request.
64
64
  * @property {import('./resource.js').RequestFilterOptions} [includeBy] Used by `fetch` to conditionally skip fetching the podlet content based on values on the request.
65
+ * @property {boolean} [earlyHints=true]
65
66
  */
66
67
 
67
68
  export default class PodiumClient extends EventEmitter {
@@ -212,6 +213,7 @@ export default class PodiumClient extends EventEmitter {
212
213
  httpAgent: this.#options.httpAgent,
213
214
  includeBy: this.#options.includeBy,
214
215
  excludeBy: this.#options.excludeBy,
216
+ earlyHints: true,
215
217
  ...options,
216
218
  };
217
219
 
@@ -1,5 +1,6 @@
1
1
  import { PassThrough } from 'stream';
2
2
  import assert from 'assert';
3
+ import { toPreloadAssetObjects } from './utils.js';
3
4
 
4
5
  /**
5
6
  * @typedef {object} PodiumClientHttpOutgoingOptions
@@ -69,6 +70,7 @@ export default class PodletClientHttpOutgoing extends PassThrough {
69
70
  #uri;
70
71
  #js;
71
72
  #css;
73
+ #hintsReceived = false;
72
74
 
73
75
  /**
74
76
  * @constructor
@@ -299,6 +301,20 @@ export default class PodletClientHttpOutgoing extends PassThrough {
299
301
  this.#redirect = value;
300
302
  }
301
303
 
304
+ get hintsReceived() {
305
+ return this.#hintsReceived;
306
+ }
307
+
308
+ set hintsReceived(value) {
309
+ this.#hintsReceived = value;
310
+ if (this.#hintsReceived) {
311
+ this.#incoming?.hints?.addReceivedHint(this.#name, {
312
+ js: this.js,
313
+ css: this.css,
314
+ });
315
+ }
316
+ }
317
+
302
318
  /**
303
319
  * Whether the podlet can signal redirects to the layout.
304
320
  *
@@ -336,6 +352,21 @@ export default class PodletClientHttpOutgoing extends PassThrough {
336
352
  this.css = this.#manifest._css;
337
353
  this.push(null);
338
354
  this.#isFallback = true;
355
+ // assume the hints from the podlet have failed and fallback assets will be used
356
+ this.hintsReceived = true;
357
+ }
358
+
359
+ writeEarlyHints(cb = () => {}) {
360
+ if (this.#incoming.response.writeEarlyHints) {
361
+ const preloads = toPreloadAssetObjects([
362
+ ...(this.js || []),
363
+ ...(this.css || []),
364
+ ]);
365
+ const link = preloads.map((preload) => preload.toHeader());
366
+ if (link.length) {
367
+ this.#incoming.response.writeEarlyHints({ link }, cb);
368
+ }
369
+ }
339
370
  }
340
371
 
341
372
  get [Symbol.toStringTag]() {
@@ -27,6 +27,7 @@ const UA_STRING = `${pkg.name} ${pkg.version}`;
27
27
  * @property {string} clientName
28
28
  * @property {import('./http.js').default} [http]
29
29
  * @property {import('abslog').AbstractLoggerOptions} [logger]
30
+ * @property {boolean} [earlyHints]
30
31
  */
31
32
 
32
33
  export default class PodletClientContentResolver {
@@ -34,6 +35,7 @@ export default class PodletClientContentResolver {
34
35
  #metrics;
35
36
  #histogram;
36
37
  #http;
38
+ #earlyHints;
37
39
 
38
40
  /**
39
41
  * @constructor
@@ -44,6 +46,8 @@ export default class PodletClientContentResolver {
44
46
  this.#http = options.http || new HTTP();
45
47
  const name = options.clientName;
46
48
  this.#log = abslog(options.logger);
49
+ this.#earlyHints =
50
+ typeof options.earlyHints === 'boolean' ? options.earlyHints : true;
47
51
  this.#metrics = new Metrics();
48
52
  this.#histogram = this.#metrics.histogram({
49
53
  name: 'podium_client_resolver_content_resolve',
@@ -135,8 +139,6 @@ export default class PodletClientContentResolver {
135
139
  outgoing.contentUri,
136
140
  );
137
141
 
138
- let hintsReceived = false;
139
-
140
142
  /** @type {import('./http.js').PodiumHttpClientRequestOptions} */
141
143
  const reqOptions = {
142
144
  rejectUnauthorized: outgoing.rejectUnauthorized,
@@ -144,8 +146,8 @@ export default class PodletClientContentResolver {
144
146
  method: 'GET',
145
147
  query: outgoing.reqOptions.query,
146
148
  headers,
147
- onInfo({ statusCode, headers }) {
148
- if (statusCode === 103 && !hintsReceived) {
149
+ onInfo: ({ statusCode, headers }) => {
150
+ if (statusCode === 103 && !outgoing.hintsReceived) {
149
151
  const parsedAssetObjects = parseLinkHeaders(headers.link);
150
152
 
151
153
  const scriptObjects = parsedAssetObjects.filter(
@@ -158,8 +160,10 @@ export default class PodletClientContentResolver {
158
160
  outgoing.js = filterAssets('content', scriptObjects);
159
161
  // set the content css asset objects
160
162
  outgoing.css = filterAssets('content', styleObjects);
163
+ // write the early hints to the browser
164
+ if (this.#earlyHints) outgoing.writeEarlyHints();
161
165
 
162
- hintsReceived = true;
166
+ outgoing.hintsReceived = true;
163
167
  }
164
168
  },
165
169
  };
package/lib/resolver.js CHANGED
@@ -11,6 +11,7 @@ import Cache from './resolver.cache.js';
11
11
  * @typedef {object} PodletClientResolverOptions
12
12
  * @property {string} clientName
13
13
  * @property {import('abslog').AbstractLoggerOptions} [logger]
14
+ * @property {boolean} [earlyHints]
14
15
  */
15
16
 
16
17
  export default class PodletClientResolver {
@@ -41,7 +42,10 @@ export default class PodletClientResolver {
41
42
  http,
42
43
  });
43
44
  this.#fallback = new Fallback({ ...options, http });
44
- this.#content = new Content({ ...options, http });
45
+ this.#content = new Content({
46
+ ...options,
47
+ http,
48
+ });
45
49
  this.#metrics = new Metrics();
46
50
 
47
51
  this.#metrics.on('error', (error) => {
package/lib/resource.js CHANGED
@@ -105,6 +105,10 @@ export default class PodiumClientResource {
105
105
  throw new TypeError(
106
106
  'you must pass an instance of "HttpIncoming" as the first argument to the .fetch() method',
107
107
  );
108
+ // add the name of this resource as expecting a hint to be received
109
+ // we use this to track across resources and emit a hint completion event once
110
+ // all hints from all resources have been received.
111
+ incoming.hints.addExpectedHint(this.#options.name);
108
112
  const outgoing = new HttpOutgoing(this.#options, reqOptions, incoming);
109
113
 
110
114
  if (this.#options.excludeBy) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podium/client",
3
- "version": "5.2.0-next.2",
3
+ "version": "5.2.0-next.4",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -40,7 +40,7 @@
40
40
  "@hapi/boom": "10.0.1",
41
41
  "@metrics/client": "2.5.3",
42
42
  "@podium/schemas": "5.0.6",
43
- "@podium/utils": "5.2.0",
43
+ "@podium/utils": "5.3.1",
44
44
  "abslog": "2.4.4",
45
45
  "http-cache-semantics": "^4.0.3",
46
46
  "lodash.clonedeep": "^4.5.0",
package/types/client.d.ts CHANGED
@@ -29,6 +29,7 @@
29
29
  * @property {boolean} [redirectable=false] Set to `true` to allow podlet to respond with a redirect. You need to look for the redirect response from the podlet and return a redirect response to the browser yourself.
30
30
  * @property {import('./resource.js').RequestFilterOptions} [excludeBy] Used by `fetch` to conditionally skip fetching the podlet content based on values on the request.
31
31
  * @property {import('./resource.js').RequestFilterOptions} [includeBy] Used by `fetch` to conditionally skip fetching the podlet content based on values on the request.
32
+ * @property {boolean} [earlyHints=true]
32
33
  */
33
34
  export default class PodiumClient extends EventEmitter<[never]> {
34
35
  /**
@@ -115,6 +116,7 @@ export type RegisterOptions = {
115
116
  * Used by `fetch` to conditionally skip fetching the podlet content based on values on the request.
116
117
  */
117
118
  includeBy?: import('./resource.js').RequestFilterOptions;
119
+ earlyHints?: boolean;
118
120
  };
119
121
  import EventEmitter from 'events';
120
122
  import Metrics from '@metrics/client';
@@ -105,6 +105,8 @@ export default class PodletClientHttpOutgoing extends PassThrough {
105
105
  * @see https://podium-lib.io/docs/layout/handling_redirects
106
106
  */
107
107
  get redirect(): PodiumRedirect;
108
+ set hintsReceived(value: boolean);
109
+ get hintsReceived(): boolean;
108
110
  set redirectable(value: boolean);
109
111
  /**
110
112
  * Whether the podlet can signal redirects to the layout.
@@ -125,6 +127,7 @@ export default class PodletClientHttpOutgoing extends PassThrough {
125
127
  */
126
128
  get isFallback(): boolean;
127
129
  pushFallback(): void;
130
+ writeEarlyHints(cb?: () => void): void;
128
131
  get [Symbol.toStringTag](): string;
129
132
  #private;
130
133
  }
@@ -3,6 +3,7 @@
3
3
  * @property {string} clientName
4
4
  * @property {import('./http.js').default} [http]
5
5
  * @property {import('abslog').AbstractLoggerOptions} [logger]
6
+ * @property {boolean} [earlyHints]
6
7
  */
7
8
  export default class PodletClientContentResolver {
8
9
  /**
@@ -25,5 +26,6 @@ export type PodletClientContentResolverOptions = {
25
26
  clientName: string;
26
27
  http?: import('./http.js').default;
27
28
  logger?: import('abslog').AbstractLoggerOptions;
29
+ earlyHints?: boolean;
28
30
  };
29
31
  import Metrics from '@metrics/client';
@@ -2,6 +2,7 @@
2
2
  * @typedef {object} PodletClientResolverOptions
3
3
  * @property {string} clientName
4
4
  * @property {import('abslog').AbstractLoggerOptions} [logger]
5
+ * @property {boolean} [earlyHints]
5
6
  */
6
7
  export default class PodletClientResolver {
7
8
  /**
@@ -31,5 +32,6 @@ export default class PodletClientResolver {
31
32
  export type PodletClientResolverOptions = {
32
33
  clientName: string;
33
34
  logger?: import('abslog').AbstractLoggerOptions;
35
+ earlyHints?: boolean;
34
36
  };
35
37
  import Metrics from '@metrics/client';
package/types/utils.d.ts CHANGED
@@ -1,8 +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 AssetJs | AssetCss>(scope: "content" | "fallback" | "all", assets: T[]): T[];
4
+ export function filterAssets<T extends AssetCss | AssetJs>(scope: "content" | "fallback" | "all", assets: T[]): T[];
5
5
  export function parseLinkHeaders(headers: any): any[];
6
6
  export function toPreloadAssetObjects(assetObjects: any): any;
7
- import { AssetJs } from '@podium/utils';
8
7
  import { AssetCss } from '@podium/utils';
8
+ import { AssetJs } from '@podium/utils';