Skip to content

feat: support multiple retry rules #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ type AssetsRetryOptions = {
onRetry?: (context: AssetsRetryHookContext) => void;
onSuccess?: (context: AssetsRetryHookContext) => void;
onFail?: (context: AssetsRetryHookContext) => void;
} | {
rules: RuntimeRetryOptions[];
inlineScript?: boolean;
minify?: boolean;
};
```

Expand Down Expand Up @@ -325,6 +329,76 @@ pluginAssetsRetry({
});
```

### rules

- **Type:** `RuntimeRetryOptions[]`
- **Default:** `undefined`

Configure multiple retry rules with different options. Each rule will be evaluated in order, and the first matching rule will be used for retry logic. This is useful when you have different retry requirements for different types of assets or domains.

When using `rules`, the plugin will:
1. Check each rule in order
2. Use the first rule whose `test` condition matches the asset URL
3. If no rule matches, the asset will not be retried

Each rule supports all the same options as the top-level configuration, including `type`, `domain`, `max`, `test`, `crossOrigin`, `delay`, `onRetry`, `onSuccess`, and `onFail`.

Example - Different retry strategies for different CDNs:

```js
pluginAssetsRetry({
rules: [
{
// Rule for primary CDN
test: /cdn1\.example\.com/,
domain: ['cdn1.example.com', 'cdn1-backup.example.com'],
max: 3,
delay: 1000,
},
{
// Rule for secondary CDN with more retries
test: /cdn2\.example\.com/,
domain: ['cdn2.example.com', 'cdn2-backup.example.com'],
max: 5,
delay: 500,
},
{
// Default rule for other assets
domain: ['default.example.com', 'default-backup.example.com'],
max: 2,
},
],
});
```

Example - Different retry strategies for different asset types:

```js
pluginAssetsRetry({
rules: [
{
// Critical JavaScript files get more retries
test: /\.js$/,
max: 5,
delay: 1000,
onFail: ({ url }) => console.error(`Critical JS failed: ${url}`),
},
{
// CSS files get fewer retries
test: /\.css$/,
max: 2,
delay: 500,
},
{
// Images get minimal retries
test: /\.(png|jpg|gif|svg)$/,
max: 1,
delay: 0,
},
],
});
```

## Notes

