We have learned how to use the CustomEvent interface in a previous post.
How can we create a progress indicator using the same JavaScript code for both browser and terminal (using Node.js)? For this we can build a fetch wrapper with a progress event using the CustomEvent interface, which is compatible with both environments.
📣 The CustomEvent interface was added in Node.js v18.7.0 as an experimental API, and it's exposed on global using the --experimental-global-customevent flag.
Implementing our event
We need to extend the EventTarget interface to dispatch events from our custom class so the clients can subscribe to our events.
class Http extends EventTarget {
…
async get(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(response.statusText);
}
const contentLength = this._getContentLength(response);
const self = this;
const res = new Response(new ReadableStream({
async start(controller) {
const reader = response.body.getReader();
let loaded = 0;
try {
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
loaded += value.byteLength;
if (contentLength) {
self.dispatchEvent(new CustomEvent('progress', {detail: {contentLength, loaded}}));
}
controller.enqueue(value);
}
controller.close();
} catch (err) {
controller.error(err);
}
}
}));
return res.blob();
}
}
export default Http;
We wrapped the ReadableStream instance of the body property into a custom implementation to notify the read progress to the listeners of the progress event. We should also read() all the content of the response until the done flag indicates that we've reached the end of the stream.
Using our progress event in the terminal
Let's import the Http class and add an event listener for the progress event. For this example we're going to use a server with download speed up to 30kbps.
const exec = async () => {
const { default: Http } = await import('./http.mjs');
const http = new Http();
const listener = e => console.log(e.detail);
http.addEventListener('progress', listener);
await http.get('https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg');
http.removeEventListener('progress', listener);
}
exec();
💡 The listener should be removed to avoid memory leaks in our server. 😉
🧠 We need to use the dynamic import() to import ES modules into CommonJS code.
To run this code, we should include the --experimental-global-customevent flag; otherwise the CustomEvent class will be undefined.
node --experimental-global-customevent index.js
Using our progress event in the browser
Let's create an index.html and import our JavaScript module using the following code:
<script type="module">
import Http from './http.mjs';
const http = new Http();
const listener = e => console.log(e.detail);
http.addEventListener('progress', listener);
await http.get('https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg');
http.removeEventListener('progress', listener);
</script>
We can run our example locally with the following command:
npx http-server
Now we can navigate to http://localhost:8080 and check the console output.
Conclusion
With the EventTarget interface we can create reusable code detached from our UI that can be connected to either HTML elements or the terminal to inform progress to our users.
If we don't want to use an experimental API behind the flag in our server we can use the EventEmitter class in Node.js.
You can check the full code example in https://github.com/navarroaxel/fetch-progress.
For this post, I have adapted the fetch-basic example from https://github.com/AnthumChris/fetch-progress-indicators by @anthumchris.
Open source rocks. 🤘


Top comments (0)