@module-federation/retry-plugin 0.18.4 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -5,88 +5,280 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
5
5
  var defaultRetries = 3;
6
6
  var defaultRetryDelay = 1e3;
7
7
  var PLUGIN_IDENTIFIER = "[ Module Federation RetryPlugin ]";
8
+ var ERROR_ABANDONED = "The request failed and has now been abandoned";
8
9
 
9
10
  // packages/retry-plugin/src/logger.ts
10
11
  import { createLogger } from "@module-federation/sdk";
11
12
  var logger = createLogger(PLUGIN_IDENTIFIER);
12
13
  var logger_default = logger;
13
14
 
15
+ // packages/retry-plugin/src/utils.ts
16
+ function rewriteWithNextDomain(currentUrl, domains) {
17
+ if (!domains || domains.length === 0)
18
+ return null;
19
+ try {
20
+ const u = new URL(currentUrl);
21
+ const currentHostname = u.hostname;
22
+ const currentPort = u.port;
23
+ const currentHost = `${currentHostname}${currentPort ? `:${currentPort}` : ""}`;
24
+ const normalized = domains.map((d) => {
25
+ try {
26
+ const du = new URL(d.startsWith("http") ? d : `https://${d}`);
27
+ return {
28
+ hostname: du.hostname,
29
+ port: du.port,
30
+ protocol: du.protocol
31
+ };
32
+ } catch {
33
+ return {
34
+ hostname: d,
35
+ port: "",
36
+ protocol: u.protocol
37
+ };
38
+ }
39
+ }).filter((d) => !!d.hostname);
40
+ if (normalized.length === 0)
41
+ return null;
42
+ let idx = -1;
43
+ for (let i = normalized.length - 1; i >= 0; i--) {
44
+ const candHost = `${normalized[i].hostname}${normalized[i].port ? `:${normalized[i].port}` : ""}`;
45
+ if (candHost === currentHost) {
46
+ idx = i;
47
+ break;
48
+ }
49
+ }
50
+ const total = normalized.length;
51
+ for (let step = 1; step <= total; step++) {
52
+ const nextIdx = ((idx >= 0 ? idx : -1) + step) % total;
53
+ const candidate = normalized[nextIdx];
54
+ const candidateHost = `${candidate.hostname}${candidate.port ? `:${candidate.port}` : ""}`;
55
+ if (candidateHost !== currentHost) {
56
+ u.hostname = candidate.hostname;
57
+ if (candidate.port !== void 0 && candidate.port !== null && candidate.port !== "") {
58
+ u.port = candidate.port;
59
+ } else {
60
+ u.port = "";
61
+ }
62
+ u.protocol = candidate.protocol || u.protocol;
63
+ return u.toString();
64
+ }
65
+ }
66
+ return null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ __name(rewriteWithNextDomain, "rewriteWithNextDomain");
72
+ function appendRetryCountQuery(url, retryIndex, key = "retryCount") {
73
+ try {
74
+ const u = new URL(url);
75
+ u.searchParams.delete(key);
76
+ u.searchParams.set(key, String(retryIndex));
77
+ return u.toString();
78
+ } catch {
79
+ return url;
80
+ }
81
+ }
82
+ __name(appendRetryCountQuery, "appendRetryCountQuery");
83
+ function getRetryUrl(baseUrl, opts = {}) {
84
+ const { domains, addQuery, retryIndex = 0, queryKey = "retryCount" } = opts;
85
+ let cleanBaseUrl = baseUrl;
86
+ try {
87
+ const urlObj = new URL(baseUrl);
88
+ urlObj.searchParams.delete(queryKey);
89
+ cleanBaseUrl = urlObj.toString();
90
+ } catch {
91
+ }
92
+ let nextUrl = rewriteWithNextDomain(cleanBaseUrl, domains) ?? cleanBaseUrl;
93
+ if (retryIndex > 0 && addQuery) {
94
+ try {
95
+ const u = new URL(nextUrl);
96
+ const originalUrl = new URL(baseUrl);
97
+ originalUrl.searchParams.delete(queryKey);
98
+ const originalQuery = originalUrl.search.startsWith("?") ? originalUrl.search.slice(1) : originalUrl.search;
99
+ if (typeof addQuery === "function") {
100
+ const newQuery = addQuery({
101
+ times: retryIndex,
102
+ originalQuery
103
+ });
104
+ u.search = newQuery ? `?${newQuery.replace(/^\?/, "")}` : "";
105
+ nextUrl = u.toString();
106
+ } else if (addQuery === true) {
107
+ u.searchParams.delete(queryKey);
108
+ u.searchParams.set(queryKey, String(retryIndex));
109
+ nextUrl = u.toString();
110
+ }
111
+ } catch {
112
+ if (addQuery === true) {
113
+ nextUrl = appendRetryCountQuery(nextUrl, retryIndex, queryKey);
114
+ }
115
+ }
116
+ }
117
+ return nextUrl;
118
+ }
119
+ __name(getRetryUrl, "getRetryUrl");
120
+ function combineUrlDomainWithPathQuery(domainUrl, pathQueryUrl) {
121
+ try {
122
+ const domainUrlObj = new URL(domainUrl);
123
+ const pathQueryUrlObj = new URL(pathQueryUrl);
124
+ domainUrlObj.pathname = pathQueryUrlObj.pathname;
125
+ domainUrlObj.search = pathQueryUrlObj.search;
126
+ return domainUrlObj.toString();
127
+ } catch {
128
+ return pathQueryUrl;
129
+ }
130
+ }
131
+ __name(combineUrlDomainWithPathQuery, "combineUrlDomainWithPathQuery");
132
+
14
133
  // packages/retry-plugin/src/fetch-retry.ts
15
- async function fetchWithRetry(params, userOriginalRetryTimes) {
16
- const { manifestUrl, options = {}, retryTimes = defaultRetries, retryDelay = defaultRetryDelay, fallback, getRetryPath } = params;
17
- const url = manifestUrl || params.url;
134
+ async function fetchRetry(params, lastRequestUrl, originalTotal) {
135
+ const {
136
+ url,
137
+ fetchOptions = {},
138
+ retryTimes = defaultRetries,
139
+ retryDelay = defaultRetryDelay,
140
+ // List of retry domains when resource loading fails. In the domains array, the first item is the default domain for static resources, and the subsequent items are backup domains. When a request to a domain fails, the system will find that domain in the array and replace it with the next domain in the array.
141
+ domains,
142
+ // Whether to add query parameters during resource retry to avoid being affected by browser and CDN cache. When set to true, retry=${times} will be added to the query, requesting in the order of retry=1, retry=2, retry=3.
143
+ addQuery,
144
+ onRetry,
145
+ onSuccess,
146
+ onError
147
+ } = params;
18
148
  if (!url) {
19
- throw new Error("[retry-plugin] manifestUrl or url is required");
149
+ throw new Error(`${PLUGIN_IDENTIFIER}: url is required in fetchWithRetry`);
150
+ }
151
+ const total = originalTotal ?? params.retryTimes ?? defaultRetries;
152
+ const isFirstAttempt = !lastRequestUrl;
153
+ let baseUrl = url;
154
+ if (!isFirstAttempt && lastRequestUrl) {
155
+ baseUrl = combineUrlDomainWithPathQuery(lastRequestUrl, url);
156
+ }
157
+ let requestUrl = baseUrl;
158
+ if (!isFirstAttempt) {
159
+ requestUrl = getRetryUrl(baseUrl, {
160
+ domains,
161
+ addQuery,
162
+ retryIndex: total - retryTimes,
163
+ queryKey: "retryCount"
164
+ });
20
165
  }
21
- const originalRetryTimes = userOriginalRetryTimes ?? params.retryTimes ?? defaultRetries;
22
- const isRetry = retryTimes !== originalRetryTimes;
23
- const retryUrl = isRetry && getRetryPath ? getRetryPath(url) : null;
24
- const requestUrl = retryUrl || url;
25
166
  try {
26
- const response = await fetch(requestUrl, options);
167
+ if (!isFirstAttempt && retryDelay > 0) {
168
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
169
+ }
170
+ const response = await fetch(requestUrl, fetchOptions);
27
171
  const responseClone = response.clone();
28
172
  if (!response.ok) {
29
- throw new Error(`Server error\uFF1A${response.status}`);
173
+ throw new Error(`${PLUGIN_IDENTIFIER}: Request failed: ${response.status} ${response.statusText || ""} | url: ${requestUrl}`);
30
174
  }
31
175
  await responseClone.json().catch((error) => {
32
- throw new Error(`Json parse error: ${error}, url is: ${requestUrl}`);
176
+ throw new Error(`${PLUGIN_IDENTIFIER}: JSON parse failed: ${error?.message || String(error)} | url: ${requestUrl}`);
33
177
  });
178
+ if (!isFirstAttempt) {
179
+ onSuccess && requestUrl && onSuccess({
180
+ domains,
181
+ url: requestUrl,
182
+ tagName: "fetch"
183
+ });
184
+ }
34
185
  return response;
35
186
  } catch (error) {
36
187
  if (retryTimes <= 0) {
37
- logger_default.log(`${PLUGIN_IDENTIFIER}: retry failed after ${defaultRetries} times for url: ${requestUrl}, now will try fallbackUrl url`);
38
- if (requestUrl && fallback && typeof fallback === "function") {
39
- return fetchWithRetry({
40
- manifestUrl: fallback(requestUrl),
41
- options,
42
- retryTimes: 0,
43
- retryDelay: 0
188
+ const attemptedRetries = total - retryTimes;
189
+ if (!isFirstAttempt && attemptedRetries > 0) {
190
+ onError && onError({
191
+ domains,
192
+ url: requestUrl,
193
+ tagName: "fetch"
44
194
  });
195
+ logger_default.log(`${PLUGIN_IDENTIFIER}: retry failed, no retries left for url: ${requestUrl}`);
45
196
  }
46
- if (error instanceof Error && error.message.includes("Json parse error")) {
47
- throw error;
48
- }
49
- throw new Error(`${PLUGIN_IDENTIFIER}: The request failed three times and has now been abandoned`);
197
+ throw new Error(`${PLUGIN_IDENTIFIER}: ${ERROR_ABANDONED}`);
50
198
  } else {
51
- retryDelay > 0 && await new Promise((resolve) => setTimeout(resolve, retryDelay));
52
- logger_default.log(`Trying again. Number of retries available\uFF1A${retryTimes - 1}`);
53
- return await fetchWithRetry({
199
+ const nextIndex = total - retryTimes + 1;
200
+ const predictedBaseUrl = combineUrlDomainWithPathQuery(requestUrl, url);
201
+ const predictedNextUrl = getRetryUrl(predictedBaseUrl, {
202
+ domains,
203
+ addQuery,
204
+ retryIndex: nextIndex,
205
+ queryKey: "retryCount"
206
+ });
207
+ onRetry && onRetry({
208
+ times: nextIndex,
209
+ domains,
210
+ url: predictedNextUrl,
211
+ tagName: "fetch"
212
+ });
213
+ logger_default.log(`${PLUGIN_IDENTIFIER}: Trying again. Number of retries left: ${retryTimes - 1}`);
214
+ return await fetchRetry({
54
215
  ...params,
55
216
  retryTimes: retryTimes - 1
56
- }, originalRetryTimes);
217
+ }, requestUrl, total);
57
218
  }
58
219
  }
59
220
  }
60
- __name(fetchWithRetry, "fetchWithRetry");
221
+ __name(fetchRetry, "fetchRetry");
61
222
 
62
223
  // packages/retry-plugin/src/script-retry.ts
63
- function scriptRetry({ scriptOption, moduleInfo, retryFn, beforeExecuteRetry = /* @__PURE__ */ __name(() => {
224
+ function scriptRetry({ retryOptions, retryFn, beforeExecuteRetry = /* @__PURE__ */ __name(() => {
64
225
  }, "beforeExecuteRetry") }) {
65
226
  return async function(params) {
66
227
  let retryWrapper;
67
- const { retryTimes = defaultRetries, retryDelay = defaultRetryDelay } = scriptOption || {};
68
- const shouldRetryThisModule = shouldRetryModule(scriptOption, moduleInfo);
69
- if (shouldRetryThisModule) {
70
- let attempts = 0;
71
- while (attempts < retryTimes) {
72
- try {
73
- beforeExecuteRetry();
228
+ let lastError;
229
+ let lastRequestUrl;
230
+ let originalUrl;
231
+ const { retryTimes = defaultRetries, retryDelay = defaultRetryDelay, domains, addQuery, onRetry, onSuccess, onError } = retryOptions || {};
232
+ let attempts = 0;
233
+ while (attempts < retryTimes) {
234
+ try {
235
+ beforeExecuteRetry();
236
+ if (retryDelay > 0) {
74
237
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
75
- retryWrapper = await retryFn({
76
- ...params,
77
- // add getRetryPath to load entry url passed by user
78
- getEntryUrl: scriptOption?.getRetryPath
79
- });
80
- break;
81
- } catch (error) {
82
- attempts++;
83
- if (attempts < retryTimes) {
84
- logger_default.log(`${PLUGIN_IDENTIFIER}: script resource retrying ${attempts} times`);
85
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
86
- } else {
87
- scriptOption?.cb && await new Promise((resolve) => scriptOption?.cb && scriptOption?.cb(resolve, error));
88
- throw error;
238
+ }
239
+ const retryIndex = attempts + 1;
240
+ retryWrapper = await retryFn({
241
+ ...params,
242
+ getEntryUrl: (url) => {
243
+ if (!originalUrl) {
244
+ originalUrl = url;
245
+ }
246
+ let baseUrl = originalUrl;
247
+ if (lastRequestUrl) {
248
+ baseUrl = combineUrlDomainWithPathQuery(lastRequestUrl, originalUrl);
249
+ }
250
+ const next = getRetryUrl(baseUrl, {
251
+ domains,
252
+ addQuery,
253
+ retryIndex,
254
+ queryKey: "retryCount"
255
+ });
256
+ onRetry && onRetry({
257
+ times: retryIndex,
258
+ domains,
259
+ url: next,
260
+ tagName: "script"
261
+ });
262
+ lastRequestUrl = next;
263
+ return next;
89
264
  }
265
+ });
266
+ onSuccess && lastRequestUrl && onSuccess({
267
+ domains,
268
+ url: lastRequestUrl,
269
+ tagName: "script"
270
+ });
271
+ break;
272
+ } catch (error) {
273
+ lastError = error;
274
+ attempts++;
275
+ if (attempts >= retryTimes) {
276
+ onError && lastRequestUrl && onError({
277
+ domains,
278
+ url: lastRequestUrl,
279
+ tagName: "script"
280
+ });
281
+ throw new Error(`${PLUGIN_IDENTIFIER}: ${ERROR_ABANDONED}`);
90
282
  }
91
283
  }
92
284
  }
@@ -94,66 +286,62 @@ function scriptRetry({ scriptOption, moduleInfo, retryFn, beforeExecuteRetry = /
94
286
  };
95
287
  }
96
288
  __name(scriptRetry, "scriptRetry");
97
- function shouldRetryModule(scriptOption, moduleInfo) {
98
- if (!scriptOption?.moduleName) {
99
- return true;
100
- }
101
- const moduleNames = scriptOption.moduleName;
102
- const currentModuleName = moduleInfo.name;
103
- const currentModuleAlias = moduleInfo?.alias;
104
- return moduleNames.some((targetName) => targetName === currentModuleName || targetName === currentModuleAlias);
105
- }
106
- __name(shouldRetryModule, "shouldRetryModule");
107
289
 
108
290
  // packages/retry-plugin/src/index.ts
109
- var loadEntryErrorCache = /* @__PURE__ */ new Set();
110
- var RetryPlugin = /* @__PURE__ */ __name(({ fetch: fetchOption, script: scriptOption }) => ({
111
- name: "retry-plugin",
112
- async fetch(manifestUrl, options) {
113
- const { retryTimes, fallback, getRetryPath } = fetchOption || {};
114
- if (fetchOption) {
115
- return fetchWithRetry({
116
- manifestUrl,
117
- options: {
291
+ var RetryPlugin = /* @__PURE__ */ __name((params) => {
292
+ if (params?.fetch || params?.script) {
293
+ logger_default.warn(`${PLUGIN_IDENTIFIER}: fetch or script config is deprecated, please use the new config style. See docs: https://module-federation.io/plugin/plugins/retry-plugin.html`);
294
+ }
295
+ const { fetchOptions = {}, retryTimes = defaultRetries, successTimes = 0, retryDelay = defaultRetryDelay, domains = [], manifestDomains = [], addQuery, onRetry, onSuccess, onError } = params || {};
296
+ return {
297
+ name: "retry-plugin",
298
+ async fetch(manifestUrl, options) {
299
+ return fetchRetry({
300
+ url: manifestUrl,
301
+ fetchOptions: {
118
302
  ...options,
119
- ...fetchOption?.options
303
+ ...fetchOptions
120
304
  },
305
+ domains: manifestDomains || domains,
306
+ addQuery,
307
+ onRetry,
308
+ onSuccess,
309
+ onError,
121
310
  retryTimes,
122
- fallback,
123
- getRetryPath
311
+ successTimes,
312
+ retryDelay
313
+ });
314
+ },
315
+ async loadEntryError({ getRemoteEntry, origin, remoteInfo, remoteEntryExports, globalLoading, uniqueKey }) {
316
+ const beforeExecuteRetry = /* @__PURE__ */ __name(() => {
317
+ delete globalLoading[uniqueKey];
318
+ }, "beforeExecuteRetry");
319
+ const getRemoteEntryRetry = scriptRetry({
320
+ retryOptions: {
321
+ retryTimes,
322
+ retryDelay,
323
+ domains,
324
+ addQuery,
325
+ onRetry,
326
+ onSuccess,
327
+ onError
328
+ },
329
+ retryFn: getRemoteEntry,
330
+ beforeExecuteRetry
124
331
  });
125
- }
126
- return fetch(manifestUrl, options);
127
- },
128
- async loadEntryError({ getRemoteEntry, origin, remoteInfo, remoteEntryExports, globalLoading, uniqueKey }) {
129
- if (!scriptOption || loadEntryErrorCache.has(uniqueKey)) {
130
- logger_default.log(`${PLUGIN_IDENTIFIER}: loadEntryError already processed for uniqueKey: ${uniqueKey}, skipping retry`);
131
- return;
132
- }
133
- loadEntryErrorCache.add(uniqueKey);
134
- const beforeExecuteRetry = /* @__PURE__ */ __name(() => {
135
- delete globalLoading[uniqueKey];
136
- }, "beforeExecuteRetry");
137
- const getRemoteEntryRetry = scriptRetry({
138
- scriptOption,
139
- moduleInfo: remoteInfo,
140
- retryFn: getRemoteEntry,
141
- beforeExecuteRetry
142
- });
143
- try {
144
332
  const result = await getRemoteEntryRetry({
145
333
  origin,
146
334
  remoteInfo,
147
335
  remoteEntryExports
148
336
  });
149
- loadEntryErrorCache.delete(uniqueKey);
150
337
  return result;
151
- } catch (error) {
152
- loadEntryErrorCache.delete(uniqueKey);
153
- throw error;
154
338
  }
155
- }
156
- }), "RetryPlugin");
339
+ };
340
+ }, "RetryPlugin");
157
341
  export {
158
- RetryPlugin
342
+ RetryPlugin,
343
+ appendRetryCountQuery,
344
+ combineUrlDomainWithPathQuery,
345
+ getRetryUrl,
346
+ rewriteWithNextDomain
159
347
  };
package/dist/index.d.mts CHANGED
@@ -1,28 +1,110 @@
1
1
  import { ModuleFederationRuntimePlugin } from '@module-federation/runtime/types';
2
2
 
3
- interface FetchWithRetryOptions {
4
- manifestUrl?: string;
5
- options?: RequestInit;
3
+ type CommonRetryOptions = {
4
+ /**
5
+ * retry request options
6
+ */
7
+ fetchOptions?: RequestInit;
8
+ /**
9
+ * retry times
10
+ */
6
11
  retryTimes?: number;
12
+ /**
13
+ * retry success times
14
+ */
15
+ successTimes?: number;
16
+ /**
17
+ * retry delay
18
+ */
7
19
  retryDelay?: number;
8
- fallback?:
9
- | (() => string)
10
- | ((url: string | URL | globalThis.Request) => string);
20
+ /**
21
+ * retry path
22
+ */
11
23
  getRetryPath?: (url: string) => string;
12
- }
13
- interface ScriptWithRetryOptions {
14
- retryTimes?: number;
15
- retryDelay?: number;
16
- moduleName?: Array<string>;
17
- cb?: (resolve: (value: unknown) => void, error: any) => void;
18
- getRetryPath?: (url: string) => string;
19
- }
24
+ /**
25
+ * add query parameter
26
+ */
27
+ addQuery?:
28
+ | boolean
29
+ | ((context: { times: number; originalQuery: string }) => string);
30
+ /**
31
+ * retry domains
32
+ */
33
+ domains?: string[];
34
+ /**
35
+ * retry manifest domains
36
+ */
37
+ manifestDomains?: string[];
38
+ /**
39
+ * retry callback
40
+ */
41
+ onRetry?: ({
42
+ times,
43
+ domains,
44
+ url,
45
+ }: {
46
+ times?: number;
47
+ domains?: string[];
48
+ url?: string;
49
+ tagName?: string;
50
+ }) => void;
51
+ /**
52
+ * retry success callback
53
+ */
54
+ onSuccess?: ({
55
+ domains,
56
+ url,
57
+ tagName,
58
+ }: {
59
+ domains?: string[];
60
+ url?: string;
61
+ tagName?: string;
62
+ }) => void;
63
+ /**
64
+ * retry failure callback
65
+ */
66
+ onError?: ({
67
+ domains,
68
+ url,
69
+ tagName,
70
+ }: {
71
+ domains?: string[];
72
+ url?: string;
73
+ tagName?: string;
74
+ }) => void;
75
+ };
76
+
77
+ type FetchRetryOptions = {
78
+ url?: string;
79
+ fetchOptions?: RequestInit;
80
+ } & CommonRetryOptions;
20
81
 
21
- type RetryPluginParams = {
22
- fetch?: FetchWithRetryOptions;
23
- script?: ScriptWithRetryOptions;
82
+ type ScriptRetryOptions = {
83
+ retryOptions: CommonRetryOptions;
84
+ retryFn: (...args: any[]) => Promise<any> | (() => Promise<any>);
85
+ beforeExecuteRetry?: (...args: any[]) => void;
24
86
  };
25
87
 
26
- declare const RetryPlugin: (params: RetryPluginParams) => ModuleFederationRuntimePlugin;
88
+ declare function rewriteWithNextDomain(currentUrl: string, domains?: string[]): string | null;
89
+ declare function appendRetryCountQuery(url: string, retryIndex: number, key?: string): string;
90
+ declare function getRetryUrl(baseUrl: string, opts?: {
91
+ domains?: string[];
92
+ addQuery?: boolean | ((context: {
93
+ times: number;
94
+ originalQuery: string;
95
+ }) => string);
96
+ retryIndex?: number;
97
+ queryKey?: string;
98
+ }): string;
99
+ /**
100
+ * Extract domain/host info from a URL and combine it with path/query from another URL
101
+ * This is useful for domain rotation while preserving original path and query parameters
102
+ * @param domainUrl - URL containing the target domain/host
103
+ * @param pathQueryUrl - URL containing the target path and query parameters
104
+ * @returns Combined URL with domain from domainUrl and path/query from pathQueryUrl
105
+ */
106
+ declare function combineUrlDomainWithPathQuery(domainUrl: string, pathQueryUrl: string): string;
107
+
108
+ declare const RetryPlugin: (params?: CommonRetryOptions) => ModuleFederationRuntimePlugin;
27
109
 
28
- export { type FetchWithRetryOptions, RetryPlugin, type RetryPluginParams, type ScriptWithRetryOptions };
110
+ export { type CommonRetryOptions, type FetchRetryOptions, RetryPlugin, type ScriptRetryOptions, appendRetryCountQuery, combineUrlDomainWithPathQuery, getRetryUrl, rewriteWithNextDomain };