When you use Assets Retry plugin, the Rsbuild injects some runtime code into the HTML and [Rspack Runtime](https://rspack.dev/misc/glossary#runtime), then serializes the Assets Retry plugin config, inserting it into the runtime code. Therefore, you need to be aware of the following:
Expand Down
74 changes: 74 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ type AssetsRetryOptions = {
onRetry?: (context: AssetsRetryHookContext) => void;
onSuccess?: (context: AssetsRetryHookContext) => void;
onFail?: (context: AssetsRetryHookContext) => void;
} | {
rules: RuntimeRetryOptions[];
inlineScript?: boolean;
minify?: boolean;
};
```

Expand Down Expand Up @@ -323,6 +327,76 @@ pluginAssetsRetry({
});
```

### rules

- **类型:** `RuntimeRetryOptions[]`
- **默认值:** `undefined`

配置多个重试规则,每个规则可以有不同的选项。规则会按顺序进行评估,第一个匹配的规则将用于重试逻辑。这在你对不同类型的资源或域名有不同的重试需求时非常有用。

使用 `rules` 时,插件会:
1. 按顺序检查每个规则
2. 使用第一个 `test` 条件匹配资源 URL 的规则
3. 如果没有规则匹配,则不会重试该资源

每个规则支持与顶层配置相同的所有选项,包括 `type`、`domain`、`max`、`test`、`crossOrigin`、`delay`、`onRetry`、`onSuccess` 和 `onFail`。

示例 - 不同 CDN 的不同重试策略:

```js
pluginAssetsRetry({
rules: [
{
// 主 CDN 的规则
test: /cdn1\.example\.com/,
domain: ['cdn1.example.com', 'cdn1-backup.example.com'],
max: 3,
delay: 1000,
},
{
// 次要 CDN 的规则,更多重试次数
test: /cdn2\.example\.com/,
domain: ['cdn2.example.com', 'cdn2-backup.example.com'],
max: 5,
delay: 500,
},
{
// 其他资源的默认规则
domain: ['default.example.com', 'default-backup.example.com'],
max: 2,
},
],
});
```

示例 - 不同资源类型的不同重试策略:

```js
pluginAssetsRetry({
rules: [
{
// 关键 JavaScript 文件获得更多重试次数
test: /\.js$/,
max: 5,
delay: 1000,
onFail: ({ url }) => console.error(`关键 JS 失败: ${url}`),
},
{
// CSS 文件获得较少的重试次数
test: /\.css$/,
max: 2,
delay: 500,
},
{
// 图片获得最少的重试次数
test: /\.(png|jpg|gif|svg)$/,
max: 1,
delay: 0,
},
],
});
```

## 注意事项

当你使用 Assets Retry 插件时,Rsbuild 会分别向 HTML 和 [Rspack Runtime](https://rspack.dev/zh/misc/glossary#runtime) 中注入运行时代码,并将 Assets Retry 插件配置的内容序列化后插入到这些代码中,因此你需要注意:
Expand Down
6 changes: 4 additions & 2 deletions src/AsyncChunkRetryPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ class AsyncChunkRetryPlugin implements Rspack.RspackPluginInstance {
readonly name = 'ASYNC_CHUNK_RETRY_PLUGIN';
readonly isRspack: boolean;
readonly minify: boolean;
readonly runtimeOptions: NormalizedRuntimeRetryOptions;
readonly runtimeOptions:
| NormalizedRuntimeRetryOptions
| NormalizedRuntimeRetryOptions[];

constructor(
options: NormalizedRuntimeRetryOptions,
options: NormalizedRuntimeRetryOptions | NormalizedRuntimeRetryOptions[],
isRspack: boolean,
minify: boolean,
) {
Expand Down
44 changes: 40 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { ensureAssetPrefix } from '@rsbuild/core';
import serialize from 'serialize-javascript';
import { AsyncChunkRetryPlugin } from './AsyncChunkRetryPlugin.js';
import type {
CompileTimeRetryOptions,
NormalizedRuntimeRetryOptions,
PluginAssetsRetryOptions,
RuntimeRetryOptions,
} from './types.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
Expand All @@ -22,8 +24,21 @@ export const PLUGIN_ASSETS_RETRY_NAME = 'rsbuild:assets-retry';

function getRuntimeOptions(
userOptions: PluginAssetsRetryOptions,
): NormalizedRuntimeRetryOptions | NormalizedRuntimeRetryOptions[] {
// Check if using rules mode
if ('rules' in userOptions && Array.isArray(userOptions.rules)) {
return userOptions.rules.map((rule) => normalizeRuntimeOptions(rule));
}

// Single options mode
const { inlineScript, minify, ...restOptions } =
userOptions as RuntimeRetryOptions & CompileTimeRetryOptions;
return normalizeRuntimeOptions(restOptions);
}

function normalizeRuntimeOptions(
options: RuntimeRetryOptions,
): NormalizedRuntimeRetryOptions {
const { inlineScript, minify, ...restOptions } = userOptions;
const defaultOptions: NormalizedRuntimeRetryOptions = {
max: 3,
type: ['link', 'script', 'img'],
Expand All @@ -35,7 +50,7 @@ function getRuntimeOptions(

const result: NormalizedRuntimeRetryOptions = {
...defaultOptions,
...restOptions,
...options,
};

// Normalize config
Expand All @@ -53,12 +68,18 @@ function getRuntimeOptions(
}

async function getRetryCode(
runtimeOptions: NormalizedRuntimeRetryOptions,
runtimeOptions:
| NormalizedRuntimeRetryOptions
| NormalizedRuntimeRetryOptions[],
minify: boolean,
): Promise<string> {
const filename = 'initialChunkRetry';
// In production, files are in dist/runtime, in development they are in src/runtime
const baseDir = __dirname.includes('/dist/')
? __dirname
: path.join(__dirname, '..', 'dist');
const runtimeFilePath = path.join(
__dirname,
baseDir,
'runtime',
minify ? `${filename}.min.js` : `${filename}.js`,
);
Expand Down Expand Up @@ -87,6 +108,21 @@ export const pluginAssetsRetry = (
} => {
const options = { ...userOptions };

// Handle rules mode
if ('rules' in options) {
if (options.minify === undefined) {
const minify =
typeof config.output.minify === 'boolean'
? config.output.minify
: config.output.minify?.js;
options.minify = minify && config.mode === 'production';
}
return options as PluginAssetsRetryOptions & {
minify: boolean;
crossorigin: boolean | 'anonymous' | 'use-credentials';
};
}

// options.crossOrigin should be same as html.crossorigin by default
if (options.crossOrigin === undefined) {
options.crossOrigin = config.html.crossorigin;
Expand Down
Loading