@module-federation/retry-plugin 0.18.3 → 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,141 +5,343 @@ 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({ url, options = {}, retryTimes = defaultRetries, retryDelay = defaultRetryDelay, fallback }) {
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;
148
+ if (!url) {
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
+ });
165
+ }
16
166
  try {
17
- const response = await fetch(url, options);
167
+ if (!isFirstAttempt && retryDelay > 0) {
168
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
169
+ }
170
+ const response = await fetch(requestUrl, fetchOptions);
18
171
  const responseClone = response.clone();
19
172
  if (!response.ok) {
20
- throw new Error(`Server error\uFF1A${response.status}`);
173
+ throw new Error(`${PLUGIN_IDENTIFIER}: Request failed: ${response.status} ${response.statusText || ""} | url: ${requestUrl}`);
21
174
  }
22
175
  await responseClone.json().catch((error) => {
23
- throw new Error(`Json parse error: ${error}, url is: ${url}`);
176
+ throw new Error(`${PLUGIN_IDENTIFIER}: JSON parse failed: ${error?.message || String(error)} | url: ${requestUrl}`);
24
177
  });
178
+ if (!isFirstAttempt) {
179
+ onSuccess && requestUrl && onSuccess({
180
+ domains,
181
+ url: requestUrl,
182
+ tagName: "fetch"
183
+ });
184
+ }
25
185
  return response;
26
186
  } catch (error) {
27
187
  if (retryTimes <= 0) {
28
- logger_default.log(`${PLUGIN_IDENTIFIER}: retry failed after ${retryTimes} times for url: ${url}, now will try fallbackUrl url`);
29
- if (fallback && typeof fallback === "function") {
30
- return fetchWithRetry({
31
- url: fallback(url),
32
- options,
33
- retryTimes: 0,
34
- retryDelay: 0
188
+ const attemptedRetries = total - retryTimes;
189
+ if (!isFirstAttempt && attemptedRetries > 0) {
190
+ onError && onError({
191
+ domains,
192
+ url: requestUrl,
193
+ tagName: "fetch"
35
194
  });
195
+ logger_default.log(`${PLUGIN_IDENTIFIER}: retry failed, no retries left for url: ${requestUrl}`);
36
196
  }
37
- if (error instanceof Error && error.message.includes("Json parse error")) {
38
- throw error;
39
- }
40
- throw new Error(`${PLUGIN_IDENTIFIER}: The request failed three times and has now been abandoned`);
197
+ throw new Error(`${PLUGIN_IDENTIFIER}: ${ERROR_ABANDONED}`);
41
198
  } else {
42
- retryDelay > 0 && await new Promise((resolve) => setTimeout(resolve, retryDelay));
43
- logger_default.log(`Trying again. Number of retries available\uFF1A${retryTimes - 1}`);
44
- return await fetchWithRetry({
45
- url,
46
- options,
47
- retryTimes: retryTimes - 1,
48
- retryDelay,
49
- fallback
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"
50
212
  });
213
+ logger_default.log(`${PLUGIN_IDENTIFIER}: Trying again. Number of retries left: ${retryTimes - 1}`);
214
+ return await fetchRetry({
215
+ ...params,
216
+ retryTimes: retryTimes - 1
217
+ }, requestUrl, total);
51
218
  }
52
219
  }
53
220
  }
54
- __name(fetchWithRetry, "fetchWithRetry");
221
+ __name(fetchRetry, "fetchRetry");
55
222
 
56
- // packages/retry-plugin/src/util.ts
57
- function scriptCommonRetry({ scriptOption, moduleInfo, retryFn, beforeExecuteRetry = /* @__PURE__ */ __name(() => {
223
+ // packages/retry-plugin/src/script-retry.ts
224
+ function scriptRetry({ retryOptions, retryFn, beforeExecuteRetry = /* @__PURE__ */ __name(() => {
58
225
  }, "beforeExecuteRetry") }) {
59
- return async function(...args) {
60
- let retryResponse;
61
- const { retryTimes = defaultRetries, retryDelay = defaultRetryDelay } = scriptOption || {};
62
- if (scriptOption?.moduleName && scriptOption?.moduleName.some((m) => moduleInfo.name === m || moduleInfo?.alias === m) || scriptOption?.moduleName === void 0) {
63
- let attempts = 0;
64
- while (attempts - 1 < retryTimes) {
65
- try {
66
- beforeExecuteRetry();
67
- retryResponse = await retryFn(...args);
68
- break;
69
- } catch (error) {
70
- attempts++;
71
- if (attempts - 1 >= retryTimes) {
72
- scriptOption?.cb && await new Promise((resolve) => scriptOption?.cb && scriptOption?.cb(resolve, error));
73
- throw error;
74
- }
75
- logger_default.log(`${PLUGIN_IDENTIFIER}: script resource retrying ${attempts} times`);
226
+ return async function(params) {
227
+ let retryWrapper;
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) {
76
237
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
77
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;
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}`);
282
+ }
78
283
  }
79
284
  }
80
- return retryResponse;
285
+ return retryWrapper;
81
286
  };
82
287
  }
83
- __name(scriptCommonRetry, "scriptCommonRetry");
288
+ __name(scriptRetry, "scriptRetry");
84
289
 
85
290
  // packages/retry-plugin/src/index.ts
86
- var RetryPlugin = /* @__PURE__ */ __name(({ fetch: fetchOption, script: scriptOption }) => ({
87
- name: "retry-plugin",
88
- async fetch(url, options) {
89
- const _options = {
90
- ...options,
91
- ...fetchOption?.options
92
- };
93
- if (fetchOption) {
94
- if (fetchOption.url) {
95
- if (url === fetchOption?.url) {
96
- return fetchWithRetry({
97
- url: fetchOption.url,
98
- options: _options,
99
- retryTimes: fetchOption?.retryTimes,
100
- fallback: fetchOption?.fallback
101
- });
102
- }
103
- } else {
104
- return fetchWithRetry({
105
- url,
106
- options: _options,
107
- retryTimes: fetchOption?.retryTimes,
108
- fallback: fetchOption?.fallback
109
- });
110
- }
111
- }
112
- return fetch(url, options);
113
- },
114
- async loadEntryError({ getRemoteEntry, origin, remoteInfo, remoteEntryExports, globalLoading, uniqueKey }) {
115
- if (!scriptOption)
116
- return;
117
- const retryFn = getRemoteEntry;
118
- const beforeExecuteRetry = /* @__PURE__ */ __name(() => delete globalLoading[uniqueKey], "beforeExecuteRetry");
119
- const getRemoteEntryRetry = scriptCommonRetry({
120
- scriptOption,
121
- moduleInfo: remoteInfo,
122
- retryFn,
123
- beforeExecuteRetry
124
- });
125
- return getRemoteEntryRetry({
126
- origin,
127
- remoteInfo,
128
- remoteEntryExports
129
- });
130
- },
131
- async getModuleFactory({ remoteEntryExports, expose, moduleInfo }) {
132
- if (!scriptOption)
133
- return;
134
- const retryFn = remoteEntryExports.get;
135
- const getRemoteEntryRetry = scriptCommonRetry({
136
- scriptOption,
137
- moduleInfo,
138
- retryFn
139
- });
140
- return getRemoteEntryRetry(expose);
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`);
141
294
  }
142
- }), "RetryPlugin");
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: {
302
+ ...options,
303
+ ...fetchOptions
304
+ },
305
+ domains: manifestDomains || domains,
306
+ addQuery,
307
+ onRetry,
308
+ onSuccess,
309
+ onError,
310
+ retryTimes,
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
331
+ });
332
+ const result = await getRemoteEntryRetry({
333
+ origin,
334
+ remoteInfo,
335
+ remoteEntryExports
336
+ });
337
+ return result;
338
+ }
339
+ };
340
+ }, "RetryPlugin");
143
341
  export {
144
- RetryPlugin
342
+ RetryPlugin,
343
+ appendRetryCountQuery,
344
+ combineUrlDomainWithPathQuery,
345
+ getRetryUrl,
346
+ rewriteWithNextDomain
145
347
  };
