Skip to content

Commit 51c0251

Browse files
authored
Merge pull request #421 from podium-lib/track_early_hints_across_resources
feat: keep track of which resources have emitted early hints and emit complete event once all resources have emitted
2 parents 97ca27a + da5eb0b commit 51c0251

6 files changed

+206
-5
lines changed

lib/http-outgoing.js

+17
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export default class PodletClientHttpOutgoing extends PassThrough {
7070
#uri;
7171
#js;
7272
#css;
73+
#hintsReceived = false;
7374

7475
/**
7576
* @constructor
@@ -300,6 +301,20 @@ export default class PodletClientHttpOutgoing extends PassThrough {
300301
this.#redirect = value;
301302
}
302303

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+
303318
/**
304319
* Whether the podlet can signal redirects to the layout.
305320
*
@@ -337,6 +352,8 @@ export default class PodletClientHttpOutgoing extends PassThrough {
337352
this.css = this.#manifest._css;
338353
this.push(null);
339354
this.#isFallback = true;
355+
// assume the hints from the podlet have failed and fallback assets will be used
356+
this.hintsReceived = true;
340357
}
341358

342359
writeEarlyHints(cb = () => {}) {

lib/resolver.content.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,6 @@ export default class PodletClientContentResolver {
139139
outgoing.contentUri,
140140
);
141141

142-
let hintsReceived = false;
143-
144142
/** @type {import('./http.js').PodiumHttpClientRequestOptions} */
145143
const reqOptions = {
146144
rejectUnauthorized: outgoing.rejectUnauthorized,
@@ -149,7 +147,7 @@ export default class PodletClientContentResolver {
149147
query: outgoing.reqOptions.query,
150148
headers,
151149
onInfo: ({ statusCode, headers }) => {
152-
if (statusCode === 103 && !hintsReceived) {
150+
if (statusCode === 103 && !outgoing.hintsReceived) {
153151
const parsedAssetObjects = parseLinkHeaders(headers.link);
154152

155153
const scriptObjects = parsedAssetObjects.filter(
@@ -165,7 +163,7 @@ export default class PodletClientContentResolver {
165163
// write the early hints to the browser
166164
if (this.#earlyHints) outgoing.writeEarlyHints();
167165

168-
hintsReceived = true;
166+
outgoing.hintsReceived = true;
169167
}
170168
},
171169
};

lib/resource.js

+4
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ export default class PodiumClientResource {
105105
throw new TypeError(
106106
'you must pass an instance of "HttpIncoming" as the first argument to the .fetch() method',
107107
);
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);
108112
const outgoing = new HttpOutgoing(this.#options, reqOptions, incoming);
109113

110114
if (this.#options.excludeBy) {

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"@hapi/boom": "10.0.1",
4141
"@metrics/client": "2.5.3",
4242
"@podium/schemas": "5.0.6",
43-
"@podium/utils": "5.2.1",
43+
"@podium/utils": "5.3.1",
4444
"abslog": "2.4.4",
4545
"http-cache-semantics": "^4.0.3",
4646
"lodash.clonedeep": "^4.5.0",

tests/integration.basic.test.js

+63
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,66 @@ tap.test('integration - 103 early hints disabled', async (t) => {
624624

625625
await server.close();
626626
});
627+
628+
tap.test(
629+
'integration - 103 early hints used to build a document head',
630+
async (t) => {
631+
t.plan(1);
632+
const header = new PodletServer({
633+
name: 'header',
634+
assets: {
635+
js: '/header/bar.js',
636+
css: '/header/bar.css',
637+
},
638+
});
639+
const footer = new PodletServer({
640+
name: 'footer',
641+
assets: {
642+
js: '/footer/bar.js',
643+
css: '/footer/bar.css',
644+
},
645+
});
646+
647+
const service = await Promise.all([header.listen(), footer.listen()]);
648+
649+
const client = new Client({ name: 'podiumClient' });
650+
651+
const headerClient = client.register(service[0].options);
652+
const footerClient = client.register(service[1].options);
653+
654+
const incoming = new HttpIncoming({ headers });
655+
656+
incoming.hints.once('complete', ({ js, css }) => {
657+
const documentHead = `
658+
<html>
659+
<head>
660+
${css.map((style) => style.toHTML()).join('')}
661+
${js.map((script) => script.toHTML()).join('')}
662+
</head>
663+
<body>
664+
`;
665+
666+
t.equal(
667+
documentHead.trim().replace(/>\s*</g, '><'),
668+
`<html>
669+
<head>
670+
<link href="/header/bar.css" type="text/css" rel="stylesheet">
671+
<link href="/footer/bar.css" type="text/css" rel="stylesheet">
672+
<script src="/header/bar.js"></script>
673+
<script src="/footer/bar.js"></script>
674+
</head>
675+
<body>`
676+
.trim()
677+
.replace(/>\s*</g, '><'),
678+
);
679+
t.end();
680+
});
681+
682+
await Promise.all([
683+
headerClient.fetch(incoming),
684+
footerClient.fetch(incoming),
685+
]);
686+
687+
await Promise.all([header.close(), footer.close()]);
688+
},
689+
);

tests/resource.test.js

+119
Original file line numberDiff line numberDiff line change
@@ -822,3 +822,122 @@ tap.test(
822822
t.end();
823823
},
824824
);
825+
826+
tap.test(
827+
'Resource().fetch - hints complete event emitted once all early hints received - single resource component',
828+
async (t) => {
829+
t.plan(1);
830+
const server = new PodletServer({
831+
version: '1.0.0',
832+
assets: {
833+
js: '/foo/bar.js',
834+
css: '/foo/bar.css',
835+
},
836+
});
837+
const service = await server.listen();
838+
839+
const client = new Client({ name: 'podiumClient' });
840+
const component = client.register(service.options);
841+
842+
const incoming = new HttpIncoming({ headers: {} });
843+
844+
incoming.hints.on('complete', () => {
845+
t.ok(true);
846+
t.end();
847+
});
848+
849+
await component.fetch(incoming);
850+
851+
await server.close();
852+
},
853+
);
854+
855+
tap.test(
856+
'Resource().fetch - hints complete event emitted once all early hints received - resource is failing',
857+
async (t) => {
858+
t.plan(3);
859+
const server = new PodletServer({
860+
version: '1.0.0',
861+
assets: {
862+
js: '/foo/bar.js',
863+
css: '/foo/bar.css',
864+
},
865+
content: '/does/not/exist',
866+
});
867+
const service = await server.listen();
868+
869+
const client = new Client({ name: 'podiumClient' });
870+
const component = client.register(service.options);
871+
872+
const incoming = new HttpIncoming({ headers: {} });
873+
874+
incoming.hints.on('complete', (assets) => {
875+
t.ok(true);
876+
t.equal(assets.js.length, 1);
877+
t.equal(assets.css.length, 1);
878+
t.end();
879+
});
880+
881+
await component.fetch(incoming);
882+
883+
await server.close();
884+
},
885+
);
886+
887+
tap.test(
888+
'Resource().fetch - hints complete event emitted once all early hints received - multiple resource components',
889+
async (t) => {
890+
t.plan(3);
891+
const server1 = new PodletServer({
892+
name: 'one',
893+
version: '1.0.0',
894+
assets: {
895+
js: '/foo/bar.js',
896+
css: '/foo/bar.css',
897+
},
898+
});
899+
const service1 = await server1.listen();
900+
const server2 = new PodletServer({
901+
name: 'two',
902+
version: '1.0.0',
903+
assets: {
904+
js: '/foo/bar.js',
905+
css: '/foo/bar.css',
906+
},
907+
});
908+
const service2 = await server2.listen();
909+
const server3 = new PodletServer({
910+
name: 'three',
911+
version: '1.0.0',
912+
assets: {
913+
js: '/foo/bar.js',
914+
css: '/foo/bar.css',
915+
},
916+
});
917+
const service3 = await server3.listen();
918+
919+
const client = new Client({ name: 'podiumClient' });
920+
const component1 = client.register(service1.options);
921+
const component2 = client.register(service2.options);
922+
const component3 = client.register(service3.options);
923+
924+
const incoming = new HttpIncoming({ headers: {} });
925+
926+
incoming.hints.on('complete', (assets) => {
927+
t.equal(assets.js.length, 3);
928+
t.equal(assets.css.length, 3);
929+
t.ok(true);
930+
t.end();
931+
});
932+
933+
await Promise.all([
934+
component1.fetch(incoming),
935+
component2.fetch(incoming),
936+
component3.fetch(incoming),
937+
]);
938+
939+
await server1.close();
940+
await server2.close();
941+
await server3.close();
942+
},
943+
);

0 commit comments

Comments
 (0)