Skip to content

Commit dc84253

Browse files
committed
feat(runtime): add loadEntryTimeout option for the remote entry script timeout
1 parent 5727cf3 commit dc84253

9 files changed

Lines changed: 125 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@module-federation/sdk': patch
3+
'@module-federation/runtime-core': patch
4+
---
5+
6+
feat: `loadEntryTimeout` runtime option — how long a remote entry script may take to load before it fails with RUNTIME-008 (default 20000 ms, `Infinity` disables the timer); `createScript` / `loadScript` accept a `timeout` and a `createScript` hook return value still overrides it

apps/website-new/docs/en/guide/runtime/runtime-api.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ const mf = createInstance({
4141
mf.loadRemote('sub1/util').then((m) => m.add(1, 2, 3));
4242
```
4343

44+
**Options**
45+
46+
Besides `name`, `remotes`, `shared`, `plugins` and `shareStrategy`, the instance accepts:
47+
48+
- `loadEntryTimeout` — how long (ms) a remote entry script may take to load before the load fails with `RUNTIME-008`. Defaults to `20000`; pass `Infinity` to wait indefinitely. A `createScript` hook that returns `timeout` still takes precedence.
49+
4450
## init <Badge type='warning'>Use with caution</Badge>
4551

4652
Used to initialize or reuse a `ModuleFederation` runtime instance.

apps/website-new/docs/zh/guide/runtime/runtime-api.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ const mf = createInstance({
4141
mf.loadRemote('sub1/util').then((m) => m.add(1, 2, 3));
4242
```
4343

44+
**配置项**
45+
46+
除了 `name``remotes``shared``plugins``shareStrategy`,实例还接受:
47+
48+
- `loadEntryTimeout` — 远程入口脚本的加载超时时间(毫秒),超时后以 `RUNTIME-008` 报错。默认 `20000`;传 `Infinity` 表示不限时。`createScript` 钩子返回的 `timeout` 仍然优先。
49+
4450
## init <Badge type='warning'>谨慎使用</Badge>
4551

4652
用于初始化或复用 `ModuleFederation` 运行时实例。

packages/runtime-core/__tests__/load.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,53 @@ describe('getRemoteEntry - script load error discrimination', () => {
126126
);
127127
});
128128

129+
it('passes loadEntryTimeout to the entry script loader', async () => {
130+
const entry = `${BASE}/success.js`;
131+
const origin = new ModuleFederation({
132+
name: 'test-host',
133+
remotes: [],
134+
loadEntryTimeout: 45000,
135+
});
136+
const remoteInfo = getRemoteInfo({ name: 'remote', entry });
137+
const setTimeoutSpy = rs.spyOn(globalThis, 'setTimeout');
138+
139+
try {
140+
await getRemoteEntry({ origin, remoteInfo });
141+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 45000);
142+
expect(setTimeoutSpy).not.toHaveBeenCalledWith(
143+
expect.any(Function),
144+
20000,
145+
);
146+
} finally {
147+
setTimeoutSpy.mockRestore();
148+
}
149+
});
150+
151+
it('does not arm the entry timer when loadEntryTimeout is Infinity', async () => {
152+
const entry = `${BASE}/success.js`;
153+
const origin = new ModuleFederation({
154+
name: 'test-host',
155+
remotes: [],
156+
loadEntryTimeout: Infinity,
157+
});
158+
const remoteInfo = getRemoteInfo({ name: 'remote', entry });
159+
const setTimeoutSpy = rs.spyOn(globalThis, 'setTimeout');
160+
161+
try {
162+
await getRemoteEntry({ origin, remoteInfo });
163+
expect(setTimeoutSpy).not.toHaveBeenCalledWith(
164+
expect.any(Function),
165+
20000,
166+
);
167+
expect(setTimeoutSpy).not.toHaveBeenCalledWith(
168+
expect.any(Function),
169+
Infinity,
170+
);
171+
} finally {
172+
setTimeoutSpy.mockRestore();
173+
}
174+
});
175+
129176
it('module entry load failure can recover through loadEntryError with getEntryUrl', async () => {
130177
const entry = createDataUrlEntry(
131178
`throw new TypeError('Failed to fetch dynamically imported module: http://localhost:4999/remoteEntry.js');`,

packages/runtime-core/src/type/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ export interface Options {
141141
plugins: Array<ModuleFederationRuntimePlugin>;
142142
inBrowser: boolean;
143143
shareStrategy?: ShareStrategy;
144+
/**
145+
* How long (ms) a remote entry script may take to load before it is reported as RUNTIME-008.
146+
* Defaults to 20000; `Infinity` disables the timer. A `createScript` hook returning `timeout` still wins.
147+
*/
148+
loadEntryTimeout?: number;
144149
}
145150

146151
export type UserOptions = Omit<

packages/runtime-core/src/utils/load.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ async function loadEntryScript({
165165
loaderHook,
166166
getEntryUrl,
167167
resourceContext,
168+
timeout,
168169
}: {
169170
name: string;
170171
globalName: string;
@@ -173,6 +174,7 @@ async function loadEntryScript({
173174
loaderHook: ModuleFederation['loaderHook'];
174175
getEntryUrl?: (url: string) => string;
175176
resourceContext?: ResourceLoadContext;
177+
timeout?: number;
176178
}): Promise<RemoteEntryExports> {
177179
const { entryExports: remoteEntryExports } = getRemoteEntryExports(
178180
name,
@@ -187,6 +189,7 @@ async function loadEntryScript({
187189
const url = getEntryUrl ? getEntryUrl(entry) : entry;
188190
return loadScript(url, {
189191
attrs: {},
192+
timeout,
190193
createScriptHook: (url, attrs) => {
191194
const res = loaderHook.lifecycle.createScript.emit({
192195
url,
@@ -244,12 +247,14 @@ async function loadEntryDom({
244247
loaderHook,
245248
getEntryUrl,
246249
resourceContext,
250+
timeout,
247251
}: {
248252
remoteInfo: RemoteInfo;
249253
remoteEntryExports?: RemoteEntryExports;
250254
loaderHook: ModuleFederation['loaderHook'];
251255
getEntryUrl?: (url: string) => string;
252256
resourceContext?: ResourceLoadContext;
257+
timeout?: number;
253258
}) {
254259
const { entry, entryGlobalName: globalName, name, type } = remoteInfo;
255260
if (isEsmRemoteType(type)) {
@@ -268,6 +273,7 @@ async function loadEntryDom({
268273
loaderHook,
269274
getEntryUrl,
270275
resourceContext,
276+
timeout,
271277
});
272278
}
273279

@@ -388,6 +394,7 @@ export async function getRemoteEntry(params: {
388394
loaderHook,
389395
getEntryUrl,
390396
resourceContext,
397+
timeout: origin.options.loadEntryTimeout,
391398
})
392399
: loadEntryNode({ remoteInfo, loaderHook, resourceContext });
393400
})

packages/runtime-core/src/utils/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ function waitForScriptPreload({
233233
);
234234
},
235235
attrs,
236+
timeout: host.options.loadEntryTimeout,
236237
createScriptHook: (hookUrl: string, hookAttrs: any) => {
237238
const res = host.loaderHook.lifecycle.createScript.emit({
238239
url: hookUrl,

packages/sdk/__tests__/dom.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,36 @@ describe('createScript', () => {
8686
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 20000);
8787
});
8888

89+
it('should use the timeout passed to createScript', () => {
90+
const url = 'https://example.com/script.js';
91+
const cb = jest.fn();
92+
createScript({ url, cb, timeout: 45000 });
93+
94+
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 45000);
95+
});
96+
97+
it('should let the createScriptHook timeout override the passed timeout', () => {
98+
const url = 'https://example.com/script.js';
99+
const cb = jest.fn();
100+
createScript({
101+
url,
102+
cb,
103+
attrs: {},
104+
timeout: 45000,
105+
createScriptHook: () => ({ timeout: 5000 }),
106+
});
107+
108+
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 5000);
109+
});
110+
111+
it('should not start a timer when the timeout is Infinity', () => {
112+
const url = 'https://example.com/script.js';
113+
const cb = jest.fn();
114+
createScript({ url, cb, timeout: Infinity });
115+
116+
expect(setTimeout).not.toHaveBeenCalled();
117+
});
118+
89119
it('should use the timeout specified in the createScriptHook', () => {
90120
const url = 'https://example.com/script.js';
91121
const cb = jest.fn();

packages/sdk/src/dom.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,26 @@ export function isStaticResourcesEqual(url1: string, url2: string): boolean {
2828
return relativeUrl1 === relativeUrl2;
2929
}
3030

31+
export const DEFAULT_SCRIPT_TIMEOUT = 20000;
32+
3133
export function createScript(info: {
3234
url: string;
3335
cb?: (value: void | PromiseLike<void>) => void;
3436
onErrorCallback?: (error: Error) => void;
3537
attrs?: Record<string, any>;
3638
needDeleteScript?: boolean;
3739
createScriptHook?: CreateScriptHookDom;
40+
/** Load timeout in ms; a `createScriptHook` return value still overrides it. `Infinity` disables the timer. */
41+
timeout?: number;
3842
}): { script: HTMLScriptElement; needAttach: boolean } {
3943
// Retrieve the existing script element by its src attribute
4044
let script: HTMLScriptElement | null = null;
4145
let needAttach = true;
42-
let timeout = 20000;
43-
let timeoutId: NodeJS.Timeout;
46+
let timeout =
47+
typeof info.timeout === 'number' && info.timeout > 0
48+
? info.timeout
49+
: DEFAULT_SCRIPT_TIMEOUT;
50+
let timeoutId: NodeJS.Timeout | undefined;
4451
const scripts = document.getElementsByTagName('script');
4552

4653
for (let i = 0; i < scripts.length; i++) {
@@ -162,9 +169,11 @@ export function createScript(info: {
162169
script.onerror = onScriptComplete.bind(null, script.onerror);
163170
script.onload = onScriptComplete.bind(null, script.onload);
164171

165-
timeoutId = setTimeout(() => {
166-
onScriptComplete(null, { type: 'error', isTimeout: true });
167-
}, timeout);
172+
if (Number.isFinite(timeout)) {
173+
timeoutId = setTimeout(() => {
174+
onScriptComplete(null, { type: 'error', isTimeout: true });
175+
}, timeout);
176+
}
168177

169178
return { script, needAttach };
170179
}
@@ -295,9 +304,10 @@ export function loadScript(
295304
info: {
296305
attrs?: Record<string, any>;
297306
createScriptHook?: CreateScriptHookDom;
307+
timeout?: number;
298308
},
299309
) {
300-
const { attrs = {}, createScriptHook } = info;
310+
const { attrs = {}, createScriptHook, timeout } = info;
301311
return new Promise<void>((resolve, reject) => {
302312
const { script, needAttach } = createScript({
303313
url,
@@ -309,6 +319,7 @@ export function loadScript(
309319
},
310320
createScriptHook,
311321
needDeleteScript: true,
322+
timeout,
312323
});
313324
needAttach && document.head.appendChild(script);
314325
});

0 commit comments

Comments
 (0)