core
core copied to clipboard
When a remote is offline, it takes multiple seconds on Windows before a remote is considered as unavailable.
Describe the bug
When a remote is offline, it takes multiple seconds on Windows before a remote is considered as unavailable, leaving the users with a blank page until the shared dependencies negotiation is done and the application is rendered.
According to my POC, a production build hosted on Netlify, when a remote is offline on Windows, it takes approximately 2500ms until the application is rendered.
On macOS, it takes about 20ms, which is a lot faster.
This delay doesn't seems to be related to any custom code but rather on how Windows behave when a connection is refused. It seems to retry 3 times before failing with ERR_CONNECTION_REFUSED.
Similar issues has been observed for other projects:
- https://stackoverflow.com/questions/19440364/why-do-failed-attempts-of-socket-connect-take-1-sec-on-windows
- https://github.com/warmcat/libwebsockets/issues/570
@ryok90 and I have been extensively discussing about this issue in the past few days on the Module Federation Discord's server and came to the conclusion that there is currently nothing offered by Module Federation to actually help with this issue: https://discord.com/channels/1055442562959290389/1060923312043212920/threads/1232010715381108929
👉🏻 We believe that the solution would be for Module Federation to include a mechanism allowing the authors to specify a timeout delay to fetch a remote, rather the relying on the OS defaults.
When the host is configuring a remote with a mf-manifest.json file, it seems like the manifest is fetched with fetch, a timeout could be introduced with an AbortSignal.
When the host is configuring a remote with a remoteEntry.js file, it seems like the remote is fetched with a script element. Something similar to the following code could be added to eagerly reject a remote when it is offline. This is how we used to do it with Module Federation 1.0:
function loadRemoteScript(url: string, { timeoutDelay = 500 }: LoadRemoteScriptOptions = {}) {
return new Promise((resolve, reject) => {
const element = document.createElement("script");
// Adding a timestamp to make sure the remote entry points are never cached.
// View: https://github.com/module-federation/module-federation-examples/issues/566.
element.src = `${url}?t=${Date.now()}`;
element.type = "text/javascript";
element.async = true;
let timeoutId: number | undefined = undefined;
let hasCanceled = false;
function cancel(error: Error) {
hasCanceled = true;
element?.parentElement?.removeChild(element);
reject({
error,
hasCanceled: true
});
}
element.onload = () => {
window.clearTimeout(timeoutId);
element?.parentElement?.removeChild(element);
resolve({});
};
element.onerror = (error: unknown) => {
if (!hasCanceled) {
window.clearTimeout(timeoutId);
element?.parentElement?.removeChild(element);
reject({
error,
hasCanceled: false
});
}
};
document.head.appendChild(element);
// Eagerly reject the loading of a script, it's too long when a remote is unavailable.
timeoutId = window.setTimeout(() => {
cancel(new Error(`[squide] Remote script "${url}" time-outed.`));
}, timeoutDelay);
});
}
Thank you,
Patrick
Reproduction
https://pat-mf-enhanced-poc.netlify.app/
Used Package Manager
pnpm
System Info
System:
OS: Windows 11 10.0.22631
CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Memory: 13.30 GB / 31.70 GB
Binaries:
Node: 21.7.1 - C:\Program Files\nodejs\node.EXE
Yarn: 1.22.19 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
npm: 10.5.0 - C:\Program Files\nodejs\npm.CMD
pnpm: 8.15.4 - ~\AppData\Roaming\npm\pnpm.CMD
Browsers:
Chrome: 124.0.6367.61
Edge: Chromium (123.0.2420.97)
Validations
- [X] Read the docs.
- [X] Read the common issues list.
- [X] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- [X] Make sure this is a Module federation issue and not a framework-specific issue.
- [X] The provided reproduction is a minimal reproducible example of the bug.
createScript hook is probbably what you are after: https://module-federation.io/plugin/dev/index.html#createscript
@ScriptedAlchemy I have been on this road, the issue with the createScript hook is that I don't have any control over when Module Federation choose to stop waiting for the remote to be available, it's basically only a script element factory.
cant you throw the script after timeout?
@ScriptedAlchemy not exactly.
If I always throw an Error in the createScript hook:
createScript({ url }) {
throw new Error(`Remote script "${url}" time-outed.`);
}
Then, it kinds of work as expected since the catch handler of my loadRemote function catch the error:
await loadRemote("remote1/HelloWorld.jsx")
.then(mod => {
console.log("Loaded remote 1", mod);
})
.catch((error) => console.log("Failed to load remote 1", error));
But the error that is catched, is not the exception I thrown:
TypeError: remoteEntryExports.get is not a function
Then, if I delay the throw of the Error by 500ms for the timeout:
createScript({ url }) {
const element = document.createElement("script");
// Adding a timestamp to make sure the remote entry points are never cached.
// View: https://github.com/module-federation/module-federation-examples/issues/566.
element.src = `${url}?t=${Date.now()}`;
element.type = "text/javascript";
element.async = true;
let timeoutId = undefined;
element.onload = () => {
window.clearTimeout(timeoutId);
}
// Eagerly reject the loading of a script, it's too long when a remote is unavailable.
timeoutId = window.setTimeout(() => {
throw new Error(`Remote script "${url}" time-outed.`);
}, 500);
return element;
}
It doesn't work anymore as the error is not catched anymore by the catch handler of the loadRemote function and it crashes the application. I don't feel like the code around the createScript hook has been written to handle a throw.
I also tried hustling something with the beforeRequest hook, but I haven't been to because of https://github.com/module-federation/universe/issues/2371.
@patricklafrance https://github.com/module-federation/core/pull/2433/files
hows that?
This would change the return types of createScript
return htmlElement | {script?: htmlElement, timeout?:number}
return {script: myScript} return {timeout: 1000} return {script,timeout} return scriptElement
@ScriptedAlchemy LGTM. One question thought, as a consumer, how do I handle:
onScriptComplete(null, new Error(`Remote script "${url}" time-outed.`)); ?
Is it with either the errorLoadRemote hook or loadRemote.catch ?
Stale issue message