package/dist/index.d.mts CHANGED
@@ -1,27 +1,110 @@
1
1
  import { ModuleFederationRuntimePlugin } from '@module-federation/runtime/types';
2
2
 
3
- interface FetchWithRetryOptions {
4
- url?: 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);
11
- }
20
+ /**
21
+ * retry path
22
+ */
23
+ getRetryPath?: (url: string) => string;
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
+ };
12
76
 
13
- interface ScriptWithRetryOptions {
14
- retryTimes?: number;
15
- retryDelay?: number;
16
- moduleName?: Array<string>;
17
- cb?: (resolve: (value: unknown) => void, error: any) => void;
18
- }
77
+ type FetchRetryOptions = {
78
+ url?: string;
79
+ fetchOptions?: RequestInit;
80
+ } & CommonRetryOptions;
19
81
 
20
- type RetryPluginParams = {
21
- fetch?: FetchWithRetryOptions;
22
- script?: ScriptWithRetryOptions;
82
+ type ScriptRetryOptions = {
83
+ retryOptions: CommonRetryOptions;
84
+ retryFn: (...args: any[]) => Promise<any> | (() => Promise<any>);
85
+ beforeExecuteRetry?: (...args: any[]) => void;
23
86
  };
24
87
 
25
- 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;
26
109
 
27
- export { type FetchWithRetryOptions, RetryPlugin, type RetryPluginParams, type ScriptWithRetryOptions };
110
+ export { type CommonRetryOptions, type FetchRetryOptions, RetryPlugin, type ScriptRetryOptions, appendRetryCountQuery, combineUrlDomainWithPathQuery, getRetryUrl, rewriteWithNextDomain };