Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: track podlet assets #441

Merged
merged 4 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ The following values can be provided:
- `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.
- `excludeBy` - {Object} - Lets you define a set of rules where a `fetch` call will not be resolved if it matches. - Optional.
- `includeBy` - {Object} - Inverse of `excludeBy`. Setting both at the same time will throw. - Optional.
- `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).

##### `excludeBy` and `includeBy`

Expand Down
40 changes: 24 additions & 16 deletions lib/http-outgoing.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PassThrough } from 'stream';
import assert from 'assert';
import { toPreloadAssetObjects, filterAssets } from './utils.js';
import { filterAssets } from './utils.js';

/**
* @typedef {object} PodiumClientHttpOutgoingOptions
Expand Down Expand Up @@ -70,6 +70,7 @@ export default class PodletClientHttpOutgoing extends PassThrough {
#uri;
#js;
#css;
#assetsReceived = false;

/**
* @constructor
Expand Down Expand Up @@ -332,6 +333,26 @@ export default class PodletClientHttpOutgoing extends PassThrough {
return this.#isFallback;
}

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

/**
* Set the assetsReceived flag.
* This is used to signal to the assets object that the client has received assets for a given podlet so it can track
* which podlets have sent their assets.
* @param {boolean} value
*/
set assetsReceived(value) {
this.#assetsReceived = value;
if (this.#assetsReceived) {
this.#incoming?.assets?.addReceivedAsset(this.#name, {
js: this.js,
css: this.css,
});
}
}

pushFallback() {
// @ts-expect-error Internal property
this.push(this.#manifest._fallback);
Expand All @@ -349,21 +370,8 @@ export default class PodletClientHttpOutgoing extends PassThrough {
: filterAssets('fallback', 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 = () => {}) {
if (this.#incoming.response.writeEarlyHints) {
const preloads = toPreloadAssetObjects([
...(this.js || []),
...(this.css || []),
]);
const link = preloads.map((preload) => preload.toHeader());
if (link.length) {
this.#incoming.response.writeEarlyHints({ link }, cb);
}
}
// assume the assets from the podlet have failed and fallback assets will be used
this.assetsReceived = true;
}

get [Symbol.toStringTag]() {
Expand Down
3 changes: 3 additions & 0 deletions lib/resolver.content.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ export default class PodletClientContentResolver {
}),
);

// mark assets as received
outgoing.assetsReceived = true;

// @ts-ignore
pipeline([body, outgoing], (err) => {
if (err) {
Expand Down
1 change: 0 additions & 1 deletion lib/resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import Cache from './resolver.cache.js';
* @typedef {object} PodletClientResolverOptions
* @property {string} clientName
* @property {import('abslog').AbstractLoggerOptions} [logger]
* @property {boolean} [earlyHints]
*/

export default class PodletClientResolver {
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,8 @@ export default class PodiumClientResource {
throw new TypeError(
'you must pass an instance of "HttpIncoming" as the first argument to the .fetch() method',
);
// set expectation to receive a response from this podlet.
incoming.assets.addExpectedAsset(this.#options.name);
const outgoing = new HttpOutgoing(this.#options, reqOptions, incoming);

if (this.#options.excludeBy) {
Expand Down Expand Up @@ -192,6 +194,8 @@ export default class PodiumClientResource {
throw new TypeError(
'you must pass an instance of "HttpIncoming" as the first argument to the .stream() method',
);
// set expectation to receive a response from this podlet.
incoming.assets.addExpectedAsset(this.#options.name);
const outgoing = new HttpOutgoing(this.#options, reqOptions, incoming);
this.#state.setInitializingState();
this.#resolver.resolve(outgoing);
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.1.0",
"@podium/utils": "5.3.2",
"@podium/utils": "5.4.0",
"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 @@ -710,3 +710,66 @@ tap.test(
server.close();
},
);

tap.test(
'integration - asset link header used to build a document head',
async (t) => {
t.plan(1);
const header = new PodletServer({
name: 'header',
assets: {
js: 'http://example.com/header/bar.js',
css: 'http://example.com/header/bar.css',
},
});
const footer = new PodletServer({
name: 'footer',
assets: {
js: 'http://example.com/footer/bar.js',
css: 'http://example.com/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 });

const headerFetch = headerClient.fetch(incoming);
const footerFetch = footerClient.fetch(incoming);

incoming.assets.once('received', ({ 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="http://example.com/header/bar.css" type="text/css" rel="stylesheet">
<link href="http://example.com/footer/bar.css" type="text/css" rel="stylesheet">
<script src="http://example.com/header/bar.js"></script>
<script src="http://example.com/footer/bar.js"></script>
</head>
<body>`
.trim()
.replace(/>\s*</g, '><'),
);
t.end();
});

await Promise.all([headerFetch, footerFetch]);

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

tap.test(
'Resource().fetch - assets received event emitted once all link header assets 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.assets.on('received', () => {
t.ok(true);
t.end();
});

await component.fetch(incoming);

await server.close();
},
);

tap.test(
'Resource().fetch - assets received event emitted once all assets 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.assets.on('received', (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 - assets received event emitted once all assets 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.assets.on('received', (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();
},
);

tap.test(
'Resource().fetch - waitForAssets method - 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: {} });

const c1 = component1.fetch(incoming);
const c2 = component2.fetch(incoming);
const c3 = component3.fetch(incoming);

const assets = await incoming.waitForAssets();
t.equal(assets.js.length, 3);
t.equal(assets.css.length, 3);
t.ok(true);

await Promise.all([c1, c2, c3]);

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