I wrote an idempotent script loader (for the browser), where the same script won't be loaded more than once.
scriptLoader.js
:
let _loadingAndLoadedScripts = [];
export async function loadJs(src) {
let hash = generateHash(src);
// scenario 1: first invocation, so load script
if (!_loadingAndLoadedScripts[hash]) {
return new Promise((resolve, reject) => {
let tag = document.createElement('script');
tag.type = 'text/javascript';
tag.id = hash;
tag.src = src;
tag.onload = () => {
tag.setAttribute('data-loaded', true);
document.dispatchEvent(new Event('scriptLoaded'));
resolve();
};
tag.onerror = () => {
console.error('Failed to load script \'' + src + '\'.');
reject();
}
_loadingAndLoadedScripts[src] = true; // save state
document.body.appendChild(tag);
});
}
// scenario 2: script is busy loading, or already loaded
else {
// if loaded, do nothing
var script = document.getElementById(hash);
let isLoaded = script && script.getAttribute('data-loaded') === 'true';
if (isLoaded) {
return Promise.resolve();
}
// else loading, so wait
else {
return new Promise((resolve, reject) => {
// ------------------ MAYBE WRAP IN TIMEOUT?
document.addEventListener('scriptLoaded', (e) => {
resolve();
}, { 'once': true });
// ------------------ MAYBE WRAP IN TIMEOUT?
});
}
}
}
function generateHash(s) {
// implementation irrelevant
}
It works.
But my JS is a bit rusty.I was thinking there could be a race condition whereby invocations 2+ (which wait for the script to load) would not register an event listener in time, and so wait forever for the event. A solution is to add a timer, and if it expires to reject
the promise. I've shown that in the code above as // MAYBE WRAP IN TIMEOUT?
.
This is the implementation I want to use inside of //-----
... //-----
:
return new Promise((resolve, reject) => {
let timer;
let eventName = 'scriptLoaded';
let eventHandler = (event) => {
clearTimeout(timer);
resolve();
}
let eventOptions = { 'once': true };
document.addEventListener(eventName, eventHandler, eventOptions);
timer = setTimeout(() => {
document.removeEventListener(eventName, eventHandler, eventOptions);
console.error('Failed to load script \'' + src + '\'.');
reject();
}, 10 * 1000);
});
It works.
But I'm wondering whether it's overkill and/or unnecessary? JS in the browser is single-threaded, so is it even possible that such a race condition would occur?