Skip to content

Commit

Permalink
Merge pull request #421 from podium-lib/track_early_hints_across_reso…
Browse files Browse the repository at this point in the history
…urces

feat: keep track of which resources have emitted early hints and emit complete event once all resources have emitted
  • Loading branch information
digitalsadhu authored Sep 24, 2024
2 parents 97ca27a + da5eb0b commit 51c0251
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 5 deletions.
17 changes: 17 additions & 0 deletions lib/http-outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export default class PodletClientHttpOutgoing extends PassThrough {
#uri;
#js;
#css;
#hintsReceived = false;

/**
* @constructor
Expand Down Expand Up @@ -300,6 +301,20 @@ export default class PodletClientHttpOutgoing extends PassThrough {
this.#redirect = value;
}

get hintsReceived() {
return this.#hintsReceived;
}

set hintsReceived(value) {
this.#hintsReceived = value;
if (this.#hintsReceived) {
this.#incoming?.hints?.addReceivedHint(this.#name, {
js: this.js,
css: this.css,
});
}
}

/**
* Whether the podlet can signal redirects to the layout.
*
Expand Down Expand Up @@ -337,6 +352,8 @@ export default class PodletClientHttpOutgoing extends PassThrough {
this.css = this.#manifest._css;
this.push(null);
this.#isFallback = true;
// assume the hints from the podlet have failed and fallback assets will be used
this.hintsReceived = true;
}

writeEarlyHints(cb = () => {}) {
Expand Down
6 changes: 2 additions & 4 deletions lib/resolver.content.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,6 @@ export default class PodletClientContentResolver {
outgoing.contentUri,
);

let hintsReceived = false;

/** @type {import('./http.js').PodiumHttpClientRequestOptions} */
const reqOptions = {
rejectUnauthorized: outgoing.rejectUnauthorized,
Expand All @@ -149,7 +147,7 @@ export default class PodletClientContentResolver {
query: outgoing.reqOptions.query,
headers,
onInfo: ({ statusCode, headers }) => {
if (statusCode === 103 && !hintsReceived) {
if (statusCode === 103 && !outgoing.hintsReceived) {
const parsedAssetObjects = parseLinkHeaders(headers.link);

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

hintsReceived = true;
outgoing.hintsReceived = true;
}
},
};
Expand Down
4 changes: 4 additions & 0 deletions lib/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export default class PodiumClientResource {
throw new TypeError(
'you must pass an instance of "HttpIncoming" as the first argument to the .fetch() method',
);
// add the name of this resource as expecting a hint to be received
// we use this to track across resources and emit a hint completion event once
// all hints from all resources have been received.
incoming.hints.addExpectedHint(this.#options.name);
const outgoing = new HttpOutgoing(this.#options, reqOptions, incoming);

if (this.#options.excludeBy) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@hapi/boom": "10.0.1",
"@metrics/client": "2.5.3",
"@podium/schemas": "5.0.6",
"@podium/utils": "5.2.1",
"@podium/utils": "5.3.1",
"abslog": "2.4.4",
"http-cache-semantics": "^4.0.3",
"lodash.clonedeep": "^4.5.0",
Expand Down
63 changes: 63 additions & 0 deletions tests/integration.basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -624,3 +624,66 @@ tap.test('integration - 103 early hints disabled', async (t) => {

await server.close();
});

tap.test(
'integration - 103 early hints used to build a document head',
async (t) => {
t.plan(1);
const header = new PodletServer({
name: 'header',
assets: {
js: '/header/bar.js',
css: '/header/bar.css',
},
});
const footer = new PodletServer({
name: 'footer',
assets: {
js: '/footer/bar.js',
css: '/footer/bar.css',
},
});

const service = await Promise.all([header.listen(), footer.listen()]);

const client = new Client({ name: 'podiumClient' });

const headerClient = client.register(service[0].options);
const footerClient = client.register(service[1].options);

const incoming = new HttpIncoming({ headers });

incoming.hints.once('complete', ({ js, css }) => {
const documentHead = `
<html>
<head>
${css.map((style) => style.toHTML()).join('')}
${js.map((script) => script.toHTML()).join('')}
</head>
<body>
`;

t.equal(
documentHead.trim().replace(/>\s*</g, '><'),
`<html>
<head>
<link href="/header/bar.css" type="text/css" rel="stylesheet">
<link href="/footer/bar.css" type="text/css" rel="stylesheet">
<script src="/header/bar.js"></script>
<script src="/footer/bar.js"></script>
</head>
<body>`
.trim()
.replace(/>\s*</g, '><'),
);
t.end();
});

await Promise.all([
headerClient.fetch(incoming),
footerClient.fetch(incoming),
]);

await Promise.all([header.close(), footer.close()]);
},
);
119 changes: 119 additions & 0 deletions tests/resource.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -822,3 +822,122 @@ tap.test(
t.end();
},
);

tap.test(
'Resource().fetch - hints complete event emitted once all early hints received - single resource component',
async (t) => {
t.plan(1);
const server = new PodletServer({
version: '1.0.0',
assets: {
js: '/foo/bar.js',
css: '/foo/bar.css',
},
});
const service = await server.listen();

const client = new Client({ name: 'podiumClient' });
const component = client.register(service.options);

const incoming = new HttpIncoming({ headers: {} });

incoming.hints.on('complete', () => {
t.ok(true);
t.end();
});

await component.fetch(incoming);

await server.close();
},
);

tap.test(
'Resource().fetch - hints complete event emitted once all early hints received - resource is failing',
async (t) => {
t.plan(3);
const server = new PodletServer({
version: '1.0.0',
assets: {
js: '/foo/bar.js',
css: '/foo/bar.css',
},
content: '/does/not/exist',
});
const service = await server.listen();

const client = new Client({ name: 'podiumClient' });
const component = client.register(service.options);

const incoming = new HttpIncoming({ headers: {} });

incoming.hints.on('complete', (assets) => {
t.ok(true);
t.equal(assets.js.length, 1);
t.equal(assets.css.length, 1);
t.end();
});

await component.fetch(incoming);

await server.close();
},
);

tap.test(
'Resource().fetch - hints complete event emitted once all early hints received - multiple resource components',
async (t) => {
t.plan(3);
const server1 = new PodletServer({
name: 'one',
version: '1.0.0',
assets: {
js: '/foo/bar.js',
css: '/foo/bar.css',
},
});
const service1 = await server1.listen();
const server2 = new PodletServer({
name: 'two',
version: '1.0.0',
assets: {
js: '/foo/bar.js',
css: '/foo/bar.css',
},
});
const service2 = await server2.listen();
const server3 = new PodletServer({
name: 'three',
version: '1.0.0',
assets: {
js: '/foo/bar.js',
css: '/foo/bar.css',
},
});
const service3 = await server3.listen();

const client = new Client({ name: 'podiumClient' });
const component1 = client.register(service1.options);
const component2 = client.register(service2.options);
const component3 = client.register(service3.options);

const incoming = new HttpIncoming({ headers: {} });

incoming.hints.on('complete', (assets) => {
t.equal(assets.js.length, 3);
t.equal(assets.css.length, 3);
t.ok(true);
t.end();
});

await Promise.all([
component1.fetch(incoming),
component2.fetch(incoming),
component3.fetch(incoming),
]);

await server1.close();
await server2.close();
await server3.close();
},
);

0 comments on commit 51c0251

Please sign in to comment.