Skip to content

Commit

Permalink
Merge pull request #441 from podium-lib/track_podlet_assets
Browse files Browse the repository at this point in the history
feat: track podlet assets
  • Loading branch information
digitalsadhu authored Nov 13, 2024
2 parents aab67bc + a0c7e63 commit 757316f
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 19 deletions.
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();
},
);

0 comments on commit 757316f

Please sign in to comment.