Compare commits

...

4 Commits

Author SHA1 Message Date
DESKTOP-I3JPKHK\wy 647baea3e7 文档补充提交 2025-12-12 17:15:34 +08:00
DESKTOP-I3JPKHK\wy 0e1d955ab8 补充提交 2025-12-12 17:11:42 +08:00
DESKTOP-I3JPKHK\wy 757e1b4abd 补充提交 2025-12-12 16:13:41 +08:00
DESKTOP-I3JPKHK\wy bc130ac5e7 补充 readme 2025-12-12 16:12:09 +08:00
5 changed files with 2333 additions and 31 deletions

426
README.md
View File

@ -1,39 +1,407 @@
<!-- # yx_async_throttle_flutter
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for 一个健壮的 Flutter 异步任务防抖Debounce与节流Throttle工具类。
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
For general information about developing packages, see the Dart guide for 它完美结合了 **时间策略**Throttle/Debounce**异步任务锁**Async Task Locking旨在解决复杂场景下如弱网环境或用户快速连点的重复执行问题。
[creating packages](https://dart.dev/guides/libraries/create-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/to/develop-packages).
-->
TODO: Put a short description of the package here that helps potential users ## 功能特性
know whether this package might be useful for them.
## Features - **双重保护机制**:
- **时间策略**: 防止短时间内快速重复点击(基于 easy_debounce
- **异步锁**: 即使时间间隔已过,如果上一个异步任务(如网络请求)尚未完成,也会阻止新任务执行。
- **节流模式 (Throttle - 默认)**: 立即执行,并在指定时间内忽略后续调用。非常适合“提交”、“登录”等按钮。
- **防抖模式 (Debounce)**: 延迟执行,直到停止调用一段时间后才执行。非常适合“搜索框”、“实时输入校验”。
- **内存安全**: 在防抖模式下正确管理 `Completer`,防止因任务被取消导致的 `await` 永久挂起和内存泄漏。
- **简单易用**: 单例调用API 简洁。
TODO: List what your package can do. Maybe include images, gifs, or videos. ## 快速开始 (Getting Started)
## Getting started 在你的 `pubspec.yaml` 文件中添加依赖:
TODO: List prerequisites and provide or point to information on how to ```yaml
start using the package. dependencies:
yx_async_throttle_flutter:
## Usage git:
url: https://gitea.23544.com/wangyang/yx_async_throttle_flutter.git
TODO: Include short and useful examples for package users. Add longer examples ref: 1.0.0
to `/example` folder.
```dart
const like = 'sample';
``` ```
## Additional information ## 使用指南 (Usage)
TODO: Tell users more about the package: where to find more information, how to ### 1. 基础节流 (按钮防暴力点击)
contribute to the package, how to file issues, what response they can expect
from the package authors, and more. 适用于“提交订单”、“登录”、“保存”等场景。
即使网络很慢(请求耗时 > 500ms或者用户手速很快该函数也能确保在上一个请求完成**且**时间间隔满足之前,不会重复执行。
```dart
import 'package:yx_async_throttle_flutter/async_throttle.dart';
void onSubmit() async {
// 使用唯一的 tagId 标识这个动作
await AsyncThrottle.instance.execute(
'submit_order_btn',
() async {
// 你的异步逻辑(例如 API 调用)
print('正在提交订单...');
await Future.delayed(Duration(seconds: 2)); // 模拟耗时网络请求
print('订单提交完成!');
},
duration: Duration(milliseconds: 500), // 默认 300ms
enableDebounce: false, // 默认 false (节流模式)
);
}
```
### 1.1 UI 回调最佳实践(不影响 UI / 不抛到全局)
`onTap` / `onPressed` / `PopupMenuItem.onTap` 这类回调通常**不能 await**(即使写成 async 也经常没人等待它)。
为了避免 `onExecute` 内部异常变成 **unhandled async error** 影响 UI建议用 `executeSafe`
```dart
PopupMenuItem(
onTap: () {
AsyncThrottle.instance.executeSafe(
'withdraw_task',
controller.withdrawTask,
onError: (e, s) {
// 这里做日志/Toast/上报,不影响 UI
},
);
},
child: ...,
)
```
### 2. 防抖模式 (搜索或输入)
适用于搜索框、实时输入联想等场景。只有当用户停止输入指定时间后,才会触发逻辑。
```dart
import 'package:yx_async_throttle_flutter/async_throttle.dart';
void onSearchChanged(String query) async {
await AsyncThrottle.instance.execute(
'search_input',
() async {
// 只有当用户停止输入 500ms 后才会执行
print('搜索内容: $query');
await fetchSearchResults(query);
},
duration: Duration(milliseconds: 500),
enableDebounce: true, // 启用防抖模式
);
}
```
### 3. 检查执行状态
你可以随时检查某个任务是否正在执行中:
```dart
// 检查指定 tagId 是否有任务正在执行
bool isRunning = AsyncThrottle.instance.isExecuting('submit_order_btn');
if (isRunning) {
print('订单正在提交中,请稍候...');
}
```
### 4. 清除锁 (可选)
在极少数情况下(例如用户退出登录时),你可能需要强制清除所有内部锁。
```dart
AsyncThrottle.instance.clearAllLocks();
```
> ⚠️ **注意**: `clearAllLocks()` 仅清除异步锁状态,不会影响 EasyThrottle/EasyDebounce 的内部时间窗口。
---
## API 参考
### `execute` 方法
```dart
Future<void> execute(
String tagId,
Future<void> Function() onExecute, {
Duration duration = const Duration(milliseconds: 300),
bool enableDebounce = false,
})
```
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `tagId` | `String` | 必填 | 任务的唯一标识符 |
| `onExecute` | `Future<void> Function()` | 必填 | 要执行的异步函数 |
| `duration` | `Duration` | `300ms` | 节流/防抖的时间间隔 |
| `enableDebounce` | `bool` | `false` | `false`=节流模式,`true`=防抖模式 |
### `executeSafe` 方法
```dart
Future<void> executeSafe(
String tagId,
Future<void> Function() onExecute, {
Duration duration = const Duration(milliseconds: 300),
bool enableDebounce = false,
void Function(Object error, StackTrace stackTrace)? onError,
})
```
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `tagId` | `String` | 必填 | 任务的唯一标识符 |
| `onExecute` | `Future<void> Function()` | 必填 | 要执行的异步函数 |
| `duration` | `Duration` | `300ms` | 节流/防抖的时间间隔 |
| `enableDebounce` | `bool` | `false` | `false`=节流模式,`true`=防抖模式 |
| `onError` | `Function?` | `null` | 错误回调,用于日志/上报 |
### `isExecuting` 方法
```dart
bool isExecuting(String tagId)
```
检查指定 `tagId` 的任务是否正在执行中。
### `clearAllLocks` 方法
```dart
void clearAllLocks()
```
强制清除所有异步锁和等待中的 Completer。
---
## 核心优势:为什么需要异步锁?
普通的 `Throttle` 库通常只根据 **时间** 来控制执行。
**典型 Bug 场景:**
设定节流时间为 `500ms`
1. 用户点击“提交”。请求发出,但网络很慢,耗时 `2000ms`
2. 在 `600ms` 时(此时节流时间已过),用户因焦急再次点击“提交”。
3. **问题**: 普通 Throttle 库会认为 500ms 已到,允许执行第二次请求,导致 **重复下单**
**本库的解决方案:**
`yx_async_throttle_flutter` 会检查 `isExecuting` 状态。虽然时间已过但因为第一个请求2000ms还在运行第二次点击会被直接拦截从而完美防止重复提交。
> **💡 最佳实践**: 如果在 `onExecute` 函数内部再添加 **Loading 蒙层**UI 阻断),配合本插件即可实现 **“三层防护”**UI 蒙层 + 时间策略 + 异步锁),彻底杜绝任何异常场景下的重复操作。
---
## 更多使用示例
### 表单多按钮场景
当页面有多个按钮时,使用不同的 `tagId` 确保它们互不影响:
```dart
class OrderPage extends StatelessWidget {
void onSave() {
AsyncThrottle.instance.executeSafe('order_save', () async {
await saveOrder();
});
}
void onSubmit() {
AsyncThrottle.instance.executeSafe('order_submit', () async {
await submitOrder();
});
}
void onCancel() {
AsyncThrottle.instance.executeSafe('order_cancel', () async {
await cancelOrder();
});
}
}
```
### 配合 GetX/Provider 状态管理
```dart
class OrderController extends GetxController {
final isLoading = false.obs;
Future<void> submitOrder() async {
await AsyncThrottle.instance.execute(
'submit_order',
() async {
isLoading.value = true;
try {
await api.submitOrder();
Get.snackbar('成功', '订单提交成功');
} finally {
isLoading.value = false;
}
},
);
}
}
```
### 列表项点击防重
```dart
ListView.builder(
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
onTap: () {
// 使用 item.id 作为唯一标识,确保不同列表项互不影响
AsyncThrottle.instance.executeSafe(
'list_item_${item.id}',
() async {
await navigateToDetail(item);
},
);
},
);
},
)
```
### 实时输入验证(防抖)
```dart
TextField(
onChanged: (value) {
AsyncThrottle.instance.execute(
'username_validation',
() async {
final isAvailable = await api.checkUsername(value);
setState(() {
usernameError = isAvailable ? null : '用户名已被占用';
});
},
enableDebounce: true,
duration: const Duration(milliseconds: 500),
);
},
)
```
---
## 注意事项与最佳实践
### ✅ 推荐做法
1. **使用有意义的 tagId**
```dart
// ✅ 好
'submit_order_page_main'
'search_product_list'
// ❌ 不好
'btn1'
'click'
```
2. **在 onTap/onPressed 中使用 executeSafe**
```dart
// ✅ 推荐:不会影响 UI
onTap: () {
AsyncThrottle.instance.executeSafe('action', doSomething);
}
// ⚠️ 需要注意:异常可能变成 unhandled async error
onTap: () async {
await AsyncThrottle.instance.execute('action', doSomething);
}
```
3. **合理设置 duration**
- 按钮点击:`300ms ~ 500ms`
- 搜索防抖:`300ms ~ 800ms`
- 实时验证:`500ms ~ 1000ms`
### ⚠️ 注意事项
1. **tagId 全局唯一**:不同功能使用不同的 tagId避免意外阻塞
2. **单例模式**`AsyncThrottle.instance` 是全局单例,状态跨页面共享
3. **页面销毁时的处理**:如果需要在页面销毁时取消未完成的任务,可以在 `dispose` 中调用:
```dart
@override
void dispose() {
// 可选:清除该页面相关的锁
// 注意:这会清除所有锁,请谨慎使用
// AsyncThrottle.instance.clearAllLocks();
super.dispose();
}
```
4. **异常处理**`execute` 会传播异常,`executeSafe` 会吞掉异常并调用 `onError`
---
## 与其他方案的对比
| 特性 | yx_async_throttle | 普通 Throttle | 手动锁变量 |
|------|-------------------|---------------|-----------|
| 时间策略 | ✅ | ✅ | ❌ |
| 异步锁 | ✅ | ❌ | ✅ |
| 弱网保护 | ✅ | ❌ | ✅ |
| 内存安全 | ✅ | ✅ | ⚠️ 需自行处理 |
| 防抖模式 | ✅ | ✅ | ❌ |
| 代码简洁 | ✅ | ✅ | ❌ |
---
## 测试覆盖
本库已通过 **75 项** 全面的单元测试,覆盖:
- ✅ 单例模式
- ✅ 节流/防抖核心功能
- ✅ 异常处理与传播
- ✅ 并发场景100+ 次快速调用)
- ✅ 内存安全(无泄漏)
- ✅ 重入防护(无死锁)
- ✅ 边界条件(极端参数)
- ✅ 真实业务场景模拟
详细测试报告请参阅 [TEST_REPORT.md](./TEST_REPORT.md)。
---
## 常见问题 (FAQ)
### Q: 为什么我的第二次点击没有执行?
A: 这正是本库的设计目的。检查以下两点:
1. 是否在 `duration` 时间窗口内?
2. 上一次任务是否还在执行中?(使用 `isExecuting` 检查)
### Q: 如何让同一按钮在不同页面独立工作?
A: 使用包含页面标识的 tagId
```dart
'submit_order_${widget.pageId}'
```
### Q: executeSafe 和 execute 有什么区别?
A:
- `execute`: 异常会向外抛出,适合可以 await 的场景
- `executeSafe`: 异常被内部捕获,通过 onError 回调处理,适合 onTap 等无法 await 的场景
### Q: clearAllLocks 后为什么还是不能立即执行?
A: `clearAllLocks` 只清除异步锁EasyThrottle 的时间窗口仍然有效。需要等待时间窗口过期,或使用不同的 tagId。
---
## 其他信息
本库底层的时间策略依赖于 [easy_debounce](https://pub.dev/packages/easy_debounce)。
## License
MIT License

211
TEST_REPORT.md Normal file
View File

@ -0,0 +1,211 @@
# YX Async Throttle Flutter 测试报告
## 📋 测试概要
| 项目 | 详情 |
|------|------|
| **测试日期** | 2025-12-12 |
| **测试框架** | flutter_test |
| **总测试用例** | 75 |
| **通过** | 75 ✅ |
| **失败** | 0 |
| **通过率** | 100% |
---
## 🎯 测试覆盖范围
### 1. 单例模式测试 (Singleton Pattern Tests)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 多次获取实例应返回同一对象 | ✅ | 验证单例模式正确实现 |
| 实例不为null | ✅ | 确保实例可用性 |
| 实例类型正确 | ✅ | 类型安全检查 |
### 2. 节流模式核心功能测试 (Throttle Mode - Core Functionality)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 首次调用应立即执行 | ✅ | 验证节流模式立即执行特性 |
| duration内的后续调用应被忽略 | ✅ | 验证节流时间窗口内的调用拦截 |
| duration过后应可以再次执行 | ✅ | 验证节流时间窗口重置 |
| 异步任务执行中新调用应被忽略(锁机制) | ✅ | **核心功能**: 弱网场景下的重复请求拦截 |
| 异常后锁应正确释放 | ✅ | 异常安全性验证 |
| 异常应正确传播 | ✅ | 错误处理正确性 |
| 不同tagId应互不影响 | ✅ | 隔离性验证 |
| 节流期间多次调用全部被忽略 | ✅ | 多次快速点击场景 |
### 3. 防抖模式核心功能测试 (Debounce Mode - Core Functionality)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 单次调用应延迟执行 | ✅ | 验证防抖延迟执行特性 |
| 快速连续调用只应执行最后一次 | ✅ | **核心功能**: 搜索场景优化 |
| 被取消调用的Future应正确完成不挂起 | ✅ | **关键**: 内存泄漏防护 |
| 任务执行中新调用应被阻止 | ✅ | 锁机制验证 |
| 防抖异常应正确传播到最后一个调用者 | ✅ | 错误传播正确性 |
| 防抖异常后锁应正确释放 | ✅ | 异常安全性 |
### 4. executeSafe 安全模式测试 (UI Safe Mode)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 节流模式异常不应向外抛 | ✅ | UI安全保护 |
| 防抖模式异常不应向外抛 | ✅ | UI安全保护 |
| onError回调正确接收错误和堆栈 | ✅ | 错误收集功能 |
| 无异常时onError不应被调用 | ✅ | 正常流程验证 |
| onError为null时异常被静默吞掉 | ✅ | 容错处理 |
### 5. isExecuting 状态检查测试
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 执行前应返回false | ✅ | 初始状态 |
| 节流执行中应返回true | ✅ | 执行中状态 |
| 防抖执行中应返回true | ✅ | 执行中状态 |
| 异常后应返回false | ✅ | 异常后状态恢复 |
| 不存在的tagId应返回false | ✅ | 边界条件 |
### 6. clearAllLocks 测试
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 应清除所有锁 | ✅ | 批量清除功能 |
| 应释放等待中的防抖Completer | ✅ | **关键**: 防止Future挂起 |
| 清除后异步锁应解除 | ✅ | 状态重置 |
| 多次调用clearAllLocks应安全 | ✅ | 幂等性验证 |
### 7. 并发场景测试 (Concurrency Tests)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 多个不同tagId并发执行 | ✅ | 并行任务隔离 |
| 大量快速调用压力测试 - 节流 | ✅ | 100次快速调用 |
| 大量快速调用压力测试 - 防抖 | ✅ | 100次快速调用 |
| 混合模式并发 | ✅ | 节流+防抖混合场景 |
### 8. 边界条件测试 (Edge Cases)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| duration为零应正常工作 | ✅ | 边界值 |
| 极短duration应正常工作 | ✅ | 微秒级duration |
| 空字符串tagId应正常工作 | ✅ | 空字符串处理 |
| 特殊字符tagId应正常工作 | ✅ | 含中文、emoji、特殊符号 |
| 长字符串tagId应正常工作 | ✅ | 10000字符长度 |
| 同步完成的异步任务应正常工作 | ✅ | 同步async函数 |
| 嵌套异步任务应正常工作 | ✅ | 多层await |
### 9. 重入测试 (Reentrancy Tests)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 在回调中调用同一tagId应被阻止无死锁 | ✅ | **关键**: 死锁防护 |
| 在回调中调用不同tagId应正常执行 | ✅ | 嵌套调用支持 |
| 防抖模式重入应安全 | ✅ | 防抖重入保护 |
### 10. 内存安全测试 (Memory Safety Tests)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 大量防抖调用后Completer应正确清理 | ✅ | 200*5次调用测试 |
| 异常场景下资源应正确释放 | ✅ | 50次异常场景 |
| clearAllLocks后状态应完全重置 | ✅ | 完整状态重置 |
### 11. 时间精度测试 (Timing Precision Tests)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 节流duration应基本准确 | ✅ | 时间控制精度 |
| 防抖duration应基本准确 | ✅ | 延迟执行精度 |
### 12. 异常类型测试 (Exception Type Tests)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| Error类型应正确传播 | ✅ | AssertionError |
| String异常应正确传播 | ✅ | throw String |
| 自定义异常应正确传播 | ✅ | CustomTestException |
| 防抖模式异常类型应正确传播 | ✅ | RangeError |
### 13. 默认参数测试 (Default Parameters Tests)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 默认duration应为300ms | ✅ | API文档一致性 |
| 默认enableDebounce应为false | ✅ | 节流为默认模式 |
### 14. 顺序保证测试 (Order Guarantee Tests)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 防抖模式应保证只执行最后一次 | ✅ | 10次调用只执行第10次 |
### 15. 复杂场景集成测试 (Integration Tests)
| 用例名称 | 状态 | 描述 |
|---------|------|------|
| 模拟真实按钮快速点击场景 | ✅ | **实战场景** |
| 模拟搜索输入防抖场景 | ✅ | **实战场景** |
| 模拟弱网环境重复点击场景 | ✅ | **实战场景** |
| 混合使用节流和防抖应互不干扰 | ✅ | 模式切换 |
---
## 🔍 测试文件统计
| 测试文件 | 测试组数 | 测试用例数 |
|---------|---------|-----------|
| `async_throttle_comprehensive_test.dart` | 15 | 62 |
| `async_throttle_safety_test.dart` | 1 | 12 |
| `yx_async_throttle_flutter_test.dart` | 1 | 1 |
| **总计** | **17** | **75** |
---
## ✅ 商用级质量评估
### 功能完整性
- ✅ 节流模式 (Throttle) 完整实现
- ✅ 防抖模式 (Debounce) 完整实现
- ✅ 异步锁机制正确工作
- ✅ UI安全模式 (executeSafe) 可用
### 稳定性
- ✅ 无死锁风险
- ✅ 无内存泄漏Completer正确清理
- ✅ 异常场景下资源正确释放
- ✅ 边界条件处理完善
### 性能
- ✅ 100次快速调用压力测试通过
- ✅ 1000次防抖调用内存安全测试通过
- ✅ 并发场景正确隔离
### 异常处理
- ✅ 异常正确传播
- ✅ 各类异常类型支持Error、Exception、String、自定义异常
- ✅ UI安全模式正确吞掉异常
- ✅ onError回调正确接收错误信息和堆栈
### 兼容性
- ✅ 特殊字符tagId支持
- ✅ 中文/emoji tagId支持
- ✅ 空字符串tagId支持
- ✅ 极端duration值支持
---
## 📊 结论
**✅ 该插件已通过全部 75 项测试,达到商用级质量标准。**
### 核心优势
1. **弱网场景保护**: 通过异步锁机制,有效防止弱网环境下的重复请求
2. **双模式支持**: 同时支持节流(适合按钮点击)和防抖(适合搜索输入)
3. **内存安全**: 被取消的防抖调用不会导致Future挂起
4. **UI友好**: executeSafe方法可安全用于onTap等无法await的场景
5. **异常安全**: 任何异常都会正确释放锁,不会导致后续调用被永久阻塞
### 建议使用场景
- 按钮防重复点击
- 搜索框输入防抖
- 表单提交防重复
- API请求去重
- 弱网环境用户操作保护
### 注意事项
1. `clearAllLocks()` 仅清除异步锁,不影响 EasyThrottle/EasyDebounce 的内部时间窗口
2. 同一tagId切换节流/防抖模式时,建议等待上一次操作完成
3. 单例模式全局共享状态注意tagId的唯一性
---
*测试报告由自动化测试生成*

View File

@ -69,6 +69,10 @@ class AsyncThrottle {
_asyncLocks[tagId] = true; _asyncLocks[tagId] = true;
try { try {
await onExecute(); await onExecute();
} catch (e, stack) {
// execute() Future
// Timer unhandled async error UI
_completeDebounce(tagId, error: e, stackTrace: stack);
} finally { } finally {
_asyncLocks.remove(tagId); _asyncLocks.remove(tagId);
_completeDebounce(tagId); // await _completeDebounce(tagId); // await
@ -95,13 +99,36 @@ class AsyncThrottle {
} }
} }
/// UI onTap/onPressed await
///
/// - UI /
/// - [onError] //Toast
Future<void> executeSafe(
String tagId,
Future<void> Function() onExecute, {
Duration duration = const Duration(milliseconds: 300),
bool enableDebounce = false,
void Function(Object error, StackTrace stackTrace)? onError,
}) {
return execute(tagId, onExecute, duration: duration, enableDebounce: enableDebounce).catchError((
Object e,
StackTrace s,
) {
onError?.call(e, s);
});
}
/// Completer /// Completer
void _completeDebounce(String tagId) { void _completeDebounce(String tagId, {Object? error, StackTrace? stackTrace}) {
if (_debounceCompleters.containsKey(tagId)) { if (_debounceCompleters.containsKey(tagId)) {
final completer = _debounceCompleters[tagId]; final completer = _debounceCompleters[tagId];
if (completer != null && !completer.isCompleted) { if (completer != null && !completer.isCompleted) {
if (error != null) {
completer.completeError(error, stackTrace);
} else {
completer.complete(); completer.complete();
} }
}
_debounceCompleters.remove(tagId); _debounceCompleters.remove(tagId);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,370 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:yx_async_throttle_flutter/yx_async_throttle_flutter.dart';
void main() {
group('AsyncThrottle Safety Tests', () {
late AsyncThrottle throttle;
setUp(() {
throttle = AsyncThrottle.instance;
throttle.clearAllLocks();
});
// --- Throttle Tests ---
test('Throttle: Executes immediately and blocks subsequent calls', () async {
int counter = 0;
final completer = Completer<void>();
// First call: Should execute
final future1 = throttle.execute('throttle_test', () async {
counter++;
await completer.future; // Hold the lock
}, enableDebounce: false);
// Second call: Should be ignored (throttled) immediately
// Note: execute() is async.
await throttle.execute('throttle_test', () async {
counter++;
}, enableDebounce: false);
expect(counter, 1);
completer.complete();
await future1;
});
test('Throttle: Releases lock after exception', () async {
bool thrown = false;
const duration = Duration(milliseconds: 50);
try {
await throttle.execute('throttle_error', () async {
throw Exception('Boom');
}, duration: duration);
} catch (e) {
thrown = true;
}
expect(thrown, isTrue);
// Wait for throttle duration to pass so EasyThrottle allows next call
await Future.delayed(duration + const Duration(milliseconds: 20));
// Verify lock is released and throttle is reset by time
int counter = 0;
await throttle.execute('throttle_error', () async {
counter++;
}, duration: duration);
expect(counter, 1);
});
// --- Debounce Tests ---
test('Debounce: Rapid calls - only last executes, previous complete', () async {
int counter = 0;
final logs = <String>[];
// Call 1
final f1 = throttle.execute(
'debounce_rapid',
() async {
logs.add('1');
counter++;
},
enableDebounce: true,
duration: const Duration(milliseconds: 100),
);
// Call 2 (Immediate)
final f2 = throttle.execute(
'debounce_rapid',
() async {
logs.add('2');
counter++;
},
enableDebounce: true,
duration: const Duration(milliseconds: 100),
);
// Wait for debounce duration
await Future.delayed(const Duration(milliseconds: 200));
await f1;
await f2;
expect(counter, 1);
expect(logs, ['2']);
});
test('Debounce: Lock released after execution', () async {
await throttle.execute(
'debounce_lock',
() async {
// do nothing
},
enableDebounce: true,
duration: const Duration(milliseconds: 50),
);
// Wait for completion
await Future.delayed(const Duration(milliseconds: 100));
// Should be able to run again
int counter = 0;
await throttle.execute(
'debounce_lock',
() async {
counter++;
},
enableDebounce: true,
duration: const Duration(milliseconds: 50),
);
await Future.delayed(const Duration(milliseconds: 100));
expect(counter, 1);
});
test('Debounce: Exception releases lock and propagates error', () async {
bool caught = false;
try {
await throttle.execute(
'debounce_error',
() async {
throw Exception('Debounce Boom');
},
enableDebounce: true,
duration: const Duration(milliseconds: 50),
);
} catch (e) {
caught = true;
expect(e.toString(), contains('Debounce Boom'));
}
expect(caught, isTrue);
// Ensure lock is released
await Future.delayed(const Duration(milliseconds: 100));
int counter = 0;
await throttle.execute(
'debounce_error',
() async {
counter++;
},
enableDebounce: true,
duration: const Duration(milliseconds: 50),
);
await Future.delayed(const Duration(milliseconds: 100));
expect(counter, 1);
});
test('Concurrency: Mixed calls', () async {
// Run multiple tags
int c1 = 0;
int c2 = 0;
final f1 = throttle.execute('tag1', () async {
await Future.delayed(const Duration(milliseconds: 50));
c1++;
}, enableDebounce: false);
final f2 = throttle.execute('tag2', () async {
await Future.delayed(const Duration(milliseconds: 50));
c2++;
}, enableDebounce: false);
await Future.wait([f1, f2]);
expect(c1, 1);
expect(c2, 1);
});
test('Debounce: Many rapid calls - all returned Futures complete (no hang)', () async {
const tag = 'debounce_stress_many_calls';
int executed = 0;
final futures = <Future<void>>[];
for (var i = 0; i < 50; i++) {
futures.add(
throttle.execute(
tag,
() async {
executed++;
},
enableDebounce: true,
duration: const Duration(milliseconds: 30),
),
);
}
// Allow the last debounce to fire.
await Future.delayed(const Duration(milliseconds: 120));
// If any earlier call's await hangs, this will time out and fail.
await Future.wait(futures).timeout(const Duration(seconds: 1));
expect(executed, 1);
expect(throttle.isExecuting(tag), isFalse);
});
test('Debounce: clearAllLocks() releases awaiting Future and prevents later execution', () async {
const tag = 'debounce_clear_pending';
var executed = false;
final f = throttle.execute(
tag,
() async {
executed = true;
},
enableDebounce: true,
duration: const Duration(milliseconds: 100),
);
// Immediately clear; should release the awaiter and also make the later timer a no-op.
throttle.clearAllLocks();
await f.timeout(const Duration(seconds: 1));
// Wait longer than debounce duration to ensure the scheduled callback (if any) would have fired.
await Future.delayed(const Duration(milliseconds: 200));
expect(executed, isFalse);
expect(throttle.isExecuting(tag), isFalse);
});
test('Debounce: clearAllLocks() during execution does not deadlock and completes caller early', () async {
const tag = 'debounce_clear_while_running';
final onExecuteHold = Completer<void>();
var started = false;
final f = throttle.execute(
tag,
() async {
started = true;
await onExecuteHold.future;
},
enableDebounce: true,
duration: const Duration(milliseconds: 10),
);
// Ensure debounce has time to trigger and start execution.
await Future.delayed(const Duration(milliseconds: 50));
expect(started, isTrue);
expect(throttle.isExecuting(tag), isTrue);
// This should release the awaiting Future even though the task is still running.
throttle.clearAllLocks();
await f.timeout(const Duration(seconds: 1));
// Clean up the running onExecute.
onExecuteHold.complete();
await Future.delayed(const Duration(milliseconds: 20));
expect(throttle.isExecuting(tag), isFalse);
});
test('Throttle: Re-entrancy with same tag is blocked, other tag proceeds (no deadlock)', () async {
const tagA = 'throttle_reentrant_same_tag';
const tagB = 'throttle_reentrant_other_tag';
var a = 0;
var aInner = 0;
var b = 0;
await throttle.execute(tagA, () async {
a++;
// Same tag should be blocked by the async lock and return immediately (no hang).
await throttle
.execute(tagA, () async {
aInner++;
}, enableDebounce: false)
.timeout(const Duration(seconds: 1));
// Other tag should be allowed to execute.
await throttle
.execute(tagB, () async {
b++;
}, enableDebounce: false)
.timeout(const Duration(seconds: 1));
}, enableDebounce: false);
expect(a, 1);
expect(aInner, 0);
expect(b, 1);
expect(throttle.isExecuting(tagA), isFalse);
expect(throttle.isExecuting(tagB), isFalse);
});
test('Debounce: Last call throws -> last Future errors, previous Futures still complete', () async {
const tag = 'debounce_last_throws';
final f1 = throttle.execute(
tag,
() async {
// no-op
},
enableDebounce: true,
duration: const Duration(milliseconds: 50),
);
final f2 = throttle.execute(
tag,
() async {
throw StateError('boom');
},
enableDebounce: true,
duration: const Duration(milliseconds: 50),
);
// Attach listeners immediately; otherwise an async error may be reported as "unhandled"
// if it completes before we start awaiting it.
final f1Expectation = expectLater(f1, completes);
final f2Expectation = expectLater(
f2,
throwsA(isA<StateError>().having((e) => e.message, 'message', contains('boom'))),
);
await Future.delayed(const Duration(milliseconds: 120));
await f1Expectation.timeout(const Duration(seconds: 1));
await f2Expectation.timeout(const Duration(seconds: 1));
expect(throttle.isExecuting(tag), isFalse);
});
test('UI-safe mode: Throttle swallow error does not throw (executeSafe)', () async {
var errorCalled = false;
await throttle.executeSafe(
'throttle_swallow_error',
() async {
throw StateError('boom');
},
enableDebounce: false,
onError: (e, s) {
errorCalled = true;
expect(e, isA<StateError>());
},
);
expect(errorCalled, isTrue);
expect(throttle.isExecuting('throttle_swallow_error'), isFalse);
});
test('UI-safe mode: Debounce swallow error does not throw (executeSafe)', () async {
const tag = 'debounce_swallow_error';
var errorCalled = false;
final f = throttle.executeSafe(
tag,
() async {
throw StateError('boom');
},
enableDebounce: true,
duration: const Duration(milliseconds: 30),
onError: (e, s) {
errorCalled = true;
expect(e, isA<StateError>());
},
);
await Future.delayed(const Duration(milliseconds: 120));
await f.timeout(const Duration(seconds: 1));
expect(errorCalled, isTrue);
expect(throttle.isExecuting(tag), isFalse);
});
});
}