[
  {
    "path": ".claude/settings.local.json",
    "content": "{\n  \"permissions\": {\n    \"allow\": [\n      \"Bash(flutter test:*)\",\n      \"Bash(flutter analyze:*)\",\n      \"Bash(dart test:*)\",\n      \"WebFetch(domain:asyncredux.com)\",\n      \"WebFetch(domain:pub.dev)\",\n      \"WebFetch(domain:raw.githubusercontent.com)\",\n      \"WebFetch(domain:github.com)\",\n      \"Bash(curl:*)\",\n      \"Bash(ls:*)\",\n      \"mcp__dart__pub_dev_search\",\n      \"mcp__dart__pub\",\n      \"mcp__dart__analyze_files\",\n      \"mcp__dart__run_tests\"\n    ],\n    \"deny\": [],\n    \"ask\": []\n  }\n}\n"
  },
  {
    "path": ".claude/skills/asyncredux-abort-dispatch/SKILL.md",
    "content": "---\nname: asyncredux-abort-dispatch\ndescription: Stops an AsyncRedux (Flutter) action from dispatching. Use only when the user mentions abortDispatch(), or explicitly asks to abort or prevent dispatch under certain conditions.\n---\n\n# AsyncRedux Aborting the Dispatch\n\n## What is abortDispatch()?\n\nThe `abortDispatch()` method is an optional method on `ReduxAction` that lets you\nconditionally prevent an action from executing. When this method returns `true`, the\nentire action is skipped—`before()`, `reduce()`, and `after()` will NOT run, and state\nremains unchanged.\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  @override\n  bool abortDispatch() {\n    // Return true to abort, false to proceed\n    return someCondition;\n  }\n\n  @override\n  AppState? reduce() {\n    // Only runs if abortDispatch() returned false\n    return state.copy(/* ... */);\n  }\n}\n```\n\n## Basic Usage\n\nThe simplest use case is checking a condition before allowing the action to proceed:\n\n```dart\nclass LoadUserProfile extends ReduxAction<AppState> {\n  @override\n  bool abortDispatch() => state.user == null;\n\n  @override\n  Future<AppState?> reduce() async {\n    // Only runs if user is logged in\n    final profile = await api.fetchProfile(state.user!.id);\n    return state.copy(profile: profile);\n  }\n}\n```\n\n## Action Lifecycle with abortDispatch()\n\nWhen `abortDispatch()` returns `true`, the complete action lifecycle is skipped:\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  @override\n  bool abortDispatch() => state.shouldSkip;  // If true:\n\n  @override\n  void before() {\n    // NOT called when aborted\n  }\n\n  @override\n  AppState? reduce() {\n    // NOT called when aborted\n  }\n\n  @override\n  void after() {\n    // NOT called when aborted\n  }\n}\n```\n\nThis differs from throwing an error in `before()`, which would still cause `after()` to\nrun.\n\n## Authentication Guard Pattern\n\nA common pattern is creating a base action that requires authentication:\n\n```dart\n/// Base action that requires an authenticated user\nabstract class AuthenticatedAction extends ReduxAction<AppState> {\n  @override\n  bool abortDispatch() => state.user == null;\n}\n\n/// Actions extending this will only run when user is logged in\nclass FetchUserOrders extends AuthenticatedAction {\n  @override\n  Future<AppState?> reduce() async {\n    // Safe to use state.user! here - abortDispatch ensures it's not null\n    final orders = await api.getOrders(state.user!.id);\n    return state.copy(orders: orders);\n  }\n}\n\nclass UpdateUserSettings extends AuthenticatedAction {\n  final Settings newSettings;\n  UpdateUserSettings(this.newSettings);\n\n  @override\n  Future<AppState?> reduce() async {\n    await api.updateSettings(state.user!.id, newSettings);\n    return state.copy(settings: newSettings);\n  }\n}\n```\n\n## Creating Base Actions with Abort Logic\n\nYou can combine multiple abort conditions in a base action:\n\n```dart\nabstract class AppAction extends ReduxAction<AppState> {\n  // Override in subclasses to add action-specific abort logic\n  bool shouldAbort() => false;\n\n  @override\n  bool abortDispatch() {\n    // Global abort conditions\n    if (state.isMaintenanceMode) return true;\n    if (state.isAppLocked) return true;\n\n    // Action-specific abort conditions\n    return shouldAbort();\n  }\n}\n\nclass RefreshData extends AppAction {\n  @override\n  bool shouldAbort() {\n    // Don't refresh if data is still fresh\n    return state.lastRefresh != null &&\n        DateTime.now().difference(state.lastRefresh!) < Duration(minutes: 5);\n  }\n\n  @override\n  Future<AppState?> reduce() async {\n    final data = await api.fetchData();\n    return state.copy(data: data, lastRefresh: DateTime.now());\n  }\n}\n```\n\n## Role-Based Authorization\n\nUse `abortDispatch()` to implement role-based access control:\n\n```dart\nabstract class AdminAction extends ReduxAction<AppState> {\n  @override\n  bool abortDispatch() => state.user?.role != UserRole.admin;\n}\n\nclass DeleteAllUsers extends AdminAction {\n  @override\n  Future<AppState?> reduce() async {\n    // Only admins can reach this code\n    await api.deleteAllUsers();\n    return state.copy(users: []);\n  }\n}\n```\n\n## Conditional Feature Actions\n\nPrevent actions when features are disabled:\n\n```dart\nclass UsePremiumFeature extends ReduxAction<AppState> {\n  @override\n  bool abortDispatch() => !state.user!.isPremium;\n\n  @override\n  AppState? reduce() {\n    // Premium-only functionality\n    return state.copy(/* ... */);\n  }\n}\n```\n\n## Built-in Mixin: AbortWhenNoInternet\n\nAsyncRedux provides `AbortWhenNoInternet`, a mixin that silently aborts actions when\nthere's no internet connection:\n\n```dart\nclass FetchLatestNews extends AppAction with AbortWhenNoInternet {\n  @override\n  Future<AppState?> reduce() async {\n    // Only runs if internet is available\n    final news = await api.fetchNews();\n    return state.copy(news: news);\n  }\n}\n```\n\nKey characteristics of `AbortWhenNoInternet`:\n\n- No error dialogs are shown\n- No exceptions are thrown\n- The action is silently cancelled\n- Only checks if device internet is on/off (not server availability)\n\nCompare with `CheckInternet` which shows an error dialog instead of silently aborting.\n\n## abortDispatch() vs Throwing in before()\n\nChoose the right approach for your use case:\n\n| Approach                       | `after()` runs? | Shows error?           | Use when             |\n|--------------------------------|-----------------|------------------------|----------------------|\n| `abortDispatch()` returns true | No              | No                     | Silently skip action |\n| Throw in `before()`            | Yes             | Yes (if UserException) | Show error to user   |\n\n```dart\n// Silent abort - user doesn't know action was skipped\nclass SilentRefresh extends ReduxAction<AppState> {\n  @override\n  bool abortDispatch() => state.isOffline;\n  // ...\n}\n\n// Visible error - user sees message\nclass ExplicitRefresh extends ReduxAction<AppState> {\n  @override\n  void before() {\n    if (state.isOffline) {\n      throw UserException('Cannot refresh while offline');\n    }\n  }\n  // ...\n}\n```\n\n## When to Use abortDispatch()\n\n**Good use cases:**\n\n- Authentication guards (action requires logged-in user)\n- Authorization checks (action requires specific role/permission)\n- Feature flags (action only for premium users)\n- Freshness checks (don't refetch if data is recent)\n- Maintenance mode (disable certain actions globally)\n- Idempotency (skip if action's effect already applied)\n\n**Consider alternatives when:**\n\n- You want the user to see an error message (throw `UserException` in `before()`)\n- You need cleanup code to run (use `before()` + `after()` pattern)\n- You're implementing rate limiting (use `Throttle` or `Debounce` mixins)\n- You're preventing duplicate dispatches (use `NonReentrant` mixin)\n\n## Complete Example\n\n```dart\n// Base action with common abort logic\nabstract class AppAction extends ReduxAction<AppState> {\n  @override\n  bool abortDispatch() {\n    // Global maintenance mode check\n    if (state.maintenanceMode) return true;\n    return false;\n  }\n}\n\n// Authenticated action that also checks maintenance mode\nabstract class AuthenticatedAction extends AppAction {\n  @override\n  bool abortDispatch() {\n    // Check parent conditions first\n    if (super.abortDispatch()) return true;\n    // Then check authentication\n    return state.currentUser == null;\n  }\n}\n\n// Admin action with full authorization chain\nabstract class AdminAction extends AuthenticatedAction {\n  @override\n  bool abortDispatch() {\n    if (super.abortDispatch()) return true;\n    return state.currentUser?.role != UserRole.admin;\n  }\n}\n\n// Concrete action using the hierarchy\nclass BanUser extends AdminAction {\n  final String userId;\n  BanUser(this.userId);\n\n  @override\n  Future<AppState?> reduce() async {\n    // Only reaches here if:\n    // 1. Not in maintenance mode\n    // 2. User is logged in\n    // 3. User is an admin\n    await api.banUser(userId);\n    return state.copy(\n      users: state.users.where((u) => u.id != userId).toList(),\n    );\n  }\n}\n```\n\n## Important Notes\n\n- `abortDispatch()` is checked before `before()`, `reduce()`, and `after()`\n- When aborted, no state changes occur\n- The action is silently skipped—no errors are thrown or logged by default\n- Use this feature judiciously; the documentation warns it's \"a powerful feature\" that\n  should only be used \"if you are sure it is the right solution\"\n\n## References\n\nURLs from the documentation:\n\n- https://asyncredux.com/flutter/advanced-actions/aborting-the-dispatch\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n- https://asyncredux.com/flutter/advanced-actions/action-status\n- https://asyncredux.com/flutter/advanced-actions/control-mixins\n- https://asyncredux.com/flutter/advanced-actions/internet-mixins\n- https://asyncredux.com/flutter/advanced-actions/action-mixins\n- https://asyncredux.com/flutter/basics/actions-and-reducers\n- https://asyncredux.com/flutter/basics/action-simplification\n"
  },
  {
    "path": ".claude/skills/asyncredux-action-status/SKILL.md",
    "content": "---\nname: asyncredux-action-status\ndescription: Checks an AsyncRedux (Flutter) action's completion status using ActionStatus right after the dispatch returns. Use only when you need to know whether an action completed, whether it failed with an error, what error it produced, or how to navigate based on success or failure.\n---\n\n# ActionStatus in AsyncRedux\n\nThe `ActionStatus` object provides information about whether an action completed successfully or encountered errors. It is returned by `dispatchAndWait()` and related methods.\n\n## Getting ActionStatus\n\nUse `dispatchAndWait()` to get the status after an action completes:\n\n```dart\nvar status = await dispatchAndWait(MyAction());\n```\n\nFrom within an action, you can also use:\n\n```dart\nvar status = await dispatchAndWait(SomeOtherAction());\n```\n\n## ActionStatus Properties\n\n### Completion Status\n\n- **`isCompleted`**: Returns `true` if the action has finished executing (whether successful or failed)\n- **`isCompletedOk`**: Returns `true` if the action finished without errors in both `before()` and `reduce()` methods\n- **`isCompletedFailed`**: Returns `true` if the action encountered errors (opposite of `isCompletedOk`)\n\n### Error Information\n\n- **`originalError`**: The error originally thrown by `before()` or `reduce()`, before any modification\n- **`wrappedError`**: The error after processing by the action's `wrapError()` method\n\n### Execution Tracking\n\nThese properties track which lifecycle methods have completed:\n\n- **`hasFinishedMethodBefore`**: Returns `true` if the `before()` method completed\n- **`hasFinishedMethodReduce`**: Returns `true` if the `reduce()` method completed\n- **`hasFinishedMethodAfter`**: Returns `true` if the `after()` method completed\n\nNote: The execution tracking properties are primarily meant for testing and debugging. In production code, focus on `isCompletedOk` and `isCompletedFailed`.\n\n## Common Use Cases\n\n### Conditional Navigation After Success\n\nThe most common production use is checking if an action succeeded before navigating:\n\n```dart\n// In a widget callback\nFuture<void> _onSavePressed() async {\n  var status = await context.dispatchAndWait(SaveFormAction());\n  if (status.isCompletedOk) {\n    Navigator.pop(context);\n  }\n}\n```\n\nAnother example with push navigation:\n\n```dart\nFuture<void> _onLoginPressed() async {\n  var status = await context.dispatchAndWait(LoginAction(\n    email: emailController.text,\n    password: passwordController.text,\n  ));\n\n  if (status.isCompletedOk) {\n    Navigator.pushReplacementNamed(context, '/home');\n  }\n  // If failed, the error will be shown via UserExceptionDialog\n}\n```\n\n### Testing Action Errors\n\nUse ActionStatus to verify that actions throw expected errors:\n\n```dart\ntest('MyAction fails with invalid input', () async {\n  var store = Store<AppState>(initialState: AppState.initial());\n\n  var status = await store.dispatchAndWait(MyAction(value: -1));\n\n  expect(status.isCompletedFailed, isTrue);\n  expect(status.wrappedError, isA<UserException>());\n  expect((status.wrappedError as UserException).msg, \"Value must be positive\");\n});\n```\n\n### Testing Action Success\n\n```dart\ntest('SaveAction completes successfully', () async {\n  var store = Store<AppState>(initialState: AppState.initial());\n\n  var status = await store.dispatchAndWait(SaveAction(data: validData));\n\n  expect(status.isCompletedOk, isTrue);\n  expect(store.state.saved, isTrue);\n});\n```\n\n### Checking Original vs Wrapped Error\n\nWhen your action uses `wrapError()` to transform errors, you can inspect both:\n\n```dart\nclass MyAction extends AppAction {\n  @override\n  Future<AppState?> reduce() async {\n    throw Exception('Network error');\n  }\n\n  @override\n  Object? wrapError(Object error, StackTrace stackTrace) {\n    return UserException('Could not save. Please try again.');\n  }\n}\n\n// In test:\nvar status = await store.dispatchAndWait(MyAction());\nexpect(status.originalError, isA<Exception>()); // The original Exception\nexpect(status.wrappedError, isA<UserException>()); // The wrapped UserException\n```\n\n## Action Lifecycle and Status\n\nThe action lifecycle runs in this order:\n\n1. `before()` - Runs first, can be used for preconditions\n2. `reduce()` - Runs second (only if `before()` succeeded)\n3. `after()` - Runs last, always executes (like a finally block)\n\nThe `isCompletedOk` property is `true` only if both `before()` and `reduce()` completed without errors. Note that errors in `after()` do not affect `isCompletedOk`.\n\nIf `before()` throws an error, `reduce()` will not run, but `after()` will still execute.\n\n## Best Practices\n\n1. **Use state changes for UI updates**: In production, prefer checking state changes rather than action status. Reserve ActionStatus for cases where you need to perform side effects (like navigation) based on success/failure.\n\n2. **Use `isCompletedOk` for navigation**: The common pattern is to navigate only after an action succeeds:\n   ```dart\n   if (status.isCompletedOk) Navigator.pop(context);\n   ```\n\n3. **Use `wrappedError` in tests**: When testing error handling, check `wrappedError` to see what the user will actually see (after `wrapError()` processing).\n\n4. **Use `originalError` for debugging**: When you need to see the underlying error before any transformation, use `originalError`.\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/advanced-actions/action-status\n- https://asyncredux.com/flutter/basics/dispatching-actions\n- https://asyncredux.com/flutter/basics/failed-actions\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/miscellaneous/navigation\n- https://asyncredux.com/flutter/testing/store-tester\n- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect\n- https://asyncredux.com/flutter/testing/testing-user-exceptions\n"
  },
  {
    "path": ".claude/skills/asyncredux-actions-no-state-change/SKILL.md",
    "content": "---\nname: asyncredux-actions-no-state-change\ndescription: Creates AsyncRedux (Flutter) actions that return null from reduce() to not change the state. Such actions can still do side effects, dispatch other actions, or do nothing. \n---\n\n# Actions That Don't Change State\n\nIn AsyncRedux, returning a new state from reducers is **optional**. When you don't need to\nmodify the application state, return `null` to keep the current state unchanged.\n\n## Basic Pattern\n\nReturn `null` from `reduce()` when no state modification is needed:\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  AppState? reduce() {\n    // Perform side effects here\n    return null; // State remains unchanged\n  }\n}\n```\n\n## Conditional State Updates\n\nOnly update state when certain conditions are met:\n\n```dart\nclass GetAmount extends ReduxAction<AppState> {\n  Future<AppState?> reduce() async {\n    int amount = await getAmount();\n    if (amount == 0)\n      return null; // No change needed\n    else\n      return state.copy(counter: state.counter + amount);\n  }\n}\n```\n\n## Coordinating Other Actions\n\nActions that dispatch other actions but don't modify state directly:\n\n```dart\nclass InitAction extends ReduxAction<AppState> {\n  AppState? reduce() {\n    dispatch(ReadDatabaseAction());\n    dispatch(StartTimersAction());\n    dispatch(TurnOnListenersAction());\n    return null; // This action doesn't change state itself\n  }\n}\n```\n\n## Triggering External Services\n\nCall external services without modifying app state:\n\n```dart\nclass SendNotification extends ReduxAction<AppState> {\n  final String message;\n  SendNotification(this.message);\n\n  Future<AppState?> reduce() async {\n    await notificationService.send(message);\n    return null;\n  }\n}\n```\n\n## Navigation Actions\n\nTrigger navigation as a side effect:\n\n```dart\nclass GoToSettings extends ReduxAction<AppState> {\n  AppState? reduce() {\n    dispatch(NavigateAction.pushNamed('/settings'));\n    return null;\n  }\n}\n```\n\n## Key Points\n\n1. Actions that do return a new state can **also** do side effects and dispatch other actions.\n2. **Return type matters**: Use `AppState?` for sync, `Future<AppState?>` for async\n3. **Null means no change**: The store keeps its current state\n\n## References\n\nURLs from the documentation:\n\n- https://asyncredux.com/flutter/basics/changing-state-is-optional\n- https://asyncredux.com/flutter/basics/actions-and-reducers\n- https://asyncredux.com/flutter/basics/sync-actions\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n"
  },
  {
    "path": ".claude/skills/asyncredux-async-actions/SKILL.md",
    "content": "---\nname: asyncredux-async-actions\ndescription: Creates AsyncRedux (Flutter) asynchronous actions for API calls, database operations, and other async work. \n---\n\n# AsyncRedux Async Actions\n\n## Basic Async Action Structure\n\nAn action becomes asynchronous when its `reduce()` method returns `Future<AppState?>`\ninstead of `AppState?`. Use this for database access, API calls, file operations, or any\nwork requiring `await`.\n\n```dart\nclass FetchUser extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    final user = await api.fetchUser();\n    return state.copy(user: user);\n  }\n}\n```\n\nUnlike traditional Redux requiring middleware, AsyncRedux makes it simple: return a\n`Future` and it works.\n\n## Critical Rule: Every Path Must Have await\n\nIf the action is async (returns a Future) and changes the state (returns a non-null\nstate), the framework requires that,  **all execution paths contain at least\none `await`**. Never declare `Future<AppState?>` if you don't actually await something.\n\n### Valid Patterns\n\n```dart\n// Simple async with await\nFuture<AppState?> reduce() async {\n  final data = await fetchData();\n  return state.copy(data: data);\n}\n\n// Using microtask (minimum valid await)\nFuture<AppState?> reduce() async {\n  await microtask;\n  return state.copy(timestamp: DateTime.now());\n}\n\n// Conditional - both paths have await\nFuture<AppState?> reduce() async {\n  if (state.needsRefresh) {\n    return await fetchAndUpdate();\n  }\n  else return await validateCurrent();\n}\n\n// Always returns null\nFuture<AppState?> reduce() async {\n  if (state.needsRefresh) {\n    await fetchAndUpdate();\n  }  \n  \n  return null;\n}\n```\n\n### Invalid Patterns (Will Cause Issues)\n\n```dart\n// WRONG: No await at all\nFuture<AppState?> reduce() async {\n  return state.copy(counter: state.counter + 1);\n}\n\n// WRONG: await only on some paths\nFuture<AppState?> reduce() async {\n  if (condition) {\n    return await fetchData();\n  }\n  return state; // No await on this path!\n}\n\n// WRONG: Calling async function without await\nFuture<AppState?> reduce() async {\n  someAsyncFunction(); // Not awaited\n  return state;\n}\n```\n\n## Using assertUncompletedFuture()\n\nFor complex reducers with multiple code paths, add `assertUncompletedFuture()` before the\nfinal return. This catches violations at runtime during development:\n\n```dart\nclass ComplexAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    if (state.cacheValid) {\n      // Complex logic that might accidentally skip await\n      return processCache();\n    }\n\n    final data = await fetchFromServer();\n    final processed = transform(data);\n\n    assertUncompletedFuture(); // Validates at least one await occurred\n    return state.copy(data: processed);\n  }\n}\n```\n\n## State Changes During Async Operations\n\nThe `state` getter can change after every `await` because other actions may modify state\nwhile yours is waiting:\n\n```dart\nclass AsyncAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    print(state.counter); // e.g., 5\n\n    await someSlowOperation();\n\n    // state.counter might now be different (e.g., 10)\n    // if another action modified it during the await\n    print(state.counter);\n\n    return state.copy(counter: state.counter + 1);\n  }\n}\n```\n\n### Using initialState for Comparison\n\nUse `initialState` to access the state as it was when the action was dispatched (never\nchanges):\n\n```dart\nclass SafeIncrement extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    final originalCounter = initialState.counter;\n\n    await validateWithServer();\n\n    // Check if state changed while we were waiting\n    if (state.counter != originalCounter) {\n      // State was modified by another action\n      return null; // Abort our change\n    }\n\n    return state.copy(counter: state.counter + 1);\n  }\n}\n```\n\n## Dispatching Async Actions\n\n### Fire and Forget\n\nUse `dispatch()` when you don't need to wait for completion:\n\n```dart\ncontext.dispatch(FetchUser());\n// Returns immediately, action runs in background\n```\n\n### Wait for Completion\n\nUse `dispatchAndWait()` to await the action's completion:\n\n```dart\nawait context.dispatchAndWait(FetchUser());\n// Continues only after action finishes AND state changes\nprint('User loaded: ${context.state.user.name}');\n```\n\n### Dispatch Multiple in Parallel\n\n```dart\n// Fire all, don't wait\ncontext.dispatchAll([FetchUser(), FetchSettings(), FetchNotifications()]);\n\n// Fire all and wait for all to complete\nawait context.dispatchAndWaitAll([FetchUser(), FetchSettings()]);\n```\n\n## Showing Loading States\n\nUse `isWaiting()` to show spinners while async actions run:\n\n```dart\nWidget build(BuildContext context) {\n  if (context.isWaiting(FetchUser)) return CircularProgressIndicator();  \n  else return Text('Hello, ${context.state.user.name}');\n}\n```\n\n## Error Handling\n\nThrow `UserException` for user-facing errors:\n\n```dart\nclass FetchUser extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    final response = await api.fetchUser();\n\n    if (response.statusCode == 404) \n      throw UserException('User not found.');    \n\n    if (response.statusCode != 200) \n      throw UserException('Failed to load user. Please try again.');    \n\n    return state.copy(user: response.data);\n  }\n}\n```\n\nCheck for failures in widgets:\n\n```dart\nWidget build(BuildContext context) {\n  if (context.isFailed(FetchUser)) {\n    return Text('Error: ${context.exceptionFor(FetchUser)?.message}');\n  }\n  // ...\n}\n```\n\n## Complete Example\n\n```dart\n// Async action with proper error handling\nclass LoadProducts extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    try {\n      final products = await api.fetchProducts();\n      return state.copy(products: products, productsLoaded: true);\n    } catch (e) {\n      throw UserException('Could not load products. Check your connection.');\n    }\n  }\n}\n\n// Widget showing all three states\nWidget build(BuildContext context) {\n  // Loading state\n  if (context.isWaiting(LoadProducts)) {\n    return Center(child: CircularProgressIndicator());\n  }\n\n  // Error state\n  if (context.isFailed(LoadProducts)) {\n    return Center(\n      child: Column(\n        children: [\n          Text(context.exceptionFor(LoadProducts)?.message ?? 'Error'),\n          ElevatedButton(\n            onPressed: () => context.dispatch(LoadProducts()),\n            child: Text('Retry'),\n          ),\n        ],\n      ),\n    );\n  }\n\n  // Success state\n  return ListView.builder(\n    itemCount: context.state.products.length,\n    itemBuilder: (_, i) => ProductTile(context.state.products[i]),\n  );\n}\n```\n\n## Return Type Warning\n\nNever return `FutureOr<AppState?>` directly. AsyncRedux must know if the action is sync or\nasync:\n\n```dart\n// CORRECT\nFuture<AppState?> reduce() async { ... }\n\n// CORRECT\nAppState? reduce() { ... }\n\n// WRONG - throws StoreException\nFutureOr<AppState?> reduce() { ... }\n```\n\n## References\n\nURLs from the documentation:\n\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/basics/actions-and-reducers\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/basics/failed-actions\n- https://asyncredux.com/flutter/basics/dispatching-actions\n- https://asyncredux.com/flutter/basics/wait-fail-succeed\n"
  },
  {
    "path": ".claude/skills/asyncredux-base-action/SKILL.md",
    "content": "---\nname: asyncredux-base-action\ndescription: Create a custom base action class for your app. Covers adding getter shortcuts to state parts, adding selector methods, implementing shared wrapError logic, and establishing project-wide action conventions.\n---\n\n# Creating a Custom Base Action Class\n\nEvery AsyncRedux application should define an abstract base action class that all actions\nextend. This provides a central place for:\n\n- Convenient getter shortcuts to state parts\n- Selector methods for common queries\n- Shared error handling logic\n- Type-safe environment access\n- Project-wide conventions\n\n## Basic Base Action Setup\n\nCreate an abstract class extending `ReduxAction<AppState>`.\nThe recomended name is `AppAction`, in file `app_action.dart`:\n\n```dart\nabstract class AppAction extends ReduxAction<AppState> {\n  // All your actions will extend this class\n}\n```\n\nThen extend `AppAction` instead of `ReduxAction<AppState>` in all your actions:\n\n```dart\nclass IncrementCounter extends AppAction {\n  @override\n  AppState reduce() => state.copy(counter: state.counter + 1);\n}\n```\n\n## Adding Getter Shortcuts to State Parts\n\nWhen your state has nested objects, add getters to simplify access:\n\n```dart\nabstract class AppAction extends ReduxAction<AppState> {\n  // Shortcuts to nested state parts\n  User get user => state.user;\n  Settings get settings => state.settings;\n  IList<Todo> get todos => state.todos;\n  Cart get cart => state.cart;\n}\n```\n\nNow actions can write cleaner code:\n\n```dart\nclass UpdateUserName extends AppAction {\n  final String name;\n  UpdateUserName(this.name);\n\n  @override\n  AppState reduce() {\n    // Instead of: state.user.name\n    // You can write: user.name\n    return state.copy(user: user.copy(name: name));\n  }\n}\n```\n\n## Adding Selector Methods\n\nFor common data lookups, add selector methods directly to your base action:\n\n```dart\nabstract class AppAction extends ReduxAction<AppState> {\n  // Getters for state parts\n  User get user => state.user;\n  IList<Item> get items => state.items;\n\n  // Selector methods\n  Item? findItemById(String id) =>\n      items.firstWhereOrNull((item) => item.id == id);\n\n  List<Item> get completedItems =>\n      items.where((item) => item.isCompleted).toList();\n\n  bool get isLoggedIn => user.isAuthenticated;\n}\n```\n\nActions can then use these selectors:\n\n```dart\nclass MarkItemComplete extends AppAction {\n  final String itemId;\n  MarkItemComplete(this.itemId);\n\n  @override\n  AppState reduce() {\n    final item = findItemById(itemId);\n    if (item == null) return null; // No change\n\n    return state.copy(\n      items: items.replaceFirstWhere(\n        (i) => i.id == itemId,\n        item.copy(isCompleted: true),\n      ),\n    );\n  }\n}\n```\n\n### Using a Separate Selector Class\n\nFor most applications, it's better to use instead a dedicated selector class to keep the\nbase action clean:\n\n```dart\nclass ActionSelect {\n  final AppState state;\n  ActionSelect(this.state);\n\n  Item? findById(String id) =>\n      state.items.firstWhereOrNull((item) => item.id == id);\n\n  List<Item> get completed =>\n      state.items.where((item) => item.isCompleted).toList();\n\n  List<Item> get pending =>\n      state.items.where((item) => !item.isCompleted).toList();\n}\n\nabstract class AppAction extends ReduxAction<AppState> {\n  ActionSelect get select => ActionSelect(state);\n}\n```\n\nThese namespaces selectors under `select`, enabling IDE autocompletion:\n\n```dart\nclass ProcessItem extends AppAction {\n  final String itemId;\n  ProcessItem(this.itemId);\n\n  @override\n  AppState reduce() {\n    // IDE autocomplete shows: select.findById, select.completed, etc.\n    final item = select.findById(itemId);\n    // ...\n  }\n}\n```\n\n## Type-Safe Environment Access\n\nFor dependency injection, override the `env` getter in your base action:\n\n```dart\nclass Environment {\n  final ApiClient api;\n  final AuthService auth;\n  final AnalyticsService analytics;\n\n  Environment({\n    required this.api,\n    required this.auth,\n    required this.analytics,\n  });\n}\n\nabstract class AppAction extends ReduxAction<AppState> {\n  // Type-safe access to environment\n  @override\n  Environment get env => super.env as Environment;\n\n  // Convenience getters for common services\n  ApiClient get api => env.api;\n  AuthService get auth => env.auth;\n}\n```\n\nActions can then use services directly:\n\n```dart\nclass FetchUserProfile extends AppAction {\n  @override\n  Future<AppState?> reduce() async {\n    // Uses the api getter from base action\n    final profile = await api.getUserProfile();\n    return state.copy(user: profile);\n  }\n}\n```\n\n## References\n\nURLs from the documentation:\n\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/advanced-actions/action-selectors\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/advanced-actions/wrapping-the-reducer\n- https://asyncredux.com/flutter/advanced-actions/action-mixins\n- https://asyncredux.com/flutter/advanced-actions/aborting-the-dispatch\n- https://asyncredux.com/flutter/basics/actions-and-reducers\n- https://asyncredux.com/flutter/basics/state\n- https://asyncredux.com/flutter/basics/using-the-store-state\n- https://asyncredux.com/flutter/miscellaneous/business-logic\n- https://asyncredux.com/flutter/miscellaneous/dependency-injection\n"
  },
  {
    "path": ".claude/skills/asyncredux-before-after/SKILL.md",
    "content": "---\nname: asyncredux-before-after\ndescription: Implement action lifecycle methods `before()` and `after()`. Covers running precondition checks, showing/hiding modal barriers, cleanup logic in `after()`, and understanding that `after()` always runs (like a finally block).\n---\n\n# AsyncRedux Before and After Methods\n\n## Action Lifecycle Overview\n\nEvery `ReduxAction` has three lifecycle methods that execute in order:\n\n1. `before()` - Runs first, before the reducer\n2. `reduce()` - The main reducer (required)\n3. `after()` - Runs last, always executes\n\nOnly `reduce()` is required. The `before()` and `after()` methods are optional hooks for managing side effects.\n\n## The before() Method\n\nThe `before()` method executes before the reducer runs. It can be synchronous or asynchronous.\n\n### Synchronous before()\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  @override\n  void before() {\n    // Runs synchronously before reduce()\n    print('Action starting');\n  }\n\n  @override\n  AppState? reduce() {\n    return state.copy(counter: state.counter + 1);\n  }\n}\n```\n\n### Asynchronous before()\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  @override\n  Future<void> before() async {\n    // Runs asynchronously before reduce()\n    await validatePermissions();\n  }\n\n  @override\n  Future<AppState?> reduce() async {\n    final data = await fetchData();\n    return state.copy(data: data);\n  }\n}\n```\n\n### Precondition Checks in before()\n\nIf `before()` throws an error, `reduce()` will NOT run. This makes it ideal for validation:\n\n```dart\nclass FetchUserData extends ReduxAction<AppState> {\n  @override\n  Future<void> before() async {\n    if (!await hasInternetConnection()) {\n      throw UserException('No internet connection');\n    }\n  }\n\n  @override\n  Future<AppState?> reduce() async {\n    // Only runs if before() completed without error\n    final user = await api.fetchUser();\n    return state.copy(user: user);\n  }\n}\n```\n\n### Common before() Use Cases\n\n- Validate preconditions (authentication, permissions)\n- Check network connectivity\n- Show loading indicators or modal barriers\n- Log action start for analytics\n- Dispatch prerequisite actions\n\n## The after() Method\n\nThe `after()` method executes after the reducer completes. Its key property: **it always runs, even if `before()` or `reduce()` throws an error**. This makes it similar to a `finally` block.\n\n### Basic after()\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    return state.copy(counter: state.counter + 1);\n  }\n\n  @override\n  void after() {\n    // Always runs, regardless of success or failure\n    print('Action completed');\n  }\n}\n```\n\n### Guaranteed Cleanup\n\nBecause `after()` always runs, it's perfect for cleanup operations:\n\n```dart\nclass SaveDocument extends ReduxAction<AppState> {\n  @override\n  Future<void> before() async {\n    dispatch(ShowSavingIndicatorAction(true));\n  }\n\n  @override\n  Future<AppState?> reduce() async {\n    await api.saveDocument(state.document);\n    return state.copy(lastSaved: DateTime.now());\n  }\n\n  @override\n  void after() {\n    // Hides indicator even if save fails\n    dispatch(ShowSavingIndicatorAction(false));\n  }\n}\n```\n\n### Important: Never Throw from after()\n\nThe `after()` method should never throw errors. Any exception thrown from `after()` will appear asynchronously in the console and cannot be caught normally:\n\n```dart\n// WRONG - Don't throw in after()\n@override\nvoid after() {\n  if (someCondition) {\n    throw Exception('This will cause problems');\n  }\n}\n\n// CORRECT - Handle errors gracefully\n@override\nvoid after() {\n  try {\n    cleanup();\n  } catch (e) {\n    // Log but don't throw\n    logger.error('Cleanup failed: $e');\n  }\n}\n```\n\n### Common after() Use Cases\n\n- Hide loading indicators or modal barriers\n- Close database connections or file handles\n- Release temporary resources\n- Log action completion for analytics\n- Dispatch follow-up actions\n\n## Modal Barrier Pattern\n\nA common pattern is showing a modal barrier (blocking overlay) during async operations:\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    String description = await read(Uri.http(\"numbersapi.com\", \"${state.counter}\"));\n    return state.copy(description: description);\n  }\n\n  @override\n  void before() => dispatch(BarrierAction(true));\n\n  @override\n  void after() => dispatch(BarrierAction(false));\n}\n```\n\nThe `BarrierAction` would update state to show/hide a loading overlay:\n\n```dart\nclass BarrierAction extends ReduxAction<AppState> {\n  final bool show;\n  BarrierAction(this.show);\n\n  @override\n  AppState reduce() => state.copy(showBarrier: show);\n}\n```\n\n## Creating Reusable Mixins\n\nFor patterns you use repeatedly, create a mixin:\n\n```dart\nmixin Barrier on ReduxAction<AppState> {\n  @override\n  void before() {\n    super.before();\n    dispatch(BarrierAction(true));\n  }\n\n  @override\n  void after() {\n    dispatch(BarrierAction(false));\n    super.after();\n  }\n}\n```\n\nThen apply it to any action:\n\n```dart\nclass FetchData extends ReduxAction<AppState> with Barrier {\n  @override\n  Future<AppState?> reduce() async {\n    // Barrier shown automatically before this runs\n    final data = await api.fetchData();\n    return state.copy(data: data);\n    // Barrier hidden automatically after (even on error)\n  }\n}\n```\n\n### Multiple Mixins\n\nYou can combine multiple mixins:\n\n```dart\nclass ImportantAction extends ReduxAction<AppState> with Barrier, NonReentrant {\n  @override\n  Future<AppState?> reduce() async {\n    // Has both modal barrier AND prevents duplicate dispatches\n    return state;\n  }\n}\n```\n\n## Error Handling Flow\n\nUnderstanding how errors interact with the lifecycle:\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  @override\n  Future<void> before() async {\n    // If this throws, reduce() is skipped, after() still runs\n  }\n\n  @override\n  Future<AppState?> reduce() async {\n    // If this throws, state is not changed, after() still runs\n  }\n\n  @override\n  void after() {\n    // ALWAYS runs regardless of errors above\n  }\n}\n```\n\n### Checking What Completed\n\nUse `ActionStatus` to determine which methods finished:\n\n```dart\nvar status = await dispatchAndWait(MyAction());\n\nif (status.hasFinishedMethodBefore) {\n  print('before() completed');\n}\n\nif (status.hasFinishedMethodReduce) {\n  print('reduce() completed');\n}\n\nif (status.hasFinishedMethodAfter) {\n  print('after() completed');\n}\n\nif (status.isCompletedOk) {\n  print('Both before() and reduce() completed without errors');\n}\n\nif (status.isCompletedFailed) {\n  print('Error: ${status.originalError}');\n}\n```\n\n## Relationship with abortDispatch()\n\nIf `abortDispatch()` returns `true`, none of the lifecycle methods run:\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  @override\n  bool abortDispatch() => state.user == null;\n\n  @override\n  void before() {\n    // Skipped if abortDispatch() returns true\n  }\n\n  @override\n  AppState? reduce() {\n    // Skipped if abortDispatch() returns true\n  }\n\n  @override\n  void after() {\n    // Skipped if abortDispatch() returns true\n  }\n}\n```\n\n## Complete Example\n\n```dart\nclass SubmitForm extends ReduxAction<AppState> {\n  final String formData;\n  SubmitForm(this.formData);\n\n  @override\n  Future<void> before() async {\n    // Validate preconditions\n    if (state.user == null) {\n      throw UserException('Please log in first');\n    }\n\n    if (!await checkInternetConnection()) {\n      throw UserException('No internet connection');\n    }\n\n    // Show loading state\n    dispatch(SetSubmittingAction(true));\n  }\n\n  @override\n  Future<AppState?> reduce() async {\n    final result = await api.submitForm(formData);\n    return state.copy(\n      lastSubmission: result,\n      submissionCount: state.submissionCount + 1,\n    );\n  }\n\n  @override\n  void after() {\n    // Always hide loading state, even on error\n    dispatch(SetSubmittingAction(false));\n\n    // Log completion\n    analytics.log('form_submitted');\n  }\n}\n```\n\n## Built-in Mixins Using before() and after()\n\nSeveral AsyncRedux mixins use these methods internally:\n\n| Mixin | Uses before() | Uses after() | Purpose |\n|-------|--------------|--------------|---------|\n| `CheckInternet` | Yes | No | Verifies connectivity, shows dialog if offline |\n| `AbortWhenNoInternet` | Yes | No | Silently aborts if offline |\n| `Throttle` | No | Yes | Limits execution frequency |\n| `NonReentrant` | Yes | Yes | Prevents duplicate dispatches |\n| `Retry` | No | Yes | Retries on failure |\n| `Debounce` | No | No | Waits for input pause (uses `wrapReduce`) |\n\nWhen using these mixins, be aware that they may already override `before()` or `after()`. Call `super.before()` and `super.after()` if you need to combine behaviors.\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/advanced-actions/action-status\n- https://asyncredux.com/flutter/advanced-actions/action-mixins\n- https://asyncredux.com/flutter/advanced-actions/aborting-the-dispatch\n- https://asyncredux.com/flutter/advanced-actions/wrapping-the-reducer\n"
  },
  {
    "path": ".claude/skills/asyncredux-check-internet-mixin/SKILL.md",
    "content": "---\nname: asyncredux-check-internet-mixin\ndescription: Add the CheckInternet mixin to ensure network connectivity before action execution. Covers automatic error dialogs, combining with NoDialog for custom UI handling, and AbortWhenNoInternet for silent abort.\n---\n\n# CheckInternet Mixin\n\nThe `CheckInternet` mixin verifies device connectivity before an action executes. If there's no internet connection, the action aborts and displays a dialog with the message: \"There is no Internet. Please, verify your connection.\"\n\n## Basic Usage\n\n```dart\nclass LoadText extends AppAction with CheckInternet {\n\n  Future<AppState?> reduce() async {\n    var response = await http.get('https://api.example.com/text');\n    return state.copy(text: response.body);\n  }\n}\n```\n\nThe mixin works by overriding the `before()` method. If the device lacks connectivity, it throws a `UserException` which triggers the standard error dialog.\n\n## Customizing the Error Message\n\nOverride `connectionException()` to return a custom `UserException`:\n\n```dart\nclass LoadText extends AppAction with CheckInternet {\n\n  @override\n  UserException connectionException(UserException error) {\n    return UserException('Unable to load data. Check your connection.');\n  }\n\n  Future<AppState?> reduce() async {\n    var response = await http.get('https://api.example.com/text');\n    return state.copy(text: response.body);\n  }\n}\n```\n\n## NoDialog Modifier\n\nUse `NoDialog` alongside `CheckInternet` to suppress the automatic error dialog. This allows you to handle connectivity failures in your widgets using `isFailed()` and `exceptionFor()`:\n\n```dart\nclass LoadText extends AppAction with CheckInternet, NoDialog {\n\n  Future<AppState?> reduce() async {\n    var response = await http.get('https://api.example.com/text');\n    return state.copy(text: response.body);\n  }\n}\n```\n\nThen handle the error in your widget:\n\n```dart\nWidget build(BuildContext context) {\n  if (context.isWaiting(LoadText)) {\n    return CircularProgressIndicator();\n  }\n\n  if (context.isFailed(LoadText)) {\n    var exception = context.exceptionFor(LoadText);\n    return Text('Error: ${exception?.message}');\n  }\n\n  return Text(context.state.text);\n}\n```\n\n## AbortWhenNoInternet\n\nUse `AbortWhenNoInternet` for silent failure when offline. The action aborts without throwing errors or displaying dialogs—as if it had never been dispatched:\n\n```dart\nclass RefreshData extends AppAction with AbortWhenNoInternet {\n\n  Future<AppState?> reduce() async {\n    var response = await http.get('https://api.example.com/data');\n    return state.copy(data: response.body);\n  }\n}\n```\n\nThis is useful for background refreshes or non-critical operations where user notification isn't needed.\n\n## UnlimitedRetryCheckInternet\n\nThis mixin combines three capabilities: internet verification, unlimited retry with exponential backoff, and non-reentrant behavior. It's ideal for essential operations like loading startup data:\n\n```dart\nclass LoadAppStartupData extends AppAction with UnlimitedRetryCheckInternet {\n\n  Future<AppState?> reduce() async {\n    var response = await http.get('https://api.example.com/startup');\n    return state.copy(startupData: response.body);\n  }\n}\n```\n\nDefault retry parameters:\n- Initial delay: 350ms\n- Multiplier: 2\n- Maximum delay with internet: 5 seconds\n- Maximum delay without internet: 1 second\n\nTrack retry attempts via the `attempts` getter and customize logging through `printRetries()`.\n\n## Mixin Compatibility\n\nImportant compatibility rules:\n- `CheckInternet` and `AbortWhenNoInternet` are **incompatible** with each other\n- Neither `CheckInternet` nor `AbortWhenNoInternet` can be combined with `UnlimitedRetryCheckInternet`\n- `CheckInternet` works well with `Retry`, `NonReentrant`, `Throttle`, `Debounce`, and optimistic mixins\n\n## Testing Internet Connectivity\n\nTwo methods for simulating connectivity in tests:\n\n**Per-action simulation** - Override `internetOnOffSimulation` within specific actions:\n\n```dart\nclass LoadText extends AppAction with CheckInternet {\n  @override\n  bool? get internetOnOffSimulation => false; // Simulate offline\n\n  Future<AppState?> reduce() async {\n    // ...\n  }\n}\n```\n\n**Global simulation** - Set `forceInternetOnOffSimulation` on the store:\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n);\nstore.forceInternetOnOffSimulation = false; // All actions see no internet\n```\n\n## Limitations\n\nThese mixins only detect device connectivity status. They cannot verify:\n- Internet provider functionality\n- Server availability\n- API endpoint reachability\n\nFor server-specific connectivity checks, implement additional validation in your action's `reduce()` method or `before()` method.\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/advanced-actions/internet-mixins\n- https://asyncredux.com/flutter/advanced-actions/action-mixins\n- https://asyncredux.com/flutter/advanced-actions/aborting-the-dispatch\n- https://asyncredux.com/flutter/basics/failed-actions\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/advanced-actions/control-mixins\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/basics/wait-fail-succeed\n- https://asyncredux.com/flutter/testing/mocking\n"
  },
  {
    "path": ".claude/skills/asyncredux-connector-pattern/SKILL.md",
    "content": "---\nname: asyncredux-connector-pattern\ndescription: Implement the Connector pattern for separating smart and dumb widgets. Covers creating StoreConnector widgets, implementing VmFactory and Vm classes, building view-models, and optimizing rebuilds with view-model equality.\n---\n\n## Overview\n\nThe connector pattern separates store access logic from UI presentation. Instead of widgets directly accessing the store via `context.state` and `context.dispatch()`, a \"smart\" connector widget extracts store data and passes it to a \"dumb\" presentational widget through constructor parameters.\n\n## Why Use the Connector Pattern?\n\n1. **Testing simplification** - Test UI widgets without creating a Redux store by passing mock data\n2. **Separation of concerns** - UI widgets focus on appearance; connectors handle business logic\n3. **Reusability** - Presentational widgets function independently of Redux\n4. **Code clarity** - Widget code is not cluttered with state access and transformation logic\n5. **Optimized rebuilds** - Only rebuild when the view-model changes\n\n## The Three Components\n\n### 1. ViewModel (Vm)\n\nContains only the data the UI widget requires. Extends `Vm` and lists equality fields:\n\n```dart\nclass CounterViewModel extends Vm {\n  final int counter;\n  final String description;\n  final VoidCallback onIncrement;\n\n  CounterViewModel({\n    required this.counter,\n    required this.description,\n    required this.onIncrement,\n  }) : super(equals: [counter, description]);\n}\n```\n\nThe `equals` list tells AsyncRedux which fields to compare when deciding whether to rebuild. Callbacks (like `onIncrement`) should NOT be included in `equals`.\n\n### 2. VmFactory\n\nTransforms store state into a view-model. Extends `VmFactory` and implements `fromStore()`:\n\n```dart\nclass CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {\n  CounterFactory(connector) : super(connector);\n\n  @override\n  CounterViewModel fromStore() => CounterViewModel(\n    counter: state.counter,\n    description: state.description,\n    onIncrement: () => dispatch(IncrementAction()),\n  );\n}\n```\n\nThe factory has access to:\n- `state` - The store state when the factory was created\n- `dispatch()` - To dispatch actions from callbacks\n- `dispatchSync()` - For synchronous dispatch\n- `connector` - Reference to the parent connector widget\n\n### 3. StoreConnector\n\nBridges the store and UI widget:\n\n```dart\nclass CounterConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, CounterViewModel>(\n      vm: () => CounterFactory(this),\n      builder: (BuildContext context, CounterViewModel vm) => CounterWidget(\n        counter: vm.counter,\n        description: vm.description,\n        onIncrement: vm.onIncrement,\n      ),\n    );\n  }\n}\n```\n\nThe \"dumb\" widget receives data through constructor parameters:\n\n```dart\nclass CounterWidget extends StatelessWidget {\n  final int counter;\n  final String description;\n  final VoidCallback onIncrement;\n\n  const CounterWidget({\n    required this.counter,\n    required this.description,\n    required this.onIncrement,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Text('$counter'),\n        Text(description),\n        ElevatedButton(\n          onPressed: onIncrement,\n          child: Text('Increment'),\n        ),\n      ],\n    );\n  }\n}\n```\n\n## Rebuild Optimization\n\nEach time an action changes the store state, `StoreConnector` compares the new view-model with the previous one. It only rebuilds if they differ (based on the `equals` list).\n\nTo prevent rebuilds even when state changes, use `notify: false`:\n\n```dart\ndispatch(MyAction(), notify: false);\n```\n\n## Advanced Factory Techniques\n\n### Accessing Connector Properties\n\nPass data from the connector widget to the factory:\n\n```dart\nclass UserConnector extends StatelessWidget {\n  final int userId;\n  const UserConnector({required this.userId});\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, UserViewModel>(\n      vm: () => UserFactory(this),\n      builder: (context, vm) => UserWidget(user: vm.user),\n    );\n  }\n}\n\nclass UserFactory extends VmFactory<AppState, UserConnector, UserViewModel> {\n  UserFactory(connector) : super(connector);\n\n  @override\n  UserViewModel fromStore() => UserViewModel(\n    // Access connector.userId here\n    user: state.users.firstWhere((u) => u.id == connector.userId),\n  );\n}\n```\n\n### state vs currentState()\n\nInside the factory:\n- `state` - The state when the factory was created (final, won't change)\n- `currentState()` - The current store state at the moment of the call\n\nThese usually match, but diverge in callbacks after `dispatchSync()`:\n\n```dart\n@override\nUserViewModel fromStore() => UserViewModel(\n  onSave: () {\n    dispatchSync(SaveAction());\n    // state still has old value\n    // currentState() has new value after SaveAction\n  },\n);\n```\n\n### Using the vm Getter in Callbacks\n\nAccess already-computed view-model fields in callbacks to avoid redundant calculations:\n\n```dart\n@override\nUserViewModel fromStore() => UserViewModel(\n  name: state.user.name,\n  onSave: () {\n    // Use vm.name instead of recalculating from state\n    print('Saving user: ${vm.name}');\n    dispatch(SaveAction(vm.name));\n  },\n);\n```\n\n**Note:** The `vm` getter is only available after `fromStore()` completes. Use it in callbacks, not during view-model construction.\n\n### Base Factory Pattern\n\nCreate a base factory to reduce boilerplate:\n\n```dart\nabstract class BaseFactory<T extends StatelessWidget, Model extends Vm>\n    extends VmFactory<AppState, T, Model> {\n  BaseFactory(T connector) : super(connector);\n\n  // Common getters\n  User get user => state.user;\n  Settings get settings => state.settings;\n}\n\nclass MyFactory extends BaseFactory<MyConnector, MyViewModel> {\n  MyFactory(connector) : super(connector);\n\n  @override\n  MyViewModel fromStore() => MyViewModel(\n    user: user,  // Uses inherited getter\n  );\n}\n```\n\n## Nullable View-Models\n\nWhen you cannot generate a valid view-model (e.g., data still loading), return null:\n\n```dart\nclass HomeConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, HomeViewModel?>(  // Nullable type\n      vm: () => HomeFactory(this),\n      builder: (BuildContext context, HomeViewModel? vm) {  // Nullable param\n        return (vm == null)\n          ? Text(\"User not logged in\")\n          : HomePage(user: vm.user);\n      },\n    );\n  }\n}\n\nclass HomeFactory extends VmFactory<AppState, HomeConnector, HomeViewModel?> {\n  HomeFactory(connector) : super(connector);\n\n  @override\n  HomeViewModel? fromStore() {  // Nullable return\n    return (state.user == null)\n      ? null\n      : HomeViewModel(user: state.user!);\n  }\n}\n```\n\n## Migrating from flutter_redux\n\nIf migrating from `flutter_redux`, you can use the `converter` parameter instead of `vm`:\n\n```dart\nclass MyConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel>(\n      converter: (store) => ViewModel.fromStore(store),\n      builder: (context, vm) => MyWidget(name: vm.name),\n    );\n  }\n}\n\nclass ViewModel extends Vm {\n  final String name;\n  final VoidCallback onSave;\n\n  ViewModel({required this.name, required this.onSave})\n    : super(equals: [name]);\n\n  static ViewModel fromStore(Store<AppState> store) {\n    return ViewModel(\n      name: store.state.name,\n      onSave: () => store.dispatch(SaveAction()),\n    );\n  }\n}\n```\n\nNote: `vm` and `converter` are mutually exclusive. The `vm` approach is recommended for new code.\n\n## Debugging Rebuilds\n\nTo observe when connectors rebuild, pass a `modelObserver` to the store:\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  modelObserver: DefaultModelObserver(),\n);\n```\n\nAdd `debug: this` to StoreConnector for connector type names in logs:\n\n```dart\nStoreConnector<AppState, ViewModel>(\n  debug: this,\n  vm: () => Factory(this),\n  builder: (context, vm) => MyWidget(vm: vm),\n);\n```\n\nOverride `toString()` in your ViewModel for custom diagnostic output:\n\n```dart\nclass MyViewModel extends Vm {\n  final int counter;\n  MyViewModel({required this.counter}) : super(equals: [counter]);\n\n  @override\n  String toString() => 'MyViewModel{counter: $counter}';\n}\n```\n\nConsole output shows rebuild information:\n```\nModel D:1 R:1 = Rebuild:true, Connector:MyWidgetConnector, Model:MyViewModel{counter: 5}\n```\n\n## Testing View-Models\n\nUse `Vm.createFrom()` to test view-models in isolation:\n\n```dart\ntest('view-model properties', () {\n  var store = Store<AppState>(initialState: AppState(name: \"Mary\"));\n  var vm = Vm.createFrom(store, MyFactory());\n\n  expect(vm.name, \"Mary\");\n});\n\ntest('view-model callbacks dispatch actions', () async {\n  var store = Store<AppState>(initialState: AppState(name: \"Mary\"));\n  var vm = Vm.createFrom(store, MyFactory());\n\n  vm.onChangeName(\"Bill\");\n  await store.waitActionType(ChangeNameAction);\n  expect(store.state.name, \"Bill\");\n});\n```\n\n**Important:** `Vm.createFrom()` can only be called once per factory instance. Create a new factory for each test.\n\n## Complete Example\n\n```dart\n// State\nclass AppState {\n  final int counter;\n  final String description;\n  AppState({required this.counter, required this.description});\n  AppState copy({int? counter, String? description}) => AppState(\n    counter: counter ?? this.counter,\n    description: description ?? this.description,\n  );\n}\n\n// Action\nclass IncrementAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => state.copy(counter: state.counter + 1);\n}\n\n// View-Model\nclass CounterViewModel extends Vm {\n  final int counter;\n  final String description;\n  final VoidCallback onIncrement;\n\n  CounterViewModel({\n    required this.counter,\n    required this.description,\n    required this.onIncrement,\n  }) : super(equals: [counter, description]);\n}\n\n// Factory\nclass CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {\n  CounterFactory(connector) : super(connector);\n\n  @override\n  CounterViewModel fromStore() => CounterViewModel(\n    counter: state.counter,\n    description: state.description,\n    onIncrement: () => dispatch(IncrementAction()),\n  );\n}\n\n// Connector (Smart Widget)\nclass CounterConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, CounterViewModel>(\n      vm: () => CounterFactory(this),\n      builder: (context, vm) => CounterWidget(\n        counter: vm.counter,\n        description: vm.description,\n        onIncrement: vm.onIncrement,\n      ),\n    );\n  }\n}\n\n// Presentational Widget (Dumb Widget)\nclass CounterWidget extends StatelessWidget {\n  final int counter;\n  final String description;\n  final VoidCallback onIncrement;\n\n  const CounterWidget({\n    required this.counter,\n    required this.description,\n    required this.onIncrement,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        Text('$counter', style: TextStyle(fontSize: 48)),\n        Text(description),\n        SizedBox(height: 20),\n        ElevatedButton(\n          onPressed: onIncrement,\n          child: Text('Increment'),\n        ),\n      ],\n    );\n  }\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/connector/connector-pattern\n- https://asyncredux.com/flutter/connector/store-connector\n- https://asyncredux.com/flutter/connector/advanced-view-model\n- https://asyncredux.com/flutter/connector/cannot-generate-view-model\n- https://asyncredux.com/flutter/connector/migrating-from-flutter-redux\n- https://asyncredux.com/flutter/testing/testing-the-view-model\n- https://asyncredux.com/flutter/basics/using-the-store-state\n- https://asyncredux.com/flutter/miscellaneous/observing-rebuilds\n"
  },
  {
    "path": ".claude/skills/asyncredux-debounce-mixin/SKILL.md",
    "content": "---\nname: asyncredux-debounce-mixin\ndescription: Add the Debounce mixin to wait for user input pauses before acting. Covers setting the `debounce` duration, implementing search-as-you-type, and avoiding excessive API calls during rapid input.\n---\n\n# Debounce Mixin\n\nThe `Debounce` mixin delays action execution until after a period of inactivity. Each new dispatch resets the timer, so the action only runs once dispatching stops for the specified duration. This is ideal for search-as-you-type functionality and avoiding excessive API calls during rapid user input.\n\n## Basic Usage\n\nAdd the `Debounce` mixin to your action class:\n\n```dart\nclass SearchText extends AppAction with Debounce {\n  final String searchTerm;\n  SearchText(this.searchTerm);\n\n  Future<AppState?> reduce() async {\n    var response = await http.get(\n      Uri.parse('https://example.com/?q=${Uri.encodeComponent(searchTerm)}')\n    );\n    return state.copy(searchResult: response.body);\n  }\n}\n```\n\nWhen the user types quickly, each keystroke dispatches `SearchText`. The mixin delays execution, and each new dispatch resets the timer. The API call only happens once the user stops typing for the debounce period.\n\n## Setting the Debounce Duration\n\nThe default debounce period is **333 milliseconds**. Override the `debounce` getter to customize:\n\n```dart\nclass SearchText extends AppAction with Debounce {\n  final String searchTerm;\n  SearchText(this.searchTerm);\n\n  // Wait 1 second of inactivity before executing\n  int get debounce => 1000;\n\n  Future<AppState?> reduce() async {\n    var response = await http.get(\n      Uri.parse('https://example.com/?q=${Uri.encodeComponent(searchTerm)}')\n    );\n    return state.copy(searchResult: response.body);\n  }\n}\n```\n\n## Custom Lock Builder\n\nBy default, all instances of a debounced action share the same lock. Override `lockBuilder()` to create independent debounce periods for different action instances:\n\n```dart\nclass SearchField extends AppAction with Debounce {\n  final String fieldId;\n  final String searchTerm;\n  SearchField(this.fieldId, this.searchTerm);\n\n  // Each fieldId gets its own independent debounce timer\n  Object? lockBuilder() => fieldId;\n\n  Future<AppState?> reduce() async {\n    // Search logic here\n  }\n}\n```\n\nThis enables multiple search fields to operate independently, each with their own debounce timer.\n\n## Debounce vs Throttle\n\nThese two mixins serve different purposes:\n\n| Mixin | Behavior | Best For |\n|-------|----------|----------|\n| **Throttle** | Runs immediately on first dispatch, then blocks subsequent dispatches for the period | Rate-limiting actions that should execute right away (e.g., refresh button) |\n| **Debounce** | Waits for quiet time, only runs after dispatches stop | Waiting for user to finish input (e.g., search-as-you-type) |\n\n**Throttle**: \"Execute now, then wait before allowing again\"\n**Debounce**: \"Wait until activity stops, then execute\"\n\n## Mixin Compatibility\n\nDebounce **can** be combined with:\n- `CheckInternet`\n- `NoDialog`\n- `AbortWhenNoInternet`\n- `NonReentrant`\n- `Fresh`\n- `Throttle`\n\nDebounce **cannot** be combined with:\n- `Retry`\n- `UnlimitedRetries`\n- `UnlimitedRetryCheckInternet`\n- `OptimisticCommand`\n- `OptimisticSync`\n- `OptimisticSyncWithPush`\n- `ServerPush`\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/advanced-actions/action-mixins\n- https://asyncredux.com/flutter/advanced-actions/control-mixins\n- https://asyncredux.com/flutter/advanced-actions/control-mixins#debounce\n- https://asyncredux.com/flutter/advanced-actions/control-mixins#throttle\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/basics/actions-and-reducers\n- https://asyncredux.com/flutter/basics/events\n- https://asyncredux.com/flutter/advanced-actions/action-mixins#compatibility\n"
  },
  {
    "path": ".claude/skills/asyncredux-debugging/SKILL.md",
    "content": "---\nname: asyncredux-debugging\ndescription: Debug AsyncRedux applications effectively. Covers printing state with store.state, checking actionsInProgress(), using ConsoleActionObserver, StateObserver for state change tracking, and tracking dispatchCount/reduceCount.\n---\n\n# Debugging AsyncRedux Applications\n\nAsyncRedux provides several tools for debugging and monitoring your application's state, actions, and behavior during development.\n\n## Inspecting Store State\n\nAccess the current state directly from the store:\n\n```dart\n// Direct state access\nprint(store.state);\n\n// Access specific parts\nprint(store.state.user.name);\nprint(store.state.cart.items);\n```\n\n## Tracking Actions in Progress\n\nUse `actionsInProgress()` to see which actions are currently being processed:\n\n```dart\n// Returns an unmodifiable Set of actions currently running\nSet<ReduxAction<AppState>> inProgress = store.actionsInProgress();\n\n// Check if any actions are running\nif (inProgress.isEmpty) {\n  print('No actions in progress');\n} else {\n  for (var action in inProgress) {\n    print('Running: ${action.runtimeType}');\n  }\n}\n\n// Get a copy of actions in progress\nSet<ReduxAction<AppState>> copy = store.copyActionsInProgress();\n\n// Check if specific actions match\nbool matches = store.actionsInProgressEqualTo(expectedSet);\n```\n\n## Dispatch and Reduce Counts\n\nTrack how many actions have been dispatched and how many state reductions have occurred:\n\n```dart\n// Total actions dispatched since store creation\nprint('Dispatch count: ${store.dispatchCount}');\n\n// Total state reductions performed\nprint('Reduce count: ${store.reduceCount}');\n```\n\nThese counters are useful for:\n- Verifying actions dispatched during tests\n- Detecting unexpected dispatches\n- Performance monitoring\n\n## Console Action Observer\n\nThe built-in `ConsoleActionObserver` prints dispatched actions to the console with color formatting:\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  // Only enable in debug mode\n  actionObservers: kReleaseMode ? null : [ConsoleActionObserver()],\n);\n```\n\nConsole output example:\n```\nI/flutter (15304): | Action MyAction\nI/flutter (15304): | Action LoadUserAction(user32)\n```\n\nActions appear in yellow (default) or green (for `WaitAction` and `NavigateAction`).\n\n### Customizing Action Output\n\nOverride `toString()` in your actions to display additional information:\n\n```dart\nclass LoginAction extends AppAction {\n  final String username;\n  LoginAction(this.username);\n\n  @override\n  Future<AppState?> reduce() async {\n    // ...\n  }\n\n  @override\n  String toString() => 'LoginAction(username: $username)';\n}\n```\n\n### Custom Color Scheme\n\nCustomize the color scheme by modifying the static `color` callback:\n\n```dart\nConsoleActionObserver.color = (action) {\n  if (action is ErrorAction) return ConsoleActionObserver.red;\n  if (action is NetworkAction) return ConsoleActionObserver.blue;\n  return ConsoleActionObserver.yellow;\n};\n```\n\nAvailable colors: `white`, `red`, `blue`, `yellow`, `green`, `grey`, `dark`.\n\n## StateObserver for State Change Logging\n\nCreate a `StateObserver` to log state changes:\n\n```dart\nclass DebugStateObserver implements StateObserver<AppState> {\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    AppState prevState,\n    AppState newState,\n    Object? error,\n    int dispatchCount,\n  ) {\n    final changed = !identical(prevState, newState);\n\n    print('--- Action #$dispatchCount: ${action.runtimeType} ---');\n    print('State changed: $changed');\n\n    if (changed) {\n      // Log specific state changes\n      if (prevState.user != newState.user) {\n        print('  User changed: ${prevState.user} -> ${newState.user}');\n      }\n      if (prevState.counter != newState.counter) {\n        print('  Counter changed: ${prevState.counter} -> ${newState.counter}');\n      }\n    }\n\n    if (error != null) {\n      print('  Error: $error');\n    }\n  }\n}\n\n// Configure store\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  stateObservers: kDebugMode ? [DebugStateObserver()] : null,\n);\n```\n\n### Detecting State Changes\n\nUse `identical()` to check if state actually changed:\n\n```dart\nbool stateChanged = !identical(prevState, newState);\n```\n\nThis is efficient because AsyncRedux uses immutable state - if the reference is the same, no change occurred.\n\n## Custom ActionObserver for Detailed Logging\n\nCreate an `ActionObserver` for detailed dispatch tracking:\n\n```dart\nclass DetailedActionObserver implements ActionObserver<AppState> {\n  final Map<ReduxAction, DateTime> _startTimes = {};\n\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    int dispatchCount, {\n    required bool ini,\n  }) {\n    if (ini) {\n      // Action started\n      _startTimes[action] = DateTime.now();\n      print('[START #$dispatchCount] ${action.runtimeType}');\n    } else {\n      // Action finished\n      final startTime = _startTimes.remove(action);\n      if (startTime != null) {\n        final duration = DateTime.now().difference(startTime);\n        print('[END #$dispatchCount] ${action.runtimeType} (${duration.inMilliseconds}ms)');\n      } else {\n        print('[END #$dispatchCount] ${action.runtimeType}');\n      }\n    }\n  }\n}\n```\n\n## Debugging Widget Rebuilds\n\nUse `ModelObserver` with `DefaultModelObserver` to track which widgets rebuild:\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  modelObserver: DefaultModelObserver(),\n);\n```\n\nOutput format:\n```\nModel D:1 R:1 = Rebuild:true, Connector:MyWidgetConnector, Model:MyViewModel{data}.\nModel D:2 R:2 = Rebuild:false, Connector:MyWidgetConnector, Model:MyViewModel{data}.\n```\n\n- `D`: Dispatch count\n- `R`: Rebuild count\n- `Rebuild`: Whether widget actually rebuilt\n- `Connector`: The StoreConnector type\n- `Model`: ViewModel with state summary\n\nEnable detailed output by passing `debug: this` to StoreConnector:\n\n```dart\nStoreConnector<AppState, MyViewModel>(\n  debug: this, // Enables connector name in output\n  converter: (store) => MyViewModel.fromStore(store),\n  builder: (context, vm) => MyWidget(vm),\n)\n```\n\n## Checking Action Status in Widgets\n\nUse context extensions to check action states:\n\n```dart\nWidget build(BuildContext context) {\n  // Check if action is currently running\n  if (context.isWaiting(LoadDataAction)) {\n    return CircularProgressIndicator();\n  }\n\n  // Check if action failed\n  if (context.isFailed(LoadDataAction)) {\n    var exception = context.exceptionFor(LoadDataAction);\n    return Text('Error: ${exception?.message}');\n  }\n\n  return Text('Data: ${context.state.data}');\n}\n```\n\n## Waiting for Conditions in Tests\n\nUse store wait methods for test debugging:\n\n```dart\n// Wait until state meets a condition\nawait store.waitCondition((state) => state.isLoaded);\n\n// Wait for specific action types to complete\nawait store.waitAllActionTypes([LoadUserAction, LoadSettingsAction]);\n\n// Wait for all actions to complete (empty list = wait for all)\nawait store.waitAllActions([]);\n\n// Wait for action condition with access to actions in progress\nawait store.waitActionCondition((actionsInProgress, triggerAction) {\n  return actionsInProgress.isEmpty;\n});\n```\n\n## Complete Debug Setup Example\n\n```dart\nvoid main() {\n  final store = Store<AppState>(\n    initialState: AppState.initialState(),\n    // Action logging (debug only)\n    actionObservers: kDebugMode\n        ? [ConsoleActionObserver(), DetailedActionObserver()]\n        : null,\n    // State change logging (debug only)\n    stateObservers: kDebugMode\n        ? [DebugStateObserver()]\n        : null,\n    // Widget rebuild tracking (debug only)\n    modelObserver: kDebugMode ? DefaultModelObserver() : null,\n    // Error observer (always enabled)\n    errorObserver: MyErrorObserver(),\n  );\n\n  // Debug print initial state\n  if (kDebugMode) {\n    print('Initial state: ${store.state}');\n    print('Dispatch count: ${store.dispatchCount}');\n  }\n\n  runApp(StoreProvider<AppState>(\n    store: store,\n    child: MyApp(),\n  ));\n}\n```\n\n## Debugging Tips\n\n1. **Print state in actions**: Use `print(state)` in your reducer to see state at that moment\n2. **Check initialState**: Access `action.initialState` to see state when action was dispatched (vs current `state`)\n3. **Use action status**: Check `action.status.isCompletedOk` or `action.status.originalError` after dispatch\n4. **Conditional logging**: Use `kDebugMode` from `package:flutter/foundation.dart` to disable in production\n5. **Override toString**: Implement `toString()` on actions and state classes for better debug output\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/miscellaneous/logging\n- https://asyncredux.com/flutter/miscellaneous/metrics\n- https://asyncredux.com/flutter/miscellaneous/observing-rebuilds\n- https://asyncredux.com/flutter/basics/store\n- https://asyncredux.com/flutter/basics/dispatching-actions\n- https://asyncredux.com/flutter/basics/wait-fail-succeed\n- https://asyncredux.com/flutter/advanced-actions/action-status\n- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect\n"
  },
  {
    "path": ".claude/skills/asyncredux-dependency-injection/SKILL.md",
    "content": "---\nname: asyncredux-dependency-injection\ndescription: Inject dependencies into actions using the environment, dependencies, and configuration pattern. Covers creating an Environment enum, a Dependencies class, passing them to the Store, accessing them from actions and widgets, and using dependency injection for testability.\n---\n\n# Dependency Injection with Environment, Dependencies, and Configuration\n\nAsyncRedux provides dependency injection through three Store parameters:\n\n- **`environment`**: Specifies if the app is running in production, staging, development, testing, etc. Should be immutable and not change during app execution. Accessible from both actions and widgets.\n- **`dependencies`**: A container for injected services (repositories, APIs, etc.), created via a factory that receives the `Store`, so it can vary based on the environment. Usually not accessible from widgets.\n- **`configuration`**: For feature flags and other configuration values. Accessible from both actions and widgets.\n\n## Step 1: Define the Environment\n\nCreate an enum (or class) specifying the app's running context:\n\n```dart\nenum Environment {\n  production,\n  staging,\n  testing;\n\n  bool get isProduction => this == Environment.production;\n  bool get isStaging => this == Environment.staging;\n  bool get isTesting => this == Environment.testing;\n}\n```\n\n## Step 2: Define the Dependencies\n\nCreate an abstract class with a factory that returns different implementations based on the environment:\n\n```dart\nabstract class Dependencies {\n  factory Dependencies(Store store) {\n    if (store.environment == Environment.production) {\n      return DependenciesProduction();\n    } else if (store.environment == Environment.staging) {\n      return DependenciesStaging();\n    } else {\n      return DependenciesTesting();\n    }\n  }\n\n  ApiClient get apiClient;\n  AuthService get authService;\n  int limit(int value);\n}\n\nclass DependenciesProduction implements Dependencies {\n  @override\n  ApiClient get apiClient => RealApiClient();\n\n  @override\n  AuthService get authService => FirebaseAuthService();\n\n  @override\n  int limit(int value) => min(value, 5);\n}\n\nclass DependenciesTesting implements Dependencies {\n  @override\n  ApiClient get apiClient => MockApiClient();\n\n  @override\n  AuthService get authService => MockAuthService();\n\n  @override\n  int limit(int value) => min(value, 1000); // Higher limit in tests\n}\n```\n\n## Step 3: Define the Configuration (optional)\n\n```dart\nclass Config {\n  bool isABtestingOn = false;\n  bool showAdminConsole = false;\n}\n```\n\n## Step 4: Pass All Three to the Store\n\nWhen creating the store, pass the environment, dependencies factory, and configuration factory:\n\n```dart\nvoid main() {\n  var store = Store<AppState>(\n    initialState: AppState.initialState(),\n    environment: Environment.production,\n    dependencies: (store) => Dependencies(store),\n    configuration: (store) => Config(),\n  );\n\n  runApp(\n    StoreProvider<AppState>(\n      store: store,\n      child: MyApp(),\n    ),\n  );\n}\n```\n\nThe `dependencies` and `configuration` parameters are factories that receive the `Store`, so they can read `store.environment` to vary their behavior.\n\n## Step 5: Access from Actions via a Base Action Class\n\nDefine a base action class with typed getters for `dependencies`, `environment`, and `configuration`:\n\n```dart\nabstract class Action extends ReduxAction<AppState> {\n  Dependencies get dependencies => super.store.dependencies as Dependencies;\n  Environment get environment => super.store.environment as Environment;\n  Config get config => super.store.configuration as Config;\n}\n```\n\nNow use them in your actions:\n\n```dart\nclass FetchUserAction extends Action {\n  final String userId;\n  FetchUserAction(this.userId);\n\n  @override\n  Future<AppState?> reduce() async {\n    final user = await dependencies.apiClient.fetchUser(userId);\n    return state.copy(user: user);\n  }\n}\n\nclass IncrementAction extends Action {\n  final int amount;\n  IncrementAction({required this.amount});\n\n  @override\n  AppState reduce() {\n    int newState = state.counter + amount;\n    int limitedState = dependencies.limit(newState);\n    return state.copy(counter: limitedState);\n  }\n}\n```\n\n## Step 6: Access from Widgets via BuildContext Extension\n\nCreate a `BuildContext` extension. The `environment` and `configuration` are available via `getEnvironment` and `getConfiguration`. Note: `dependencies` should usually NOT be accessed from widgets.\n\n```dart\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  /// Access the environment from widgets (does not trigger rebuilds).\n  Environment get environment => getEnvironment<AppState>() as Environment;\n\n  /// Access the configuration from widgets (does not trigger rebuilds).\n  Config get config => getConfiguration<AppState>() as Config;\n}\n```\n\nUse in widgets:\n\n```dart\nclass MyHomePage extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    final env = context.environment;\n    int counter = context.state;\n\n    return Scaffold(\n      appBar: AppBar(title: const Text('Dependency Injection Example')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            // Use the environment to change the UI.\n            Text('Running in ${env}.', textAlign: TextAlign.center),\n            Text('$counter', style: const TextStyle(fontSize: 30)),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () => dispatch(IncrementAction(amount: 1)),\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n```\n\n## Step 7 (if using StoreConnector): Access from VmFactory\n\nIf you use `StoreConnector`, extend `VmFactory` with typed getters:\n\n```dart\nabstract class AppFactory<T extends Widget?, Model extends Vm>\n    extends VmFactory<AppState, T, Model> {\n  AppFactory([T? connector]) : super(connector);\n\n  Dependencies get dependencies => store.dependencies as Dependencies;\n  Environment get environment => store.environment as Environment;\n  Config get config => store.configuration as Config;\n}\n```\n\n## Testing with Different Environments\n\nThe pattern makes testing straightforward by injecting test implementations:\n\n```dart\nvoid main() {\n  group('IncrementAction', () {\n    test('increments counter with test dependencies', () async {\n      var store = Store<AppState>(\n        initialState: AppState(counter: 0),\n        environment: Environment.testing,\n        dependencies: (store) => Dependencies(store), // Returns DependenciesTesting\n      );\n\n      await store.dispatchAndWait(IncrementAction(amount: 5));\n\n      // DependenciesTesting has limit of 1000, so value is 5\n      expect(store.state.counter, 5);\n    });\n\n    test('production dependencies limit counter', () async {\n      var store = Store<AppState>(\n        initialState: AppState(counter: 3),\n        environment: Environment.production,\n        dependencies: (store) => Dependencies(store), // Returns DependenciesProduction\n      );\n\n      await store.dispatchAndWait(IncrementAction(amount: 10));\n\n      // DependenciesProduction limits to 5\n      expect(store.state.counter, 5);\n    });\n  });\n}\n```\n\n## Complete Working Example\n\n```dart\nimport 'dart:math';\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\nlate Store<int> store;\n\nvoid main() {\n  store = Store<int>(\n    initialState: 0,\n    environment: Environment.production,\n    dependencies: (store) => Dependencies(store),\n  );\n  runApp(MyApp());\n}\n\nenum Environment {\n  production,\n  staging,\n  testing;\n\n  bool get isProduction => this == Environment.production;\n  bool get isStaging => this == Environment.staging;\n  bool get isTesting => this == Environment.testing;\n}\n\nabstract class Dependencies {\n  factory Dependencies(Store store) {\n    if (store.environment == Environment.production) {\n      return DependenciesProduction();\n    } else if (store.environment == Environment.staging) {\n      return DependenciesStaging();\n    } else {\n      return DependenciesTesting();\n    }\n  }\n\n  int limit(int value);\n}\n\nclass DependenciesProduction implements Dependencies {\n  @override\n  int limit(int value) => min(value, 5);\n}\n\nclass DependenciesStaging implements Dependencies {\n  @override\n  int limit(int value) => min(value, 25);\n}\n\nclass DependenciesTesting implements Dependencies {\n  @override\n  int limit(int value) => min(value, 1000);\n}\n\nabstract class Action extends ReduxAction<int> {\n  Dependencies get dependencies => super.store.dependencies as Dependencies;\n}\n\nclass IncrementAction extends Action {\n  final int amount;\n  IncrementAction({required this.amount});\n\n  @override\n  int reduce() {\n    int newState = state + amount;\n    int limitedState = dependencies.limit(newState);\n    return limitedState;\n  }\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<int>(\n      store: store,\n      child: MaterialApp(home: MyHomePage()),\n    );\n  }\n}\n\nclass MyHomePage extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    final env = context.environment;\n    int counter = context.state;\n\n    return Scaffold(\n      appBar: AppBar(title: const Text('Dependency Injection Example')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            Text('Running in ${env}.', textAlign: TextAlign.center),\n            const Text(\n              'You have pushed the button this many times:\\n'\n              '(limited by the environment)',\n              textAlign: TextAlign.center,\n            ),\n            Text('$counter', style: const TextStyle(fontSize: 30)),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () => dispatch(IncrementAction(amount: 1)),\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n\nextension BuildContextExtension on BuildContext {\n  int get state => getState<int>();\n  int read() => getRead<int>();\n  R select<R>(R Function(int state) selector) => getSelect<int, R>(selector);\n  R? event<R>(Evt<R> Function(int state) selector) => getEvent<int, R>(selector);\n  Environment get environment => getEnvironment<int>() as Environment;\n}\n```\n\n## Key Benefits\n\n- **Separation of concerns**: `environment` identifies the running context, `dependencies` provides services, `configuration` holds feature flags\n- **Testability**: Swap implementations by changing the environment, without changing action code\n- **Type safety**: Typed getters in base action class provide compile-time checking\n- **Factory pattern**: The `dependencies` and `configuration` factories receive the `Store`, allowing them to vary based on `environment`\n- **Scoped dependencies**: Each store instance has its own environment/dependencies/configuration, preventing test contamination\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/miscellaneous/dependency-injection\n- https://asyncredux.com/flutter/testing/mocking\n- https://asyncredux.com/flutter/basics/store\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/connector/store-connector\n- https://asyncredux.com/flutter/testing/store-tester\n- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect\n- https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_dependency_injection.dart\n"
  },
  {
    "path": ".claude/skills/asyncredux-dispatching-actions/SKILL.md",
    "content": "---\nname: asyncredux-dispatching-actions\ndescription: Dispatch actions using all available methods: `dispatch()`, `dispatchAndWait()`, `dispatchAll()`, `dispatchAndWaitAll()`, and `dispatchSync()`. Covers dispatching from widgets via context extensions and from within other actions.\n---\n\n# Dispatching Actions\n\nThe foundational principle of AsyncRedux: **the only way to change the application state is by dispatching actions.** You can dispatch from widgets (via context extensions) or from within other actions.\n\n## Five Dispatch Methods\n\n### 1. dispatch()\n\nThe standard method that returns immediately. For synchronous actions, state updates before return; for async actions, the process begins and completes later.\n\n```dart\ndispatch(MyAction());\n```\n\n### 2. dispatchAndWait()\n\nReturns a `Future` that completes when the action finishes and state changes, regardless of whether the action is sync or async. Returns an `ActionStatus` object.\n\n```dart\nvar status = await dispatchAndWait(MyAction());\nif (status.isCompletedOk) {\n  Navigator.pop(context);\n}\n```\n\n### 3. dispatchAll()\n\nDispatches multiple actions in parallel, returning the list of dispatched actions.\n\n```dart\ndispatchAll([BuyAction('IBM'), SellAction('TSLA')]);\n```\n\n### 4. dispatchAndWaitAll()\n\nDispatches actions in parallel and waits for all to complete.\n\n```dart\nawait dispatchAndWaitAll([\n  BuyAction('IBM'),\n  SellAction('TSLA'),\n]);\n```\n\n### 5. dispatchSync()\n\nLike `dispatch()` but throws a `StoreException` if the action is asynchronous. Use when synchronous execution is mandatory.\n\n```dart\ndispatchSync(MyAction());\n```\n\n## Dispatching from Widgets\n\nAll dispatch methods are available as `BuildContext` extensions:\n\n```dart\ncontext.dispatch(Action());\ncontext.dispatchAll([Action1(), Action2()]);\nawait context.dispatchAndWait(Action());\nawait context.dispatchAndWaitAll([Action1(), Action2()]);\ncontext.dispatchSync(Action());\n```\n\nExample button implementation:\n\n```dart\nElevatedButton(\n  onPressed: () => context.dispatch(Increment()),\n  child: Text('Increment'),\n)\n```\n\nFor async dispatch in callbacks:\n\n```dart\nElevatedButton(\n  onPressed: () async {\n    var status = await context.dispatchAndWait(SaveAction());\n    if (status.isCompletedOk) {\n      Navigator.pop(context);\n    }\n  },\n  child: Text('Save'),\n)\n```\n\n## Dispatching from Within Actions\n\nAll dispatch methods are available inside actions via the `ReduxAction` base class:\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  Future<AppState?> reduce() async {\n    // Dispatch another action and wait for it\n    await dispatchAndWait(LoadDataAction());\n\n    // Dispatch without waiting\n    dispatch(LogAction('Data loaded'));\n\n    return state.copy(loaded: true);\n  }\n}\n```\n\n### Dispatching in before() and after()\n\nYou can dispatch actions in the `before()` and `after()` lifecycle methods:\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  Future<AppState?> reduce() async {\n    String description = await fetchData();\n    return state.copy(description: description);\n  }\n\n  void before() => dispatch(BarrierAction(true));\n  void after() => dispatch(BarrierAction(false));\n}\n```\n\n## ActionStatus\n\nThe `dispatchAndWait()` method returns an `ActionStatus` object with useful properties:\n\n```dart\nvar status = await dispatchAndWait(MyAction());\n\n// Check completion state\nstatus.isCompleted;       // Action finished executing\nstatus.isCompletedOk;     // Completed without errors\nstatus.isCompletedFailed; // Completed with errors\n\n// Access error information\nstatus.originalError;     // Error thrown by before/reduce\nstatus.wrappedError;      // Error after wrapError() processing\n\n// Check method completion\nstatus.hasFinishedMethodBefore;\nstatus.hasFinishedMethodReduce;\nstatus.hasFinishedMethodAfter;\n```\n\nYou can also access status directly from the action instance:\n\n```dart\nvar action = MyAction();\nawait dispatchAndWait(action);\nprint(action.status.isCompletedOk);\n```\n\n## The notify Parameter\n\nDispatch methods accept an optional `notify` parameter (default `true`) that controls whether widgets rebuild on state changes:\n\n```dart\n// Dispatch without triggering widget rebuilds\ndispatch(MyAction(), notify: false);\n```\n\n## Summary Table\n\n| Method | Returns | Waits? | Use Case |\n|--------|---------|--------|----------|\n| `dispatch()` | `void` | No | Fire and forget |\n| `dispatchAndWait()` | `Future<ActionStatus>` | Yes | Need to know when done |\n| `dispatchAll()` | `List<ReduxAction>` | No | Multiple parallel actions |\n| `dispatchAndWaitAll()` | `Future<void>` | Yes | Wait for all parallel actions |\n| `dispatchSync()` | `void` | N/A | Enforce sync execution |\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/basics/dispatching-actions\n- https://asyncredux.com/flutter/basics/using-the-store-state\n- https://asyncredux.com/flutter/basics/sync-actions\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/basics/store\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/advanced-actions/action-status\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect\n- https://asyncredux.com/flutter/miscellaneous/advanced-waiting\n"
  },
  {
    "path": ".claude/skills/asyncredux-error-handling/SKILL.md",
    "content": "---\nname: asyncredux-error-handling\ndescription: Implement comprehensive error handling for actions. Covers the `wrapError()` method for action-level error wrapping, GlobalWrapError for app-wide error transformation, ErrorObserver for logging/monitoring, and the error handling flow (before → reduce → after).\n---\n\n# Error Handling in AsyncRedux\n\nAsyncRedux provides a comprehensive error handling system with multiple layers: action-level wrapping, global error transformation, and error observation for logging/monitoring.\n\n## Error Flow and Action Lifecycle\n\nWhen errors occur during action execution:\n\n1. If `before()` throws an error, the reducer doesn't execute and state remains unchanged\n2. If `reduce()` throws an error, execution halts without state modification\n3. The `after()` method **always** runs, even when errors occur (like a `finally` block)\n\n**Processing order:** `wrapError()` → `GlobalWrapError` → `ErrorObserver`\n\n## Throwing Errors from Actions\n\nActions can throw errors using `throw`. When an error is thrown, the reducer stops and state is not modified:\n\n```dart\nclass TransferMoney extends AppAction {\n  final double amount;\n  TransferMoney(this.amount);\n\n  AppState? reduce() {\n    if (amount == 0) {\n      throw UserException('You cannot transfer zero money.');\n    }\n    return state.copy(cash: state.cash - amount);\n  }\n}\n```\n\n## UserException for User-Facing Errors\n\n`UserException` is a built-in class for errors that users can understand and potentially fix (not code bugs):\n\n```dart\nclass SaveUser extends AppAction {\n  final String name;\n  SaveUser(this.name);\n\n  Future<AppState?> reduce() async {\n    if (name.length < 4)\n      throw UserException('Name must have 4 letters.');\n\n    await saveUser(name);\n    return null;\n  }\n}\n```\n\nWhen a `UserException` is thrown, it's added to a special error queue in the store and can be displayed via `UserExceptionDialog`.\n\n### Displaying UserExceptions\n\nWrap your home page with `UserExceptionDialog` below both `StoreProvider` and `MaterialApp`:\n\n```dart\nUserExceptionDialog<AppState>(\n  onShowUserExceptionDialog: (context, exception) => showDialog(...),\n  child: MyHomePage(),\n)\n```\n\n## Action-Level Error Wrapping with wrapError()\n\nThe `wrapError()` method acts as a catch block for entire actions. It receives the original error and stack trace, and must return:\n- A modified error (to transform the error)\n- `null` (to suppress/disable the error)\n- The unchanged error (to pass it through)\n\n```dart\nclass LogoutAction extends AppAction {\n  @override\n  Object? wrapError(Object error, StackTrace stackTrace) {\n    return LogoutError(\"Logout failed\", cause: error);\n  }\n\n  Future<AppState?> reduce() async {\n    await authService.logout();\n    return state.copy(user: null);\n  }\n}\n```\n\n### Mixin Pattern for Reusable Error Handling\n\nCreate mixins for consistent error transformation across multiple actions:\n\n```dart\nmixin ShowUserException on AppAction {\n  String getErrorMessage();\n\n  @override\n  Object? wrapError(Object error, StackTrace stackTrace) {\n    return UserException(getErrorMessage()).addCause(error);\n  }\n}\n\nclass LoadDataAction extends AppAction with ShowUserException {\n  @override\n  String getErrorMessage() => 'Failed to load data. Please try again.';\n\n  Future<AppState?> reduce() async {\n    var data = await api.loadData();\n    return state.copy(data: data);\n  }\n}\n```\n\n### Suppressing Errors\n\nReturn `null` from `wrapError()` to suppress errors without further propagation:\n\n```dart\n@override\nObject? wrapError(Object error, StackTrace stackTrace) {\n  if (error is CancelledException) {\n    return null; // Silently ignore cancellation\n  }\n  return error;\n}\n```\n\n## Global Error Handling with GlobalWrapError\n\n`GlobalWrapError` processes all action errors centrally. This is useful for transforming third-party library errors (like Firebase or platform exceptions):\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  globalWrapError: MyGlobalWrapError(),\n);\n\nclass MyGlobalWrapError extends GlobalWrapError {\n  @override\n  Object? wrap(Object error, StackTrace stackTrace, ReduxAction<AppState> action) {\n    // Transform platform exceptions to user-friendly messages\n    if (error is PlatformException && error.code == \"Error performing get\") {\n      return UserException('Check your internet connection').addCause(error);\n    }\n\n    // Transform Firebase errors\n    if (error is FirebaseException) {\n      return UserException('Service temporarily unavailable').addCause(error);\n    }\n\n    // Pass through all other errors unchanged\n    return error;\n  }\n}\n```\n\nReturn `null` from `GlobalWrapError.wrap()` to suppress errors globally.\n\n## Error Observation with ErrorObserver\n\n`ErrorObserver` receives all errors with context about the action and store. Use it for logging, monitoring, or analytics:\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  errorObserver: MyErrorObserver<AppState>(),\n);\n\nclass MyErrorObserver<St> implements ErrorObserver<St> {\n  @override\n  bool observe(\n    Object error,\n    StackTrace stackTrace,\n    ReduxAction<St> action,\n    Store<St> store,\n  ) {\n    // Log the error\n    print(\"Error during ${action.runtimeType}: $error\");\n\n    // Send to crash reporting service\n    crashlytics.recordError(error, stackTrace);\n\n    // Return true to rethrow, false to swallow\n    return true;\n  }\n}\n```\n\nThe `observe` method returns:\n- `true` to rethrow the error (default behavior)\n- `false` to swallow the error silently\n\n## UserExceptionAction for Mid-Action Errors\n\nFor showing error feedback while allowing the action to continue (without stopping execution):\n\n```dart\nclass ConvertAction extends AppAction {\n  final String text;\n  ConvertAction(this.text);\n\n  Future<AppState?> reduce() async {\n    var value = int.tryParse(text);\n    if (value == null) {\n      // Show error but continue action\n      dispatch(UserExceptionAction('Please enter a valid number'));\n      return null; // No state change\n    }\n    return state.copy(counter: value);\n  }\n}\n```\n\n## Checking Action Failure Status\n\n### Using ActionStatus\n\nAfter dispatching with `dispatchAndWait()`, check the status:\n\n```dart\nvar status = await store.dispatchAndWait(SaveAction());\n\nif (status.isCompletedOk) {\n  Navigator.pop(context);\n} else if (status.isCompletedFailed) {\n  var error = status.wrappedError;\n  print('Save failed: $error');\n}\n```\n\n**ActionStatus properties:**\n- `isCompletedOk`: Action finished without errors\n- `isCompletedFailed`: Action encountered errors\n- `originalError`: The error as thrown from `before` or `reduce`\n- `wrappedError`: The error after transformation by `wrapError()`\n\n### Using isFailed in Widgets\n\nCheck action failure state in the UI:\n\n```dart\nWidget build(BuildContext context) {\n  if (context.isFailed(LoadDataAction)) {\n    var exception = context.exceptionFor(LoadDataAction);\n    return Column(\n      children: [\n        Text('Error: ${exception?.message}'),\n        ElevatedButton(\n          onPressed: () => context.dispatch(LoadDataAction()),\n          child: Text('Retry'),\n        ),\n      ],\n    );\n  }\n\n  if (context.isWaiting(LoadDataAction)) {\n    return CircularProgressIndicator();\n  }\n\n  return DataWidget(data: context.state.data);\n}\n```\n\nThe error is cleared automatically when the action is dispatched again.\n\nTo manually clear the error:\n```dart\ncontext.clearExceptionFor(LoadDataAction);\n```\n\n## Testing Error Handling\n\nTest that actions fail with expected errors:\n\n```dart\ntest('action throws UserException for invalid input', () async {\n  var store = Store<AppState>(initialState: AppState.initialState());\n\n  var status = await store.dispatchAndWait(SaveUser('abc')); // too short\n\n  expect(status.isCompletedFailed, isTrue);\n  var error = status.wrappedError;\n  expect(error, isA<UserException>());\n  expect((error as UserException).msg, 'Name must have 4 letters.');\n});\n```\n\nTest multiple exceptions via the error queue:\n\n```dart\ntest('multiple actions accumulate errors', () async {\n  var store = Store<AppState>(initialState: AppState.initialState());\n\n  await store.dispatchAndWaitAll([\n    InvalidAction1(),\n    InvalidAction2(),\n    InvalidAction3(),\n  ]);\n\n  var errors = store.errors;\n  expect(errors.length, 3);\n  expect(errors[0].msg, 'First error message');\n});\n```\n\n## Complete Store Setup with Error Handling\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  globalWrapError: MyGlobalWrapError(),\n  errorObserver: MyErrorObserver<AppState>(),\n  actionObservers: [Log.printer(formatter: Log.verySimpleFormatter)],\n);\n\nclass MyGlobalWrapError extends GlobalWrapError {\n  @override\n  Object? wrap(Object error, StackTrace stackTrace, ReduxAction<AppState> action) {\n    if (error is SocketException) {\n      return UserException('No internet connection').addCause(error);\n    }\n    return error;\n  }\n}\n\nclass MyErrorObserver<St> implements ErrorObserver<St> {\n  @override\n  bool observe(Object error, StackTrace stackTrace, ReduxAction<St> action, Store<St> store) {\n    // Skip logging UserExceptions (they're expected)\n    if (error is! UserException) {\n      crashlytics.recordError(error, stackTrace);\n    }\n    return true;\n  }\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/advanced-actions/wrapping-the-reducer\n- https://asyncredux.com/flutter/basics/failed-actions\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n- https://asyncredux.com/flutter/basics/store\n- https://asyncredux.com/flutter/testing/testing-user-exceptions\n- https://asyncredux.com/flutter/basics/wait-fail-succeed\n- https://asyncredux.com/flutter/miscellaneous/logging\n- https://asyncredux.com/flutter/advanced-actions/action-status\n"
  },
  {
    "path": ".claude/skills/asyncredux-events/SKILL.md",
    "content": "---\nname: asyncredux-events\ndescription: Use the Event class to interact with Flutter's stateful widgets (TextField, ListView, etc.). Covers creating Event objects in state, consuming events with `context.event()`, scrolling lists, changing text fields, and the event lifecycle.\n---\n\n# Events in AsyncRedux\n\nEvents are **single-use notifications** used to trigger side effects in widgets. They're designed for controlling native Flutter widgets like `TextField` and `ListView` that manage their own state through controllers.\n\n## When to Use Events\n\nUse events for:\n- **Controller actions**: Clearing text, changing text, scrolling lists, focusing inputs\n- **One-off UI actions**: Showing dialogs, snackbars, triggering animations\n- **Implicit state changes**: Navigation, any action that should happen exactly once\n\nDo NOT use events for:\n- Values that need to be read multiple times (use regular state instead)\n- Data that should be persisted (events should never be saved to local storage)\n\n## Setup: Add context.event() Extension\n\nAdd the `event` method to your BuildContext extension:\n\n```dart\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n    getSelect<AppState, R>(selector);\n\n  // Add this for events:\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n    getEvent<AppState, R>(selector);\n}\n```\n\n## Creating Events\n\n### Boolean Events\n\nFor simple triggers that don't carry data:\n\n```dart\n// Create an unspent event (will return true once)\nvar clearTextEvt = Evt();\n\n// Create a spent event (will return false)\nvar clearTextEvt = Evt.spent();\n```\n\n### Typed Events\n\nFor events that carry a value:\n\n```dart\n// Create an unspent event with a value (will return value once, then null)\nvar changeTextEvt = Evt<String>(\"New text\");\nvar scrollToIndexEvt = Evt<int>(42);\n\n// Create a spent event (will return null)\nvar changeTextEvt = Evt<String>.spent();\n```\n\n## Declaring Events in State\n\nInitialize all events as **spent** in your initial state:\n\n```dart\nclass AppState {\n  final Evt clearTextEvt;\n  final Evt<String> changeTextEvt;\n  final Evt<int> scrollToIndexEvt;\n\n  AppState({\n    required this.clearTextEvt,\n    required this.changeTextEvt,\n    required this.scrollToIndexEvt,\n  });\n\n  static AppState initialState() => AppState(\n    clearTextEvt: Evt.spent(),\n    changeTextEvt: Evt<String>.spent(),\n    scrollToIndexEvt: Evt<int>.spent(),\n  );\n\n  AppState copy({\n    Evt? clearTextEvt,\n    Evt<String>? changeTextEvt,\n    Evt<int>? scrollToIndexEvt,\n  }) => AppState(\n    clearTextEvt: clearTextEvt ?? this.clearTextEvt,\n    changeTextEvt: changeTextEvt ?? this.changeTextEvt,\n    scrollToIndexEvt: scrollToIndexEvt ?? this.scrollToIndexEvt,\n  );\n}\n```\n\n## Dispatching Events from Actions\n\nActions create **unspent** events and place them in state:\n\n```dart\n// Boolean event - triggers clearing the text field\nclass ClearTextAction extends AppAction {\n  AppState reduce() => state.copy(clearTextEvt: Evt());\n}\n\n// Typed event - changes the text field to a new value\nclass ChangeTextAction extends AppAction {\n  final String newText;\n  ChangeTextAction(this.newText);\n\n  AppState reduce() => state.copy(changeTextEvt: Evt<String>(newText));\n}\n\n// Typed event from async operation\nclass FetchAndSetTextAction extends AppAction {\n  Future<AppState> reduce() async {\n    String text = await api.fetchText();\n    return state.copy(changeTextEvt: Evt<String>(text));\n  }\n}\n\n// Scroll to a specific index in a ListView\nclass ScrollToItemAction extends AppAction {\n  final int index;\n  ScrollToItemAction(this.index);\n\n  AppState reduce() => state.copy(scrollToIndexEvt: Evt<int>(index));\n}\n```\n\n## Consuming Events in Widgets\n\nUse `context.event()` in the widget's build method. **The event is consumed (marked as spent) immediately when read.**\n\n### TextField Example\n\n```dart\nclass MyTextField extends StatefulWidget {\n  @override\n  State<MyTextField> createState() => _MyTextFieldState();\n}\n\nclass _MyTextFieldState extends State<MyTextField> {\n  final controller = TextEditingController();\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // Consume the clear event - returns true once, then false\n    bool shouldClear = context.event((s) => s.clearTextEvt);\n    if (shouldClear) {\n      controller.clear();\n    }\n\n    // Consume the change event - returns the value once, then null\n    String? newText = context.event((s) => s.changeTextEvt);\n    if (newText != null) {\n      controller.text = newText;\n    }\n\n    return TextField(controller: controller);\n  }\n}\n```\n\n### ListView Scrolling Example\n\n```dart\nclass MyListView extends StatefulWidget {\n  @override\n  State<MyListView> createState() => _MyListViewState();\n}\n\nclass _MyListViewState extends State<MyListView> {\n  final scrollController = ScrollController();\n  final itemHeight = 50.0;\n\n  @override\n  void dispose() {\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final items = context.select((s) => s.items);\n\n    // Consume the scroll event\n    int? scrollToIndex = context.event((s) => s.scrollToIndexEvt);\n    if (scrollToIndex != null) {\n      // Schedule the scroll after the frame is built\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        scrollController.animateTo(\n          scrollToIndex * itemHeight,\n          duration: Duration(milliseconds: 300),\n          curve: Curves.easeOut,\n        );\n      });\n    }\n\n    return ListView.builder(\n      controller: scrollController,\n      itemCount: items.length,\n      itemBuilder: (context, index) => SizedBox(\n        height: itemHeight,\n        child: Text(items[index]),\n      ),\n    );\n  }\n}\n```\n\n## Event Lifecycle\n\n1. **Created as spent**: Events start as `Evt.spent()` in initial state\n2. **Dispatched as unspent**: Action creates `Evt()` or `Evt<T>(value)` and puts it in state\n3. **Widget rebuilds**: State change triggers widget rebuild\n4. **Consumed once**: `context.event()` returns the value and marks the event as spent\n5. **Returns null/false**: Subsequent reads return `null` (typed) or `false` (boolean)\n\n## Important Rules\n\n### Each Event Can Only Be Consumed by One Widget\n\nIf multiple widgets need the same trigger, create separate events:\n\n```dart\nclass AppState {\n  final Evt clearSearchEvt;      // For search field\n  final Evt clearCommentsEvt;    // For comments field\n  // ...\n}\n```\n\n### Don't Use Events for Persistent Data\n\nEvents are mutable and designed for one-time use. Never persist them to local storage.\n\n### Event Equality Prevents Unnecessary Rebuilds\n\nEvents have special equality methods that prevent unnecessary widget rebuilds when used correctly with the selector pattern.\n\n## Advanced: Checking Event Status Without Consuming\n\nUse these methods to check an event's status without consuming it:\n\n```dart\n// Check if an event has been consumed\nbool consumed = myEvent.isSpent;\n\n// Check if an event is ready to be consumed\nbool ready = myEvent.isNotSpent;\n\n// Get the underlying state without consuming\nvar eventState = myEvent.state;\n```\n\n## Advanced: Event.map() for Transformations\n\nTransform an event's value:\n\n```dart\n// Map an event to a different type\nEvt<String> nameEvt = Evt<int>(42).map((value) => 'Item $value');\n```\n\n## Advanced: Consuming from Multiple Event Sources\n\nWhen you need to consume from multiple possible event sources:\n\n```dart\n// Create an event that consumes from first non-spent source\nvar combined = Event.from([event1, event2, event3]);\n\n// Or use the static method\nvar value = Event.consumeFrom([event1, event2, event3]);\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/basics/events\n- https://asyncredux.com/flutter/miscellaneous/advanced-events\n- https://asyncredux.com/flutter/basics/using-the-store-state\n- https://asyncredux.com/flutter/connector/store-connector\n"
  },
  {
    "path": ".claude/skills/asyncredux-flutter-hooks/SKILL.md",
    "content": "---\nname: asyncredux-flutter-hooks\ndescription: Integrate AsyncRedux with the flutter_hooks package. Covers adding flutter_hooks_async_redux, using the useSelector hook, and combining hooks with AsyncRedux state management.\n---\n\n## Overview\n\nThe `flutter_hooks_async_redux` package provides a hooks-based API for accessing AsyncRedux state. If you prefer functional components with hooks over the widget-based `StoreConnector` pattern, this package lets you use hooks like `useSelector` and `useDispatch` to interact with the Redux store.\n\n## Installation\n\nAdd these dependencies to your `pubspec.yaml`:\n\n```yaml\ndependencies:\n  flutter_hooks: ^0.21.2\n  async_redux: ^24.2.2\n  flutter_hooks_async_redux: ^3.1.0\n```\n\nThen run `flutter pub get`.\n\n## Core Hooks\n\n### useSelector\n\nSelects a part of the state and subscribes to updates. The widget rebuilds when the selected value changes:\n\n```dart\nString username = useSelector<AppState, String>((state) => state.username);\n```\n\nThe `distinct` parameter (default `true`) controls whether the widget rebuilds only when the selected value changes.\n\n### Creating a Custom useAppState Hook\n\nFor convenience, define a custom hook that's pre-typed for your state:\n\n```dart\nT useAppState<T>(T Function(AppState state) converter, {bool distinct = true}) =>\n    useSelector<AppState, T>(converter, distinct: distinct);\n```\n\nThis simplifies state access throughout your app:\n\n```dart\n// Instead of:\nString username = useSelector<AppState, String>((state) => state.username);\n\n// Use:\nString username = useAppState((state) => state.username);\n```\n\n### useDispatch\n\nDispatches actions that may change the store state. Works with both sync and async actions:\n\n```dart\nclass MyWidget extends HookWidget {\n  @override\n  Widget build(BuildContext context) {\n    var dispatch = useDispatch();\n\n    return ElevatedButton(\n      onPressed: () => dispatch(IncrementAction()),\n      child: Text('Increment'),\n    );\n  }\n}\n```\n\n### useDispatchAndWait\n\nDispatches an action and returns a `Future<ActionStatus>` that resolves when the action completes:\n\n```dart\nclass MyWidget extends HookWidget {\n  @override\n  Widget build(BuildContext context) {\n    var dispatchAndWait = useDispatchAndWait();\n    var dispatch = useDispatch();\n\n    Future<void> handleSubmit() async {\n      // Wait for first action to complete\n      await dispatchAndWait(DoThisFirstAction());\n      // Then dispatch the second\n      dispatch(DoThisSecondAction());\n    }\n\n    return ElevatedButton(\n      onPressed: handleSubmit,\n      child: Text('Submit'),\n    );\n  }\n}\n```\n\nYou can also check the action status:\n\n```dart\nvar status = await dispatchAndWait(MyAction());\nif (status.isCompletedOk) {\n  // Action succeeded\n}\n```\n\n### useDispatchSync\n\nEnforces synchronous action dispatch. Throws `StoreException` if you attempt to dispatch an async action:\n\n```dart\nvar dispatchSync = useDispatchSync();\ndispatchSync(MySyncAction()); // OK\ndispatchSync(MyAsyncAction()); // Throws StoreException\n```\n\n## Waiting and Error Hooks\n\n### useIsWaiting\n\nChecks if an async action is currently being processed:\n\n```dart\nclass MyWidget extends HookWidget {\n  @override\n  Widget build(BuildContext context) {\n    var dispatch = useDispatch();\n    var isLoading = useIsWaiting(LoadDataAction);\n\n    return Column(\n      children: [\n        if (isLoading) CircularProgressIndicator(),\n        ElevatedButton(\n          onPressed: () => dispatch(LoadDataAction()),\n          child: Text('Load'),\n        ),\n      ],\n    );\n  }\n}\n```\n\nYou can check by action type, action instance, or multiple types:\n\n```dart\n// By action type\nvar isWaiting = useIsWaiting(MyAction);\n\n// By action instance\nvar action = MyAction();\ndispatch(action);\nvar isWaiting = useIsWaiting(action);\n\n// Multiple types - true if ANY are in progress\nvar isWaiting = useIsWaiting([BuyAction, SellAction]);\n```\n\n### useIsFailed\n\nChecks if an action has failed:\n\n```dart\nvar isFailed = useIsFailed(MyAction);\n\nif (isFailed) {\n  return Text('Something went wrong');\n}\n```\n\n### useExceptionFor\n\nRetrieves the `UserException` from a failed action:\n\n```dart\nvar exception = useExceptionFor(MyAction);\n\nif (exception != null) {\n  return Text(exception.reason ?? 'Unknown error');\n}\n```\n\n### useClearExceptionFor\n\nGets a function to clear the exception state for an action:\n\n```dart\nvar clearExceptionFor = useClearExceptionFor();\n\n// Clear exception when user dismisses error\nElevatedButton(\n  onPressed: () => clearExceptionFor(MyAction),\n  child: Text('Dismiss'),\n)\n```\n\n## Complete Example\n\nHere's a full example combining multiple hooks:\n\n```dart\nclass UserProfileWidget extends HookWidget {\n  @override\n  Widget build(BuildContext context) {\n    // Select state\n    var username = useAppState((state) => state.user.name);\n    var email = useAppState((state) => state.user.email);\n\n    // Dispatch hooks\n    var dispatch = useDispatch();\n    var dispatchAndWait = useDispatchAndWait();\n\n    // Loading and error state\n    var isLoading = useIsWaiting(UpdateProfileAction);\n    var isFailed = useIsFailed(UpdateProfileAction);\n    var exception = useExceptionFor(UpdateProfileAction);\n    var clearException = useClearExceptionFor();\n\n    Future<void> handleUpdate() async {\n      var status = await dispatchAndWait(UpdateProfileAction());\n      if (status.isCompletedOk) {\n        // Show success message\n      }\n    }\n\n    return Column(\n      children: [\n        Text('Username: $username'),\n        Text('Email: $email'),\n\n        if (isLoading)\n          CircularProgressIndicator(),\n\n        if (isFailed && exception != null)\n          Row(\n            children: [\n              Text(exception.reason ?? 'Update failed'),\n              IconButton(\n                icon: Icon(Icons.close),\n                onPressed: () => clearException(UpdateProfileAction),\n              ),\n            ],\n          ),\n\n        ElevatedButton(\n          onPressed: isLoading ? null : handleUpdate,\n          child: Text('Update Profile'),\n        ),\n      ],\n    );\n  }\n}\n```\n\n## Hook Parameters Reference\n\n| Hook | Accepts | Returns |\n|------|---------|---------|\n| `useSelector<St, T>` | Converter function | Selected value of type T |\n| `useDispatch` | None | Dispatch function |\n| `useDispatchAndWait` | None | Function returning `Future<ActionStatus>` |\n| `useDispatchSync` | None | Sync dispatch function |\n| `useIsWaiting` | Action type, instance, or list of types | `bool` |\n| `useIsFailed` | Action type, instance, or list of types | `bool` |\n| `useExceptionFor` | Action type, instance, or list of types | `UserException?` |\n| `useClearExceptionFor` | None | Clear function |\n\n## Hooks vs StoreConnector\n\nChoose hooks when:\n- You prefer functional widget patterns\n- You're already using `flutter_hooks` in your project\n- You want concise state access without view-model boilerplate\n\nChoose `StoreConnector` when:\n- You want explicit separation between UI and state logic\n- You need the structured view-model pattern for testing\n- You're not using hooks elsewhere in your project\n\nBoth approaches work well with AsyncRedux - pick the one that fits your team's preferences.\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/other-packages/using-flutter-hooks-package\n- https://pub.dev/packages/flutter_hooks_async_redux\n- https://github.com/marcglasberg/flutter_hooks_async_redux\n"
  },
  {
    "path": ".claude/skills/asyncredux-navigation/SKILL.md",
    "content": "---\nname: asyncredux-navigation\ndescription: Handle navigation through actions using NavigateAction. Covers setting up the navigator key, dispatching NavigateAction for push/pop/replace, and testing navigation in isolation.\n---\n\n# Navigation with NavigateAction\n\nAsyncRedux enables app navigation through action dispatching, making it easier to unit test navigation logic. This approach is optional and currently supports Navigator 1 only.\n\n## Setup\n\n### 1. Create and Register the Navigator Key\n\nCreate a global navigator key and register it with NavigateAction during app initialization:\n\n```dart\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\nfinal navigatorKey = GlobalKey<NavigatorState>();\n\nvoid main() async {\n  NavigateAction.setNavigatorKey(navigatorKey);\n  // ... rest of initialization\n  runApp(MyApp());\n}\n```\n\n### 2. Configure MaterialApp\n\nPass the same navigator key to your MaterialApp:\n\n```dart\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        routes: {\n          '/': (context) => HomePage(),\n          '/details': (context) => DetailsPage(),\n          '/settings': (context) => SettingsPage(),\n        },\n        navigatorKey: navigatorKey,\n      ),\n    );\n  }\n}\n```\n\n## Dispatching Navigation Actions\n\n### Push Operations\n\n```dart\n// Push a named route\ndispatch(NavigateAction.pushNamed('/details'));\n\n// Push a route with a Route object\ndispatch(NavigateAction.push(\n  MaterialPageRoute(builder: (context) => DetailsPage()),\n));\n\n// Push and replace current route (named)\ndispatch(NavigateAction.pushReplacementNamed('/newRoute'));\n\n// Push and replace current route (with Route object)\ndispatch(NavigateAction.pushReplacement(\n  MaterialPageRoute(builder: (context) => NewPage()),\n));\n\n// Pop current route and push a new named route\ndispatch(NavigateAction.popAndPushNamed('/otherRoute'));\n\n// Push named route and remove all routes until predicate is true\ndispatch(NavigateAction.pushNamedAndRemoveUntil(\n  '/home',\n  (route) => false, // Removes all routes\n));\n\n// Push named route and remove all routes (convenience method)\ndispatch(NavigateAction.pushNamedAndRemoveAll('/home'));\n\n// Push route and remove until predicate\ndispatch(NavigateAction.pushAndRemoveUntil(\n  MaterialPageRoute(builder: (context) => HomePage()),\n  (route) => false,\n));\n```\n\n### Pop Operations\n\n```dart\n// Pop the current route\ndispatch(NavigateAction.pop());\n\n// Pop with a result value\ndispatch(NavigateAction.pop(result: 'some_value'));\n\n// Pop routes until predicate is true\ndispatch(NavigateAction.popUntil((route) => route.isFirst));\n\n// Pop until reaching a specific named route\ndispatch(NavigateAction.popUntilRouteName('/home'));\n\n// Pop until reaching a specific route\ndispatch(NavigateAction.popUntilRoute(someRoute));\n```\n\n### Replace Operations\n\n```dart\n// Replace a specific route with a new one\ndispatch(NavigateAction.replace(\n  oldRoute: currentRoute,\n  newRoute: MaterialPageRoute(builder: (context) => NewPage()),\n));\n\n// Replace the route below the current one\ndispatch(NavigateAction.replaceRouteBelow(\n  anchorRoute: currentRoute,\n  newRoute: MaterialPageRoute(builder: (context) => NewPage()),\n));\n```\n\n### Remove Operations\n\n```dart\n// Remove a specific route\ndispatch(NavigateAction.removeRoute(routeToRemove));\n\n// Remove the route below a specific route\ndispatch(NavigateAction.removeRouteBelow(anchorRoute));\n```\n\n## Complete Example\n\n```dart\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\nlate Store<AppState> store;\nfinal navigatorKey = GlobalKey<NavigatorState>();\n\nvoid main() async {\n  NavigateAction.setNavigatorKey(navigatorKey);\n  store = Store<AppState>(initialState: AppState());\n  runApp(MyApp());\n}\n\nclass AppState {}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        routes: {\n          '/': (context) => HomePage(),\n          '/details': (context) => DetailsPage(),\n        },\n        navigatorKey: navigatorKey,\n      ),\n    );\n  }\n}\n\nclass HomePage extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('Home')),\n      body: Center(\n        child: ElevatedButton(\n          child: Text('Go to Details'),\n          onPressed: () => context.dispatch(NavigateAction.pushNamed('/details')),\n        ),\n      ),\n    );\n  }\n}\n\nclass DetailsPage extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('Details')),\n      body: Center(\n        child: ElevatedButton(\n          child: Text('Go Back'),\n          onPressed: () => context.dispatch(NavigateAction.pop()),\n        ),\n      ),\n    );\n  }\n}\n```\n\n## Getting the Current Route Name\n\nRather than storing the current route in your app state (which can create complications), access it directly:\n\n```dart\nString routeName = NavigateAction.getCurrentNavigatorRouteName(context);\n```\n\n## Navigation from Actions\n\nYou can dispatch navigation actions from within other actions:\n\n```dart\nclass LoginAction extends ReduxAction<AppState> {\n  final String username;\n  final String password;\n\n  LoginAction({required this.username, required this.password});\n\n  @override\n  Future<AppState?> reduce() async {\n    final user = await api.login(username, password);\n\n    // Navigate to home after successful login\n    dispatch(NavigateAction.pushReplacementNamed('/home'));\n\n    return state.copy(user: user);\n  }\n}\n```\n\n## Testing Navigation\n\nNavigateAction enables unit testing of navigation without widget or driver tests:\n\n```dart\ntest('login navigates to home on success', () async {\n  final store = Store<AppState>(initialState: AppState());\n\n  // Capture dispatched actions\n  NavigateAction? navigateAction;\n  store.actionObservers.add((action, ini, prevState, newState) {\n    if (action is NavigateAction) {\n      navigateAction = action;\n    }\n  });\n\n  await store.dispatchAndWait(LoginAction(\n    username: 'test',\n    password: 'password',\n  ));\n\n  // Assert navigation type\n  expect(navigateAction!.type, NavigateType.pushReplacementNamed);\n\n  // Assert route name\n  expect(\n    (navigateAction!.details as NavigatorDetails_PushReplacementNamed).routeName,\n    '/home',\n  );\n});\n```\n\n### NavigateType Enum Values\n\nThe `NavigateType` enum includes values for all navigation operations:\n\n- `push`, `pushNamed`\n- `pop`\n- `pushReplacement`, `pushReplacementNamed`\n- `popAndPushNamed`\n- `pushAndRemoveUntil`, `pushNamedAndRemoveUntil`, `pushNamedAndRemoveAll`\n- `popUntil`, `popUntilRouteName`, `popUntilRoute`\n- `replace`, `replaceRouteBelow`\n- `removeRoute`, `removeRouteBelow`\n\n## Important Notes\n\n- Navigation via AsyncRedux is entirely optional\n- Currently supports Navigator 1 only\n- For modern navigation packages (like go_router), you'll need to create custom action implementations\n- Don't store the current route in your app state; use `getCurrentNavigatorRouteName()` instead\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/miscellaneous/navigation\n- https://asyncredux.com/flutter/testing/testing-navigation\n- https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_navigate.dart\n- https://raw.githubusercontent.com/marcglasberg/async_redux/master/lib/src/navigate_action.dart\n"
  },
  {
    "path": ".claude/skills/asyncredux-nonreentrant-mixin/SKILL.md",
    "content": "---\nname: asyncredux-nonreentrant-mixin\ndescription: Add the NonReentrant mixin to prevent an action from dispatching while already in progress. Covers preventing duplicate form submissions, avoiding race conditions, and protecting long-running operations.\n---\n\n# NonReentrant Mixin\n\nThe `NonReentrant` mixin prevents concurrent execution of the same action type. When an action instance is already running, new dispatches of that same action are silently aborted.\n\n## Basic Usage\n\nAdd the `NonReentrant` mixin to any action that should not run concurrently:\n\n```dart\nclass SaveAction extends AppAction with NonReentrant {\n  Future<AppState?> reduce() async {\n    await http.put('http://myapi.com/save', body: 'data');\n    return null;\n  }\n}\n```\n\nWith this mixin:\n- If the user clicks \"Save\" multiple times rapidly, only the first dispatch executes\n- Subsequent dispatches while the first is running are silently aborted\n- No duplicate API calls or race conditions occur\n\n## How It Works\n\nThe `NonReentrant` mixin overrides the `abortDispatch` method. When `abortDispatch()` returns `true`, the action's `before()`, `reduce()`, and `after()` methods will not run, and the state stays unchanged.\n\nBy default, checks are based on the action's runtime type - multiple instances of the same action class cannot run simultaneously.\n\n## Common Use Cases\n\n1. **Preventing duplicate form submissions** - Stop users from accidentally submitting forms multiple times\n2. **Protecting API calls** - Ensure save/update/delete operations don't fire concurrently\n3. **Resource-intensive tasks** - Prevent expensive computations from running in parallel\n4. **Avoiding race conditions** - Ensure sequential execution of operations that must not overlap\n\n## Customization\n\n### Allow Different Parameters to Run Concurrently\n\nOverride `nonReentrantKeyParams()` to allow actions with different parameters to run in parallel:\n\n```dart\nclass SaveItemAction extends AppAction with NonReentrant {\n  final String itemId;\n  SaveItemAction(this.itemId);\n\n  @override\n  Object? nonReentrantKeyParams() => itemId;\n\n  Future<AppState?> reduce() async {\n    await saveItem(itemId);\n    return null;\n  }\n}\n```\n\nWith this customization:\n- `SaveItemAction('A')` and `SaveItemAction('B')` can run concurrently\n- Two `SaveItemAction('A')` dispatches will still block each other\n\n### Share Keys Across Different Action Types\n\nOverride `computeNonReentrantKey()` to make different action classes block each other:\n\n```dart\nclass SaveUserAction extends AppAction with NonReentrant {\n  final String orderId;\n  SaveUserAction(this.orderId);\n\n  @override\n  Object? computeNonReentrantKey() => orderId;\n\n  Future<AppState?> reduce() async { ... }\n}\n\nclass DeleteUserAction extends AppAction with NonReentrant {\n  final String orderId;\n  DeleteUserAction(this.orderId);\n\n  @override\n  Object? computeNonReentrantKey() => orderId;\n\n  Future<AppState?> reduce() async { ... }\n}\n```\n\nThis prevents `SaveUserAction('123')` and `DeleteUserAction('123')` from running simultaneously - useful when different operations on the same resource must not overlap.\n\n## Combining with Other Mixins\n\nYou can combine `NonReentrant` with other compatible mixins:\n\n```dart\nclass LoadDataAction extends AppAction with CheckInternet, NonReentrant {\n  Future<AppState?> reduce() async {\n    final data = await fetchData();\n    return state.copy(data: data);\n  }\n}\n```\n\n**Incompatible mixins:** `NonReentrant` cannot be combined with:\n- `Throttle`\n- `UnlimitedRetryCheckInternet`\n- Most optimistic update mixins (check the compatibility matrix)\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/advanced-actions/control-mixins\n- https://asyncredux.com/flutter/advanced-actions/action-mixins\n- https://asyncredux.com/flutter/advanced-actions/aborting-the-dispatch\n- https://asyncredux.com/flutter/basics/dispatching-actions\n- https://asyncredux.com/flutter/basics/async-actions\n"
  },
  {
    "path": ".claude/skills/asyncredux-observers/SKILL.md",
    "content": "---\nname: asyncredux-observers\ndescription: Set up observers for debugging and monitoring. Covers implementing actionObservers for dispatch logging, stateObserver for state change tracking, combining observers with globalWrapError, and using observers for analytics.\n---\n\n# Setting Up Observers for Debugging and Monitoring\n\nAsyncRedux provides several observer types for monitoring actions, state changes, errors, and widget rebuilds. These observers are configured when creating the Store.\n\n## Overview of Observer Types\n\n| Observer Type | Purpose |\n|--------------|---------|\n| `ActionObserver` | Monitor action dispatch (start and end) |\n| `StateObserver` | Monitor state changes after actions |\n| `ErrorObserver` | Monitor and handle action errors |\n| `ModelObserver` | Monitor widget rebuilds (for StoreConnector) |\n\n## Store Configuration with Observers\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  actionObservers: [ConsoleActionObserver()],\n  stateObservers: [MyStateObserver()],\n  errorObserver: MyErrorObserver(),\n  modelObserver: DefaultModelObserver(),\n);\n```\n\n## ActionObserver\n\nThe `ActionObserver` monitors when actions are dispatched and when they complete. It triggers twice per action: at the start (INI) and at the end (END).\n\n### ActionObserver Abstract Class\n\n```dart\nabstract class ActionObserver<St> {\n  void observe(\n    ReduxAction<St> action,\n    int dispatchCount, {\n    required bool ini,\n  });\n}\n```\n\n### Parameters\n\n- `action`: The dispatched action instance\n- `dispatchCount`: Sequential number of this dispatch\n- `ini`: `true` when action starts (INI phase), `false` when it ends (END phase)\n\n### Observation Phases\n\n**INI Phase**: Action dispatch begins. The reducer hasn't modified state yet. Sync reducers may complete during this phase; async reducers start their async process.\n\n**END Phase**: The reducer has finished and returned the new state. State modifications are now observable.\n\n**Important**: Receiving an END observation does not guarantee all effects have finished. Async operations that were not awaited may continue running and dispatch additional actions later.\n\n### Built-in ConsoleActionObserver\n\nAsyncRedux provides `ConsoleActionObserver` for development debugging:\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  actionObservers: kReleaseMode ? null : [ConsoleActionObserver()],\n);\n```\n\nThis prints actions in yellow to the console. Override `toString()` in your actions to display additional information:\n\n```dart\nclass LoadUserAction extends AppAction {\n  final String username;\n  LoadUserAction(this.username);\n\n  @override\n  Future<AppState?> reduce() async {\n    // ...\n  }\n\n  @override\n  String toString() => 'LoadUserAction(username: $username)';\n}\n```\n\n### Using Log.printer for Formatted Output\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  actionObservers: [Log.printer(formatter: Log.verySimpleFormatter)],\n);\n```\n\n### Custom ActionObserver Implementation\n\n```dart\nclass MyActionObserver implements ActionObserver<AppState> {\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    int dispatchCount, {\n    required bool ini,\n  }) {\n    final phase = ini ? 'START' : 'END';\n    print('[$phase] Action #$dispatchCount: ${action.runtimeType}');\n  }\n}\n```\n\n## StateObserver\n\nThe `StateObserver` is notified of all state changes, allowing you to track, log, or record state history.\n\n### StateObserver Abstract Class\n\n```dart\nabstract class StateObserver<St> {\n  void observe(\n    ReduxAction<St> action,\n    St prevState,\n    St newState,\n    Object? error,\n    int dispatchCount,\n  );\n}\n```\n\n### Parameters\n\n- `action`: The action that triggered the change\n- `prevState`: State before the reducer executed\n- `newState`: State returned by the reducer\n- `error`: Null if successful; contains the thrown error otherwise\n- `dispatchCount`: Sequential dispatch number\n\n### Detecting State Changes\n\nCompare states using `identical()` to detect actual changes:\n\n```dart\nbool stateChanged = !identical(prevState, newState);\n```\n\n### Custom StateObserver for Logging\n\n```dart\nclass StateLogger implements StateObserver<AppState> {\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    AppState prevState,\n    AppState newState,\n    Object? error,\n    int dispatchCount,\n  ) {\n    final changed = !identical(prevState, newState);\n    print('Action #$dispatchCount: ${action.runtimeType}');\n    print('  State changed: $changed');\n    if (error != null) {\n      print('  Error: $error');\n    }\n  }\n}\n```\n\n### StateObserver for Undo/Redo\n\nA common use case is recording state history for undo/redo functionality:\n\n```dart\nclass UndoRedoObserver implements StateObserver<AppState> {\n  final List<AppState> _history = [];\n  int _currentIndex = -1;\n  final int maxHistorySize;\n\n  UndoRedoObserver({this.maxHistorySize = 50});\n\n  bool get canUndo => _currentIndex > 0;\n  bool get canRedo => _currentIndex < _history.length - 1;\n\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    AppState prevState,\n    AppState newState,\n    Object? error,\n    int dispatchCount,\n  ) {\n    // Skip undo/redo actions to avoid recording navigation\n    if (action is UndoAction || action is RedoAction) return;\n\n    // Skip if state didn't change\n    if (identical(prevState, newState)) return;\n\n    // Remove \"future\" states if we're navigating\n    if (_currentIndex < _history.length - 1) {\n      _history.removeRange(_currentIndex + 1, _history.length);\n    }\n\n    // Add new state\n    _history.add(newState);\n    _currentIndex = _history.length - 1;\n\n    // Enforce max history size\n    if (_history.length > maxHistorySize) {\n      _history.removeAt(0);\n      _currentIndex--;\n    }\n  }\n\n  AppState? getPreviousState() {\n    if (!canUndo) return null;\n    _currentIndex--;\n    return _history[_currentIndex];\n  }\n\n  AppState? getNextState() {\n    if (!canRedo) return null;\n    _currentIndex++;\n    return _history[_currentIndex];\n  }\n}\n```\n\n## ErrorObserver\n\nThe `ErrorObserver` monitors all errors thrown by actions and can suppress or allow them to propagate.\n\n### Error Handling Flow\n\nThe error handling order is:\n1. `wrapError()` (action-level)\n2. `GlobalWrapError` (app-level)\n3. `ErrorObserver` (monitoring/logging)\n\n### ErrorObserver Implementation\n\n```dart\nclass MyErrorObserver<St> implements ErrorObserver<St> {\n  @override\n  bool observe(\n    Object error,\n    StackTrace stackTrace,\n    ReduxAction<St> action,\n    Store store,\n  ) {\n    // Log the error\n    print('Error in ${action.runtimeType}: $error');\n    print(stackTrace);\n\n    // Send to crash reporting service\n    crashReporter.recordError(error, stackTrace, reason: action.runtimeType.toString());\n\n    // Return true to rethrow the error, false to suppress it\n    return true;\n  }\n}\n```\n\n### Store Configuration with ErrorObserver\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  errorObserver: MyErrorObserver<AppState>(),\n);\n```\n\n### Combining with GlobalWrapError\n\nUse `GlobalWrapError` to transform errors before they reach the `ErrorObserver`:\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  globalWrapError: MyGlobalWrapError(),\n  errorObserver: MyErrorObserver<AppState>(),\n);\n\nclass MyGlobalWrapError extends GlobalWrapError {\n  @override\n  Object? wrap(Object error, StackTrace stackTrace, ReduxAction<dynamic> action) {\n    // Transform platform errors to user-friendly messages\n    if (error is PlatformException) {\n      return UserException('Check your internet connection').addCause(error);\n    }\n    return error;\n  }\n}\n```\n\n## ModelObserver\n\nThe `ModelObserver` monitors widget rebuilds when using `StoreConnector`. This is useful for debugging rebuild behavior and ensuring efficient state updates.\n\n### Setup\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  modelObserver: DefaultModelObserver(),\n);\n```\n\n### Console Output\n\n`DefaultModelObserver` prints rebuild information:\n\n```\nModel D:1 R:1 = Rebuild:true, Connector:MyWidgetConnector, Model:MyViewModel{B}.\nModel D:2 R:2 = Rebuild:false, Connector:MyWidgetConnector, Model:MyViewModel{B}.\nModel D:3 R:3 = Rebuild:true, Connector:MyWidgetConnector, Model:MyViewModel{C}.\n```\n\n- `D`: Dispatch count\n- `R`: Rebuild count\n- `Rebuild`: Whether the widget actually rebuilt\n- `Connector`: The StoreConnector type\n- `Model`: The ViewModel with current state\n\n### Configuration for Better Output\n\nPass `debug: this` to `StoreConnector` to enable connector type printing:\n\n```dart\nclass MyWidgetConnector extends StatelessWidget with StoreConnector<AppState, MyViewModel> {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, MyViewModel>(\n      debug: this, // Enable for ModelObserver output\n      converter: (store) => MyViewModel.fromStore(store),\n      builder: (context, vm) => MyWidget(vm),\n    );\n  }\n}\n```\n\nOverride `ViewModel.toString()` for custom diagnostic information.\n\n## Using Observers for Analytics\n\n### Metrics Observer Pattern\n\nCreate a metrics observer that delegates to action-specific tracking methods:\n\n```dart\nabstract class AppAction extends ReduxAction<AppState> {\n  /// Override in specific actions to track metrics\n  void trackEvent(MetricsService metrics) {}\n}\n\nclass MetricsObserver implements StateObserver<AppState> {\n  final MetricsService metrics;\n\n  MetricsObserver(this.metrics);\n\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    AppState prevState,\n    AppState newState,\n    Object? error,\n    int dispatchCount,\n  ) {\n    if (action is AppAction) {\n      action.trackEvent(metrics);\n    }\n  }\n}\n```\n\nThen override `trackEvent` in specific actions:\n\n```dart\nclass PurchaseAction extends AppAction {\n  final Product product;\n  PurchaseAction(this.product);\n\n  @override\n  Future<AppState?> reduce() async {\n    await purchaseService.buy(product);\n    return state.copy(purchases: state.purchases.add(product));\n  }\n\n  @override\n  void trackEvent(MetricsService metrics) {\n    metrics.trackPurchase(productId: product.id, price: product.price);\n  }\n}\n```\n\n### Analytics ActionObserver\n\nTrack all dispatched actions for analytics:\n\n```dart\nclass AnalyticsObserver implements ActionObserver<AppState> {\n  final AnalyticsService analytics;\n\n  AnalyticsObserver(this.analytics);\n\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    int dispatchCount, {\n    required bool ini,\n  }) {\n    // Only track at start (ini) to avoid double-counting\n    if (ini) {\n      analytics.trackEvent(\n        'action_dispatched',\n        parameters: {'action_type': action.runtimeType.toString()},\n      );\n    }\n  }\n}\n```\n\n## Complete Example: Store with All Observers\n\n```dart\n// observers.dart\nclass ConsoleStateObserver implements StateObserver<AppState> {\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    AppState prevState,\n    AppState newState,\n    Object? error,\n    int dispatchCount,\n  ) {\n    final changed = !identical(prevState, newState);\n    print('[$dispatchCount] ${action.runtimeType} - Changed: $changed');\n    if (error != null) print('  Error: $error');\n  }\n}\n\nclass CrashReportingErrorObserver implements ErrorObserver<AppState> {\n  @override\n  bool observe(Object error, StackTrace stackTrace, ReduxAction<AppState> action, Store store) {\n    // Don't report UserExceptions (they're expected)\n    if (error is! UserException) {\n      FirebaseCrashlytics.instance.recordError(error, stackTrace);\n    }\n    return true; // Rethrow the error\n  }\n}\n\n// main.dart\nvoid main() {\n  final store = Store<AppState>(\n    initialState: AppState.initialState(),\n    // Only enable console observers in debug mode\n    actionObservers: kDebugMode ? [ConsoleActionObserver()] : null,\n    stateObservers: kDebugMode ? [ConsoleStateObserver()] : null,\n    // Always enable error observer\n    errorObserver: CrashReportingErrorObserver(),\n    // Transform errors globally\n    globalWrapError: MyGlobalWrapError(),\n  );\n\n  runApp(StoreProvider<AppState>(\n    store: store,\n    child: MyApp(),\n  ));\n}\n```\n\n## Multiple Observers\n\nYou can use multiple observers of the same type:\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  actionObservers: [\n    ConsoleActionObserver(),\n    AnalyticsObserver(analyticsService),\n    PerformanceObserver(),\n  ],\n  stateObservers: [\n    StateLogger(),\n    UndoRedoObserver(),\n    MetricsObserver(metricsService),\n  ],\n);\n```\n\nAll observers will be notified in the order they are listed.\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/miscellaneous/logging\n- https://asyncredux.com/flutter/miscellaneous/metrics\n- https://asyncredux.com/flutter/miscellaneous/observing-rebuilds\n- https://asyncredux.com/flutter/miscellaneous/undo-and-redo\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/basics/store\n"
  },
  {
    "path": ".claude/skills/asyncredux-optimistic-update-mixin/SKILL.md",
    "content": "---\nname: asyncredux-optimistic-update-mixin\ndescription: Add the OptimisticUpdate mixin for instant UI feedback before server confirmation. Covers immediate state changes, automatic rollback on failure, and optionally notifying users of rollback.\n---\n\n# Optimistic Update Mixins\n\nAsyncRedux provides three optimistic update mixins for different scenarios:\n\n| Mixin | Use Case |\n|-------|----------|\n| `OptimisticCommand` | One-time operations (create, delete, submit) with rollback |\n| `OptimisticSync` | Rapid toggling/interactions with coalescing |\n| `OptimisticSyncWithPush` | Real-time server push scenarios with revision tracking |\n\n## OptimisticCommand\n\nUse for one-time server operations where immediate UI feedback matters: creating todos, deleting items, submitting forms, or processing payments.\n\n### Basic Example\n\nWithout optimistic updates (user waits for server):\n\n```dart\nclass SaveTodo extends AppAction {\n  final Todo newTodo;\n  SaveTodo(this.newTodo);\n\n  Future<AppState?> reduce() async {\n    await saveTodo(newTodo);\n    var reloadedList = await loadTodoList();\n    return state.copy(todoList: reloadedList);\n  }\n}\n```\n\nWith OptimisticCommand (instant UI feedback):\n\n```dart\nclass SaveTodo extends AppAction with OptimisticCommand {\n  final Todo newTodo;\n  SaveTodo(this.newTodo);\n\n  // Value to apply immediately to UI\n  Object? optimisticValue() => newTodo;\n\n  // Extract current value from state (for rollback comparison)\n  Object? getValueFromState(AppState state)\n    => state.todoList.getById(newTodo.id);\n\n  // Apply value to state and return new state\n  AppState applyValueToState(AppState state, Object? value)\n    => state.copy(todoList: state.todoList.add(value as Todo));\n\n  // Send to server (retries if using Retry mixin)\n  Future<Object?> sendCommandToServer(Object? value) async\n    => await saveTodo(newTodo);\n\n  // Optional: reload from server on error\n  Future<Object?> reloadFromServer() async\n    => await loadTodoList();\n}\n```\n\n### How Rollback Works\n\nIf `sendCommandToServer` fails, the mixin automatically rolls back only if the current state still matches the optimistic value. This avoids undoing newer changes made while the request was in flight.\n\nOverride these methods to customize rollback:\n\n```dart\n// Determine whether to restore previous state\nbool shouldRollback() => true;\n\n// Specify exact state to restore\nAppState? rollbackState() => previousState;\n```\n\n### Non-Reentrant by Default\n\nOptimisticCommand prevents concurrent execution of the same action. Use `nonReentrantKeyParams()` to allow parallel operations on different items:\n\n```dart\nclass SaveTodo extends AppAction with OptimisticCommand {\n  final String itemId;\n  SaveTodo(this.itemId);\n\n  // Allow SaveTodo('A') and SaveTodo('B') to run simultaneously\n  // but prevent two SaveTodo('A') from running together\n  Object? nonReentrantKeyParams() => itemId;\n\n  // ... rest of implementation\n}\n```\n\nCheck if action is in progress in UI:\n\n```dart\nif (context.isWaiting(SaveTodo)) {\n  return CircularProgressIndicator();\n}\n```\n\n### Combining with Other Mixins\n\n- **With Retry**: Only `sendCommandToServer` retries; optimistic UI remains stable\n- **With CheckInternet**: No optimistic state applied when offline\n\n## OptimisticSync\n\nUse for rapid user interactions (toggling likes, switches, sliders) where only the final value matters and intermediate states can be discarded.\n\n### Toggle Example\n\n```dart\nclass ToggleLike extends AppAction with OptimisticSync<AppState, bool> {\n  final String itemId;\n  ToggleLike(this.itemId);\n\n  // Allow concurrent operations on different items\n  Object? optimisticSyncKeyParams() => itemId;\n\n  // Value to apply optimistically (toggle current value)\n  bool valueToApply() => !state.items[itemId].liked;\n\n  // Apply optimistic change to state\n  AppState applyOptimisticValueToState(AppState state, bool isLiked)\n    => state.copy(items: state.items.setLiked(itemId, isLiked));\n\n  // Extract current value from state\n  bool getValueFromState(AppState state) => state.items[itemId].liked;\n\n  // Send to server\n  Future<Object?> sendValueToServer(Object? value) async\n    => await api.setLiked(itemId, value);\n\n  // Optional: Apply server response to state\n  AppState? applyServerResponseToState(AppState state, Object serverResponse)\n    => state.copy(items: state.items.setLiked(itemId, serverResponse as bool));\n\n  // Optional: Handle completion/errors\n  Future<AppState?> onFinish(Object? error) async {\n    if (error != null) {\n      // Reload from server on failure\n      var reloaded = await api.getItem(itemId);\n      return state.copy(items: state.items.update(itemId, reloaded));\n    }\n    return null;\n  }\n}\n```\n\n### How Coalescing Works\n\nMultiple rapid changes are merged into minimal server requests:\n\n1. User taps like button 5 times quickly\n2. UI updates instantly each time (toggle, toggle, toggle...)\n3. Only **one** server request sends the final state\n4. If state changes during in-flight request, a follow-up request sends the new final value\n\n## OptimisticSyncWithPush\n\nUse when your app receives real-time server updates (WebSockets, Firebase) across multiple devices modifying shared data.\n\n### Key Differences from OptimisticSync\n\n- Each local dispatch increments a `localRevision` counter\n- Server pushes do NOT increment `localRevision`\n- Follow-up logic compares revisions instead of just values\n- Stale pushes are automatically ignored\n\n### Implementation\n\n```dart\nclass ToggleLike extends AppAction with OptimisticSyncWithPush<AppState, bool> {\n  final String itemId;\n  ToggleLike(this.itemId);\n\n  Object? optimisticSyncKeyParams() => itemId;\n\n  bool valueToApply() => !state.items[itemId].liked;\n\n  AppState applyOptimisticValueToState(AppState state, bool isLiked)\n    => state.copy(items: state.items.setLiked(itemId, isLiked));\n\n  bool getValueFromState(AppState state) => state.items[itemId].liked;\n\n  // Read server revision from state\n  int? getServerRevisionFromState(Object? key)\n    => state.items[key as String].serverRevision;\n\n  AppState? applyServerResponseToState(AppState state, Object serverResponse)\n    => state.copy(items: state.items.setLiked(itemId, serverResponse as bool));\n\n  Future<Object?> sendValueToServer(Object? value) async {\n    // Get local revision BEFORE await\n    int localRev = localRevision();\n\n    var response = await api.setLiked(itemId, value, localRev: localRev);\n\n    // Record server's revision after response\n    informServerRevision(response.serverRev);\n\n    return response.liked;\n  }\n}\n```\n\n### ServerPush Mixin\n\nHandle incoming server pushes with automatic stale detection:\n\n```dart\nclass PushLikeUpdate extends AppAction with ServerPush<AppState> {\n  final String itemId;\n  final bool liked;\n  final int serverRev;\n\n  PushLikeUpdate({\n    required this.itemId,\n    required this.liked,\n    required this.serverRev,\n  });\n\n  // Link to corresponding OptimisticSyncWithPush action\n  Type associatedAction() => ToggleLike;\n\n  Object? optimisticSyncKeyParams() => itemId;\n\n  int serverRevision() => serverRev;\n\n  int? getServerRevisionFromState(Object? key)\n    => state.items[key as String].serverRevision;\n\n  AppState? applyServerPushToState(AppState state, Object? key, int serverRevision)\n    => state.copy(\n         items: state.items.update(\n           key as String,\n           (item) => item.copy(liked: liked, serverRevision: serverRevision),\n         ),\n       );\n}\n```\n\nIf incoming `serverRevision` ≤ current known revision, the push is automatically ignored. This prevents older server states from overwriting newer ones.\n\n### Data Model for Revision Tracking\n\nStore server revisions in your data model:\n\n```dart\nclass Item {\n  final bool liked;\n  final int? serverRevision;\n\n  Item({required this.liked, this.serverRevision});\n\n  Item copy({bool? liked, int? serverRevision}) => Item(\n    liked: liked ?? this.liked,\n    serverRevision: serverRevision ?? this.serverRevision,\n  );\n}\n```\n\n## Notifying Users of Rollback\n\nTo notify users when a rollback occurs, use `UserException` in your error handling:\n\n```dart\nclass SaveTodo extends AppAction with OptimisticCommand {\n  // ... required methods ...\n\n  Future<Object?> sendCommandToServer(Object? value) async {\n    try {\n      return await saveTodo(newTodo);\n    } catch (e) {\n      // Throw UserException to show dialog after rollback\n      throw UserException('Failed to save. Your change was reverted.').addCause(e);\n    }\n  }\n}\n```\n\nOr use `onFinish` with OptimisticSync:\n\n```dart\nFuture<AppState?> onFinish(Object? error) async {\n  if (error != null) {\n    // Dispatch a notification action\n    dispatch(UserExceptionAction('Failed to update. Reverting...'));\n\n    // Reload correct state from server\n    var reloaded = await api.getItem(itemId);\n    return state.copy(items: state.items.update(itemId, reloaded));\n  }\n  return null;\n}\n```\n\n## Choosing the Right Mixin\n\n| Scenario | Mixin |\n|----------|-------|\n| Create/delete/submit operations | `OptimisticCommand` |\n| Toggle switches, like buttons | `OptimisticSync` |\n| Sliders, rapid input changes | `OptimisticSync` |\n| Multi-device with real-time sync | `OptimisticSyncWithPush` + `ServerPush` |\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/advanced-actions/optimistic-mixins\n- https://asyncredux.com/flutter/advanced-actions/action-mixins\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n- https://asyncredux.com/flutter/advanced-actions/control-mixins\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/basics/failed-actions\n"
  },
  {
    "path": ".claude/skills/asyncredux-persistence/SKILL.md",
    "content": "---\nname: asyncredux-persistence\ndescription: Implement local state persistence using Persistor. Covers creating a custom Persistor class, implementing `readState()`, `persistDifference()`, `deleteState()`, using LocalPersist helper, throttling saves, and pausing/resuming persistence with app lifecycle.\n---\n\n## Overview\n\nAsyncRedux provides state persistence by passing a `persistor` object to the Store. This maintains app state on disk, enabling restoration between sessions.\n\n## Store Initialization with Persistor\n\nAt startup, read any existing state from disk, create default state if none exists, then initialize the store:\n\n```dart\nvar persistor = MyPersistor();\n\nvar initialState = await persistor.readState();\n\nif (initialState == null) {\n  initialState = AppState.initialState();\n  await persistor.saveInitialState(initialState);\n}\n\nvar store = Store<AppState>(\n  initialState: initialState,\n  persistor: persistor,\n);\n```\n\n## The Persistor Abstract Class\n\nThe `Persistor<St>` base class defines these methods:\n\n```dart\nabstract class Persistor<St> {\n  /// Read persisted state, or return null if none exists\n  Future<St?> readState();\n\n  /// Delete state from disk\n  Future<void> deleteState();\n\n  /// Save state changes. Provides both newState and lastPersistedState\n  /// so you can compare them and save only the difference.\n  Future<void> persistDifference({\n    required St? lastPersistedState,\n    required St newState\n  });\n\n  /// Convenience method for initial saves\n  Future<void> saveInitialState(St state) =>\n    persistDifference(lastPersistedState: null, newState: state);\n\n  /// Controls save frequency. Return null to disable throttling.\n  Duration get throttle => const Duration(seconds: 2);\n}\n```\n\n## Creating a Custom Persistor\n\nExtend the abstract class and implement the required methods:\n\n```dart\nclass MyPersistor extends Persistor<AppState> {\n\n  @override\n  Future<AppState?> readState() async {\n    // Read state from disk (e.g., from SharedPreferences, file, etc.)\n    return null;\n  }\n\n  @override\n  Future<void> deleteState() async {\n    // Delete state from disk\n  }\n\n  @override\n  Future<void> persistDifference({\n    required AppState? lastPersistedState,\n    required AppState newState,\n  }) async {\n    // Save state to disk.\n    // You can compare lastPersistedState with newState to save only changes.\n  }\n\n  @override\n  Duration get throttle => const Duration(seconds: 2);\n}\n```\n\n## Throttling\n\nThe `throttle` getter controls how often state is saved. All changes within the throttle window are collected and saved in a single call. The default is 2 seconds.\n\n```dart\n// Save at most every 5 seconds\n@override\nDuration get throttle => const Duration(seconds: 5);\n\n// Disable throttling (save immediately on every change)\n@override\nDuration? get throttle => null;\n```\n\n## Forcing Immediate Save\n\nDispatch `PersistAction()` to save immediately, bypassing the throttle:\n\n```dart\nstore.dispatch(PersistAction());\n```\n\n## Pausing and Resuming Persistence\n\nControl persistence with these store methods:\n\n```dart\nstore.pausePersistor();           // Pause saving\nstore.persistAndPausePersistor(); // Save current state, then pause\nstore.resumePersistor();          // Resume saving\n```\n\n## App Lifecycle Integration\n\nPause persistence when the app goes to background and resume when it becomes active. Create an `AppLifecycleManager` widget:\n\n```dart\nclass AppLifecycleManager extends StatefulWidget {\n  final Widget child;\n\n  const AppLifecycleManager({\n    Key? key,\n    required this.child,\n  }) : super(key: key);\n\n  @override\n  _AppLifecycleManagerState createState() => _AppLifecycleManagerState();\n}\n\nclass _AppLifecycleManagerState extends State<AppLifecycleManager>\n    with WidgetsBindingObserver {\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState lifecycle) {\n    store.dispatch(ProcessLifecycleChange_Action(lifecycle));\n  }\n\n  @override\n  Widget build(BuildContext context) => widget.child;\n}\n```\n\nCreate an action to handle lifecycle changes:\n\n```dart\nclass ProcessLifecycleChange_Action extends ReduxAction<AppState> {\n  final AppLifecycleState lifecycle;\n\n  ProcessLifecycleChange_Action(this.lifecycle);\n\n  @override\n  Future<AppState?> reduce() async {\n    if (lifecycle == AppLifecycleState.resumed ||\n        lifecycle == AppLifecycleState.inactive) {\n      store.resumePersistor();\n    } else if (lifecycle == AppLifecycleState.paused ||\n        lifecycle == AppLifecycleState.detached) {\n      store.persistAndPausePersistor();\n    } else {\n      throw AssertionError(lifecycle);\n    }\n    return null;\n  }\n}\n```\n\nWrap your app with the lifecycle manager:\n\n```dart\nStoreProvider<AppState>(\n  store: store,\n  child: AppLifecycleManager(\n    child: MaterialApp( ... ),\n  ),\n)\n```\n\n## LocalPersist Helper\n\nThe `LocalPersist` class simplifies disk operations for Android/iOS. It works with simple object structures containing only primitives, lists, and maps.\n\n```dart\nimport 'package:async_redux/local_persist.dart';\n\n// Create instance with a file name\nvar persist = LocalPersist(\"myFile\");\n\n// Save data\nList<Object> simpleObjs = [\n  'Hello',\n  42,\n  true,\n  [100, 200, {\"name\": \"John\"}],\n];\nawait persist.save(simpleObjs);\n\n// Load data\nList<Object> loaded = await persist.load();\n\n// Append data\nList<Object> moreObjs = ['more', 'data'];\nawait persist.save(moreObjs, append: true);\n\n// File operations\nint length = await persist.length();\nbool exists = await persist.exists();\nawait persist.delete();\n\n// JSON operations for single objects\nawait persist.saveJson(simpleObj);\nObject? simpleObj = await persist.loadJson();\n```\n\n**Note:** `LocalPersist` only supports simple objects. For complex nested structures or custom classes, you need to implement serialization yourself (e.g., using JSON encoding with `toJson`/`fromJson` methods).\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/miscellaneous/persistence\n- https://asyncredux.com/flutter/basics/store\n- https://asyncredux.com/flutter/miscellaneous/database-and-cloud\n- https://asyncredux.com/flutter/intro\n- https://asyncredux.com/flutter/testing/mocking\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/about\n- https://asyncredux.com/flutter/testing/store-tester\n- https://asyncredux.com/flutter/miscellaneous/advanced-waiting\n"
  },
  {
    "path": ".claude/skills/asyncredux-provider-integration/SKILL.md",
    "content": "---\nname: asyncredux-provider-integration\ndescription: Integrate AsyncRedux with the Provider package. Covers using provider_for_redux, the ReduxSelector widget, and choosing between StoreConnector and ReduxSelector approaches.\n---\n\n# Provider Integration with AsyncRedux\n\nThe `provider_for_redux` package bridges Provider and AsyncRedux, enabling you to use Provider's dependency injection with Redux state management.\n\n## Installation\n\nAdd the package to your `pubspec.yaml`:\n\n```yaml\ndependencies:\n  provider_for_redux: ^8.0.0\n```\n\n## Setting Up AsyncReduxProvider\n\nReplace `StoreProvider` with `AsyncReduxProvider` to expose three items to descendant widgets:\n\n- The Redux store (`Store<AppState>`)\n- The application state (`AppState`)\n- The dispatch method (`Dispatch`)\n\n```dart\nimport 'package:provider_for_redux/provider_for_redux.dart';\n\nvoid main() {\n  final store = Store<AppState>(initialState: AppState.initial());\n\n  runApp(\n    AsyncReduxProvider<AppState>.value(\n      value: store,\n      child: MaterialApp(home: MyHomePage()),\n    ),\n  );\n}\n```\n\n## Accessing State with Provider.of\n\nAccess store components directly using standard Provider patterns:\n\n```dart\n// Access state (rebuilds on state changes)\nfinal counter = Provider.of<AppState>(context).counter;\n\n// Access dispatch (use listen: false for actions)\nProvider.of<Dispatch>(context, listen: false)(IncrementAction());\n\n// Access the store directly\nfinal store = Provider.of<Store<AppState>>(context, listen: false);\n```\n\n## ReduxConsumer Widget\n\n`ReduxConsumer` provides store, state, and dispatch in a single builder, simplifying access:\n\n```dart\nReduxConsumer<AppState>(\n  builder: (context, store, state, dispatch, child) {\n    return Column(\n      children: [\n        Text('Counter: ${state.counter}'),\n        Text('Description: ${state.description}'),\n        ElevatedButton(\n          onPressed: () => dispatch(IncrementAction()),\n          child: Text('Increment'),\n        ),\n      ],\n    );\n  },\n)\n```\n\n## ReduxSelector Widget\n\n`ReduxSelector` prevents unnecessary rebuilds by selecting specific state portions. Only when selected values change does the widget rebuild.\n\n### Using a List (Recommended)\n\nThe simplest approach - explicitly list the properties that should trigger rebuilds:\n\n```dart\nReduxSelector<AppState, dynamic>(\n  selector: (context, state) => [state.counter, state.description],\n  builder: (context, store, state, dispatch, model, child) {\n    return Column(\n      children: [\n        Text('Counter: ${state.counter}'),\n        Text('Description: ${state.description}'),\n        ElevatedButton(\n          onPressed: () => dispatch(IncrementAction()),\n          child: Text('Increment'),\n        ),\n      ],\n    );\n  },\n)\n```\n\n### Using a Custom Model\n\nFor structured data, use a Tuple or custom class:\n\n```dart\nReduxSelector<AppState, Tuple2<int, String>>(\n  selector: (context, state) => Tuple2(state.counter, state.description),\n  builder: (context, store, state, dispatch, model, child) {\n    return Column(\n      children: [\n        Text('Counter: ${model.item1}'),\n        Text('Description: ${model.item2}'),\n        ElevatedButton(\n          onPressed: () => dispatch(IncrementAction()),\n          child: Text('Increment'),\n        ),\n      ],\n    );\n  },\n)\n```\n\n## Choosing Between StoreConnector and ReduxSelector\n\nBoth approaches manage widget rebuilds during state changes, but serve different use cases:\n\n### Use StoreConnector When:\n\n- You want to separate smart (store-aware) and dumb (presentational) widgets\n- You need view-models with explicit equality comparison\n- You're building reusable UI components that shouldn't know about Redux\n- You want to test UI widgets in isolation without a store\n\n```dart\nclass MyCounterConnector extends StatelessWidget {\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel>(\n      vm: () => Factory(this),\n      builder: (context, vm) => MyCounter(\n        counter: vm.counter,\n        description: vm.description,\n        onIncrement: vm.onIncrement,\n      ),\n    );\n  }\n}\n\nclass ViewModel extends Vm {\n  final int counter;\n  final String description;\n  final VoidCallback onIncrement;\n\n  ViewModel({\n    required this.counter,\n    required this.description,\n    required this.onIncrement,\n  }) : super(equals: [counter, description]);\n}\n```\n\n### Use ReduxSelector When:\n\n- You prefer minimal boilerplate\n- Direct store access within a single widget is acceptable\n- You want Provider-style dependency injection\n- You're integrating with existing Provider-based code\n\n```dart\nReduxSelector<AppState, dynamic>(\n  selector: (context, state) => [state.counter, state.description],\n  builder: (context, store, state, dispatch, model, child) {\n    return MyCounter(\n      counter: state.counter,\n      description: state.description,\n      onIncrement: () => dispatch(IncrementAction()),\n    );\n  },\n)\n```\n\n## Migration Strategy\n\nBoth Provider and AsyncRedux connectors work simultaneously, enabling gradual migration:\n\n```dart\n// Old code using StoreConnector continues to work\nclass OldFeatureConnector extends StatelessWidget {\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, OldViewModel>(\n      vm: () => OldFactory(this),\n      builder: (context, vm) => OldFeatureWidget(vm: vm),\n    );\n  }\n}\n\n// New code can use ReduxSelector\nclass NewFeatureWidget extends StatelessWidget {\n  Widget build(BuildContext context) {\n    return ReduxSelector<AppState, dynamic>(\n      selector: (context, state) => [state.newFeature],\n      builder: (context, store, state, dispatch, model, child) {\n        return NewFeatureContent(feature: state.newFeature);\n      },\n    );\n  }\n}\n```\n\nThis allows you to migrate incrementally without rewriting your entire application.\n\n## Comparison Summary\n\n| Aspect | StoreConnector | ReduxSelector |\n|--------|---------------|---------------|\n| Boilerplate | More (ViewModel + Factory) | Less (inline selector) |\n| Separation | Smart/Dumb widget pattern | Single widget |\n| Testing | Easy to test UI in isolation | Requires store setup |\n| Provider compatibility | Native AsyncRedux | Full Provider integration |\n| Rebuild control | Via ViewModel equality | Via selector list |\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/other-packages/using-the-provider-package\n- https://asyncredux.com/flutter/connector/store-connector\n- https://asyncredux.com/flutter/connector/connector-pattern\n- https://asyncredux.com/flutter/basics/using-the-store-state\n- https://asyncredux.com/flutter/miscellaneous/widget-selectors\n- https://pub.dev/packages/provider_for_redux\n- https://github.com/marcglasberg/provider_for_redux\n"
  },
  {
    "path": ".claude/skills/asyncredux-retry-mixin/SKILL.md",
    "content": "---\nname: asyncredux-retry-mixin\ndescription: Add the Retry mixin for automatic retry with exponential backoff on action failure. Covers using Retry alone for limited retries, combining with UnlimitedRetries for infinite retries, and configuring retry behavior.\n---\n\n# Retry Mixin\n\nThe `Retry` mixin automatically retries failed actions using exponential backoff. When an error occurs in the `reduce()` method, the action is re-executed with progressively increasing delays between attempts.\n\n## Basic Usage\n\nAdd the `Retry` mixin to any action that should automatically retry on failure:\n\n```dart\nclass LoadDataAction extends AppAction with Retry {\n  Future<AppState?> reduce() async {\n    var data = await fetchDataFromServer();\n    return state.copy(data: data);\n  }\n}\n```\n\nWith this mixin:\n- If the action fails, it automatically retries up to 3 times (default)\n- Each retry waits longer than the previous (exponential backoff)\n- If all retries fail, the original error is thrown\n\n## Configuration Parameters\n\nOverride these getters to customize retry behavior:\n\n```dart\nclass LoadDataAction extends AppAction with Retry {\n\n  // Delay before first retry (default: 350ms)\n  int get initialDelay => 500;\n\n  // Multiplier for delay growth (default: 2)\n  int get multiplier => 2;\n\n  // Maximum retry attempts (default: 3)\n  int get maxRetries => 5;\n\n  // Upper limit on delay to prevent excessive waits (default: 5000ms)\n  int get maxDelay => 10000;\n\n  Future<AppState?> reduce() async {\n    var data = await fetchDataFromServer();\n    return state.copy(data: data);\n  }\n}\n```\n\n| Parameter | Default | Purpose |\n|-----------|---------|---------|\n| `initialDelay` | 350 ms | Waiting period before first retry |\n| `multiplier` | 2 | Growth factor for delays between attempts |\n| `maxRetries` | 3 | Maximum retry count (total executions = maxRetries + 1) |\n| `maxDelay` | 5 sec | Upper limit on delay to prevent excessive waits |\n\n### Retry Sequence Example\n\nWith default settings (initialDelay=350ms, multiplier=2, maxRetries=3):\n\n1. **Initial attempt** - Action runs, fails\n2. **Wait 350ms** - First retry, fails\n3. **Wait 700ms** - Second retry, fails\n4. **Wait 1400ms** - Third retry, fails\n5. **Error thrown** - All retries exhausted\n\n## Timing Considerations\n\nRetry delays start **after** the reducer finishes, not from when the action was dispatched. If `reduce()` takes 1 second to fail and `initialDelay` is 350ms, the first retry starts 1.35 seconds after the action began.\n\n## Tracking Retry Attempts\n\nAccess the `attempts` getter within your action to know which attempt is currently running:\n\n```dart\nclass LoadDataAction extends AppAction with Retry {\n  Future<AppState?> reduce() async {\n    print('Attempt ${attempts + 1}'); // 0-indexed, so first attempt is 0\n\n    if (attempts > 0) {\n      // Maybe try a different server on retries\n      return state.copy(data: await fetchFromBackupServer());\n    }\n\n    return state.copy(data: await fetchFromPrimaryServer());\n  }\n}\n```\n\n## Unlimited Retries\n\nCombine `UnlimitedRetries` with `Retry` to retry indefinitely until the action succeeds:\n\n```dart\nclass CriticalSyncAction extends AppAction with Retry, UnlimitedRetries {\n  Future<AppState?> reduce() async {\n    await syncCriticalData();\n    return state.copy(syncComplete: true);\n  }\n}\n```\n\nThis is equivalent to setting `maxRetries` to `-1`.\n\n**Warning:** Using `await dispatchAndWait(action)` with `UnlimitedRetries` may hang indefinitely if the action continues failing. Use with caution and consider whether the action has a realistic chance of eventually succeeding.\n\n## Important Behavior Notes\n\n### Only reduce() Failures Trigger Retry\n\nThe `Retry` mixin only retries when errors occur in the `reduce()` method. Failures in the `before()` method do **not** trigger retries - they fail immediately.\n\n```dart\nclass LoadDataAction extends AppAction with Retry {\n\n  @override\n  Future<void> before() async {\n    // Errors here will NOT trigger retry - action fails immediately\n    await validatePermissions();\n  }\n\n  Future<AppState?> reduce() async {\n    // Only errors here trigger the retry mechanism\n    return state.copy(data: await fetchData());\n  }\n}\n```\n\n### Actions Become Asynchronous\n\nAll actions using the `Retry` mixin become asynchronous, regardless of their original synchronous nature. This is because the retry mechanism needs to wait between attempts.\n\n## Combining with NonReentrant (Best Practice)\n\nMost actions using `Retry` should also include the `NonReentrant` mixin to prevent multiple instances from running simultaneously:\n\n```dart\nclass SaveDataAction extends AppAction with NonReentrant, Retry {\n  Future<AppState?> reduce() async {\n    await saveToServer();\n    return state.copy(saved: true);\n  }\n}\n```\n\nThis prevents scenarios where:\n- User clicks \"Save\" multiple times\n- Multiple retry sequences run in parallel\n- Server receives duplicate or conflicting requests\n\n## Combining with CheckInternet\n\nFor network operations, combine `Retry` with `CheckInternet` to ensure connectivity before attempting the action:\n\n```dart\nclass FetchUserProfile extends AppAction with CheckInternet, Retry {\n  Future<AppState?> reduce() async {\n    var profile = await api.getUserProfile();\n    return state.copy(profile: profile);\n  }\n}\n```\n\nThe `CheckInternet` mixin runs first. If there's no connection, the action fails immediately without attempting retries.\n\n## Common Use Cases\n\n### API Calls with Transient Failures\n\n```dart\nclass FetchProductsAction extends AppAction with Retry {\n  int get maxRetries => 3;\n  int get initialDelay => 500;\n\n  Future<AppState?> reduce() async {\n    var products = await api.getProducts();\n    return state.copy(products: products);\n  }\n}\n```\n\n### Critical Sync Operations\n\n```dart\nclass SyncPendingChanges extends AppAction with Retry, UnlimitedRetries {\n  int get initialDelay => 1000;\n  int get maxDelay => 30000; // Cap at 30 seconds between retries\n\n  Future<AppState?> reduce() async {\n    await syncService.pushPendingChanges();\n    return state.copy(hasPendingChanges: false);\n  }\n}\n```\n\n### Payment Processing with Extended Retries\n\n```dart\nclass ProcessPaymentAction extends AppAction with NonReentrant, Retry {\n  final double amount;\n\n  ProcessPaymentAction(this.amount);\n\n  int get maxRetries => 5;\n  int get initialDelay => 1000;\n  int get multiplier => 2;\n  int get maxDelay => 10000;\n\n  Future<AppState?> reduce() async {\n    var result = await paymentGateway.process(amount);\n    return state.copy(paymentStatus: result.status);\n  }\n}\n```\n\n## Mixin Compatibility\n\n**Compatible with:**\n- `CheckInternet`\n- `NoDialog`\n- `AbortWhenNoInternet`\n- `NonReentrant`\n- `Throttle`\n- `Debounce`\n\n**Can be combined with:**\n- `UnlimitedRetries` (enables infinite retries)\n\n## Full Example with All Options\n\n```dart\nclass RobustApiAction extends AppAction\n    with CheckInternet, NonReentrant, Retry {\n\n  // Retry configuration\n  int get initialDelay => 500;     // 500ms before first retry\n  int get multiplier => 2;          // Double delay each time\n  int get maxRetries => 4;          // Try up to 5 times total\n  int get maxDelay => 8000;         // Never wait more than 8 seconds\n\n  Future<AppState?> reduce() async {\n    if (attempts > 0) {\n      print('Retry attempt $attempts');\n    }\n\n    var data = await api.fetchCriticalData();\n    return state.copy(data: data);\n  }\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/advanced-actions/action-mixins\n- https://asyncredux.com/flutter/advanced-actions/control-mixins\n- https://asyncredux.com/flutter/advanced-actions/wrapping-the-reducer\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/basics/failed-actions\n- https://asyncredux.com/flutter/basics/wait-fail-succeed\n"
  },
  {
    "path": ".claude/skills/asyncredux-selectors/SKILL.md",
    "content": "---\nname: asyncredux-selectors\ndescription: Create and cache selectors for efficient state access. Covers writing selector functions, caching with `cache1` and `cache2`, the reselect pattern, and avoiding repeated computations in widgets.\n---\n\n## What Are Selectors?\n\nSelectors are functions that extract specific data from the Redux store state. They provide three key benefits:\n\n1. **Compute derived data** - Transform or filter state into the format your widget needs\n2. **Abstract state structure** - Components don't depend on how the state is organized\n3. **Enable caching (memoization)** - Avoid unnecessary recalculations\n\n## The Problem: Repeated Computations\n\nWhen displaying filtered or computed data, without selectors you might write:\n\n```dart\n// INEFFICIENT - filters the entire list on every access\nstate.users.where((user) => user.name.startsWith(\"A\")).toList()[index].name;\n```\n\nThis filtering operation runs every time the widget rebuilds, even when the data hasn't changed.\n\n## Basic Selector Functions\n\nCreate a selector function that performs the computation once:\n\n```dart\nList<User> selectUsersStartingWith(AppState state, String text) {\n  return state.users.where((user) => user.name.startsWith(text)).toList();\n}\n```\n\n## Cached Selectors (Reselectors)\n\nFor expensive computations, wrap your selector with a cache function. AsyncRedux provides built-in caching utilities.\n\n### Basic Caching Example\n\n```dart\nList<User> selectUsersStartingWith(AppState state, {required String text}) =>\n    _selectUsersStartingWith(state)(text);\n\nstatic final _selectUsersStartingWith = cache1state_1param(\n  (AppState state) => (String text) =>\n      state.users.where((user) => user.name.startsWith(text)).toList()\n);\n```\n\n### Optimized Caching (State Subset)\n\nFor better performance, only depend on the specific state subset that matters:\n\n```dart\nList<User> selectUsersStartingWith(AppState state, {required String text}) =>\n    _selectUsersStartingWith(state.users)(text);\n\nstatic final _selectUsersStartingWith = cache1state_1param(\n  (List<User> users) => (String text) =>\n      users.where((user) => user.name.startsWith(text)).toList()\n);\n```\n\nThis version only recalculates when `state.users` changes, not when any part of `state` changes.\n\n## Available Cache Functions\n\nAsyncRedux provides these caching functions:\n\n| Function | States | Parameters | Use Case |\n|----------|--------|------------|----------|\n| `cache1state` | 1 | 0 | Simple computed value from one state |\n| `cache1state_1param` | 1 | 1 | Filtered/computed value with one param |\n| `cache1state_2params` | 1 | 2 | Computation with two parameters |\n| `cache1state_0params_x` | 1 | Many | Variable number of parameters |\n| `cache2states` | 2 | 0 | Combines two state portions |\n| `cache2states_1param` | 2 | 1 | Combines two states with one param |\n| `cache2states_2params` | 2 | 2 | Combines two states with two params |\n| `cache2states_0params_x` | 2 | Many | Two states, variable params |\n| `cache3states` | 3 | 0 | Combines three state portions |\n| `cache3states_0params_x` | 3 | Many | Three states, variable params |\n\nThe naming convention: `cache[N]state[s]_[M]param[s]` where N = number of states, M = number of parameters.\n\n## Cache Characteristics\n\n- **Multiple cached results** - Maintains separate caches for different parameter combinations\n- **Weak-map storage** - Automatically discards cached data when states change or fall out of use\n- **Memory efficient** - Won't hold obsolete information\n\n## Action Selectors\n\nCreate selectors that actions can use via a dedicated class:\n\n```dart\nclass ActionSelect {\n  final AppState state;\n  ActionSelect(this.state);\n\n  List<Item> get items => state.items;\n  Item get selectedItem => state.selectedItem;\n\n  Item? findById(int id) =>\n      state.items.firstWhereOrNull((item) => item.id == id);\n\n  Item? searchByText(String text) =>\n      state.items.firstWhereOrNull((item) => item.text.contains(text));\n\n  int get selectedIndex => state.items.indexOf(state.selectedItem);\n}\n```\n\nAdd a getter in your base action:\n\n```dart\nabstract class AppAction extends ReduxAction<AppState> {\n  ActionSelect get select => ActionSelect(state);\n}\n```\n\nUsage in actions:\n\n```dart\nclass LoadItemAction extends AppAction {\n  final int itemId;\n  LoadItemAction(this.itemId);\n\n  @override\n  AppState? reduce() {\n    var item = select.findById(itemId);\n    if (item == null) return null;\n    return state.copy(selectedItem: item);\n  }\n}\n```\n\n## Widget Selectors\n\nCreate a `WidgetSelect` class for organized widget-level selectors:\n\n```dart\nclass WidgetSelect {\n  final BuildContext context;\n  WidgetSelect(this.context);\n\n  List<Item> get items => context.select((st) => st.items);\n  Item get selectedItem => context.select((st) => st.selectedItem);\n\n  Item? findById(int id) =>\n      context.select((st) => st.items.firstWhereOrNull((item) => item.id == id));\n\n  Item? searchByText(String text) =>\n      context.select((st) => st.items.firstWhereOrNull((item) => item.text.contains(text)));\n\n  int get selectedIndex =>\n      context.select((st) => st.items.indexOf(st.selectedItem));\n}\n```\n\nAdd to your BuildContext extension:\n\n```dart\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n  R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n  WidgetSelect get selector => WidgetSelect(this);\n}\n```\n\nUsage in widgets:\n\n```dart\nWidget build(BuildContext context) {\n  final item = context.selector.findById(42);\n  return Text(item?.name ?? 'Not found');\n}\n```\n\n## Reusing Action Selectors in Widgets\n\nWidget selectors can leverage action selectors to avoid duplication:\n\n```dart\nclass WidgetSelect {\n  final BuildContext context;\n  WidgetSelect(this.context);\n\n  Item? findById(int id) =>\n      context.select((st) => ActionSelect(st).findById(id));\n\n  Item? searchByText(String text) =>\n      context.select((st) => ActionSelect(st).searchByText(text));\n}\n```\n\n## Important Guidelines\n\n### Avoid context.state Inside Selectors\n\nNever use `context.state` inside selector functions - this defeats selective rebuilding:\n\n```dart\n// WRONG - rebuilds on any state change\nvar items = context.select((state) => context.state.items.where(...));\n\n// CORRECT - only rebuilds when items change\nvar items = context.select((state) => state.items.where(...));\n```\n\n### Never Nest context.select Calls\n\nNesting `context.select` causes errors:\n\n```dart\n// WRONG - will cause errors\nvar result = context.select((state) =>\n  context.select((s) => s.items).where(...)  // Nested select!\n);\n\n// CORRECT\nvar items = context.select((state) => state.items);\nvar result = items.where(...).toList();\n```\n\n## Comparison with External Reselect Package\n\nAsyncRedux's built-in caching differs from the external `reselect` package:\n\n| Feature | AsyncRedux | reselect |\n|---------|------------|----------|\n| Results per selector | Multiple (different params) | One only |\n| Memory on state change | Discards old cache | Retains indefinitely |\n\n## Complete Example: Cached Filtered List\n\n```dart\n// Selector with caching\nclass UserSelectors {\n  static List<User> usersStartingWith(AppState state, String prefix) =>\n      _usersStartingWith(state.users)(prefix);\n\n  static final _usersStartingWith = cache1state_1param(\n    (List<User> users) => (String prefix) =>\n        users.where((u) => u.name.startsWith(prefix)).toList()\n  );\n\n  static List<User> activeUsers(AppState state) =>\n      _activeUsers(state.users);\n\n  static final _activeUsers = cache1state(\n    (List<User> users) => users.where((u) => u.isActive).toList()\n  );\n}\n\n// Usage in widget\nWidget build(BuildContext context) {\n  var filtered = context.select(\n    (state) => UserSelectors.usersStartingWith(state, 'A')\n  );\n  return ListView.builder(\n    itemCount: filtered.length,\n    itemBuilder: (_, i) => Text(filtered[i].name),\n  );\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/miscellaneous/cached-selectors\n- https://asyncredux.com/flutter/miscellaneous/widget-selectors\n- https://asyncredux.com/flutter/advanced-actions/action-selectors\n- https://asyncredux.com/flutter/basics/using-the-store-state\n- https://asyncredux.com/flutter/connector/store-connector\n- https://asyncredux.com/flutter/connector/advanced-view-model\n"
  },
  {
    "path": ".claude/skills/asyncredux-setup/SKILL.md",
    "content": "---\nname: asyncredux-setup\ndescription: Initialize, setup and configure AsyncRedux in a Flutter app. Use it whenever starting a new AsyncRedux project, or when the user requests.\n---\n\n# AsyncRedux Setup\n\n## Adding the Dependency\n\nAdd AsyncRedux to your `pubspec.yaml`:\n\n```yaml\ndependencies:\n  async_redux: ^25.6.1\n```\n\nCheck [pub.dev](https://pub.dev/packages/async_redux) for the latest version.\n\n## Creating the State Class\n\nCreate an immutable `AppState` class (in file `app_state.dart`) with:\n\n* `copy()` method\n* `==` equals method\n* `hashCode` method\n* `initialState()` static factory\n\nIf the app is new, and you don't have any state yet, create an empty `AppState`:\n\n```dart\n@immutable\nclass AppState {\n  AppState();\n  static AppState initialState() => AppState();\n  AppState copy() => AppState();\n  \n  @override\n  bool operator ==(Object other) =>\n    identical(this, other) ||\n    other is AppState && runtimeType == other.runtimeType;\n\n  @override\n  int get hashCode => 0;\n}\n```\n\nIf there is existing state, create the `AppState` that incorporates that state.\nThis is an example:\n\n```dart\n@immutable\nclass AppState {\n  final String name;\n  final int age;\n  AppState({required this.name, required this.age});\n\n  static AppState initialState() => AppState(name: \"\", age: 0);\n\n  AppState copy({String? name, int? age}) => AppState(\n    name: name ?? this.name,\n    age: age ?? this.age,\n  );\n  \n  @override\n  bool operator ==(Object other) =>\n    identical(this, other) ||\n    other is AppState &&\n      runtimeType == other.runtimeType &&\n      name == other.name &&\n      age == other.age;\n\n  @override\n  int get hashCode => Object.hash(name, age);\n}\n\n```\n\nAll fields must be `final` (immutable). Add additional helper methods as needed:\n\n```dart\nAppState withName(String name) => copy(name: name);\nAppState withAge(int age) => copy(age: age);\n```\n\n## Creating the Store\n\nFind the place where you initialize your app (usually in `main.dart`),\nand import your `AppState` class (adapt the path as needed) and the AsyncRedux package:\n\n```dart\nimport 'app_state.dart'; \nimport 'package:async_redux/async_redux.dart';\n```\n\nCreate the store with your initial state. Note that `PersistorDummy`,\n`GlobalWrapErrorDummy`, and `ConsoleActionObserver` are provided by AsyncRedux for basic\nsetups. In the future these can be replaced with custom implementations as needed.\n\n```dart\nlate Store store;\n\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  // Create the persistor, and try to read any previously saved state.\n  var persistor = PersistorDummy<AppState>();\n  AppState? initialState = await persistor.readState();\n  \n  // If there is no saved state, create a new empty one and save it.\n  if (initialState == null) {\n    initialState = AppState.initialState();\n    await persistor.saveInitialState(initialState);\n  }\n    \n  // Create the store.\n  store = Store<AppState>(\n    initialState: initialState,\n    persistor: persistor,\n    globalWrapError: GlobalWrapErrorDummy(),    \n    actionObservers: [ConsoleActionObserver()],\n  );\n\n  runApp(...);\n}\n```\n\n## Wrapping with StoreProvider\n\nWrap your app with `StoreProvider` to make the store accessible.\nFind the root of the widget tree of the app, and add it above `MaterialApp` (or\n`CupertinoApp`, adapting as needed). Note you will need to import the `store` too.\n\n```dart\nimport 'package:async_redux/async_redux.dart';\n\nWidget build(context) {\n  return StoreProvider<AppState>(\n    store: store,\n    child: MaterialApp( ... ),\n  );\n}\n```\n\n## Required Context Extensions\n\nYou **must** add this extension to your file containing `AppState` (this is required for\neasier state access in widgets):\n\n```dart\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n```\n\n## Required base action\n\nCreate file `app_action.dart` with this abstract class extending `ReduxAction<AppState>`:\n\n```dart\n/// All actions extend this class.\nabstract class AppAction extends ReduxAction<AppState> {\n\nActionSelect get select => ActionSelect(state);\n}\n\n// Dedicated selector class to keep the base action clean.\nclass ActionSelect {\n  final AppState state;\n  ActionSelect(this.state);\n}\n```\n\n## Update CLAUDE.md\n\nAdd the following information to the project's `CLAUDE.md`, so that all actions extend\nthis\nbase action:\n\n```markdown\n## Base Action\n\nAll actions should extend `AppAction` instead of `ReduxAction<AppState>`. \nThere is a dedicated selector class called `ActionSelect` to keep the base action clean,\nby namespacing selectors under `select` and enabling IDE autocompletion. Example:\n\n  ```dart\n  class ProcessItem extends AppAction {\n    final String itemId;\n    ProcessItem(this.itemId);\n    \n    @override\n    AppState reduce() {\n      // IDE autocomplete shows: select.findById, select.completed, etc.\n      final item = select.findById(itemId);\n      // ...\n    }\n  }\n  ```\n\n```\n"
  },
  {
    "path": ".claude/skills/asyncredux-state-access/SKILL.md",
    "content": "---\nname: asyncredux-state-access\ndescription: Access store state in widgets using `context.state`, `context.select()`, and `context.read()`. Covers when to use each method, setting up BuildContext extensions, and optimizing widget rebuilds with selective state access.\n---\n\n## BuildContext Extension Setup\n\nTo access your application state in widgets, first define a `BuildContext` extension. Add this to your project (typically in a shared file that all widgets can import):\n\n```dart\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n  AppState read() => getRead<AppState>();\n  R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n  R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);\n}\n```\n\nReplace `AppState` with your actual state class name.\n\n## The Three State Access Methods\n\n### context.state\n\nGrants access to the entire state object. All widgets that use `context.state` will automatically rebuild whenever the store state changes (any part of it).\n\n```dart\nWidget build(BuildContext context) {\n  return Text('Counter: ${context.state.counter}');\n}\n```\n\n### context.select()\n\nRetrieves only specific state portions. This is more efficient as it only rebuilds the widget when the selected part of the state changes.\n\n```dart\nWidget build(BuildContext context) {\n  var counter = context.select((state) => state.counter);\n  return Text('Counter: $counter');\n}\n```\n\n### context.read()\n\nRetrieves state without triggering rebuilds. Use this in event handlers, `initState`, or anywhere you need to read state once without subscribing to changes.\n\n```dart\nvoid _onButtonPressed() {\n  var currentCount = context.read().counter;\n  print('Current count is $currentCount');\n}\n```\n\n## When to Use Each Method\n\n| Method | Use In | Triggers Rebuilds? | Best For |\n|--------|--------|-------------------|----------|\n| `context.state` | `build` method | Yes, on any state change | Simple widgets or when you need many state properties |\n| `context.select()` | `build` method | Only when selected part changes | Performance-sensitive widgets |\n| `context.read()` | `initState`, event handlers, callbacks | No | One-time reads, button handlers |\n\n## Accessing Multiple State Properties\n\nWhen you need several pieces of state, you have two options:\n\n**Option 1: Multiple select calls**\n\n```dart\nWidget build(BuildContext context) {\n  var name = context.select((state) => state.user.name);\n  var email = context.select((state) => state.user.email);\n  var itemCount = context.select((state) => state.items.length);\n  // Widget rebuilds only if name, email, or itemCount changes\n  return Text('$name ($email) - $itemCount items');\n}\n```\n\n**Option 2: Dart records for combined selection**\n\n```dart\nWidget build(BuildContext context) {\n  var (name, email) = context.select((state) => (state.user.name, state.user.email));\n  return Text('$name ($email)');\n}\n```\n\n## Additional Context Methods for Action States\n\nBeyond state access, the context extension provides methods for tracking async action progress:\n\n```dart\nWidget build(BuildContext context) {\n  // Check if an action is currently running\n  if (context.isWaiting(LoadDataAction)) {\n    return CircularProgressIndicator();\n  }\n\n  // Check if an action failed\n  if (context.isFailed(LoadDataAction)) {\n    var exception = context.exceptionFor(LoadDataAction);\n    return Text('Error: ${exception?.message}');\n  }\n\n  // Show the data\n  return Text('Data: ${context.state.data}');\n}\n```\n\nAvailable methods:\n- `context.isWaiting(ActionType)` - Returns true if the action is in progress\n- `context.isFailed(ActionType)` - Returns true if the action recently failed\n- `context.exceptionFor(ActionType)` - Gets the exception from a failed action\n- `context.clearExceptionFor(ActionType)` - Manually clears the stored exception\n\n## Widget Selectors Pattern\n\nFor complex selection logic, create a `WidgetSelect` class to organize reusable selectors:\n\n```dart\nclass WidgetSelect {\n  final BuildContext context;\n  WidgetSelect(this.context);\n\n  // Getter shortcuts\n  List<Item> get items => context.select((state) => state.items);\n  User get currentUser => context.select((state) => state.user);\n\n  // Custom finder methods\n  Item? findById(int id) => context.select(\n    (state) => state.items.firstWhereOrNull((item) => item.id == id)\n  );\n\n  List<Item> searchByText(String text) => context.select(\n    (state) => state.items.where((item) => item.name.contains(text)).toList()\n  );\n}\n```\n\nAdd it to your BuildContext extension:\n\n```dart\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n  // ... other methods ...\n  WidgetSelect get selector => WidgetSelect(this);\n}\n```\n\nUsage in widgets:\n\n```dart\nWidget build(BuildContext context) {\n  var user = context.selector.currentUser;\n  var item = context.selector.findById(42);\n  return Text('${user.name}: ${item?.name}');\n}\n```\n\n## Important Guidelines\n\n### Avoid context.state inside selectors\n\nNever use `context.state` inside your selector functions. This defeats the purpose of selective rebuilding:\n\n```dart\n// WRONG - rebuilds on any state change\nvar items = context.select((state) => context.state.items.where(...));\n\n// CORRECT - only rebuilds when items change\nvar items = context.select((state) => state.items.where(...));\n```\n\n### Never nest context.select calls\n\nNesting `context.select` causes errors. Always apply selection at the top level:\n\n```dart\n// WRONG - will cause errors\nvar result = context.select((state) =>\n  context.select((s) => s.items).where(...) // Nested select!\n);\n\n// CORRECT\nvar items = context.select((state) => state.items);\nvar result = items.where(...);\n```\n\n## Debugging Rebuilds\n\nTo observe when widgets rebuild (useful for performance debugging), use a `ModelObserver`:\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  modelObserver: DefaultModelObserver(),\n);\n```\n\nThe `DefaultModelObserver` logs console output showing:\n- Whether a rebuild occurred\n- Which connector/widget triggered it\n- The view model state\n\nExample output:\n```\nModel D:1 R:1 = Rebuild:true, Connector:MyWidgetConnector, Model:MyViewModel{counter: 5}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/basics/using-the-store-state\n- https://asyncredux.com/flutter/miscellaneous/widget-selectors\n- https://asyncredux.com/flutter/miscellaneous/observing-rebuilds\n- https://asyncredux.com/flutter/miscellaneous/cached-selectors\n- https://asyncredux.com/flutter/basics/store\n- https://asyncredux.com/flutter/advanced-actions/action-selectors\n- https://asyncredux.com/flutter/connector/store-connector\n- https://asyncredux.com/flutter/intro\n- https://asyncredux.com/flutter/basics/wait-fail-succeed\n"
  },
  {
    "path": ".claude/skills/asyncredux-state-design/SKILL.md",
    "content": "---\nname: asyncredux-state-design\ndescription: Design immutable state classes following AsyncRedux best practices. Includes creating the AppState class with a `copy()` method, defining `initialState()`, composing nested state objects, and optionally using the fast_immutable_collections package for IList, ISet, and IMap.\n---\n\n# AsyncRedux State Design\n\n## Core Principle: Immutability\n\nState classes must be immutable—fields cannot be modified after creation. Instead of changing state directly, you create new instances. All fields should be marked `final`.\n\n## Basic State Class Structure\n\n```dart\nclass AppState {\n  final String name;\n  final int age;\n\n  AppState({required this.name, required this.age});\n\n  static AppState initialState() => AppState(name: \"\", age: 0);\n\n  AppState copy({String? name, int? age}) =>\n      AppState(\n        name: name ?? this.name,\n        age: age ?? this.age,\n      );\n}\n```\n\n### Key Components\n\n1. **Final fields** - All state fields must be `final`\n2. **`initialState()`** - Static factory method providing default values\n3. **`copy()` method** - Creates modified instances without mutating original\n\n## The copy() Method Pattern\n\nThe `copy()` method accepts optional parameters for each field. If a parameter is null, it keeps the existing value:\n\n```dart\nAppState copy({String? name, int? age}) =>\n    AppState(\n      name: name ?? this.name,\n      age: age ?? this.age,\n    );\n```\n\nYou can also add convenience methods:\n\n```dart\nAppState withName(String name) => copy(name: name);\nAppState withAge(int age) => copy(age: age);\n```\n\n## Nested/Composite State\n\nFor complex applications, compose multiple state classes within a single `AppState`:\n\n```dart\nclass AppState {\n  final TodoList todoList;\n  final User user;\n  final Settings settings;\n\n  AppState({\n    required this.todoList,\n    required this.user,\n    required this.settings,\n  });\n\n  static AppState initialState() => AppState(\n    todoList: TodoList.initialState(),\n    user: User.initialState(),\n    settings: Settings.initialState(),\n  );\n\n  AppState copy({\n    TodoList? todoList,\n    User? user,\n    Settings? settings,\n  }) =>\n      AppState(\n        todoList: todoList ?? this.todoList,\n        user: user ?? this.user,\n        settings: settings ?? this.settings,\n      );\n}\n```\n\nEach nested class follows the same pattern:\n\n```dart\nclass User {\n  final String name;\n  final String email;\n\n  User({required this.name, required this.email});\n\n  static User initialState() => User(name: \"\", email: \"\");\n\n  User copy({String? name, String? email}) =>\n      User(\n        name: name ?? this.name,\n        email: email ?? this.email,\n      );\n}\n```\n\n## Updating Nested State in Actions\n\n```dart\nclass UpdateUserName extends ReduxAction<AppState> {\n  final String name;\n  UpdateUserName(this.name);\n\n  @override\n  AppState reduce() {\n    var newUser = state.user.copy(name: name);\n    return state.copy(user: newUser);\n  }\n}\n```\n\n## Using fast_immutable_collections\n\nFor lists, sets, and maps, use the `fast_immutable_collections` package (by the same author as AsyncRedux):\n\n```yaml\ndependencies:\n  fast_immutable_collections: ^10.0.0\n```\n\n### IList Example\n\nUse `Iterable` in constructors and copy methods, with `IList.orNull()` for conversion. This lets callers pass any iterable (List, Set, IList) without manual conversion:\n\n```dart\nimport 'package:fast_immutable_collections/fast_immutable_collections.dart';\n\nclass AppState {\n  final IList<Todo> todos;\n\n  AppState({\n    Iterable<Todo>? todos,\n  }) : todos = IList.orNull(todos) ?? const IList.empty();\n\n  static AppState initialState() => AppState();\n\n  AppState copy({Iterable<Todo>? todos}) =>\n      AppState(todos: IList.orNull(todos) ?? this.todos);\n\n  // Convenience methods with business logic\n  AppState addTodo(Todo todo) => copy(todos: todos.add(todo));\n  AppState removeTodo(Todo todo) => copy(todos: todos.remove(todo));\n  AppState toggleTodo(int index) => copy(\n    todos: todos.replace(index, todos[index].copy(done: !todos[index].done)),\n  );\n}\n\n// Flexible usage:\nvar state = AppState();                           // Empty list\nvar state = AppState(todos: [todo1, todo2]);      // List works\nvar state = AppState(todos: {todo1, todo2});      // Set works\nvar state = AppState(todos: existingIList);       // IList reused (no copy)\n```\n\n### IMap Example\n\nUse `Map` in constructors and copy methods, with `IMap.orNull()` for conversion:\n\n```dart\nclass AppState {\n  final IMap<String, User> usersById;\n\n  AppState({\n    Map<String, User>? usersById,\n  }) : usersById = IMap.orNull(usersById) ?? const IMap.empty();\n\n  static AppState initialState() => AppState();\n\n  AppState copy({Map<String, User>? usersById}) =>\n      AppState(usersById: IMap.orNull(usersById) ?? this.usersById);\n\n  AppState addUser(User user) => copy(usersById: usersById.add(user.id, user));\n  AppState removeUser(String id) => copy(usersById: usersById.remove(id));\n}\n```\n\n### ISet Example\n\nUse `Iterable` in constructors and copy methods, with `ISet.orNull()` for conversion:\n\n```dart\nclass AppState {\n  final ISet<String> selectedIds;\n\n  AppState({\n    Iterable<String>? selectedIds,\n  }) : selectedIds = ISet.orNull(selectedIds) ?? const ISet.empty();\n\n  static AppState initialState() => AppState();\n\n  AppState copy({Iterable<String>? selectedIds}) =>\n      AppState(selectedIds: ISet.orNull(selectedIds) ?? this.selectedIds);\n\n  AppState toggleSelection(String id) => copy(\n    selectedIds: selectedIds.contains(id)\n        ? selectedIds.remove(id)\n        : selectedIds.add(id),\n  );\n}\n```\n\n## Events in State\n\nFor one-time UI interactions (scrolling, text field changes), use `Evt`:\n\n```dart\nclass AppState {\n  final Evt clearTextEvt;\n  final Evt<String> changeTextEvt;\n\n  AppState({\n    required this.clearTextEvt,\n    required this.changeTextEvt,\n  });\n\n  static AppState initialState() => AppState(\n    clearTextEvt: Evt.spent(),\n    changeTextEvt: Evt<String>.spent(),\n  );\n\n  AppState copy({\n    Evt? clearTextEvt,\n    Evt<String>? changeTextEvt,\n  }) =>\n      AppState(\n        clearTextEvt: clearTextEvt ?? this.clearTextEvt,\n        changeTextEvt: changeTextEvt ?? this.changeTextEvt,\n      );\n}\n```\n\nEvents are initialized as \"spent\" and become active when replaced with new instances in actions.\n\n## Business Logic in State Classes\n\nAsyncRedux recommends placing business logic in state classes, not in actions or widgets:\n\n```dart\nclass TodoList {\n  final IList<Todo> items;\n\n  TodoList({required this.items});\n\n  // Business logic methods\n  int get completedCount => items.where((t) => t.done).length;\n  int get pendingCount => items.length - completedCount;\n  double get completionRate => items.isEmpty ? 0 : completedCount / items.length;\n\n  IList<Todo> get completed => items.where((t) => t.done).toIList();\n  IList<Todo> get pending => items.where((t) => !t.done).toIList();\n\n  TodoList addTodo(Todo todo) => TodoList(items: items.add(todo));\n  TodoList removeTodo(Todo todo) => TodoList(items: items.remove(todo));\n}\n```\n\nActions become simple orchestrators:\n\n```dart\nclass AddTodo extends ReduxAction<AppState> {\n  final Todo todo;\n  AddTodo(this.todo);\n\n  @override\n  AppState reduce() => state.copy(\n    todoList: state.todoList.addTodo(todo),\n  );\n}\n```\n\n## State Access in Actions\n\nActions access state through getters:\n\n- **`state`** - Current state (updates after each `await` in async actions)\n- **`initialState`** - State when the action was first dispatched (never changes)\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    var originalValue = initialState.counter; // Preserved\n    await someAsyncWork();\n    var currentValue = state.counter; // May have changed\n    return state.copy(counter: currentValue + 1);\n  }\n}\n```\n\n## Testing Benefits\n\nImmutable state with pure methods makes unit testing straightforward:\n\n```dart\nvoid main() {\n  test('addTodo adds item to list', () {\n    var state = AppState.initialState();\n    var todo = Todo(text: 'Test', done: false);\n\n    var newState = state.addTodo(todo);\n\n    expect(newState.todos.length, 1);\n    expect(newState.todos.first.text, 'Test');\n    expect(state.todos.length, 0); // Original unchanged\n  });\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/basics/state\n- https://asyncredux.com/flutter/basics/sync-actions\n- https://asyncredux.com/flutter/basics/changing-state-is-optional\n- https://asyncredux.com/flutter/basics/actions-and-reducers\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/basics/events\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/miscellaneous/business-logic\n- https://asyncredux.com/flutter/miscellaneous/persistence\n- https://asyncredux.com/flutter/connector/store-connector\n- https://asyncredux.com/flutter/testing/mocking\n- https://asyncredux.com/flutter/intro\n- https://asyncredux.com/flutter/about\n"
  },
  {
    "path": ".claude/skills/asyncredux-streams-timers/SKILL.md",
    "content": "---\nname: asyncredux-streams-timers\ndescription: Manage Streams and Timers with AsyncRedux. Covers creating actions to start/stop streams, storing stream subscriptions in store props, dispatching actions from stream callbacks, and proper cleanup with disposeProps().\n---\n\n# AsyncRedux Streams and Timers\n\n## Core Principles\n\nTwo fundamental rules for working with streams and timers in AsyncRedux:\n\n1. **Don't send streams or timers down to widgets.** Don't declare, subscribe, or unsubscribe to them inside widgets.\n\n2. **Don't put streams or timers in the Redux store state.** They produce state changes, but they are not state themselves.\n\nInstead, store streams and timers in the store's **props** - a key-value container that can hold any object type.\n\n## Store Props API\n\nAsyncRedux provides methods for managing props in both `Store` and `ReduxAction`:\n\n### `setProp(key, value)`\n\nStores an object (timer, stream subscription, etc.) in the store's props:\n\n```dart\nsetProp('myTimer', Timer.periodic(Duration(seconds: 1), callback));\nsetProp('priceStream', priceStream.listen(onData));\n```\n\n### `prop<T>(key)`\n\nRetrieves a property from the store:\n\n```dart\nvar timer = prop<Timer>('myTimer');\nvar subscription = prop<StreamSubscription>('priceStream');\n```\n\n### `disposeProp(key)`\n\nDisposes a single property by its key. Automatically cancels/closes timers, futures, and stream subscriptions:\n\n```dart\ndisposeProp('myTimer'); // Cancels the timer and removes from props\n```\n\n### `disposeProps([predicate])`\n\nDisposes multiple properties. Without a predicate, disposes all Timer, Future, and Stream-related props:\n\n```dart\n// Dispose all timers, futures, stream subscriptions\ndisposeProps();\n\n// Dispose only timers\ndisposeProps(({Object? key, Object? value}) => value is Timer);\n\n// Dispose props with specific keys\ndisposeProps(({Object? key, Object? value}) => key.toString().startsWith('temp_'));\n```\n\n## Timer Pattern\n\n### Starting a Timer\n\nCreate an action that sets up a `Timer.periodic` and stores it in props:\n\n```dart\nclass StartPollingAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    // Store the timer in props\n    setProp('pollingTimer', Timer.periodic(\n      Duration(seconds: 5),\n      (timer) => dispatch(FetchDataAction()),\n    ));\n    return null; // No state change from this action\n  }\n}\n```\n\n### Stopping a Timer\n\nCreate an action to dispose the timer:\n\n```dart\nclass StopPollingAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    disposeProp('pollingTimer');\n    return null;\n  }\n}\n```\n\n### Timer with Tick Count\n\nAccess the timer's tick count in callbacks:\n\n```dart\nclass StartTimerAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    setProp('myTimer', Timer.periodic(\n      Duration(seconds: 1),\n      (timer) => dispatch(UpdateTickAction(timer.tick)),\n    ));\n    return null;\n  }\n}\n\nclass UpdateTickAction extends ReduxAction<AppState> {\n  final int tick;\n  UpdateTickAction(this.tick);\n\n  @override\n  AppState? reduce() => state.copy(tickCount: tick);\n}\n```\n\n## Stream Pattern\n\n### Subscribing to a Stream\n\nCreate an action that subscribes to a stream and stores the subscription:\n\n```dart\nclass StartListeningAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    final subscription = myDataStream.listen(\n      (data) => dispatch(DataReceivedAction(data)),\n      onError: (error) => dispatch(StreamErrorAction(error)),\n    );\n    setProp('dataSubscription', subscription);\n    return null;\n  }\n}\n```\n\n### Unsubscribing from a Stream\n\n```dart\nclass StopListeningAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    disposeProp('dataSubscription');\n    return null;\n  }\n}\n```\n\n### Handling Stream Data\n\nThe stream callback dispatches an action with the data, which updates the state:\n\n```dart\nclass DataReceivedAction extends ReduxAction<AppState> {\n  final MyData data;\n  DataReceivedAction(this.data);\n\n  @override\n  AppState? reduce() => state.copy(latestData: data);\n}\n```\n\n## Lifecycle Management\n\n### Screen-Specific Streams/Timers\n\nUse `StoreConnector`'s `onInit` and `onDispose` callbacks:\n\n```dart\nclass PriceScreen extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, _Vm>(\n      vm: () => _Factory(),\n      onInit: _onInit,\n      onDispose: _onDispose,\n      builder: (context, vm) => PriceWidget(price: vm.price),\n    );\n  }\n\n  void _onInit(Store<AppState> store) {\n    store.dispatch(StartPriceStreamAction());\n  }\n\n  void _onDispose(Store<AppState> store) {\n    store.dispatch(StopPriceStreamAction());\n  }\n}\n```\n\n### App-Wide Streams/Timers\n\nStart after store creation, stop when app closes:\n\n```dart\nvoid main() {\n  final store = Store<AppState>(initialState: AppState.initialState());\n\n  // Start app-wide streams/timers\n  store.dispatch(StartGlobalPollingAction());\n\n  runApp(StoreProvider<AppState>(\n    store: store,\n    child: MyApp(),\n  ));\n}\n\n// In your app's dispose logic\nstore.dispatch(StopGlobalPollingAction());\nstore.disposeProps(); // Clean up all remaining props\nstore.shutdown();\n```\n\n### Single Action That Toggles\n\nCombine start/stop in one action:\n\n```dart\nclass TogglePollingAction extends ReduxAction<AppState> {\n  final bool start;\n  TogglePollingAction(this.start);\n\n  @override\n  AppState? reduce() {\n    if (start) {\n      setProp('polling', Timer.periodic(\n        Duration(seconds: 5),\n        (_) => dispatch(RefreshDataAction()),\n      ));\n    } else {\n      disposeProp('polling');\n    }\n    return null;\n  }\n}\n```\n\n## Complete Example: Real-Time Price Updates\n\n```dart\n// State\nclass AppState {\n  final double price;\n  final bool isStreaming;\n\n  AppState({required this.price, required this.isStreaming});\n\n  static AppState initialState() => AppState(price: 0.0, isStreaming: false);\n\n  AppState copy({double? price, bool? isStreaming}) => AppState(\n    price: price ?? this.price,\n    isStreaming: isStreaming ?? this.isStreaming,\n  );\n}\n\n// Start streaming prices\nclass StartPriceStreamAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    // Don't start if already streaming\n    if (state.isStreaming) return null;\n\n    final subscription = priceService.priceStream.listen(\n      (price) => dispatch(UpdatePriceAction(price)),\n      onError: (e) => dispatch(PriceStreamErrorAction(e)),\n    );\n\n    setProp('priceSubscription', subscription);\n    return state.copy(isStreaming: true);\n  }\n}\n\n// Stop streaming prices\nclass StopPriceStreamAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    if (!state.isStreaming) return null;\n\n    disposeProp('priceSubscription');\n    return state.copy(isStreaming: false);\n  }\n}\n\n// Handle price updates\nclass UpdatePriceAction extends ReduxAction<AppState> {\n  final double price;\n  UpdatePriceAction(this.price);\n\n  @override\n  AppState? reduce() => state.copy(price: price);\n}\n\n// Handle stream errors\nclass PriceStreamErrorAction extends ReduxAction<AppState> {\n  final Object error;\n  PriceStreamErrorAction(this.error);\n\n  @override\n  AppState? reduce() {\n    // Stop streaming on error\n    disposeProp('priceSubscription');\n    return state.copy(isStreaming: false);\n  }\n}\n```\n\n## Testing onInit/onDispose\n\nUse `ConnectorTester` to test lifecycle callbacks without full widget tests:\n\n```dart\ntest('starts and stops polling on screen lifecycle', () async {\n  var store = Store<AppState>(initialState: AppState.initialState());\n  var connectorTester = store.getConnectorTester(PriceScreen());\n\n  // Simulate screen entering view\n  connectorTester.runOnInit();\n  var startAction = await store.waitAnyActionTypeFinishes([StartPriceStreamAction]);\n  expect(store.state.isStreaming, true);\n\n  // Simulate screen leaving view\n  connectorTester.runOnDispose();\n  var stopAction = await store.waitAnyActionTypeFinishes([StopPriceStreamAction]);\n  expect(store.state.isStreaming, false);\n});\n```\n\n## Cleanup on Store Shutdown\n\nCall `disposeProps()` before shutting down the store to clean up all remaining timers and stream subscriptions:\n\n```dart\n// Clean up all Timer, Future, and Stream-related props\nstore.disposeProps();\n\n// Shut down the store\nstore.shutdown();\n```\n\nThe `disposeProps()` method automatically:\n- Cancels `Timer` objects\n- Cancels `StreamSubscription` objects\n- Closes `StreamController` and `StreamSink` objects\n- Ignores `Future` objects (to prevent unhandled errors)\n\nRegular (non-disposable) props are kept unless you provide a predicate that matches them.\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/miscellaneous/streams-and-timers\n- https://asyncredux.com/flutter/basics/store\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/testing/testing-oninit-ondispose\n- https://asyncredux.com/flutter/miscellaneous/dependency-injection\n"
  },
  {
    "path": ".claude/skills/asyncredux-sync-actions/SKILL.md",
    "content": "---\nname: asyncredux-sync-actions\ndescription: Creates AsyncRedux (Flutter) synchronous actions that update state immediately by implementing reduce() to return a new state. \n---\n\n# AsyncRedux Sync Actions\n\n## Basic Sync Action Structure\n\nA synchronous action returns `AppState?` from its `reduce()` method. The action completes\nimmediately and state updates right away.\n\n```dart\nclass Increment extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() => state.copy(counter: state.counter + 1);\n}\n```\n\n## Key Components\n\n### Extending ReduxAction\n\nEvery action extends `ReduxAction<AppState>`:\n\n```dart\nclass MyAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    // Return new state\n  }\n}\n```\n\n### The `state` Getter\n\nInside `reduce()`, access current state via the `state` getter:\n\n```dart\nclass ToggleFlag extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() => state.copy(flag: !state.flag);\n}\n```\n\n### Passing Parameters via Constructor\n\nPass data to actions through constructor fields:\n\n```dart\nclass SetName extends ReduxAction<AppState> {\n  final String name;\n  SetName(this.name);\n\n  @override\n  AppState? reduce() => state.copy(name: name);\n}\n\nclass IncrementBy extends ReduxAction<AppState> {\n  final int amount;\n  IncrementBy({required this.amount});\n\n  @override\n  AppState? reduce() => state.copy(counter: state.counter + amount);\n}\n```\n\n### Modifying Nested State\n\nFor nested state objects, create the new nested object first:\n\n```dart\nclass UpdateUserName extends ReduxAction<AppState> {\n  final String name;\n  UpdateUserName(this.name);\n\n  @override\n  AppState? reduce() {\n    var newUser = state.user.copy(name: name);\n    return state.copy(user: newUser);\n  }\n}\n```\n\n## Dispatching Sync Actions\n\n### From Widgets\n\nUse context extensions:\n\n```dart\n// Fire and forget\ncontext.dispatch(Increment());\n\n// With parameters\ncontext.dispatch(SetName('Alice'));\ncontext.dispatch(IncrementBy(amount: 5));\n```\n\n### Immediate State Update\n\nSync actions update state immediately:\n\n```dart\nprint(store.state.counter); // 2\nstore.dispatch(IncrementBy(amount: 3));\nprint(store.state.counter); // 5\n```\n\n### Guaranteed Sync with dispatchSync()\n\nThe `dispatchSync()` throws `StoreException` if the action is async. Otherwise, it\nbehaves exactly like `dispatch()`.\n\nUse `dispatchSync()` only in the rare cases when you must ensure the action is synchronous\nbecause you need the state to be applied right after the dispatch returns. \n\n```dart\ncontext.dispatchSync(Increment());\n```\n\n### From Other Actions\n\nActions can dispatch other actions:\n\n```dart\nclass ResetAndIncrement extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    dispatch(Reset());\n    dispatch(Increment());\n    return null; // This action itself doesn't change state\n  }\n}\n```\n\n## Returning Null (No State Change)\n\nReturn `null` when you don't need to change state:\n\n```dart\nclass LogCurrentState extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    print('Current counter: ${state.counter}');\n    return null; // No state change\n  }\n}\n```\n\nConditional state changes:\n\n```dart\nclass IncrementIfPositive extends ReduxAction<AppState> {\n  final int amount;\n  IncrementIfPositive(this.amount);\n\n  @override\n  AppState? reduce() {\n    if (amount <= 0) return null;\n    return state.copy(counter: state.counter + amount);\n  }\n}\n```\n\n## Action Simplification with Base Class\n\nCreate a base action class to reduce boilerplate:\n\n```dart\n// Define once\nabstract class AppAction extends ReduxAction<AppState> {}\n\n// Use everywhere\nclass Increment extends AppAction {\n  @override\n  AppState? reduce() => state.copy(counter: state.counter + 1);\n}\n\nclass SetName extends AppAction {\n  final String name;\n  SetName(this.name);\n\n  @override\n  AppState? reduce() => state.copy(name: name);\n}\n```\n\nYou can add shared functionality to your base class:\n\n```dart\nabstract class AppAction extends ReduxAction<AppState> {\n  // Shortcuts to state parts\n  User get user => state.user;\n  Settings get settings => state.settings;\n}\n\nclass UpdateEmail extends AppAction {\n  final String email;\n  UpdateEmail(this.email);\n\n  @override\n  AppState? reduce() => state.copy(\n    user: user.copy(email: email), // Uses shortcut\n  );\n}\n```\n\n## Return Type Warning\n\nThe `reduce()` method signature is `FutureOr<AppState?>`. For sync actions, always return\n`AppState?` directly:\n\n```dart\n// CORRECT - Sync action\nAppState? reduce() => state.copy(counter: state.counter + 1);\n\n// WRONG - Don't return FutureOr directly\nFutureOr<AppState?> reduce() => state.copy(counter: state.counter + 1);\n```\n\nIf you return `FutureOr<AppState?>` directly, AsyncRedux cannot determine if the action is\nsync or async and will throw a `StoreException`.\n\n## Complete Example\n\n```dart\n// State\nclass AppState {\n  final int counter;\n  final String name;\n\n  AppState({required this.counter, required this.name});\n\n  static AppState initialState() => AppState(counter: 0, name: '');\n\n  AppState copy({int? counter, String? name}) => AppState(\n    counter: counter ?? this.counter,\n    name: name ?? this.name,\n  );\n}\n\n// Base action\nabstract class AppAction extends ReduxAction<AppState> {}\n\n// Sync actions\nclass Increment extends AppAction {\n  @override\n  AppState? reduce() => state.copy(counter: state.counter + 1);\n}\n\nclass Decrement extends AppAction {\n  @override\n  AppState? reduce() => state.copy(counter: state.counter - 1);\n}\n\nclass IncrementBy extends AppAction {\n  final int amount;\n  IncrementBy(this.amount);\n\n  @override\n  AppState? reduce() => state.copy(counter: state.counter + amount);\n}\n\nclass SetName extends AppAction {\n  final String name;\n  SetName(this.name);\n\n  @override\n  AppState? reduce() => state.copy(name: name);\n}\n\nclass Reset extends AppAction {\n  @override\n  AppState? reduce() => AppState.initialState();\n}\n\n// Usage in widget\nElevatedButton(\n  onPressed: () => context.dispatch(IncrementBy(5)),\n  child: Text('Add 5'),\n)\n```\n\n## References\n\nURLs from the documentation:\n\n- https://asyncredux.com/flutter/basics/sync-actions\n- https://asyncredux.com/flutter/basics/actions-and-reducers\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/basics/dispatching-actions\n- https://asyncredux.com/flutter/basics/action-simplification\n- https://asyncredux.com/flutter/basics/changing-state-is-optional\n"
  },
  {
    "path": ".claude/skills/asyncredux-testing-basics/SKILL.md",
    "content": "---\nname: asyncredux-testing-basics\ndescription: Write unit tests for AsyncRedux actions using the Store directly. Covers creating test stores with initial state, using `dispatchAndWait()`, checking state after actions, verifying action errors via ActionStatus, and testing async actions.\n---\n\n# Testing AsyncRedux Actions\n\nThe recommended approach for testing AsyncRedux is to use the `Store` directly rather than the deprecated `StoreTester`. This provides a clean, straightforward testing pattern.\n\n## Creating a Test Store\n\nCreate a store with test-specific initial state:\n\n```dart\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:async_redux/async_redux.dart';\n\nvoid main() {\n  test('should increment counter', () async {\n    // Create store with initial state\n    var store = Store<AppState>(\n      initialState: AppState(counter: 0, name: ''),\n    );\n\n    // Test your actions here\n  });\n}\n```\n\nFor test isolation, create a fresh store in each test:\n\n```dart\nvoid main() {\n  late Store<AppState> store;\n\n  setUp(() {\n    store = Store<AppState>(\n      initialState: AppState.initialState(),\n    );\n  });\n\n  tearDown(() {\n    store.shutdown();\n  });\n\n  // Tests go here\n}\n```\n\n## Basic Test Pattern: Dispatch, Wait, Expect\n\nUse `dispatchAndWait()` to dispatch an action and wait for it to complete:\n\n```dart\ntest('SaveNameAction updates the name', () async {\n  var store = Store<AppState>(\n    initialState: AppState(name: ''),\n  );\n\n  await store.dispatchAndWait(SaveNameAction('John'));\n\n  expect(store.state.name, 'John');\n});\n```\n\n## Testing Async Actions\n\nAsync actions work the same way - `dispatchAndWait()` returns only when the action fully completes:\n\n```dart\nclass FetchUserAction extends ReduxAction<AppState> {\n  final String userId;\n  FetchUserAction(this.userId);\n\n  Future<AppState?> reduce() async {\n    var user = await api.fetchUser(userId);\n    return state.copy(user: user);\n  }\n}\n\ntest('FetchUserAction loads user data', () async {\n  var store = Store<AppState>(\n    initialState: AppState(user: null),\n  );\n\n  await store.dispatchAndWait(FetchUserAction('123'));\n\n  expect(store.state.user, isNotNull);\n  expect(store.state.user!.id, '123');\n});\n```\n\n## Testing Multiple Actions in Parallel\n\nUse `dispatchAndWaitAll()` to dispatch multiple actions and wait for all to complete:\n\n```dart\ntest('can buy and sell stocks in parallel', () async {\n  var store = Store<AppState>(\n    initialState: AppState(portfolio: Portfolio.empty()),\n  );\n\n  await store.dispatchAndWaitAll([\n    BuyAction('IBM', quantity: 10),\n    SellAction('TSLA', quantity: 5),\n  ]);\n\n  expect(store.state.portfolio.holdings['IBM'], 10);\n  expect(store.state.portfolio.holdings['TSLA'], isNull);\n});\n```\n\n## Verifying Action Errors with ActionStatus\n\n`dispatchAndWait()` returns an `ActionStatus` object that lets you verify if an action succeeded or failed:\n\n```dart\ntest('SaveAction fails with invalid data', () async {\n  var store = Store<AppState>(\n    initialState: AppState.initialState(),\n  );\n\n  var status = await store.dispatchAndWait(SaveAction(amount: -100));\n\n  expect(status.isCompletedFailed, isTrue);\n  expect(status.isCompletedOk, isFalse);\n});\n```\n\n### ActionStatus Properties\n\n- **`isCompleted`**: Whether the action finished executing\n- **`isCompletedOk`**: True if action finished without errors (both `before()` and `reduce()` completed successfully)\n- **`isCompletedFailed`**: True if action threw an error\n- **`originalError`**: The error thrown by `before()` or `reduce()`\n- **`wrappedError`**: The error after `wrapError()` processing\n- **`hasFinishedMethodBefore`**: Whether `before()` completed\n- **`hasFinishedMethodReduce`**: Whether `reduce()` completed\n- **`hasFinishedMethodAfter`**: Whether `after()` completed\n\n## Testing UserException Errors\n\nTest that actions throw appropriate `UserException` errors:\n\n```dart\nclass TransferMoney extends ReduxAction<AppState> {\n  final double amount;\n  TransferMoney(this.amount);\n\n  AppState? reduce() {\n    if (amount <= 0) {\n      throw UserException('Amount must be positive.');\n    }\n    return state.copy(balance: state.balance - amount);\n  }\n}\n\ntest('TransferMoney throws UserException for invalid amount', () async {\n  var store = Store<AppState>(\n    initialState: AppState(balance: 1000),\n  );\n\n  var status = await store.dispatchAndWait(TransferMoney(0));\n\n  expect(status.isCompletedFailed, isTrue);\n\n  var error = status.wrappedError;\n  expect(error, isA<UserException>());\n  expect((error as UserException).msg, 'Amount must be positive.');\n});\n```\n\n## Testing Multiple Errors with Error Queue\n\nWhen multiple actions fail, check the store's error queue:\n\n```dart\ntest('multiple actions can fail', () async {\n  var store = Store<AppState>(\n    initialState: AppState.initialState(),\n  );\n\n  await store.dispatchAndWaitAll([\n    InvalidAction1(),\n    InvalidAction2(),\n  ]);\n\n  // Check errors in the store's error queue\n  expect(store.errors.length, 2);\n});\n```\n\n## Conditional Navigation After Action Success\n\nA common pattern is navigating only after an action succeeds:\n\n```dart\ntest('navigate only on successful save', () async {\n  var store = Store<AppState>(\n    initialState: AppState.initialState(),\n  );\n\n  var status = await store.dispatchAndWait(SaveAction(data: validData));\n\n  expect(status.isCompletedOk, isTrue);\n  // In real code: if (status.isCompletedOk) Navigator.pop(context);\n});\n```\n\n## Testing State Unchanged on Error\n\nWhen an action throws, state should remain unchanged:\n\n```dart\ntest('state unchanged when action fails', () async {\n  var store = Store<AppState>(\n    initialState: AppState(counter: 5),\n  );\n\n  var initialState = store.state;\n\n  await store.dispatchAndWait(FailingAction());\n\n  // State should not have changed\n  expect(store.state.counter, 5);\n  expect(store.state, initialState);\n});\n```\n\n## Using MockStore for Dependency Isolation\n\nUse `MockStore` to mock specific actions in tests:\n\n```dart\ntest('with mocked dependency action', () async {\n  var store = MockStore<AppState>(\n    initialState: AppState.initialState(),\n    mocks: {\n      // Disable the action (don't run it)\n      FetchFromServerAction: null,\n\n      // Or replace with custom state modification\n      FetchFromServerAction: (action, state) =>\n        state.copy(data: 'mocked data'),\n    },\n  );\n\n  await store.dispatchAndWait(ActionThatDependsOnFetch());\n\n  expect(store.state.data, 'mocked data');\n});\n```\n\n## Advanced Wait Methods for Complex Tests\n\nFor complex async scenarios, use these additional wait methods:\n\n```dart\n// Wait for a specific state condition\nawait store.waitCondition((state) => state.isLoaded);\n\n// Wait for all given action types to complete\nawait store.waitAllActionTypes([LoadAction, ProcessAction]);\n\n// Wait for any action of given types to finish\nawait store.waitAnyActionTypeFinishes([LoadAction]);\n\n// Wait until no actions are in progress\nawait store.waitAllActions([]);\n```\n\n## Test File Organization\n\nRecommended naming convention for test files:\n\n- Widget: `my_feature.dart`\n- State tests: `my_feature_STATE_test.dart`\n- Connector tests: `my_feature_CONNECTOR_test.dart`\n- Presentation tests: `my_feature_PRESENTATION_test.dart`\n\n## Complete Test Example\n\n```dart\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:async_redux/async_redux.dart';\n\nvoid main() {\n  group('IncrementAction', () {\n    late Store<AppState> store;\n\n    setUp(() {\n      store = Store<AppState>(\n        initialState: AppState(counter: 0),\n      );\n    });\n\n    test('increments counter by 1', () async {\n      await store.dispatchAndWait(IncrementAction());\n      expect(store.state.counter, 1);\n    });\n\n    test('increments counter multiple times', () async {\n      await store.dispatchAndWait(IncrementAction());\n      await store.dispatchAndWait(IncrementAction());\n      await store.dispatchAndWait(IncrementAction());\n      expect(store.state.counter, 3);\n    });\n\n    test('handles concurrent increments', () async {\n      await store.dispatchAndWaitAll([\n        IncrementAction(),\n        IncrementAction(),\n        IncrementAction(),\n      ]);\n      expect(store.state.counter, 3);\n    });\n  });\n\n  group('FetchDataAction', () {\n    test('succeeds with valid response', () async {\n      var store = Store<AppState>(\n        initialState: AppState(data: null),\n      );\n\n      var status = await store.dispatchAndWait(FetchDataAction());\n\n      expect(status.isCompletedOk, isTrue);\n      expect(store.state.data, isNotNull);\n    });\n\n    test('fails gracefully on error', () async {\n      var store = Store<AppState>(\n        initialState: AppState(data: null),\n      );\n\n      var status = await store.dispatchAndWait(\n        FetchDataAction(simulateError: true),\n      );\n\n      expect(status.isCompletedFailed, isTrue);\n      expect(status.wrappedError, isA<UserException>());\n      expect(store.state.data, isNull); // State unchanged\n    });\n  });\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/testing/store-tester\n- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect\n- https://asyncredux.com/flutter/testing/test-files\n- https://asyncredux.com/flutter/testing/mocking\n- https://asyncredux.com/flutter/testing/testing-user-exceptions\n- https://asyncredux.com/flutter/advanced-actions/action-status\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/basics/dispatching-actions\n- https://asyncredux.com/flutter/basics/failed-actions\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/basics/store\n- https://asyncredux.com/flutter/miscellaneous/advanced-waiting\n"
  },
  {
    "path": ".claude/skills/asyncredux-testing-view-models/SKILL.md",
    "content": "---\nname: asyncredux-testing-view-models\ndescription: Test StoreConnector view-models in isolation. Covers creating view-models with `Vm.createFrom()`, testing view-model properties, testing callbacks that dispatch actions, and verifying state changes from callbacks.\n---\n\n# Testing View-Models in AsyncRedux\n\nView-models created by `VmFactory` can be tested in isolation without building widgets. Use `Vm.createFrom()` to instantiate the view-model directly, then verify properties and execute callbacks.\n\n## Creating a View-Model for Testing\n\nUse `Vm.createFrom()` with a store and factory instance:\n\n```dart\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:async_redux/async_redux.dart';\n\ntest('view-model has correct properties', () {\n  var store = Store<AppState>(\n    initialState: AppState(name: 'Mary', counter: 5),\n  );\n\n  var vm = Vm.createFrom(store, CounterFactory());\n\n  expect(vm.counter, 5);\n  expect(vm.name, 'Mary');\n});\n```\n\n**Important:** `Vm.createFrom()` can only be called once per factory instance. Create a new factory for each test.\n\n## Testing View-Model Properties\n\nVerify that the factory correctly transforms state into view-model properties:\n\n```dart\nclass CounterViewModel extends Vm {\n  final int counter;\n  final String description;\n  final VoidCallback onIncrement;\n\n  CounterViewModel({\n    required this.counter,\n    required this.description,\n    required this.onIncrement,\n  }) : super(equals: [counter, description]);\n}\n\nclass CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {\n  @override\n  CounterViewModel fromStore() => CounterViewModel(\n    counter: state.counter,\n    description: 'Count is ${state.counter}',\n    onIncrement: () => dispatch(IncrementAction()),\n  );\n}\n\ntest('factory transforms state correctly', () {\n  var store = Store<AppState>(\n    initialState: AppState(counter: 10),\n  );\n\n  var vm = Vm.createFrom(store, CounterFactory());\n\n  expect(vm.counter, 10);\n  expect(vm.description, 'Count is 10');\n});\n```\n\n## Testing Callbacks That Dispatch Actions\n\nWhen testing callbacks, invoke them and then use wait methods to verify actions were dispatched and state changed:\n\n```dart\ntest('onIncrement dispatches IncrementAction', () async {\n  var store = Store<AppState>(\n    initialState: AppState(counter: 0),\n  );\n\n  var vm = Vm.createFrom(store, CounterFactory());\n\n  // Invoke the callback\n  vm.onIncrement();\n\n  // Wait for the action to complete\n  await store.waitActionType(IncrementAction);\n\n  // Verify state changed\n  expect(store.state.counter, 1);\n});\n```\n\n## Wait Methods for Callback Testing\n\nSeveral wait methods help verify callback behavior:\n\n### waitActionType\n\nWait for a specific action type to finish:\n\n```dart\ntest('callback dispatches expected action', () async {\n  var store = Store<AppState>(initialState: AppState(name: ''));\n  var vm = Vm.createFrom(store, UserFactory());\n\n  vm.onSave('John');\n  await store.waitActionType(SaveNameAction);\n\n  expect(store.state.name, 'John');\n});\n```\n\n### waitAllActionTypes\n\nWait for multiple action types to complete:\n\n```dart\ntest('callback triggers multiple actions', () async {\n  var store = Store<AppState>(initialState: AppState.initialState());\n  var vm = Vm.createFrom(store, CheckoutFactory());\n\n  vm.onCheckout();\n  await store.waitAllActionTypes([ValidateCartAction, ProcessPaymentAction]);\n\n  expect(store.state.orderCompleted, isTrue);\n});\n```\n\n### waitAnyActionTypeFinishes\n\nWait for any matching action to finish, useful when testing actions that may or may not be dispatched:\n\n```dart\ntest('refresh triggers data fetch', () async {\n  var store = Store<AppState>(initialState: AppState.initialState());\n  var vm = Vm.createFrom(store, DataFactory());\n\n  vm.onRefresh();\n  var action = await store.waitAnyActionTypeFinishes([FetchDataAction]);\n\n  expect(action, isA<FetchDataAction>());\n  expect(store.state.data, isNotEmpty);\n});\n```\n\n### waitCondition\n\nWait for state to meet a specific condition:\n\n```dart\ntest('loading completes when data is fetched', () async {\n  var store = Store<AppState>(initialState: AppState(isLoading: false, data: null));\n  var vm = Vm.createFrom(store, DataFactory());\n\n  vm.onLoad();\n  await store.waitCondition((state) => state.data != null);\n\n  expect(store.state.isLoading, isFalse);\n  expect(store.state.data, isNotNull);\n});\n```\n\n### waitAllActions\n\nWait until no actions are in progress:\n\n```dart\ntest('all actions complete', () async {\n  var store = Store<AppState>(initialState: AppState.initialState());\n  var vm = Vm.createFrom(store, BatchFactory());\n\n  vm.onProcessBatch();\n  await store.waitAllActions([]);\n\n  expect(store.state.batchProcessed, isTrue);\n});\n```\n\n## Testing Callbacks with Action Status\n\nVerify that callbacks dispatch actions that succeed or fail appropriately:\n\n```dart\ntest('save callback handles errors', () async {\n  var store = Store<AppState>(\n    initialState: AppState(data: ''),\n  );\n\n  var vm = Vm.createFrom(store, FormFactory());\n\n  // Trigger save with invalid data\n  vm.onSave('');\n\n  // dispatchAndWait returns ActionStatus, but when testing callbacks,\n  // use waitActionType and check store.errors\n  await store.waitActionType(SaveAction);\n\n  // Check if action failed\n  expect(store.errors, isNotEmpty);\n});\n```\n\n## Testing Async Callbacks\n\nAsync callbacks work the same way - wait for the dispatched actions:\n\n```dart\nclass UserFactory extends VmFactory<AppState, UserConnector, UserViewModel> {\n  @override\n  UserViewModel fromStore() => UserViewModel(\n    user: state.user,\n    onRefresh: () => dispatch(FetchUserAction()),\n  );\n}\n\ntest('onRefresh loads user data', () async {\n  var store = Store<AppState>(\n    initialState: AppState(user: null),\n  );\n\n  var vm = Vm.createFrom(store, UserFactory());\n\n  vm.onRefresh();\n  await store.waitActionType(FetchUserAction);\n\n  expect(store.state.user, isNotNull);\n});\n```\n\n## Testing with Mocked Actions\n\nUse `MockStore` to mock actions triggered by callbacks:\n\n```dart\ntest('callback with mocked dependency', () async {\n  var store = MockStore<AppState>(\n    initialState: AppState(data: null),\n    mocks: {\n      // Mock the API call to return test data\n      FetchDataAction: (action, state) => state.copy(data: 'mocked data'),\n    },\n  );\n\n  var vm = Vm.createFrom(store, DataFactory());\n\n  vm.onFetch();\n  await store.waitActionType(FetchDataAction);\n\n  expect(store.state.data, 'mocked data');\n});\n```\n\n## Testing onInit and onDispose Lifecycle\n\nUse `ConnectorTester` to test lifecycle callbacks without building widgets:\n\n```dart\nclass MyScreen extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreConnector<AppState, MyViewModel>(\n    vm: () => MyFactory(),\n    onInit: (store) => store.dispatch(StartPollingAction()),\n    onDispose: (store) => store.dispatch(StopPollingAction()),\n    builder: (context, vm) => MyWidget(vm: vm),\n  );\n}\n\ntest('onInit dispatches StartPollingAction', () async {\n  var store = Store<AppState>(initialState: AppState.initialState());\n  var connectorTester = store.getConnectorTester(MyScreen());\n\n  connectorTester.runOnInit();\n  var action = await store.waitAnyActionTypeFinishes([StartPollingAction]);\n\n  expect(action, isA<StartPollingAction>());\n});\n\ntest('onDispose dispatches StopPollingAction', () async {\n  var store = Store<AppState>(initialState: AppState.initialState());\n  var connectorTester = store.getConnectorTester(MyScreen());\n\n  connectorTester.runOnDispose();\n  var action = await store.waitAnyActionTypeFinishes([StopPollingAction]);\n\n  expect(action, isA<StopPollingAction>());\n});\n```\n\n## Complete Test Example\n\n```dart\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:async_redux/async_redux.dart';\n\n// View-Model\nclass TodoViewModel extends Vm {\n  final List<String> todos;\n  final bool isLoading;\n  final void Function(String) onAddTodo;\n  final void Function(int) onRemoveTodo;\n  final VoidCallback onRefresh;\n\n  TodoViewModel({\n    required this.todos,\n    required this.isLoading,\n    required this.onAddTodo,\n    required this.onRemoveTodo,\n    required this.onRefresh,\n  }) : super(equals: [todos, isLoading]);\n}\n\n// Factory\nclass TodoFactory extends VmFactory<AppState, TodoConnector, TodoViewModel> {\n  @override\n  TodoViewModel fromStore() => TodoViewModel(\n    todos: state.todos,\n    isLoading: state.isLoading,\n    onAddTodo: (text) => dispatch(AddTodoAction(text)),\n    onRemoveTodo: (index) => dispatch(RemoveTodoAction(index)),\n    onRefresh: () => dispatch(FetchTodosAction()),\n  );\n}\n\nvoid main() {\n  group('TodoFactory', () {\n    late Store<AppState> store;\n\n    setUp(() {\n      store = Store<AppState>(\n        initialState: AppState(todos: [], isLoading: false),\n      );\n    });\n\n    test('creates view-model with correct initial properties', () {\n      var vm = Vm.createFrom(store, TodoFactory());\n\n      expect(vm.todos, isEmpty);\n      expect(vm.isLoading, isFalse);\n    });\n\n    test('onAddTodo dispatches AddTodoAction', () async {\n      var vm = Vm.createFrom(store, TodoFactory());\n\n      vm.onAddTodo('Buy milk');\n      await store.waitActionType(AddTodoAction);\n\n      expect(store.state.todos, contains('Buy milk'));\n    });\n\n    test('onRemoveTodo dispatches RemoveTodoAction', () async {\n      store = Store<AppState>(\n        initialState: AppState(todos: ['Task 1', 'Task 2'], isLoading: false),\n      );\n      var vm = Vm.createFrom(store, TodoFactory());\n\n      vm.onRemoveTodo(0);\n      await store.waitActionType(RemoveTodoAction);\n\n      expect(store.state.todos, ['Task 2']);\n    });\n\n    test('onRefresh fetches todos', () async {\n      var vm = Vm.createFrom(store, TodoFactory());\n\n      vm.onRefresh();\n      await store.waitCondition((state) => !state.isLoading);\n\n      expect(store.state.todos, isNotEmpty);\n    });\n  });\n}\n```\n\n## Test Organization\n\nFollow the recommended naming convention for test files:\n\n- Widget: `todo_screen.dart`\n- Connector: `todo_screen_connector.dart`\n- State tests: `todo_screen_STATE_test.dart`\n- Connector tests: `todo_screen_CONNECTOR_test.dart`\n- Presentation tests: `todo_screen_PRESENTATION_test.dart`\n\nConnector tests focus on view-model logic - verifying properties are correctly derived from state and callbacks dispatch appropriate actions.\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/testing/testing-the-view-model\n- https://asyncredux.com/flutter/testing/testing-oninit-ondispose\n- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect\n- https://asyncredux.com/flutter/testing/test-files\n- https://asyncredux.com/flutter/testing/mocking\n- https://asyncredux.com/flutter/testing/store-tester\n- https://asyncredux.com/flutter/connector/store-connector\n- https://asyncredux.com/flutter/connector/advanced-view-model\n- https://asyncredux.com/flutter/connector/connector-pattern\n- https://asyncredux.com/flutter/miscellaneous/advanced-waiting\n"
  },
  {
    "path": ".claude/skills/asyncredux-testing-wait-methods/SKILL.md",
    "content": "---\nname: asyncredux-testing-wait-methods\ndescription: Use advanced wait methods for complex test scenarios. Covers `waitCondition()`, `waitAllActions()`, `waitActionType()`, `waitAllActionTypes()`, `waitAnyActionTypeFinishes()`, and the `completeImmediately` parameter.\n---\n\n# Advanced Wait Methods for Testing\n\nWhen testing complex async scenarios in AsyncRedux, the basic `dispatchAndWait()` may not be sufficient. The store provides several advanced wait methods for fine-grained control over when tests proceed.\n\n## Overview of Wait Methods\n\n| Method | Purpose |\n|--------|---------|\n| `waitCondition()` | Wait until state meets a condition |\n| `waitAllActions()` | Wait for specific actions to complete, or until no actions are in progress |\n| `waitActionType()` | Wait until no action of a given type is in progress |\n| `waitAllActionTypes()` | Wait until no actions of the given types are in progress |\n| `waitAnyActionTypeFinishes()` | Wait until ANY action of given types finishes |\n| `waitActionCondition()` | Low-level: wait until actions in progress meet a custom condition |\n\n## waitCondition()\n\nWaits until the state meets a given condition. Returns the action that triggered the state change.\n\n```dart\nFuture<ReduxAction<St>?> waitCondition(\n  bool Function(St) condition, {\n  bool completeImmediately = true,  // Note: default is TRUE here\n  int? timeoutMillis,\n})\n```\n\n### Basic Usage\n\n```dart\ntest('waitCondition waits for state to match', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  // Dispatch an async action that will change the state\n  store.dispatch(IncrementActionAsync());\n\n  // Wait until count becomes 2\n  var action = await store.waitCondition((state) => state.count == 2);\n\n  expect(store.state.count, 2);\n  expect(action, isA<IncrementActionAsync>());\n});\n```\n\n### Condition Already True\n\nBy default, if the condition is already true, the future completes immediately:\n\n```dart\ntest('completes immediately when condition already true', () async {\n  var store = Store<AppState>(initialState: AppState(count: 5));\n\n  // Condition is already true - completes immediately\n  await store.waitCondition((state) => state.count == 5);\n\n  expect(store.state.count, 5);\n});\n```\n\n### Using completeImmediately: false\n\nTo require that the condition must become true (not already be true):\n\n```dart\ntest('throws when condition already true with completeImmediately: false', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  // This will throw because condition is already true\n  expect(\n    () => store.waitCondition(\n      (state) => state.count == 1,\n      completeImmediately: false,\n    ),\n    throwsA(isA<StoreException>()),\n  );\n});\n```\n\n## waitAllActions()\n\nWaits for specific actions to finish, or waits until no actions are in progress (when passed an empty list or null).\n\n```dart\nFuture<void> waitAllActions(\n  List<ReduxAction<St>>? actions, {\n  bool completeImmediately = false,  // Note: default is FALSE here\n  int? timeoutMillis,\n})\n```\n\n### Wait for All Actions to Complete\n\n```dart\ntest('waitAllActions waits for all dispatched actions', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  var action1 = DelayedIncrementAction(10, delayMillis: 50);\n  var action2 = DelayedIncrementAction(100, delayMillis: 100);\n  var action3 = DelayedIncrementAction(1000, delayMillis: 20);\n\n  // Dispatch actions in parallel\n  store.dispatch(action1);\n  store.dispatch(action2);\n  store.dispatch(action3);\n\n  expect(store.state.count, 1); // Not changed yet\n\n  // Wait for all three actions to finish\n  await store.waitAllActions([action1, action2, action3]);\n\n  expect(store.state.count, 1 + 10 + 100 + 1000);\n});\n```\n\n### Wait Until No Actions in Progress\n\nPass an empty list or null to wait until no actions are running:\n\n```dart\ntest('waitAllActions with empty list waits for all to finish', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  store.dispatch(DelayedAction(10, delayMillis: 50));\n  store.dispatch(DelayedAction(100, delayMillis: 100));\n  store.dispatch(DelayedAction(1000, delayMillis: 20));\n\n  expect(store.state.count, 1);\n\n  // Wait until ALL actions finish (no actions in progress)\n  await store.waitAllActions([]);\n\n  expect(store.state.count, 1 + 10 + 100 + 1000);\n});\n```\n\n### Selective Waiting\n\nWait for only some actions to finish, ignoring others:\n\n```dart\ntest('wait for specific actions only', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  var action50 = DelayedAction(10, delayMillis: 50);\n  var action100 = AnotherDelayedAction(100, delayMillis: 100);\n  var action200 = SlowAction(100000, delayMillis: 200); // Very slow\n  var action10 = DelayedAction(1000, delayMillis: 10);\n\n  store.dispatch(action50);\n  store.dispatch(action100);\n  store.dispatch(action200); // We don't wait for this one\n  store.dispatch(action10);\n\n  // Wait for only the fast actions\n  await store.waitAllActions([action50, action100, action10]);\n\n  // The slow action hasn't finished yet\n  expect(store.state.count, 1 + 10 + 100 + 1000);\n});\n```\n\n## waitActionType()\n\nWaits until no action of the given type is in progress. Returns the action that finished (or null if no action was in progress).\n\n```dart\nFuture<ReduxAction<St>?> waitActionType(\n  Type actionType, {\n  bool completeImmediately = false,\n  int? timeoutMillis,\n})\n```\n\n### Basic Usage\n\n```dart\ntest('waitActionType waits for action type to finish', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  store.dispatch(DelayedAction(1000, delayMillis: 10));\n\n  expect(store.state.count, 1);\n\n  // Wait for any DelayedAction to finish\n  var action = await store.waitActionType(DelayedAction);\n\n  expect(store.state.count, 1001);\n  expect(action, isA<DelayedAction>());\n});\n```\n\n### Checking Action Status\n\n```dart\ntest('can check status of finished action', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  store.dispatch(ActionThatMayFail());\n\n  var action = await store.waitActionType(ActionThatMayFail);\n\n  expect(action?.status.isCompletedOk, isTrue);\n  // Or check for errors:\n  // expect(action?.status.originalError, isA<UserException>());\n});\n```\n\n### Waiting for Multiple Types Sequentially\n\n```dart\ntest('wait for multiple action types', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  store.dispatch(AnotherDelayedAction(123, delayMillis: 100));\n  store.dispatch(DelayedAction(1000, delayMillis: 10));\n\n  expect(store.state.count, 1);\n\n  // DelayedAction finishes first (10ms)\n  await store.waitActionType(DelayedAction);\n  expect(store.state.count, 1001);\n\n  // AnotherDelayedAction finishes later (100ms)\n  await store.waitActionType(AnotherDelayedAction);\n  expect(store.state.count, 1124);\n});\n```\n\n## waitAllActionTypes()\n\nWaits until ALL actions of the given types are NOT in progress.\n\n```dart\nFuture<void> waitAllActionTypes(\n  List<Type> actionTypes, {\n  bool completeImmediately = false,\n  int? timeoutMillis,\n})\n```\n\n### Basic Usage\n\n```dart\ntest('waitAllActionTypes waits for all types', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  store.dispatch(DelayedAction(10, delayMillis: 50));\n  store.dispatch(AnotherDelayedAction(100, delayMillis: 100));\n  store.dispatch(SlowAction(100000, delayMillis: 200));\n  store.dispatch(DelayedAction(1000, delayMillis: 10));\n\n  expect(store.state.count, 1);\n\n  // Wait for DelayedAction and AnotherDelayedAction types only\n  await store.waitAllActionTypes([DelayedAction, AnotherDelayedAction]);\n\n  // SlowAction hasn't finished yet (200ms), but we didn't wait for it\n  expect(store.state.count, 1 + 10 + 100 + 1000);\n});\n```\n\n## waitAnyActionTypeFinishes()\n\n**Important:** This method is different from the others. It waits until ANY action of the given types **finishes dispatching**, even if those actions weren't in progress when the method was called.\n\n```dart\nFuture<ReduxAction<St>> waitAnyActionTypeFinishes(\n  List<Type> actionTypes, {\n  int? timeoutMillis,\n})\n```\n\n### Use Case: Waiting for Nested Actions\n\nThis is useful when an action dispatches other actions internally, and you want to wait for one of those nested actions to finish:\n\n```dart\ntest('waitAnyActionTypeFinishes waits for nested action', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  // StartAction dispatches DelayedAction internally\n  store.dispatch(StartAction());\n\n  // Wait for DelayedAction to finish (even though it wasn't dispatched yet)\n  var action = await store.waitAnyActionTypeFinishes([DelayedAction]);\n\n  expect(action, isA<DelayedAction>());\n  expect(action.status.isCompletedOk, isTrue);\n});\n```\n\n### Multiple Types - First One to Finish\n\n```dart\ntest('returns first action type to finish', () async {\n  var store = Store<AppState>(initialState: AppState());\n\n  store.dispatch(ProcessStocksAction()); // Dispatches BuyAction or SellAction\n\n  // Wait for either BuyAction or SellAction to finish\n  var action = await store.waitAnyActionTypeFinishes([BuyAction, SellAction]);\n\n  expect(action.runtimeType, anyOf(equals(BuyAction), equals(SellAction)));\n});\n```\n\n## waitActionCondition()\n\nLow-level method that waits until the set of in-progress actions meets a custom condition. This is what the other wait methods use internally.\n\n```dart\nFuture<(Set<ReduxAction<St>>, ReduxAction<St>?)> waitActionCondition(\n  bool Function(Set<ReduxAction<St>> actions, ReduxAction<St>? triggerAction) condition, {\n  bool completeImmediately = false,\n  String completedErrorMessage = \"Awaited action condition was already true\",\n  int? timeoutMillis,\n})\n```\n\n### Example: Custom Condition\n\n```dart\ntest('waitActionCondition with custom condition', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  // Wait until no actions are in progress\n  await store.waitActionCondition(\n    (actions, triggerAction) => actions.isEmpty,\n    completeImmediately: true,\n  );\n});\n```\n\n## The completeImmediately Parameter\n\nThis parameter controls behavior when the condition is already met when the method is called:\n\n| Method | Default | When `true` | When `false` |\n|--------|---------|-------------|--------------|\n| `waitCondition` | `true` | Completes immediately | Throws `StoreException` |\n| `waitAllActions` | `false` | Completes immediately | Throws `StoreException` |\n| `waitActionType` | `false` | Completes immediately, returns `null` | Throws `StoreException` |\n| `waitAllActionTypes` | `false` | Completes immediately | Throws `StoreException` |\n| `waitActionCondition` | `false` | Completes immediately | Throws `StoreException` |\n\n**Note:** `waitCondition` defaults to `true` because it's commonly used to check \"is state ready?\", where you want to proceed if it's already ready. The other methods default to `false` because they're typically used to wait for actions that should be in progress.\n\n```dart\ntest('completeImmediately behavior', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  // waitCondition: completeImmediately defaults to TRUE\n  await store.waitCondition((state) => state.count == 1); // OK, completes\n\n  // waitAllActions: completeImmediately defaults to FALSE\n  expect(\n    () => store.waitAllActions([]), // No actions in progress\n    throwsA(isA<StoreException>()),\n  );\n\n  // Use completeImmediately: true to allow it\n  await store.waitAllActions([], completeImmediately: true); // OK\n});\n```\n\n## Timeout Configuration\n\nAll wait methods support a `timeoutMillis` parameter. The default timeout is 10 minutes.\n\n```dart\ntest('waitCondition with timeout', () async {\n  var store = Store<AppState>(initialState: AppState(count: 1));\n\n  // This condition will never be true, so it times out\n  expect(\n    () => store.waitCondition(\n      (state) => state.count == 999,\n      timeoutMillis: 10, // 10ms timeout\n    ),\n    throwsA(isA<TimeoutException>()),\n  );\n});\n```\n\n### Global Timeout Configuration\n\nModify `Store.defaultTimeoutMillis` to change the default for all wait methods:\n\n```dart\nvoid main() {\n  // Set global default timeout to 30 seconds\n  Store.defaultTimeoutMillis = 30 * 1000;\n\n  // To disable timeout entirely, use -1\n  Store.defaultTimeoutMillis = -1;\n}\n```\n\n## Complete Test Example\n\n```dart\nimport 'dart:async';\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Wait Methods', () {\n    test('waitCondition waits for state change', () async {\n      var store = Store<State>(initialState: State(1));\n\n      // Dispatch async action\n      store.dispatch(IncrementActionAsync());\n\n      // Wait for state to change\n      await store.waitCondition((state) => state.count == 2);\n\n      expect(store.state.count, 2);\n    });\n\n    test('waitAllActions waits for all actions', () async {\n      var store = Store<State>(initialState: State(1));\n\n      store.dispatch(DelayedAction(10, delayMillis: 50));\n      store.dispatch(DelayedAction(100, delayMillis: 100));\n      store.dispatch(DelayedAction(1000, delayMillis: 20));\n\n      await store.waitAllActions([]);\n\n      expect(store.state.count, 1111);\n    });\n\n    test('waitActionType waits for specific type', () async {\n      var store = Store<State>(initialState: State(1));\n\n      store.dispatch(DelayedAction(1000, delayMillis: 10));\n\n      var action = await store.waitActionType(DelayedAction);\n\n      expect(store.state.count, 1001);\n      expect(action?.status.isCompletedOk, isTrue);\n    });\n\n    test('waitAllActionTypes waits for multiple types', () async {\n      var store = Store<State>(initialState: State(1));\n\n      store.dispatch(DelayedAction(10, delayMillis: 50));\n      store.dispatch(AnotherAction(100, delayMillis: 100));\n\n      await store.waitAllActionTypes([DelayedAction, AnotherAction]);\n\n      expect(store.state.count, 111);\n    });\n\n    test('waitAnyActionTypeFinishes waits for first finish', () async {\n      var store = Store<State>(initialState: State(1));\n\n      store.dispatch(DelayedAction(1, delayMillis: 10));\n\n      var action = await store.waitAnyActionTypeFinishes([DelayedAction]);\n\n      expect(action, isA<DelayedAction>());\n      expect(action.status.isCompletedOk, isTrue);\n    });\n  });\n}\n\n// Test state and actions\nclass State {\n  final int count;\n  State(this.count);\n}\n\nclass IncrementActionAsync extends ReduxAction<State> {\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(Duration(milliseconds: 10));\n    return State(state.count + 1);\n  }\n}\n\nclass DelayedAction extends ReduxAction<State> {\n  final int increment;\n  final int delayMillis;\n\n  DelayedAction(this.increment, {required this.delayMillis});\n\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(Duration(milliseconds: delayMillis));\n    return State(state.count + increment);\n  }\n}\n\nclass AnotherAction extends DelayedAction {\n  AnotherAction(int increment, {required int delayMillis})\n      : super(increment, delayMillis: delayMillis);\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect\n- https://asyncredux.com/flutter/testing/store-tester\n- https://asyncredux.com/flutter/miscellaneous/wait-condition\n- https://asyncredux.com/flutter/miscellaneous/advanced-waiting\n- https://asyncredux.com/flutter/testing/mocking\n- https://asyncredux.com/flutter/basics/dispatching-actions\n"
  },
  {
    "path": ".claude/skills/asyncredux-throttle-mixin/SKILL.md",
    "content": "---\nname: asyncredux-throttle-mixin\ndescription: Add the Throttle mixin to prevent actions from running too frequently. Covers setting the throttle duration in milliseconds, use cases like price refresh, and how freshness/staleness works.\n---\n\n# Throttle Mixin\n\nThe `Throttle` mixin limits action execution to at most once per throttle period. When an action is dispatched multiple times within the defined window, only the first execution runs while subsequent calls abort silently. After the period expires, the next dispatch is permitted.\n\n## Basic Usage\n\n```dart\nclass LoadPrices extends AppAction with Throttle {\n\n  // Throttle period in milliseconds (default is 1000ms)\n  int get throttle => 5000; // 5 seconds\n\n  Future<AppState?> reduce() async {\n    var prices = await fetchCurrentPrices();\n    return state.copy(prices: prices);\n  }\n}\n```\n\nThe default throttle duration is **1000 milliseconds** (1 second). Override the `throttle` getter to set a custom duration.\n\n## How Throttle Works (Freshness/Staleness)\n\nThrottle uses a \"freshness window\" concept:\n\n1. **First dispatch**: Action runs immediately, data becomes \"fresh\"\n2. **During throttle period**: Data is considered fresh, subsequent dispatches are aborted\n3. **After throttle period expires**: Data becomes \"stale\", next dispatch is allowed to run\n\nThis ensures that frequently triggered actions (like a \"Refresh Prices\" button) don't overwhelm your server while still allowing updates after a reasonable interval.\n\n```dart\n// User taps \"Refresh\" rapidly 5 times in 2 seconds\n// With a 5-second throttle:\n// - 1st tap: Action runs, prices update\n// - 2nd-5th taps: Silently aborted (data still \"fresh\")\n// - Tap after 5 seconds: Action runs again (data now \"stale\")\n```\n\n## Throttle vs Debounce\n\n| Aspect | Throttle | Debounce |\n|--------|----------|----------|\n| **When it runs** | Immediately on first dispatch | After dispatches stop |\n| **Blocking** | Blocks for the period after running | Resets timer on each dispatch |\n| **Use case** | Price refresh, rate-limited APIs | Search-as-you-type |\n\n## Bypassing Throttle\n\nOverride `ignoreThrottle` to conditionally skip rate limiting:\n\n```dart\nclass LoadPrices extends AppAction with Throttle {\n  final bool forceRefresh;\n\n  LoadPrices({this.forceRefresh = false});\n\n  int get throttle => 5000;\n\n  // Bypass throttle when force refresh is requested\n  bool get ignoreThrottle => forceRefresh;\n\n  Future<AppState?> reduce() async {\n    var prices = await fetchCurrentPrices();\n    return state.copy(prices: prices);\n  }\n}\n\n// Normal dispatch - respects throttle\ndispatch(LoadPrices());\n\n// Force refresh - ignores throttle\ndispatch(LoadPrices(forceRefresh: true));\n```\n\n## Failure Handling\n\nBy default, the throttle lock persists even after errors, preventing immediate retry:\n\n```dart\nclass LoadPrices extends AppAction with Throttle {\n  int get throttle => 5000;\n\n  // Allow immediate retry if the action fails\n  bool get removeLockOnError => true;\n\n  Future<AppState?> reduce() async {\n    var prices = await fetchCurrentPrices();\n    return state.copy(prices: prices);\n  }\n}\n```\n\n### Manual Lock Control\n\nFor more control, use these methods:\n\n```dart\n// Remove the lock for this specific action type\nremoveLock();\n\n// Remove locks for all throttled actions\nremoveAllLocks();\n```\n\n## Custom Locking Strategies\n\nOverride `lockBuilder()` to implement different locking behaviors:\n\n```dart\nclass LoadPricesForSymbol extends AppAction with Throttle {\n  final String symbol;\n\n  LoadPricesForSymbol(this.symbol);\n\n  int get throttle => 5000;\n\n  // Use the symbol as part of the lock key\n  // This allows throttling per symbol instead of per action type\n  Object lockBuilder() => 'LoadPrices_$symbol';\n\n  Future<AppState?> reduce() async {\n    var price = await fetchPrice(symbol);\n    return state.copy(prices: state.prices.add(symbol, price));\n  }\n}\n\n// These can run in parallel (different lock keys):\ndispatch(LoadPricesForSymbol('AAPL'));\ndispatch(LoadPricesForSymbol('GOOGL'));\n\n// But this will be throttled (same lock key as first):\ndispatch(LoadPricesForSymbol('AAPL')); // Aborted if within 5 seconds\n```\n\n## Common Use Cases\n\n### Price/Data Refresh\n\n```dart\nclass RefreshStockPrices extends AppAction with Throttle {\n  int get throttle => 10000; // At most once every 10 seconds\n\n  Future<AppState?> reduce() async {\n    var prices = await stockApi.getAllPrices();\n    return state.copy(stockPrices: prices);\n  }\n}\n```\n\n### Rate-Limited API Calls\n\n```dart\nclass SyncWithServer extends AppAction with Throttle {\n  int get throttle => 30000; // At most once every 30 seconds\n\n  Future<AppState?> reduce() async {\n    var data = await api.sync();\n    return state.copy(lastSync: DateTime.now(), data: data);\n  }\n}\n```\n\n### Preventing Button Spam\n\n```dart\nclass SubmitFeedback extends AppAction with Throttle {\n  final String feedback;\n\n  SubmitFeedback(this.feedback);\n\n  int get throttle => 60000; // At most once per minute\n\n  Future<AppState?> reduce() async {\n    await api.submitFeedback(feedback);\n    return state.copy(feedbackSubmitted: true);\n  }\n}\n```\n\n## Mixin Compatibility\n\n**Compatible with:**\n- `CheckInternet`\n- `NoDialog`\n- `AbortWhenNoInternet`\n- `Retry`\n- `UnlimitedRetries`\n- `Debounce`\n\n**Incompatible with:**\n- `NonReentrant` (use one or the other, not both)\n- `OptimisticUpdate`\n- `OptimisticSync`\n- `OptimisticSyncWithPush`\n\n## Combining Multiple Mixins\n\n```dart\nclass LoadPrices extends AppAction\n    with CheckInternet, Throttle, Retry {\n\n  int get throttle => 5000;\n\n  Future<AppState?> reduce() async {\n    // CheckInternet ensures connectivity\n    // Throttle prevents excessive calls\n    // Retry handles transient failures\n    var prices = await fetchCurrentPrices();\n    return state.copy(prices: prices);\n  }\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/advanced-actions/action-mixins\n- https://asyncredux.com/flutter/advanced-actions/control-mixins\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n- https://asyncredux.com/flutter/advanced-actions/aborting-the-dispatch\n- https://asyncredux.com/flutter/advanced-actions/optimistic-mixins\n- https://asyncredux.com/flutter/advanced-actions/internet-mixins\n- https://asyncredux.com/flutter/basics/dispatching-actions\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/basics/failed-actions\n- https://asyncredux.com/flutter/miscellaneous/refresh-indicators\n- https://asyncredux.com/flutter/miscellaneous/database-and-cloud\n- https://asyncredux.com/flutter/miscellaneous/wait-condition\n- https://asyncredux.com/flutter/testing/mocking\n- https://asyncredux.com/flutter/about\n- https://asyncredux.com/flutter/intro\n"
  },
  {
    "path": ".claude/skills/asyncredux-undo-redo/SKILL.md",
    "content": "---\nname: asyncredux-undo-redo\ndescription: Implement undo/redo functionality using state observers. Covers recording state history with stateObserver, creating a RecoverStateAction, implementing undo for the full state or partial state, and managing history limits.\n---\n\n# Undo and Redo in AsyncRedux\n\nAsyncRedux simplifies undo/redo through a straightforward pattern: create a state observer to save states to a history list, and create actions to navigate through that history.\n\n## Understanding StateObserver\n\nThe `StateObserver` abstract class tracks state modifications. Implement its `observe` method to be notified of state changes:\n\n```dart\nabstract class StateObserver<St> {\n  void observe(\n    ReduxAction<St> action,\n    St stateIni,\n    St stateEnd,\n    Object? error,\n    int dispatchCount,\n  );\n}\n```\n\n**Parameters:**\n- `action` - The dispatched action that triggered the state change\n- `stateIni` - The state before the reducer applied changes\n- `stateEnd` - The new state returned by the reducer\n- `error` - Null if successful; contains the thrown error otherwise\n- `dispatchCount` - Sequential dispatch number\n\n**Timing:** The observer fires right after the reducer returns, before both the `after()` method and error-wrapping processes.\n\n## Step 1: Create the UndoRedoObserver\n\n```dart\nclass UndoRedoObserver implements StateObserver<AppState> {\n  final List<AppState> _history = [];\n  int _currentIndex = -1;\n  final int maxHistorySize;\n\n  UndoRedoObserver({this.maxHistorySize = 50});\n\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    AppState stateIni,\n    AppState stateEnd,\n    Object? error,\n    int dispatchCount,\n  ) {\n    // Skip if action had an error\n    if (error != null) return;\n\n    // Skip if state didn't change\n    if (stateIni == stateEnd) return;\n\n    // Skip undo/redo actions to prevent recursive history entries\n    if (action is UndoAction || action is RedoAction) return;\n\n    // When navigating backwards then performing new actions,\n    // clear the \"future\" history\n    if (_currentIndex < _history.length - 1) {\n      _history.removeRange(_currentIndex + 1, _history.length);\n    }\n\n    // Add the new state to history\n    _history.add(stateEnd);\n    _currentIndex = _history.length - 1;\n\n    // Enforce maximum history size by removing oldest entries\n    while (_history.length > maxHistorySize) {\n      _history.removeAt(0);\n      _currentIndex--;\n    }\n  }\n\n  /// Returns the previous state, or null if at the beginning\n  AppState? getPreviousState() {\n    if (_currentIndex > 0) {\n      _currentIndex--;\n      return _history[_currentIndex];\n    }\n    return null;\n  }\n\n  /// Returns the next state, or null if at the end\n  AppState? getNextState() {\n    if (_currentIndex < _history.length - 1) {\n      _currentIndex++;\n      return _history[_currentIndex];\n    }\n    return null;\n  }\n\n  bool get canUndo => _currentIndex > 0;\n  bool get canRedo => _currentIndex < _history.length - 1;\n}\n```\n\n## Step 2: Register the Observer with the Store\n\nPass the observer to `stateObservers` during store creation:\n\n```dart\n// Create the observer instance so actions can access it\nfinal undoRedoObserver = UndoRedoObserver(maxHistorySize: 100);\n\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  stateObservers: [undoRedoObserver],\n);\n```\n\n## Step 3: Create Navigation Actions\n\nCreate `UndoAction` and `RedoAction` that retrieve states from history:\n\n```dart\nclass UndoAction extends ReduxAction<AppState> {\n  final UndoRedoObserver observer;\n\n  UndoAction(this.observer);\n\n  @override\n  AppState? reduce() {\n    return observer.getPreviousState();\n  }\n}\n\nclass RedoAction extends ReduxAction<AppState> {\n  final UndoRedoObserver observer;\n\n  RedoAction(this.observer);\n\n  @override\n  AppState? reduce() {\n    return observer.getNextState();\n  }\n}\n```\n\n**Alternative:** Access the observer through dependency injection using the environment pattern:\n\n```dart\nclass UndoAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    final observer = env.undoRedoObserver;\n    return observer.getPreviousState();\n  }\n}\n```\n\n## Step 4: Integrate with the UI\n\nDispatch undo/redo actions from widgets:\n\n```dart\nclass UndoRedoButtons extends StatelessWidget {\n  final UndoRedoObserver observer;\n\n  const UndoRedoButtons({required this.observer});\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        IconButton(\n          icon: Icon(Icons.undo),\n          onPressed: observer.canUndo\n              ? () => context.dispatch(UndoAction(observer))\n              : null,\n        ),\n        IconButton(\n          icon: Icon(Icons.redo),\n          onPressed: observer.canRedo\n              ? () => context.dispatch(RedoAction(observer))\n              : null,\n        ),\n      ],\n    );\n  }\n}\n```\n\n## Partial State Undo/Redo\n\nThe same approach works to undo/redo only **part** of the state. This is useful when you want to track changes to a specific slice of state independently.\n\n```dart\nclass PartialUndoRedoObserver implements StateObserver<AppState> {\n  final List<DocumentState> _history = [];\n  int _currentIndex = -1;\n  final int maxHistorySize;\n\n  PartialUndoRedoObserver({this.maxHistorySize = 50});\n\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    AppState stateIni,\n    AppState stateEnd,\n    Object? error,\n    int dispatchCount,\n  ) {\n    if (error != null) return;\n    if (action is UndoDocumentAction || action is RedoDocumentAction) return;\n\n    // Only track changes to the document portion of state\n    if (stateIni.document == stateEnd.document) return;\n\n    if (_currentIndex < _history.length - 1) {\n      _history.removeRange(_currentIndex + 1, _history.length);\n    }\n\n    _history.add(stateEnd.document);\n    _currentIndex = _history.length - 1;\n\n    while (_history.length > maxHistorySize) {\n      _history.removeAt(0);\n      _currentIndex--;\n    }\n  }\n\n  DocumentState? getPreviousDocument() {\n    if (_currentIndex > 0) {\n      _currentIndex--;\n      return _history[_currentIndex];\n    }\n    return null;\n  }\n\n  DocumentState? getNextDocument() {\n    if (_currentIndex < _history.length - 1) {\n      _currentIndex++;\n      return _history[_currentIndex];\n    }\n    return null;\n  }\n}\n\nclass UndoDocumentAction extends ReduxAction<AppState> {\n  final PartialUndoRedoObserver observer;\n\n  UndoDocumentAction(this.observer);\n\n  @override\n  AppState? reduce() {\n    final previousDoc = observer.getPreviousDocument();\n    if (previousDoc == null) return null;\n    return state.copy(document: previousDoc);\n  }\n}\n```\n\n## Managing History Limits\n\nKey considerations for history management:\n\n1. **Set appropriate limits** - Balance memory usage with undo depth needs\n2. **Remove oldest entries** - When exceeding the limit, remove from the beginning\n3. **Clear future history** - When new actions occur after undoing, discard the redo stack\n4. **Filter irrelevant actions** - Skip actions that don't change state or are navigation actions\n\n```dart\n// Example: Different limits for different use cases\nfinal documentObserver = UndoRedoObserver(maxHistorySize: 100); // Heavy undo\nfinal preferencesObserver = UndoRedoObserver(maxHistorySize: 10); // Light undo\n```\n\n## Complete Example\n\n```dart\n// observer.dart\nclass UndoRedoObserver implements StateObserver<AppState> {\n  final List<AppState> _history = [];\n  int _currentIndex = -1;\n  final int maxHistorySize;\n\n  UndoRedoObserver({this.maxHistorySize = 50});\n\n  @override\n  void observe(\n    ReduxAction<AppState> action,\n    AppState stateIni,\n    AppState stateEnd,\n    Object? error,\n    int dispatchCount,\n  ) {\n    if (error != null) return;\n    if (stateIni == stateEnd) return;\n    if (action is UndoAction || action is RedoAction) return;\n\n    if (_currentIndex < _history.length - 1) {\n      _history.removeRange(_currentIndex + 1, _history.length);\n    }\n\n    _history.add(stateEnd);\n    _currentIndex = _history.length - 1;\n\n    while (_history.length > maxHistorySize) {\n      _history.removeAt(0);\n      _currentIndex--;\n    }\n  }\n\n  AppState? getPreviousState() {\n    if (_currentIndex > 0) {\n      _currentIndex--;\n      return _history[_currentIndex];\n    }\n    return null;\n  }\n\n  AppState? getNextState() {\n    if (_currentIndex < _history.length - 1) {\n      _currentIndex++;\n      return _history[_currentIndex];\n    }\n    return null;\n  }\n\n  bool get canUndo => _currentIndex > 0;\n  bool get canRedo => _currentIndex < _history.length - 1;\n\n  void clear() {\n    _history.clear();\n    _currentIndex = -1;\n  }\n}\n\n// actions.dart\nclass UndoAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() => env.undoRedoObserver.getPreviousState();\n}\n\nclass RedoAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() => env.undoRedoObserver.getNextState();\n}\n\n// main.dart\nvoid main() {\n  final undoRedoObserver = UndoRedoObserver(maxHistorySize: 100);\n\n  final store = Store<AppState>(\n    initialState: AppState.initialState(),\n    stateObservers: [undoRedoObserver],\n    environment: Environment(undoRedoObserver: undoRedoObserver),\n  );\n\n  runApp(\n    StoreProvider<AppState>(\n      store: store,\n      child: MyApp(),\n    ),\n  );\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/miscellaneous/undo-and-redo\n- https://asyncredux.com/flutter/basics/store\n- https://asyncredux.com/flutter/miscellaneous/logging\n- https://asyncredux.com/flutter/miscellaneous/metrics\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/testing/store-tester\n- https://asyncredux.com/flutter/basics/sync-actions\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n- https://asyncredux.com/flutter/basics/async-actions\n"
  },
  {
    "path": ".claude/skills/asyncredux-user-exceptions/SKILL.md",
    "content": "---\nname: asyncredux-user-exceptions\ndescription: Handle user-facing errors with UserException. Covers throwing UserException from actions, setting up UserExceptionDialog, customizing error dialogs with `onShowUserExceptionDialog`, and using UserExceptionAction for non-interrupting error display.\n---\n\n# UserException in AsyncRedux\n\n`UserException` is a special error type for user-facing errors that should be displayed to the user rather than logged as bugs. These represent issues the user can address or should be informed about.\n\n## Throwing UserException from Actions\n\nThrow `UserException` when an action encounters a user-facing error:\n\n```dart\nclass TransferMoney extends AppAction {\n  final double amount;\n  TransferMoney(this.amount);\n\n  AppState? reduce() {\n    if (amount == 0) {\n      throw UserException('You cannot transfer zero money.');\n    }\n    return state.copy(cash: state.cash - amount);\n  }\n}\n```\n\nFor async actions with validation:\n\n```dart\nclass SaveUser extends AppAction {\n  final String name;\n  SaveUser(this.name);\n\n  Future<AppState?> reduce() async {\n    if (name.length < 4)\n      throw UserException('Name must have at least 4 letters.');\n\n    await saveUser(name);\n    return null;\n  }\n}\n```\n\n## Converting Errors to UserException\n\nUse `addCause()` to preserve the original error while showing a user-friendly message:\n\n```dart\nclass ConvertAction extends AppAction {\n  final String text;\n  ConvertAction(this.text);\n\n  Future<AppState?> reduce() async {\n    try {\n      var value = int.parse(text);\n      return state.copy(counter: value);\n    } catch (error) {\n      throw UserException('Please enter a valid number')\n        .addCause(error);\n    }\n  }\n}\n```\n\n## Setting Up UserExceptionDialog\n\nWrap your home page with `UserExceptionDialog` below both `StoreProvider` and `MaterialApp`:\n\n```dart\nWidget build(context) {\n  return StoreProvider<AppState>(\n    store: store,\n    child: MaterialApp(\n      home: UserExceptionDialog<AppState>(\n        child: MyHomePage(),\n      ),\n    ),\n  );\n}\n```\n\nIf you omit the `onShowUserExceptionDialog` parameter, a default dialog appears with the error message and an OK button.\n\n## Customizing Error Dialogs\n\nUse `onShowUserExceptionDialog` to create custom error dialogs:\n\n```dart\nUserExceptionDialog<AppState>(\n  onShowUserExceptionDialog: (BuildContext context, UserException exception) {\n    showDialog(\n      context: context,\n      builder: (context) => AlertDialog(\n        title: Text('Error'),\n        content: Text(exception.message ?? 'An error occurred'),\n        actions: [\n          TextButton(\n            onPressed: () => Navigator.of(context).pop(),\n            child: Text('OK'),\n          ),\n        ],\n      ),\n    );\n  },\n  child: MyHomePage(),\n)\n```\n\nFor non-standard error presentation (like snackbars or banners), you can modify the behavior by accessing the `didUpdateWidget` method in a custom implementation.\n\n## UserExceptionAction for Non-Interrupting Errors\n\nUse `UserExceptionAction` to show an error dialog without throwing an exception or stopping action execution:\n\n```dart\n// Show error dialog without failing the action\ndispatch(UserExceptionAction('Please enter a valid number'));\n```\n\nThis is useful when you want to notify the user of an issue mid-action while continuing execution:\n\n```dart\nclass ConvertAction extends AppAction {\n  final String text;\n  ConvertAction(this.text);\n\n  Future<AppState?> reduce() async {\n    var value = int.tryParse(text);\n    if (value == null) {\n      // Shows dialog but action continues\n      dispatch(UserExceptionAction('Invalid number, using default'));\n      value = 0;\n    }\n    return state.copy(counter: value);\n  }\n}\n```\n\n## Reusable Error Handling with Mixins\n\nCreate mixins to standardize UserException conversion across actions:\n\n```dart\nmixin ShowUserException on AppAction {\n  String getErrorMessage();\n\n  Object? wrapError(Object error, StackTrace stackTrace) {\n    return UserException(getErrorMessage()).addCause(error);\n  }\n}\n\nclass ConvertAction extends AppAction with ShowUserException {\n  final String text;\n  ConvertAction(this.text);\n\n  @override\n  String getErrorMessage() => 'Please enter a valid number.';\n\n  Future<AppState?> reduce() async {\n    var value = int.parse(text); // Any error becomes UserException\n    return state.copy(counter: value);\n  }\n}\n```\n\n## Global Error Handling with GlobalWrapError\n\nHandle third-party or framework errors uniformly across all actions:\n\n```dart\nvar store = Store<AppState>(\n  initialState: AppState.initialState(),\n  globalWrapError: MyGlobalWrapError(),\n);\n\nclass MyGlobalWrapError extends GlobalWrapError {\n  @override\n  Object? wrap(Object error, StackTrace stackTrace, ReduxAction<dynamic> action) {\n    if (error is PlatformException &&\n        error.code == 'Error performing get') {\n      return UserException('Check your internet connection')\n        .addCause(error);\n    }\n    // Return the error unchanged for other cases\n    return error;\n  }\n}\n```\n\n**Processing order**: Action's `wrapError()` -> `GlobalWrapError` -> `ErrorObserver`\n\n## Error Queue\n\nThrown `UserException` instances are stored in a dedicated error queue within the store. The queue is consumed by `UserExceptionDialog` to display error messages. You can configure the maximum queue capacity in the Store constructor.\n\n## Checking Failed Actions in Widgets\n\nUse these methods to check action failure status and display errors inline:\n\n```dart\nWidget build(BuildContext context) {\n  if (context.isFailed(SaveUserAction)) {\n    var exception = context.exceptionFor(SaveUserAction);\n    return Column(\n      children: [\n        Text('Failed: ${exception?.message}'),\n        ElevatedButton(\n          onPressed: () {\n            context.clearExceptionFor(SaveUserAction);\n            context.dispatch(SaveUserAction(name));\n          },\n          child: Text('Retry'),\n        ),\n      ],\n    );\n  }\n  return Text('User saved successfully');\n}\n```\n\nNote: Error states automatically clear when an action is redispatched, so manual cleanup before retry is usually unnecessary.\n\n## Testing UserExceptions\n\nTest that actions throw `UserException` correctly:\n\n```dart\ntest('should throw UserException for invalid input', () async {\n  var store = Store<AppState>(initialState: AppState.initialState());\n\n  var status = await store.dispatchAndWait(TransferMoney(0));\n\n  expect(status.isCompletedFailed, isTrue);\n  var error = status.wrappedError;\n  expect(error, isA<UserException>());\n  expect((error as UserException).message, 'You cannot transfer zero money.');\n});\n```\n\nTest multiple exceptions using the error queue:\n\n```dart\ntest('should collect multiple UserExceptions', () async {\n  var store = Store<AppState>(initialState: AppState.initialState());\n\n  await store.dispatchAndWaitAll([\n    InvalidAction1(),\n    InvalidAction2(),\n    InvalidAction3(),\n  ]);\n\n  var errors = store.errors;\n  expect(errors.length, 3);\n  expect(errors[0].message, 'First error message');\n});\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/sitemap.xml\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/basics/failed-actions\n- https://asyncredux.com/flutter/testing/testing-user-exceptions\n- https://asyncredux.com/flutter/basics/wait-fail-succeed\n- https://asyncredux.com/flutter/advanced-actions/wrapping-the-reducer\n- https://asyncredux.com/flutter/basics/store\n"
  },
  {
    "path": ".claude/skills/asyncredux-wait-condition/SKILL.md",
    "content": "---\nname: asyncredux-wait-condition\ndescription: Use `waitCondition()` inside actions to pause execution until state meets criteria. Covers waiting for price thresholds, coordinating between actions, and implementing conditional workflows.\n---\n\n# Waiting for State Conditions with waitCondition()\n\nThe `waitCondition()` method pauses execution until the application state satisfies a specific condition. It's available on both the `Store` and `ReduxAction` classes.\n\n## Method Signature\n\n```dart\nFuture<ReduxAction<St>?> waitCondition(\n  bool Function(St) condition, {\n  bool completeImmediately = true,\n  int? timeoutMillis,\n});\n```\n\n**Parameters:**\n- **condition**: A function that takes the current state and returns `true` when the desired condition is met\n- **completeImmediately**: If `true` (default), completes immediately when the condition is already satisfied. If `false`, waits for a state change to meet the condition\n- **timeoutMillis**: Maximum time to wait (defaults to 10 minutes). Set to `-1` to disable timeout\n\n**Returns:** The action that triggered the condition to become true, or `null` if condition was already met.\n\n## Basic Usage Inside an Action\n\nUse `waitCondition()` when your action needs to wait for a prerequisite state before proceeding:\n\n```dart\nclass AddAppointmentAction extends ReduxAction<AppState> {\n  final String title;\n  final DateTime date;\n\n  AddAppointmentAction({required this.title, required this.date});\n\n  @override\n  Future<AppState?> reduce() async {\n    // Ensure calendar exists before adding appointment\n    if (state.calendar == null) {\n      dispatch(CreateCalendarAction());\n\n      // Wait until calendar is available\n      await waitCondition((state) => state.calendar != null);\n    }\n\n    // Now safe to add the appointment\n    return state.copy(\n      calendar: state.calendar!.addAppointment(\n        Appointment(title: title, date: date),\n      ),\n    );\n  }\n}\n```\n\n## Waiting for Value Thresholds\n\nWait for numeric values to reach specific thresholds:\n\n```dart\nclass ExecuteTradeAction extends ReduxAction<AppState> {\n  final double targetPrice;\n\n  ExecuteTradeAction(this.targetPrice);\n\n  @override\n  Future<AppState?> reduce() async {\n    // Wait until stock price reaches target\n    await waitCondition((state) => state.stockPrice >= targetPrice);\n\n    // Execute the trade at or above target price\n    return state.copy(\n      tradeExecuted: true,\n      executionPrice: state.stockPrice,\n    );\n  }\n}\n```\n\n## Coordinating Between Actions\n\nUse `waitCondition()` to coordinate dependent actions:\n\n```dart\nclass ProcessOrderAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    // Dispatch parallel data loading\n    dispatch(LoadInventoryAction());\n    dispatch(LoadPricingAction());\n\n    // Wait for both to complete\n    await waitCondition((state) =>\n      state.inventoryLoaded && state.pricingLoaded\n    );\n\n    // Both are now available - proceed with order processing\n    final total = calculateTotal(state.inventory, state.pricing);\n    return state.copy(orderTotal: total);\n  }\n}\n```\n\n## Implementing Conditional Workflows\n\nCreate multi-step workflows that wait for user input or external events:\n\n```dart\nclass CheckoutWorkflowAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    // Step 1: Wait for cart to be ready\n    await waitCondition((state) => state.cart.isNotEmpty);\n\n    // Step 2: Start payment processing\n    dispatch(InitiatePaymentAction());\n\n    // Step 3: Wait for payment confirmation\n    await waitCondition((state) =>\n      state.paymentStatus == PaymentStatus.confirmed ||\n      state.paymentStatus == PaymentStatus.failed\n    );\n\n    if (state.paymentStatus == PaymentStatus.failed) {\n      throw UserException('Payment failed. Please try again.');\n    }\n\n    // Step 4: Complete the order\n    return state.copy(orderCompleted: true);\n  }\n}\n```\n\n## Using the Return Value\n\n`waitCondition()` returns the action that caused the condition to become true:\n\n```dart\nclass MonitorPriceAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    // Wait for price change and get the action that changed it\n    final triggeringAction = await waitCondition(\n      (state) => state.price > 100,\n    );\n\n    // Can inspect which action triggered the condition\n    if (triggeringAction is PriceUpdateAction) {\n      print('Price updated by: ${triggeringAction.source}');\n    }\n\n    return state.copy(alertTriggered: true);\n  }\n}\n```\n\n## Using completeImmediately Parameter\n\nControl behavior when the condition is already met:\n\n```dart\nclass WaitForNewDataAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    // completeImmediately: false means wait for a NEW state change\n    // even if condition is currently satisfied\n    await waitCondition(\n      (state) => state.dataVersion > 0,\n      completeImmediately: false,  // Wait for fresh data\n    );\n\n    return state.copy(dataProcessed: true);\n  }\n}\n```\n\n## Setting Timeouts\n\nPrevent indefinite waiting with custom timeouts:\n\n```dart\nclass TimeSensitiveAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    try {\n      // Wait maximum 5 seconds for condition\n      await waitCondition(\n        (state) => state.isReady,\n        timeoutMillis: 5000,\n      );\n    } catch (e) {\n      // Timeout exceeded - handle gracefully\n      throw UserException('Operation timed out. Please try again.');\n    }\n\n    return state.copy(processed: true);\n  }\n}\n```\n\n## Using waitCondition() from the Store\n\nIn tests or widgets, call `waitCondition()` directly on the store:\n\n```dart\n// In a test\ntest('waits for data to load', () async {\n  var store = Store<AppState>(initialState: AppState.initial());\n\n  store.dispatch(LoadDataAction());\n\n  // Wait for loading to complete\n  await store.waitCondition((state) => state.isLoaded);\n\n  expect(store.state.data, isNotNull);\n});\n```\n\n## Testing with waitCondition()\n\n`waitCondition()` is useful in tests to wait for expected state:\n\n```dart\ntest('processes order after inventory loads', () async {\n  var store = Store<AppState>(\n    initialState: AppState(inventoryLoaded: false),\n  );\n\n  // Start the process\n  store.dispatch(ProcessOrderAction());\n\n  // Simulate inventory loading\n  await Future.delayed(Duration(milliseconds: 100));\n  store.dispatch(LoadInventoryCompleteAction());\n\n  // Wait for order processing to complete\n  await store.waitCondition((state) => state.orderProcessed);\n\n  expect(store.state.orderTotal, greaterThan(0));\n});\n```\n\n## Comparison with Other Wait Methods\n\n| Method | Use Case |\n|--------|----------|\n| `waitCondition()` | Wait for state to satisfy a predicate |\n| `dispatchAndWait()` | Wait for a specific action to complete |\n| `waitAllActions([])` | Wait for all current actions to finish |\n| `waitActionType()` | Wait for an action of a specific type |\n\n## Common Patterns\n\n### Wait for Initialization\n\n```dart\nclass AppStartupAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    dispatch(LoadUserAction());\n    dispatch(LoadSettingsAction());\n    dispatch(LoadCacheAction());\n\n    // Wait for all initialization to complete\n    await waitCondition((state) =>\n      state.user != null &&\n      state.settings != null &&\n      state.cacheReady\n    );\n\n    return state.copy(appReady: true);\n  }\n}\n```\n\n### Wait for User Confirmation\n\n```dart\nclass DeleteAccountAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    // Show confirmation dialog\n    dispatch(ShowConfirmationDialogAction(\n      message: 'Are you sure you want to delete your account?',\n    ));\n\n    // Wait for user response\n    await waitCondition((state) =>\n      state.confirmationResult != null\n    );\n\n    if (state.confirmationResult != true) {\n      return null; // User cancelled\n    }\n\n    // Proceed with deletion\n    await api.deleteAccount();\n    return state.copy(accountDeleted: true);\n  }\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/miscellaneous/wait-condition\n- https://asyncredux.com/flutter/miscellaneous/advanced-waiting\n- https://asyncredux.com/flutter/advanced-actions/redux-action\n- https://asyncredux.com/flutter/testing/store-tester\n- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/basics/dispatching-actions\n- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer\n"
  },
  {
    "path": ".claude/skills/asyncredux-wait-fail-succeed/SKILL.md",
    "content": "---\nname: asyncredux-wait-fail-succeed\ndescription: Show loading states and handle action failures in widgets. Covers `isWaiting(ActionType)` for spinners, `isFailed(ActionType)` for error states, `exceptionFor(ActionType)` for error messages, and `clearExceptionFor()` to reset failure states.\n---\n\n# AsyncRedux Wait, Fail, Succeed\n\nAsyncRedux provides context extension methods to track async action states: waiting (in progress), failed (error), and succeeded (complete). These are essential for showing spinners, error messages, and success states in the UI.\n\n## Four Core Methods\n\n| Method | Returns | Purpose |\n|--------|---------|---------|\n| `isWaiting(ActionType)` | `bool` | True if the action is currently running |\n| `isFailed(ActionType)` | `bool` | True if the action recently failed |\n| `exceptionFor(ActionType)` | `UserException?` | The exception from a failed action |\n| `clearExceptionFor(ActionType)` | `void` | Manually clears stored exception |\n\n## Showing a Loading Spinner\n\nUse `isWaiting()` to display a spinner while an action runs:\n\n```dart\nWidget build(BuildContext context) {\n  if (context.isWaiting(FetchDataAction)) {\n    return CircularProgressIndicator();\n  }\n  return Text('Data: ${context.state.data}');\n}\n```\n\nThe widget automatically rebuilds when the action starts and completes.\n\n## Showing Error States\n\nUse `isFailed()` and `exceptionFor()` to display error messages:\n\n```dart\nWidget build(BuildContext context) {\n  if (context.isFailed(FetchDataAction)) {\n    var exception = context.exceptionFor(FetchDataAction);\n    return Text('Error: ${exception?.message}');\n  }\n  return Text('Data: ${context.state.data}');\n}\n```\n\n## Combined Pattern: Loading, Error, and Success\n\nThe typical pattern handles all three states:\n\n```dart\nWidget build(BuildContext context) {\n  // Loading state\n  if (context.isWaiting(GetItemsAction)) {\n    return Center(child: CircularProgressIndicator());\n  }\n\n  // Error state with retry\n  if (context.isFailed(GetItemsAction)) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        Text('Failed to load items'),\n        Text(context.exceptionFor(GetItemsAction)?.message ?? ''),\n        ElevatedButton(\n          onPressed: () => context.dispatch(GetItemsAction()),\n          child: Text('Retry'),\n        ),\n      ],\n    );\n  }\n\n  // Success state\n  return ListView.builder(\n    itemCount: context.state.items.length,\n    itemBuilder: (context, index) => ListTile(\n      title: Text(context.state.items[index].name),\n    ),\n  );\n}\n```\n\n## Automatic Error Clearing\n\nWhen an action is dispatched again, any previous error for that action type is automatically cleared. This means:\n\n- User sees error\n- User taps \"Retry\" which dispatches the action again\n- `isFailed()` becomes false immediately\n- `isWaiting()` becomes true\n- If action succeeds, widget shows success state\n- If action fails again, `isFailed()` becomes true with the new exception\n\n## Manual Error Clearing\n\nUse `clearExceptionFor()` when you need to dismiss an error without retrying:\n\n```dart\nWidget build(BuildContext context) {\n  if (context.isFailed(SubmitFormAction)) {\n    return AlertDialog(\n      title: Text('Error'),\n      content: Text(context.exceptionFor(SubmitFormAction)?.message ?? ''),\n      actions: [\n        TextButton(\n          onPressed: () {\n            context.clearExceptionFor(SubmitFormAction);\n          },\n          child: Text('Dismiss'),\n        ),\n        TextButton(\n          onPressed: () => context.dispatch(SubmitFormAction()),\n          child: Text('Retry'),\n        ),\n      ],\n    );\n  }\n  // ...\n}\n```\n\n## How Actions Fail\n\nActions fail when they throw an error in `before()` or `reduce()`. Use `UserException` for user-facing errors:\n\n```dart\nclass FetchDataAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    final response = await api.fetchData();\n\n    if (response.statusCode == 404) {\n      throw UserException('Data not found.');\n    }\n\n    if (response.statusCode != 200) {\n      throw UserException('Failed to load data. Please try again.');\n    }\n\n    return state.copy(data: response.data);\n  }\n}\n```\n\n## Checking Multiple Actions\n\nYou can check multiple action types for waiting or failure:\n\n```dart\nWidget build(BuildContext context) {\n  // Check if any of several actions are running\n  bool isLoading = context.isWaiting(FetchUserAction) ||\n                   context.isWaiting(FetchSettingsAction);\n\n  if (isLoading) {\n    return CircularProgressIndicator();\n  }\n\n  // Check for any failures\n  if (context.isFailed(FetchUserAction)) {\n    return Text('Failed to load user');\n  }\n  if (context.isFailed(FetchSettingsAction)) {\n    return Text('Failed to load settings');\n  }\n\n  return MyContent();\n}\n```\n\n## Pull-to-Refresh Integration\n\nCombine with `dispatchAndWait()` for refresh indicators:\n\n```dart\nclass MyListWidget extends StatelessWidget {\n  Future<void> _onRefresh(BuildContext context) {\n    return context.dispatchAndWait(RefreshItemsAction());\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return RefreshIndicator(\n      onRefresh: () => _onRefresh(context),\n      child: ListView.builder(\n        itemCount: context.state.items.length,\n        itemBuilder: (context, index) => ListTile(\n          title: Text(context.state.items[index].name),\n        ),\n      ),\n    );\n  }\n}\n```\n\n## Complete Example\n\n```dart\nclass LoadProductsAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    final products = await api.fetchProducts();\n    if (products.isEmpty) {\n      throw UserException('No products available.');\n    }\n    return state.copy(products: products);\n  }\n}\n\nclass ProductsScreen extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('Products')),\n      body: _buildBody(context),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () => context.dispatch(LoadProductsAction()),\n        child: Icon(Icons.refresh),\n      ),\n    );\n  }\n\n  Widget _buildBody(BuildContext context) {\n    if (context.isWaiting(LoadProductsAction)) {\n      return Center(child: CircularProgressIndicator());\n    }\n\n    if (context.isFailed(LoadProductsAction)) {\n      final error = context.exceptionFor(LoadProductsAction);\n      return Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            Icon(Icons.error, size: 64, color: Colors.red),\n            SizedBox(height: 16),\n            Text(error?.message ?? 'An error occurred'),\n            SizedBox(height: 16),\n            ElevatedButton(\n              onPressed: () => context.dispatch(LoadProductsAction()),\n              child: Text('Try Again'),\n            ),\n          ],\n        ),\n      );\n    }\n\n    final products = context.state.products;\n    if (products.isEmpty) {\n      return Center(child: Text('No products yet. Tap refresh to load.'));\n    }\n\n    return ListView.builder(\n      itemCount: products.length,\n      itemBuilder: (context, index) => ListTile(\n        title: Text(products[index].name),\n        subtitle: Text('\\$${products[index].price}'),\n      ),\n    );\n  }\n}\n```\n\n## References\n\nURLs from the documentation:\n- https://asyncredux.com/flutter/basics/wait-fail-succeed\n- https://asyncredux.com/flutter/miscellaneous/advanced-waiting\n- https://asyncredux.com/flutter/advanced-actions/action-status\n- https://asyncredux.com/flutter/basics/failed-actions\n- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions\n- https://asyncredux.com/flutter/basics/using-the-store-state\n- https://asyncredux.com/flutter/basics/dispatching-actions\n- https://asyncredux.com/flutter/basics/async-actions\n- https://asyncredux.com/flutter/miscellaneous/refresh-indicators\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": ""
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "name: Build\n\non:\n  push:\n    branches:\n      - master\n      - develop\n  pull_request:\n\njobs:\n  test:\n    name: Run tests\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: subosito/flutter-action@v2\n        with:\n          channel: stable\n      - run: flutter pub get\n      - run: flutter test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/.flutter-plugins-dependencies\n**/flutter_export_environment.sh\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.packages\n.pub-cache/\n.pub/\n/build/\nbuild/\nios/.generated/\nios/Flutter/Generated.xcconfig\nios/Runner/GeneratedPluginRegistrant.*\npubspec.lock\n*.lock\n.flutter-plugins-dependencies\n\n# Android related\n**/android/**/gradle-wrapper.jar\n**/android/.gradle\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\n!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages\n"
  },
  {
    "path": ".metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: b712a172f9694745f50505c93340883493b505e5\n  channel: stable\n\nproject_type: package\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "_Visit\nthe <a href=\"https://github.com/marcglasberg/SameAppDifferentTech/blob/main/MobileAppFlutterRedux/README.md\">\nAsyncRedux App Example GitHub Repo</a> for a full-fledged example app\nshowcasing the fundamentals and best practices._\n\nSponsored by [MyText.ai](https://mytext.ai)\n\n[![](./example/SponsoredByMyTextAi.png)](https://mytext.ai)\n\n## 28.0.0-dev.3\n\n* **DEPRECATION WARNING:** `Store.globalWrapError` and `Store.errorObserver`\n  are now deprecated. Use the new `globalErrorObserver` instead. \n\n* You can now provide a _global error observer_ using the `globalWrapError` parameter in\n  the `Store` constructor:\n\n  ```dart\n  var store = Store<AppState>(\n    initialState: AppState(),\n    globalErrorObserver: (store) => MyGlobalErrorObserver(),\n  }   \n  \n  class MyGlobalErrorObserver extends GlobalErrorObserver {\n  \n    @override\n    void wrap() {  \n      // Here you can use:\n      // `error` -> Thrown by the action, AFTER being processed by the action's `wrapError`. \n      // `originalError` -> Error BEFORE being processed by the action's `wrapError`. \n      // `stackTrace` -> The stack trace of the error. \n      // `action` -> The action that threw the error.\n      // `store` -> Use it to read the `store.environment` or `store.configuration`.  \n    }\n  }\n  ```     \n\n  # Use cases\n\n    1. Use this to set up your app to use 3rd-party services like Sentry or Firebase\n       Crashlytics to monitor your app for errors in production, and print them to the\n       console in development and testing. Since you are setting it up in a centralized\n       way, you don't have to \"pollute\" your code with logging calls.\n\n    2. Use this to have a global place to convert some exceptions into `UserException`s.\n       For example, Firebase may throw some `PlatformException`s in response to a bad\n       connection to the server. In this case, you may want to show the user a dialog\n       explaining that the connection is bad, which you can do by converting it to\n       a `UserException`. Note, this could also be done in the `ReduxAction.wrapError`,\n       but then you'd have to add it to all actions that use Firebase.\n\n* **BREAKING:** Removed the deprecated `Store.wrapError`.\n  Use the new `globalErrorObserver` instead.\n\n* **BREAKING:** Removed the deprecated:\n    - `ActionStatus.isBeforeDone` (replace with `hasFinishedMethodBefore`)\n    - `isReduceDone` (replace with `hasFinishedMethodReduce`)\n    - `isAfterDone` (replace with `hasFinishedMethodAfter`)\n    - `isFinished` (replace with `isBeforeDone && isReduceDone && isAfterDone`)\n\n## 27.1.1\n\n* Added `store.removeError(source)` to remove `UserException` errors from the error queue.\n  You can pass it a `UserException`, an `ActionStatus`, or a `ReduxAction`.\n  This is sometimes useful in tests. For example:\n\n  ```dart                       \n  // Dispatch some action\n  var status = await store.dispatchAndWait(SomeAction());\n  \n  // Check the action failed as expected    \n  expect(status.originalError, isError<CloudException>('Insufficient balance.'));  \n    \n  // Make sure there are no more errors\n  store.removeError(status);  \n  expect(store.errors, isEmpty);\n  ```   \n\n* `ActionStatus.context` now has a reference to the action and the store.\n\n## 27.0.0\n\n* **BREAKING:** This version is only a breaking change if you are using the `enviroment`\n  parameter of the `Store` constructor to do dependency injection.\n\n  The `Store` constructor now accepts `dependencies` and `configuration` parameters,\n  in addition to `environment`. See file `main_dependency_injection.dart` in the `example`\n  directory for an example.\n\n  This provides for very granular\n  dependency injection, for all app needs:\n\n    - `environment`: Specifies if the app is running in production, staging, development,\n      testing, etc. Should be immutable and not change during app execution. Example:\n\n      ```dart\n      enum Environment {\n        production, staging, testing;        \n        bool get isProduction => this == Environment.production;\n        bool get isStaging => this == Environment.staging;\n        bool get isTesting => this == Environment.testing;\n      }\n      ```\n\n        - `dependencies`: A container for injected dependencies (like services,\n          repositories, APIs, etc.), created via a factory that receives the `Store`,\n          so it can vary based on the environment and/or the configuration. Example:\n\n          ```dart\n          abstract class Dependencies {\n        \n            factory Dependencies(Store store) {\n              if (store.environment == Environment.production) {\n                return DependenciesProduction();\n              } else if (store.environment == Environment.staging) {\n                return DependenciesStaging();\n              } else {\n                return DependenciesTesting();\n              }\n            }\n          }\n          ```\n\n    - `configuration`: For feature flags and other configuration values.\n\n      ```dart\n      class Config {\n         // Add whatever configuration values you need here, if any.\n         bool isABtestingOn = false;         \n         bool showAdminConsole = false;\n         ...\n      }  \n      ```\n\n    - `configuration`: For feature flags and other configuration values.\n\nThis is how you create a store with these three parameters:\n\n  ```dart\n  store = Store<AppState>(\n    initialState: AppState.initial(),\n    environment: Environment.production,\n    dependencies: (store) => Dependencies(store),\n    configuration: (store) => Configuration(store),\n  );\n  ```\n\n* **BREAKING:** `Store.env` has been renamed to `Store.environment`.\n\n* **BREAKING:** Removed `ReduxAction.env`. Access it through `store.environment` instead.\n  It's recommended to define a typed getter in your base action class:\n\n  ```dart\n  abstract class Action extends ReduxAction<AppState> {\n    Dependencies get dependencies => super.store.dependencies as Dependencies;\n    Environment get environment => super.store.environment as Environment;\n    Config get config => super.store.configuration as Config;\n  }\n  ```\n\n* **BREAKING:** Removed `VmFactory.env`. Access dependencies through `store.dependencies`\n  instead. Define a typed getter in your base factory class:\n\n  ```dart\n  abstract class AppFactory<T extends Widget?, Model extends Vm>\n      extends VmFactory<AppState, T, Model> {\n    AppFactory([T? connector]) : super(connector);\n  \n    Dependencies get dependencies => store.dependencies as Dependencies;\n    Environment get environment => store.environment as Environment;\n    Config get config => store.configuration as Config;\n  }\n  ```  \n\n* **Final thoughts**: Why is AsyncRedux now providing dependency injection features?\n  The reason is testing. When you create a store in a test, you provide the environment,\n  dependencies, and configuration as parameters. As soon as the test ends, and the store\n  is disposed, the environment, dependencies and configuration are disposed with it.\n  This makes tests less verbose and less prone to memory leaks.\n\n## 26.4.2\n\n* Added the `Polling` mixin and `Poll` enum.\n\n  Use this mixin to periodically dispatch an action at a fixed interval,\n  keeping data fresh by fetching it from a server. This is useful for\n  refreshing prices, checking for new messages, or monitoring wallet balances.\n\n  Control polling with the `Poll` enum: `Poll.start` to begin polling\n  (also runs the action immediately), `Poll.stop` to cancel it,\n  `Poll.runNowAndRestart` to run immediately and restart the timer, and `Poll.once`\n  to run immediately without affecting the timer.\n\n  The default interval is 10 seconds, but you can override `pollInterval`.\n\n  ```dart\n  class PollPrices extends AppAction with Polling {\n    @override final Poll poll;\n    PollPrices({this.poll = Poll.start});\n\n    @override\n    ReduxAction<AppState> createPollingAction() => PollPrices();\n\n    @override\n    Future<AppState?> reduce() async {\n      final prices = await api.getPrices();\n      return state.copy(prices: prices);\n    }\n  }\n\n  // Start polling (also runs reduce immediately):\n  dispatch(PollPrices());\n\n  // Stop polling:\n  dispatch(PollPrices(poll: Poll.stop));\n  ```\n\n  ### Flexible architecture:\n\n  You can use a single action for both polling control and work, or separate them into two\n  action types. For example, you could have a `ControlPricePolling` action that only\n  starts/stops the polling, and a separate `FetchPrices` action that does the actual\n  fetching.\n\n## 26.3.3\n\n* Added Claude Code **Skills** to help developers use `async_redux` with AI assistants.\n  See: https://github.com/marcglasberg/async_redux/tree/master/.claude/skills\n\n## 26.2.2\n\n* Improved the `Fresh` mixin.\n\n* Improved mixin docs.\n\n## 26.2.1\n\n* Improved the `OptimisticSyncWithPush` mixin.\n\n## 26.2.0\n\n* Added the `OptimisticCommand` mixin.\n\n  Use this mixin for command-based operations where you want to optimistically\n  update the UI immediately, send a command to the server, and automatically\n  rollback if the server request fails.\n\n  This is useful for **blocking** user interactions like adding a todo item,\n  deleting a record, or updating user settings, where you want instant UI\n  feedback but also need to ensure consistency with the server.\n\n  It's blocking in the sense that the user cannot perform other operation\n  in the same state until the command completes (success or failure).\n\n  See file `example/lib/main_optimistic_command.dart` for an example app\n  demonstrating the use of `OptimisticCommand` in a like button.\n\n  ```dart\n  class SaveTodo extends AppAction with OptimisticCommand {\n    final Todo newTodo;\n    SaveTodo(this.newTodo);\n \n    // The new Todo is going to be optimistically applied to the state, right away.\n    @override\n    Object? optimisticValue() => newTodo;\n \n    // We teach the action how to read the Todo from the state.\n    @override\n    Object? getValueFromState(AppState state) => state.todoList.getById(newTodo.id);\n \n    // We teach the action how to add the new Todo to the state.\n    @override\n    AppState applyValueToState(AppState state, Object? value)\n      => state.copy(todoList: state.todoList.add(newTodo));\n \n    // Contact the server to send the command (save the Todo). I\n    @override\n    Future<Todo> sendCommandToServer(Object? newTodo) async => await saveTodo(newTodo);\n                \n    // If the server returns a value, we may apply it to the state.\n    @override\n    AppState applyServerResponseToState(AppState state, Todo todo)\n      => state.copy(todoList: state.todoList.add(todo));\n\n    // Reload from the cloud (in case of error).\n    @override\n    Future<Object?> reloadFromServer() async => await loadTodo();\n  }\n  ```\n\n  ### Key features:\n\n    - **Instant UI update**: The state is updated immediately when the action\n      is dispatched, before the server request completes.\n\n    - **Automatic rollback**: If `sendCommandToServer` fails, the mixin checks\n      if the current state still contains the optimistic value. If so, it\n      safely rolls back to the initial value.\n\n    - **Non-reentrant by default**: Concurrent dispatches of the same action\n      type are prevented. Use `nonReentrantKeyParams()` to allow parallel\n      execution for different parameters (e.g., different item IDs).\n\n    - **Optional reload**: Override `reloadFromServer()` to fetch fresh data\n      from the server after the command completes (success or failure).\n\n\n* Added the `OptimisticSync` mixin.\n\n  Use this mixin for **non-blocking** user interactions where you want instant\n  UI feedback and automatic synchronization with the server.\n  It's non-blocking in the sense that the user can continue performing other\n  operations in the same state while synchronization is in progress.\n  The mixin handles rapid user interactions gracefully by coalescing requests\n  and ensuring eventual consistency.\n\n  This is ideal for toggle buttons (like/unlike, follow/unfollow), sliders,\n  switches, or any control where the user might interact multiple times\n  before the server responds.\n\n  See file `example/lib/main_optimistic_sync.dart` for an example app\n  demonstrating the use of `OptimisticSync` in a like button.\n\n  ```dart\n  class ToggleLike extends ReduxAction<AppState>\n      with OptimisticSync<AppState, bool> {\n    final String itemId;\n    ToggleLike(this.itemId);\n\n    // Differentiate by item ID so different items can sync independently.\n    Object? optimisticSyncKeyParams() => itemId;\n\n    // The value to apply optimistically (toggle current state).\n    bool valueToApply() => !state.items[itemId].isLiked;\n\n    // Apply the value to the state.\n    AppState applyOptimisticValueToState(AppState state, bool isLiked)\n        => state.copyWith(items: state.items.setLiked(itemId, isLiked));\n\n    // Get the current value from the state.\n    bool getValueFromState(AppState state) => state.items[itemId].isLiked;\n\n    // Send the value to the server.\n    Future<Object?> sendValueToServer(Object? value) async {\n      var response = await api.setLiked(itemId, value as bool);\n      return response.liked; // Return server-confirmed value, or null.\n    }\n\n    // Apply server response to the state (optional).\n    AppState? applyServerResponseToState(AppState state, Object response)\n        => state.copyWith(items: state.items.setLiked(itemId, response as bool));\n  }\n  ```\n\n  ### How it works:\n\n    1. **Optimistic update**: When dispatched, the UI is updated immediately.\n\n    2. **Request coalescing**: If the user interacts again while a request is\n       in flight, the new value is applied to the UI but no new request is\n       sent yet. The mixin waits for the current request to complete.\n\n    3. **Follow-up requests**: After the request completes, the mixin checks\n       if the state value differs from what was sent. If so, it sends a\n       follow-up request with the latest value.\n\n    4. **Server response**: When the state finally stabilizes, the server\n       response is applied (if provided).\n\n  ### Example scenario:\n\n  User rapidly clicks a like button: Like → Unlike → Like\n\n    1. First click: UI shows \"liked\", request sent with `true`\n    2. Second click: UI shows \"unliked\", no new request yet (one in flight)\n    3. Third click: UI shows \"liked\", no new request yet\n    4. First request completes: Mixin sees state (`true`) matches what was\n       sent (`true`), so no follow-up needed\n    5. UI remains \"liked\", server is in sync\n\n  ### Customization:\n\n  Override `onFinish()` to run code after synchronization completes:\n\n  ```dart\n  Future<AppState?> onFinish(Object? error) async {\n    if (error != null) {\n      // Reload from server on error\n      var data = await api.loadItem(itemId);\n      return state.copyWith(items: state.items.update(itemId, data));\n    }\n    return null;\n  }\n  ```\n\n* Added the `OptimisticSyncWithPush` and `ServerPush` mixins.\n\n  Use these mixins together when your app receives server-pushed updates\n  (WebSockets, Server-Sent Events, Firebase, etc.) that may modify the same\n  state your actions control.\n\n  Read the documentation in their own code to understand how they work.\n\n  **Important:** If your app does NOT receive server-pushed updates, you should\n  use the simpler `OptimisticSync` mixin instead.\n\n  See file `example/lib/main_optimistic_sync_with_push.dart` for an example app\n  demonstrating the use of `OptimisticSyncWithPush` in a like button.\n\n## 26.1.0\n\n* Updated website documentation in [asyncredux.com](https://asyncredux.com).\n\n* Added the `Fresh` mixin.\n\n  Suppose you want to load from the server the information needed to show a\n  `UserProfileScreen`. You can dispatch action `LoadUserProfile` from the\n  `initState()` method of your widget:\n\n  ```dart \n  class UserProfileScreen extends StatefulWidget {\n    _UserProfileScreenState createState() => _UserProfileScreenState();\n  }\n\n  class _UserProfileScreenState extends State<UserProfileScreen> {\n    void initState() {\n      super.initState();\n      store.dispatch(LoadUserProfile()); // Here!\n    }\n\n    Widget build(BuildContext context) => ...\n  }\n  ```\n\n  Now, add `with Fresh` to the `LoadUserProfile` action, which loads the user\n  profile:\n\n  ```dart\n  class LoadUserProfile extends AppAction with Fresh {\n\n    Future<AppState> reduce() async {\n      var profile = await loadUserProfile();\n      return state.copy(profile: profile);\n    }\n  }\n  ```           \n\n  To keep the data fresh for one minute, override `freshFor`, which is in\n  milliseconds.\n\n  ```dart\n  class LoadUserProfile extends AppAction with Fresh {\n     int freshFor = 60000; // Here!\n     ...\n  }\n  ```\n\n  Now, if the user leaves the screen and returns in less than a minute, the\n  profile will not be loaded again, because it is still fresh. If the user\n  returns later, the information is loaded again.\n\n  ### Another example\n\n  Suppose widget `UserAvatar` loads its own information when mounted:\n\n  ```dart\n    class UserAvatar extends StatefulWidget {\n        final String userId;\n        UserAvatar(this.userId);\n        _UserAvatarState createState() => _UserAvatarState();\n    }\n  \n    class _UserAvatarState extends State<UserAvatar> {\n        void initState() {\n          super.initState();\n          store.dispatch(LoadUserAvatar(widget.userId)); // Here!\n        }\n  \n        Widget build(BuildContext context) => ...\n    }\n  ```      \n\n  Now add `with Fresh` to the `LoadUserAvatar` action, which loads the user\n  avatar, and also override `freshKeyParams()` so that each different user id\n  has its own fresh period:\n\n  ```dart\n  class LoadUserAvatar extends AppAction with Fresh {\n    final String userId;\n    LoadUserAvatar(this.userId);\n  \n    Object? freshKeyParams() => userId; // Here!\n    \n    Future<AppState> reduce() async {\n      var avatar = await loadUserAvatar(userId);\n      return state.copy(avatars: {...state.avatars, userId: avatar});\n    }\n  }\n  ```     \n\n  Now, if the avatar for a given user is shown more than once in the screen, it\n  will only be loaded for that user once, effectively deduplicating\n  the loading of the same avatar multiple times.\n\n  ### In more detail\n\n  The `Fresh` mixin lets you mark the result of an action as \"fresh\" for a set\n  amount of time. While the result is fresh, repeated dispatches of the same\n  action (or of other actions that share the same fresh key) are skipped because\n  the current state already has valid data. When the fresh period ends, the\n  result becomes \"stale\" and the next dispatch runs the action again.\n\n  This is useful for actions that load information from a server. You can think\n  of the fresh period as the time during which the loaded data is still good to\n  use.\n\n  ### Fresh-keys\n\n  By default, the fresh-key is based on the action `runtimeType` and the value\n  returned by `freshKeyParams()`. If you need separate fresh periods per id,\n  url, or some other field, override `freshKeyParams()`:\n\n  ```dart\n  class LoadUserCart extends AppAction with Fresh {\n    final String userId;\n    LoadUserCart(this.userId);\n\n    // Each different `userId` in action LoadUserCart has its own fresh period.\n    Object? freshKeyParams() => userId;\n    ...\n  }\n  ```\n\n  You can also return a tuple if you want the key to depend on more than one\n  field:\n\n  ```dart\n  // Each different `LoadUserCart`, `userId`, and `cartId` combination has its own fresh period.\n  Object? freshKeyParams() => (userId, cartId);\n  ```\n\n  The `Fresh` mixin has many other useful features. See the documentation at\n  [asyncredux.com](https://asyncredux.com) to learn about `ignoreFresh`,\n  `computeFreshKey()`, and more.\n\n* Mixins now warn you when you use incompatible mixins together.\n\n* Now, you can simply use `dispatch(action)` in widgets, instead of\n  `context.dispatch(action)`. For example:\n\n  ```dart\n  Widget build(BuildContext context) {\n    return ElevatedButton(\n      onPressed: () {\n        dispatch(MyAction()); // Here!\n      },\n    child: Text('Press me'),\n    );\n  }\n  ```\n\n  The same applies to the other dispatch extension methods like\n  `dispatchAndWait()`, `dispatchAll()`, `dispatchAndWaitAll()`,\n  and `dispatchSync()`.\n\n  Note this works only when your app has a single StoreProvider, which is\n  recommended and almost always true. Otherwise, you need to continue using\n  `context.dispatch()` etc.\n\n## 26.0.0\n\n* **BREAKING**: This version requires newer Android tooling (Android Gradle\n  Plugin 8.12.1 or higher, Gradle 8.13 or higher, and Kotlin 2.2.0). Projects\n  using older Android setups must update their environment before upgrading to\n  this release. Workaround: If you want to keep using older Gradle plugins,\n  simply add the following to the dependencies in your `pubspec.yaml` file:\n  `connectivity_plus: ^6.0.0`.\n\n\n* You can now use the new `MockBuildContext` to test **connector widgets**\n  (smart widgets) that rely on `BuildContext` extensions like `context.state`,\n  `context.select()`, `context.dispatch()`, and others.\n\n  This lets you test both state and callbacks without putting the widget in the\n  widget tree (regular `test` calls, no need to use `testWidgets`).\n  For example:\n\n  ```dart\n  // Define your smart widget (connector) using context extensions.\n  class MyConnector extends StatelessWidget {\n    @override\n    Widget build(BuildContext context) {\n      return MyWidget(\n        name: context.state.name,\n        onChangeName: () => context.dispatch(ChangeName('Bob')),\n      );\n    }\n  }\n  \n  class ChangeName extends ReduxAction<AppState> {\n    final String newName;\n    ChangeName(this.newName);\n    AppState reduce() => state.copy(name: newName);\n  }\n\n  // Test the connector.\n  test('MyConnector', () {\n    // Create a store with the desired state.\n    var store = Store<AppState>(initialState: AppState(name: 'John'));\n\n    // Create a mock context and build the widget.\n    var context = MockBuildContext(store);\n    var widget = MyConnector().build(context) as MyWidget;\n\n    // Test the widget state.\n    expect(widget.name, 'John');\n\n    // Test the widget callbacks. \n    widget.onChangeName();\n    expect(store.state.name, 'Bob');\n  });\n  ```                       \n\n  Note, the dumb widget `MyWidget` is a simple widget that takes `name` and\n  `onChangeName`. You can test it with normal presentation tests (using\n  `testWidgets`) without a store. Just pass the needed values to its\n  constructor. For example:\n\n  ```dart\n  // Define your dumb widget.\n  class MyWidget extends StatelessWidget {\n    final String name;\n    final VoidCallback onChangeName;\n    const MyWidget({required this.name, required this.onChangeName});\n\n    @override\n    Widget build(BuildContext context) {\n      return TextButton(onPressed: onChangeName, child: Text(name));\n    }\n  }\n       \n  // Test it.\n  testWidgets('MyWidget', (tester) async {\n    bool called = false;\n    await tester.pumpWidget(MaterialApp(\n      home: MyWidget(name: 'John', onChangeName: () => called = true),\n    ));\n\n    expect(find.text('John'), findsOneWidget);\n    await tester.tap(find.byType(TextButton));\n    expect(called, true);\n  });\n  ```\n\n* `StoreConnector` is now considered deprecated.\n  It will **not** be marked as deprecated and will **never** be removed,\n  but you don't need to use it for new code.\n  For new code, when you want to implement the smart/dumb widget pattern,\n  prefer `BuildContext` extensions to implement the pattern,\n  along with `MockBuildContext` for testing, as shown above.\n\n  The goal of `StoreConnector` was to separate dumb widgets from smart widgets\n  and let you test the view model without mounting it. Then you could test the\n  dumb widget with simple presentation tests.\n  `MockBuildContext` gives you the same benefits, because the dumb widget\n  itself, when built with a mock context, works as the view model you can\n  inspect and use to call callbacks.\n\n  This makes `StoreConnector` unnecessary. `MockBuildContext` is simpler to use\n  and avoids extra view model classes and factories.\n\n## 25.6.3\n\n* You can now use context extensions to dispatch actions from the `initState()`\n  and `dispose()` methods of a `StatefulWidget`.\n\n  ```dart\n  class MyScreen extends StatefulWidget {    \n    State<MyScreen> createState() => _MyScreenState();\n  }\n\n  class _MyScreenState extends State<MyScreen> {\n    \n    void initState() {\n      super.initState();\n      context.dispatch(LoadDataAction());\n    }\n\n    void dispose() {\n      context.dispatch(CleanupAction());\n      super.dispose();\n    }\n\n    Widget build(BuildContext context) => Text(context.state.data);\n  }\n  ```                                            \n\n  Note: For this feature to work, your app must have a single `StoreProvider`\n  (that's usually the case).\n\n## 25.6.2\n\n* You can now use the selector extension `context.select((state) => ...)` to\n  select only the part of the state you need in your widget, so that your widget\n  only rebuilds when that particular part of the state changes. For example:\n\n  ```dart\n  var myInfo = context.select((state) => state.myInfo);\n  ```                          \n\n  Note you can also access your state directly with `context.state.myInfo`,\n  but that will rebuild your widget whenever **any** part of the state changes.\n  Using `context.select()` is more efficient because it only rebuilds\n  your widget when the selected part of the state changes.\n\n  Suggestion: When creating the first draft of your widget, you may\n  use `context.state` just to get started quickly, and then later change it\n  to use `context.select()` to optimize the rebuilds.\n\n  If you want to read your state and NOT rebuild your widget when the state\n  changes, you can use `context.read()`. For example:\n\n  ```dart\n  var myInfo = context.read().myInfo;\n  ```\n\n  However, to use `context.select()`, `context.read()`, and `context.state`\n  as shown above, you need to define the following extension method in your\n  own code (assuming your state class is called `AppState`):\n\n  ```dart  \n  extension BuildContextExtension on BuildContext {        \n    R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);    \n  }\n  ```\n\n  Note, you can also use the other context extension methods like\n  `context.dispatch`, `context.isWaiting`, `context.isFailed`,\n  `context.exceptionFor`, `context.event`, `context.clearExceptionFor`,\n  `context.env`, and much more.\n\n  See\n  the: <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_select.dart\">\n  Select Example</a>.\n\n* You can now use the event extension `context.event((state) => ...)` to consume\n  events from the state. These are one-time notifications used to trigger side\n  effects in widgets, such as showing dialogs, clearing text fields, or\n  navigating to new screens. Unlike regular state values, events are\n  automatically \"consumed\" (marked as spent) after being read, ensuring they\n  only trigger once.\n\n  First, define events in your state class and initialize them as spent:\n\n  ```dart\n  class AppState {\n    final Evt clearTextEvt;\n    final Evt<String> changeTextEvt;\n\n    AppState({required this.clearTextEvt, required this.changeTextEvt});\n\n    static AppState initialState() => AppState(\n      clearTextEvt: Evt.spent(),\n      changeTextEvt: Evt<String>.spent(),\n    );\n  }\n  ```\n\n  Then, your actions create new events by adding them in the state:\n\n  ```dart          \n  // Boolean event.\n  class ClearText extends AppAction {    \n    AppState reduce() => state.copy(clearTextEvt: Evt());\n  }\n                \n  // Event with a String payload.\n  class ChangeText extends AppAction {    \n    Future<AppState> reduce() async {\n      String newText = await fetchTextFromApi();\n      return state.copy(changeTextEvt: Evt<String>(newText));\n    }\n  }\n  ```\n\n  Finally, use `context.event((state) => ...)` to consume events\n  in the build method of your widgets:\n\n  ```dart\n  bool clearText = context.event((state) => state.clearTextEvt);\n  if (clearText) controller.clear();\n\n  String? newText = context.event((state) => state.changeTextEvt);\n  if (newText != null) controller.text = newText;\n  ```\n\n  To use `context.event()` as shown above, you need to define the following\n  extension method in your own code (assuming your state class is called\n  `AppState`):\n\n  ```dart\n  extension BuildContextExtension on BuildContext {    \n    R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);\n  }\n  ```\n\n  Important notes:\n    - Events are consumed only once. After consumption, they are marked as \"\n      spent\"\n      and won't trigger again until a new event is dispatched.\n    - Each event can be consumed by **only one widget**. If you need multiple\n      widgets to react to the same trigger, use separate events in the state.\n    - Initialize events in the state as spent: `Evt.spent()` or\n      `Evt<T>.spent()`.\n    - For events with **no generic type** (`Evt`): Returns **true** if the event\n      was dispatched, or **false** if it was already spent.\n    - For events with **a value type** (`Evt<T>`): Returns the **value** if the\n      event was dispatched, or **null** if it was already spent.\n\n  See\n  the: <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_event.dart\">\n  Event Example</a>.\n\n* You can now use the environment extension `context.env` to access the store\n  \"environment\" for dependency injection. This environment is a container for\n  injected services that can be accessed from both widgets and actions.\n\n  First, define your environment interface and implementation:\n\n  ```dart\n  abstract class Environment {\n    ApiService get api;\n    AuthService get auth;\n  }\n\n  class EnvironmentImpl implements Environment {    \n    final ApiService api = ApiServiceImpl();\n    final AuthService auth = AuthServiceImpl();\n  }\n  ```\n\n  Then, provide the environment when creating the store:\n\n  ```dart\n  var store = Store<AppState>(\n    initialState: AppState.initialState(),\n    environment: EnvironmentImpl(),\n  );\n  ```\n\n  To access the environment in your actions, extend `ReduxAction` to provide\n  typed access:\n\n  ```dart\n  abstract class Action extends ReduxAction<AppState> {    \n    Environment get env => super.env as Environment;\n  }\n                                       \n  // Usage\n  class LoadUserAction extends Action {    \n    Future<AppState> reduce() async {\n      var user = await env.api.getUser();\n      return state.copy(user: user);\n    }\n  }\n  ```\n\n  To access the environment in your widgets, define an extension method:\n\n  ```dart\n  extension BuildContextExtension on BuildContext {    \n    Environment get env => getEnvironment<AppState>() as Environment;\n  }\n  ```\n\n  Then use it in your widgets:\n\n  ```dart\n  Widget build(BuildContext context) {\n    final env = context.env;\n    // Use env.api, env.auth, etc.\n    ...\n  }\n  ```\n\n  Benefits of using the environment:\n    - **Dependency Injection**: Inject services, repositories, and other\n      dependencies.\n    - **Testability**: Easily swap implementations for testing (mock services,\n      test APIs, etc.).\n    - **Clean Architecture**: Keep your actions and widgets decoupled from\n      concrete implementations.\n\n  See\n  the: <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_dependency_injection.dart\">\n  Environment Example</a>.\n\n## 25.4.0\n\n* Added `Store.disposeProp(key)` and `Action.disposeProp(key)` methods to\n  dispose and remove Futures/Timers/Streams that were previously set\n  using `setProp()`.\n  See [Streams and Timers](https://asyncredux.com/flutter/miscellaneous/streams-and-timers/).\n\n## 25.3.1\n\n* In tests, you can now use `store.dispatchAndWaitAllActions`. It first\n  dispatches an `action`, and then it waits until ALL current actions in\n  progress finish dispatching. In other words, it helps make sure that the\n  app state \"settled\" before you check the state.\n\n  ```dart\n  await store.dispatchAndWaitAllActions(MyAction());\n  ```\n\n* Some internal properties used by the provided mixins are now tied to the\n  `Store` so that they reset when the store is recreated. This is useful to make\n  sure tests are not affected by previous tests. For example, if you dispatch an\n  action that has some throttle, then recreate the store for another test, you\n  can dispatch the same action again without waiting for the throttle to expire.\n  You can also manually delete all those properties by calling\n  `store.internalMixinProps.clear()`.\n\n* If you are running tests, you can change `store.forceInternetOnOffSimulation`\n  to simulate the internet connection as ON or OFF for the provided mixins\n  `CheckInternet`, `AbortWhenNoInternet`, and `UnlimitedRetryCheckInternet`:\n\n  ```dart           \n  // There is internet\n  store.forceInternetOnOffSimulation = () => false;\n  \n  // There is no internet\n  store.forceInternetOnOffSimulation = () => false;\n  \n  // Uses the real internet connection status (default).\n  store.forceInternetOnOffSimulation = () => null;\n  ```\n\n## 25.1.1\n\n* The `Throttle` action mixin now has an `ignoreThrottle` parameter, which\n  allows you to ignore the throttle period for a specific action. This is useful\n  when you want to bypass the throttle for certain actions, while still applying\n  it to others. For example:\n\n  ```dart\n  class MyAction extends ReduxAction<AppState> with Throttle {\n     final bool force;\n     MyAction({this.force = false});  \n     bool get ignoreThrottle => force; // Here!   \n     ...\n  }\n  ```\n\n* The `Throttle` action mixin now has a `removeLockOnError` parameter, which\n  removes the lock when an error occurs. This is useful when you want to allow\n  a failed action to run again within the throttle period. For example:\n\n  ```dart\n  class MyAction extends ReduxAction<AppState> with Throttle {\n     bool removeLockOnError = true; // Here!\n     ...\n  }\n  ```\n\n* If your app uses AsyncRedux and your server\n  uses [Serverpod](https://serverpod.dev/), you can add the Dart-only core\n  package https://pub.dev/packages/async_redux_core to your server side.\n  Now, if you throw a `UserException` in your backend code, that exception will\n  automatically be thrown in the frontend. As long as the Serverpod cloud\n  function is called inside an action, AsyncRedux will display the\n  exception message to the user in a dialog (or other UI element that you can\n  customize). Note: This can also be used\n  with [package i18n_extension_core](https://pub.dartlang.org/packages/i18n_extension_core)\n  to make sure the error message gets translated to the user's language.\n  For example: `UserException('The password you typed is invalid'.i18n);` in the\n  backend, will reach the frontend already translated as\n  `UserException('La contraseña que ingresaste no es válida')` if the user\n  device is in Spanish.\n\n  Setup: For all this to work in Serverpod, after you import `async_redux_core`\n  in the `pubspec.yaml` file of the server project, you must add the\n  `UserException` class to your `generator.yaml` file, in its `extraClasses`\n  section:\n\n  ```yaml  \n  type: server\n  ...\n  \n    extraClasses:\n      - package:async_redux_core/async_redux_core.dart:UserException\n  ```  \n\n  Note: AsyncRedux also works with [Celest](https://celest.dev/) since 22.1.0.\n\n## 25.0.0\n\n* **BREAKING**: The action's `wrapReduce` method now returns `FutureOr<St?>`\n  instead of returning `FutureOr<St?> Function()`\n  This breaking change is unlikely to affect you in any way, because the\n  `wrapReduce` is an advanced feature mostly used to implement action mixins,\n  like `Retry` and `Debounce`.\n\n\n* You can now use the `Debounce` action mixin.\n  Debouncing delays the execution of a function until after a certain period\n  of inactivity. Each time the debounced function is called, the period of\n  inactivity (or wait time) is reset.\n\n  The function will only execute after it stops being called for the duration\n  of the wait time. Debouncing is useful in situations where you want to\n  ensure that a function is not called too frequently and only runs after\n  some “quiet time.”\n\n  For example, it’s commonly used for handling input validation in text fields,\n  where you might not want to validate the input every time the user presses\n  a key, but rather after they've stopped typing for a certain amount of time.\n\n  The `debounce` value is given in milliseconds, and the default is 333\n  milliseconds (1/3 of a second). You can override this default:\n\n  ```dart\n  class MyAction extends ReduxAction<AppState> with Debounce {\n     final int debounce = 1000; // Here!\n     ...\n  }\n  ```\n\n  ### Advanced debounce usage\n\n  The debounce is, by default, based on the action `runtimeType`. This means\n  it will reset the debounce period when another action of the same\n  runtimeType was is dispatched within the debounce period. In other words,\n  the runtimeType is the \"lock\". If you want to debounce based on a different\n  lock, you can override the `lockBuilder` method. For example, here\n  we debounce two different actions based on the same lock:\n\n  ```dart\n  class MyAction1 extends ReduxAction<AppState> with Debounce {\n     Object? lockBuilder() => 'myLock';\n     ...\n  }\n  \n  class MyAction2 extends ReduxAction<AppState> with Debounce {\n     Object? lockBuilder() => 'myLock';\n     ...\n  }\n  ```\n\n  Another example is to debounce based on some field of the action:\n\n  ```dart\n  class MyAction extends ReduxAction<AppState> with Debounce {\n     final String lock;\n     MyAction(this.lock);\n     Object? lockBuilder() => lock;\n     ...\n  }\n  ```\n\n  See\n  the [Documentation](https://asyncredux.com/flutter/advanced-actions/action-mixins#debounce).\n\n\n* You can now use the `Throttle` action mixin.\n  Throttling ensures the action will be dispatched at most once in the\n  specified throttle period. In other words, it prevents the action from\n  running too frequently.\n\n  If an action is dispatched multiple times within a throttle period, it will\n  only execute the first time, and the others will be aborted. After the\n  throttle period has passed, the action will be allowed to execute again,\n  which will reset the throttle period.\n\n  If you use the action to load information, the throttle period may be\n  considered as the time the loaded information is \"fresh\". After the\n  throttle period, the information is considered \"stale\" and the action will\n  be allowed to load the information again.\n\n  For example, if you are using a `StatefulWidget` that needs to load some\n  information, you can dispatch the loading action when widget is created,\n  and specify a throttle period so that it doesn't load the information again\n  too often.\n\n  If you are using a `StoreConnector`, you can use the `onInit` parameter:\n\n  ```dart\n  class MyScreenConnector extends StatelessWidget {\n    Widget build(BuildContext context) => StoreConnector<AppState, _Vm>(\n      vm: () => _Factory(),\n      onInit: _onInit, // Here!\n      builder: (context, vm) {\n        return MyScreenConnector(\n          information: vm.information,\n          ...\n        ),\n      );\n  \n    void _onInit(Store<AppState> store) {\n      store.dispatch(LoadAction());\n    }\n  }\n  ```\n\n  and then:\n\n  ```dart\n  class LoadAction extends ReduxAction<AppState> with Throttle {\n  \n    final int throttle = 5000;\n  \n    Future<AppState?> reduce() async {\n      var information = await loadInformation();\n      return state.copy(information: information);\n    }\n  }\n  ```\n\n  The `throttle` is given in milliseconds, and the default is 1000\n  milliseconds (1 second). You can override this default:\n\n  ```dart\n  class MyAction extends ReduxAction<AppState> with Throttle {\n     final int throttle = 500; // Here!\n     ...\n  }\n  ```\n\n  ### Advanced throttle usage\n\n  The throttle is, by default, based on the action `runtimeType`. This means\n  it will throttle an action if another action of the same runtimeType was\n  previously dispatched within the throttle period. In other words, the\n  runtimeType is the \"lock\". If you want to throttle based on a different\n  lock, you can override the `lockBuilder` method. For example, here\n  we throttle two different actions based on the same lock:\n\n  ```dart\n  class MyAction1 extends ReduxAction<AppState> with Throttle {\n     Object? lockBuilder() => 'myLock';\n     ...\n  }\n  \n  class MyAction2 extends ReduxAction<AppState> with Throttle {\n     Object? lockBuilder() => 'myLock';\n     ...\n  }\n  ```\n\n  Another example is to throttle based on some field of the action:\n\n  ```dart\n  class MyAction extends ReduxAction<AppState> with Throttle {\n     final String lock;\n     MyAction(this.lock);\n     Object? lockBuilder() => lock;\n     ...\n  }\n  ```   \n\n  See\n  the [Documentation](https://asyncredux.com/flutter/advanced-actions/action-mixins#throttle).\n\n## 24.0.7\n\n* Added some missing params to `MockStore` constructor.\n\n## 24.0.6\n\n* Fixed translation typo bug.\n\n## 24.0.2\n\n* `LocalPersist` and `LocalJsonPersist` now allow you to define the base\n  directory by\n  setting the `useBaseDirectory` static field. The default is, as before, the\n  application's documents directory. Other options are the cache directory\n  (`LocalPersist.useAppCacheDir`), the downloads directory\n  (`LocalPersist.useAppDownloadsDir`), or any other custom directory\n  (`LocalPersist.useCustomBaseDirectory`).\n\n## 23.2.0\n\n* You can now use the `UnlimitedRetryCheckInternet` to check if there is\n  internet when you\n  run some action that needs it. If there is no internet, the action will abort\n  silently\n  and then retried unlimited times, until there is internet. It will also retry\n  if there\n  is internet but the action failed.\n\n\n* You can provide a `CloudSync` object to the store constructor. It's similar to\n  the `Persistor`, but can be used to synchronize the state of the application\n  with the server. This is experimental.\n\n\n* Fixed `isWaiting()` for checking multiple actions and when state doesn't\n  change.\n\n## 23.1.1\n\n* New: AsyncRedux website at https://asyncredux.com\n\n* New: [AsyncRedux for React](https://www.npmjs.com/package/async-redux-react)\n\n## 23.0.2\n\n* Fixed `isWaiting()` when action fails.\n\n## 23.0.1\n\n* Fixed `disposeProps`.\n\n## 23.0.0\n\n* Now using `connectivity_plus: 6.0.3` or up.\n\n## 22.5.0\n\n* You can now use `dispatchAll()` and `dispatchAndWaitAll()` to dispatch\n  multiple actions\n  in parallel. For example:\n\n  ```dart   \n  class BuyAndSell extends Action {\n    Future<AppState> reduce() async {\n       \n      await dispatchAndWaitAll([\n        BuyAction('IBM'), \n        SellAction('TSLA')\n      ]);\n  \n      return state.copy(message: 'New cash balance is ${state.cash}');\n    }\n  }\n  ```  \n\n## 22.4.9\n\n* For those who use `flutter_hooks`, you can now use the\n  new https://pub.dev/packages/flutter_hooks_async_redux package\n  to add Redux to flutter_hooks.\n\n## 22.3.0\n\n* In the `reduce` method of your actions you can now access the _initial state_\n  of the action, by using the `initialState` getter. In other words, you have\n  access to a copy of the state as it was when the action was first dispatched.\n  This is useful when you need to calculate some value asynchronously, and then\n  you only want to apply the result to the state if that value hasn't changed in\n  the meantime. For example:\n\n  ```dart\n  class MyAction extends ReduxAction<AppState> {\n    Future<AppState> reduce() async {\n      var newValue = await someAsyncStuff();\n      if (state.value == initialState.value) return state.copyWith(value: newValue);\n      else return null;\n    }\n  }\n  ```   \n\n## 22.1.0\n\n* You can now use `var isWaiting = context.isWaiting(MyAction)` to check if an\n  async action of the given type is currently being processed. You can then use\n  this\n  boolean to show a loading spinner in your widget.\n  Note: Inside your `VmFactory` you can also use\n  `isWaiting: isWaiting(MyAction)`. See\n  the <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_show_spinner.dart\">\n  Show Spinner Example</a>.\n\n\n* You can now use `var isFailed = context.isFailed(MyAction)` to check if an\n  action of the given type has thrown an `UserException`. You can then use this\n  boolean to show an error message.\n  You can also get the exception with\n  `var exception = context.exceptionFor(MyAction)` to\n  use its error message, and clear the exception with\n  `context.clearExceptionFor(MyAction)`.\n  Note: Inside your `VmFactory` you can also use `isFailed: isFailed(MyAction)`\n  etc. See\n  the <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_show_error_dialog.dart\">\n  Show Error Dialog Example</a>.\n\n\n* You can add **mixins** to your actions, to accomplish common tasks:\n\n    - `CheckInternet` ensures actions only run with internet, otherwise an error\n      dialog\n      prompts users to check their connection:\n\n      ```dart\n      class LoadText extends ReduxAction<AppState> with CheckInternet {\n          \n      Future<String> reduce() async {\n          var response = await http.get('http://numbersapi.com/42');\n          ...      \n      }}\n      ```\n\n    - `NoDialog` can be added to `CheckInternet` so that no dialog is opened.\n      Instead, you can display some information in your widgets:\n\n      ```dart\n      class LoadText extends Action with CheckInternet, NoDialog { ... }\n      \n      if (context.isFailed(LoadText)) Text('No Internet connection');\n      ```\n\n    - `AbortWhenNoInternet` aborts the action silently (without showing any\n      dialogs)\n      if there is no internet connection.\n\n    - `NonReentrant` prevents reentrant actions, so that when you dispatch an\n      action\n      that's already running it gets aborted (no errors are shown).\n\n    - `Retry` retries the action a few times with exponential backoff, if it\n      fails.\n      Add `UnlimitedRetries` to retry the action indefinitely:\n\n      ```dart\n      class LoadText extends ReduxAction<AppState> with Retry, UnlimitedRetries, NonReentrant { \n      ```\n\n  Other mixins will be provided in the future, for Throttling, Debouncing and\n  Caching.\n\n\n* Some features of the `async_redux` package are now available in a standalone\n  Dart-only core package: https://pub.dev/packages/async_redux_core. You may use\n  that core package when you are developing a Dart server (backend)\n  with [Celest](https://celest.dev/), or when developing your own Dart-only\n  package that does not depend on Flutter.\n  Note: For the moment, the core package simply contains the `UserException`,\n  and nothing else.\n  If you now import `async_redux_core` in your Celest server code and throw an\n  `UserException` there, the exception message will automatically be shown in a\n  dialog to the user in your client app (if you use the `UserExceptionDialog`\n  feature).\n\n  > **For Flutter applications nothing changes.**\n  > You don't need to import the core package directly.\n  > You should continue to use this async_redux package, which already exports\n  > the code that's now in the core package.\n\n* You can now access the store inside of widgets, and have your widgets rebuild\n  when the state changes, by using `context.state` and `context.dispatch` etc.\n  This is only useful when you want to access the store state, and dispatch\n  actions directly inside your widgets, instead of using the `StoreConnector` (\n  dumb widget / smart widget pattern). For example:\n\n  ```dart\n  // Read state (will rebuild when the state changes) \n  var myInfo = context.state.myInfo;\n  \n  // Dispatch action\n  context.dispatch(MyAction());\n  \n  // Use isWaiting to show a spinner\n  if (context.isWaiting(MyAction)) return CircularProgressIndicator();\n  \n  // Use isFailed to show an error message\n  if (context.isFailed(MyAction)) return Text('Loading failed');\n                                                                   \n  // Use exceptionFor to get the error message from the exception\n  if (context.isFailed(MyAction)) return Text(context.exceptionFor(MyAction).message);\n  \n  // Use clearExceptionFor to clear the error\n  context.clearExceptionFor(MyAction);\n  ```      \n\n  However, to use `context.state` as shown above, you need to define the\n  following extension method in your own code (assuming your state class is\n  called `AppState`):\n\n  ```dart  \n  extension BuildContextExtension on BuildContext {\n     AppState get state => getState<AppState>();       \n  }\n  ```     \n\n  See\n  the: <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/store_connector_examples/main_extension_vs_store_connector.dart\">\n  Connector vs Provider Example</a>.\n\n\n* You can now get and set properties in the `Store` using the `prop` and\n  `setProp` methods.\n  These methods are available in `Store`, in `ReduxAction`, and in `VmFactory`.\n  They can be used to save global values, but scoped to the store.\n  For example, you could save timers, streams or futures used by actions:\n\n  ```dart  \n  setProp(\"timer\", Timer(Duration(seconds: 1), () => print(\"tick\")));\n  var timer = prop<Timer>(\"timer\");\n  timer.cancel();\n  ```   \n\n  You can later use `store.disposeProps` to stop, close or ignore, all stream\n  related objects, timers and futures, saved as props in the store. It will also\n  remove them from there.\n\n## 22.0.0\n\n* **BREAKING**: `StoreConnector.model` was removed, after being deprecated\n  for a long time. Please, use the `vm` parameter instead. See classes\n  `VmFactory` and `Vm`.\n\n* **BREAKING**: `ReduxAction.reduceWithState()` was removed, after being\n  deprecated for a long time.\n\n* **BREAKING**: `StoreProvider.of` was removed. See `context.state` and\n  `context.dispatch` etc, in version 22.1.0 above.\n\n* **BREAKING**: The `UserException` class was modified so that it was\n  possible to move it to the `async_redux_core`. If your use of `UserException`\n  was limited to specifying the error message, then you don't need to change\n  anything:\n  `throw UserException('Error message')` will continue to work as before.\n  However, for other more advanced features you will have to read the\n  `UserException`\n  documentation and adapt. In the new public API of `UserException` you can now\n  specify a `message`, `reason`, `code`, `errorText` and `ifOpenDialog` in the\n  constructor, and then you can use methods `addCallbacks`, `addCause`,\n  `addProps`,\n  `withErrorText` and `noDialog` to add more information:\n\n  ```dart\n  throw UserException('Invalid number', reason: 'Must be less than 42')\n     .addCallbacks(onOk: () => print('OK'), onCancel: () => print('CANCEL'))\n     .addCause(FormatException('Invalid input'))\n     .addProps({'number': 42}))                                                  \n     .withErrorText('Type a smaller number')\n     .noDialog;\n  ```                  \n\n  Note the `code` parameter can only be a number now. If you were using a\n  different type,\n  for example enums, you can now include it in the props, like\n  so: `throw UserException('').addProps({'code': myError.invalidInput}).` or you\n  can even\n  create an extension method which allows you to\n  write `throw UserException('').withCode(myError.invalidInput).`\n  However, please read the new `UserException` documentation to learn about the\n  recommended way to use `code` to define the text of the error messages, and\n  even easily\n  translate them to the user language by using\n  the [i18n_extension](https://pub.dev/packages/i18n_extension) translations\n  package.\n\n* To test the view-model generated by a `VmFactory`, you can now use the static\n  method `Vm.createFrom(store, factory)`. The method will return the view-model,\n  which you\n  can use to inspect the view-model properties directly, or call any of the\n  view-model\n  callbacks. Example:\n\n  ```dart\n  var store = Store(initialState: User(\"Mary\"));\n  var vm = Vm.createFrom(store, MyFactory());\n  \n  // Checking a view-model property.    \n  expect(vm.user.name, \"Mary\");\n  \n  // Calling a view-model callback and waiting for the action to finish.  \n  vm.onChangeNameTo(\"Bill\"); // Dispatches SetNameAction(\"Bill\").\n  await store.waitActionType(SetNameAction);\n  expect(store.state.name, \"Bill\");    \n  \n  // Calling a view-model callback and waiting for the state to change.\n  vm.onChangeNameTo(\"Bill\"); // Dispatches SetNameAction(\"Bill\").\n  await store.waitCondition((state) => state.name == \"Bill\");\n  expect(store.state.name, \"Bill\");\n  ```\n\n* DEPRECATION WARNING: While the `StoreTester` is a powerful tool with advanced\n  features\n  that are beneficial for the most complex testing scenarios, for **almost all\n  tests**\n  it's now recommended to use the `Store` directly. This approach involves\n  waiting for an\n  action to complete its dispatch process or for the store state to meet a\n  certain\n  condition. After this, you can verify the current state or action using the\n  new\n  methods `store.dispatchAndWait`, `store.waitCondition`,\n  `store.waitActionCondition`,\n  `store.waitAllActions`, `store.waitActionType`, `store.waitAllActionTypes`,\n  and `store.waitAnyActionTypeFinishes`. For example:\n\n  ```dart\n  // Wait for some action to dispatch and check the state. \n  await store.dispatchAndWait(MyAction());\n  expect(store.state.name, 'John')\n  \n  // Wait for some action to dispatch, and check for errors in the action status.\n  var status = await dispatchAndWait(MyAction());\n  expect(status.originalError, isA<UserException>());\n  \n  // Dispatches two actions in SERIES (one after the other).\n  await dispatchAndWait(SomeAsyncAction());\n  await dispatchAndWait(AnotherAsyncAction());\n  \n  // Dispatches two actions in PARALLEL and wait for their TYPES.\n  expect(store.state.portfolio, ['TSLA']);\n  dispatch(BuyAction('IBM'));\n  dispatch(SellAction('TSLA'));\n  await store.waitAllActionTypes([BuyAction, SellAction]);\n  expect(store.state.portfolio, ['IBM']);\n  \n  // Dispatches two actions in PARALLEL and wait for them.\n  let action1 = BuyAction('IBM');\n  let action2 = BuyAction('TSLA');\n  dispatch(action1);\n  dispatch(action2);\n  await store.waitAllActions([action1, action2]);\n  expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  \n  // Wait until no actions are in progress.\n  dispatch(BuyStock('IBM'));\n  dispatch(BuyStock('TSLA'));  \n  await waitAllActions([]);                 \n  expect(state.stocks, ['IBM', 'TSLA']);\n  \n  // Wait for some action of a given type.\n  dispatch(ChangeNameAction());\n  var action = store.waitActionType(ChangeNameAction);\n  expect(action, isA<ChangeNameAction>());\n  expect(action.status.isCompleteOk, isTrue);\n  expect(store.state.name, 'Bill');\n  \n  // Wait until any action of the given types finishes dispatching.\n  dispatch(BuyOrSellAction());   \n  var action = store.waitAnyActionTypeFinishes([BuyAction, SellAction]);  \n  expect(store.state.portfolio.contains('IBM'), isTrue);\n  \n  // Wait for some state condition.\n  expect(store.state.name, 'John')               \n  dispatch(ChangeNameAction(\"Bill\"));\n  var action = await store.waitCondition((state) => state.name == \"Bill\");\n  expect(action, isA<ChangeNameAction>());\n  expect(store.state.name, 'Bill');  \n  ```                          \n\n  Note the `StoreTester` will NOT be removed, now or in the future. It's just\n  not the\n  recommended\n  way to test the store anymore.\n\n## 21.7.1\n\n* DEPRECATION WARNING:\n    - Replace `action.isFinished` with `action.status.isCompletedOk`\n    - Replace `action.status.isBeforeDone` with\n      `action.status.hasFinishedMethodBefore`\n    - Replace `action.status.isReduceDone` with\n      `action.status.hasFinishedMethodReduce`\n    - Replace `action.status.isAfterDone` with\n      `action.status.hasFinishedMethodAfter`\n    - Replace `action.status.isFinished` with `action.status.isCompletedOk`\n\n\n* The `action.status` now has a few more values:\n    - `isCompleted` if the action has completed executing, either with or\n      without errors.\n    - `isCompletedOk` if the action has completed with no errors.\n    - `isCompletedFailed` if the action has completed with errors.\n    - `originalError` Holds the error thrown by the action's before/reduce\n      methods, if\n      any.\n    - `wrappedError` Holds the error thrown by the action, after it was\n      processed by the\n      action's `wrapError` and the `globalWrapError`.\n\n## 21.6.0\n\n* DEPRECATION WARNING: The `wrapError` parameter of the `Store` constructor is\n  now\n  deprecated in\n  favor of the `globalWrapError` parameter. The reason for this deprecation is\n  that the\n  new `GlobalWrapError` works in the same way as the action's\n  `ReduxAction.wrapError`,\n  while `WrapError` does not. The difference is that when `WrapError` returns\n  `null`, the\n  original\n  error is not modified, while with `GlobalWrapError` returning `null` will\n  instead\n  disable the\n  error. In other words, where your old `WrapError` returned `null`, your new\n  `GlobalWrapError`\n  should return the original `error`:\n\n  ```\n  // WrapError (deprecated):\n  Object? wrap(error, stackTrace, action) {\n     if (error is MyException) return null; // Keep the error unaltered.\n     else return processError(error);\n  }\n  \n  // GlobalWrapError:\n  Object? wrap( error, stackTrace, action) {\n     if (error is MyException) return error; // Keep the error unaltered.\n     else return processError(error);\n  }\n  ```\n  Also note, `GlobalWrapError` is more powerful because it can disable the\n  error,\n  whereas `WrapError` cannot.\n\n* Throwing an error in the action's `wrapError` or in the `GlobalWrapError` was\n  disallowed\n  (you needed to make sure it never happened). Now, it's allowed. If instead of\n  RETURNING\n  an error\n  you THROW an error inside these wrappers, AsyncRedux will catch it and use it\n  instead\n  the original\n  error. In other words, returning an error or throwing an error from inside the\n  wrappers\n  now has\n  the same effect. However, it is still recommended to return the error rather\n  than\n  throwing it.\n\n## 21.5.0\n\n* DEPRECATION WARNING: Method `dispatchAsync` was renamed to `dispatchAndWait`.\n  The old\n  name is\n  still available, but deprecated and will be removed. The new name is more\n  descriptive of\n  what the\n  method does, and the fact that `dispatchAndWait` can be used to dispatch both\n  sync and\n  async\n  actions. The only difference between `dispatchAndWait` and `dispatch` is that\n  `dispatchAndWait`\n  returns a `Future` which can be awaited to know when the action is finished.\n\n## 21.1.1\n\n* `await StoreTester.dispatchAndWait(action)` dispatches an action, and then\n  waits until\n  it\n  finishes. This is the same as\n  doing: `storeTester.dispatch(action); await storeTester.wait(action);`.\n\n## 21.0.2\n\n* Flutter 3.16.0 compatible.\n\n## 20.0.2\n\n* Fixed `WrapReduce` (which may be used to wrap the reducer to allow for some\n  pre- or\n  post-processing) to avoid async reducers to be called twice.\n\n## 20.0.0\n\n* Flutter 3.10.0 and Dart 3.0.0\n\n## 19.0.2\n\n* Docs improvement.\n\n## 19.0.1\n\n* Flutter 3.7.5, Dart 2.19.2, fast_immutable_collections: 9.0.0.\n\n* **BREAKING**: The `Action.wrapError(error, stackTrace)` method now also\n  gets the\n  stacktrace\n  instead of just the error. If your code breaks, just add the extra parameter,\n  like so:\n  `Object wrapError(error) => ...` turns into\n  `Object wrapError(error, _) => ...`\n\n<br>\n\n* **BREAKING**: When a `Persistor` is provided to the Store, it now considers\n  the\n  `initialState` is already persisted. Before this change, it considered nothing\n  was\n  persisted. Note: Before you create the store, you are allowed to call the\n  `Persistor`\n  methods\n  directly: `Persistor.saveInitialState()`, `readState()` and `deleteState()`.\n  However, after you create the store, please don't call those methods yourself\n  anymore.\n  If you do it, AsyncRedux cannot keep track of which state was persisted. After\n  store\n  creation,\n  if necessary, you should use the corresponding methods\n  `Store.saveInitialStateInPersistence()`,\n  `Store.readStateFromPersistence()` and `Store.deleteStateFromPersistence()`.\n  These\n  methods let\n  AsyncRedux keep track of the persisted state, so that it's able to call\n  `Persistor.persistDifference()` with the appropriate parameters.\n\n<br>\n\n* Method `Store.getLastPersistedStateFromPersistor()` returns the state that was\n  last persisted to the local persistence. It's unlikely you will use this\n  method yourself.\n\n<br>\n\n* **BREAKING**: The factory declaration used to have two type parameters, but\n  now it\n  has three:\n  `class Factory extends VmFactory<AppState, MyConnector, MyViewModel>`\n  With that change, you can now reference the view-model inside the Factory\n  methods, by\n  using\n  the `vm` getter. Example:\n    ```\n    ViewModel fromStore() =>\n      ViewModel(\n        value: _calculateValue(),\n        onTap: _onTap);\n    }  \n    \n    void _onTap() => dispatch(SaveValueAction(vm.value)); // Use the value from the vm.\n    ```\n\n  Note 1: You can only use the `vm` getter after the `fromStore()` method is\n  called, which\n  means\n  you cannot reference the `vm` inside of the `fromStore()` method itself. If\n  you do that,\n  you'll get a `StoreException`. You also cannot use the `vm` getter if the\n  view-model is\n  null.\n\n  Note 2: To reduce boilerplate, and not having to pass the `AppState` type\n  parameter\n  whenever you\n  create a Factory, I recommend you define a base Factory, like so:\n    ```\n    abstract class BaseFactory<T extends Widget?, Model extends Vm> extends VmFactory<AppState, T, Model> {\n        BaseFactory([T? connector]) : super(connector);\n    }\n    ```\n\n* Added class LocalJsonPersist to help persist the state as pure Json.\n\n## 18.0.2\n\n* Fixed small bug when persistor is paused before being used once.\n\n## 18.0.0\n\n* Version bump of dependencies.\n\n## 17.0.1\n\n* Fixed issue with the StoreConnector.shouldUpdateModel method when the widget\n  updates.\n\n## 17.0.0\n\n* The `StateObserver.observe()` method signature changed to include an `error`\n  parameter:\n  ```\n  void observe(\n     ReduxAction<St> action,\n     St stateIni,\n     St stateEnd,\n     Object? error,\n     int dispatchCount,\n     );\n  ```\n\n  The state-observers are now also called when the action reducer complete with\n  a error.\n  In this case, the `error` object will not be null. This makes it easier to use\n  state-observers\n  for metrics. Please, see the documentation for the recommended clean-code way\n  to do\n  this.\n\n## 16.1.0\n\n* Added another cache function, for 2 states and 3 parameters:\n  `cache2states_3params`.\n\n## 16.0.0\n\n* **BREAKING**: Async `reduce()` methods (those that return Futures) are now\n  called\n  synchronously (in the same microtask of their dispatch), just like a regular\n  async\n  function is.\n  In other words, now dispatching a sync action works just the same as calling a\n  sync\n  function,\n  and dispatching an async action works just the same as calling an async\n  function.\n\n  ```\n  // Example: The below code will print: \"BEFORE a1 f1 AFTER a2 f2\"  \n  \n  print('BEFORE');\n  dispatch(MyAsyncAction());\n  asyncFunction();\n  print('AFTER');     \n          \n  class MyAsyncAction extends ReduxAction<AppState> {\n     Future<AppState?> reduce() async {\n        print('a1');\n        await microtask;\n        print('a2');\n        return state;\n        }  \n  }\n  \n  Future<void> asyncFunction() async {\n     print('f1');\n     await Future.microtask((){});\n     print('f2');     \n     } \n\n  ```  \n\n  Before version `16.0.0`, the `reduce()` method was called in a later\n  microtask. Please\n  note, the\n  async `reduce()` methods continue to return and apply the state in a later\n  microtask (\n  this did\n  not change).\n\n  The above breaking change is unlikely to affect you in any way, but if you\n  want the old behavior, just add `await microtask;` to the first line of your\n  `reduce()` method.\n\n<br>\n\n* **BREAKING**: When your reducer is async (i.e., returns `Future<AppState>`)\n  you must\n  make sure\n  you **do not return a completed future**, meaning all execution paths of the\n  reducer\n  must pass\n  through at least one `await` keyword. In other words, don't return a Future if\n  you don't\n  need it.\n  If your reducer has no `await`s, you must return `AppState?` instead of\n  `Future<AppState?>`, or\n  add `await microtask;` to the start of your reducer, or return `null`. For\n  example:\n\n  ```dart \n  // These are right:\n  AppState? reduce() { return state; }\n  AppState? reduce() { someFunc(); return state; }\n  Future<AppState?> reduce() async { await someFuture(); return state; }\n  Future<AppState?> reduce() async { await microtask; return state; }\n  Future<AppState?> reduce() async { if (state.someBool) return await calculation(); return null; }\n  \n  // But these are wrong:\n  Future<AppState?> reduce() async { return state; }\n  Future<AppState?> reduce() async { someFunc(); return state; }\n  Future<AppState?> reduce() async { if (state.someBool) return await calculation(); return state; }\n  ```\n\n  If you don't follow this rule, AsyncRedux may seem to work ok, but will\n  eventually misbehave.\n\n  It's generally easy to make sure you are not returning a completed future.\n  In the rare case your reducer function is very complex, and you are unsure\n  that all code paths pass through an `await`, just\n  add `assertUncompletedFuture();` at the very END of your `reduce`\n  method, right before the `return`. If you do that, an error will be shown in\n  the console if the `reduce` method ever returns a completed future.\n\n  If you're an advanced user interested in the details, check the\n  <a href=\"https://github.com/marcglasberg/async_redux/blob/master/test/sync_async_test.dart\">\n  sync/async tests</a>.\n\n<br>\n\n* When the `Event` class was created, Flutter did not have another class with\n  that name.\n  Now there is. For this reason, a typedef now allows you to use `Evt` instead.\n  If you need, you can hide one of them, by importing AsyncRedux like this:\n\n  ```dart\n  import 'package:async_redux/async_redux.dart' hide Event;\n  ```\n  or\n\n  ```dart\n  import 'package:async_redux/async_redux.dart' hide Evt;  \n  ```\n\n## 15.0.0\n\n* Flutter 3.0 support.\n\n## 14.1.4\n\n* `NavigateAction.popUntilRouteName()` can print the routes (for debugging).\n\n## 14.1.2\n\n* Better stacktrace for wrapped errors in actions.\n\n## 14.1.1\n\n* The store persistor can now be paused and resumed, with methods\n  `store.pausePersistor()`,\n  `store.persistAndPausePersistor()` and `store.resumePersistor()`. This may be\n  used\n  together with\n  the app lifecycle, to prevent a persistence process to start when the app is\n  being shut\n  down. For\n  example:\n\n  ```     \n  child: StoreProvider<AppState>(\n  store: store,\n    child: AppLifecycleManager( // Add this widget here to capture lifecycle events.\n      child: MaterialApp( \n  ...     \n  \n  class AppLifecycleManager extends StatefulWidget {\n    final Widget child;\n    const AppLifecycleManager({Key? key, required this.child}) : super(key: key);  \n    _AppLifecycleManagerState createState() => _AppLifecycleManagerState();\n  }\n  \n  class _AppLifecycleManagerState extends State<AppLifecycleManager> with WidgetsBindingObserver {\n  \n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n  }\n  \n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n  \n  void didChangeAppLifecycleState(AppLifecycleState lifecycle) {\n    store.dispatch(ProcessLifecycleChange_Action(lifecycle));\n  }\n  \n  Widget build(BuildContext context) => widget.child;\n  }\n\n  class ProcessLifecycleChangeAction extends ReduxAction<AppState> {\n     final AppLifecycleState lifecycle;\n     ProcessLifecycleChangeAction(this.lifecycle);\n\n     Future<AppState?> reduce() async {\n       if (lifecycle == AppLifecycleState.resumed || lifecycle == AppLifecycleState.inactive) {\n         store.resumePersistor();  \n       } else if (lifecycle == AppLifecycleState.paused || lifecycle == AppLifecycleState.detached) {\n         store.persistAndPausePersistor();\n       } else\n         throw AssertionError(lifecycle);\n\n       return null;\n     }\n   }\n  ```  \n\n* When logging out of the app, you can call `store.deletePersistedState()` to\n  ask the\n  persistor to\n  delete the state from disk.\n\n* **BREAKING**: This is a very minor change, unlikely to affect you. The\n  signature for\n  the `Action.wrapError` method has changed from `Object? wrapError(error)`\n  to `Object? wrapError(Object error)`. If you get an error when you upgrade,\n  you can fix\n  it by\n  changing the method that broke into `Object? wrapError(dynamic error)`.\n\n* **BREAKING**: Context is now nullable for these StoreConnector methods:\n  ```\n  void onInitialBuildCallback(BuildContext? context, Store<St> store, Model viewModel);\n  void onDidChangeCallback(BuildContext? context, Store<St> store, Model viewModel);\n  void onWillChangeCallback(BuildContext? context, Store<St> store, Model previousVm, Model newVm);\n  ```   \n\n## 13.3.1\n\n* Version bump of dependencies.\n\n## 13.2.2\n\n* Version bump of dependencies.\n\n## 13.2.1\n\n* Fixed `MockStore.dispatchAsync()` and `MockStore.dispatchSync()` methods.\n  Note: `dispatchAsync` was later renamed to `dispatchAndWait`.\n\n## 13.2.0\n\n* `delay` parameter for `WaitAction.add()` and `WaitAction.remove()` methods.\n\n## 13.1.0\n\n* Added missing `dispatchSync` and `dispatchAsync` to `StoreTester`.\n  Note: `dispatchAsync` was later renamed to `dispatchAndWait`.\n\n## 13.0.6\n\n* Added missing `dispatchSync` to `VmFactory`.\n\n## 13.0.5\n\n* Sometimes, the store state is such that it's not possible to create a\n  view-model. In\n  those cases,\n  the `fromStore()` method in the `Factory` can now return a `null` view-model.\n  In that\n  case,\n  the `builder()` method in the `StoreConnector` can detect that the view-model\n  is `null`,\n  and then\n  return some widget that does not depend on the view-model. For example:\n\n  ```\n  return StoreConnector<AppState, ViewModel?>(\n    vm: () => Factory(this),\n    builder: (BuildContext context, ViewModel? vm) {\n      return (vm == null)\n        ? Text(\"The user is not logged in\")\n        : MyHomePage(user: vm.user)\n  \n  ...              \n         \n  class Factory extends VmFactory<AppState, MyHomePageConnector, ViewModel> {   \n  ViewModel? fromStore() {\n    return (store.state.user == null)\n        ? null\n        : ViewModel(user: store.state.user)\n  \n  ...\n  \n  class ViewModel extends Vm {\n    final User user;  \n    ViewModel({required this.user}) : super(equals: [user]);\n  ```\n\n## 13.0.4\n\n* `dispatch` can be used to dispatch both sync and async actions. It returns a\n  `FutureOr`.\n  You can\n  await the result or not, as desired.\n\n* `dispatchAsync` can also be used to dispatch both sync and async actions. But\n  it always\n  returns a\n  `Future` (not a `FutureOr`). Use this only when you explicitly need a\n  `Future`, for\n  example, when\n  working with the `RefreshIndicator` widget. Note: `dispatchAsync` was later\n  renamed\n  to `dispatchAndWait`.\n\n* `dispatchSync` allows you to dispatch SYNC actions only. In that case,\n  `dispatchSync(action)` is\n  exactly the same as `dispatch(action)`. However, if your action is ASYNC,\n  `dispatchSync`\n  will\n  throw an error. Use this only when you need to make sure an action is sync (\n  meaning it\n  impacts the\n  store state immediately when it returns). This is not very common. Important:\n  An action\n  is sync if\n  and only if both its `before` and `reduce` methods are sync. If any or both\n  these\n  methods return a\n  Future, then the action is async and will throw an error when used with\n  `dispatchSync`.\n\n* `StoreTester.getConnectorTester` helps test `StoreConnector`s methods, such as\n  `onInit`,\n  `onDispose` and `onWillChange`. For example, suppose you have a\n  `StoreConnector` which\n  dispatches `SomeAction` on its `onInit`. You could test it like this:\n\n  ``` \n  class MyConnector extends StatelessWidget { \n     Widget build(BuildContext context) => StoreConnector<AppState, Vm>(\n        vm: () => _Factory(), \n        onInit: _onInit, \n        builder: (context, vm) { ... } \n     } \n  \n  void _onInit(Store<AppState> store) => store.dispatch(SomeAction()); \n  } \n  \n  var storeTester = StoreTester(...); \n  var connectorTester = storeTester.getConnectorTester(MyConnector()); \n  connectorTester.runOnInit(); \n  var info = await tester.waitUntil(SomeAction);  \n  ```\n  For more information, see section **Testing the StoreConnector** in the\n  README.md file.\n\n* Fix: `UserExceptionDialog` now shows all `UserException`s. It was discarding\n  some of\n  them under\n  some circumstances, in a regression created in version 4.0.4.\n\n* In the `Store` constructor you can now set `maxErrorsQueued` to control the\n  maximum\n  number of\n  errors the `UserExceptionDialog` error-queue can hold. Default is `10`.\n\n* `ConsoleActionObserver` is now provided to print action details to the\n  console.\n\n* `WaitAction.toString()` now returns a better description.\n\n## 12.0.4\n\n* `NavigateAction.toString()` now returns a better description, like\n  `Action NavigateAction.pop()`.\n\n* Fixed `NavigateAction.popUntilRouteName` and\n  `NavigateAction.pushNamedAndRemoveAll` to\n  return the\n  correct `.type`.\n\n* Added section `Dependency Injection` in README.md.\n\n## 12.0.3\n\n* Improved error messages when the reducer returns an invalid type.\n\n* New `StoreTester` methods: `waitUntilAll()` and `waitUntilAllGetLast()`.\n\n* Passing an environment to the store, to help with dependency injection:\n  `Store(environment: ...)`\n\n## 12.0.0\n\n* **BREAKING**: Improved state typing for some `Store` parameters. You will\n  now have to use `Persistor<AppState>` instead of `Persistor`, and `WrapError<AppState>`\n  instead of `WrapError` etc.\n\n* Global `Store(wrapReduce: ...)`. You may now globally wrap the reducer to\n  allow for some\n  pre or\n  post-processing. Note: if the action also have a wrapReduce method, this\n  global wrapper\n  will be\n  called AFTER (it will wrap the action's wrapper which wraps the action's\n  reducer).\n\n* Downgraded dev_dependencies `test: ^1.16.0`\n\n## 11.0.1\n\n* You can now provide callbacks `onOk` and `onCancel` to an `UserException`.\n  This allows\n  you to\n  dispatch actions when the user dismisses the error dialog. When using the\n  default `UserExceptionDialog`: (i) if only `onOk` is provided, it will be\n  called when\n  the dialog\n  is dismissed, no matter how. (ii) If both `onOk` and `onCancel` are provided,\n  then\n  `onOk` will be\n  called only when the OK button is pressed, while `onCancel` will be called\n  when the\n  dialog is\n  dismissed by any other means.\n\n## 11.0.0\n\n* **BREAKING**: The `dispatchFuture` function is not necessary anymore. Just\n  rename it\n  to `dispatch`, since now the `dispatch` function always returns a future, and\n  you can\n  await it or\n  not, as desired.\n\n* **BREAKING**: `ReduxAction.hasFinished()` has been deprecated. It should be\n  renamed to `isFinished`.\n\n* The `dispatch` function now returns an `ActionStatus`. Usually you will\n  discard this\n  info, but you\n  may use it to know if the action completed with no errors. For example,\n  suppose a\n  `SaveAction`\n  looks like this:\n\n  ```                                      \n  class SaveAction extends ReduxAction<AppState> {      \n    Future<AppState> reduce() async {\n\t  bool isSaved = await saveMyInfo(); \n      if (!isSaved) throw UserException(\"Save failed.\");\t \n\t  ...\n    }\n  }\n  ```\n\n  Then, when you save some info, you want to leave the current screen if and\n  only if the\n  save\n  process succeeded:\n\n  ```\n  var status = await dispatch(SaveAction(info));\n  if (status.isFinished) dispatch(NavigateAction.pop()); // Or: Navigator.pop(context) \n  ```              \n\n## 10.0.1\n\n* **BREAKING**: The new `UserExceptionDialog.useLocalContext` parameter now\n  allows the `UserExceptionDialog` to be put in the `builder` parameter of the\n  `MaterialApp` widget. Even if you use this dialog, it is unlikely this will be a\n  breaking change for you. But if it is, and your error dialog now has problems, simply\n  make `useLocalContext: true` to return to the old behavior.\n\n* **BREAKING**: `StoreConnector` parameters `onInitialBuild`, `onDidChange`\n  and `onWillChange` now also get the context and the store. For example, where\n  you previously\n  had `onInitialBuild(vm) {...}` now you have\n  `onInitialBuild(context, store, vm) {...}`.\n\n## 9.0.9\n\n* LocalPersist `saveJson()` and `loadJson()` methods.\n\n## 9.0.8\n\n* FIC and weak-map version bump.\n\n## 9.0.7\n\n* NNBD improvements.\n* FIC version bump.\n\n## 9.0.1\n\n* Downgrade to file: ^6.0.0 to improve compatibility.\n\n## 9.0.0\n\n* Nullsafe.\n\n## 8.0.0\n\n* Uses nullsafe dependencies (it's not yet itself nullsafe).\n\n* **BREAKING**: Cache functions (for memoization) have been renamed and extended.\n\n## 7.0.2\n\n* LocalPersist: Better handling of mock file-systems.\n\n## 7.0.1\n\n* **BREAKING**:\n\n  Now the `vm` parameter in the `StoreConnector` is a function that creates a\n  `VmFactory` (instead\n  of being a `VmFactory` object itself).\n\n  So, to upgrade, you just need to provide this:\n\n  ```\n  vm: () => MyFactory(this),\n  ```\n\n  Instead of this:\n\n  ```                \n  // Deprecated.\n  vm: MyFactory(this), \n  ```\n\n  Now the `StoreConnector` will create a `VmFactory` every time it needs a\n  view-model. The\n  Factory\n  will have access to:\n\n    1) `state` getter: The state the store was holding when the factory and the\n       view-model\n       were\n       created. This state is final inside the factory.\n\n    2) `currentState()` method: The current (most recent) store state. This will\n       return\n       the current\n       state the store holds at the time the method is called.\n\n* New store parameter `immutableCollectionEquality` lets you override the\n  equality used\n  for\n  immutable collections from\n  the <a href=\"https://pub.dev/packages/fast_immutable_collections\">\n  fast_immutable_collections</a> package.\n\n## 6.0.3\n\n* StoreTester.dispatchState().\n\n## 6.0.2\n\n* VmFactory.getAndRemoveFirstError().\n\n## 6.0.1\n\n* `NavigateAction` now closely follows the `Navigator` api:  `push()`,\n  `pop()`, `popAndPushNamed()`, `pushNamed()`, `pushReplacement()`,\n  `pushAndRemoveUntil()`,\n  `replace()`, `replaceRouteBelow()`, `pushReplacementNamed()`,\n  `pushNamedAndRemoveUntil()`,\n  `pushNamedAndRemoveAll()`, `popUntil()`, `removeRoute()`,\n  `removeRouteBelow()`,\n  `popUntilRouteName()` and `popUntilRoute()`.\n\n## 1.0.0\n\n* Initial commit: 2019/Aug05\n"
  },
  {
    "path": "LICENSE",
    "content": "Async_redux Package License (05 Aug 2019):\nhttps://github.com/marcglasberg/async_redux/blob/master/LICENSE\n\nMIT License\n\nCopyright (c) 2019 Marcelo Glasberg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n*******************************************************************************\nThird-Party licenses of software used in AsyncRedux:\n*******************************************************************************\n\nRedux Package License (05 Aug 2019):\nhttps://github.com/johnpryan/redux.dart/blob/master/LICENSE\n\nMIT License\n\nCopyright (c) 2016 John Ryan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n*******************************************************************************\n\nFlutter_redux Package License (05 Aug 2019):\nhttps://github.com/brianegan/flutter_redux/blob/master/LICENSE\n\nThe MIT License (MIT)\nCopyright (c) 2017 Brian Egan\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without restriction,\nincluding without limitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of the Software,\nand to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\nDAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\nOTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n\n*******************************************************************************\n\nEquatable Package License (05 Aug 2019):\nhttps://github.com/felangel/equatable/blob/master/LICENSE\n\nMIT License\n\nCopyright (c) 2018 Felix Angelov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n*******************************************************************************\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://asyncredux.com/img/platipus_FlutterReact.jpg\">\n\n[![Pub Version](https://img.shields.io/pub/v/async_redux?style=flat-square&logo=dart)](https://pub.dev/packages/async_redux)\n[![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter)\n[![GitHub stars](https://img.shields.io/github/stars/marcglasberg/async_redux?style=social)](https://github.com/marcglasberg/async_redux)\n![Code Climate issues](https://img.shields.io/github/issues/marcglasberg/async_redux?style=flat-square)\n![GitHub closed issues](https://img.shields.io/github/issues-closed/marcglasberg/async_redux?style=flat-square)\n![GitHub contributors](https://img.shields.io/github/contributors/marcglasberg/async_redux?style=flat-square)\n![GitHub repo size](https://img.shields.io/github/repo-size/marcglasberg/async_redux?style=flat-square)\n![GitHub forks](https://img.shields.io/github/forks/marcglasberg/async_redux?style=flat-square)\n![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)\n[![Developed by Marcelo Glasberg](https://img.shields.io/badge/Developed%20by%20Marcelo%20Glasberg-blue.svg)](https://glasberg.dev/)\n[![Glasberg.dev on pub.dev](https://img.shields.io/pub/publisher/async_redux.svg)](https://pub.dev/publishers/glasberg.dev/packages)\n[![Platforms](https://badgen.net/pub/flutter-platform/async_redux)](https://pub.dev/packages/async_redux)\n\n#### Created by\n\n**[Marcelo Glasberg](https://glasberg.dev)**\n| [LinkedIn](https://linkedin.com/in/marcglasberg/) | [GitHub](https://github.com/marcglasberg/)\n\n#### Contributors\n\n<a href=\"https://github.com/marcglasberg/async_redux/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=marcglasberg/async_redux&columns=9\"/>\n</a>\n\n#### Sponsor\n\n[![](./example/SponsoredByMyTextAi.png)](https://mytext.ai)\n\n# AsyncRedux | *state management*\n\n* Simple to learn, easy to use\n* Handles complex applications with millions of users\n* Testable\n\nYou'll be able to create apps much faster,\nand other people on your team will easily understand and modify your code.\n\n## What is it?\n\nAn optimized reimagined version of Redux.\nA mature solution, battle-tested in hundreds of real-world applications.\nCreated by [Marcelo Glasberg](https://github.com/marcglasberg)\n(see [all my packages](https://pub.dev/publishers/glasberg.dev/packages)).\n\n> There is also a version for React\n> [called Kiss State](https://kissforreact.org/)\n\n> If you use Bloc, check [Bloc Superpowers](https://pub.dev/packages/bloc_superpowers)\n\n> Optionally use AsyncRedux with [Provider](https://pub.dev/packages/provider_for_redux)\n> or [Flutter Hooks](https://pub.dev/packages/flutter_hooks_async_redux)\n\n# Documentation\n\n### Complete docs → **https://asyncredux.com**\n\n### Claude Code Skills → [Copy from the repo on GitHub](https://github.com/marcglasberg/async_redux/tree/main/.claude/skills)\n\n&nbsp;\n\n_Below is a quick overview._\n\n***\n\n# Store and state\n\nThe **store** holds all the application **state**.\n\n```dart\n// The application state\nclass AppState {\n  final String name;\n  final int age;\n  AppState(this.name, this.age);\n}\n\n// Create the store with the initial state\nvar store = Store<AppState>(\n   initialState: AppState('Mary', 25)\n);\n```\n\n&nbsp;\n\nTo use the store, add it in a `StoreProvider` at the top of your widget tree.\n\n```dart\nWidget build(context) {\n  return StoreProvider<AppState>(\n    store: store,\n    child: MaterialApp( ... ), \n    );                      \n}\n```\n\n&nbsp;\n\n# Widgets use the state\n\nUsing `context.state`, your widgets rebuild when the state changes.\n\n```dart\nclass MyWidget extends StatelessWidget {\n\n  Widget build(context)\n    => Text('${context.state.name} has ${context.state.age} years old');\n}\n```\n\nOr use `context.select()` to get only the parts of the state you need.\n\n```dart\nWidget build(context) {\n\n  var state = context.select((st) => (\n     name: st.user.name, \n     age: st.user.age),\n  );\n  \n  return Text('${state.name} has ${state.age} years old');\n}\n```\n\nThis also works:\n\n```dart\nWidget build(context) {\n  var name = context.select((st) => st.name);\n  var age = context.select((st) => st.age);\n  \n  return Text('$name has $age years old');\n}\n```\n\n&nbsp;\n\n# Actions change the state\n\nThe application state is **immutable**,\nso the only way to change it is by **dispatching** an **action**.\n\n```dart\n// Dispatch an action\ndispatch(Increment());\n\n// Dispatch multiple actions\ndispatchAll([Increment(), LoadText()]);\n\n// Dispatch an action and wait for it to finish\nawait dispatchAndWait(Increment());\n\n// Dispatch multiple actions and wait for them to finish\nawait dispatchAndWaitAll([Increment(), LoadText()]);\n```\n\n&nbsp;\n\nAn **action** is a class with a name that describes what it does, like\n`Increment`, `LoadText`, or `BuyStock`.\n\nIt must include a method called `reduce`. This \"reducer\" has access to the\ncurrent state, and must return a new one to replace it.\n\n```dart\nclass Increment extends Action {\n\n  // The reducer has access to the current state\n  AppState reduce() \n    => AppState(state.name, state.age + 1); // Returns new state\n}\n```\n\n&nbsp;\n\n# Widgets can dispatch actions\n\nIn your widgets, use `context.dispatch` to dispatch actions.\n\n```dart\nclass MyWidget extends StatelessWidget {\n \n  Widget build(context) { \n    return ElevatedButton(\n      onPressed: () => context.dispatch(Increment());\n    }     \n}\n```\n\n&nbsp;\n\n# Actions can do asynchronous work\n\nActions may download information from the internet, or do any other async work.\n\n```dart\nclass LoadText extends Action {\n\n  // This reducer returns a Future\n  Future<AppState> reduce() async {\n\n    // Download something from the internet\n    var response = await http.get('https://dummyjson.com/todos/1');\n    var newName = state.response.body;\n\n    // Change the state with the downloaded information\n    return AppState(newName, state.age);\n  }\n}\n```\n\n&nbsp;\n\n# Actions can throw errors\n\nIf something bad happens, you can simply **throw an error**. In this case, the\nstate will not change. Errors are caught globally and can be handled in a\ncentral place, later.\n\nIn special, if you throw a `UserException`, which is a type provided by Async\nRedux, a dialog (or other UI) will open automatically, showing the error message\nto the user.\n\n```dart\nclass LoadText extends Action {\n    \n  Future<String> reduce() async {  \n    var response = await http.get('https://dummyjson.com/todos/1');\n\n    if (response.statusCode == 200) return response.body;\n    else throw UserException('Failed to load');         \n  }\n}\n```\n\n&nbsp;\n\nTo show a spinner while an asynchronous action is running, use\n`isWaiting(action)`.\n\nTo show an error message inside the widget, use `isFailed(action)`.\n\n```dart\nclass MyWidget extends StatelessWidget {\n\n  Widget build(context) {\n    \n    if (context.isWaiting(LoadText)) return CircularProgressIndicator();\n    if (context.isFailed(LoadText)) return Text('Loading failed...');\n    return Text(context.state);\n  }\n}\n```\n\n&nbsp;\n\n# Actions can dispatch other actions\n\nYou can use `dispatchAndWait` to dispatch an action and wait for it to finish.\n\n```dart\nclass LoadTextAndIncrement extends Action {\n\n  Future<AppState> reduce() async {    \n    \n    // Dispatch and wait for the action to finish\n    await dispatchAndWait(LoadText());\n    \n    // Only then, increment the state\n    return state.copy(count: state.count + 1);\n  }\n}\n```\n\n&nbsp;\n\nYou can also dispatch actions in **parallel** and wait for them to finish:\n\n```dart\nclass BuyAndSell extends Action {\n\n  Future<AppState> reduce() async {\n  \n    // Dispatch and wait for both actions to finish\n    await dispatchAndWaitAll([\n      BuyAction('IBM'), \n      SellAction('TSLA')\n    ]);\n    \n    return state.copy(message: 'New cash balance is ${state.cash}');\n  }\n}\n```\n\n&nbsp;\n\nYou can also use `waitCondition` to wait until the `state` changes in a certain\nway:\n\n```dart\nclass SellStockForPrice extends Action {\n  final String stock;\n  final double limitPrice;\n  SellStockForPrice(this.stock, this.limitPrice);\n\n  Future<AppState?> reduce() async {  \n  \n    // Wait until the stock price is higher than the limit price\n    await waitCondition(\n      (st) => st.stocks[stock].price >= limitPrice\n    );\n      \n    // Only then, post the sell order to the backend\n    var amount = await postSellOrder(stock);    \n    \n    return state.copy(\n      stocks: state.stocks.setAmount(stock, amount),\n    ); \n}\n```\n\n&nbsp;\n\n# Add mixins to your actions\n\nYou can use **mixins** to accomplish common tasks.\n\n## Check for Internet connectivity\n\nMixin `CheckInternet` ensures actions only run with internet,\notherwise an **error dialog** prompts users to check their connection:\n\n```dart\nclass LoadText extends Action with CheckInternet {\n      \n   Future<String> reduce() async {\n      var response = await http.get('https://dummyjson.com/todos/1');\n      ...      \n   }\n}   \n```\n\n&nbsp;\n\nMixin `NoDialog` can be added to `CheckInternet` so that no dialog is opened.\nInstead, you can display some information in your widgets:\n\n```dart\nclass LoadText extends Action with CheckInternet, NoDialog { \n  ... \n  }\n\nclass MyWidget extends StatelessWidget {\n  Widget build(context) {     \n     if (context.isFailed(LoadText)) Text('No Internet connection');\n  }\n}   \n```\n\n&nbsp;\n\nMixin `AbortWhenNoInternet` aborts the action silently (without showing any\ndialogs) if there is no internet connection.\n\n&nbsp;\n\n## NonReentrant\n\nMixin `NonReentrant` prevents an action from being dispatched while it's\nalready running.\n\n```dart\nclass LoadText extends Action with NonReentrant {\n   ...\n}\n```\n\n&nbsp;\n\n## Retry\n\nMixin `Retry` retries the action a few times with exponential backoff,\nif it fails. Add `UnlimitedRetries` to retry indefinitely:\n\n```dart\nclass LoadText extends Action with Retry, UnlimitedRetries {\n   ...\n}\n```\n\n&nbsp;\n\n## UnlimitedRetryCheckInternet\n\nMixin `UnlimitedRetryCheckInternet` checks if there is internet when you run\nsome action that needs it. If there is no internet, the action will abort\nsilently and then retried unlimited times, until there is internet. It will also\nretry if there is internet but the action failed.\n\n```dart\nclass LoadText extends Action with UnlimitedRetryCheckInternet {\n   ...\n}\n```\n\n&nbsp;\n\n## Fresh\n\nMixin `Fresh` prevents a dispatched action from reloading the same information\nwhile it is still up to date. The first dispatch always runs and loads the data.\nWhile the data is _fresh_, later dispatches do nothing. When the fresh period\nends, the data becomes _stale_ and the action may run again.\n\n```dart\nclass LoadPrices extends Action with Fresh {  \n  \n  final int freshFor = 5000; // Milliseconds\n\n  Future<AppState> reduce() async {      \n    var result = await loadJson('https://example.com/prices');              \n    return state.copy(prices: result);\n  }\n}\n```\n\n&nbsp;\n\n## Throttle\n\nMixin Throttle limits how often an action can run, acting as a simple rate\nlimit. The first dispatch runs right away.\nAny later dispatches during the throttle period are ignored.\nOnce the period ends, the next dispatch is allowed to run again.\n\n```dart\nclass RefreshFeed extends Action with Throttle {\n  final int throttle = 3000; // Milliseconds\n\n  Future<AppState> reduce() async {\n    final items = await loadJson('https://example.com/feed');\n    return state.copy(feedItems: items);\n  }\n}\n```\n\n&nbsp;\n\n## Debounce\n\nMixin `Debounce` limits how often an action occurs in response to rapid inputs.\nFor example, when a user types in a search bar, debouncing ensures that not\nevery keystroke triggers a server request. Instead, it waits until the user\npauses typing before acting.\n\n```dart\nclass SearchText extends Action with Debounce {\n  final String searchTerm;\n  SearchText(this.searchTerm);\n  \n  final int debounce = 300; // Milliseconds\n\n  Future<AppState> reduce() async {\n      \n    var response = await http.get(\n      Uri.parse('https://example.com/?q=' + encoded(searchTerm))\n    );\n        \n    return state.copy(searchResult: response.body);\n  }\n}\n```\n\n&nbsp;\n\n## Polling\n\nMixin `Polling` runs an action periodically.\nUse it to keep data fresh by repeatedly fetching from a server.\n\n```dart\nclass GetStockPrice extends Action with Polling {\n  final Poll poll;\n  GetStockPrice([this.poll = Poll.once]);\n\n  Duration get pollInterval => const Duration(minutes: 1);\n\n  Action createPollingAction() => GetStockPrice();\n\n  Future<AppState> reduce() async {\n    var result = await loadJson('https://example.com/prices');\n    return state.copy(prices: result);\n  }\n}\n```\n\nThe `poll` parameter controls the behavior.\n\n```dart\n// Run only once\ndispatch(GetStockPrice());\n\n// Start polling\ndispatch(GetStockPrice(Poll.start));\n\n// Stop polling\ndispatch(GetStockPrice(Poll.stop));\n```\n\n&nbsp;\n\n## OptimisticCommand\n\nMixin `OptimisticCommand` helps you provide instant feedback on **blocking**\nactions that save information to the server. You immediately apply state changes\nas if they were already successful. The UI prevents the user from making other\nchanges until the server confirms the update. If the update fails, the change\nis rolled back.\n\n```dart\nclass SaveTodo extends Action with OptimisticCommand {\n  final Todo todo;\n  SaveTodo(this.todo); \n   \n  async reduce() { ... } \n}\n```\n\n&nbsp;\n\n## OptimisticSync\n\nMixin `OptimisticSync` helps you provide instant feedback on **non-blocking**\nactions that save information to the server. The UI does **not** prevent the\nuser from making other changes. Changes are applied locally right away,\nwhile the mixin synchronizes those changes with the server in the background.\n\n```dart\nclass SaveLike extends Action with OptimisticSync {\n  final bool isLiked;\n  SaveLike(this.isLiked); \n  \n  async reduce() { ... } \n}\n```\n\n&nbsp;\n\n## OptimisticSyncWithPush\n\nMixin `OptimisticSyncWithPush` is similar to `OptimisticSync`, but it also\nassumes that the app listens to the server, for example via WebSockets.\nIt supports server versioning and multiple clients updating the same data\nconcurrently.\n\n```dart\nclass SaveLike extends Action with OptimisticSyncWithPush {\n  final bool isLiked;\n  SaveLike(this.isLiked); \n  \n  async reduce() { ... } \n}\n```\n\n&nbsp;\n\n# Events\n\nYou can use `Evt()` to create events that perform one-time operations,\nto work with widgets like **TextField** or **ListView** that manage their\nown internal state.\n\n```dart\n// Action that changes the text of a TextField\nclass ChangeText extends Action {\n  final String newText;\n  ChangeText(this.newText);    \n  AppState reduce() => state.copy(changeText: Evt(newText));\n  }\n}\n\n// Action that scrolls a ListView to the top\nclass ScrollToTop extends Action {\n  AppState reduce() => state.copy(scroll: Evt(0));\n  }\n}\n```\n\nThen, consume the events in your widgets:\n\n```dart\nWidget build(context) {\n\n  var clearText = context.event((st) => st.clearTextEvt);\n  if (clearText) controller.clear();\n\n  var newText = context.event((st) => st.changeTextEvt);\n  if (newText != null) controller.text = newText;\n  \n  return ...\n}    \n```\n\n&nbsp;\n\n# Persist the state\n\nYou can add a `persistor` to save the state to the local device disk.\n\n```dart\nvar store = Store<AppState>(\n  persistor: MyPersistor(),  \n);  \n```\n\n&nbsp;\n\n# Testing your app is easy\n\nJust dispatch actions and wait for them to finish.\nThen, verify the new state or check if some error was thrown.\n\n```dart\nclass AppState {  \n  List<String> items;    \n  int selectedItem;\n}\n\ntest('Selecting an item', () async {   \n\n    var store = Store<AppState>(\n      initialState: AppState(        \n        items: ['A', 'B', 'C']\n        selectedItem: -1, // No item selected\n      ));\n    \n    // Should select item 2                \n    await store.dispatchAndWait(SelectItem(2));    \n    expect(store.state.selectedItem, 'B');\n    \n    // Fail to select item 42\n    var status = await store.dispatchAndWait(SelectItem(42));    \n    expect(status.originalError, isA<>(UserException));\n});\n```\n\n&nbsp;\n\n# Advanced setup\n\nIf you are the Team Lead, you set up the app's infrastructure in a central\nplace, and allow your developers to concentrate solely on the business logic.\n\nYou can add a `stateObserver` to collect app metrics, an `errorObserver` to log\nerrors, an `actionObserver` to print information to the console during\ndevelopment, and a `globalWrapError` to catch all errors.\n\n```dart\nvar store = Store<String>(    \n  stateObserver: [MyStateObserver()],\n  errorObserver: [MyErrorObserver()],\n  actionObservers: [MyActionObserver()],\n  globalWrapError: MyGlobalWrapError(),\n```\n\n&nbsp;\n\nFor example, the following `globalWrapError` handles `PlatformException` errors\nthrown by Firebase. It converts them into `UserException` errors, which are\nbuilt-in types that automatically show a message to the user in an error dialog:\n\n```dart\nObject? wrap(error, stackTrace, action) =>\n  (error is PlatformException)\n    ? UserException('Error connecting to Firebase')\n    : error;\n}  \n```\n\n&nbsp;\n\n# Advanced action configuration\n\nThe Team Lead may create a base action class that all actions will extend, and\nadd some common functionality to it. For example, getter shortcuts to important\nparts of the state, and selectors to help find information.\n\n```dart\nclass AppState {  \n  List<Item> items;    \n  int selectedItem;\n}\n\nclass Action extends ReduxAction<AppState> {\n\n  // Getter shortcuts   \n  List<Item> get items => state.items;\n  Item get selectedItem => state.selectedItem;\n  \n  // Selectors \n  Item? findById(int id) => items.firstWhereOrNull((item) => item.id == id);\n  Item? searchByText(String text) => items.firstWhereOrNull((item) => item.text.contains(text));\n  int get selectedIndex => items.indexOf(selectedItem);     \n}\n```\n\n&nbsp;\n\nNow, all actions can use them to access the state in their reducers:\n\n```dart\nclass SelectItem extends Action {\n  final int id;\n  SelectItem(this.id);\n    \n  AppState reduce() {\n    Item? item = findById(id);\n    if (item == null) throw UserException('Item not found');\n    return state.copy(selected: item);\n  }    \n}\n```         \n\n&nbsp;\n\n# Claude Code Skills\n\nThis package includes **Skills** that help you use `async_redux` with\nClaude Code and other AI agents.\n\nTo use it, you have to copy the skills\nfrom [this repository](https://github.com/marcglasberg/async_redux/tree/master/.claude/skills)\nto your project.\n[Learn more](https://asyncredux.com/flutter/claude-code-skills).\n\n---\n\n### Complete docs → **https://asyncredux.com**\n\n***\n\n## Created by Marcelo Glasberg\n\n<a href=\"https://glasberg.dev\">_glasberg.dev_</a>\n<br>\n<a href=\"https://github.com/marcglasberg\">_github.com/marcglasberg_</a>\n<br>\n<a href=\"https://www.linkedin.com/in/marcglasberg/\">\n_linkedin.com/in/marcglasberg/_</a>\n<br>\n<a href=\"https://twitter.com/glasbergmarcelo\">_twitter.com/glasbergmarcelo_</a>\n<br>\n<a href=\"https://stackoverflow.com/users/3411681/marcg\">\n_stackoverflow.com/users/3411681/marcg_</a>\n<br>\n<a href=\"https://medium.com/@marcglasberg\">_medium.com/@marcglasberg_</a>\n<br>\n\n*I wrote Google's official Flutter documentation on layout rules*:\n\n* <a href=\"https://flutter.dev/docs/development/ui/layout/constraints\">\n  Understanding\n  constraints</a>\n\n*Flutter packages I've authored:*\n\n* <a href=\"https://pub.dev/packages/bloc_superpowers\">bloc_superpowers</a>\n* <a href=\"https://pub.dev/packages/i18n_extension\">i18n_extension</a>\n* <a href=\"https://pub.dev/packages/async_redux\">async_redux</a>\n* <a href=\"https://pub.dev/packages/provider_for_redux\">provider_for_redux</a>\n* <a href=\"https://pub.dev/packages/align_positioned\">align_positioned</a>\n* <a href=\"https://pub.dev/packages/network_to_file_image\">\n  network_to_file_image</a>\n* <a href=\"https://pub.dev/packages/image_pixels\">image_pixels</a>\n* <a href=\"https://pub.dev/packages/matrix4_transform\">matrix4_transform</a>\n* <a href=\"https://pub.dev/packages/back_button_interceptor\">\n  back_button_interceptor</a>\n* <a href=\"https://pub.dev/packages/indexed_list_view\">indexed_list_view</a>\n* <a href=\"https://pub.dev/packages/animated_size_and_fade\">\n  animated_size_and_fade</a>\n* <a href=\"https://pub.dev/packages/assorted_layout_widgets\">\n  assorted_layout_widgets</a>\n* <a href=\"https://pub.dev/packages/weak_map\">weak_map</a>\n* <a href=\"https://pub.dev/packages/themed\">themed</a>\n* <a href=\"https://pub.dev/packages/bdd_framework\">bdd_framework</a>\n* <a href=\"https://pub.dev/packages/tiktoken_tokenizer_gpt4o_o1\">\n  tiktoken_tokenizer_gpt4o_o1</a>\n\n*The JavaScript/TypeScript packages I've authored:*\n\n* [Kiss State, for React](https://kissforreact.org/) (similar to AsyncRedux,\n  but for React)\n* [Easy BDD Tool, for Jest](https://www.npmjs.com/package/easy-bdd-tool-jest)\n\n*My Medium Articles:*\n\n* <a href=\"https://medium.com/@marcglasberg/bloc-superpowers-2ef9262ed962\">\n  Bloc Superpowers: Flutter/Dart package for enhancing your Cubits</a>  \n* <a href=\"https://medium.com/flutter-community/https-medium-com-marcglasberg-async-redux-33ac5e27d5f6\">\n  AsyncRedux: Flutter’s non-boilerplate version of Redux</a> \n  (versions: <a href=\"https://medium.com/flutterando/async-redux-pt-brasil-e783ceb13c43\">\n  Português</a>)\n* <a href=\"https://medium.com/flutter-community/i18n-extension-flutter-b966f4c65df9\">\n  i18n_extension</a> \n  (versions: <a href=\"https://medium.com/flutterando/qual-a-forma-f%C3%A1cil-de-traduzir-seu-app-flutter-para-outros-idiomas-ab5178cf0336\">\n  Português</a>)\n* <a href=\"https://medium.com/flutter-community/flutter-the-advanced-layout-rule-even-beginners-must-know-edc9516d1a2\">\n  Flutter: The Advanced Layout Rule Even Beginners Must Know</a> \n  (versions: <a href=\"https://habr.com/ru/post/500210/\">русский</a>)\n* <a href=\"https://medium.com/flutter-community/the-new-way-to-create-themes-in-your-flutter-app-7fdfc4f3df5f\">\n  The New Way to create Themes in your Flutter App</a> \n"
  },
  {
    "path": "analysis_options.yaml",
    "content": "# Specify analysis options.\n#\n# Until there are meta linter rules, each desired lint must be explicitly enabled.\n# See: https://github.com/dart-lang/linter/issues/288\n#\n# For a list of lints, see: http://dart-lang.github.io/linter/lints/\n# See the configuration guide for more\n# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer\n#\n# There are four similar analysis options files in the flutter repos:\n#   - analysis_options.yaml (this file)\n#   - packages/flutter/lib/analysis_options_user.yaml\n#   - https://github.com/flutter/plugins/blob/master/analysis_options.yaml\n#   - https://github.com/flutter/engine/blob/master/analysis_options.yaml\n#\n# This file contains the analysis options used by Flutter tools, such as IntelliJ,\n# Android Studio, and the `flutter analyze` command.\n#\n# The flutter/plugins repo contains a copy of this file, which should be kept\n# in sync with this file.\n\nanalyzer:\n  #  strong-mode:\n  #    implicit-casts: false\n  #    implicit-dynamic: false\n  errors:\n    # treat missing required parameters as a warning (not a hint)\n    missing_required_param: warning\n    # treat missing returns as a warning (not a hint)\n    missing_return: error\n    # allow having TODOs in the code\n    todo: ignore\n  exclude:\n    - 'bin/cache/**'\n    - 'lib/src/http/**'\n\n# https://github.com/dart-lang/linter/blob/master/example/all.yaml\nlinter:\n  rules:\n    - always_declare_return_types\n    - annotate_overrides\n    - avoid_empty_else\n    - avoid_field_initializers_in_const_classes\n    # - avoid_function_literals_in_foreach_calls\n    - avoid_init_to_null\n    - avoid_null_checks_in_equality_operators\n    - avoid_relative_lib_imports\n    - avoid_renaming_method_parameters\n    - avoid_return_types_on_setters\n    - avoid_slow_async_io\n    - await_only_futures\n    # - camel_case_types\n    - cancel_subscriptions\n    - control_flow_in_finally\n    - directives_ordering\n    - empty_catches\n    - empty_constructor_bodies\n    - empty_statements\n    - hash_and_equals\n    - implementation_imports\n    - collection_methods_unrelated_type\n    - library_names\n    - library_prefixes\n    - no_duplicate_case_values\n    - overridden_fields\n    - package_names\n    - package_prefixed_library_names\n    - prefer_adjacent_string_concatenation\n    - prefer_asserts_in_initializer_lists\n    - prefer_collection_literals\n    - prefer_conditional_assignment\n    - prefer_const_constructors\n    # - prefer_const_constructors_in_immutables\n    - prefer_const_declarations\n    - prefer_contains\n    - prefer_final_fields\n    # - prefer_foreach\n    - prefer_generic_function_type_aliases\n    - prefer_initializing_formals\n    - prefer_is_empty\n    - prefer_is_not_empty\n    - prefer_typing_uninitialized_variables\n    - recursive_getters\n    - slash_for_doc_comments\n    - sort_unnamed_constructors_first\n    - test_types_in_equals\n    - throw_in_finally\n    - type_init_formals\n    - unnecessary_brace_in_string_interps\n    - unnecessary_getters_setters\n    - unnecessary_null_aware_assignments\n    - unnecessary_null_in_if_null_operators\n    - unnecessary_overrides\n    - unnecessary_this\n    - unrelated_type_equality_checks\n    - use_rethrow_when_possible\n    - valid_regexps\n  # - unnecessary_new\n  # - unnecessary_const\n  # - non_constant_identifier_names\n  # - always_put_control_body_on_new_line\n  # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219\n  # - always_specify_types\n  # - avoid_annotating_with_dynamic # conflicts with always_specify_types\n  # - avoid_as\n  # - avoid_bool_literals_in_conditional_expressions # not yet tested\n  # - avoid_catches_without_on_clauses # we do this commonly\n  # - avoid_catching_errors # we do this commonly\n  # - avoid_classes_with_only_static_members\n  # - avoid_double_and_int_checks # only useful when targeting JS runtime\n  # - avoid_js_rounded_ints # only useful when targeting JS runtime\n  # - avoid_positional_boolean_parameters # not yet tested\n  # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356)\n  # - avoid_returning_null # we do this commonly\n  # - avoid_returning_this # https://github.com/dart-lang/linter/issues/842\n  # - avoid_setters_without_getters # not yet tested\n  # - avoid_single_cascade_in_expression_statements # not yet tested\n  # - avoid_types_as_parameter_names # https://github.com/dart-lang/linter/pull/954/files\n  # - avoid_types_on_closure_parameters # conflicts with always_specify_types\n  # - avoid_unused_constructor_parameters # https://github.com/dart-lang/linter/pull/847\n  # - cascade_invocations # not yet tested\n  # - close_sinks # https://github.com/flutter/flutter/issues/5789\n  # - comment_references # blocked on https://github.com/dart-lang/dartdoc/issues/1153\n  # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204\n  # - invariant_booleans # https://github.com/flutter/flutter/issues/5790\n  # - join_return_with_assignment # not yet tested\n  # - literal_only_boolean_expressions # https://github.com/flutter/flutter/issues/5791\n  # - no_adjacent_strings_in_list\n  # - omit_local_variable_types # opposite of always_specify_types\n  # - one_member_abstracts # too many false positives\n  # - only_throw_errors # https://github.com/flutter/flutter/issues/5792\n  # - parameter_assignments # we do this commonly\n  # - prefer_const_literals_to_create_immutables\n  # - prefer_constructors_over_static_methods # not yet tested\n  # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods\n  # - prefer_final_locals\n  # - prefer_function_declarations_over_variables # not yet tested\n  # - prefer_interpolation_to_compose_strings # not yet tested\n  # - prefer_iterable_whereType # https://github.com/dart-lang/sdk/issues/32463\n  # - prefer_single_quotes\n  # - sort_constructors_first\n  # - type_annotate_public_apis # subset of always_specify_types\n  # - unawaited_futures # https://github.com/flutter/flutter/issues/5793\n  # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498\n  # - unnecessary_parenthesis\n  # - unnecessary_statements # not yet tested\n  # - use_setters_to_change_properties # not yet tested\n  # - use_string_buffers # https://github.com/dart-lang/linter/pull/664\n  # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review\n  # - void_checks # not yet tested\n"
  },
  {
    "path": "context_select_patterns.md",
    "content": "# Extension Patterns for AsyncRedux `getSelect`\n\nThis guide demonstrates different extension patterns you can use with\nAsyncRedux's `context.select()` method to create clean, type-safe selectors in\nyour Flutter apps.\n\n## Table of Contents\n\n- [Pattern 1: Basic Extension (Recommended Minimum)](#pattern-1-basic-extension-recommended-minimum)\n- [Pattern 2: Type-Specific Selectors](#pattern-2-type-specific-selectors-for-better-intellisense)\n- [Pattern 3: Domain-Specific Selectors](#pattern-3-domain-specific-selectors-for-complex-apps)\n- [Pattern 4: Combined Selectors for Complex State](#pattern-4-combined-selectors-for-complex-state)\n- [Pattern 5: Nullable State Handling](#pattern-5-nullable-state-handling)\n- [Recommendations](#recommendations)\n\n## Example App State\n\nAll patterns below assume the following app state structure:\n\n```dart\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Your app state\nclass AppState {\n  final User user;\n  final List<Product> products;\n  final Cart cart;\n  final Settings settings;\n\n  AppState({\n    required this.user,\n    required this.products,\n    required this.cart,\n    required this.settings,\n  });\n}\n\nclass User {\n  final String name;\n  final int age;\n  final bool isPremium;\n\n  User({required this.name, required this.age, required this.isPremium});\n}\n\nclass Product {\n  final String id;\n  final String name;\n  final double price;\n\n  Product({required this.id, required this.name, required this.price});\n}\n\nclass Cart {\n  final List<Product> items;\n\n  Cart({required this.items});\n}\n\nclass Settings {\n  final bool darkMode;\n  final String language;\n\n  Settings({required this.darkMode, required this.language});\n}\n```\n\n---\n\n## Pattern 1: Basic Extension (Recommended Minimum)\n\nThis is the **recommended** starting point for most apps. It provides a clean,\nsimple API with full type inference.\n\n### Extension Definition\n\n```dart\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n  AppState read() => getRead<AppState>();\n  R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n  R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);\n}\n```\n\n### Usage Example\n\n```dart\nclass BasicExample extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    // Clean and simple - types are inferred!\n    final userName = context.select((st) => st.user.name);\n    final userAge = context.select((st) => st.user.age);\n    final isPremium = context.select((st) => st.user.isPremium);\n\n    return Column(\n      children: [\n        Text('Name: $userName'),\n        Text('Age: $userAge'),\n        Text('Premium: $isPremium'),\n      ],\n    );\n  }\n}\n```\n\n### Benefits\n\n- Simple and clean API\n- Full type inference - no need to specify types repeatedly\n- Minimal boilerplate\n- Access to full state via `context.state` when needed\n\n---\n\n## Pattern 2: Type-Specific Selectors (For Better IntelliSense)\n\nAdd type-specific methods for common types to get better IDE autocomplete and\ntype safety.\n\n### Extension Definition\n\n```dart\nextension TypedContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  R _select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  // Type-specific methods for common types\n  String selectString(String Function(AppState state) selector) =>\n      _select(selector);\n\n  int selectInt(int Function(AppState state) selector) => _select(selector);\n\n  bool selectBool(bool Function(AppState state) selector) => _select(selector);\n\n  double selectDouble(double Function(AppState state) selector) =>\n      _select(selector);\n\n  List<T> selectList<T>(List<T> Function(AppState state) selector) =>\n      _select(selector);\n\n  Map<K, V> selectMap<K, V>(Map<K, V> Function(AppState state) selector) =>\n      _select(selector);\n}\n```\n\n### Usage Example\n\n```dart\nclass TypedExample extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    // Explicit type methods can help with IDE autocomplete\n    final userName = context.selectString((state) => state.user.name);\n    final userAge = context.selectInt((state) => state.user.age);\n    final isPremium = context.selectBool((state) => state.user.isPremium);\n    final prices = context.selectList<double>(\n      (state) => state.products.map((p) => p.price).toList(),\n    );\n\n    return Column(\n      children: [\n        Text('Name: $userName'),\n        Text('Age: $userAge'),\n        Text('Premium: $isPremium'),\n        Text('Prices: ${prices.join(', ')}'),\n      ],\n    );\n  }\n}\n```\n\n### Benefits\n\n- Better IDE autocomplete\n- Explicit type declarations can help with complex nested types\n- Still maintains type safety\n\n---\n\n## Pattern 3: Domain-Specific Selectors (For Complex Apps)\n\nCreate domain-specific getters for commonly accessed data. This is ideal for\nlarge apps with many screens that repeatedly access the same state slices.\n\n### Extension Definition\n\n```dart\nextension DomainContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  R _select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  // User-specific selectors\n  User get user => _select((state) => state.user);\n\n  String get userName => _select((state) => state.user.name);\n\n  int get userAge => _select((state) => state.user.age);\n\n  bool get isPremiumUser => _select((state) => state.user.isPremium);\n\n  // Cart-specific selectors\n  List<Product> get cartItems => _select((state) => state.cart.items);\n\n  int get cartItemCount => _select((state) => state.cart.items.length);\n\n  double get cartTotal => _select(\n        (state) => state.cart.items.fold(0.0, (sum, item) => sum + item.price),\n      );\n\n  // Settings-specific selectors\n  bool get isDarkMode => _select((state) => state.settings.darkMode);\n\n  String get appLanguage => _select((state) => state.settings.language);\n\n  // Computed selectors\n  bool get hasItemsInCart => _select((state) => state.cart.items.isNotEmpty);\n\n  bool get isEligibleForFreeShipping => _select(\n        (state) =>\n            state.cart.items.fold(0.0, (sum, item) => sum + item.price) > 50,\n      );\n}\n```\n\n### Usage Example\n\n```dart\nclass DomainExample extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    // Super clean - like accessing properties!\n    return Column(\n      children: [\n        Text('User: ${context.userName}'),\n        Text('Age: ${context.userAge}'),\n        Text('Premium: ${context.isPremiumUser}'),\n        Text('Cart Items: ${context.cartItemCount}'),\n        Text('Cart Total: \\$${context.cartTotal}'),\n        Text('Dark Mode: ${context.isDarkMode}'),\n        if (context.hasItemsInCart)\n          Text('Free Shipping: ${context.isEligibleForFreeShipping}'),\n      ],\n    );\n  }\n}\n```\n\n### Benefits\n\n- Extremely clean usage - reads like natural properties\n- Encapsulates complex selector logic\n- Great for large apps with repeated access patterns\n- Centralizes state access logic\n\n---\n\n## Pattern 4: Combined Selectors for Complex State\n\nUse records or view models to select multiple related values at once, reducing\nthe number of selector calls.\n\n### Extension Definition\n\n```dart\nextension CombinedContextExtension on BuildContext {\n  R _select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  // Select multiple related values at once using records\n  ({String name, int age, bool isPremium}) get userInfo => _select(\n        (state) => (\n          name: state.user.name,\n          age: state.user.age,\n          isPremium: state.user.isPremium,\n        ),\n      );\n\n  // Select computed view models\n  CartSummary get cartSummary => _select(\n        (state) => CartSummary(\n          itemCount: state.cart.items.length,\n          total: state.cart.items.fold(0.0, (sum, item) => sum + item.price),\n          isEmpty: state.cart.items.isEmpty,\n        ),\n      );\n}\n\nclass CartSummary {\n  final int itemCount;\n  final double total;\n  final bool isEmpty;\n\n  CartSummary({\n    required this.itemCount,\n    required this.total,\n    required this.isEmpty,\n  });\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is CartSummary &&\n          itemCount == other.itemCount &&\n          total == other.total &&\n          isEmpty == other.isEmpty;\n\n  @override\n  int get hashCode => Object.hash(itemCount, total, isEmpty);\n}\n```\n\n### Usage Example\n\n```dart\nclass CombinedExample extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    // Get multiple values with one selector\n    final user = context.userInfo;\n    final cart = context.cartSummary;\n\n    return Column(\n      children: [\n        Text('User: ${user.name}, ${user.age} years old'),\n        Text('Premium: ${user.isPremium}'),\n        Text('Cart: ${cart.itemCount} items, \\$${cart.total}'),\n        if (cart.isEmpty) Text('Your cart is empty'),\n      ],\n    );\n  }\n}\n```\n\n### Benefits\n\n- Reduces number of selector calls\n- Groups related data logically\n- View models can encapsulate complex computations\n- Better performance when multiple values change together\n\n### Important Note\n\nRemember to implement `==` and `hashCode` for view model classes to ensure\nproper change detection and prevent unnecessary rebuilds.\n\n---\n\n## Pattern 5: Nullable State Handling\n\nHandle optional or nullable state gracefully with default values and safe\nselectors.\n\n### Extension Definition\n\n```dart\nextension NullableContextExtension on BuildContext {\n  R _select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  // Safe selectors with default values\n  String selectUserName({String defaultValue = 'Guest'}) => _select(\n      (state) => state.user.name.isEmpty ? defaultValue : state.user.name);\n\n  int selectUserAge({int defaultValue = 0}) =>\n      _select((state) => state.user.age > 0 ? state.user.age : defaultValue);\n\n  // Optional selectors\n  T? selectOptional<T>(T? Function(AppState state) selector) =>\n      _select(selector);\n}\n```\n\n### Usage Example\n\n```dart\nclass NullableExample extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    // Get values with fallbacks\n    final userName = context.selectUserName(defaultValue: 'Anonymous');\n    final userAge = context.selectUserAge(defaultValue: 18);\n\n    return Column(\n      children: [\n        Text('Name: $userName'),\n        Text('Age: $userAge'),\n      ],\n    );\n  }\n}\n```\n\n### Benefits\n\n- Gracefully handles missing or empty data\n- Provides sensible defaults\n- Reduces null checks in UI code\n\n---\n\n## Recommendations\n\n### 1. Start Simple\n\nUse **Pattern 1** (Basic Extension) for most apps:\n\n```dart\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n}\n```\n\nThis gives you:\n\n- `context.state` for full state access\n- `context.select((state) => ...)` with automatic type inference\n- No need to specify AppState or return type repeatedly\n\n### 2. Add Type-Specific Methods\n\nIf you find yourself repeatedly selecting the same types and want better IDE\nsupport, add typed methods (Pattern 2).\n\n### 3. Domain Methods for Large Apps\n\nFor complex apps with many screens, create domain-specific getters (Pattern 3)\nfor commonly accessed data. This makes your code more readable and maintainable.\n\n### 4. Performance Tips\n\n- The selector function is called on every state change to check if a rebuild is\n  needed\n- Keep selectors simple and fast\n- For expensive computations, consider caching/memoization\n- Avoid creating new objects in selectors unless necessary (or implement proper\n  `==` and `hashCode`)\n\n### 5. Testing\n\nExtensions make testing easier:\n\n- You can mock the context\n- Create test-specific extensions\n- Selectors are pure functions that are easy to test\n\n### 6. Combining Patterns\n\nYou can combine multiple patterns in a single extension:\n\n```dart\nextension AppContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  // Pattern 1: Generic selector\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  // Pattern 3: Domain-specific selectors for common use cases\n  String get userName => select((state) => state.user.name);\n  int get cartItemCount => select((state) => state.cart.items.length);\n}\n```\n\nThis provides both flexibility and convenience where you need it most.\n"
  },
  {
    "path": "example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/.flutter-plugins-dependencies\n**/flutter_export_environment.sh\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.packages\n.pub-cache/\n.pub/\n/build/\nbuild/\nios/.generated/\nios/Flutter/Generated.xcconfig\nios/Runner/GeneratedPluginRegistrant.*\npubspec.lock\n*.lock\n.flutter-plugins-dependencies\n\n# Android related\n**/android/**/gradle-wrapper.jar\n**/android/.gradle\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\n!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages\n"
  },
  {
    "path": "example/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: \"9f455d2486bcb28cad87b062475f42edc959f636\"\n  channel: \"stable\"\n\nproject_type: app\n\n# Tracks metadata for the flutter migrate command\nmigration:\n  platforms:\n    - platform: root\n      create_revision: 9f455d2486bcb28cad87b062475f42edc959f636\n      base_revision: 9f455d2486bcb28cad87b062475f42edc959f636\n    - platform: android\n      create_revision: 9f455d2486bcb28cad87b062475f42edc959f636\n      base_revision: 9f455d2486bcb28cad87b062475f42edc959f636\n    - platform: ios\n      create_revision: 9f455d2486bcb28cad87b062475f42edc959f636\n      base_revision: 9f455d2486bcb28cad87b062475f42edc959f636\n    - platform: web\n      create_revision: 9f455d2486bcb28cad87b062475f42edc959f636\n      base_revision: 9f455d2486bcb28cad87b062475f42edc959f636\n    - platform: windows\n      create_revision: 9f455d2486bcb28cad87b062475f42edc959f636\n      base_revision: 9f455d2486bcb28cad87b062475f42edc959f636\n\n  # User provided section\n\n  # List of Local paths (relative to this file) that should be\n  # ignored by the migrate tool.\n  #\n  # Files that are not part of the templates will be ignored by default.\n  unmanaged_files:\n    - 'lib/main.dart'\n    - 'ios/Runner.xcodeproj/project.pbxproj'\n"
  },
  {
    "path": "example/README.md",
    "content": "# Examples\n\n1. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main.dart\">main</a>\n\n    This example shows a counter and a button.\n    When the button is tapped, the counter will increment synchronously.\n    \n    In this simple example, the app state is simply a number (the counter),\n    and thus the store is defined as `Store<int>`. The initial state is `0`.    \n\n2. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_increment_async.dart\">main_increment_async</a>\n   \n   This example shows a counter, a text description, and a button.\n   When the button is tapped, the counter will increment synchronously,\n   while an async process downloads some text description that relates\n   to the counter number.  \n\n3. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_before_and_after.dart\">main_before_and_after</a>\n   \n    This example shows a counter, a text description, and a button.\n    When the button is tapped, the counter will increment synchronously,\n    while an async process downloads some text description that relates\n    to the counter number.\n   \n    While the async process is running, a redish modal barrier will prevent\n    the user from tapping the button. The model barrier is removed even if\n    the async process ends with an error, which can be simulated by turning\n    off the internet connection (putting the phone in airplane mode).\n\n4. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_static_view_model.dart\">main_static_view_model</a>\n\n    This example shows how to use the same `ViewModel` architecture of flutter_redux.\n    This is specially useful if you are migrating from flutter_redux.  \n    Here, you use the `StoreConnector`'s `converter` parameter, instead of the `vm` parameter.\n    And `ViewModel` doesn't extend `Vm`, but has a static factory:\n    `converter: (store) => ViewModel.fromStore(store)`.    \n\n5. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/test/main_before_and_after_STATE_test.dart\">main_before_and_after_STATE_test</a>\n\n   This example displays the testing capabilities of AsyncRedux: \n   How to test the store, actions, sync and async reducers, \n   by using the StoreTester. **Important:** To run the tests, put this file in a test directory.\n \n6. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_show_error_dialog.dart\">main_show_error_dialog</a>\n    \n    This example lets you enter a name and click save.\n    If the name has less than 4 chars, an error dialog will be shown.    \n\n7. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_navigate.dart\">main_navigate</a>\n\n    This example shows a route in the screen, all red. \n    When you tap the screen it will push a new route, all blue.\n    When you tap the screen again it will pop the blue route.\n\n8. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_event.dart\">main_event</a>\n\n   This example shows a text-field, and two buttons.\n   When the first button is tapped, an async process downloads some text from the internet\n   and puts it in the text-field.\n   When the second button is tapped, the text-field is cleared.\n   \n   This is meant to demonstrate the use of *events* to change a controller state.\n    \n   It also demonstrates the use of an abstract class to override the action's `before()` and `after()` methods.\n    \n9. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_infinite_scroll.dart\">main_infinite_scroll.dart</a>\n\n   This example demonstrates how to get a `Future` that completes when an action is done.\n   It shows a list of number descriptions. \n   If you pull to refresh the page (scroll above the top of the page) \n   a `RefreshIndicator` will appear until the list is updated with different data.\n   \n10. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_wait_action_simple.dart\">main_wait_action_simple</a>\n\n   This example is the same as the one in `main_before_and_after.dart`.\n   However, instead of declaring a `MyWaitAction`, it uses the build-in `WaitAction`.         \n   \n11. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_wait_action_advanced_1.dart\">main_wait_action_advanced_1</a>\n\n   This example demonstrates how to use `WaitAction` in advanced ways.   \n   10 buttons are shown. When a button is clicked it will be replaced by a downloaded text description. \n   Each button shows a progress indicator while its description is downloading. \n   The screen title shows the text \"Downloading...\" if any of the buttons is currently downloading.   \n   \n12. <a href=\"https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_wait_action_advanced_2.dart\">main_wait_action_advanced_2</a>\n\n   This example is the same as the one in `main_wait_action_advanced_1.dart`.\n   However, instead of only using flags in the `WaitAction`, it uses both flags and references.\n"
  },
  {
    "path": "example/analysis_options.yaml",
    "content": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n# The issues identified by the analyzer are surfaced in the UI of Dart-enabled\n# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be\n# invoked from the command line by running `flutter analyze`.\n\n# The following line activates a set of recommended lints for Flutter apps,\n# packages, and plugins designed to encourage good coding practices.\ninclude: package:flutter_lints/flutter.yaml\n\nlinter:\n  # The lint rules applied to this project can be customized in the\n  # section below to disable rules from the `package:flutter_lints/flutter.yaml`\n  # included above or to enable additional rules. A list of all available lints\n  # and their documentation is published at https://dart.dev/lints.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n    # avoid_print: false  # Uncomment to disable the `avoid_print` rule\n    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "example/android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n.cxx/\n\n# Remember to never publicly share your keystore.\n# See https://flutter.dev/to/reference-keystore\nkey.properties\n**/*.keystore\n**/*.jks\n"
  },
  {
    "path": "example/android/app/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.application\")\n    id(\"kotlin-android\")\n    // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.\n    id(\"dev.flutter.flutter-gradle-plugin\")\n}\n\nandroid {\n    namespace = \"com.example.example\"\n    compileSdk = flutter.compileSdkVersion\n    ndkVersion = flutter.ndkVersion\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_11\n        targetCompatibility = JavaVersion.VERSION_11\n    }\n\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_11.toString()\n    }\n\n    defaultConfig {\n        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).\n        applicationId = \"com.example.example\"\n        // You can update the following values to match your application needs.\n        // For more information, see: https://flutter.dev/to/review-gradle-config.\n        minSdk = flutter.minSdkVersion\n        targetSdk = flutter.targetSdkVersion\n        versionCode = flutter.versionCode\n        versionName = flutter.versionName\n    }\n\n    buildTypes {\n        release {\n            // TODO: Add your own signing config for the release build.\n            // Signing with the debug keys for now, so `flutter run --release` works.\n            signingConfig = signingConfigs.getByName(\"debug\")\n        }\n    }\n}\n\nflutter {\n    source = \"../..\"\n}\n"
  },
  {
    "path": "example/android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "example/android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <application\n        android:label=\"example\"\n        android:name=\"${applicationName}\"\n        android:icon=\"@mipmap/ic_launcher\">\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTop\"\n            android:taskAffinity=\"\"\n            android:theme=\"@style/LaunchTheme\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n            android:hardwareAccelerated=\"true\"\n            android:windowSoftInputMode=\"adjustResize\">\n            <!-- Specifies an Android theme to apply to this Activity as soon as\n                 the Android process has started. This theme is visible to the user\n                 while the Flutter UI initializes. After that, this theme continues\n                 to determine the Window background behind the Flutter UI. -->\n            <meta-data\n              android:name=\"io.flutter.embedding.android.NormalTheme\"\n              android:resource=\"@style/NormalTheme\"\n              />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n        <!-- Don't delete the meta-data below.\n             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->\n        <meta-data\n            android:name=\"flutterEmbedding\"\n            android:value=\"2\" />\n    </application>\n    <!-- Required to query activities that can process text, see:\n         https://developer.android.com/training/package-visibility and\n         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.\n\n         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.PROCESS_TEXT\"/>\n            <data android:mimeType=\"text/plain\"/>\n        </intent>\n    </queries>\n</manifest>\n"
  },
  {
    "path": "example/android/app/src/main/kotlin/com/example/example/MainActivity.kt",
    "content": "package com.example.example\n\nimport io.flutter.embedding.android.FlutterActivity\n\nclass MainActivity : FlutterActivity()\n"
  },
  {
    "path": "example/android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "example/android/app/src/main/res/drawable-v21/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"?android:colorBackground\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "example/android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "example/android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "example/android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "example/android/build.gradle.kts",
    "content": "allprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nval newBuildDir: Directory =\n    rootProject.layout.buildDirectory\n        .dir(\"../../build\")\n        .get()\nrootProject.layout.buildDirectory.value(newBuildDir)\n\nsubprojects {\n    val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)\n    project.layout.buildDirectory.value(newSubprojectBuildDir)\n}\nsubprojects {\n    project.evaluationDependsOn(\":app\")\n}\n\ntasks.register<Delete>(\"clean\") {\n    delete(rootProject.layout.buildDirectory)\n}\n"
  },
  {
    "path": "example/android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.12-all.zip\n"
  },
  {
    "path": "example/android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "example/android/settings.gradle.kts",
    "content": "pluginManagement {\n    val flutterSdkPath =\n        run {\n            val properties = java.util.Properties()\n            file(\"local.properties\").inputStream().use { properties.load(it) }\n            val flutterSdkPath = properties.getProperty(\"flutter.sdk\")\n            require(flutterSdkPath != null) { \"flutter.sdk not set in local.properties\" }\n            flutterSdkPath\n        }\n\n    includeBuild(\"$flutterSdkPath/packages/flutter_tools/gradle\")\n\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\n\nplugins {\n    id(\"dev.flutter.flutter-plugin-loader\") version \"1.0.0\"\n    id(\"com.android.application\") version \"8.9.1\" apply false\n    id(\"org.jetbrains.kotlin.android\") version \"2.1.0\" apply false\n}\n\ninclude(\":app\")\n"
  },
  {
    "path": "example/ios/.gitignore",
    "content": "**/dgph\n*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/ephemeral/\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "example/ios/Flutter/AppFrameworkInfo.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CFBundleDevelopmentRegion</key>\n  <string>en</string>\n  <key>CFBundleExecutable</key>\n  <string>App</string>\n  <key>CFBundleIdentifier</key>\n  <string>io.flutter.flutter.app</string>\n  <key>CFBundleInfoDictionaryVersion</key>\n  <string>6.0</string>\n  <key>CFBundleName</key>\n  <string>App</string>\n  <key>CFBundlePackageType</key>\n  <string>FMWK</string>\n  <key>CFBundleShortVersionString</key>\n  <string>1.0</string>\n  <key>CFBundleSignature</key>\n  <string>????</string>\n  <key>CFBundleVersion</key>\n  <string>1.0</string>\n  <key>MinimumOSVersion</key>\n  <string>13.0</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "example/ios/Flutter/Debug.xcconfig",
    "content": "#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "example/ios/Flutter/Release.xcconfig",
    "content": "#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "example/ios/Runner/AppDelegate.swift",
    "content": "import Flutter\nimport UIKit\n\n@main\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    GeneratedPluginRegistrant.register(with: self)\n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}\n"
  },
  {
    "path": "example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"83.5x83.5\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-83.5x83.5@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"1024x1024\",\n      \"idiom\" : \"ios-marketing\",\n      \"filename\" : \"Icon-App-1024x1024@1x.png\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "example/ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"12121\" systemVersion=\"16G29\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"12089\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n                            </imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"1a2-6s-vTC\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"4X2-HB-R7a\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"168\" height=\"185\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "example/ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"10117\" systemVersion=\"15F34\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"10085\"/>\n    </dependencies>\n    <scenes>\n        <!--Flutter View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"FlutterViewController\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"y3c-jy-aDJ\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"wfy-db-euE\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"8bC-Xf-vdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"600\" height=\"600\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"calibratedWhite\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "example/ios/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>Example</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>example</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "example/ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "example/ios/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 97C146E61CF9000F007C117D /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 97C146ED1CF9000F007C117D;\n\t\t\tremoteInfo = Runner;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t9705A1C41CF9048500538489 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t331C8082294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t331C807B294A618700263BE5 /* RunnerTests.swift */,\n\t\t\t);\n\t\t\tpath = RunnerTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t331C8082294A63A400263BE5 /* RunnerTests */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t331C8080294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t331C807D294A63A400263BE5 /* Sources */,\n\t\t\t\t331C807F294A63A400263BE5 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C8080294A63A400263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 97C146ED1CF9000F007C117D;\n\t\t\t\t\t};\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t\t331C8080294A63A400263BE5 /* RunnerTests */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C807F294A63A400263BE5 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\",\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\";\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t331C807D294A63A400263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 97C146ED1CF9000F007C117D /* Runner */;\n\t\t\ttargetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t97C146FA1CF9000F007C117D /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FB1CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t249021D4217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.example;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t331C8088294A63A400263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C8089294A63A400263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C808A294A63A400263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147041CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t97C147061CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.example;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147071CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.example;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C8088294A63A400263BE5 /* Debug */,\n\t\t\t\t331C8089294A63A400263BE5 /* Release */,\n\t\t\t\t331C808A294A63A400263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 97C146E61CF9000F007C117D /* Project object */;\n}\n"
  },
  {
    "path": "example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      customLLDBInitFile = \"$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            parallelizable = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"331C8080294A63A400263BE5\"\n               BuildableName = \"RunnerTests.xctest\"\n               BlueprintName = \"RunnerTests\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      customLLDBInitFile = \"$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      enableGPUValidationMode = \"1\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "example/ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "example/ios/RunnerTests/RunnerTests.swift",
    "content": "import Flutter\nimport UIKit\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add code to the Runner application, consider adding tests here.\n    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.\n  }\n\n}\n"
  },
  {
    "path": "example/lib/main.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<int> store;\n\n/// This example shows a counter and a button.\n/// When the button is tapped, the counter will increment synchronously.\n///\n/// In this simple example, the app state is simply a number (the counter),\n/// and thus the store is defined as `Store<int>`. The initial state is 0.\n///\nvoid main() {\n  store = Store<int>(initialState: 0);\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<int>(\n        store: store,\n        child: MaterialApp(\n          home: MyHomePage(),\n        ),\n      );\n}\n\n/// This action increments the counter by [amount]].\nclass IncrementAction extends ReduxAction<int> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  @override\n  int reduce() => state + amount;\n}\n\n/// This is a \"smart-widget\" that directly accesses the store state using\n/// `context.state` and dispatches actions using `dispatch`.\nclass MyHomePage extends StatelessWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    // In this simple example, the counter is the state.\n    // This will rebuild whenever the state changes.\n    // In more complex cases where we want the widget to rebuild only when\n    // specific parts of the state change, we can use `context.select` instead.\n    final counter = context.state;\n\n    return Scaffold(\n      appBar: AppBar(title: const Text('Increment Example')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('You have pushed the button this many times:'),\n            Text('$counter', style: const TextStyle(fontSize: 30))\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        // Dispatch action directly from widget\n        onPressed: () => dispatch(IncrementAction(amount: 1)),\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n\n/// Recommended to create this extension.\nextension BuildContextExtension on BuildContext {\n  int get state => getState<int>();\n\n  int read() => getRead<int>();\n\n  R select<R>(R Function(int state) selector) => getSelect<int, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/main_before_and_after.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example shows a counter, a text description, and a button.\n/// When the button is tapped, the counter will increment synchronously,\n/// while an async process downloads some text description that relates\n/// to the counter number (using the Star Wars API: https://swapi.dev).\n///\n/// While the async process is running, a reddish modal barrier will prevent\n/// the user from tapping the button. The model barrier is removed even if\n/// the async process ends with an error, which can be simulated by turning\n/// off the internet connection (putting the phone in airplane mode).\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state, which in this case is a counter, a description, and a waiting flag.\n@immutable\nclass AppState {\n  final int counter;\n  final String description;\n  final bool waiting;\n\n  AppState({\n    required this.counter,\n    required this.description,\n    required this.waiting,\n  });\n\n  AppState copy({int? counter, String? description, bool? waiting}) => AppState(\n        counter: counter ?? this.counter,\n        description: description ?? this.description,\n        waiting: waiting ?? this.waiting,\n      );\n\n  static AppState initialState() =>\n      AppState(counter: 0, description: \"\", waiting: false);\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          counter == other.counter &&\n          description == other.description &&\n          waiting == other.waiting;\n\n  @override\n  int get hashCode =>\n      counter.hashCode ^ description.hashCode ^ waiting.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePage(),\n      ));\n}\n\n/// This action increments the counter by 1,\n/// and then gets some description text relating to the new counter number.\nclass IncrementAndGetDescriptionAction extends ReduxAction<AppState> {\n  //\n  // Async reducer.\n  // To make it async we simply return Future<AppState> instead of AppState.\n  @override\n  Future<AppState> reduce() async {\n    // First, we increment the counter, synchronously.\n    dispatch(IncrementAction(amount: 1));\n\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/${state.counter}/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String description = json['name'] ?? 'Unknown character';\n\n    // After we get the response, we can modify the state with it,\n    // without having to dispatch another action.\n    return state.copy(description: description);\n  }\n\n  // This adds a modal barrier while the async process is running.\n  @override\n  void before() => dispatch(BarrierAction(true));\n\n  // This removes the modal barrier when the async process ends,\n  // even if there was some error in the process.\n  // You can test it by turning off the internet connection.\n  @override\n  void after() => dispatch(BarrierAction(false));\n}\n\nclass BarrierAction extends ReduxAction<AppState> {\n  final bool waiting;\n\n  BarrierAction(this.waiting);\n\n  @override\n  AppState reduce() {\n    return state.copy(waiting: waiting);\n  }\n}\n\n/// This action increments the counter by [amount]].\nclass IncrementAction extends ReduxAction<AppState> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  // Synchronous reducer.\n  @override\n  AppState reduce() => state.copy(counter: state.counter + amount);\n}\n\nclass MyHomePage extends StatelessWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    final counter = context.select((st) => st.counter);\n    final description = context.select((st) => st.description);\n    final waiting = context.select((st) => st.waiting);\n\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(title: const Text('Before and After Example')),\n          body: Center(\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                const Text('You have pushed the button this many times:'),\n                Text('$counter', style: const TextStyle(fontSize: 30)),\n                Text(\n                  description,\n                  style: const TextStyle(fontSize: 15),\n                  textAlign: TextAlign.center,\n                ),\n              ],\n            ),\n          ),\n          floatingActionButton: FloatingActionButton(\n            onPressed: () => dispatch(IncrementAndGetDescriptionAction()),\n            child: const Icon(Icons.add),\n          ),\n        ),\n        if (waiting) ModalBarrier(color: Colors.red.withAlpha((255.0 * 0.4).round())),\n      ],\n    );\n  }\n}\n\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/main_dependency_injection.dart",
    "content": "import 'dart:math';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<int> store;\n\n/// This example shows how to provide both an environment and dependencies to the Store,\n/// to help with dependency injection. The \"dependencies\" is a container for the\n/// injected services. You can have many dependency implementations, one\n/// for production, others for tests etc. In this case, we're using the\n/// [DependenciesProduction].\n///\n/// You should extend [ReduxAction] to provide typed access to the [Dependencies]\n/// inside your actions.\n///\n/// You should also define a context extension (See [BuildContextExtension.environment]\n/// below) to provide typed access to the [Environment] inside your widgets.\n///\nvoid main() {\n  //\n  store = Store<int>(\n    initialState: 0,\n    environment: Environment.production,\n    dependencies: (store) => Dependencies(store),\n  );\n\n  runApp(MyApp());\n}\n\nenum Environment {\n  production,\n  staging,\n  testing;\n\n  bool get isProduction => this == Environment.production;\n\n  bool get isStaging => this == Environment.staging;\n\n  bool get isTesting => this == Environment.testing;\n}\n\n/// The Dependencies class is a container for the injected services.\n/// We can have many dependency implementations, one for production, others for\n/// staging, tests etc.\nabstract class Dependencies {\n  factory Dependencies(Store store) {\n    if (store.environment == Environment.production) {\n      return DependenciesProduction();\n    } else if (store.environment == Environment.staging) {\n      return DependenciesStaging();\n    } else {\n      return DependenciesTesting();\n    }\n  }\n\n  /// This demonstrates how the environment can be used to change the behavior of the\n  /// dependencies. In this case, we have a method that limits the counter value,\n  /// and the limit is different in production:\n  /// - We limit the counter at 5, when in production\n  /// - We limit the counter at 1000, when in staging or testing.\n  int limit(int value);\n}\n\n/// Limit is 5 in production. \nclass DependenciesProduction implements Dependencies {\n  @override\n  int limit(int value) => min(value, 5);\n}\n\n/// Limit is 25 in staging.\nclass DependenciesStaging implements Dependencies {\n  @override\n  int limit(int value) => min(value, 25);\n}\n\n/// Limit is 1000 in testing.\nclass DependenciesTesting implements Dependencies {\n  @override\n  int limit(int value) => min(value, 1000);\n}\n\n/// Extend [ReduxAction] to provide typed access to the [Dependencies].\nabstract class Action extends ReduxAction<int> {\n  Dependencies get dependencies => super.store.dependencies as Dependencies;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<int>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePage(),\n      ),\n    );\n  }\n}\n\n/// This action increments the counter by [amount], using [env].\nclass IncrementAction extends Action {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  @override\n  int reduce() {\n    int newState = state + amount;\n    int limitedState = dependencies.limit(newState);\n    return limitedState;\n  }\n}\n\nclass MyHomePage extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    final env = context.environment;\n    int counter = context.state;\n\n    return Scaffold(\n      appBar: AppBar(title: const Text('Dependency Injection Example')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            // We can use the environment to change the UI as well.\n            Text('Running in ${env}.', textAlign: TextAlign.center),\n            //\n            const Text(\n              'You have pushed the button this many times:\\n'\n              '(limited by the environment)',\n              textAlign: TextAlign.center,\n            ),\n            //\n            Text('$counter', style: const TextStyle(fontSize: 30))\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () => dispatch(IncrementAction(amount: 1)),\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n\nextension BuildContextExtension on BuildContext {\n  int get state => getState<int>();\n\n  int read() => getRead<int>();\n\n  R select<R>(R Function(int state) selector) => getSelect<int, R>(selector);\n\n  R? event<R>(Evt<R> Function(int state) selector) => getEvent<int, R>(selector);\n\n  /// This is in case the UI needs to know if we are in production, staging or testing.\n  Environment get environment => getEnvironment<int>() as Environment;\n}\n"
  },
  {
    "path": "example/lib/main_event.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example is meant to demonstrate the use of \"events\" (of type [Event] or\n/// [Evt]) to change a controller state, or perform any other one-time operation.\n///\n/// It shows a text-field, and two buttons.\n/// When the first button is tapped, an async process downloads\n/// some text from the internet and puts it in the text-field.\n/// When the second button is tapped, the text-field is cleared.\n///\n/// It also demonstrates the use of an abstract class [BarrierAction]\n/// to override the action's before() and after() methods.\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state, which in this case is a counter and two events.\n@immutable\nclass AppState {\n  final int counter;\n  final bool waiting;\n  final Event clearTextEvt;\n  final Event<String> changeTextEvt;\n\n  AppState({\n    required this.counter,\n    required this.waiting,\n    required this.clearTextEvt,\n    required this.changeTextEvt,\n  });\n\n  AppState copy({\n    int? counter,\n    bool? waiting,\n    Event? clearTextEvt,\n    Event<String>? changeTextEvt,\n  }) =>\n      AppState(\n        counter: counter ?? this.counter,\n        waiting: waiting ?? this.waiting,\n        clearTextEvt: clearTextEvt ?? this.clearTextEvt,\n        changeTextEvt: changeTextEvt ?? this.changeTextEvt,\n      );\n\n  static AppState initialState() => AppState(\n        counter: 1,\n        waiting: false,\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          counter == other.counter &&\n          waiting == other.waiting;\n\n  @override\n  int get hashCode => counter.hashCode ^ waiting.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(\n          home: MyHomePage(),\n        ),\n      );\n}\n\n/// This action orders the text-controller to clear.\nclass ClearTextAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => state.copy(clearTextEvt: Event());\n}\n\n/// Actions that extend [BarrierAction] show a modal barrier while their async processes run.\nabstract class BarrierAction extends ReduxAction<AppState> {\n  @override\n  void before() => dispatch(_WaitAction(true));\n\n  @override\n  void after() => dispatch(_WaitAction(false));\n}\n\nclass _WaitAction extends ReduxAction<AppState> {\n  final bool waiting;\n\n  _WaitAction(this.waiting);\n\n  @override\n  AppState reduce() => state.copy(waiting: waiting);\n}\n\n/// This action downloads some new text, and then creates an event\n/// that tells the text-controller to display that new text.\nclass ChangeTextAction extends BarrierAction {\n  @override\n  Future<AppState> reduce() async {\n    //\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/${state.counter}/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String newText = json['name'] ?? 'Unknown Star Wars character';\n\n    return state.copy(\n      counter: state.counter + 1,\n      changeTextEvt: Event<String>(newText),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  _MyHomePageState createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  late TextEditingController controller;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = TextEditingController();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    //\n    var waiting = context.select((state) => state.waiting);\n\n    // Event that tells the controller to clear its text.\n    var clearText = context.event((state) => state.clearTextEvt);\n    if (clearText) controller.clear();\n\n    // Event that tells the controller to change its text.\n    var newText = context.event((state) => state.changeTextEvt);\n    if (newText != null) controller.text = newText;\n\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(title: const Text('Event Example')),\n          body: Center(\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                const Text('This is a TextField. Click to edit it:'),\n                TextField(controller: controller),\n                const SizedBox(height: 20),\n                FloatingActionButton(\n                  onPressed: () => dispatch(ChangeTextAction()),\n                  child: const Text(\"Change\"),\n                ),\n                const SizedBox(height: 20),\n                FloatingActionButton(\n                  onPressed: () => dispatch(ClearTextAction()),\n                  child: const Text(\"Clear\"),\n                ),\n              ],\n            ),\n          ),\n        ),\n        if (waiting) ModalBarrier(color: Colors.red.withAlpha((255.0 * 0.4).round())),\n      ],\n    );\n  }\n}\n\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/main_event_2.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example is similar to example `main_event.dart`, meant to demonstrate\n/// the use of \"events\" (of type [Event] or [Evt]) to change a controller state,\n/// or perform any other one-time operation.\n///\n/// However, here we consume the events in the `didChangeDependencies()` method\n/// of the stateful widget, instead of in the `build()` method.\n///\n/// To allow that, we need to turn off the debug mode of the `getEvent()` method,\n/// as shown in the extension method below:\n///\n/// ```dart\n/// extension BuildContextExtension on BuildContext {\n///   R? event<R>(Evt<R> Function(AppState state) selector) =>\n///       getEvent<AppState, R>(selector, debug: false);\n/// }\n/// ```\n///\n/// Use with care, as invalid usage in methods like `initState` will\n/// no longer be detected once the debug check is off.\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state, which in this case is a counter and two events.\n@immutable\nclass AppState {\n  final int counter;\n  final bool waiting;\n  final Event clearTextEvt;\n  final Event<String> changeTextEvt;\n\n  AppState({\n    required this.counter,\n    required this.waiting,\n    required this.clearTextEvt,\n    required this.changeTextEvt,\n  });\n\n  AppState copy({\n    int? counter,\n    bool? waiting,\n    Event? clearTextEvt,\n    Event<String>? changeTextEvt,\n  }) =>\n      AppState(\n        counter: counter ?? this.counter,\n        waiting: waiting ?? this.waiting,\n        clearTextEvt: clearTextEvt ?? this.clearTextEvt,\n        changeTextEvt: changeTextEvt ?? this.changeTextEvt,\n      );\n\n  static AppState initialState() => AppState(\n        counter: 1,\n        waiting: false,\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          counter == other.counter &&\n          waiting == other.waiting;\n\n  @override\n  int get hashCode => counter.hashCode ^ waiting.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(\n          home: MyHomePage(),\n        ),\n      );\n}\n\n/// This action orders the text-controller to clear.\nclass ClearTextAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => state.copy(clearTextEvt: Event());\n}\n\n/// Actions that extend [BarrierAction] show a modal barrier while their async processes run.\nabstract class BarrierAction extends ReduxAction<AppState> {\n  @override\n  void before() => dispatch(_WaitAction(true));\n\n  @override\n  void after() => dispatch(_WaitAction(false));\n}\n\nclass _WaitAction extends ReduxAction<AppState> {\n  final bool waiting;\n\n  _WaitAction(this.waiting);\n\n  @override\n  AppState reduce() => state.copy(waiting: waiting);\n}\n\n/// This action downloads some new text, and then creates an event\n/// that tells the text-controller to display that new text.\nclass ChangeTextAction extends BarrierAction {\n  @override\n  Future<AppState> reduce() async {\n    //\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/${state.counter}/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String newText = json['name'] ?? 'Unknown Star Wars character';\n\n    return state.copy(\n      counter: state.counter + 1,\n      changeTextEvt: Event<String>(newText),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  _MyHomePageState createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  late TextEditingController controller;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = TextEditingController();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n\n    // Event that tells the controller to clear its text.\n    var clearText = context.event((state) => state.clearTextEvt);\n    if (clearText) controller.clear();\n\n    // Event that tells the controller to change its text.\n    var newText = context.event((state) => state.changeTextEvt);\n    if (newText != null) controller.text = newText;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    //\n    var waiting = context.select((state) => state.waiting);\n\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(title: const Text('Event in didChangeDependencies Example')),\n          body: Center(\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                const Text('This is a TextField. Click to edit it:'),\n                TextField(controller: controller),\n                const SizedBox(height: 20),\n                FloatingActionButton(\n                  onPressed: () => dispatch(ChangeTextAction()),\n                  child: const Text(\"Change\"),\n                ),\n                const SizedBox(height: 20),\n                FloatingActionButton(\n                  onPressed: () => dispatch(ClearTextAction()),\n                  child: const Text(\"Clear\"),\n                ),\n              ],\n            ),\n          ),\n        ),\n        if (waiting) ModalBarrier(color: Colors.red.withAlpha((255.0 * 0.4).round())),\n      ],\n    );\n  }\n}\n\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector, debug: false);\n}\n"
  },
  {
    "path": "example/lib/main_infinite_scroll.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example shows a List of Star Wars characters.\n///\n/// - Scrolling to the bottom of the list will async load the next 20 characters.\n///\n/// - Scrolling past the top of the list (pull to refresh) will use\n/// `dispatchAndWait` to dispatch an action and get a future that tells the\n/// `RefreshIndicator` when the action completes.\n///\n/// - `isWaiting(LoadMoreAction)` prevents the user from loading more while the\n/// async action is running.\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(\n    initialState: state,\n    actionObservers: [Log<AppState>.printer()],\n    modelObserver: DefaultModelObserver(),\n  );\n  runApp(MyApp());\n}\n\n@immutable\nclass AppState {\n  final List<String> numTrivia;\n\n  AppState({required this.numTrivia});\n\n  AppState copy({List<String>? numTrivia}) =>\n      AppState(numTrivia: numTrivia ?? this.numTrivia);\n\n  static AppState initialState() => AppState(numTrivia: <String>[]);\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          numTrivia == other.numTrivia;\n\n  @override\n  int get hashCode => numTrivia.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(\n          debugShowCheckedModeBanner: false,\n          home: MyHomePage(),\n        ),\n      );\n}\n\nclass LoadMoreAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    List<String> list = List.from(state.numTrivia);\n    int start = state.numTrivia.length + 1;\n\n    // Fetch 20 people concurrently.\n    final responses = await Future.wait(\n      List.generate(20,\n          (i) => get(Uri.parse('https://swapi.dev/api/people/${start + i}/'))),\n    );\n\n    for (final response in responses) {\n      if (response.statusCode == 200) {\n        final data = jsonDecode(response.body);\n        list.add(data['name'] ?? 'Unknown character');\n      }\n    }\n\n    return state.copy(numTrivia: list);\n  }\n}\n\nclass RefreshAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    List<String> list = [];\n\n    // Fetch the first 20 people concurrently.\n    final responses = await Future.wait(\n      List.generate(\n          20, (i) => get(Uri.parse('https://swapi.dev/api/people/${i + 1}/'))),\n    );\n\n    for (final response in responses) {\n      if (response.statusCode == 200) {\n        final data = jsonDecode(response.body);\n        list.add(data['name'] ?? 'Unknown character');\n      }\n    }\n\n    return state.copy(numTrivia: list);\n  }\n}\n\n/// This is a \"smart-widget\" that directly accesses the store to select state\n/// and dispatch actions, using context.select(), dispatch(), etc.\nclass MyHomePage extends StatefulWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  _MyHomePageState createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  late ScrollController _controller;\n\n  @override\n  void initState() {\n    super.initState();\n\n    // Dispatch the initial refresh action\n    dispatch(RefreshAction());\n\n    _controller = ScrollController()..addListener(_scrollListener);\n  }\n\n  void _scrollListener() {\n    // Get the current loading state\n    final isLoading = context.isWaiting(LoadMoreAction);\n\n    // Load more when scrolled to the bottom\n    if (!isLoading &&\n        _controller.position.maxScrollExtent == _controller.position.pixels) {\n      context.dispatch(LoadMoreAction());\n    }\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // Select only the numTrivia list from state. Rebuilds only when numTrivia changes.\n    final numTrivia = context.select((state) => state.numTrivia);\n\n    // Check if LoadMoreAction is currently running\n    final isLoading = context.isWaiting(LoadMoreAction);\n\n    return Scaffold(\n      appBar: AppBar(title: const Text('Infinite Scroll Example')),\n      body: numTrivia.isEmpty\n          ? Container()\n          : RefreshIndicator(\n              onRefresh: () => context.dispatchAndWait(RefreshAction()),\n              child: ListView.builder(\n                controller: _controller,\n                itemCount: numTrivia.length + 1,\n                itemBuilder: (context, index) {\n                  // Show loading spinner at the end\n                  if (index == numTrivia.length) {\n                    return Padding(\n                      padding: EdgeInsets.all(8.0),\n                      child: Center(\n                        child: isLoading\n                            ? CircularProgressIndicator()\n                            : SizedBox(height: 30),\n                      ),\n                    );\n                  } else {\n                    return ListTile(\n                      leading: CircleAvatar(child: Text(index.toString())),\n                      title: Text(numTrivia[index]),\n                    );\n                  }\n                },\n              ),\n            ),\n    );\n  }\n}\n\n/// Recommended extension methods for accessing state and dispatching actions.\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/main_is_waiting_works_when_multiple_actions.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n/// This example shows how to show a spinner while any of two actions\n/// ([IncrementAction] and [MultiplyAction]) is running.\n///\n/// Writing this:\n///\n/// ```dart\n/// isWaiting([IncrementAction, MultiplyAction])\n/// ```\n///\n/// Is the same as writing this:\n///\n/// ```dart\n/// isWaiting(IncrementAction) || isWaiting(MultiplyAction)\n/// ```\n///\n/// The `isCalculating` variable is defined in the `build` method\n/// of widget [MyHomePage]:\n///\n/// ```dart\n/// bool isCalculating = isWaiting([IncrementAction, MultiplyAction]);\n/// ```\n///\n/// In more detail:\n/// - There are two floating action buttons: one to increment the counter\n///   and another to multiply it by 2.\n/// - When any of the buttons is tapped, its respective action is dispatched.\n/// - While any of the actions is running, both buttons show a spinner\n///   and are disabled.\n///\nvoid main() {\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    var store = Store<AppState>(initialState: AppState(counter: 0));\n    store.onChange.listen(print);\n\n    return MaterialApp(\n      theme: ThemeData(\n        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\n        useMaterial3: true,\n      ),\n      home: StoreProvider(\n        store: store,\n        child: const MyHomePage(),\n      ),\n    );\n  }\n}\n\nclass MyHomePage extends StatelessWidget {\n  const MyHomePage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    int counter = context.select((state) => state.counter);\n    bool isCalculating = context.isWaiting([IncrementAction, MultiplyAction]);\n\n    return MyHomePageContent(\n      title: 'IsWaiting multiple actions',\n      counter: counter,\n      isCalculating: isCalculating,\n      increment: () => dispatch(IncrementAction()),\n      multiply: () => dispatch(MultiplyAction()),\n    );\n  }\n}\n\nclass MyHomePageContent extends StatelessWidget {\n  const MyHomePageContent({\n    super.key,\n    required this.title,\n    required this.counter,\n    required this.isCalculating,\n    required this.increment,\n    required this.multiply,\n  });\n\n  final String title;\n  final int counter;\n  final bool isCalculating;\n  final VoidCallback increment, multiply;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        backgroundColor: Theme.of(context).colorScheme.inversePrimary,\n        title: Text(title),\n      ),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('Result:'),\n            Text(\n              '$counter',\n              style: Theme.of(context).textTheme.headlineMedium,\n            ),\n          ],\n        ),\n      ),\n      floatingActionButton: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FloatingActionButton(\n            onPressed: isCalculating ? null : increment,\n            elevation: isCalculating ? 0 : 6,\n            backgroundColor: isCalculating ? Colors.grey[300] : Colors.blue,\n            child: isCalculating\n                ? const Padding(\n                    padding: const EdgeInsets.all(16.0),\n                    child: const CircularProgressIndicator(),\n                  )\n                : const Icon(Icons.add),\n          ),\n          const SizedBox(height: 16),\n          FloatingActionButton(\n            onPressed: isCalculating ? null : multiply,\n            elevation: isCalculating ? 0 : 6,\n            backgroundColor: isCalculating ? Colors.grey[300] : Colors.blue,\n            child: isCalculating\n                ? const Padding(\n                    padding: const EdgeInsets.all(16.0),\n                    child: const CircularProgressIndicator(),\n                  )\n                : const Icon(Icons.close),\n          )\n        ],\n      ),\n    );\n  }\n}\n\nclass AppState {\n  final int counter;\n\n  AppState({required this.counter});\n\n  AppState copy({int? counter}) => AppState(counter: counter ?? this.counter);\n\n  @override\n  String toString() {\n    return '.\\n.\\n.\\nAppState{counter: $counter}\\n.\\n.\\n';\n  }\n}\n\nclass IncrementAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await Future.delayed(const Duration(seconds: 1));\n    return AppState(counter: state.counter + 1);\n  }\n}\n\nclass MultiplyAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await Future.delayed(const Duration(seconds: 1));\n    return AppState(counter: state.counter * 2);\n  }\n}\n\n/// Recommended to create this extension.\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/main_navigate.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\nlate Store<AppState> store;\n\nfinal navigatorKey = GlobalKey<NavigatorState>();\n\nvoid main() async {\n  NavigateAction.setNavigatorKey(navigatorKey);\n  store = Store<AppState>(initialState: AppState());\n  runApp(MyApp());\n}\n\nfinal routes = {\n  '/': (BuildContext context) => Page1(),\n  \"/myRoute\": (BuildContext context) => Page2(),\n};\n\nclass AppState {}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        routes: routes,\n        navigatorKey: navigatorKey,\n      ),\n    );\n  }\n}\n\nclass Page extends StatelessWidget {\n  final Color? color;\n  final String? text;\n  final VoidCallback onChangePage;\n\n  Page({this.color, this.text, required this.onChangePage});\n\n  @override\n  Widget build(BuildContext context) => ElevatedButton(\n        style: ElevatedButton.styleFrom(backgroundColor: color),\n        child: Text(text!),\n        onPressed: onChangePage,\n      );\n}\n\nclass Page1 extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Page(\n      color: Colors.red,\n      text: \"Tap me to push a new route!\",\n      onChangePage: () => dispatch(NavigateAction.pushNamed(\"/myRoute\")),\n    );\n  }\n}\n\nclass Page2 extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Page(\n      color: Colors.blue,\n      text: \"Tap me to pop this route!\",\n      onChangePage: () => dispatch(NavigateAction.pop()),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/main_optimistic_command.dart",
    "content": "/// This example is meant to demonstrate the [OptimisticCommand] mixin in action.\n/// The screen is split into two halves: the top shows the UI state (Redux), and\n/// the bottom shows the simulated database state (server).\n///\n/// ## Use cases to try:\n///\n/// ### 1. Optimistic update\n/// Tap the heart icon. Notice the UI updates instantly (top half), while the\n/// database takes ~3.5 seconds to update (bottom half shows \"Saving...\").\n///\n/// ### 2. Non-reentrant behavior\n/// While \"Saving...\" is displayed, notice the button becomes semi-transparent\n/// and disabled. This prevents conflicting concurrent requests.\n///\n/// ### 3. Server response applied\n/// After saving completes, both halves show the same state. The server response\n/// is applied to ensure the UI reflects the actual saved value.\n///\n/// ### 4. Rollback on error\n/// First tap the heart to start saving. While \"Saving...\" is displayed, tap\n/// \"Request fails\" at the bottom. The UI will rollback to its previous state\n/// once the simulated error occurs.\n///\n/// ### 5. External database changes (no push)\n/// Use the \"Liked\" or \"Not Liked\" buttons at the bottom to change the database\n/// directly. The UI may update only if a request is still in progress, because\n/// the request response will overwrite the UI state when it completes.\n/// But when there is no request in progress, the UI state won't update,\n/// because OptimisticCommand doesn't support push notifications. The UI only\n/// syncs when you tap the heart again.\n///\n/// Note: If you use push, try mixin [OptimisticSyncWithPush] instead.\n///\nimport 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport \"package:meta/meta.dart\";\n\nlate Store<AppState> store;\n\nvoid main() {\n  store = Store<AppState>(\n    initialState: AppState(liked: false),\n    actionObservers: [ConsoleActionObserver()],\n  );\n  runApp(const MyApp());\n}\n\nclass AppState {\n  final bool liked;\n\n  AppState({required this.liked});\n\n  @useResult\n  AppState copy({bool? isLiked}) => AppState(liked: isLiked ?? this.liked);\n\n  @override\n  String toString() => 'AppState(liked: $liked)';\n}\n\nclass SetLike extends AppAction {\n  final bool isLiked;\n\n  SetLike(this.isLiked);\n\n  @override\n  AppState reduce() => state.copy(isLiked: isLiked);\n\n  @override\n  String toString() => '${super.toString()}($isLiked)';\n}\n\nclass ToggleLike extends AppAction with OptimisticCommand<AppState> {\n  @override\n  Object? optimisticValue() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(isLiked: value as bool);\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    bool isLiked = serverResponse as bool;\n    return state.copy(isLiked: isLiked);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? value) =>\n      server.saveLike(value as bool);\n\n  // If there was an error, reload the value from the database.\n  @override\n  Future<Object?> reloadFromServer() => server.reload();\n\n  @override\n  String toString() => '${super.toString()}(${!state.liked})';\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        debugShowCheckedModeBanner: false,\n        title: 'OptimisticCommand Mixin Demo',\n        theme: ThemeData(primarySwatch: Colors.blue),\n        home: const MyHomePage(),\n      ),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n  const MyHomePage({super.key});\n\n  @override\n  State<MyHomePage> createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  late Timer _timer;\n\n  @override\n  void initState() {\n    super.initState();\n    // Refresh the UI periodically to show the database state.\n    _timer = Timer.periodic(const Duration(milliseconds: 100), (_) {\n      setState(() {});\n    });\n  }\n\n  @override\n  void dispose() {\n    _timer.cancel();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isWaiting = context.isWaiting(ToggleLike);\n\n    return Scaffold(\n      appBar: AppBar(title: const Text('OptimisticCommand Mixin Demo')),\n      body: Column(\n        children: [\n          // Top half: Like button (Redux state)\n          Expanded(\n            child: Container(\n              color: Colors.blue.shade50,\n              child: Center(\n                child: StoreConnector<AppState, bool>(\n                  converter: (store) => store.state.liked,\n                  builder: (context, liked) {\n                    return Column(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        const Text(\n                          'UI State (AsyncRedux)',\n                          style: TextStyle(\n                            fontSize: 18,\n                            fontWeight: FontWeight.bold,\n                          ),\n                        ),\n                        const SizedBox(height: 20),\n                        Opacity(\n                          opacity: isWaiting ? 0.25 : 1.0,\n                          child: IconButton(\n                            iconSize: 80,\n                            icon: Icon(\n                              liked ? Icons.favorite : Icons.favorite_border,\n                              color: liked ? Colors.red : Colors.grey,\n                            ),\n                            //\n                            // We could also disable the button using isWaiting:\n                            // onPressed: isWaiting ? null : () => store.dispatch(ToggleLike()),\n                            onPressed: () => store.dispatch(ToggleLike()),\n                          ),\n                        ),\n                        const SizedBox(height: 10),\n                        Text(\n                          liked ? 'Liked' : 'Not Liked',\n                          style: const TextStyle(fontSize: 24),\n                        ),\n                        const SizedBox(height: 20),\n                        Text(\n                          context.isWaiting(ToggleLike)\n                              ? 'Saving...'\n                              : '',\n                          style: const TextStyle(\n                            fontSize: 14,\n                            color: Colors.grey,\n                          ),\n                        ),\n                        Text(\n                          'Button action is aborted while saving',\n                          style: const TextStyle(\n                            fontSize: 14,\n                            color: Colors.grey,\n                          ),\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            ),\n          ),\n          // Divider\n          Container(\n            height: 2,\n            color: Colors.grey.shade400,\n          ),\n          // Bottom half: Database state\n          Expanded(\n            child: Container(\n              color: Colors.green.shade50,\n              child: Center(\n                child: Column(\n                  mainAxisAlignment: MainAxisAlignment.center,\n                  children: [\n                    const Text(\n                      'Database State (Simulated)',\n                      style: TextStyle(\n                        fontSize: 18,\n                        fontWeight: FontWeight.bold,\n                      ),\n                    ),\n                    const SizedBox(height: 20),\n                    Icon(\n                      server.databaseLiked\n                          ? Icons.favorite\n                          : Icons.favorite_border,\n                      size: 80,\n                      color: server.databaseLiked ? Colors.red : Colors.grey,\n                    ),\n                    const SizedBox(height: 10),\n                    Text(\n                      server.databaseLiked ? 'Liked' : 'Not Liked',\n                      style: const TextStyle(fontSize: 24),\n                    ),\n                    const SizedBox(height: 20),\n                    Row(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        Text(\n                          server.isRequestInProgress\n                              ? 'Saving ${server.requestCount}...'\n                              : 'Idle',\n                          style: TextStyle(\n                            fontSize: 16,\n                            color: server.isRequestInProgress\n                                ? Colors.orange\n                                : Colors.grey,\n                            fontWeight: server.isRequestInProgress\n                                ? FontWeight.bold\n                                : FontWeight.normal,\n                          ),\n                        ),\n                        const SizedBox(width: 10),\n                        if (server.isRequestInProgress)\n                          const SizedBox(\n                            width: 16,\n                            height: 16,\n                            child: CircularProgressIndicator(\n                              strokeWidth: 2,\n                              color: Colors.orange,\n                            ),\n                          ),\n                      ],\n                    ),\n                    const SizedBox(height: 10),\n                    Text(\n                      'Updates after server round-trip (${(server.delayBeforeWrite + server.delayAfterWrite) / 1000}s)',\n                      style: TextStyle(\n                        fontSize: 14,\n                        color: Colors.grey,\n                      ),\n                    ),\n                    const SizedBox(height: 10),\n                    Text(\n                      'Number of requests received: ${server.requestCount}',\n                      style: TextStyle(\n                        fontSize: 14,\n                        color: Colors.grey,\n                      ),\n                    ),\n                    const SizedBox(height: 30),\n                    Container(\n                      padding: const EdgeInsets.all(16),\n                      decoration: BoxDecoration(\n                        border: Border.all(color: Colors.grey.shade400),\n                        borderRadius: BorderRadius.circular(8),\n                      ),\n                      child: Column(\n                        children: [\n                          const Text(\n                            'Simulate external change to the database:'\n                                '\\n'\n                                '(there is no push)'\n                                '\\n',\n                            style: TextStyle(fontSize: 14, color: Colors.grey),\n                            textAlign: TextAlign.center,\n                          ),\n                          const SizedBox(height: 8),\n                          Row(\n                            mainAxisSize: MainAxisSize.min,\n                            children: [\n                              ElevatedButton.icon(\n                                onPressed: () =>\n                                    server.simulateExternalChange(true),\n                                icon: const Icon(Icons.favorite, size: 16),\n                                label: const Text('Liked'),\n                                style: ElevatedButton.styleFrom(\n                                  backgroundColor: Colors.red.shade100,\n                                  foregroundColor: Colors.red.shade900,\n                                ),\n                              ),\n                              const SizedBox(width: 16),\n                              ElevatedButton.icon(\n                                onPressed: () =>\n                                    server.simulateExternalChange(false),\n                                icon:\n                                    const Icon(Icons.favorite_border, size: 16),\n                                label: const Text('Not Liked'),\n                                style: ElevatedButton.styleFrom(\n                                  backgroundColor: Colors.grey.shade200,\n                                  foregroundColor: Colors.grey.shade700,\n                                ),\n                              ),\n                            ],\n                          ),\n                          const SizedBox(height: 8),\n                          ElevatedButton.icon(\n                            onPressed: server.isRequestInProgress\n                                ? () {\n                                    server.shouldFail = true;\n                                  }\n                                : null,\n                            icon: const Icon(Icons.error_outline, size: 16),\n                            label: const Text('Request fails'),\n                            style: ElevatedButton.styleFrom(\n                              backgroundColor: Colors.orange.shade100,\n                              foregroundColor: Colors.orange.shade900,\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nabstract class AppAction extends ReduxAction<AppState> {}\n\n// ////////////////////////////////////////////////////////////////////////////\n\n/// Singleton instance of the simulated server.\nfinal server = SimulatedServer();\n\n/// Simulates a remote server with database and request handling.\n/// All server-side state and behavior is encapsulated here to clearly separate\n/// it from the local app state managed by Redux.\nclass SimulatedServer {\n  // ---------------------------------------------------------------------------\n  // Server State\n  // ---------------------------------------------------------------------------\n\n  /// The \"database\" value stored on the server.\n  bool databaseLiked = false;\n\n  /// Whether a request is currently being processed.\n  bool isRequestInProgress = false;\n\n  /// Total number of requests received by the server.\n  int requestCount = 0;\n\n  /// When true, the next request will fail (for testing error handling).\n  bool shouldFail = false;\n\n  /// Simulated network delay before writing to database (ms).\n  int delayBeforeWrite = 1500;\n\n  /// Simulated network delay after writing to database (ms).\n  int delayAfterWrite = 2000;\n\n  // ---------------------------------------------------------------------------\n  // Server Methods\n  // ---------------------------------------------------------------------------\n\n  /// Simulates saving to the database.\n  /// Returns the current database value after save completes.\n  Future<bool> saveLike(bool flag) async {\n    requestCount++;\n    isRequestInProgress = true;\n    await _interruptibleDelay(delayBeforeWrite);\n\n    databaseLiked = flag;\n    await _interruptibleDelay(delayAfterWrite);\n    isRequestInProgress = false;\n\n    // Return the current value in the database.\n    // This may differ from the saved value, simulating server-side logic.\n    return databaseLiked;\n  }\n\n  /// Simulates reloading the current value from the database.\n  Future<bool> reload() async {\n    await Future.delayed(const Duration(milliseconds: 300));\n    return databaseLiked;\n  }\n\n  /// Simulates an external change to the database (e.g., from another client).\n  /// Note: OptimisticCommand does not support push notifications.\n  void simulateExternalChange(bool liked) {\n    databaseLiked = liked;\n  }\n\n  /// Interruptible delay that checks [shouldFail] every 50ms.\n  /// Allows simulating mid-flight request failures.\n  Future<void> _interruptibleDelay(int milliseconds) async {\n    const checkInterval = 50;\n    int remaining = milliseconds;\n    while (remaining > 0) {\n      if (shouldFail) {\n        shouldFail = false;\n        isRequestInProgress = false;\n        throw Exception('Simulated server error');\n      }\n      final wait = remaining < checkInterval ? remaining : checkInterval;\n      await Future.delayed(Duration(milliseconds: wait));\n      remaining -= checkInterval;\n    }\n  }\n}\n"
  },
  {
    "path": "example/lib/main_optimistic_sync.dart",
    "content": "/// This example is meant to demonstrate the [OptimisticSync] mixin in action.\n/// The screen is split into two halves: the top shows the UI state (Redux), and\n/// the bottom shows the simulated database state (server).\n///\n/// ## Use cases to try:\n///\n/// ### 1. Optimistic update\n/// Tap the heart icon. The UI updates instantly (top half), while the database\n/// takes ~3.5 seconds to update (bottom half shows \"Saving...\").\n///\n/// ### 2. Coalescing (key feature)\n/// Tap the heart rapidly multiple times while \"Saving...\" is displayed. Notice:\n/// - The UI toggles instantly on each tap (always responsive).\n/// - Only one request is in flight at a time (\"Saving 1...\").\n/// - When the request completes, if the current UI state differs from what was\n///   sent, a follow-up request is automatically sent (\"Saving 2...\").\n/// - If you toggle an even number of times, no follow-up is needed because the\n///   final state matches what was originally sent.\n///\n/// ### 3. Button always enabled\n/// Unlike [OptimisticCommand], the button is never disabled. This allows rapid\n/// interactions without waiting for server responses.\n///\n/// ### 4. Reload on error\n/// Tap the heart to start saving. While \"Saving...\" is displayed, tap \"Request\n/// fails\". The UI keeps its optimistic state, but [OptimisticSync.onFinish] is\n/// called with the error. In this example, we show an error dialog,\n/// immediately revert to the initial state before the action, and then,\n/// just to be sure, reload the value from the database.\n///\n/// ### 5. External database changes (no push)\n/// Use the \"Liked\" or \"Not Liked\" buttons at the bottom to change the database\n/// directly. The UI may update only if a request is still in progress, because\n/// the request response will overwrite the UI state when it completes.\n/// But when there is no request in progress, the UI state won't update,\n/// because [OptimisticSync] doesn't support push notifications. The UI only\n/// syncs when you tap the heart again.\n///\n/// Note: If you use push, try mixin [OptimisticSyncWithPush] instead.\n///\nimport 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport \"package:meta/meta.dart\";\n\nlate Store<AppState> store;\n\nvoid main() {\n  store = Store<AppState>(\n    initialState: AppState(liked: false),\n    actionObservers: [ConsoleActionObserver()],\n  );\n  runApp(const MyApp());\n}\n\nclass AppState {\n  final bool liked;\n\n  AppState({required this.liked});\n\n  @useResult\n  AppState copy({bool? isLiked}) => AppState(liked: isLiked ?? this.liked);\n\n  @override\n  String toString() => 'AppState(liked: $liked)';\n}\n\nclass SetLike extends AppAction {\n  final bool isLiked;\n\n  SetLike(this.isLiked);\n\n  @override\n  AppState reduce() => state.copy(isLiked: isLiked);\n\n  @override\n  String toString() => '${super.toString()}($isLiked)';\n}\n\nclass ToggleLike extends AppAction with OptimisticSync<AppState, bool> {\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(\n          AppState state, bool optimisticValueToApply) =>\n      state.copy(isLiked: optimisticValueToApply);\n\n  @override\n  AppState applyServerResponseToState(AppState state, Object? serverResponse) {\n    bool isLiked = serverResponse as bool;\n    return state.copy(isLiked: isLiked);\n  }\n\n  @override\n  Future<bool> sendValueToServer(Object? value) =>\n      server.saveLike(value as bool);\n\n  // If there was an error:\n  // 1. Show an error message to the user.\n  // 2. Immediately revert to the initial state before the action.\n  // 3. Then, to be sure, reload the value from the database.\n  @override\n  Future<AppState?> onFinish(Object? error) async {\n    if (error == null) return null;\n\n    // 1. Show an error message to the user.\n    dispatch(\n      UserExceptionAction('The server request failed',\n          reason: 'The like status was rolled back and then reloaded.'),\n    );\n\n    // 2. Immediately rollback to the initial state before the action.\n    dispatchState(state.copy(isLiked: getValueFromState(initialState)));\n\n    // 3. Then, to be sure, reload the value from the database.\n    bool isLiked = await server.reload();\n    return state.copy(isLiked: isLiked);\n  }\n\n  @override\n  String toString() => '${super.toString()}(${!state.liked})';\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        debugShowCheckedModeBanner: false,\n        title: 'OptimisticSync Mixin Demo',\n        theme: ThemeData(primarySwatch: Colors.blue),\n        home: UserExceptionDialog<AppState>(\n          child: const MyHomePage(),\n        ),\n      ),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n  const MyHomePage({super.key});\n\n  @override\n  State<MyHomePage> createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  late Timer _timer;\n\n  @override\n  void initState() {\n    super.initState();\n    // Refresh the UI periodically to show the database state.\n    _timer = Timer.periodic(const Duration(milliseconds: 100), (_) {\n      setState(() {});\n    });\n  }\n\n  @override\n  void dispose() {\n    _timer.cancel();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('OptimisticSync Mixin Demo')),\n      body: Column(\n        children: [\n          // Top half: Like button (Redux state)\n          Expanded(\n            child: Container(\n              color: Colors.blue.shade50,\n              child: Center(\n                child: StoreConnector<AppState, bool>(\n                  converter: (store) => store.state.liked,\n                  builder: (context, liked) {\n                    return Column(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        const Text(\n                          'UI State (AsyncRedux)',\n                          style: TextStyle(\n                            fontSize: 18,\n                            fontWeight: FontWeight.bold,\n                          ),\n                        ),\n                        const SizedBox(height: 20),\n                        IconButton(\n                          iconSize: 80,\n                          icon: Icon(\n                            liked ? Icons.favorite : Icons.favorite_border,\n                            color: liked ? Colors.red : Colors.grey,\n                          ),\n                          onPressed: () {\n                            store.dispatch(ToggleLike());\n                          },\n                        ),\n                        const SizedBox(height: 10),\n                        Text(\n                          liked ? 'Liked' : 'Not Liked',\n                          style: const TextStyle(fontSize: 24),\n                        ),\n                        const SizedBox(height: 20),\n                        const Text(\n                          'Tap rapidly to see coalescing in action!',\n                          style: TextStyle(\n                            fontSize: 14,\n                            color: Colors.grey,\n                          ),\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            ),\n          ),\n          // Divider\n          Container(\n            height: 2,\n            color: Colors.grey.shade400,\n          ),\n          // Bottom half: Database state\n          Expanded(\n            child: Container(\n              color: Colors.green.shade50,\n              child: Center(\n                child: Column(\n                  mainAxisAlignment: MainAxisAlignment.center,\n                  children: [\n                    const Text(\n                      'Database State (Simulated)',\n                      style: TextStyle(\n                        fontSize: 18,\n                        fontWeight: FontWeight.bold,\n                      ),\n                    ),\n                    const SizedBox(height: 20),\n                    Icon(\n                      server.databaseLiked\n                          ? Icons.favorite\n                          : Icons.favorite_border,\n                      size: 80,\n                      color: server.databaseLiked ? Colors.red : Colors.grey,\n                    ),\n                    const SizedBox(height: 10),\n                    Text(\n                      server.databaseLiked ? 'Liked' : 'Not Liked',\n                      style: const TextStyle(fontSize: 24),\n                    ),\n                    const SizedBox(height: 20),\n                    Row(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        Text(\n                          server.isRequestInProgress\n                              ? 'Saving ${server.requestCount}...'\n                              : 'Idle',\n                          style: TextStyle(\n                            fontSize: 16,\n                            color: server.isRequestInProgress\n                                ? Colors.orange\n                                : Colors.grey,\n                            fontWeight: server.isRequestInProgress\n                                ? FontWeight.bold\n                                : FontWeight.normal,\n                          ),\n                        ),\n                        const SizedBox(width: 10),\n                        if (server.isRequestInProgress)\n                          const SizedBox(\n                            width: 16,\n                            height: 16,\n                            child: CircularProgressIndicator(\n                              strokeWidth: 2,\n                              color: Colors.orange,\n                            ),\n                          ),\n                      ],\n                    ),\n                    const SizedBox(height: 10),\n                    Text(\n                      'Updates after server round-trip (${(server.delayBeforeWrite + server.delayAfterWrite) / 1000}s)',\n                      style: TextStyle(\n                        fontSize: 14,\n                        color: Colors.grey,\n                      ),\n                    ),\n                    const SizedBox(height: 10),\n                    Text(\n                      'Number of requests received: ${server.requestCount}',\n                      style: TextStyle(\n                        fontSize: 14,\n                        color: Colors.grey,\n                      ),\n                    ),\n                    const SizedBox(height: 30),\n                    Container(\n                      padding: const EdgeInsets.all(16),\n                      decoration: BoxDecoration(\n                        border: Border.all(color: Colors.grey.shade400),\n                        borderRadius: BorderRadius.circular(8),\n                      ),\n                      child: Column(\n                        children: [\n                          const Text(\n                            'Simulate external change to the database:',\n                            style: TextStyle(fontSize: 14, color: Colors.grey),\n                          ),\n                          const SizedBox(height: 8),\n                          Row(\n                            mainAxisSize: MainAxisSize.min,\n                            children: [\n                              ElevatedButton.icon(\n                                onPressed: () =>\n                                    server.simulateExternalChange(true),\n                                icon: const Icon(Icons.favorite, size: 16),\n                                label: const Text('Liked'),\n                                style: ElevatedButton.styleFrom(\n                                  backgroundColor: Colors.red.shade100,\n                                  foregroundColor: Colors.red.shade900,\n                                ),\n                              ),\n                              const SizedBox(width: 16),\n                              ElevatedButton.icon(\n                                onPressed: () =>\n                                    server.simulateExternalChange(false),\n                                icon:\n                                    const Icon(Icons.favorite_border, size: 16),\n                                label: const Text('Not Liked'),\n                                style: ElevatedButton.styleFrom(\n                                  backgroundColor: Colors.grey.shade200,\n                                  foregroundColor: Colors.grey.shade700,\n                                ),\n                              ),\n                            ],\n                          ),\n                          const SizedBox(height: 8),\n                          ElevatedButton.icon(\n                            onPressed: server.isRequestInProgress\n                                ? () {\n                                    server.shouldFail = true;\n                                  }\n                                : null,\n                            icon: const Icon(Icons.error_outline, size: 16),\n                            label: const Text('Request fails'),\n                            style: ElevatedButton.styleFrom(\n                              backgroundColor: Colors.orange.shade100,\n                              foregroundColor: Colors.orange.shade900,\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nabstract class AppAction extends ReduxAction<AppState> {}\n\n// ////////////////////////////////////////////////////////////////////////////\n\n/// Singleton instance of the simulated server.\nfinal server = SimulatedServer();\n\n/// Simulates a remote server with database and request handling.\n/// All server-side state and behavior is encapsulated here to clearly separate\n/// it from the local app state managed by Redux.\nclass SimulatedServer {\n  // ---------------------------------------------------------------------------\n  // Server State\n  // ---------------------------------------------------------------------------\n\n  /// The \"database\" value stored on the server.\n  bool databaseLiked = false;\n\n  /// Whether a request is currently being processed.\n  bool isRequestInProgress = false;\n\n  /// Total number of requests received by the server.\n  int requestCount = 0;\n\n  /// When true, the next request will fail (for testing error handling).\n  bool shouldFail = false;\n\n  /// Simulated network delay before writing to database (ms).\n  int delayBeforeWrite = 1500;\n\n  /// Simulated network delay after writing to database (ms).\n  int delayAfterWrite = 2000;\n\n  // ---------------------------------------------------------------------------\n  // Server Methods\n  // ---------------------------------------------------------------------------\n\n  /// Simulates saving to the database.\n  /// Returns the current database value after save completes.\n  Future<bool> saveLike(bool flag) async {\n    requestCount++;\n    isRequestInProgress = true;\n    await _interruptibleDelay(delayBeforeWrite);\n\n    databaseLiked = flag;\n    await _interruptibleDelay(delayAfterWrite);\n    isRequestInProgress = false;\n\n    // Return the current value in the database.\n    // This may differ from the saved value, simulating server-side logic.\n    return databaseLiked;\n  }\n\n  /// Simulates reloading the current value from the database.\n  Future<bool> reload() async {\n    await Future.delayed(const Duration(milliseconds: 500));\n    return databaseLiked;\n  }\n\n  /// Simulates an external change to the database (e.g., from another client).\n  void simulateExternalChange(bool liked) {\n    databaseLiked = liked;\n  }\n\n  /// Interruptible delay that checks [shouldFail] every 50ms.\n  /// Allows simulating mid-flight request failures.\n  Future<void> _interruptibleDelay(int milliseconds) async {\n    const checkInterval = 50;\n    int remaining = milliseconds;\n    while (remaining > 0) {\n      if (shouldFail) {\n        shouldFail = false;\n        isRequestInProgress = false;\n        throw Exception('Simulated server error');\n      }\n      final wait = remaining < checkInterval ? remaining : checkInterval;\n      await Future.delayed(Duration(milliseconds: wait));\n      remaining -= checkInterval;\n    }\n  }\n}\n"
  },
  {
    "path": "example/lib/main_optimistic_sync_with_push.dart",
    "content": "/// This example is meant to demonstrate the [OptimisticSyncWithPush] mixin in\n/// action. The screen is split into two halves: the top shows the UI state\n/// (Redux), and the bottom shows the simulated database state (server).\n///\n/// ## Use cases to try:\n///\n/// ### 1. Optimistic update\n/// Tap the heart icon. The UI updates instantly (top half), while the database\n/// takes ~3.5 seconds to update (bottom half shows \"Saving...\").\n///\n/// ### 2. Coalescing\n/// Tap the heart rapidly multiple times while \"Saving...\" is displayed. Notice:\n/// - The UI toggles instantly on each tap (always responsive).\n/// - Only one request is in flight at a time (\"Saving 1...\").\n/// - When the request completes, if the current UI state differs from what was\n///   sent, a follow-up request is automatically sent (\"Saving 2...\").\n///\n/// ### 3. Push updates (key feature)\n/// With \"Push database changes\" switch ON (default), tap \"Liked\" or \"Not Liked\"\n/// buttons to simulate an external change from another device. The UI updates\n/// immediately via the simulated WebSocket push. This is the key difference\n/// from [OptimisticSync], which doesn't support push.\n///\n/// ### 4. Push disabled behavior\n/// Turn OFF the \"Push database changes\" switch, then tap \"Liked\" or \"Not Liked\".\n/// The database changes but the UI doesn't update (no push). The UI only syncs\n/// when you tap the heart again.\n///\n/// ### 5. Push during in-flight request\n/// With push ON, tap the heart to start saving. While \"Saving...\" is displayed,\n/// tap \"Liked\" or \"Not Liked\" to simulate an external change. Notice how the\n/// mixin handles the race condition using revision tracking, ensuring eventual\n/// consistency.\n///\n/// ### 6. Reload on error\n/// Tap the heart to start saving. While \"Saving...\" is displayed, tap \"Request\n/// fails\". The UI keeps its optimistic state, but [OptimisticSyncWithPush.onFinish]\n/// is called with the error. In this example, we reload from the database\n/// to restore the correct state.\n///\n/// ### 7. Persistence\n/// Close and restart the app. The last known state is persisted using\n/// shared_preferences (see class [MyPersistor] below) and restored on startup.\n/// When using PUSH, we must persist the server revision as well to ensure\n/// correct operation across app restarts.\n///\n/// Note: If you DO NOT use push, try mixins [OptimisticSync] or\n/// [OptimisticCommand] instead. They are much easier to implement since they\n/// don't require revision tracking.\n///\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:math';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:fast_immutable_collections/fast_immutable_collections.dart';\nimport 'package:flutter/material.dart';\nimport 'package:meta/meta.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nlate Store<AppState> store;\nlate MyPersistor persistor;\n\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  // Create the persistor.\n  persistor = MyPersistor();\n\n  // Load persisted state.\n  var initialState = await persistor.readState();\n\n  // If no persisted state exists, create the default initial state and save it.\n  if (initialState == null) {\n    initialState = AppState(liked: false);\n    await persistor.saveInitialState(initialState);\n  }\n\n  // Initialize the server SIMULATION, by setting the like and revision counter.\n  // In production, this would be the real server, using a database.\n  // The key is (ToggleLike, null) as computed by computeOptimisticSyncKey().\n  server.revisionCounter = initialState.getServerRevision((ToggleLike, null));\n  server.databaseLiked = initialState.liked;\n\n  store = Store<AppState>(\n    initialState: initialState,\n    actionObservers: [ConsoleActionObserver()],\n    persistor: persistor,\n  );\n  runApp(const MyApp());\n}\n\nclass AppState {\n  final bool liked;\n\n  /// Stores the last known server revision for each [OptimisticSyncWithPush]\n  /// action. Keys are stringified versions of action keys (e.g.,\n  /// \"(ToggleLike, null)\"). It's persisted with [MyPersistor] to maintain\n  /// correct operation across app restarts. The mixin uses these revisions to\n  /// detect stale push updates and ensure eventual consistency.\n  final IMap<String, int> serverRevisionMap;\n\n  AppState({required this.liked, IMap<String, int>? serverRevisionMap})\n      : serverRevisionMap = serverRevisionMap ?? const IMapConst({});\n\n  @useResult\n  AppState copy({bool? isLiked, IMap<String, int>? serverRevisionMap}) =>\n      AppState(\n        liked: isLiked ?? this.liked,\n        serverRevisionMap: serverRevisionMap ?? this.serverRevisionMap,\n      );\n\n  /// Returns a copy of the state with the server revision updated for the given key.\n  @useResult\n  AppState withServerRevision(Object? key, int revision) => copy(\n        serverRevisionMap: serverRevisionMap.add(\n          _keyToString(key),\n          revision,\n        ),\n      );\n\n  /// Returns the server revision for the given key, or -1 if not found.\n  int getServerRevision(Object? key) =>\n      serverRevisionMap.get(_keyToString(key)) ?? -1;\n\n  Map<String, dynamic> toJson() => {\n        'liked': liked,\n        'serverRevisionMap': serverRevisionMap.unlock,\n      };\n\n  factory AppState.fromJson(Map<String, dynamic> json) => AppState(\n        liked: json['liked'] as bool? ?? false,\n        serverRevisionMap: IMap<String, int>.fromEntries(\n          (json['serverRevisionMap'] as Map<String, dynamic>? ?? {})\n              .entries\n              .map((e) => MapEntry(e.key, e.value as int)),\n        ),\n      );\n\n  @override\n  String toString() =>\n      'AppState(liked: $liked, serverRevisionMap: $serverRevisionMap)';\n}\n\n/// Converts an action key to a String for persistence.\n/// The key is typically the runtimeType of the action, or a custom identifier for keyed actions.\nString _keyToString(Object? key) => key?.toString() ?? '_default_';\n\n/// Persistor that saves AppState to shared_preferences.\nclass MyPersistor extends Persistor<AppState> {\n  static const _key = 'app_state';\n\n  @override\n  Future<AppState?> readState() async {\n    final prefs = await SharedPreferences.getInstance();\n    final jsonString = prefs.getString(_key);\n    if (jsonString == null) return null;\n    try {\n      final json = jsonDecode(jsonString) as Map<String, dynamic>;\n      print('Loaded AppState from prefs: $json');\n      return AppState.fromJson(json);\n    } catch (e) {\n      return null;\n    }\n  }\n\n  @override\n  Future<void> deleteState() async {\n    final prefs = await SharedPreferences.getInstance();\n    await prefs.remove(_key);\n  }\n\n  @override\n  Future<void> persistDifference({\n    required AppState? lastPersistedState,\n    required AppState newState,\n  }) async {\n    final prefs = await SharedPreferences.getInstance();\n    final json = jsonEncode(newState.toJson());\n    await prefs.setString(_key, json);\n  }\n\n  /// Short throttle for this demo to save changes quickly.\n  @override\n  Duration? get throttle => const Duration(milliseconds: 300);\n}\n\n/// Represents the server's response including the revision number.\nclass ServerResponse {\n  final bool liked;\n  final int serverRevision;\n  final int localRevision;\n  final int deviceId;\n\n  ServerResponse({\n    required this.liked,\n    required this.serverRevision,\n    required this.localRevision,\n    required this.deviceId,\n  });\n}\n\n/// ServerPush action for handling WebSocket push updates.\n/// This action properly integrates with [OptimisticSyncWithPush].\nclass PushLikeUpdate extends AppAction with ServerPush {\n  final bool liked;\n  final int serverRev;\n  final int localRev;\n  final int deviceId;\n\n  PushLikeUpdate({\n    required this.liked,\n    required this.serverRev,\n    required this.localRev,\n    required this.deviceId,\n  });\n\n  /// Return the Type of the associated OptimisticSyncWithPush action.\n  @override\n  Type associatedAction() => ToggleLike;\n\n  @override\n  PushMetadata pushMetadata() {\n    print('Incoming metadata: ${(\n      serverRevision: serverRev,\n      localRevision: localRev,\n      deviceId: deviceId,\n    )}');\n\n    return (\n      serverRevision: serverRev,\n      localRevision: localRev,\n      deviceId: deviceId,\n    );\n  }\n\n  /// Apply the pushed data to state and save the revision.\n  @override\n  AppState? applyServerPushToState(\n    AppState state,\n    Object? key,\n    int serverRevision,\n  ) =>\n      state.copy(isLiked: liked).withServerRevision(key, serverRevision);\n\n  /// Return the current server revision from state for this key.\n  @override\n  int getServerRevisionFromState(Object? key) => state.getServerRevision(key);\n\n  @override\n  String toString() =>\n      '${super.toString()}(liked: $liked, serverRev: $serverRev)';\n}\n\nclass ToggleLike extends AppAction with OptimisticSyncWithPush<AppState, bool> {\n  // Store the server revision from the response.\n  int _serverRevFromResponse = 0;\n\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(\n    AppState state,\n    bool optimisticValueToApply,\n  ) =>\n      state.copy(isLiked: optimisticValueToApply);\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object? serverResponse) {\n    // Apply both the liked value and the server revision.\n    // Use computeOptimisticSyncKey() to get the same key used by the mixin.\n    return state\n        .copy(isLiked: serverResponse as bool)\n        .withServerRevision(computeOptimisticSyncKey(), _serverRevFromResponse);\n  }\n\n  @override\n  Future<bool> sendValueToServer(\n    Object? optimisticValue,\n    int localRevision,\n    int deviceId,\n  ) async {\n    print('Sending to server: $optimisticValue');\n    // Send to server and get response with revision.\n    final response = await server.saveLike(\n      optimisticValue as bool,\n      localRevision,\n      deviceId,\n    );\n\n    // Store the server revision for use in applyServerResponseToState.\n    print('Server response: $response');\n\n    // Inform the mixin about the server revision.\n    informServerRevision(response.serverRevision);\n\n    return response.liked;\n  }\n\n  /// Return the current server revision from state.\n  @override\n  int getServerRevisionFromState(Object? key) => state.getServerRevision(key);\n\n  // If there was an error, revert the state to the database value.\n  @override\n  Future<AppState?> onFinish(Object? error) async {\n    if (error == null) return null;\n\n    // If there was an error, reload the value from the database.\n    bool isLiked = await server.reload();\n    return state.copy(isLiked: isLiked);\n  }\n\n  @override\n  String toString() => '${super.toString()}(${!state.liked})';\n}\n\n/// Resets all state: deletes persisted state and resets server simulation.\nclass ResetAllState extends AppAction {\n  @override\n  Future<AppState?> reduce() async {\n    // Delete persisted state.\n    await persistor.deleteState();\n\n    // Reset server simulation.\n    server.reset();\n\n    // Return fresh initial state.\n    return AppState(liked: false);\n  }\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        debugShowCheckedModeBanner: false,\n        title: 'OptimisticSyncWithPush Mixin Demo',\n        theme: ThemeData(primarySwatch: Colors.blue),\n        home: const MyHomePage(),\n      ),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n  const MyHomePage({super.key});\n\n  @override\n  State<MyHomePage> createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  late Timer _timer;\n\n  @override\n  void initState() {\n    super.initState();\n    // Refresh the UI periodically to show the database state.\n    _timer = Timer.periodic(const Duration(milliseconds: 100), (_) {\n      setState(() {});\n    });\n  }\n\n  @override\n  void dispose() {\n    _timer.cancel();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('OptimisticSyncWithPush Mixin Demo'),\n        actions: [\n          IconButton(\n            icon: const Icon(Icons.delete_outline, size: 20),\n            tooltip: 'Reset all state',\n            onPressed: () => store.dispatch(ResetAllState()),\n          ),\n        ],\n      ),\n      body: Column(\n        children: [\n          // Top half: Like button (Redux state)\n          Expanded(\n            child: Container(\n              color: Colors.blue.shade50,\n              child: Center(\n                child: StoreConnector<AppState, bool>(\n                  converter: (store) => store.state.liked,\n                  builder: (context, liked) {\n                    return Column(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        const Text(\n                          'UI State (AsyncRedux)',\n                          style: TextStyle(\n                            fontSize: 18,\n                            fontWeight: FontWeight.bold,\n                          ),\n                        ),\n                        const SizedBox(height: 20),\n                        IconButton(\n                          iconSize: 80,\n                          icon: Icon(\n                            liked ? Icons.favorite : Icons.favorite_border,\n                            color: liked ? Colors.red : Colors.grey,\n                          ),\n                          onPressed: () {\n                            store.dispatch(ToggleLike());\n                          },\n                        ),\n                        const SizedBox(height: 10),\n                        Text(\n                          liked ? 'Liked' : 'Not Liked',\n                          style: const TextStyle(fontSize: 24),\n                        ),\n                        const SizedBox(height: 20),\n                        const Text(\n                          'Tap rapidly to see coalescing in action!',\n                          style: TextStyle(\n                            fontSize: 14,\n                            color: Colors.grey,\n                          ),\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            ),\n          ),\n          // Divider\n          Container(\n            height: 2,\n            color: Colors.grey.shade400,\n          ),\n          // Bottom half: Database state\n          Expanded(\n            child: Container(\n              color: Colors.green.shade50,\n              child: Center(\n                child: Column(\n                  mainAxisAlignment: MainAxisAlignment.center,\n                  children: [\n                    const Text(\n                      'Database State (Simulated)',\n                      style: TextStyle(\n                        fontSize: 18,\n                        fontWeight: FontWeight.bold,\n                      ),\n                    ),\n                    const SizedBox(height: 20),\n                    Icon(\n                      server.databaseLiked\n                          ? Icons.favorite\n                          : Icons.favorite_border,\n                      size: 80,\n                      color: server.databaseLiked ? Colors.red : Colors.grey,\n                    ),\n                    const SizedBox(height: 10),\n                    Text(\n                      server.databaseLiked ? 'Liked' : 'Not Liked',\n                      style: const TextStyle(fontSize: 24),\n                    ),\n                    const SizedBox(height: 20),\n                    Row(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        Text(\n                          server.isRequestInProgress\n                              ? 'Saving ${server.requestCount}...'\n                              : 'Idle',\n                          style: TextStyle(\n                            fontSize: 16,\n                            color: server.isRequestInProgress\n                                ? Colors.orange\n                                : Colors.grey,\n                            fontWeight: server.isRequestInProgress\n                                ? FontWeight.bold\n                                : FontWeight.normal,\n                          ),\n                        ),\n                        const SizedBox(width: 10),\n                        if (server.isRequestInProgress)\n                          const SizedBox(\n                            width: 16,\n                            height: 16,\n                            child: CircularProgressIndicator(\n                              strokeWidth: 2,\n                              color: Colors.orange,\n                            ),\n                          ),\n                      ],\n                    ),\n                    const SizedBox(height: 10),\n                    Text(\n                      'Updates after server round-trip (${(server.delayBeforeWrite + server.delayAfterWrite) / 1000}s)',\n                      style: TextStyle(\n                        fontSize: 14,\n                        color: Colors.grey,\n                      ),\n                    ),\n                    const SizedBox(height: 10),\n                    Text(\n                      'Number of requests received: ${server.requestCount}',\n                      style: TextStyle(\n                        fontSize: 14,\n                        color: Colors.grey,\n                      ),\n                    ),\n                    const SizedBox(height: 30),\n                    Container(\n                      padding: const EdgeInsets.all(16),\n                      decoration: BoxDecoration(\n                        border: Border.all(color: Colors.grey.shade400),\n                        borderRadius: BorderRadius.circular(8),\n                      ),\n                      child: Column(\n                        children: [\n                          const Text(\n                            'Simulate external change to the database:',\n                            style: TextStyle(fontSize: 14, color: Colors.grey),\n                          ),\n                          const SizedBox(height: 8),\n                          Row(\n                            mainAxisSize: MainAxisSize.min,\n                            children: [\n                              ElevatedButton.icon(\n                                onPressed: () =>\n                                    server.simulateExternalChange(true),\n                                icon: const Icon(Icons.favorite, size: 16),\n                                label: const Text('Liked'),\n                                style: ElevatedButton.styleFrom(\n                                  backgroundColor: Colors.red.shade100,\n                                  foregroundColor: Colors.red.shade900,\n                                ),\n                              ),\n                              const SizedBox(width: 16),\n                              ElevatedButton.icon(\n                                onPressed: () =>\n                                    server.simulateExternalChange(false),\n                                icon:\n                                    const Icon(Icons.favorite_border, size: 16),\n                                label: const Text('Not Liked'),\n                                style: ElevatedButton.styleFrom(\n                                  backgroundColor: Colors.grey.shade200,\n                                  foregroundColor: Colors.grey.shade700,\n                                ),\n                              ),\n                            ],\n                          ),\n                          const SizedBox(height: 8),\n                          ElevatedButton.icon(\n                            onPressed: server.isRequestInProgress\n                                ? () {\n                                    server.shouldFail = true;\n                                  }\n                                : null,\n                            icon: const Icon(Icons.error_outline, size: 16),\n                            label: const Text('Request fails'),\n                            style: ElevatedButton.styleFrom(\n                              backgroundColor: Colors.orange.shade100,\n                              foregroundColor: Colors.orange.shade900,\n                            ),\n                          ),\n                          const SizedBox(height: 12),\n                          Row(\n                            mainAxisSize: MainAxisSize.min,\n                            children: [\n                              const Text(\n                                'Push database changes',\n                                style: TextStyle(fontSize: 14),\n                              ),\n                              const SizedBox(width: 8),\n                              Switch(\n                                value: server.websocketPushEnabled,\n                                onChanged: (value) {\n                                  setState(() {\n                                    server.websocketPushEnabled = value;\n                                  });\n                                },\n                              ),\n                            ],\n                          ),\n                        ],\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nabstract class AppAction extends ReduxAction<AppState> {}\n\n// ////////////////////////////////////////////////////////////////////////////\n\n/// Singleton instance of the simulated server.\nfinal server = SimulatedServer();\n\n/// Simulates a remote server with database, WebSocket push, and request handling.\n/// All server-side state and behavior is encapsulated here to clearly separate\n/// it from the local app state managed by Redux.\nclass SimulatedServer {\n  // ---------------------------------------------------------------------------\n  // Server State\n  // ---------------------------------------------------------------------------\n\n  /// The \"database\" value stored on the server.\n  bool databaseLiked = false;\n\n  /// Whether a request is currently being processed.\n  bool isRequestInProgress = false;\n\n  /// Total number of requests received by the server.\n  int requestCount = 0;\n\n  /// When true, the next request will fail (for testing error handling).\n  bool shouldFail = false;\n\n  /// Whether the server should push changes via \"WebSocket\" after writes.\n  bool websocketPushEnabled = true;\n\n  /// Server-side revision counter. Incremented on each successful write.\n  /// In production, this would be managed by the actual server/database.\n  int revisionCounter = 0;\n\n  /// Simulated network delay before writing to database (ms).\n  int delayBeforeWrite = 1500;\n\n  /// Simulated network delay after writing to database (ms).\n  int delayAfterWrite = 2000;\n\n  // ---------------------------------------------------------------------------\n  // Server Methods\n  // ---------------------------------------------------------------------------\n\n  /// Simulates saving to the database.\n  /// Returns a [ServerResponse] with the current liked value and server revision.\n  Future<ServerResponse> saveLike(\n    bool flag,\n    int localRevision,\n    int deviceId,\n  ) async {\n    print('Save started');\n    requestCount++;\n    isRequestInProgress = true;\n    print('flag = $flag, localRev = $localRevision, deviceId = $deviceId');\n    await _interruptibleDelay(delayBeforeWrite);\n\n    // Save flag and increment server revision (simulate server-side versioning).\n    databaseLiked = flag;\n    revisionCounter++;\n    final currentServerRev = revisionCounter;\n\n    print(\n        'flag = $flag, serverRev = $currentServerRev, localRev = $localRevision, deviceId = $deviceId');\n    if (websocketPushEnabled)\n      push(\n        isLiked: flag,\n        serverRev: currentServerRev,\n        localRev: localRevision,\n        deviceId: deviceId,\n      );\n\n    await _interruptibleDelay(delayAfterWrite);\n    isRequestInProgress = false;\n    print('flag = $flag, serverRev = $currentServerRev');\n    print('Save ended');\n\n    return ServerResponse(\n      liked: databaseLiked,\n      serverRevision: currentServerRev,\n      localRevision: localRevision,\n      deviceId: deviceId,\n    );\n  }\n\n  /// Simulates reloading the current value from the database.\n  Future<bool> reload() async {\n    await Future.delayed(const Duration(milliseconds: 300));\n    return databaseLiked;\n  }\n\n  /// Simulates a WebSocket push from the server to the client.\n  Future<void> push({\n    required bool isLiked,\n    required int serverRev,\n    required int localRev,\n    required int deviceId,\n  }) async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    store.dispatch(PushLikeUpdate(\n      liked: isLiked,\n      serverRev: serverRev,\n      localRev: localRev,\n      deviceId: deviceId,\n    ));\n  }\n\n  /// Simulates an external change to the database (e.g., from another client).\n  void simulateExternalChange(bool liked) {\n    databaseLiked = liked;\n    if (websocketPushEnabled) {\n      revisionCounter++;\n      push(\n        isLiked: databaseLiked,\n        serverRev: revisionCounter,\n        localRev: Random().nextInt(4294967296),\n        deviceId: Random().nextInt(4294967296),\n      );\n    }\n  }\n\n  /// Resets the server to its initial state.\n  void reset() {\n    databaseLiked = false;\n    isRequestInProgress = false;\n    requestCount = 0;\n    shouldFail = false;\n    revisionCounter = 0;\n  }\n\n  /// Interruptible delay that checks [shouldFail] every 50ms.\n  /// Allows simulating mid-flight request failures.\n  Future<void> _interruptibleDelay(int milliseconds) async {\n    const checkInterval = 50;\n    int remaining = milliseconds;\n    while (remaining > 0) {\n      if (shouldFail) {\n        shouldFail = false;\n        isRequestInProgress = false;\n        throw Exception('Simulated server error');\n      }\n      final wait = remaining < checkInterval ? remaining : checkInterval;\n      await Future.delayed(Duration(milliseconds: wait));\n      remaining -= checkInterval;\n    }\n  }\n}\n"
  },
  {
    "path": "example/lib/main_polling.dart",
    "content": "import 'dart:math';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example demonstrates the [Polling] mixin with all [Poll] enum values:\n///\n/// - [Poll.start] — Starts polling and runs reduce immediately. If polling is\n///   already active, does nothing.\n///\n/// - [Poll.stop] — Cancels the timer and skips reduce.\n///\n/// - [Poll.runNowAndRestart] — Runs reduce immediately and restarts the timer\n///   from that moment. If polling is not active, behaves like [Poll.start].\n///\n/// - [Poll.once] — Runs reduce immediately without starting, stopping, or\n///   restarting polling.\n///\n/// The app simulates polling a stock price every 3 seconds. Four buttons\n/// demonstrate each [Poll] value, and the UI shows the current price,\n/// how many times it has been fetched, and whether polling is active.\n///\nvoid main() {\n  store = Store<AppState>(initialState: AppState.initialState());\n  runApp(MyApp());\n}\n\n// =============================================================================\n// State\n// =============================================================================\n\n@immutable\nclass AppState {\n  final double price;\n  final int fetchCount;\n  final bool isPolling;\n\n  AppState({\n    required this.price,\n    required this.fetchCount,\n    required this.isPolling,\n  });\n\n  AppState copy({double? price, int? fetchCount, bool? isPolling}) => AppState(\n        price: price ?? this.price,\n        fetchCount: fetchCount ?? this.fetchCount,\n        isPolling: isPolling ?? this.isPolling,\n      );\n\n  static AppState initialState() => AppState(\n        price: 100.0,\n        fetchCount: 0,\n        isPolling: false,\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          price == other.price &&\n          fetchCount == other.fetchCount &&\n          isPolling == other.isPolling;\n\n  @override\n  int get hashCode => price.hashCode ^ fetchCount.hashCode ^ isPolling.hashCode;\n}\n\n// =============================================================================\n// App\n// =============================================================================\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(\n          home: MyHomePage(),\n        ),\n      );\n}\n\n// =============================================================================\n// Actions\n// =============================================================================\n\n/// A polling action that simulates fetching a stock price.\n///\n/// This uses \"Option 1\" (single action for everything): the same action class\n/// both controls polling and does the work. The [createPollingAction] returns\n/// the same action type with [Poll.once], so timer ticks run reduce without\n/// restarting the timer.\nclass PollStockPriceAction extends ReduxAction<AppState> with Polling {\n  @override\n  final Poll poll;\n\n  PollStockPriceAction({this.poll = Poll.once});\n\n  @override\n  Duration get pollInterval => const Duration(seconds: 3);\n\n  @override\n  ReduxAction<AppState> createPollingAction() => PollStockPriceAction(poll: Poll.once);\n\n  /// Update the [isPolling] flag in state whenever the polling status changes.\n  @override\n  void before() {\n    switch (poll) {\n      case Poll.start:\n      case Poll.runNowAndRestart:\n        dispatch(SetPollingFlagAction(true));\n      case Poll.stop:\n        dispatch(SetPollingFlagAction(false));\n      case Poll.once:\n        break;\n    }\n  }\n\n  @override\n  AppState reduce() {\n    // Simulate a price change: random walk around the current price.\n    final random = Random();\n    final change = (random.nextDouble() - 0.5) * 4; // -2.0 to +2.0\n    final newPrice = (state.price + change).clamp(50.0, 200.0);\n\n    return state.copy(\n      price: double.parse(newPrice.toStringAsFixed(2)),\n      fetchCount: state.fetchCount + 1,\n    );\n  }\n}\n\n/// Marks polling as active or inactive in the state (so the UI can reflect it).\nclass SetPollingFlagAction extends ReduxAction<AppState> {\n  final bool isPolling;\n\n  SetPollingFlagAction(this.isPolling);\n\n  @override\n  AppState reduce() => state.copy(isPolling: isPolling);\n}\n\n// =============================================================================\n// Home page\n// =============================================================================\n\nclass MyHomePage extends StatelessWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    var price = context.select((AppState s) => s.price);\n    var fetchCount = context.select((AppState s) => s.fetchCount);\n    var isPolling = context.select((AppState s) => s.isPolling);\n\n    return Scaffold(\n      appBar: AppBar(title: const Text('Polling Mixin Example')),\n      body: Padding(\n        padding: const EdgeInsets.all(24),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.stretch,\n          children: [\n            // Price display\n            Card(\n              child: Padding(\n                padding: const EdgeInsets.all(24),\n                child: Column(\n                  children: [\n                    const Text('Stock Price',\n                        style: TextStyle(fontSize: 16, color: Colors.grey)),\n                    const SizedBox(height: 8),\n                    Text(\n                      '\\$${price.toStringAsFixed(2)}',\n                      style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),\n                    ),\n                    const SizedBox(height: 8),\n                    Text('Fetched $fetchCount time${fetchCount == 1 ? '' : 's'}'),\n                    const SizedBox(height: 8),\n                    Row(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        Icon(\n                          isPolling ? Icons.sync : Icons.sync_disabled,\n                          color: isPolling ? Colors.green : Colors.grey,\n                        ),\n                        const SizedBox(width: 8),\n                        Text(\n                          isPolling ? 'Polling active (every 3s)' : 'Polling inactive',\n                          style: TextStyle(color: isPolling ? Colors.green : Colors.grey),\n                        ),\n                      ],\n                    ),\n                  ],\n                ),\n              ),\n            ),\n\n            const SizedBox(height: 24),\n            const Text('Poll values:',\n                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),\n            const SizedBox(height: 12),\n\n            // Poll.start\n            ElevatedButton.icon(\n              icon: const Icon(Icons.play_arrow),\n              label: const Text('Poll.start'),\n              style: ElevatedButton.styleFrom(backgroundColor: Colors.green),\n              onPressed: () => dispatch(PollStockPriceAction(poll: Poll.start)),\n            ),\n            const Padding(\n              padding: EdgeInsets.only(left: 16, bottom: 12, top: 4),\n              child: Text(\n                'Starts polling and runs reduce immediately. '\n                'If polling is already active, does nothing.',\n                style: TextStyle(fontSize: 12, color: Colors.grey),\n              ),\n            ),\n\n            // Poll.stop\n            ElevatedButton.icon(\n              icon: const Icon(Icons.stop),\n              label: const Text('Poll.stop'),\n              style: ElevatedButton.styleFrom(backgroundColor: Colors.red),\n              onPressed: () => dispatch(PollStockPriceAction(poll: Poll.stop)),\n            ),\n            const Padding(\n              padding: EdgeInsets.only(left: 16, bottom: 12, top: 4),\n              child: Text(\n                'Cancels the timer and skips reduce.',\n                style: TextStyle(fontSize: 12, color: Colors.grey),\n              ),\n            ),\n\n            // Poll.runNowAndRestart\n            ElevatedButton.icon(\n              icon: const Icon(Icons.refresh),\n              label: const Text('Poll.runNowAndRestart'),\n              style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),\n              onPressed: () =>\n                  dispatch(PollStockPriceAction(poll: Poll.runNowAndRestart)),\n            ),\n            const Padding(\n              padding: EdgeInsets.only(left: 16, bottom: 12, top: 4),\n              child: Text(\n                'Runs reduce immediately and restarts the timer from now. '\n                'If not active, behaves like Poll.start.',\n                style: TextStyle(fontSize: 12, color: Colors.grey),\n              ),\n            ),\n\n            // Poll.once\n            ElevatedButton.icon(\n              icon: const Icon(Icons.looks_one),\n              label: const Text('Poll.once'),\n              style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),\n              onPressed: () => dispatch(PollStockPriceAction(poll: Poll.once)),\n            ),\n            const Padding(\n              padding: EdgeInsets.only(left: 16, bottom: 12, top: 4),\n              child: Text(\n                'Runs reduce immediately without starting, stopping, '\n                'or restarting polling.',\n                style: TextStyle(fontSize: 12, color: Colors.grey),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\n// =============================================================================\n// BuildContext extension\n// =============================================================================\n\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/main_select.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example shows a counter, a text character, and a button.\n/// When the button is tapped, the counter will increment synchronously,\n/// while an async process downloads some text character that relates\n/// to the counter number (using the Star Wars API: https://swapi.dev).\n///\n/// If there is no internet connection, it will display a dialog to the\n/// user, saying: \"There is no Internet\". This is implemented with mixin\n/// `CheckInternet` added to action `IncrementAndGetDescriptionAction`,\n/// and a `UserExceptionDialog` added below `MaterialApp`.\n///\n/// Open the console to see when each widget rebuilds. Here are the 4 widgets:\n///\n/// 1. MyHomePage (red): rebuilds only during the initial build.\n///\n/// 2. CounterWidget (blue): rebuilds when you press the `+` button.\n///\n/// 3. DescriptionWidget (yellow): rebuilds only when the character loads.\n///\n/// 4. LoadingStatusWidget (grey): rebuilds when [IncrementAndGetDescriptionAction]\n///    is dispatched, and when it finishes (either successfully or with error).\n///\n/// It should start like this:\n///\n/// ```\n/// Restarted application in 271ms.\n/// 🔴 MyHomePage rebuilt\n/// 🔵 CounterWidget rebuilt\n/// 💛 DescriptionWidget rebuilt\n/// 🍏 LoadingStatusWidget rebuilt\n/// 🔴 MyHomePage rebuilt\n/// 🔵 CounterWidget rebuilt\n/// 💛 DescriptionWidget rebuilt\n/// 🍏 LoadingStatusWidget rebuilt\n/// ```\n///\n/// When you press the `+` button, you should immediately see these extra lines:\n/// ```\n/// 🍏 LoadingStatusWidget rebuilt\n/// 🔵 CounterWidget rebuilt\n/// ```\n///\n/// And then, a moment later, when the character loads:\n///\n/// ```\n/// 🍏 LoadingStatusWidget rebuilt\n/// 💛 DescriptionWidget rebuilt\n/// ```\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state, which in this case is a counter and a character.\n@immutable\nclass AppState {\n  final int counter;\n  final String character;\n\n  AppState({\n    required this.counter,\n    required this.character,\n  });\n\n  AppState copy({int? counter, String? character}) => AppState(\n        counter: counter ?? this.counter,\n        character: character ?? this.character,\n      );\n\n  static AppState initialState() => AppState(counter: 0, character: \"\");\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          counter == other.counter &&\n          character == other.character;\n\n  @override\n  int get hashCode => counter.hashCode ^ character.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(\n          home: UserExceptionDialog<AppState>(\n            child: MyHomePage(),\n          ),\n        ),\n      );\n}\n\n/// This action increments the counter by 1,\n/// and then gets some character text relating to the new counter number.\nclass IncrementAndGetDescriptionAction extends ReduxAction<AppState>\n    with CheckInternet {\n  //\n  // Async reducer.\n  // To make it async we simply return Future<AppState> instead of AppState.\n  @override\n  Future<AppState> reduce() async {\n    // First, we increment the counter, synchronously.\n    dispatch(IncrementAction(amount: 1));\n\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/${state.counter}/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String character = json['name'] ?? 'Unknown character';\n\n    // After we get the response, we can modify the state with it,\n    // without having to dispatch another action.\n    return state.copy(character: character);\n  }\n\n  @override\n  Object? wrapError(error, StackTrace stackTrace) {\n    print('Error in IncrementAndGetDescriptionAction: $error');\n\n    return (error is UserException)\n        ? error\n        : const UserException('Failed to load.');\n  }\n}\n\n/// This action increments the counter by [amount]].\nclass IncrementAction extends ReduxAction<AppState> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  // Synchronous reducer.\n  @override\n  AppState reduce() => state.copy(counter: state.counter + amount);\n}\n\n/// This is a \"smart-widget\" that directly accesses the store to dispatch actions.\n/// It uses extracted widgets (CounterWidget and DescriptionWidget) that each\n/// independently select their own state and rebuild only when needed.\nclass MyHomePage extends StatelessWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    print('🔴 MyHomePage rebuilt');\n\n    return Scaffold(\n      appBar: AppBar(title: const Text('Star Wars Character Example')),\n      body: const Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            CounterWidget(),\n            DescriptionWidget(),\n            LoadingStatusWidget(),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        // Dispatch action directly from widget\n        onPressed: () => dispatch(IncrementAndGetDescriptionAction()),\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n\n/// Widget that selects and displays ONLY the counter.\n/// Rebuilds ONLY when the counter changes, not when character changes.\nclass CounterWidget extends StatelessWidget {\n  const CounterWidget({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    print('🔵 CounterWidget rebuilt');\n\n    // Select only counter. Rebuilds only when counter changes.\n    final counter = context.select((st) => st.counter);\n\n    return Column(\n      children: [\n        const Text('Star Wars character for counter:'),\n        Text('$counter', style: const TextStyle(fontSize: 30)),\n      ],\n    );\n  }\n}\n\n/// Widget that selects and displays ONLY the character.\n/// Rebuilds ONLY when the character changes, not when counter changes.\nclass DescriptionWidget extends StatelessWidget {\n  const DescriptionWidget({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    print('💛 DescriptionWidget rebuilt');\n\n    return Text(\n      context.select((st) => st.character),\n      style: const TextStyle(fontSize: 15, color: Colors.black),\n      textAlign: TextAlign.center,\n    );\n  }\n}\n\n/// Widget that selects and displays ONLY the character.\n/// Rebuilds ONLY when the character changes, not when counter changes.\nclass LoadingStatusWidget extends StatelessWidget {\n  const LoadingStatusWidget({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    print('🍏 LoadingStatusWidget rebuilt');\n\n    bool isWaiting = context.isWaiting(IncrementAndGetDescriptionAction);\n    bool isFailed = context.isFailed(IncrementAndGetDescriptionAction);\n\n    return Text(\n      isFailed\n          ? 'Error loading character!'\n          : isWaiting\n              ? 'Loading character...'\n              : '',\n      style: const TextStyle(fontSize: 15, color: Colors.grey),\n      textAlign: TextAlign.center,\n    );\n  }\n}\n\n/// Recommended to create this extension.\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/main_select_2.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Recommended to create this extension.\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n  AppState read() => getRead<AppState>();\n  R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n}\n\nvoid main() {\n  final store = Store<AppState>(initialState: AppState.initialState());\n  runApp(MyApp(store: store));\n}\n\nclass MyApp extends StatelessWidget {\n  final Store<AppState> store;\n\n  const MyApp({Key? key, required this.store}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        debugShowCheckedModeBanner: false,\n        title: 'Select Demo',\n        theme: ThemeData(primarySwatch: Colors.blue),\n        home: const MainScreen(),\n      ),\n    );\n  }\n}\n\nclass MainScreen extends StatelessWidget {\n  const MainScreen({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('AsyncRedux Select Demo'),\n      ),\n      body: Padding(\n        padding: const EdgeInsets.all(16.0),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.stretch,\n          children: [\n            // Display widgets\n            const Card(\n              child: Padding(\n                padding: EdgeInsets.all(16.0),\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(\n                      'Widget States:',\n                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),\n                    ),\n                    SizedBox(height: 16),\n                    ContextStateWidget(),\n                    SizedBox(height: 8),\n                    ContextReadWidget(),\n                    SizedBox(height: 8),\n                    SelectDateWidget(),\n                    SizedBox(height: 8),\n                    SelectFlagWidget(),\n                  ],\n                ),\n              ),\n            ),\n            const SizedBox(height: 24),\n\n            // Control buttons\n            const Text(\n              'Actions:',\n              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),\n            ),\n            const SizedBox(height: 16),\n\n            ElevatedButton.icon(\n              onPressed: () => context.dispatch(IncrementNumberAction()),\n              icon: const Icon(Icons.add),\n              label: const Text('Increment Number'),\n            ),\n            const SizedBox(height: 8),\n\n            ElevatedButton.icon(\n              onPressed: () => context.dispatch(AddXToTextAction()),\n              icon: const Icon(Icons.text_fields),\n              label: const Text('Add X to Text'),\n            ),\n            const SizedBox(height: 8),\n\n            ElevatedButton.icon(\n              onPressed: () => context.dispatch(AddDayToDateAction()),\n              icon: const Icon(Icons.calendar_today),\n              label: const Text('Add Day to Date'),\n            ),\n            const SizedBox(height: 8),\n\n            ElevatedButton.icon(\n              onPressed: () => context.dispatch(ToggleFlagAction()),\n              icon: const Icon(Icons.flag),\n              label: const Text('Toggle Flag'),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\n/// WIDGET 1: Uses `context.state` which uses `getState<AppState>()`.\n/// This widget rebuilds on ANY state change.\n///\nclass ContextStateWidget extends StatelessWidget {\n  const ContextStateWidget({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    print('🔴 ContextStateWidget rebuilt');\n\n    // Will rebuild automatically on ANY state changes.\n    var state = context.state;\n\n    return Container(\n      padding: const EdgeInsets.all(12),\n      decoration: BoxDecoration(\n        color: Colors.red.shade50,\n        borderRadius: BorderRadius.circular(8),\n        border: Border.all(color: Colors.red.shade200),\n      ),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const Text(\n            '1. getState (notify: true)',\n            style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red),\n          ),\n          Text('Number: ${state.number}'),\n          Text('Text: ${state.text}'),\n          Text('Date: ${state.date.toString().split(' ')[0]}'),\n          Text('Flag: ${state.flag}'),\n          const Text(\n            'Rebuilds on ANY change',\n            style: TextStyle(fontSize: 11, fontStyle: FontStyle.italic),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\n/// WIDGET 2: Uses `context.read()` which uses `getRead<AppState>()`.\n/// This widget does NOT rebuild on ANY state change.\n///\nclass ContextReadWidget extends StatelessWidget {\n  const ContextReadWidget({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    print('🟡 ContextReadWidget rebuilt');\n\n    // It will NEVER rebuild automatically on state changes.\n    final state = context.read();\n\n    return Container(\n      padding: const EdgeInsets.all(12),\n      decoration: BoxDecoration(\n        color: Colors.yellow.shade50,\n        borderRadius: BorderRadius.circular(8),\n        border: Border.all(color: Colors.yellow.shade700),\n      ),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const Text(\n            '2. getState (notify: false)',\n            style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orange),\n          ),\n          Text('Number: ${state.number}'),\n          Text('Text: ${state.text}'),\n          Text('Date: ${state.date.toString().split(' ')[0]}'),\n          Text('Flag: ${state.flag}'),\n          const Text(\n            'Never rebuilds (shows initial state)',\n            style: TextStyle(fontSize: 11, fontStyle: FontStyle.italic),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\n/// WIDGET 3: Uses `context.select()` which uses `getSelect<AppState, R>()`.\n/// This widget rebuilds ONLY when the selected part of the state changes.\n///\nclass SelectDateWidget extends StatelessWidget {\n  const SelectDateWidget({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    print('🟢 SelectDateWidget rebuilt');\n\n    // Will rebuild automatically ONLY when `state.date` changes.\n    // The return type (DateTime) is automatically inferred!\n    final date = context.select((st) => st.date);\n\n    return Container(\n      padding: const EdgeInsets.all(12),\n      decoration: BoxDecoration(\n        color: Colors.green.shade50,\n        borderRadius: BorderRadius.circular(8),\n        border: Border.all(color: Colors.green.shade400),\n      ),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const Text(\n            '3. select (date only)',\n            style: TextStyle(fontWeight: FontWeight.bold, color: Colors.green),\n          ),\n          Text('Date: ${date.toString().split(' ')[0]}'),\n          const Text(\n            'Only rebuilds when date changes',\n            style: TextStyle(fontSize: 11, fontStyle: FontStyle.italic),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\n/// WIDGET 3: Uses `context.select()` which uses `getSelect<AppState, R>()`.\n/// This widget rebuilds ONLY when the selected part of the state changes.\n///\nclass SelectFlagWidget extends StatelessWidget {\n  const SelectFlagWidget({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    print('🔵 SelectFlagWidget rebuilt');\n\n    // Will rebuild automatically ONLY when `state.flag` changes.\n    // The return type (bool) is automatically inferred!\n    final flag = context.select((st) => st.flag);\n\n    return Container(\n      padding: const EdgeInsets.all(12),\n      decoration: BoxDecoration(\n        color: Colors.blue.shade50,\n        borderRadius: BorderRadius.circular(8),\n        border: Border.all(color: Colors.blue.shade400),\n      ),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const Text(\n            '4. select (flag only)',\n            style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue),\n          ),\n          Text('Flag: $flag'),\n          const Text(\n            'Only rebuilds when flag changes',\n            style: TextStyle(fontSize: 11, fontStyle: FontStyle.italic),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass AppState {\n  final int number;\n  final String text;\n  final DateTime date;\n  final bool flag;\n\n  AppState({\n    required this.number,\n    required this.text,\n    required this.date,\n    required this.flag,\n  });\n\n  static AppState initialState() => AppState(\n    number: 0,\n    text: 'Hello',\n    date: DateTime(2024, 1, 1),\n    flag: false,\n  );\n\n  AppState copyWith({\n    int? number,\n    String? text,\n    DateTime? date,\n    bool? flag,\n  }) {\n    return AppState(\n      number: number ?? this.number,\n      text: text ?? this.text,\n      date: date ?? this.date,\n      flag: flag ?? this.flag,\n    );\n  }\n\n  @override\n  String toString() => 'AppState(number: $number, text: $text, date: $date, flag: $flag)';\n}\n\nclass IncrementNumberAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(number: state.number + 1);\n  }\n}\n\nclass AddXToTextAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(text: state.text + 'X');\n  }\n}\n\nclass AddDayToDateAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(date: state.date.add(const Duration(days: 1)));\n  }\n}\n\nclass ToggleFlagAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(flag: !state.flag);\n  }\n}\n\n////////////////////////////////////////////////////////////////////////////////\n\n// USAGE NOTES\n\n/*\nThis example shows the difference between various state access methods:\n\n1. **ContextStateWidget** (Red):\n   - Uses: `context.state` (via extension)\n   - Rebuilds on ANY state change\n   - Shows all state values\n   - Watch the console: prints on every action\n\n2. **ContextReadWidget** (Yellow):\n   - Uses: `context.read()` (via extension)\n   - NEVER rebuilds automatically\n   - Always shows initial state values\n   - Only prints once during initial build\n\n3. **SelectDateWidget** (Green):\n   - Uses: `context.select((state) => state.date)` (via extension, type inferred)\n   - Only rebuilds when date changes\n   - Only shows date value\n   - Watch the console: only prints when \"Add Day\" is pressed\n\n4. **SelectFlagWidget** (Blue):\n   - Uses: `context.select((state) => state.flag)` (via extension, type inferred)\n   - Only rebuilds when flag changes\n   - Only shows flag value\n   - Watch the console: only prints when \"Toggle Flag\" is pressed\n\nRun the app and watch the console output to see which widgets rebuild!\n\nExpected behavior:\n- Press \"Increment Number\": Only red widget rebuilds\n- Press \"Add X to Text\": Only red widget rebuilds\n- Press \"Add Day to Date\": Red and green widgets rebuild\n- Press \"Toggle Flag\": Red and blue widgets rebuild\n*/\n"
  },
  {
    "path": "example/lib/main_show_error_dialog.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\nlate Store<AppState> store;\n\n/// This example lets you enter a name and click save.\n/// If the name has less than 4 chars, an error dialog will be shown.\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state, which in this case is the user name.\n@immutable\nclass AppState {\n  final String? name;\n\n  AppState({this.name});\n\n  AppState copy({String? name}) => AppState(name: name ?? this.name);\n\n  static AppState initialState() => AppState(name: \"\");\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          name == other.name;\n\n  @override\n  int get hashCode => name.hashCode;\n}\n\n/// To display errors, put the [UserExceptionDialog] below [StoreProvider] and [MaterialApp].\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(\n          home: UserExceptionDialog<AppState>(\n            child: MyHomePage(),\n          ),\n        ),\n      );\n}\n\nclass SaveUserAction extends ReduxAction<AppState> {\n  final String name;\n\n  SaveUserAction(this.name);\n\n  @override\n  AppState reduce() {\n    print(\"Saving '$name'.\");\n\n    if (name.length < 4)\n      throw const UserException(\"Name needs 4 letters or more.\",\n          errorText: 'At least 4 letters.');\n\n    return state.copy(name: name);\n  }\n\n  @override\n  Object wrapError(error, stackTrace) => //\n      const UserException(\"Save failed\")\n          .addCause(error)\n          .addCallbacks(onOk: () => print(\"Dialog was dismissed.\"));\n// Note we could also have a CANCEL button here:\n// .addCallbacks(onOk: ..., onCancel: () => print(\"CANCEL pressed, or dialog dismissed.\"));\n}\n\nclass MyHomePage extends StatefulWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  _MyHomePageState createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  TextEditingController? controller;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = TextEditingController();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // Use context.select to get the name from the state\n    var name = context.select((AppState state) => state.name);\n\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(title: const Text('Show Error Dialog Example')),\n          body: Center(\n            child: Padding(\n              padding: const EdgeInsets.all(12.0),\n              child: Column(\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [\n                  const Text(\n                      'Type a name and save:\\n(See error if less than 4 chars)',\n                      textAlign: TextAlign.center),\n                  //\n                  TextField(\n                    controller: controller,\n                    onChanged: (text) {\n                      // This is optional, as the exception is already cleared when the\n                      // action dispatches again. Comment it out to see the difference.\n                      if (text.length >= 4) context.clearExceptionFor(SaveUserAction);\n                    },\n                    onSubmitted: (String text) =>\n                        dispatch(SaveUserAction(text)),\n                  ),\n                  const SizedBox(height: 30),\n                  //\n                  // If the save failed, show the error message in red text.\n                  if (context.isFailed(SaveUserAction))\n                    Text(\n                      context.exceptionFor(SaveUserAction)?.errorText ?? '',\n                      style: const TextStyle(color: Colors.red),\n                    ),\n                  //\n                  Text('Current Name: $name'),\n                ],\n              ),\n            ),\n          ),\n          floatingActionButton: FloatingActionButton(\n            onPressed: () => dispatch(SaveUserAction(controller!.text)),\n            child: const Text(\"Save\"),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/main_show_spinner.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\nimport 'dart:async';\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\nlate Store<AppState> store;\n\n/// This example shows a counter and a button.\n/// When the button is tapped, the counter will increment asynchronously.\nvoid main() {\n  store = Store<AppState>(initialState: AppState(counter: 0, something: 0));\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: const MaterialApp(home: HomePage()),\n      );\n}\n\nclass HomePage extends StatelessWidget {\n  const HomePage({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Show Spinner Example')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('You have pushed the button this many times:'),\n            CounterWidget(),\n          ],\n        ),\n      ),\n      // Here we disable the button while the `WaitAndIncrementAction` action is running.\n      floatingActionButton: context.isWaiting(WaitAndIncrementAction)\n          ? const FloatingActionButton(\n              disabledElevation: 0,\n              onPressed: null,\n              child: SizedBox(width: 25, height: 25, child: CircularProgressIndicator()))\n          : FloatingActionButton(\n              disabledElevation: 0,\n              onPressed: () => dispatch(WaitAndIncrementAction()),\n              child: const Icon(Icons.add),\n            ),\n    );\n  }\n}\n\n/// This action waits for 2 seconds, then increments the counter by 1.\nclass WaitAndIncrementAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await Future.delayed(const Duration(seconds: 2));\n    return AppState(\n      counter: state.counter + 1,\n      something: state.something,\n    );\n  }\n}\n\nclass CounterWidget extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    var _isWaiting = context.isWaiting(WaitAndIncrementAction);\n\n    return Text(\n      '${context.state.counter}',\n      style: TextStyle(fontSize: 40, color: _isWaiting ? Colors.grey[350] : Colors.black),\n    );\n  }\n}\n\nextension _BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n}\n\nclass AppState {\n  int counter;\n  int something;\n\n  AppState({\n    required this.counter,\n    required this.something,\n  });\n\n  @override\n  String toString() => 'AppState{counter: $counter}';\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState && runtimeType == other.runtimeType && counter == other.counter;\n\n  @override\n  int get hashCode => counter.hashCode;\n}\n"
  },
  {
    "path": "example/lib/main_wait_action_advanced_1.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example shows how to use [WaitAction] in advanced ways.\n/// For this to work, the [AppState] must have a [wait] field of type [Wait],\n/// and this field must be in the [AppState.copy] method as a named parameter.\n///\n/// 10 buttons are shown. When a button is clicked it will be\n/// replaced by a downloaded text description. Each button shows a progress\n/// indicator while its description is downloading. The screen title shows\n/// the text \"Downloading...\" if any of the buttons is currently downloading.\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state contains a [wait] object of type [Wait].\n@immutable\nclass AppState {\n  final Map<int, String> descriptions;\n  final Wait wait;\n\n  AppState({required this.descriptions, required this.wait});\n\n  /// The copy method has a named [wait] parameter of type [Wait].\n  AppState copy({int? counter, Map<int, String>? descriptions, Wait? wait}) =>\n      AppState(\n        descriptions: descriptions ?? this.descriptions,\n        wait: wait ?? this.wait,\n      );\n\n  /// The [wait] parameter is instantiated to `Wait()`.\n  static AppState initialState() => AppState(\n        descriptions: {},\n        wait: Wait(),\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          descriptions == other.descriptions &&\n          wait == other.wait;\n\n  @override\n  int get hashCode => descriptions.hashCode ^ wait.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePage(),\n      ));\n}\n\nclass GetDescriptionAction extends ReduxAction<AppState> {\n  int index;\n\n  GetDescriptionAction(this.index);\n\n  @override\n  Future<AppState> reduce() async {\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/$index/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String description = json['name'] ?? 'Unknown character';\n\n    await Future.delayed(const Duration(seconds: 2)); // Adds some more delay.\n\n    Map<int, String> newDescriptions = Map.of(state.descriptions);\n    newDescriptions[index] = description;\n\n    return state.copy(descriptions: newDescriptions);\n  }\n\n  // The wait starts here. We use the index as a wait-flag reference.\n  @override\n  void before() => dispatch(WaitAction.add(index));\n\n  // The wait ends here. We remove the index from the wait-flag references.\n  @override\n  void after() => dispatch(WaitAction.remove(index));\n}\n\nclass MyItem extends StatelessWidget {\n  final int index;\n\n  MyItem({\n    required this.index,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    // Use context.select to get the description and waiting state for this specific index\n    var description =\n        context.select((AppState state) => state.descriptions[index] ?? \"\");\n\n    /// If index is waiting, `state.wait.isWaiting(index)` returns true.\n    var waiting =\n        context.select((AppState state) => state.wait.isWaiting(index));\n\n    Widget contents;\n\n    if (waiting)\n      contents = _progressIndicator();\n    else if (description.isNotEmpty)\n      contents = _indexDescription(description);\n    else\n      contents = _button(context);\n\n    return Container(height: 70, child: Center(child: contents));\n  }\n\n  MaterialButton _button(BuildContext context) => MaterialButton(\n        color: Colors.blue,\n        child: Text(\"CLICK $index\",\n            style: const TextStyle(fontSize: 15), textAlign: TextAlign.center),\n        onPressed: () => dispatch(GetDescriptionAction(index)),\n      );\n\n  Text _indexDescription(String description) => Text(description,\n      style: const TextStyle(fontSize: 15), textAlign: TextAlign.center);\n\n  CircularProgressIndicator _progressIndicator() =>\n      const CircularProgressIndicator(\n        valueColor: AlwaysStoppedAnimation<Color>(Colors.red),\n      );\n}\n\nclass MyHomePage extends StatelessWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    /// If there is any waiting, `state.wait.isWaitingAny` will return true.\n    var waiting = context.select((AppState state) => state.wait.isWaitingAny);\n\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(\n              title: Text(waiting\n                  ? \"Downloading...\"\n                  : \"Advanced WaitAction Example 1\")),\n          body: ListView.builder(\n            itemCount: 10,\n            itemBuilder: (context, index) => MyItem(index: index),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/main_wait_action_advanced_2.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example is the same as the one in `main_wait_action_advanced_1.dart`.\n/// However, instead of only using flags in the [WaitAction], it uses both\n/// flags and references.\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state contains a [wait] object of type [Wait].\n@immutable\nclass AppState {\n  final Map<int, String> descriptions;\n  final Wait wait;\n\n  AppState({\n    required this.descriptions,\n    required this.wait,\n  });\n\n  /// The copy method has a named [wait] parameter of type [Wait].\n  AppState copy({int? counter, Map<int, String>? descriptions, Wait? wait}) =>\n      AppState(\n        descriptions: descriptions ?? this.descriptions,\n        wait: wait ?? this.wait,\n      );\n\n  /// The [wait] parameter is instantiated to `Wait()`.\n  static AppState initialState() => AppState(\n        descriptions: {},\n        wait: Wait(),\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          descriptions == other.descriptions &&\n          wait == other.wait;\n\n  @override\n  int get hashCode => descriptions.hashCode ^ wait.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePage(),\n      ));\n}\n\nclass GetDescriptionAction extends ReduxAction<AppState> {\n  int index;\n\n  GetDescriptionAction(this.index);\n\n  @override\n  Future<AppState> reduce() async {\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/$index/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String description = json['name'] ?? 'Unknown character';\n\n    await Future.delayed(const Duration(seconds: 2)); // Adds some more delay.\n\n    Map<int, String> newDescriptions = Map.of(state.descriptions);\n    newDescriptions[index] = description;\n\n    return state.copy(descriptions: newDescriptions);\n  }\n\n  // The wait starts here. We use the index as a wait-flag reference.\n  @override\n  void before() => dispatch(WaitAction.add(\"button-download\", ref: index));\n\n  // The wait ends here. We remove the index from the wait-flag references.\n  @override\n  void after() => dispatch(WaitAction.remove(\"button-download\", ref: index));\n}\n\nclass MyItem extends StatelessWidget {\n  final int index;\n\n  MyItem({\n    required this.index,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    // Use context.select to get the description and waiting state for this specific index\n    var description =\n        context.select((AppState state) => state.descriptions[index] ?? \"\");\n\n    /// If index is waiting, `state.wait.isWaiting(\"button-download\", ref: index)` returns true.\n    var waiting = context.select((AppState state) =>\n        state.wait.isWaiting(\"button-download\", ref: index));\n\n    Widget contents;\n\n    if (waiting)\n      contents = _progressIndicator();\n    else if (description.isNotEmpty)\n      contents = _indexDescription(description);\n    else\n      contents = _button(context);\n\n    return Container(height: 70, child: Center(child: contents));\n  }\n\n  MaterialButton _button(BuildContext context) => MaterialButton(\n        color: Colors.blue,\n        child: Text(\"CLICK $index\",\n            style: const TextStyle(fontSize: 15), textAlign: TextAlign.center),\n        onPressed: () => dispatch(GetDescriptionAction(index)),\n      );\n\n  Text _indexDescription(String description) => Text(description,\n      style: const TextStyle(fontSize: 15), textAlign: TextAlign.center);\n\n  CircularProgressIndicator _progressIndicator() =>\n      const CircularProgressIndicator(\n        valueColor: AlwaysStoppedAnimation<Color>(Colors.red),\n      );\n}\n\nclass MyHomePage extends StatelessWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    /// If there is any waiting, `state.wait.isWaitingAny` will return true.\n    var waiting = context.select((AppState state) => state.wait.isWaitingAny);\n\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(\n              title: Text(waiting\n                  ? \"Downloading...\"\n                  : \"Advanced WaitAction Example 2\")),\n          body: ListView.builder(\n            itemCount: 10,\n            itemBuilder: (context, index) => MyItem(index: index),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/main_wait_action_simple.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example is the same as the one in `main_before_and_after.dart`.\n/// However, instead of declaring a `MyWaitAction`, it uses the build-in\n/// [WaitAction].\n///\n/// For this to work, the [AppState] must have a [wait] field of type [Wait],\n/// and this field must be in the [AppState.copy] method as a named parameter.\n///\n/// While the async process is running, the action's `before` method will\n/// add the action itself as a wait-flag reference:\n///\n/// ```\n/// void before() => dispatch(WaitAction.add(this));\n/// ```\n///\n/// The [ViewModel] will read this info from `state.wait.isWaitingAny` to\n/// turn on the modal barrier.\n///\n/// When the async process finishes, the action's before method will\n/// remove the action from the wait-flag set:\n///\n/// ```\n/// void after() => dispatch(WaitAction.remove(this));\n/// ```\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state contains a [wait] object of type [Wait].\n@immutable\nclass AppState {\n  final int counter;\n  final String description;\n  final Wait wait;\n\n  AppState({\n    required this.counter,\n    required this.description,\n    required this.wait,\n  });\n\n  /// The copy method has a named [wait] parameter of type [Wait].\n  AppState copy({int? counter, String? description, Wait? wait}) => AppState(\n        counter: counter ?? this.counter,\n        description: description ?? this.description,\n        wait: wait ?? this.wait,\n      );\n\n  /// The [wait] parameter is instantiated to `Wait()`.\n  static AppState initialState() => AppState(\n        counter: 0,\n        description: \"\",\n        wait: Wait(),\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          counter == other.counter &&\n          description == other.description &&\n          wait == other.wait;\n\n  @override\n  int get hashCode => counter.hashCode ^ description.hashCode ^ wait.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(home: MyHomePage()),\n      );\n}\n\n/// Use it like this:\n/// `class MyAction extends ReduxAction<AppState> with WithWaitState`\nmixin WithWaitState implements ReduxAction<AppState> {\n  // Wait starts here. Add the action itself (`this`) as a wait-flag reference.\n  @override\n  void before() => dispatch(WaitAction.add(this));\n\n  // Wait ends here. Remove the action from the wait-flag references.\n  @override\n  void after() => dispatch(WaitAction.remove(this));\n}\n\nclass IncrementAndGetDescriptionAction extends ReduxAction<AppState>\n    with WithWaitState {\n  @override\n  Future<AppState> reduce() async {\n    dispatch(IncrementAction(amount: 1));\n\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/${state.counter}/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String description = json['name'] ?? 'Unknown character';\n\n    return state.copy(description: description);\n  }\n}\n\nclass IncrementAction extends ReduxAction<AppState> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  @override\n  AppState reduce() => state.copy(counter: state.counter + amount);\n}\n\nclass MyHomePage extends StatelessWidget {\n  MyHomePage({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    // Use context.select to get state values\n    var counter = context.select((AppState state) => state.counter);\n    var description = context.select((AppState state) => state.description);\n\n    /// While action `IncrementAndGetDescriptionAction` is running,\n    /// [isWaiting] will be true.\n    var isWaiting = context.select((AppState state) =>\n        state.wait.isWaitingForType<IncrementAndGetDescriptionAction>());\n\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(title: const Text('Wait Action Example')),\n          body: Center(\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                const Text('You have pushed the button this many times:'),\n                Text('$counter', style: const TextStyle(fontSize: 30)),\n                Text(description,\n                    style: const TextStyle(fontSize: 15),\n                    textAlign: TextAlign.center),\n              ],\n            ),\n          ),\n          floatingActionButton: FloatingActionButton(\n            onPressed: () => dispatch(IncrementAndGetDescriptionAction()),\n            child: const Icon(Icons.add),\n          ),\n        ),\n        if (isWaiting) ModalBarrier(color: Colors.red.withAlpha((255.0 * 0.4).round())),\n      ],\n    );\n  }\n}\n\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/README.md",
    "content": "# StoreConnector Examples\n\nThis directory contains examples of how to use the `StoreConnector` widget from\nthe AsyncRedux package.\n\nThe `StoreConnector` widget is used to connect your Flutter widgets to the\nRedux store, allowing them to access the state and dispatch actions.\n\nIt's generally not necessary to use the `StoreConnector` widget directly, as\n`AsyncRedux` allows you to use the extensions `context.state`,\n`context.select()`, `context.read()`, `context.dispatch()`, etc.\n\nHowever, the `StoreConnector` allows you to completely separate the\npresentation layer from the business logic, including the selection of\nthe part of the state that the widget needs. This can make your code\nmore modular and easier to maintain.\n\nWhen should you use the `StoreConnector`?\n\n* When you want to create a reusable widget that is not coupled to AsyncRedux\n  and the Redux store.\n\n* When you want to test the presentation layer of your app in isolation, without\n  needing to set up the Redux store.\n\n* When the selection of the state is complex, and you want to encapsulate it in\n  a separate class (ViewModel).\n\nA good rule of thumb is to start with using `context.state`, `context.select()`,\n`context.dispatch()`, etc. and only switch to using the `StoreConnector` when\nyou find a specific need for it.\n\n## Code examples:\n\nThe code below uses \"context extensions\" directly in the widget\n(no smart/dumb widget separation):\n\n```dart\n// Dumb widget (Uses Context extensions)\nclass MyHomePageContent extends StatelessWidget {\n  ...\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        Text('Counter: ${context.select((AppState state) => state.counter)}'),\n        if (context.isWaiting([IncrementAction, MultiplyAction])) CircularProgressIndicator(),\n        Row(\n          children: [\n            ElevatedButton(\n              onPressed: () => context.dispatch(IncrementAction()),\n              child: Text('Increment')\n            ),\n            ElevatedButton(\n              onPressed: () => context.dispatch(MultiplyAction()),\n              child: Text('Multiply')\n            ),\n          ],\n        ),\n      ],\n    );\n```\n\nThe code below is equivalent.\nIt uses \"context extensions\" and also smart/dumb widget separation:\n\n```dart\n// Smart widget (Uses Context extensions)\nreturn MyHomePageContent(\n   title: 'IsWaiting multiple actions',\n   counter: context.select((state) => state.counter),\n   isCalculating: context.isWaiting([IncrementAction, MultiplyAction]),\n   increment: () => context.dispatch(IncrementAction()),\n   multiply: () => context.dispatch(MultiplyAction()),\n);\n\n// Dumb widget (no direct Redux usage)\nclass MyHomePageContent extends StatelessWidget {\n  ...\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        Text('Counter: $counter'),\n        if (isCalculating) CircularProgressIndicator(),\n        Row(          \n          children: [\n            ElevatedButton(onPressed: increment, child: Text('Increment')),            \n            ElevatedButton(onPressed: multiply, child: Text('Multiply')),\n            ...\n```\n\nThe code below is equivalent.\nIt uses StoreConnector, Factory, View Model,\nand also smart/dumb widget separation:\n\n```\n// Smart widget (Uses StoreConnector/Factory/Vm)\nWidget build(BuildContext context) {\n  return StoreConnector<AppState, CounterVm>(\n    vm: () => CounterVmFactory(), // Here, uses the factory defined below.\n    shouldUpdateModel: (s) => s.counter >= 0,\n    builder: (context, vm) {\n      return MyHomePageContent(\n        title: 'IsWaiting multiple actions (Store Connector)',\n        counter: vm.counter,\n        isCalculating: vm.isCalculating,\n        increment: vm.increment,\n        multiply: vm.multiply,\n      );\n    },\n  );\n} \n\nclass CounterVmFactory extends VmFactory<AppState, MyHomePage, CounterVm> {\n  CounterVm fromStore() => CounterVm( // Here, uses the view model defined below.\n    counter: state.counter,\n    isCalculating: isWaiting([IncrementAction, MultiplyAction]),\n    increment: () => dispatch(IncrementAction()),\n    multiply: () => dispatch(MultiplyAction()),\n  );\n}\n\nclass CounterVm extends Vm {\n  final int counter;\n  final bool isCalculating;\n  final VoidCallback increment, multiply;\n\n  CounterVm({\n    required this.counter,\n    required this.isCalculating,\n    required this.increment,\n    required this.multiply,\n  }) : super(equals: [counter, isCalculating]);\n}\n\n// Dumb widget (no direct Redux usage)\nclass MyHomePageContent extends StatelessWidget {\n  ...\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        Text('Counter: $counter'),\n        if (isCalculating) CircularProgressIndicator(),\n        Row(          \n          children: [\n            ElevatedButton(onPressed: increment, child: Text('Increment')),            \n            ElevatedButton(onPressed: multiply, child: Text('Multiply')),\n            ...  \n```\n\n## The difference between the 3 approaches\n\n### Approach 1: Context Extensions (No Separation)\n\nUses context extensions directly in the widget without any smart/dumb widget\nseparation. All logic and presentation are in one place.\n\n### Approach 2: Context Extensions (With Smart/Dumb Separation)\n\nUses context extensions in a \"smart\" container widget that passes data and\ncallbacks to a \"dumb\" presentational widget.\n\n### Approach 3: StoreConnector with VmFactory\n\nUses StoreConnector, VmFactory, and ViewModels with smart/dumb widget separation\nfor maximum decoupling.\n\n## Key Differences:\n\n### 1. Boilerplate & Complexity\n\n- **Approach 1 (Direct Context Extensions)**: Minimal boilerplate, everything\n  inline. Simplest to write and understand initially.\n- **Approach 2 (Context Extensions + Separation)**: Moderate boilerplate,\n  requires defining props for the dumb widget.\n- **Approach 3 (StoreConnector)**: Most boilerplate, requires 3 additional\n  classes (`CounterVm`, `CounterVmFactory`, and using `StoreConnector`).\n\n### 2. Where Business Logic Lives\n\n- **Approach 1**: Business logic (selectors, dispatches) mixed directly with UI\n  code in the build method.\n- **Approach 2**: Business logic in the smart widget, but still uses context\n  extensions directly.\n- **Approach 3**: Business logic fully encapsulated in `VmFactory.fromStore()`\n  with no direct Redux dependencies in widgets.\n\n### 3. Separation of Concerns\n\n- **Approach 1**: No separation - Redux awareness and UI are completely\n  intertwined.\n- **Approach 2**: Partial separation - UI is isolated in dumb widget, but smart\n  widget still directly uses Redux.\n- **Approach 3**: Full separation - Complete decoupling through ViewModel\n  abstraction.\n\n### 4. Reusability\n\n- **Approach 1**: Widget is tightly coupled to Redux store structure. Hard to\n  reuse or test without full Redux setup.\n- **Approach 2**: Dumb widget is reusable with any data source. Smart widget\n  still tied to Redux.\n- **Approach 3**: Both dumb widget and ViewModel pattern are highly reusable.\n  VmFactory can be shared across multiple widgets.\n\n### 5. Testing Strategy\n\n- **Approach 1**: Requires full Redux store setup to test. Cannot test UI in\n  isolation.\n- **Approach 2**: Can test dumb widget with simple props. Smart widget still\n  needs Redux for testing.\n- **Approach 3**: Can test dumb widget with props, and separately unit test\n  VmFactory business logic without UI.\n\n### 6. Refactoring & Maintenance\n\n- **Approach 1**: Changes to store structure require updates throughout the\n  widget. Hard to track all dependencies.\n- **Approach 2**: Store changes only affect smart widget. Dumb widget remains\n  stable.\n- **Approach 3**: Store changes isolated to VmFactory. Both widgets and\n  ViewModel interface can remain stable.\n\n## Recommendations:\n\n### Use Approach 1 (Direct Context Extensions) when:\n\n- You're building simple widgets or prototypes\n- The widget is used in only one place\n- Testing the full widget with Redux is acceptable\n- You want minimal boilerplate and fastest development\n\n### Use Approach 2 (Context Extensions + Smart/Dumb) when:\n\n- You want better testability without full ViewModel complexity\n- The UI component might be reused with different data\n- You prefer a balance between simplicity and separation\n- Your team is familiar with Redux but wants cleaner components\n\n### Use Approach 3 (StoreConnector/VmFactory) when:\n\n- You have complex business logic requiring isolated testing\n- Multiple widgets need the same state transformations\n- You want complete decoupling and maximum testability\n- You're building large, team-based applications\n- You need to enforce consistent architectural patterns\n\n### General Guidelines:\n\nThe \"dumb widget\" pattern (used in Approaches 2 & 3) is valuable because:\n\n1. It makes widgets easily testable without store\n2. It makes the UI reusable with different data sources\n3. It clearly shows the widget's API (what data it needs)\n\nStart with Approach 1 for simplicity, then refactor to Approach 2 or 3 as your\nneeds grow. The transition path is natural:\n\n- **1 → 2**: Extract props to create a dumb widget\n- **2 → 3**: Replace context extensions with StoreConnector and VmFactory\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_async__store_connector.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example demonstrates:\n/// - The use of [StoreConnector], [VmFactory], and [ViewModel].\n/// - Doing async work inside an action.\n///\n/// It shows a counter, a text description, and a button.\n/// When the button is tapped, the counter will increment synchronously,\n/// while an async process downloads some text description that relates\n/// to the counter number (using the Star Wars API: https://swapi.dev).\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state, which in this case is a counter and a description.\n@immutable\nclass AppState {\n  final int counter;\n  final String description;\n\n  AppState({\n    required this.counter,\n    required this.description,\n  });\n\n  AppState copy({int? counter, String? description}) => AppState(\n        counter: counter ?? this.counter,\n        description: description ?? this.description,\n      );\n\n  static AppState initialState() => AppState(counter: 0, description: \"\");\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          counter == other.counter &&\n          description == other.description;\n\n  @override\n  int get hashCode => counter.hashCode ^ description.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePageConnector(),\n      ));\n}\n\n/// This action increments the counter by 1,\n/// and then gets some description text relating to the new counter number.\nclass IncrementAndGetDescriptionAction extends ReduxAction<AppState> {\n  //\n  // Async reducer.\n  // To make it async we simply return Future<AppState> instead of AppState.\n  @override\n  Future<AppState> reduce() async {\n    // First, we increment the counter, synchronously.\n    dispatch(IncrementAction(amount: 1));\n\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/${state.counter}/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String description = json['name'] ?? 'Unknown character';\n\n    // After we get the response, we can modify the state with it,\n    // without having to dispatch another action.\n    return state.copy(description: description);\n  }\n}\n\n/// This action increments the counter by [amount]].\nclass IncrementAction extends ReduxAction<AppState> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  // Synchronous reducer.\n  @override\n  AppState reduce() => state.copy(counter: state.counter + amount);\n}\n\n/// This widget is a connector. It connects the store to \"dumb-widget\".\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel>(\n      vm: () => Factory(this),\n      builder: (BuildContext context, ViewModel vm) => MyHomePage(\n        counter: vm.counter,\n        description: vm.description,\n        onIncrement: vm.onIncrement,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass Factory extends VmFactory<AppState, MyHomePageConnector, ViewModel> {\n  Factory(connector) : super(connector);\n\n  @override\n  ViewModel fromStore() => ViewModel(\n        counter: state.counter,\n        description: state.description,\n        onIncrement: _onIncrement,\n      );\n\n  void _onIncrement() {\n    dispatch(IncrementAndGetDescriptionAction());\n\n    print('Counter in the the view-model = ${vm.counter}');\n    print(\n        'Counter in the state when the view-model was created = ${state.counter}');\n    print('Counter in the current state = ${currentState().counter}');\n  }\n}\n\n/// The view-model holds the part of the Store state the dumb-widget needs.\nclass ViewModel extends Vm {\n  final int counter;\n  final String description;\n  final VoidCallback onIncrement;\n\n  ViewModel({\n    required this.counter,\n    required this.description,\n    required this.onIncrement,\n  }) : super(equals: [counter, description]);\n}\n\nclass MyHomePage extends StatelessWidget {\n  final int? counter;\n  final String? description;\n  final VoidCallback? onIncrement;\n\n  MyHomePage({\n    Key? key,\n    this.counter,\n    this.description,\n    this.onIncrement,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Increment Example (StoreConnector)')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('You have pushed the button this many times:'),\n            Text('$counter', style: const TextStyle(fontSize: 30)),\n            Text(description!,\n                style: const TextStyle(fontSize: 15),\n                textAlign: TextAlign.center),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: onIncrement,\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_async_base_factory__store_connector.dart.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example demonstrates:\n/// - The use of [StoreConnector], [VmFactory], and [ViewModel].\n/// - Doing async work inside an action.\n/// - How to create a [BaseFactory] to reduce code duplication. Once you add\n///   this class to your code, it knows your state is [AppState], and you can\n///   avoid repeating that in all your factories. For example, instead of writing\n///   `VmFactory<AppState, T, Model>`, you can simply write\n///   `BaseVmFactory<T, Model>`.\n///\n/// It shows a counter, a text description, and a button.\n/// When the button is tapped, the counter will increment synchronously,\n/// while an async process downloads some text description that relates\n/// to the counter number (using the Star Wars API: https://swapi.dev).\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state, which in this case is a counter and a description.\n@immutable\nclass AppState {\n  final int counter;\n  final String description;\n\n  AppState({\n    required this.counter,\n    required this.description,\n  });\n\n  AppState copy({int? counter, String? description}) => AppState(\n        counter: counter ?? this.counter,\n        description: description ?? this.description,\n      );\n\n  static AppState initialState() => AppState(counter: 0, description: \"\");\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          counter == other.counter &&\n          description == other.description;\n\n  @override\n  int get hashCode => counter.hashCode ^ description.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePageConnector(),\n      ));\n}\n\n/// This action increments the counter by 1,\n/// and then gets some description text relating to the new counter number.\nclass IncrementAndGetDescriptionAction extends ReduxAction<AppState> {\n  //\n  // Async reducer.\n  // To make it async we simply return Future<AppState> instead of AppState.\n  @override\n  Future<AppState> reduce() async {\n    // First, we increment the counter, synchronously.\n    dispatch(IncrementAction(amount: 1));\n\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/${state.counter}/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String description = json['name'] ?? 'Unknown character';\n\n    // After we get the response, we can modify the state with it,\n    // without having to dispatch another action.\n    return state.copy(description: description);\n  }\n}\n\n/// This action increments the counter by [amount]].\nclass IncrementAction extends ReduxAction<AppState> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  // Synchronous reducer.\n  @override\n  AppState reduce() => state.copy(counter: state.counter + amount);\n}\n\n/// This widget is a connector. It connects the store to \"dumb-widget\".\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel>(\n      vm: () => Factory(this),\n      builder: (BuildContext context, ViewModel vm) => MyHomePage(\n        counter: vm.counter,\n        description: vm.description,\n        onIncrement: vm.onIncrement,\n      ),\n    );\n  }\n}\n\nabstract class BaseFactory<T extends Widget?, Model extends Vm>\n    extends VmFactory<AppState, T, Model> {\n  BaseFactory([T? connector]) : super(connector);\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass Factory extends BaseFactory<MyHomePageConnector, ViewModel> {\n  Factory(connector) : super(connector);\n\n  @override\n  ViewModel fromStore() => ViewModel(\n        counter: state.counter,\n        description: state.description,\n        onIncrement: _onIncrement,\n      );\n\n  void _onIncrement() {\n    dispatch(IncrementAndGetDescriptionAction());\n\n    print('Counter in the the view-model = ${vm.counter}');\n    print(\n        'Counter in the state when the view-model was created = ${state.counter}');\n    print('Counter in the current state = ${currentState().counter}');\n  }\n}\n\n/// The view-model holds the part of the Store state the dumb-widget needs.\nclass ViewModel extends Vm {\n  final int counter;\n  final String description;\n  final VoidCallback onIncrement;\n\n  ViewModel({\n    required this.counter,\n    required this.description,\n    required this.onIncrement,\n  }) : super(equals: [counter, description]);\n}\n\nclass MyHomePage extends StatelessWidget {\n  final int? counter;\n  final String? description;\n  final VoidCallback? onIncrement;\n\n  MyHomePage({\n    Key? key,\n    this.counter,\n    this.description,\n    this.onIncrement,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Increment Example (StoreConnector)')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('You have pushed the button this many times:'),\n            Text('$counter', style: const TextStyle(fontSize: 30)),\n            Text(description!,\n                style: const TextStyle(fontSize: 15),\n                textAlign: TextAlign.center),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: onIncrement,\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_environment__store_connector.dart",
    "content": "import 'dart:math';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<int> store;\n\n/// This example shows how to provide an environment to the Store, to help\n/// with dependency injection. The environment is a container for the\n/// injected services. You can have many environment implementations, one\n/// for production, others for tests etc. In this case, we're using the\n/// [DependenciesImpl].\n///\n/// You should extend [ReduxAction] to provide typed access to the [Dependencies]\n/// inside your actions.\n///\n/// In case you use [StoreConnector], you should also extend [VmFactory] to\n/// provide typed access to the [Dependencies] inside your factories.\n///\nvoid main() {\n  store = Store<int>(\n    initialState: 0,\n    dependencies: (store) => DependenciesImpl(),\n  );\n  runApp(MyApp());\n}\n\n/// The environment is a container for the injected services.\nabstract class Dependencies {\n  int incrementer(int value, int amount);\n\n  int limit(int value);\n}\n\n/// We can have many environment implementations, one for production, others for\n/// staging, tests etc. In this case, we're using the [DependenciesImpl].\nclass DependenciesImpl implements Dependencies {\n  @override\n  int incrementer(int value, int amount) => value + amount;\n\n  /// We'll limit the counter at 5.\n  @override\n  int limit(int value) => min(value, 5);\n}\n\n/// Extend [ReduxAction] to provide typed access to the [Dependencies].\nabstract class Action extends ReduxAction<int> {\n  Dependencies get dependencies => super.store.dependencies as Dependencies;\n}\n\n/// Extend [VmFactory] to provide typed access to the [Dependencies] when\n/// using [StoreConnector].\nabstract class AppFactory<T extends Widget?, Model extends Vm>\n    extends VmFactory<int, T, Model> {\n  AppFactory([T? connector]) : super(connector);\n\n  Dependencies get dependencies => store.dependencies as Dependencies;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<int>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePageConnector(),\n      ));\n}\n\n/// This action increments the counter by [amount], using [env].\nclass IncrementAction extends Action {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  @override\n  int reduce() => dependencies.incrementer(state, amount);\n}\n\n/// This widget is a connector. It uses a [StoreConnector] to connect the store\n/// to [MyHomePage] (the dumb-widget). Each time the state changes, it creates\n/// a view-model, and compares it with the view-model created with the previous\n/// state. If the view-model changed, the connector rebuilds. If you don't need\n/// to use connectors, you can just use `context.state`, `context.select`,\n/// `context.dispatch` etc, directly in your widgets.\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<int, ViewModel>(\n      vm: () => Factory(this),\n      builder: (BuildContext context, ViewModel vm) => MyHomePage(\n        counter: vm.counter,\n        onIncrement: vm.onIncrement,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model ([ViewModel]) for the [StoreConnector].\n/// It uses [env].\nclass Factory extends AppFactory<MyHomePageConnector, ViewModel> {\n  Factory(connector) : super(connector);\n\n  @override\n  ViewModel fromStore() => ViewModel(\n        counter: dependencies.limit(state),\n        onIncrement: () => dispatch(IncrementAction(amount: 1)),\n      );\n}\n\n/// A view-model is a helper object to a [StoreConnector] widget. It holds the\n/// part of the Store state the corresponding dumb-widget needs.\nclass ViewModel extends Vm {\n  final int counter;\n  final VoidCallback onIncrement;\n\n  ViewModel({\n    required this.counter,\n    required this.onIncrement,\n  }) : super(equals: [counter]);\n}\n\nclass MyHomePage extends StatelessWidget {\n  final int? counter;\n  final VoidCallback? onIncrement;\n\n  MyHomePage({\n    Key? key,\n    this.counter,\n    this.onIncrement,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Dependency Injection Example')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text(\n              'You have pushed the button this many times:\\n'\n              '(limited to 5)',\n              textAlign: TextAlign.center,\n            ),\n            Text('$counter', style: const TextStyle(fontSize: 30))\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: onIncrement,\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_event__store_connector.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example shows a text-field, and two buttons.\n/// When the first button is tapped, an async process downloads\n/// some text from the internet and puts it in the text-field.\n/// When the second button is tapped, the text-field is cleared.\n///\n/// This is meant to demonstrate the use of \"events\" to change\n/// a controller state.\n///\n/// It also demonstrates the use of an abstract class [BarrierAction]\n/// to override the action's before() and after() methods.\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state, which in this case is a counter and two events.\n@immutable\nclass AppState {\n  final int counter;\n  final bool waiting;\n  final Event clearTextEvt;\n  final Event<String> changeTextEvt;\n\n  AppState({\n    required this.counter,\n    required this.waiting,\n    required this.clearTextEvt,\n    required this.changeTextEvt,\n  });\n\n  AppState copy({\n    int? counter,\n    bool? waiting,\n    Event? clearTextEvt,\n    Event<String>? changeTextEvt,\n  }) =>\n      AppState(\n        counter: counter ?? this.counter,\n        waiting: waiting ?? this.waiting,\n        clearTextEvt: clearTextEvt ?? this.clearTextEvt,\n        changeTextEvt: changeTextEvt ?? this.changeTextEvt,\n      );\n\n  static AppState initialState() => AppState(\n        counter: 1,\n        waiting: false,\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          counter == other.counter &&\n          waiting == other.waiting;\n\n  @override\n  int get hashCode => counter.hashCode ^ waiting.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(\n          home: MyHomePageConnector(),\n        ),\n      );\n}\n\n/// This action orders the text-controller to clear.\nclass ClearTextAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => state.copy(clearTextEvt: Event());\n}\n\n/// Actions that extend [BarrierAction] show a modal barrier while their async processes run.\nabstract class BarrierAction extends ReduxAction<AppState> {\n  @override\n  void before() => dispatch(_WaitAction(true));\n\n  @override\n  void after() => dispatch(_WaitAction(false));\n}\n\nclass _WaitAction extends ReduxAction<AppState> {\n  final bool waiting;\n\n  _WaitAction(this.waiting);\n\n  @override\n  AppState reduce() => state.copy(waiting: waiting);\n}\n\n/// This action downloads some new text, and then creates an event\n/// that tells the text-controller to display that new text.\nclass ChangeTextAction extends BarrierAction {\n  @override\n  Future<AppState> reduce() async {\n    //\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/${state.counter}/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String newText = json['name'] ?? 'Unknown character';\n\n    return state.copy(\n      counter: state.counter + 1,\n      changeTextEvt: Event<String>(newText),\n    );\n  }\n}\n\n/// This widget is a connector. It connects the store to \"dumb-widget\".\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel>(\n      vm: () => Factory(this),\n      builder: (BuildContext context, ViewModel vm) => MyHomePage(\n        waiting: vm.waiting,\n        clearTextEvt: vm.clearTextEvt,\n        changeTextEvt: vm.changeTextEvt,\n        onClear: vm.onClear,\n        onChange: vm.onChange,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass Factory extends VmFactory<AppState, MyHomePageConnector, ViewModel> {\n  Factory(connector) : super(connector);\n\n  @override\n  ViewModel fromStore() => ViewModel(\n        waiting: state.waiting,\n        clearTextEvt: state.clearTextEvt,\n        changeTextEvt: state.changeTextEvt,\n        onClear: () => dispatch(ClearTextAction()),\n        onChange: () => dispatch(ChangeTextAction()),\n      );\n}\n\n/// The view-model holds the part of the Store state the dumb-widget needs.\nclass ViewModel extends Vm {\n  final bool? waiting;\n  final Event? clearTextEvt;\n  final Event<String>? changeTextEvt;\n  final VoidCallback onClear;\n  final VoidCallback onChange;\n\n  ViewModel({\n    required this.waiting,\n    required this.clearTextEvt,\n    required this.changeTextEvt,\n    required this.onClear,\n    required this.onChange,\n  }) : super(equals: [waiting!, clearTextEvt!, changeTextEvt!]);\n}\n\nclass MyHomePage extends StatefulWidget {\n  final bool? waiting;\n  final Event? clearTextEvt;\n  final Event<String>? changeTextEvt;\n  final VoidCallback? onClear;\n  final VoidCallback? onChange;\n\n  MyHomePage({\n    Key? key,\n    this.waiting,\n    this.clearTextEvt,\n    this.changeTextEvt,\n    this.onClear,\n    this.onChange,\n  }) : super(key: key);\n\n  @override\n  _MyHomePageState createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  late TextEditingController controller;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = TextEditingController();\n  }\n\n  @override\n  void didUpdateWidget(MyHomePage oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    consumeEvents();\n  }\n\n  void consumeEvents() {\n    if (widget.clearTextEvt!.consume())\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        if (mounted) controller.clear();\n      });\n\n    String? newText = widget.changeTextEvt!.consume();\n    if (newText != null)\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        if (mounted) controller.text = newText;\n      });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(title: const Text('Event Example')),\n          body: Center(\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                const Text('This is a TextField. Click to edit it:'),\n                TextField(controller: controller),\n                const SizedBox(height: 20),\n                FloatingActionButton(onPressed: widget.onChange, child: const Text(\"Change\")),\n                const SizedBox(height: 20),\n                FloatingActionButton(onPressed: widget.onClear, child: const Text(\"Clear\")),\n              ],\n            ),\n          ),\n        ),\n        if (widget.waiting!) ModalBarrier(color: Colors.red.withAlpha((255.0 * 0.4).round())),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_extension_vs_store_connector.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\nlate Store<AppState> store;\n\n/// This example shows a counter and a button.\n/// When the button is tapped, the counter will increment synchronously.\nvoid main() {\n  store = Store<AppState>(initialState: AppState(counter: 0, something: 0));\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: const MaterialApp(home: HomePage()),\n      );\n}\n\nclass HomePage extends StatelessWidget {\n  const HomePage({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Connector vs Provider Example')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            GetsStateFromStoreConnector(),\n            const SizedBox(height: 40),\n            GetsStateFromBuildContextExtension(),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        disabledElevation: 0,\n        onPressed: () => context.dispatch(IncrementAction()),\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n\nclass IncrementAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return AppState(\n      counter: state.counter + 1,\n      something: state.something,\n    );\n  }\n}\n\nclass GetsStateFromStoreConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector(\n      converter: (Store<AppState> store) => store.state.counter,\n      builder: (context, value) => Column(\n        children: [\n          Text('$value',\n              style: const TextStyle(fontSize: 30, color: Colors.black)),\n          const Text(\n            'Value read with the StoreConnector:\\n`StoreConnector(builder: (context, value) => ...)`',\n            style: const TextStyle(fontSize: 13),\n            textAlign: TextAlign.center,\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass GetsStateFromBuildContextExtension extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Text('${context.state.counter}',\n            style: const TextStyle(fontSize: 30, color: Colors.black)),\n        const Text(\n          'Value read with the StoreProvider:\\n`context.state.counter`',\n          style: TextStyle(fontSize: 13),\n          textAlign: TextAlign.center,\n        ),\n      ],\n    );\n  }\n}\n\nextension _BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n}\n\nclass AppState {\n  int counter;\n  int something;\n\n  AppState({\n    required this.counter,\n    required this.something,\n  });\n\n  @override\n  String toString() => 'AppState{counter: $counter}';\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          counter == other.counter;\n\n  @override\n  int get hashCode => counter.hashCode;\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_infinite_scroll__store_connector.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example shows a List of Star Wars characters.\n/// Scrolling to the bottom of the list will async load the next 20 characters.\n/// Scrolling past the top of the list (pull to refresh) will use\n/// `dispatchAndWait` to dispatch an action and get a future that tells the\n/// `RefreshIndicator` when the action completes.\n///\n/// `isWaiting(LoadMoreAction)` prevents the user from loading more while the\n/// async action is running.\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(\n    initialState: state,\n    actionObservers: [Log<AppState>.printer()],\n    modelObserver: DefaultModelObserver(),\n  );\n  runApp(MyApp());\n}\n\n@immutable\nclass AppState {\n  final List<String> numTrivia;\n\n  AppState({required this.numTrivia});\n\n  AppState copy({List<String>? numTrivia}) =>\n      AppState(numTrivia: numTrivia ?? this.numTrivia);\n\n  static AppState initialState() => AppState(numTrivia: <String>[]);\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          numTrivia == other.numTrivia;\n\n  @override\n  int get hashCode => numTrivia.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(\n          debugShowCheckedModeBanner: false,\n          home: MyHomePageConnector(),\n        ),\n      );\n}\n\nclass LoadMoreAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    List<String> list = List.from(state.numTrivia);\n    int start = state.numTrivia.length + 1;\n\n    // Fetch 20 people concurrently.\n    final responses = await Future.wait(\n      List.generate(20,\n          (i) => get(Uri.parse('https://swapi.dev/api/people/${start + i}/'))),\n    );\n\n    for (final response in responses) {\n      if (response.statusCode == 200) {\n        final data = jsonDecode(response.body);\n        list.add(data['name'] ?? 'Unknown character');\n      }\n    }\n\n    return state.copy(numTrivia: list);\n  }\n}\n\nclass RefreshAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    List<String> list = [];\n\n    // Fetch the first 20 people concurrently.\n    final responses = await Future.wait(\n      List.generate(\n          20, (i) => get(Uri.parse('https://swapi.dev/api/people/${i + 1}/'))),\n    );\n\n    for (final response in responses) {\n      if (response.statusCode == 200) {\n        final data = jsonDecode(response.body);\n        list.add(data['name'] ?? 'Unknown character');\n      }\n    }\n\n    return state.copy(numTrivia: list);\n  }\n}\n\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel>(\n      debug: this,\n      vm: () => Factory(this),\n      onInit: (st) => st.dispatch(RefreshAction()),\n      builder: (BuildContext context, ViewModel vm) => MyHomePage(\n        numTrivia: vm.numTrivia,\n        isLoading: vm.isLoading,\n        loadMore: vm.loadMore,\n        onRefresh: vm.onRefresh,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass Factory extends VmFactory<AppState, MyHomePageConnector, ViewModel> {\n  Factory(connector) : super(connector);\n\n  @override\n  ViewModel fromStore() {\n    return ViewModel(\n      numTrivia: state.numTrivia,\n      isLoading: isWaiting(LoadMoreAction),\n      loadMore: () => dispatch(LoadMoreAction()),\n      onRefresh: () => dispatchAndWait(RefreshAction()),\n    );\n  }\n}\n\n/// The view-model holds the part of the Store state the dumb-widget needs.\nclass ViewModel extends Vm {\n  final List<String> numTrivia;\n  final bool isLoading;\n  final VoidCallback loadMore;\n  final Future<void> Function() onRefresh;\n\n  ViewModel({\n    required this.numTrivia,\n    required this.isLoading,\n    required this.loadMore,\n    required this.onRefresh,\n  }) : super(equals: [\n          numTrivia,\n          isLoading,\n        ]);\n}\n\nclass MyHomePage extends StatefulWidget {\n  final List<String> numTrivia;\n  final bool isLoading;\n  final VoidCallback loadMore;\n  final Future<void> Function() onRefresh;\n\n  MyHomePage({\n    Key? key,\n    required this.numTrivia,\n    required this.isLoading,\n    required this.loadMore,\n    required this.onRefresh,\n  }) : super(key: key);\n\n  @override\n  _MyHomePageState createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  late ScrollController _controller;\n\n  @override\n  void initState() {\n    _controller = ScrollController()\n      ..addListener(() {\n        if (!widget.isLoading &&\n            _controller.position.maxScrollExtent ==\n                _controller.position.pixels) {\n          widget.loadMore();\n        }\n      });\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Infinite Scroll Example (StoreConnector)')),\n      body: (widget.numTrivia.isEmpty)\n          ? Container()\n          : RefreshIndicator(\n              onRefresh: widget.onRefresh,\n              child: ListView.builder(\n                controller: _controller,\n                itemCount: widget.numTrivia.length + (widget.isLoading ? 1 : 0),\n                itemBuilder: (context, index) {\n                  // Show loading spinner at the end\n                  if (index == widget.numTrivia.length) {\n                    return const Padding(\n                      padding: EdgeInsets.all(16.0),\n                      child: Center(child: CircularProgressIndicator()),\n                    );\n                  } else\n                    return ListTile(\n                      leading: CircleAvatar(child: Text(index.toString())),\n                      title: Text(widget.numTrivia[index]),\n                    );\n                },\n              ),\n            ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_is_waiting_works_when_multiple_actions__store_connector.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n/// This example shows how to show a spinner while any of two actions\n/// ([IncrementAction] and [MultiplyAction]) is running.\n///\n/// Writing this:\n///\n/// ```dart\n/// isWaiting([IncrementAction, MultiplyAction])\n/// ```\n///\n/// Is the same as writing this:\n///\n/// ```dart\n/// isWaiting(IncrementAction) || context.isWaiting(MultiplyAction)\n/// ```\n///\n/// See how the `isCalculating` variable is defined in the [CounterVmFactory].\n///\nvoid main() {\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    var store = Store<AppState>(initialState: AppState(counter: 0));\n    store.onChange.listen(print);\n\n    return MaterialApp(\n      theme: ThemeData(\n        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\n        useMaterial3: true,\n      ),\n      home: StoreProvider(\n        store: store,\n        child: const MyHomePage(),\n      ),\n    );\n  }\n}\n\nclass MyHomePage extends StatelessWidget {\n  const MyHomePage({super.key});\n\n  /// The code below, which uses a [StoreConnector], [CounterVmFactory],\n  /// and [CounterVm], is equivalent to:\n  ///\n  /// ```dart\n  /// return MyHomePageContent(\n  ///   title: 'IsWaiting multiple actions',\n  ///   counter: context.select((state) => state.counter),\n  ///   isCalculating: context.isWaiting([IncrementAction, MultiplyAction]),\n  ///   increment: () => context.dispatch(IncrementAction()),\n  ///   multiply: () => context.dispatch(MultiplyAction()),\n  /// );\n  /// ```\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, CounterVm>(\n      vm: () => CounterVmFactory(),\n      shouldUpdateModel: (s) => s.counter >= 0,\n      builder: (context, vm) {\n        return MyHomePageContent(\n          title: 'IsWaiting multiple actions (Store Connector)',\n          counter: vm.counter,\n          isCalculating: vm.isCalculating,\n          increment: vm.increment,\n          multiply: vm.multiply,\n        );\n      },\n    );\n  }\n}\n\nclass MyHomePageContent extends StatelessWidget {\n  const MyHomePageContent({\n    super.key,\n    required this.title,\n    required this.counter,\n    required this.isCalculating,\n    required this.increment,\n    required this.multiply,\n  });\n\n  final String title;\n  final int counter;\n  final bool isCalculating;\n  final VoidCallback increment, multiply;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        backgroundColor: Theme.of(context).colorScheme.inversePrimary,\n        title: Text(title),\n      ),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('Result:'),\n            Text(\n              '$counter',\n              style: Theme.of(context).textTheme.headlineMedium,\n            ),\n          ],\n        ),\n      ),\n      floatingActionButton: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FloatingActionButton(\n            onPressed: isCalculating ? null : increment,\n            elevation: isCalculating ? 0 : 6,\n            backgroundColor: isCalculating ? Colors.grey[300] : Colors.blue,\n            child: isCalculating\n                ? const Padding(\n                    padding: const EdgeInsets.all(16.0),\n                    child: const CircularProgressIndicator(),\n                  )\n                : const Icon(Icons.add),\n          ),\n          const SizedBox(height: 16),\n          FloatingActionButton(\n            onPressed: isCalculating ? null : multiply,\n            elevation: isCalculating ? 0 : 6,\n            backgroundColor: isCalculating ? Colors.grey[300] : Colors.blue,\n            child: isCalculating\n                ? const Padding(\n                    padding: const EdgeInsets.all(16.0),\n                    child: const CircularProgressIndicator(),\n                  )\n                : const Icon(Icons.close),\n          )\n        ],\n      ),\n    );\n  }\n}\n\nclass AppState {\n  final int counter;\n\n  AppState({required this.counter});\n\n  AppState copy({int? counter}) => AppState(counter: counter ?? this.counter);\n\n  @override\n  String toString() {\n    return '.\\n.\\n.\\nAppState{counter: $counter}\\n.\\n.\\n';\n  }\n}\n\nclass CounterVm extends Vm {\n  final int counter;\n  final bool isCalculating;\n  final VoidCallback increment, multiply;\n\n  CounterVm({\n    required this.counter,\n    required this.isCalculating,\n    required this.increment,\n    required this.multiply,\n  }) : super(equals: [counter, isCalculating]);\n}\n\nclass CounterVmFactory extends VmFactory<AppState, MyHomePage, CounterVm> {\n  @override\n  CounterVm fromStore() => CounterVm(\n        counter: state.counter,\n        isCalculating: isWaiting([IncrementAction, MultiplyAction]),\n        increment: () => dispatch(IncrementAction()),\n        multiply: () => dispatch(MultiplyAction()),\n      );\n}\n\nclass IncrementAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await Future.delayed(const Duration(seconds: 1));\n    return AppState(counter: state.counter + 1);\n  }\n}\n\nclass MultiplyAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await Future.delayed(const Duration(seconds: 1));\n    return AppState(counter: state.counter * 2);\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_is_waiting_works_when_state_unchanged__store_connector.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\n/// This example demonstrates that `isWaiting` works even for actions that\n/// return `null` (i.e., actions that don't change the state).\nvoid main() {\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    var store = Store<AppState>(initialState: AppState(counter: 0));\n    store.onChange.listen(print);\n\n    return MaterialApp(\n      theme: ThemeData(\n        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\n        useMaterial3: true,\n      ),\n      home: StoreProvider(\n        store: store,\n        child: const MyHomePage(),\n      ),\n    );\n  }\n}\n\nclass MyHomePage extends StatelessWidget {\n  const MyHomePage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, CounterVm>(\n      vm: () => CounterVmFactory(),\n      shouldUpdateModel: (s) => s.counter >= 0,\n      builder: (context, vm) {\n        return MyHomePageContent(\n          title: 'IsWaiting works when state unchanged',\n          counter: vm.counter,\n          isIncrementing: vm.isIncrementing,\n          increment: vm.increment,\n        );\n      },\n    );\n  }\n}\n\nclass MyHomePageContent extends StatelessWidget {\n  const MyHomePageContent({\n    super.key,\n    required this.title,\n    required this.counter,\n    required this.isIncrementing,\n    required this.increment,\n  });\n\n  final String title;\n  final int counter;\n  final bool isIncrementing;\n  final VoidCallback increment;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        backgroundColor: Theme.of(context).colorScheme.inversePrimary,\n        title: Text(title),\n      ),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('You pushed the button:'),\n            Text(\n              '$counter',\n              style: Theme.of(context).textTheme.headlineMedium,\n            ),\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: isIncrementing ? null : increment,\n        elevation: isIncrementing ? 0 : 6,\n        backgroundColor: isIncrementing ? Colors.grey[300] : Colors.blue,\n        child: isIncrementing ? const Padding(\n          padding: const EdgeInsets.all(16.0),\n          child: const CircularProgressIndicator(),\n        ) : const Icon(Icons.add),\n      ),\n    );\n  }\n}\n\nclass AppState {\n  final int counter;\n\n  AppState({required this.counter});\n\n  AppState copy({int? counter}) => AppState(counter: counter ?? this.counter);\n\n  @override\n  String toString() {\n    return '.\\n.\\n.\\nAppState{counter: $counter}\\n.\\n.\\n';\n  }\n}\n\nclass CounterVm extends Vm {\n  final int counter;\n  final bool isIncrementing;\n  final VoidCallback increment;\n\n  CounterVm({\n    required this.counter,\n    required this.isIncrementing,\n    required this.increment,\n  }) : super(equals: [\n          counter,\n          isIncrementing,\n        ]);\n}\n\nclass CounterVmFactory extends VmFactory<AppState, MyHomePage, CounterVm> {\n  @override\n  CounterVm fromStore() => CounterVm(\n        counter: state.counter,\n        isIncrementing: isWaiting(IncrementAction),\n        increment: () => dispatch(IncrementAction()),\n      );\n}\n\nclass IncrementAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    dispatch(DoIncrementAction());\n    await Future.delayed(const Duration(milliseconds: 1250));\n    return null;\n  }\n}\n\nclass DoIncrementAction extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    return AppState(counter: state.counter + 1);\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_navigate__store_connector.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\nlate Store<AppState> store;\n\nfinal navigatorKey = GlobalKey<NavigatorState>();\n\nvoid main() async {\n  NavigateAction.setNavigatorKey(navigatorKey);\n  store = Store<AppState>(initialState: AppState());\n  runApp(MyApp());\n}\n\nfinal routes = {\n  '/': (BuildContext context) => Page1Connector(),\n  \"/myRoute\": (BuildContext context) => Page2Connector(),\n};\n\nclass AppState {}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        routes: routes,\n        navigatorKey: navigatorKey,\n      ),\n    );\n  }\n}\n\nclass Page extends StatelessWidget {\n  final Color? color;\n  final String? text;\n  final VoidCallback onChangePage;\n\n  Page({this.color, this.text, required this.onChangePage});\n\n  @override\n  Widget build(BuildContext context) => ElevatedButton(\n        style: ElevatedButton.styleFrom(backgroundColor: color),\n        child: Text(text!),\n        onPressed: onChangePage,\n      );\n}\n\nclass Page1Connector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel1>(\n      vm: () => Factory1(),\n      builder: (BuildContext context, ViewModel1 vm) => Page(\n        color: Colors.red,\n        text: \"Tap me to push a new route!\",\n        onChangePage: vm.onChangePage,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass Factory1 extends VmFactory<AppState, Page1Connector, ViewModel1> {\n  @override\n  ViewModel1 fromStore() =>\n      ViewModel1(onChangePage: () => dispatch(NavigateAction.pushNamed(\"/myRoute\")));\n}\n\n/// The view-model holds the part of the Store state the dumb-widget needs.\nclass ViewModel1 extends Vm {\n  final VoidCallback onChangePage;\n\n  ViewModel1({required this.onChangePage});\n}\n\nclass Page2Connector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel2>(\n      vm: () => Factory2(),\n      builder: (BuildContext context, ViewModel2 vm) => Page(\n        color: Colors.blue,\n        text: \"Tap me to pop this route!\",\n        onChangePage: vm.onChangePage,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass Factory2 extends VmFactory<AppState, Page1Connector, ViewModel2> {\n  @override\n  ViewModel2 fromStore() => ViewModel2(\n        onChangePage: () => dispatch(NavigateAction.pop()),\n      );\n}\n\n/// The view-model holds the part of the Store state the dumb-widget needs.\nclass ViewModel2 extends Vm {\n  final VoidCallback onChangePage;\n\n  ViewModel2({required this.onChangePage});\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_null_viewmodel__connector.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<int> store;\n\n/// This example shows a counter and a button. It's similar to the `main.dart`\n/// example. However when the counter is `5` the view-model created by the\n/// Factory's `fromStore()` will be `null`.\n///\n/// The `StoreConnector` accept `null` view-models. And when it gets a `null`\n/// view-model it simply replaces the screen with a `ViewModel is null` text.\n///\nvoid main() {\n  store = Store<int>(initialState: 0);\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<int>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePageConnector(),\n      ));\n}\n\nclass IncrementAction extends ReduxAction<int> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  @override\n  int reduce() => state + amount;\n}\n\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    ///\n    /// 1) The StoreConnector uses `ViewModel?` instead of `ViewModel`.\n    return StoreConnector<int, ViewModel?>(\n      vm: () => Factory(this),\n\n      /// 2) The builder uses `ViewModel?` instead of `ViewModel`.\n      builder: (BuildContext context, ViewModel? vm) {\n        return (vm == null)\n            ? const Material(\n                child: Center(\n                  child: const Text(\"ViewModel is null\"),\n                ),\n              )\n            : MyHomePage(\n                counter: vm.counter,\n                onIncrement: vm.onIncrement,\n              );\n      },\n    );\n  }\n}\n\nclass Factory extends VmFactory<int, MyHomePageConnector, ViewModel> {\n  Factory(connector) : super(connector);\n\n  /// 3) The `fromStore` method uses `ViewModel?` instead of `ViewModel`.\n  @override\n  ViewModel? fromStore() {\n    return (store.state == 5)\n        ? null\n        : ViewModel(\n            counter: state,\n            onIncrement: () => dispatch(IncrementAction(amount: 1)),\n          );\n  }\n}\n\nclass ViewModel extends Vm {\n  final int counter;\n  final VoidCallback onIncrement;\n\n  ViewModel({\n    required this.counter,\n    required this.onIncrement,\n  }) : super(equals: [counter]);\n}\n\nclass MyHomePage extends StatelessWidget {\n  final int? counter;\n  final VoidCallback? onIncrement;\n\n  MyHomePage({\n    Key? key,\n    this.counter,\n    this.onIncrement,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Null ViewModel Example')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('You have pushed the button this many times:'),\n            Text('$counter', style: const TextStyle(fontSize: 30))\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: onIncrement,\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_should_update_model__store_connector.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<int> store;\n\n/// This example shows how to prevent creating view-models from invalid states,\n/// using [StoreConnector.shouldUpdateModel].\n///\n/// When the button is tapped, the counter will increment 5 times,\n/// synchronously. So, the sequence would be 0, 5, 10, 15, 20, 25 etc.\n///\n/// However, we consider odd numbers invalid (StoreConnector.shouldUpdateModel\n/// returns `false` for odd numbers).\n/// \n/// Therefore, it will display 0, 4, 10, 14, 20, 24 etc.\n///\nvoid main() {\n  store = Store<int>(initialState: 0);\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<int>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePageConnector(),\n      ));\n}\n\n/// This action increments the counter by [amount]].\nclass IncrementAction extends ReduxAction<int> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  @override\n  int reduce() => state + amount;\n}\n\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<int, ViewModel>(\n      vm: () => Factory(this),\n      //\n      // Should update the view-model only when the counter is even.\n      shouldUpdateModel: (int count) => count % 2 == 0,\n      //\n      builder: (BuildContext context, ViewModel vm) => MyHomePage(\n        counter: vm.counter,\n        onIncrement: vm.onIncrement,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass Factory extends VmFactory<int, MyHomePageConnector, ViewModel> {\n  Factory(connector) : super(connector);\n\n  @override\n  ViewModel fromStore() {\n    return ViewModel(\n      counter: state,\n      onIncrement: () {\n        // Increment 5 times.\n        dispatch(IncrementAction(amount: 1));\n        dispatch(IncrementAction(amount: 1));\n        dispatch(IncrementAction(amount: 1));\n        dispatch(IncrementAction(amount: 1));\n        dispatch(IncrementAction(amount: 1));\n      },\n    );\n  }\n}\n\nclass ViewModel extends Vm {\n  final int counter;\n  final VoidCallback onIncrement;\n\n  ViewModel({\n    required this.counter,\n    required this.onIncrement,\n  }) : super(equals: [counter]);\n}\n\nclass MyHomePage extends StatelessWidget {\n  final int? counter;\n  final VoidCallback? onIncrement;\n\n  MyHomePage({\n    Key? key,\n    this.counter,\n    this.onIncrement,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Increment Example')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Padding(\n              padding: const EdgeInsets.all(20.0),\n              child: Text('Each time you push the button it increments 5 times.\\n\\n'\n                  'But only even values are valid to appear in the UI.\\n\\n'\n                  'This demonstrates the use of StoreConnector.shouldUpdateModel.'),\n            ),\n            Text('$counter', style: const TextStyle(fontSize: 30))\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: onIncrement,\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_spinner__store_connector.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\nimport 'dart:async';\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\nlate Store<AppState> store;\n\n/// This example shows a counter and a button.\n/// When the button is tapped, the counter will increment asynchronously.\nvoid main() {\n  store = Store<AppState>(initialState: AppState(counter: 0, something: 0));\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(\n          home: UserExceptionDialog<AppState>(\n            child: const HomePage(),\n          ),\n        ),\n      );\n}\n\nclass HomePage extends StatelessWidget {\n  const HomePage({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Spinner With StoreConnector')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('You have pushed the button this many times:'),\n            CounterWidget(),\n          ],\n        ),\n      ),\n      // Here we disable the button while the `WaitAndIncrementAction` action is running.\n      floatingActionButton: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          _FailWithDialog_ButtonConnector(),\n          const SizedBox(width: 12),\n          _FailNoDialog_ButtonConnector(),\n          const SizedBox(width: 12),\n          _PlusButtonConnector(),\n        ],\n      ),\n    );\n  }\n}\n\nclass _PlusButtonConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel>(\n      vm: () => Factory(this),\n      builder: (context, vm) {\n        return vm.isWaiting1\n            ? const FloatingActionButton(\n                disabledElevation: 0,\n                onPressed: null,\n                child: SizedBox(width: 25, height: 25, child: CircularProgressIndicator()))\n            : FloatingActionButton(\n                disabledElevation: 0,\n                onPressed: () => context.dispatch(WaitAndIncrementAction()),\n                child: const Icon(Icons.add),\n              );\n      },\n    );\n  }\n}\n\nclass _FailWithDialog_ButtonConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel>(\n      vm: () => Factory(this),\n      builder: (context, vm) {\n        return vm.isWaiting2\n            ? const FloatingActionButton(\n                disabledElevation: 0,\n                onPressed: null,\n                child: SizedBox(width: 25, height: 25, child: CircularProgressIndicator()))\n            : FloatingActionButton(\n                disabledElevation: 0,\n                onPressed: () => context.dispatch(FailWithDialogAction()),\n                child: const Text('Fail with dialog', textAlign: TextAlign.center),\n              );\n      },\n    );\n  }\n}\n\nclass _FailNoDialog_ButtonConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel>(\n      vm: () => Factory(this),\n      builder: (context, vm) {\n        return vm.isWaiting3\n            ? const FloatingActionButton(\n                disabledElevation: 0,\n                onPressed: null,\n                child: SizedBox(width: 25, height: 25, child: CircularProgressIndicator()))\n            : FloatingActionButton(\n                disabledElevation: 0,\n                onPressed: () => context.dispatch(FailNoDialogAction()),\n                child: const Text('Fail no dialog', textAlign: TextAlign.center),\n              );\n      },\n    );\n  }\n}\n\nclass Factory extends VmFactory<AppState, Widget, ViewModel> {\n  Factory(connector) : super(connector);\n\n  @override\n  ViewModel fromStore() {\n    return ViewModel(\n      isWaiting1: isWaiting(WaitAndIncrementAction),\n      isWaiting2: isWaiting(FailWithDialogAction),\n      isWaiting3: isWaiting(FailNoDialogAction),\n    );\n  }\n}\n\nclass ViewModel extends Vm {\n  final bool isWaiting1, isWaiting2, isWaiting3;\n\n  ViewModel({\n    required this.isWaiting1,\n    required this.isWaiting2,\n    required this.isWaiting3,\n  }) : super(equals: [isWaiting1, isWaiting2, isWaiting3]);\n}\n\n/// This action waits for 2 seconds, then increments the counter by 1.\nclass WaitAndIncrementAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await Future.delayed(const Duration(seconds: 2));\n    return AppState(\n      counter: state.counter + 1,\n      something: state.something,\n    );\n  }\n}\n\n/// This action waits for 2 seconds, then fails with a dialog.\nclass FailWithDialogAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await Future.delayed(const Duration(seconds: 2));\n    throw const UserException('The increment failed!');\n  }\n}\n\n/// This action waits for 2 seconds, then fails with no dialog.\nclass FailNoDialogAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await Future.delayed(const Duration(seconds: 2));\n    throw const UserException('The increment failed!').noDialog;\n  }\n}\n\nclass CounterWidget extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Text(\n      '${context.state.counter}',\n      style: const TextStyle(fontSize: 40, color: Colors.black),\n    );\n  }\n}\n\nextension _BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n}\n\nclass AppState {\n  int counter;\n  int something;\n\n  AppState({\n    required this.counter,\n    required this.something,\n  });\n\n  @override\n  String toString() => 'AppState{counter: $counter}';\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState && runtimeType == other.runtimeType && counter == other.counter;\n\n  @override\n  int get hashCode => counter.hashCode;\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_static_view_model__store_connector.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<int> store;\n\n/// This example shows how to use the same view-model architecture of the\n/// flutter_redux package. This is specially useful if you are migrating\n/// from flutter_redux.\n///\n/// Here, you use the `StoreConnector`'s `converter` parameter,\n/// instead of the `vm` parameter.\n///\n/// Your `ViewModel` class may or may not extend `Vm`, but it\n/// must have a static factory method, usually named `fromStore`:\n///\n/// `converter: (store) => ViewModel.fromStore(store)`.\n///\nvoid main() {\n  store = Store<int>(initialState: 0);\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<int>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePageConnector(),\n      ));\n}\n\n/// This action increments the counter by [amount]].\nclass IncrementAction extends ReduxAction<int> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  @override\n  int reduce() => state + amount;\n}\n\n/// This widget is a connector. It connects the store to \"dumb-widget\".\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<int, ViewModel>(\n      converter: (store) => ViewModel.fromStore(store),\n      builder: (BuildContext context, ViewModel vm) => MyHomePage(\n        counter: vm.counter,\n        onIncrement: vm.onIncrement,\n      ),\n    );\n  }\n}\n\n/// The view-model holds the part of the Store state the dumb-widget needs.\nclass ViewModel extends Vm {\n  final int counter;\n  final VoidCallback onIncrement;\n\n  ViewModel({\n    required this.counter,\n    required this.onIncrement,\n  }) : super(equals: [counter]);\n\n  /// Static factory called by the StoreConnector's converter parameter.\n  static ViewModel fromStore(Store<int> store) {\n    return ViewModel(\n      counter: store.state,\n      onIncrement: () => store.dispatch(IncrementAction(amount: 1)),\n    );\n  }\n}\n\nclass MyHomePage extends StatelessWidget {\n  final int? counter;\n  final VoidCallback? onIncrement;\n\n  MyHomePage({\n    Key? key,\n    this.counter,\n    this.onIncrement,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('Static Factory ViewModel Example'),\n        backgroundColor: Colors.green,\n      ),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('You have pushed the button this many times:'),\n            Text('$counter', style: const TextStyle(fontSize: 30))\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: onIncrement,\n        child: const Icon(Icons.add),\n        backgroundColor: Colors.green,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_sync__store_connector.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<int> store;\n\n/// This example demonstrates:\n/// - The use of [StoreConnector], [VmFactory], and [ViewModel].\n/// - Doing synchronous work inside an action.\n///\n/// It shows a counter and a button.\n/// When the button is tapped, the counter will increment synchronously.\n///\n/// Note: In this simple example, the app state is simply a number (the\n/// counter), so the store is defined as `Store<int>` with initial state 0.\n/// For more realistic examples of app states, see the other examples in this\n/// package, that define state as an immutable class named `AppState`.\n///\nvoid main() {\n  store = Store<int>(initialState: 0);\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<int>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePageConnector(),\n      ));\n}\n\n/// This action increments the counter by [amount]].\nclass IncrementAction extends ReduxAction<int> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  @override\n  int reduce() => state + amount;\n}\n\n/// This widget is a connector.\n/// It connects the store to [MyHomePage] (the dumb-widget).\n/// Each time the state changes, it creates a view-model, and compares it\n/// with the view-model created with the previous state.\n/// Only if the view-model changed, the connector rebuilds.\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<int, ViewModel>(\n      vm: () => Factory(this),\n      builder: (BuildContext context, ViewModel vm) => MyHomePage(\n        counter: vm.counter,\n        onIncrement: vm.onIncrement,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass Factory extends VmFactory<int, MyHomePageConnector, ViewModel> {\n  Factory(connector) : super(connector);\n\n  @override\n  ViewModel fromStore() => ViewModel(\n        counter: state,\n        onIncrement: () => dispatch(IncrementAction(amount: 1)),\n      );\n}\n\n/// A view-model is a helper object to a [StoreConnector] widget. It holds the\n/// part of the Store state the corresponding dumb-widget needs, and may also\n/// convert this state part into a more convenient format for the dumb-widget\n/// to work with.\n///\n/// You must implement equals/hashcode for the view-model class to work.\n/// Otherwise, the [StoreConnector] will think the view-model changed everytime,\n/// and thus will rebuild everytime. This won't create any visible problems\n/// to your app, but is inefficient and may be slow.\n///\n/// By extending the [Vm] class you can implement equals/hashcode without\n/// having to override these methods. Instead, simply list all fields\n/// (which are not immutable, like functions) to the [equals] parameter\n/// in the constructor.\n///\nclass ViewModel extends Vm {\n  final int counter;\n  final VoidCallback onIncrement;\n\n  ViewModel({\n    required this.counter,\n    required this.onIncrement,\n  }) : super(equals: [counter]);\n}\n\n/// This is the \"dumb-widget\". It has no notion of the store, the state, the\n/// connector or the view-model. It just gets the parameters it needs to display\n/// itself, and callbacks it should call when reacting to the user interface.\nclass MyHomePage extends StatelessWidget {\n  final int? counter;\n  final VoidCallback? onIncrement;\n\n  MyHomePage({\n    Key? key,\n    this.counter,\n    this.onIncrement,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Increment Example (StoreConnector)')),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('You have pushed the button this many times:'),\n            Text('$counter', style: const TextStyle(fontSize: 30))\n          ],\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: onIncrement,\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_wait_action_advanced_1__store_connector.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example shows how to use [WaitAction] in advanced ways.\n/// For this to work, the [AppState] must have a [wait] field of type [Wait],\n/// and this field must be in the [AppState.copy] method as a named parameter.\n///\n/// 10 buttons are shown. When a button is clicked it will be\n/// replaced by a downloaded text description. Each button shows a progress\n/// indicator while its description is downloading. The screen title shows\n/// the text \"Downloading...\" if any of the buttons is currently downloading.\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state contains a [wait] object of type [Wait].\n@immutable\nclass AppState {\n  final Map<int, String> descriptions;\n  final Wait wait;\n\n  AppState({required this.descriptions, required this.wait});\n\n  /// The copy method has a named [wait] parameter of type [Wait].\n  AppState copy({int? counter, Map<int, String>? descriptions, Wait? wait}) => AppState(\n        descriptions: descriptions ?? this.descriptions,\n        wait: wait ?? this.wait,\n      );\n\n  /// The [wait] parameter is instantiated to `Wait()`.\n  static AppState initialState() => AppState(\n        descriptions: {},\n        wait: Wait(),\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          descriptions == other.descriptions &&\n          wait == other.wait;\n\n  @override\n  int get hashCode => descriptions.hashCode ^ wait.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePageConnector(),\n      ));\n}\n\nclass GetDescriptionAction extends ReduxAction<AppState> {\n  int index;\n\n  GetDescriptionAction(this.index);\n\n  @override\n  Future<AppState> reduce() async {\n\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/$index/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String description = json['name'] ?? 'Unknown character';\n\n    await Future.delayed(const Duration(seconds: 2)); // Adds some more delay.\n\n    Map<int, String> newDescriptions = Map.of(state.descriptions);\n    newDescriptions[index] = description;\n\n    return state.copy(descriptions: newDescriptions);\n  }\n\n  // The wait starts here. We use the index as a wait-flag reference.\n  @override\n  void before() => dispatch(WaitAction.add(index));\n\n  // The wait ends here. We remove the index from the wait-flag references.\n  @override\n  void after() => dispatch(WaitAction.remove(index));\n}\n\n/// This widget is a connector. It connects the store to \"dumb-widget\".\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, PageViewModel>(\n      vm: () => PageVmFactory(this),\n      builder: (BuildContext context, PageViewModel vm) => MyHomePage(\n        onGetDescription: vm.onGetDescription,\n        waiting: vm.waiting,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass PageVmFactory extends VmFactory<AppState, MyHomePageConnector, PageViewModel> {\n  PageVmFactory(connector) : super(connector);\n\n  @override\n  PageViewModel fromStore() => PageViewModel(\n        /// If there is any waiting, `state.wait.isWaitingAny` will return true.\n        waiting: state.wait.isWaitingAny,\n\n        onGetDescription: (int index) => dispatch(GetDescriptionAction(index)),\n      );\n}\n\nclass PageViewModel extends Vm {\n  final bool waiting;\n  final void Function(int) onGetDescription;\n\n  PageViewModel({\n    required this.waiting,\n    required this.onGetDescription,\n  }) : super(equals: [waiting]);\n}\n\n/// This widget is a connector. It connects the store to \"dumb-widget\".\nclass MyItemConnector extends StatelessWidget {\n  final int index;\n  final void Function(int) onGetDescription;\n\n  MyItemConnector({\n    required this.index,\n    required this.onGetDescription,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ItemViewModel>(\n      vm: () => ItemVmFactory(this),\n      builder: (BuildContext context, ItemViewModel vm) => MyItem(\n        description: vm.description,\n        waiting: vm.waiting,\n        index: index,\n        onGetDescription: onGetDescription,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass ItemVmFactory extends VmFactory<AppState, MyItemConnector, ItemViewModel> {\n  ItemVmFactory(connector) : super(connector);\n\n  @override\n  ItemViewModel fromStore() => ItemViewModel(\n        description: state.descriptions[connector.index] ?? \"\",\n\n        /// If index is waiting, `state.wait.isWaiting(index)` returns true.\n        waiting: state.wait.isWaiting(connector.index),\n      );\n}\n\nclass ItemViewModel extends Vm {\n  final String description;\n  final bool waiting;\n\n  ItemViewModel({\n    required this.description,\n    required this.waiting,\n  }) : super(equals: [description, waiting]);\n}\n\nclass MyItem extends StatelessWidget {\n  final String description;\n  final bool waiting;\n  final int index;\n  final void Function(int) onGetDescription;\n\n  MyItem({\n    required this.description,\n    required this.waiting,\n    required this.index,\n    required this.onGetDescription,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    Widget contents;\n\n    if (waiting)\n      contents = _progressIndicator();\n    else if (description.isNotEmpty)\n      contents = _indexDescription();\n    else\n      contents = _button();\n\n    return Container(height: 70, child: Center(child: contents));\n  }\n\n  MaterialButton _button() => MaterialButton(\n        color: Colors.blue,\n        child:\n            Text(\"CLICK $index\", style: const TextStyle(fontSize: 15), textAlign: TextAlign.center),\n        onPressed: () => onGetDescription(index),\n      );\n\n  Text _indexDescription() =>\n      Text(description, style: const TextStyle(fontSize: 15), textAlign: TextAlign.center);\n\n  CircularProgressIndicator _progressIndicator() => const CircularProgressIndicator(\n        valueColor: AlwaysStoppedAnimation<Color>(Colors.red),\n      );\n}\n\nclass MyHomePage extends StatelessWidget {\n  final bool waiting;\n  final void Function(int) onGetDescription;\n\n  MyHomePage({\n    Key? key,\n    required this.waiting,\n    required this.onGetDescription,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(title: Text(waiting ? \"Downloading...\" : \"Advanced WaitAction Example 1\")),\n          body: ListView.builder(\n            itemCount: 10,\n            itemBuilder: (context, index) => MyItemConnector(\n              index: index,\n              onGetDescription: onGetDescription,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_wait_action_advanced_2__store_connector.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example is the same as the one in `main_wait_action_advanced_1__store_connector.dart`.\n/// However, instead of only using flags in the [WaitAction], it uses both\n/// flags and references.\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state contains a [wait] object of type [Wait].\n@immutable\nclass AppState {\n  final Map<int, String> descriptions;\n  final Wait wait;\n\n  AppState({\n    required this.descriptions,\n    required this.wait,\n  });\n\n  /// The copy method has a named [wait] parameter of type [Wait].\n  AppState copy({int? counter, Map<int, String>? descriptions, Wait? wait}) => AppState(\n        descriptions: descriptions ?? this.descriptions,\n        wait: wait ?? this.wait,\n      );\n\n  /// The [wait] parameter is instantiated to `Wait()`.\n  static AppState initialState() => AppState(\n        descriptions: {},\n        wait: Wait(),\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          descriptions == other.descriptions &&\n          wait == other.wait;\n\n  @override\n  int get hashCode => descriptions.hashCode ^ wait.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        home: MyHomePageConnector(),\n      ));\n}\n\nclass GetDescriptionAction extends ReduxAction<AppState> {\n  int index;\n\n  GetDescriptionAction(this.index);\n\n  @override\n  Future<AppState> reduce() async {\n\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/$index/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String description = json['name'] ?? 'Unknown character';\n\n    await Future.delayed(const Duration(seconds: 2)); // Adds some more delay.\n\n    Map<int, String> newDescriptions = Map.of(state.descriptions);\n    newDescriptions[index] = description;\n\n    return state.copy(descriptions: newDescriptions);\n  }\n\n  // The wait starts here. We use the index as a wait-flag reference.\n  @override\n  void before() => dispatch(WaitAction.add(\"button-download\", ref: index));\n\n  // The wait ends here. We remove the index from the wait-flag references.\n  @override\n  void after() => dispatch(WaitAction.remove(\"button-download\", ref: index));\n}\n\n/// This widget is a connector. It connects the store to \"dumb-widget\".\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, PageViewModel>(\n      vm: () => PageVmFactory(this),\n      builder: (BuildContext context, PageViewModel vm) => MyHomePage(\n        onGetDescription: vm.onGetDescription,\n        waiting: vm.waiting,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass PageVmFactory extends VmFactory<AppState, MyHomePageConnector, PageViewModel> {\n  PageVmFactory(connector) : super(connector);\n\n  @override\n  PageViewModel fromStore() => PageViewModel(\n        /// If there is any waiting, `state.wait.isWaitingAny` will return true.\n        waiting: state.wait.isWaitingAny,\n\n        onGetDescription: (int index) => dispatch(GetDescriptionAction(index)),\n      );\n}\n\nclass PageViewModel extends Vm {\n  final bool waiting;\n  final void Function(int) onGetDescription;\n\n  PageViewModel({\n    required this.waiting,\n    required this.onGetDescription,\n  }) : super(equals: [waiting]);\n}\n\n/// This widget is a connector. It connects the store to \"dumb-widget\".\nclass MyItemConnector extends StatelessWidget {\n  final int index;\n  final void Function(int) onGetDescription;\n\n  MyItemConnector({\n    required this.index,\n    required this.onGetDescription,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ItemViewModel>(\n      vm: () => ItemVmFactory(this),\n      builder: (BuildContext context, ItemViewModel vm) => MyItem(\n        description: vm.description,\n        waiting: vm.waiting,\n        index: index,\n        onGetDescription: onGetDescription,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass ItemVmFactory extends VmFactory<AppState, MyItemConnector, ItemViewModel> {\n  ItemVmFactory(connector) : super(connector);\n\n  @override\n  ItemViewModel fromStore() => ItemViewModel(\n        description: state.descriptions[connector.index] ?? \"\",\n\n        /// If index is waiting, `state.wait.isWaiting(index)` returns true.\n        waiting: state.wait.isWaiting(\"button-download\", ref: connector.index),\n      );\n}\n\n/// The view-model holds the part of the Store state the dumb-widget needs.\nclass ItemViewModel extends Vm {\n  final String description;\n  final bool waiting;\n\n  ItemViewModel({\n    required this.description,\n    required this.waiting,\n  }) : super(equals: [description, waiting]);\n}\n\nclass MyItem extends StatelessWidget {\n  final String description;\n  final bool waiting;\n  final int index;\n  final void Function(int) onGetDescription;\n\n  MyItem({\n    required this.description,\n    required this.waiting,\n    required this.index,\n    required this.onGetDescription,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    Widget contents;\n\n    if (waiting)\n      contents = _progressIndicator();\n    else if (description.isNotEmpty)\n      contents = _indexDescription();\n    else\n      contents = _button();\n\n    return Container(height: 70, child: Center(child: contents));\n  }\n\n  MaterialButton _button() => MaterialButton(\n        color: Colors.blue,\n        child:\n            Text(\"CLICK $index\", style: const TextStyle(fontSize: 15), textAlign: TextAlign.center),\n        onPressed: () => onGetDescription(index),\n      );\n\n  Text _indexDescription() =>\n      Text(description, style: const TextStyle(fontSize: 15), textAlign: TextAlign.center);\n\n  CircularProgressIndicator _progressIndicator() => const CircularProgressIndicator(\n        valueColor: AlwaysStoppedAnimation<Color>(Colors.red),\n      );\n}\n\nclass MyHomePage extends StatelessWidget {\n  final bool waiting;\n  final void Function(int) onGetDescription;\n\n  MyHomePage({\n    Key? key,\n    required this.waiting,\n    required this.onGetDescription,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(title: Text(waiting ? \"Downloading...\" : \"Advanced WaitAction Example 2\")),\n          body: ListView.builder(\n            itemCount: 10,\n            itemBuilder: (context, index) => MyItemConnector(\n              index: index,\n              onGetDescription: onGetDescription,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/store_connector_examples/main_wait_action_simple__store_connector.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate Store<AppState> store;\n\n/// This example is the same as the one in `main_before_and_after.dart`.\n/// However, instead of declaring a `MyWaitAction`, it uses the build-in\n/// [WaitAction].\n///\n/// For this to work, the [AppState] must have a [wait] field of type [Wait],\n/// and this field must be in the [AppState.copy] method as a named parameter.\n///\n/// While the async process is running, the action's `before` method will\n/// add the action itself as a wait-flag reference:\n///\n/// ```\n/// void before() => dispatch(WaitAction.add(this));\n/// ```\n///\n/// The [ViewModel] will read this info from `state.wait.isWaitingAny` to\n/// turn on the modal barrier.\n///\n/// When the async process finishes, the action's before method will\n/// remove the action from the wait-flag set:\n///\n/// ```\n/// void after() => dispatch(WaitAction.remove(this));\n/// ```\n///\nvoid main() {\n  var state = AppState.initialState();\n  store = Store<AppState>(initialState: state);\n  runApp(MyApp());\n}\n\n/// The app state contains a [wait] object of type [Wait].\n@immutable\nclass AppState {\n  final int counter;\n  final String description;\n  final Wait wait;\n\n  AppState({\n    required this.counter,\n    required this.description,\n    required this.wait,\n  });\n\n  /// The copy method has a named [wait] parameter of type [Wait].\n  AppState copy({int? counter, String? description, Wait? wait}) => AppState(\n        counter: counter ?? this.counter,\n        description: description ?? this.description,\n        wait: wait ?? this.wait,\n      );\n\n  /// The [wait] parameter is instantiated to `Wait()`.\n  static AppState initialState() => AppState(\n        counter: 0,\n        description: \"\",\n        wait: Wait(),\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          counter == other.counter &&\n          description == other.description &&\n          wait == other.wait;\n\n  @override\n  int get hashCode => counter.hashCode ^ description.hashCode ^ wait.hashCode;\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) => StoreProvider<AppState>(\n        store: store,\n        child: MaterialApp(home: MyHomePageConnector()),\n      );\n}\n\n/// Use it like this:\n/// `class MyAction extends ReduxAction<AppState> with WithWaitState`\nmixin WithWaitState implements ReduxAction<AppState> {\n  // Wait starts here. Add the action itself (`this`) as a wait-flag reference.\n  @override\n  void before() => dispatch(WaitAction.add(this));\n\n  // Wait ends here. Remove the action from the wait-flag references.\n  @override\n  void after() => dispatch(WaitAction.remove(this));\n}\n\nclass IncrementAndGetDescriptionAction extends ReduxAction<AppState> with WithWaitState {\n  @override\n  Future<AppState> reduce() async {\n    dispatch(IncrementAction(amount: 1));\n\n    // Then, we start and wait for some asynchronous process.\n    Response response = await get(\n      Uri.parse(\"https://swapi.dev/api/people/${state.counter}/\"),\n    );\n    Map<String, dynamic> json = jsonDecode(response.body);\n    String description = json['name'] ?? 'Unknown character';\n\n    return state.copy(description: description);\n  }\n}\n\nclass IncrementAction extends ReduxAction<AppState> {\n  final int amount;\n\n  IncrementAction({required this.amount});\n\n  @override\n  AppState reduce() => state.copy(counter: state.counter + amount);\n}\n\n/// This widget is a connector. It connects the store to \"dumb-widget\".\nclass MyHomePageConnector extends StatelessWidget {\n  MyHomePageConnector({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, ViewModel>(\n      vm: () => Factory(this),\n      builder: (BuildContext context, ViewModel vm) => MyHomePage(\n        counter: vm.counter,\n        description: vm.description,\n        onIncrement: vm.onIncrement,\n        isWaiting: vm.isWaiting,\n      ),\n    );\n  }\n}\n\n/// Factory that creates a view-model for the StoreConnector.\nclass Factory extends VmFactory<AppState, MyHomePageConnector, ViewModel> {\n  Factory(connector) : super(connector);\n\n  @override\n  ViewModel fromStore() => ViewModel(\n        counter: state.counter,\n        description: state.description,\n\n        /// While action `IncrementAndGetDescriptionAction` is running,\n        /// [isWaiting] will be true.\n        isWaiting: state.wait.isWaitingForType<IncrementAndGetDescriptionAction>(),\n\n        onIncrement: () => dispatch(IncrementAndGetDescriptionAction()),\n      );\n}\n\n/// The view-model holds the part of the Store state the dumb-widget needs.\nclass ViewModel extends Vm {\n  final int counter;\n  final String description;\n  final bool isWaiting;\n  final VoidCallback onIncrement;\n\n  ViewModel({\n    required this.counter,\n    required this.description,\n    required this.isWaiting,\n    required this.onIncrement,\n  }) : super(equals: [counter, description, isWaiting]);\n}\n\nclass MyHomePage extends StatelessWidget {\n  final int counter;\n  final String description;\n  final bool isWaiting;\n  final VoidCallback onIncrement;\n\n  MyHomePage({\n    Key? key,\n    required this.counter,\n    required this.description,\n    required this.isWaiting,\n    required this.onIncrement,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: AppBar(title: const Text('Wait Action Example')),\n          body: Center(\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                const Text('You have pushed the button this many times:'),\n                Text('$counter', style: const TextStyle(fontSize: 30)),\n                Text(description,\n                    style: const TextStyle(fontSize: 15), textAlign: TextAlign.center),\n              ],\n            ),\n          ),\n          floatingActionButton: FloatingActionButton(\n            onPressed: onIncrement,\n            child: const Icon(Icons.add),\n          ),\n        ),\n        if (isWaiting) ModalBarrier(color: Colors.red.withAlpha((255.0 * 0.4).round())),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "example/pubspec.yaml",
    "content": "name: example\ndescription: Examples for async_redux.\npublish_to: \"none\"\nversion: 1.0.0+1\n\nenvironment:\n  sdk: '>=3.0.0 <4.0.0'\n  flutter: \">=3.16.0\"\n\ndependencies:\n  http: ^1.1.0\n  async_redux:\n    path: ../\n  flutter:\n    sdk: flutter\n  shared_preferences: ^2.5.4\n  fast_immutable_collections: ^11.1.0\n\ndev_dependencies:\n  test: ^1.16.0\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n"
  },
  {
    "path": "example/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    If you are serving your web app in a path other than the root, change the\n    href value below to reflect the base path you are serving from.\n\n    The path provided below has to start and end with a slash \"/\" in order for\n    it to work correctly.\n\n    For more details:\n    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base\n\n    This is a placeholder for base href that will be replaced by the value of\n    the `--base-href` argument provided to `flutter build`.\n  -->\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"A new Flutter project.\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"example\">\n  <link rel=\"apple-touch-icon\" href=\"icons/Icon-192.png\">\n\n  <!-- Favicon -->\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon.png\"/>\n\n  <title>example</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n</head>\n<body>\n  <script src=\"flutter_bootstrap.js\" async></script>\n</body>\n</html>\n"
  },
  {
    "path": "example/web/manifest.json",
    "content": "{\n    \"name\": \"example\",\n    \"short_name\": \"example\",\n    \"start_url\": \".\",\n    \"display\": \"standalone\",\n    \"background_color\": \"#0175C2\",\n    \"theme_color\": \"#0175C2\",\n    \"description\": \"A new Flutter project.\",\n    \"orientation\": \"portrait-primary\",\n    \"prefer_related_applications\": false,\n    \"icons\": [\n        {\n            \"src\": \"icons/Icon-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-maskable-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        },\n        {\n            \"src\": \"icons/Icon-maskable-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        }\n    ]\n}\n"
  },
  {
    "path": "example/windows/.gitignore",
    "content": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n"
  },
  {
    "path": "example/windows/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.14)\nproject(example LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"example\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(VERSION 3.14...3.25)\n\n# Define build configuration option.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n# Define settings for the Profile build mode.\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build; see runner/CMakeLists.txt.\nadd_subdirectory(\"runner\")\n\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# Support files are copied into place next to the executable, so that it can\n# run in place. This is done instead of making a separate bundle (as on Linux)\n# so that building and running from within Visual Studio will work.\nset(BUILD_BUNDLE_DIR \"$<TARGET_FILE_DIR:${BINARY_NAME}>\")\n# Make the \"install\" step default, as it's required to run.\nset(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Copy the native assets provided by the build.dart from all packages.\nset(NATIVE_ASSETS_DIR \"${PROJECT_BUILD_DIR}native_assets/windows/\")\ninstall(DIRECTORY \"${NATIVE_ASSETS_DIR}\"\n   DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n   COMPONENT Runtime)\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\ninstall(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  CONFIGURATIONS Profile;Release\n  COMPONENT Runtime)\n"
  },
  {
    "path": "example/windows/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.14)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# Set fallback configurations for older versions of the flutter tool.\nif (NOT DEFINED FLUTTER_TARGET_PLATFORM)\n  set(FLUTTER_TARGET_PLATFORM \"windows-x64\")\nendif()\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      ${FLUTTER_TARGET_PLATFORM} $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "example/windows/flutter/generated_plugin_registrant.cc",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <connectivity_plus/connectivity_plus_windows_plugin.h>\n\nvoid RegisterPlugins(flutter::PluginRegistry* registry) {\n  ConnectivityPlusWindowsPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"ConnectivityPlusWindowsPlugin\"));\n}\n"
  },
  {
    "path": "example/windows/flutter/generated_plugin_registrant.h",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUGIN_REGISTRANT_\n\n#include <flutter/plugin_registry.h>\n\n// Registers Flutter plugins.\nvoid RegisterPlugins(flutter::PluginRegistry* registry);\n\n#endif  // GENERATED_PLUGIN_REGISTRANT_\n"
  },
  {
    "path": "example/windows/flutter/generated_plugins.cmake",
    "content": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  connectivity_plus\n)\n\nlist(APPEND FLUTTER_FFI_PLUGIN_LIST\n)\n\nset(PLUGIN_BUNDLED_LIBRARIES)\n\nforeach(plugin ${FLUTTER_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})\n  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})\nendforeach(plugin)\n\nforeach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})\nendforeach(ffi_plugin)\n"
  },
  {
    "path": "example/windows/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(runner LANGUAGES CXX)\n\n# Define the application target. To change its name, change BINARY_NAME in the\n# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer\n# work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add preprocessor definitions for the build version.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION=\\\"${FLUTTER_VERSION}\\\"\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}\")\n\n# Disable Windows macros that collide with C++ standard library functions.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\n\n# Add dependency libraries and include directories. Add any application-specific\n# dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_link_libraries(${BINARY_NAME} PRIVATE \"dwmapi.lib\")\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n"
  },
  {
    "path": "example/windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n#else\n#define VERSION_AS_NUMBER 1,0,0,0\n#endif\n\n#if defined(FLUTTER_VERSION)\n#define VERSION_AS_STRING FLUTTER_VERSION\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"com.example\" \"\\0\"\n            VALUE \"FileDescription\", \"example\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"example\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2025 com.example. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"example.exe\" \"\\0\"\n            VALUE \"ProductName\", \"example\" \"\\0\"\n            VALUE \"ProductVersion\", VERSION_AS_STRING \"\\0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1252\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "example/windows/runner/flutter_window.cpp",
    "content": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n\n  flutter_controller_->engine()->SetNextFrameCallback([&]() {\n    this->Show();\n  });\n\n  // Flutter can complete the first frame before the \"show window\" callback is\n  // registered. The following call ensures a frame is pending to ensure the\n  // window is shown. It is a no-op if the first frame hasn't completed yet.\n  flutter_controller_->ForceRedraw();\n\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nLRESULT\nFlutterWindow::MessageHandler(HWND hwnd, UINT const message,\n                              WPARAM const wparam,\n                              LPARAM const lparam) noexcept {\n  // Give Flutter, including plugins, an opportunity to handle window messages.\n  if (flutter_controller_) {\n    std::optional<LRESULT> result =\n        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,\n                                                      lparam);\n    if (result) {\n      return *result;\n    }\n  }\n\n  switch (message) {\n    case WM_FONTCHANGE:\n      flutter_controller_->engine()->ReloadSystemFonts();\n      break;\n  }\n\n  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);\n}\n"
  },
  {
    "path": "example/windows/runner/flutter_window.h",
    "content": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n\n#include <memory>\n\n#include \"win32_window.h\"\n\n// A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n public:\n  // Creates a new FlutterWindow hosting a Flutter view running |project|.\n  explicit FlutterWindow(const flutter::DartProject& project);\n  virtual ~FlutterWindow();\n\n protected:\n  // Win32Window:\n  bool OnCreate() override;\n  void OnDestroy() override;\n  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,\n                         LPARAM const lparam) noexcept override;\n\n private:\n  // The project to run.\n  flutter::DartProject project_;\n\n  // The Flutter instance hosted by this window.\n  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;\n};\n\n#endif  // RUNNER_FLUTTER_WINDOW_H_\n"
  },
  {
    "path": "example/windows/runner/main.cpp",
    "content": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_window.h\"\n#include \"utils.h\"\n\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command) {\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  std::vector<std::string> command_line_arguments =\n      GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n  if (!window.Create(L\"example\", origin, size)) {\n    return EXIT_FAILURE;\n  }\n  window.SetQuitOnClose(true);\n\n  ::MSG msg;\n  while (::GetMessage(&msg, nullptr, 0, 0)) {\n    ::TranslateMessage(&msg);\n    ::DispatchMessage(&msg);\n  }\n\n  ::CoUninitialize();\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "example/windows/runner/resource.h",
    "content": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON                    101\n\n// Next default values for new objects\n//\n#ifdef APSTUDIO_INVOKED\n#ifndef APSTUDIO_READONLY_SYMBOLS\n#define _APS_NEXT_RESOURCE_VALUE        102\n#define _APS_NEXT_COMMAND_VALUE         40001\n#define _APS_NEXT_CONTROL_VALUE         1001\n#define _APS_NEXT_SYMED_VALUE           101\n#endif\n#endif\n"
  },
  {
    "path": "example/windows/runner/runner.exe.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10 and Windows 11 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n    </application>\n  </compatibility>\n</assembly>\n"
  },
  {
    "path": "example/windows/runner/utils.cpp",
    "content": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iostream>\n\nvoid CreateAndAttachConsole() {\n  if (::AllocConsole()) {\n    FILE *unused;\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stdout)) {\n      _dup2(_fileno(stdout), 1);\n    }\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stderr)) {\n      _dup2(_fileno(stdout), 2);\n    }\n    std::ios::sync_with_stdio();\n    FlutterDesktopResyncOutputStreams();\n  }\n}\n\nstd::vector<std::string> GetCommandLineArguments() {\n  // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.\n  int argc;\n  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);\n  if (argv == nullptr) {\n    return std::vector<std::string>();\n  }\n\n  std::vector<std::string> command_line_arguments;\n\n  // Skip the first argument as it's the binary name.\n  for (int i = 1; i < argc; i++) {\n    command_line_arguments.push_back(Utf8FromUtf16(argv[i]));\n  }\n\n  ::LocalFree(argv);\n\n  return command_line_arguments;\n}\n\nstd::string Utf8FromUtf16(const wchar_t* utf16_string) {\n  if (utf16_string == nullptr) {\n    return std::string();\n  }\n  unsigned int target_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, nullptr, 0, nullptr, nullptr)\n    -1; // remove the trailing null character\n  int input_length = (int)wcslen(utf16_string);\n  std::string utf8_string;\n  if (target_length == 0 || target_length > utf8_string.max_size()) {\n    return utf8_string;\n  }\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      input_length, utf8_string.data(), target_length, nullptr, nullptr);\n  if (converted_length == 0) {\n    return std::string();\n  }\n  return utf8_string;\n}\n"
  },
  {
    "path": "example/windows/runner/utils.h",
    "content": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the process, and redirects stdout and stderr to\n// it for both the runner and the Flutter library.\nvoid CreateAndAttachConsole();\n\n// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string\n// encoded in UTF-8. Returns an empty std::string on failure.\nstd::string Utf8FromUtf16(const wchar_t* utf16_string);\n\n// Gets the command line arguments passed in as a std::vector<std::string>,\n// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.\nstd::vector<std::string> GetCommandLineArguments();\n\n#endif  // RUNNER_UTILS_H_\n"
  },
  {
    "path": "example/windows/runner/win32_window.cpp",
    "content": "#include \"win32_window.h\"\n\n#include <dwmapi.h>\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\n/// Window attribute that enables dark mode window decorations.\n///\n/// Redefined in case the developer's machine has a Windows SDK older than\n/// version 10.0.22000.0.\n/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute\n#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE\n#define DWMWA_USE_IMMERSIVE_DARK_MODE 20\n#endif\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n/// Registry key for app theme preference.\n///\n/// A value of 0 indicates apps should use dark mode. A non-zero or missing\n/// value indicates apps should use light mode.\nconstexpr const wchar_t kGetPreferredBrightnessRegKey[] =\n  L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize\";\nconstexpr const wchar_t kGetPreferredBrightnessRegValue[] = L\"AppsUseLightTheme\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n  }\n  FreeLibrary(user32_module);\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registrar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::Create(const std::wstring& title,\n                         const Point& origin,\n                         const Size& size) {\n  Destroy();\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  UpdateTheme(window);\n\n  return OnCreate();\n}\n\nbool Win32Window::Show() {\n  return ShowWindow(window_handle_, SW_SHOWNORMAL);\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n\n    case WM_DWMCOLORIZATIONCOLORCHANGED:\n      UpdateTheme(hwnd);\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n\nvoid Win32Window::UpdateTheme(HWND const window) {\n  DWORD light_mode;\n  DWORD light_mode_size = sizeof(light_mode);\n  LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,\n                               kGetPreferredBrightnessRegValue,\n                               RRF_RT_REG_DWORD, nullptr, &light_mode,\n                               &light_mode_size);\n\n  if (result == ERROR_SUCCESS) {\n    BOOL enable_dark_mode = light_mode == 0;\n    DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,\n                          &enable_dark_mode, sizeof(enable_dark_mode));\n  }\n}\n"
  },
  {
    "path": "example/windows/runner/win32_window.h",
    "content": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <memory>\n#include <string>\n\n// A class abstraction for a high DPI-aware Win32 Window. Intended to be\n// inherited from by classes that wish to specialize with custom\n// rendering and input handling\nclass Win32Window {\n public:\n  struct Point {\n    unsigned int x;\n    unsigned int y;\n    Point(unsigned int x, unsigned int y) : x(x), y(y) {}\n  };\n\n  struct Size {\n    unsigned int width;\n    unsigned int height;\n    Size(unsigned int width, unsigned int height)\n        : width(width), height(height) {}\n  };\n\n  Win32Window();\n  virtual ~Win32Window();\n\n  // Creates a win32 window with |title| that is positioned and sized using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size this function will scale the inputted width and height as\n  // as appropriate for the default monitor. The window is invisible until\n  // |Show| is called. Returns true if the window was created successfully.\n  bool Create(const std::wstring& title, const Point& origin, const Size& size);\n\n  // Show the current window. Returns true if the window was successfully shown.\n  bool Show();\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  // Update the window frame's theme to match the system theme.\n  static void UpdateTheme(HWND const window);\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  },
  {
    "path": "lib/async_redux.dart",
    "content": "export 'package:async_redux_core/async_redux_core.dart';\n\nexport 'src/action_mixins.dart';\nexport 'src/action_observer.dart';\nexport 'src/advanced_user_exception.dart';\nexport 'src/cache.dart';\nexport 'src/cloud_sync.dart';\nexport 'src/connection_exception.dart';\nexport 'src/error_observer.dart';\nexport 'src/event_redux.dart';\nexport 'src/global_wrap_error.dart';\nexport 'src/log.dart';\nexport 'src/mock_build_context.dart';\nexport 'src/mock_store.dart';\nexport 'src/model_observer.dart';\nexport 'src/navigate_action.dart';\nexport 'src/persistor.dart';\nexport 'src/state_observer.dart';\nexport 'src/store.dart';\nexport 'src/store_exception.dart';\nexport 'src/store_provider_and_connector.dart';\nexport 'src/store_tester.dart';\nexport 'src/test_info.dart';\nexport 'src/user_exception_dialog.dart';\nexport 'src/view_model.dart';\nexport 'src/wait.dart';\nexport 'src/wait_action.dart';\nexport 'src/wrap_reduce.dart';\n"
  },
  {
    "path": "lib/local_json_persist.dart",
    "content": "export 'src/local_json_persist.dart';\n"
  },
  {
    "path": "lib/local_persist.dart",
    "content": "export 'src/local_persist.dart';\n"
  },
  {
    "path": "lib/src/action_mixins.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:connectivity_plus/connectivity_plus.dart';\nimport 'package:fast_immutable_collections/fast_immutable_collections.dart';\nimport 'package:meta/meta.dart';\n\n/// Mixin [CheckInternet] can be used to check if there is internet when you\n/// run some action that needs internet connection. Just add `with CheckInternet`\n/// to your action. For example:\n///\n/// ```dart\n/// class LoadText extends ReduxAction<AppState> with CheckInternet {\n///   Future<String> reduce() async {\n///\n///   Response response = await get(Uri.parse(\"https://swapi.dev/api/people/42/\"));\n///   Map<String, dynamic> json = jsonDecode(response.body);\n///   return json['name'] ?? 'Unknown';\n///   }\n/// }\n/// ```\n///\n/// It will automatically check if there is internet before running the action.\n/// If there is no internet, the action will fail, stop executing, and will\n/// show a dialog to the user with title:\n/// 'There is no Internet' and content: 'Please, verify your connection.'.\n///\n/// Also, you can display some information in your widgets when the action fails:\n///\n/// ```dart\n/// if (context.isFailed(LoadText)) Text('No Internet connection');\n/// ```\n///\n/// Or you can use the exception text itself:\n/// ```dart\n/// if (context.isFailed(LoadText)) Text(context.exceptionFor(LoadText)?.errorText ?? 'No Internet connection');\n/// ```\n///\n/// If you don't want the dialog to open, you can add the [NoDialog] mixin.\n///\n/// If you want to customize the dialog or the `errorText`, you can override the\n/// method [connectionException] and return a [UserException] with the desired\n/// message.\n///\n/// IMPORTANT: It only checks if the internet is on or off on the device,\n/// not if the internet provider is really providing the service or if the\n/// server is available. So, it is possible that the check succeeds\n/// but internet requests still fail.\n///\n/// Notes:\n/// - This mixin can safely be combined with [NonReentrant] or [Throttle] (not both).\n/// - It should not be combined with other mixins or classes that override [before].\n/// - It should not be combined with other mixins or classes that check the internet connection.\n/// - It should not be combined with [AbortWhenNoInternet] and [UnlimitedRetryCheckInternet].\n///\n/// See also:\n/// * [NoDialog] - To just show a message in your widget, and not open a dialog.\n/// * [AbortWhenNoInternet] - If you want to silently abort the action when there is no internet.\n///\nmixin CheckInternet<St> on ReduxAction<St> {\n  bool get ifOpenDialog => true;\n\n  UserException connectionException(List<ConnectivityResult> result) =>\n      ConnectionException.noConnectivity;\n\n  /// If you are running tests, you can override this getter to simulate the\n  /// internet connection as on or off:\n  ///\n  /// - Return `true` if there IS internet.\n  /// - Return `false` if there is NO internet.\n  /// - Return `null` to use the real internet connection status (default).\n  ///\n  /// If you want to change this for all actions using mixins [CheckInternet],\n  /// [AbortWhenNoInternet], and [UnlimitedRetryCheckInternet], you can\n  /// do that at the store level:\n  ///\n  /// ```dart\n  /// store.forceInternetOnOffSimulation = () => false;\n  /// ```\n  ///\n  /// Using [Store.forceInternetOnOffSimulation] is also useful during tests,\n  /// for testing what happens when you have no internet connection. And since\n  /// it's tied to the store, it automatically resets when the store is\n  /// recreated.\n  ///\n  bool? get internetOnOffSimulation => store.forceInternetOnOffSimulation();\n\n  Future<List<ConnectivityResult>> checkConnectivity() async {\n    if (internetOnOffSimulation != null)\n      return internetOnOffSimulation!\n          ? [ConnectivityResult.wifi]\n          : [ConnectivityResult.none];\n\n    return await (Connectivity().checkConnectivity());\n  }\n\n  @mustCallSuper\n  @override\n  Future<void> before() async {\n    _cannot_combine_mixins_CheckInternet_AbortWhenNoInternet_UnlimitedRetryCheckInternet();\n\n    super.before();\n    var result = await checkConnectivity();\n\n    if (result.contains(ConnectivityResult.none))\n      throw connectionException(result).withDialog(ifOpenDialog);\n  }\n\n  void\n      _cannot_combine_mixins_CheckInternet_AbortWhenNoInternet_UnlimitedRetryCheckInternet() {\n    _incompatible<CheckInternet, AbortWhenNoInternet>(this);\n    _incompatible<CheckInternet, UnlimitedRetryCheckInternet>(this);\n  }\n}\n\n/// Mixin [NoDialog] can only be applied on [CheckInternet]. Example:\n///\n/// ```dart\n/// class LoadText extends ReduxAction<AppState> with CheckInternet, NoDialog {\n///   Future<String> reduce() async {\n///     Response response = await get(Uri.parse(\"https://swapi.dev/api/people/42/\"));\n///     Map<String, dynamic> json = jsonDecode(response.body);\n///     return json['name'] ?? 'Unknown';\n///   }\n/// }\n/// ```\n///\n/// It will turn off showing a dialog when there is no internet.\n/// But you can still display some information in your widgets:\n///\n/// ```dart\n/// if (context.isFailed(LoadText)) Text('No Internet connection');\n/// ```\n///\n/// Or you can use the exception text itself:\n/// ```dart\n/// if (context.isFailed(LoadText)) Text(context.exceptionFor(LoadText)?.errorText ?? 'No Internet connection');\n/// ```\n///\nmixin NoDialog<St> on CheckInternet<St> {\n  @override\n  bool get ifOpenDialog => false;\n}\n\n/// Mixin [AbortWhenNoInternet] can be used to check if there is internet when\n/// you run some action that needs it. If there is no internet, the action will\n/// abort silently, as if it had never been dispatched.\n///\n/// Just add `with AbortWhenNoInternet` to your action. For example:\n///\n/// ```dart\n/// class LoadText extends ReduxAction<AppState> with AbortWhenNoInternet {\n///   Future<String> reduce() async {\n///     Response response = await get(Uri.parse(\"https://swapi.dev/api/people/42/\"));\n///     Map<String, dynamic> json = jsonDecode(response.body);\n///     return json['name'] ?? 'Unknown';\n///   }\n/// }\n/// ```\n///\n/// IMPORTANT: It only checks if the internet is on or off on the device, not if the internet\n/// provider is really providing the service or if the server is available. So, it is possible that\n/// this function returns true and the request still fails.\n///\n/// Notes:\n/// - This mixin can safely be combined with [NonReentrant] or [Throttle] (not both).\n/// - It should not be combined with other mixins or classes that override [before].\n/// - It should not be combined with other mixins or classes that check the internet connection.\n/// - It should not be combined with [CheckInternet], [NoDialog], and [UnlimitedRetryCheckInternet].\n///\n/// See also:\n/// * [CheckInternet] - If you want to show a dialog to the user when there is no internet.\n/// * [NoDialog] - To just show a message in your widget, and not open a dialog.\n///\nmixin AbortWhenNoInternet<St> on ReduxAction<St> {\n  //\n  /// If you are running tests, you can override this getter to simulate the\n  /// internet connection as on or off:\n  ///\n  /// - Return `true` if there IS internet.\n  /// - Return `false` if there is NO internet.\n  /// - Return `null` to use the real internet connection status (default).\n  ///\n  /// If you want to change this for all actions using mixins [CheckInternet],\n  /// [AbortWhenNoInternet], and [UnlimitedRetryCheckInternet], you can\n  /// do that at the store level:\n  ///\n  /// ```dart\n  /// store.forceInternetOnOffSimulation = () => false;\n  /// ```\n  ///\n  /// Using [Store.forceInternetOnOffSimulation] is also useful during tests,\n  /// for testing what happens when you have no internet connection. And since\n  /// it's tied to the store, it automatically resets when the store is\n  /// recreated.\n  ///\n  bool? get internetOnOffSimulation => store.forceInternetOnOffSimulation();\n\n  Future<List<ConnectivityResult>> checkConnectivity() async {\n    if (internetOnOffSimulation != null)\n      return internetOnOffSimulation!\n          ? [ConnectivityResult.wifi]\n          : [ConnectivityResult.none];\n\n    return await (Connectivity().checkConnectivity());\n  }\n\n  @mustCallSuper\n  @override\n  Future<void> before() async {\n    _cannot_combine_mixins_CheckInternet_AbortWhenNoInternet_UnlimitedRetryCheckInternet();\n\n    super.before();\n    var result = await checkConnectivity();\n    if (result.contains(ConnectivityResult.none))\n      throw AbortDispatchException();\n  }\n\n  void\n      _cannot_combine_mixins_CheckInternet_AbortWhenNoInternet_UnlimitedRetryCheckInternet() {\n    _incompatible<AbortWhenNoInternet, CheckInternet>(this);\n    _incompatible<AbortWhenNoInternet, UnlimitedRetryCheckInternet>(this);\n  }\n}\n\n/// Mixin [NonReentrant] can be used to abort the action in case the action\n/// is still running from a previous dispatch. Just add `with NonReentrant`\n/// to your action. For example:\n///\n/// ```dart\n/// class SaveAction extends ReduxAction<AppState> with NonReentrant {\n///   Future<String> reduce() async {\n///     await http.put('http://myapi.com/save', body: 'data');\n///   }}\n/// ```\n///\n/// ## Advanced usage\n///\n/// The non-reentrant check is, by default, based on the action [runtimeType].\n/// This means it will abort an action if another action of the same runtimeType\n/// is currently running. If you want to check based on more than simply the\n/// [runtimeType], you can override the [nonReentrantKeyParams] method.\n/// For example, here we use a field of the action to differentiate:\n///\n/// ```dart\n/// class SaveItem extends ReduxAction<AppState> with NonReentrant {\n///    final String itemId;\n///    SaveItem(this.itemId);\n///\n///    Object? nonReentrantKeyParams() => itemId;\n///    ...\n/// }\n/// ```\n///\n/// With this setup, `SaveItem('A')` and `SaveItem('B')` can run in parallel,\n/// but two `SaveItem('A')` cannot.\n///\n/// You can also use [computeNonReentrantKey] if you want different action types\n/// to share the same non-reentrant key. Check the documentation of that method\n/// for more information.\n///\n/// Notes:\n/// - This mixin can safely be combined with [CheckInternet], [NoDialog], and [AbortWhenNoInternet].\n/// - It should not be combined with other mixins or classes that override [abortDispatch] or [after].\n/// - It should not be combined with [Throttle], [UnlimitedRetryCheckInternet], or [Fresh].\n///\nmixin NonReentrant<St> on ReduxAction<St> {\n  //\n  /// By default the non-reentrant key is based on the action [runtimeType].\n  /// Override [nonReentrantKeyParams] so that actions of the SAME TYPE\n  /// but with different parameters do not block each other.\n  ///\n  /// For example:\n  ///\n  /// ```dart\n  /// class SaveItem extends ReduxAction<AppState> with NonReentrant {\n  ///   final String itemId;\n  ///   SaveItem(this.itemId);\n  ///\n  ///   Object? nonReentrantKeyParams() => itemId;\n  ///   ...\n  /// }\n  /// ```\n  ///\n  /// Now `SaveItem('A')` and `SaveItem('B')` can run in parallel,\n  /// but two concurrent dispatches of `SaveItem('A')` will not both run.\n  ///\n  Object? nonReentrantKeyParams() => null;\n\n  /// By default the non-reentrant key combines the action [runtimeType]\n  /// with [nonReentrantKeyParams]. Override this method if you want\n  /// different action types to share the same non-reentrant key.\n  ///\n  /// ```dart\n  /// class SaveUser extends ReduxAction<AppState> with NonReentrant {\n  ///   final String oderId;\n  ///   SaveUser(this.oderId);\n  ///\n  ///   Object? computeNonReentrantKey() => orderId;\n  ///   ...\n  /// }\n  ///\n  /// class DeleteUser extends ReduxAction<AppState> with NonReentrant {\n  ///   final String oderId;\n  ///   DeleteUser(this.oderId);\n  ///\n  ///   Object? computeNonReentrantKey() => orderId;\n  ///   ...\n  /// }\n  /// ```\n  ///\n  /// With this setup, `SaveUser('123')` and `DeleteUser('123')` cannot run\n  /// at the same time because they share the same key.\n  ///\n  Object computeNonReentrantKey() => (runtimeType, nonReentrantKeyParams());\n\n  /// The set of keys that are currently running.\n  Set<Object?> get _nonReentrantKeySet =>\n      store.internalMixinProps.nonReentrantKeySet;\n\n  Object? _nonReentrantKey;\n\n  @override\n  bool abortDispatch() {\n    _cannot_combine_mixins_Fresh_Throttle_NonReentrant_UnlimitedRetryCheckInternet();\n\n    // This mixin should not be combined with other mixins or classes that\n    // set `abortDispatch`, but just in case, we call super first, and we\n    // only set the lock if `super.abortDispatch()` does not want to abort.\n    //\n    // In the code `class MyAction extends AppAction with NonReentrant, OtherMixin`\n    // the order of execution is:\n    //\n    // 1. MyAction.abortDispatch()\n    // 2. OtherMixin.abortDispatch()\n    // 3. NonReentrant.abortDispatch()\n    // 4. AppAction.abortDispatch()\n    //\n    // In other words, any mixin or base class that runs `abortDispatch`\n    // before `NonReentrant` (in the example `MyAction` and `OtherMixin`)\n    // should only call `NonReentrant`'s `abortDispatch` if it wants to proceed.\n    // If the mixin or base class wants to abort (return true), it should not\n    // call NonReentrant with `super.abortDispatch()`.\n    //\n    // For example, this is wrong for `MyAction` or `OtherMixin`:\n    // ```dart\n    // bool abortDispatch() {\n    //\n    //   // Wrong: Always calls NonReentrant's abortDispatch\n    //   if (super.abortDispatch()) return true;\n    //\n    //   bool otherConditions = ...\n    //   return otherConditions;\n    // }\n    // ```\n    // And this is right:\n    // ```dart\n    // bool abortDispatch() {\n    //\n    //   bool otherConditions = ...\n    //   if (otherConditions) return true;\n    //\n    //   // Last thing (NonReentrant's abortDispatch is conditionally called)\n    //   return super.abortDispatch();\n    // }\n    // ```\n    if (super.abortDispatch()) return true;\n\n    _nonReentrantKey = computeNonReentrantKey();\n\n    // If the key is already in the set, abort.\n    if (_nonReentrantKeySet.contains(_nonReentrantKey))\n      return true;\n    //\n    // Otherwise, add the key and allow dispatch.\n    else {\n      _nonReentrantKeySet.add(_nonReentrantKey);\n      return false;\n    }\n  }\n\n  @override\n  void after() {\n    // Remove the key when the action finishes (success or failure).\n    _nonReentrantKeySet.remove(_nonReentrantKey);\n  }\n\n  void\n      _cannot_combine_mixins_Fresh_Throttle_NonReentrant_UnlimitedRetryCheckInternet() {\n    _incompatible<NonReentrant, Fresh>(this);\n    _incompatible<NonReentrant, Throttle>(this);\n    _incompatible<NonReentrant, UnlimitedRetryCheckInternet>(this);\n    _incompatible<NonReentrant, OptimisticCommand>(this);\n  }\n}\n\n/// Mixin [Retry] will retry the [reduce] method if it throws an error.\n/// Note: If the `before` method throws an error, the retry will NOT happen.\n///\n/// You can override the following parameters:\n///\n/// * [initialDelay]: The delay before the first retry attempt.\n///   Default is `350` milliseconds.\n///\n/// * [multiplier]: The factor by which the delay increases for each subsequent\n///   retry. Default is `2`, which means the default delays are: 350 millis,\n///   700 millis, and 1.4 seg.\n///\n/// * [maxRetries]: The maximum number of retries before giving up.\n///   Default is `3`, meaning it will try a total of 4 times.\n///\n/// * [maxDelay]: The maximum delay between retries to avoid excessively long\n///   wait times. Default is `5` seconds.\n///\n/// If you want to retry unlimited times, you can add the [UnlimitedRetries] mixin.\n///\n/// Note: The retry delay only starts after the reducer finishes executing. For example,\n/// if the reducer takes 1 second to fail, and the retry delay is 350 millis, the first\n/// retry will happen 1.35 seconds after the first reducer started.\n///\n/// When the action finally fails (`maxRetries` was reached),\n/// the last error will be rethrown, and the previous ones will be ignored.\n///\n/// You should NOT combine this with [CheckInternet] or [AbortWhenNoInternet],\n/// because the retry will not work.\n///\n/// However, for most actions that use [Retry], consider also adding [NonReentrant] to avoid\n/// multiple instances of the same action running at the same time:\n///\n/// ```dart\n/// class MyAction extends ReduxAction<AppState> with Retry, NonReentrant { ... }\n/// ```\n///\n/// Keep in mind that all actions using the [Retry] mixin will become asynchronous,\n/// even if the original action was synchronous.\n///\n/// Notes:\n/// - Combining [Retry] with [CheckInternet] or [AbortWhenNoInternet] will\n///   not retry when there is no internet. It will only retry if there IS\n///   internet but the action fails for some other reason. To retry indefinitely\n///   until internet is available, use [UnlimitedRetryCheckInternet] instead.\n/// - It should not be combined with [Debounce], [UnlimitedRetryCheckInternet].\n/// - When combined with [OptimisticCommand], the retry logic is handled by\n///   [OptimisticCommand] to avoid UI flickering. Only the\n///   [OptimisticCommand.sendCommandToServer] call is retried, keeping the\n///   optimistic state in place. See [OptimisticCommand] for more details.\n///\nmixin Retry<St> on ReduxAction<St> {\n  //\n  /// The delay before the first retry attempt.\n  Duration get initialDelay => const Duration(milliseconds: 350);\n\n  /// The factor by which the delay increases for each subsequent retry.\n  /// Must be greater than 1, otherwise it will be set to 2.\n  double get multiplier => 2;\n\n  /// The maximum number of retries before giving up.\n  /// Must be greater than 0, otherwise it will not retry.\n  /// The total number of attempts is maxRetries + 1.\n  int get maxRetries => 3;\n\n  /// The maximum delay between retries to avoid excessively long wait times.\n  /// The default is 5 seconds.\n  Duration get maxDelay => const Duration(milliseconds: 5000);\n\n  int _attempts = 0;\n\n  /// The number of retry attempts so far. If the action has not been retried yet, it will be 0.\n  /// If the action finished successfully, it will be equal or less than [maxRetries].\n  /// If the action failed and gave up, it will be equal to [maxRetries] plus 1.\n  int get attempts => _attempts;\n\n  @override\n  Future<St?> wrapReduce(Reducer<St> reduce) async {\n    _cannot_combine_mixins_Debounce_Retry_UnlimitedRetryCheckInternet();\n    _cannot_combine_mixins_Retry_UnlimitedRetryCheckInternet_OptimisticSync_OptimisticSyncWithPush_ServerPush();\n\n    // When combined with OptimisticCommand, we skip the retry logic here.\n    // OptimisticCommand will handle retries internally to avoid UI flickering.\n    // See OptimisticCommand.reduce for details.\n    if (this is OptimisticCommand) {\n      FutureOr<St?> newState = reduce();\n      if (newState is Future) newState = await newState;\n      return newState;\n    }\n\n    FutureOr<St?> newState;\n\n    try {\n      await microtask;\n      newState = reduce();\n      if (newState is Future) newState = await newState;\n    }\n    //\n    catch (error) {\n      _attempts++;\n      if ((maxRetries >= 0) && (_attempts > maxRetries)) rethrow;\n\n      var currentDelay = nextDelay();\n      await Future.delayed(currentDelay);\n\n      // Retry the action.\n      return wrapReduce(reduce);\n    }\n    return newState;\n  }\n\n  Duration? _currentDelay;\n\n  /// Start with the [initialDelay], and then increase it by [multiplier] each time this is called.\n  /// If the delay exceeds [maxDelay], it will be set to [maxDelay].\n  Duration nextDelay() {\n    var _multiplier = multiplier;\n    if (_multiplier <= 1) _multiplier = 2;\n\n    _currentDelay = (_currentDelay == null) //\n        ? initialDelay //\n        : _currentDelay! * _multiplier;\n\n    if (_currentDelay! > maxDelay) _currentDelay = maxDelay;\n\n    return _currentDelay!;\n  }\n\n  void _cannot_combine_mixins_Debounce_Retry_UnlimitedRetryCheckInternet() {\n    _incompatible<Retry, Debounce>(this);\n    _incompatible<Retry, UnlimitedRetryCheckInternet>(this);\n    _incompatible<Retry, Polling>(this);\n  }\n\n  void\n      _cannot_combine_mixins_Retry_UnlimitedRetryCheckInternet_OptimisticSync_OptimisticSyncWithPush_ServerPush() {\n    _incompatible<Retry, UnlimitedRetryCheckInternet>(this);\n    _incompatible<Retry, OptimisticSync>(this);\n    _incompatible<Retry, OptimisticSyncWithPush>(this);\n    _incompatible<Retry, ServerPush>(this);\n  }\n}\n\n/// Mixin [UnlimitedRetries] can be added to the [Retry] mixin, to retry\n/// indefinitely:\n///\n/// ```dart\n/// class MyAction extends ReduxAction<AppState> with Retry, UnlimitedRetries { ... }\n/// ```\n///\n/// This is the same as setting [maxRetries] to -1.\n///\n/// Note: If you `await dispatchAndWait(action)` and the action uses [UnlimitedRetries],\n/// it may never finish if it keeps failing. So, be careful when using it.\n///\nmixin UnlimitedRetries<St> on Retry<St> {\n  @override\n  int get maxRetries => -1;\n}\n\n/// Mixin [OptimisticCommand] is for actions that represent a command.\n/// A command is something you want to run on the server once per dispatch.\n/// Typical examples are:\n///\n/// * Create something (add todo, create comment, send message)\n/// * Delete something\n/// * Submit a form\n/// * Upload a file\n/// * Checkout, place order, confirm payment\n///\n/// This mixin gives fast UI feedback by applying an optimistic state change\n/// immediately, then running the command on the server, and optionally rolling\n/// back and reloading.\n///\n///\n/// ## When to use the `OptimisticSync` mixin instead\n///\n/// Use [OptimisticSync] or [OptimisticSyncWithPush] when the action is a save\n/// operation, meaning only the final value matters and intermediate values\n/// can be skipped. Typical examples are:\n///\n/// * Like or follow toggle\n/// * Settings switch\n/// * Slider, checkbox\n/// * Update a field where the last value wins\n///\n/// In save operations, users may tap many times quickly. `OptimisticSync` is\n/// built for that and will coalesce rapid changes into a minimal number of\n/// server calls. [OptimisticCommand] is not built for that.\n///\n///\n/// ## The problem\n///\n/// Let's use a Todo app as an example. We want to save a new Todo to a\n/// TodoList. This code saves the Todo, then reloads the TodoList from the cloud:\n///\n/// ```dart\n/// class SaveTodo extends ReduxAction<AppState> {\n///   final Todo newTodo;\n///   SaveTodo(this.newTodo);\n///\n///   Future<AppState> reduce() async {\n///     try {\n///       // Saves the new Todo to the cloud.\n///       await saveTodo(newTodo);\n///     } finally {\n///       // Loads the complete TodoList from the cloud.\n///       var reloadedTodoList = await loadTodoList();\n///       return state.copy(todoList: reloadedTodoList);\n///     }\n///   }\n/// }\n/// ```\n///\n/// The problem with the above code is that it may take a second to update the\n/// todoList on screen, while we save then load.\n///\n/// The solution is to optimistically update the TodoList before saving:\n///\n/// ```dart\n/// class SaveTodo extends ReduxAction<AppState> {\n///   final Todo newTodo;\n///   SaveTodo(this.newTodo);\n///\n///   Future<AppState> reduce() async {\n///     // Updates the TodoList optimistically.\n///     dispatch(UpdateStateAction((state)\n///       => state.copy(todoList: state.todoList.add(newTodo))));\n///\n///     try {\n///       // Saves the new Todo to the cloud.\n///       await saveTodo(newTodo);\n///     } finally {\n///       // Loads the complete TodoList from the cloud.\n///       var reloadedTodoList = await loadTodoList();\n///       return state.copy(todoList: reloadedTodoList);\n///     }\n///   }\n/// }\n/// ```\n///\n/// That's better. But if saving fails, users still have to wait for the reload\n/// until they see the reverted state. We can further improve this:\n///\n/// ```dart\n/// class SaveTodo extends ReduxAction<AppState> {\n///   final Todo newTodo;\n///   SaveTodo(this.newTodo);\n///\n///   Future<AppState> reduce() async {\n///     // Updates the TodoList optimistically.\n///     var newTodoList = state.todoList.add(newTodo);\n///     dispatchState(state.copy(todoList: newTodoList));\n///\n///     try {\n///       // Saves the new Todo to the cloud.\n///       await saveTodo(newTodo);\n///     } catch (e) {\n///       // If the state still contains our optimistic update, we rollback.\n///       // If the state now contains something else, we do not rollback.\n///       if (state.todoList == newTodoList) {\n///         return state.copy(todoList: initialState.todoList); // Rollback.\n///       }\n///       rethrow;\n///     } finally {\n///       // Loads the complete TodoList from the cloud.\n///       var reloadedTodoList = await loadTodoList();\n///       dispatchState(state.copy(todoList: reloadedTodoList));\n///     }\n///   }\n/// }\n/// ```\n///\n/// Now the user sees the rollback immediately after the saving fails. The\n/// [OptimisticCommand] mixin helps you implement this pattern easily, and\n/// takes care of the edge cases.\n///\n/// ## How to use this mixin\n///\n/// You must provide:\n/// * [optimisticValue] returns the optimistic value you want to apply right away\n/// * [getValueFromState] extracts the current value from a given state\n/// * [applyValueToState] applies a value to a given state and returns the new state\n/// * [sendCommandToServer] runs the server command (it may use the action fields)\n/// * [reloadFromServer] optionally reloads from the server (do not override to skip)\n/// * [applyReloadResultToState] applies the reload result to the state (the default uses [applyValueToState])\n///\n/// Important details:\n///\n/// * The optimistic update is applied immediately.\n///\n/// * If [sendCommandToServer] fails, rollback happens only if the current state\n///   still matches the optimistic value created by this dispatch. The rollback\n///   restores the value from [initialState].\n///\n/// * Reload is optional. If implemented, it runs after [sendCommandToServer]\n///   finishes, only in case of error (this can be changed by overriding\n///   [shouldReload] to return true).\n///\n/// ### Complete example using the mixin\n///\n/// ```dart\n/// class SaveTodo extends AppAction with OptimisticCommand {\n///   final Todo newTodo;\n///   SaveTodo(this.newTodo);\n///\n///   // The new Todo is going to be optimistically applied to the state, right away.\n///   @override\n///   Object? optimisticValue() => newTodo;\n///\n///   // We teach the action how to read the Todo from the state.\n///   @override\n///   Object? getValueFromState(AppState state) => state.todoList.getById(newTodo.id);\n///\n///   // Apply the value to the state.\n///   @override\n///   AppState applyValueToState(AppState state, Object? value)\n///     => state.copy(todoList: state.todoList.add(newTodo));\n///\n///   // Contact the server to send the command (save the Todo). I\n///   @override\n///   Future<Todo> sendCommandToServer(Object? newTodo) async => await saveTodo(newTodo);\n///\n///   // If the server returns a value, we may apply it to the state.\n///   @override\n///   AppState applyServerResponseToState(AppState state, Todo todo)\n///     => state.copy(todoList: state.todoList.add(todo));\n///\n///   // Reload from the cloud (in case of error).\n///   @override\n///   Future<Object?> reloadFromServer() async => await loadTodo();\n/// }\n/// ```\n///\n///\n/// ## Non-reentrant behavior\n///\n/// [OptimisticCommand] is always non-reentrant. If the same action is\n/// dispatched while a previous dispatch is still running, the new dispatch\n/// is aborted. This prevents race conditions such as:\n///\n/// * Conflicting optimistic updates overwriting each other\n/// * Incorrect rollback behavior (the rollback check may no longer match)\n/// * Race conditions in the reload phase\n/// * Server side conflicts from concurrent requests\n///\n/// Your UI should let the user know that the command is in progress,\n/// so they do not try to dispatch it again until it finishes. That's easy\n/// to do with AsyncRedux, just check if the action is in progress:\n///\n/// ```dart\n/// bool isSaving = context.isWaiting(SaveTodo);\n/// ```\n///\n/// By default, the non-reentrant check is based on the action [runtimeType].\n/// If your action has parameters and you want to allow concurrent dispatches\n/// for different parameters (for example, saving different items), override\n/// [nonReentrantKeyParams]. For example:\n///\n/// ```dart\n/// class SaveTodo extends AppAction with OptimisticCommand {\n///   final String orderId;\n///   SaveTodo(this.orderId);\n///\n///   @override\n///   Object? nonReentrantKeyParams() => orderId;\n///   ...\n/// }\n/// ```\n///\n/// This allows SaveTodo('A') and SaveTodo('B') to run concurrently, while\n/// blocking concurrent dispatches of SaveTodo('A') with itself.\n///\n/// This is useful for commands that you **do** want to run in parallel,\n/// as long as they are for different items. Common examples include:\n///\n/// * Uploading multiple files at the same time (key by fileId)\n/// * Sending multiple chat messages at the same time (key by clientMessageId)\n/// * Enqueuing multiple jobs at the same time (key by jobId)\n///\n/// In these cases, each key runs non-reentrantly, but different keys can run\n/// concurrently.\n///\n/// You can also use [computeNonReentrantKey] if you want different action types\n/// to share the same non-reentrant key. Check the documentation of that method\n/// for more information.\n///\n///\n/// ## [Retry]\n///\n/// When combined with [Retry], only the [sendCommandToServer] call is retried,\n/// not the optimistic update or rollback. This prevents UI flickering that\n/// would otherwise occur if the entire reduce was retried on each attempt.\n///\n/// The optimistic state remains in place during retries, and rollback only\n/// happens if all retry attempts fail.\n///\n///\n/// # [CheckInternet] or [AbortWhenNoInternet]\n///\n/// When combined with [CheckInternet] and [AbortWhenNoInternet], when offline:\n///\n/// * No optimistic state is applied\n/// * No lock is acquired\n/// * No server call is attempted\n/// * The action fails and your dialog shows (for [CheckInternet])\n///\n///\n/// Notes:\n/// - Equality checks use the == operator. Make sure your value type implements\n///   == in a way that makes sense for optimistic checks.\n/// - It can be combined with [Retry], [CheckInternet] and [AbortWhenNoInternet].\n/// - It should not be combined with [NonReentrant], [Throttle], [Debounce],\n///   [Fresh], [UnlimitedRetryCheckInternet], [UnlimitedRetries],\n///   [OptimisticSync], [OptimisticSyncWithPush], or [ServerPush].\n///\n/// See also:\n/// * [OptimisticSync] and [OptimisticSyncWithPush] for save operations.\n///\nmixin OptimisticCommand<St> on ReduxAction<St> {\n  //\n  /// Override this method to return the value that you want to update, and\n  /// that you want to apply optimistically to the state.\n  ///\n  /// You can access the fields of the action, and the current [state], and\n  /// return the new value.\n  ///\n  /// ```dart\n  /// Object? optimisticValue() => newTodo;\n  /// ```\n  Object? optimisticValue();\n\n  /// Using the given [state], you should apply the given [value] to it, and\n  /// return the result. This will be used to apply the optimistic value to\n  /// the state, and also later to rollback, if necessary, by applying the\n  /// initial value.\n  ///\n  /// ```dart\n  /// AppState applyValueToState(state, newTodoList)\n  ///   => state.copy(todoList: newTodoList);\n  /// ```\n  St applyValueToState(St state, Object? value);\n\n  /// Using the given [state], you should return the current value from that\n  /// state. This is used to check if the state still contains the optimistic\n  /// value, so the mixin knows whether it is safe to rollback.\n  ///\n  /// ```dart\n  /// Object? getValueFromState(AppState state) => state.todoList;\n  /// ```\n  Object? getValueFromState(St state);\n\n  /// You should save the [optimisticValue] or other related value in the cloud,\n  /// and optionally return the server's response.\n  ///\n  /// Note: You can ignore [optimisticValue] and use the action fields instead,\n  /// if that makes more sense for your API.\n  ///\n  /// If [sendCommandToServer] returns a non-null value, that value will be\n  /// passed to [applyServerResponseToState] to update the state.\n  ///\n  /// ```dart\n  /// Future<Object?> sendCommandToServer(newTodoList) async {\n  ///   var response = await saveTodo(newTodo);\n  ///   return response; // Return server-confirmed value, or null.\n  /// }\n  /// ```\n  Future<Object?> sendCommandToServer(Object? optimisticValue);\n\n  /// Override [applyServerResponseToState] to return a new state, where the\n  /// given [serverResponse] (previously received from the server when running\n  /// [sendCommandToServer]) is applied to the current [state]. Example:\n  ///\n  /// ```dart\n  /// AppState? applyServerResponseToState(state, serverResponse) =>\n  ///     state.copyWith(todoList: serverResponse.todoList);\n  /// ```\n  ///\n  /// Note [serverResponse] is never `null` here, because this method is only\n  /// called when [sendCommandToServer] returned a non-null value.\n  ///\n  /// If you decide you DO NOT want to apply the server response to the state,\n  /// simply return `null`.\n  ///\n  St? applyServerResponseToState(St state, Object serverResponse) => null;\n\n  /// Override to reload the value from the cloud.\n  /// If you want to skip reload, do not override this method.\n  ///\n  /// Note: If you are using a realtime database or WebSockets to receive\n  /// server pushed updates, you may not need to reload here.\n  ///\n  /// ```dart\n  /// Future<Object?> reloadFromServer() => loadTodoList();\n  /// ```\n  Future<Object?> reloadFromServer() {\n    throw UnimplementedError();\n  }\n\n  /// Returns the state to apply when the command fails and the mixin decides\n  /// it is safe to rollback.\n  ///\n  /// This method is called only when:\n  /// * [sendCommandToServer] throws, AND\n  /// * [shouldRollback] returns true. By default it returns true only if the\n  ///   current value in the store still matches the optimistic value created\n  ///   by this dispatch (so we do not rollback over newer changes).\n  ///\n  /// Parameters:\n  ///\n  /// * [initialValue] is the value extracted from [initialState] using\n  ///   [getValueFromState]. It represents what the value was when this action\n  ///   was first dispatched.\n  ///\n  /// * [optimisticValue] is the value returned by [optimisticValue] and applied\n  ///   optimistically by this dispatch.\n  ///\n  /// * [error] is the error thrown by [sendCommandToServer].\n  ///\n  /// By default, the mixin restores [initialValue] by calling [applyValueToState].\n  ///\n  /// Override this method if rollback is not simply \"put the old value back\".\n  /// For example, you may want to:\n  /// * Keep the optimistic item but mark it as failed.\n  /// * Remove only the item you added, while keeping other local changes.\n  /// * Roll back multiple parts of the state, not just the value handled by\n  ///   [applyValueToState].\n  ///\n  /// Return `null` to skip rollback even when the mixin would normally rollback.\n  ///\n  St? rollbackState({\n    required Object? initialValue,\n    required Object? optimisticValue,\n    required Object error,\n  }) =>\n      applyValueToState(state, initialValue);\n\n  /// Returns true if the mixin should rollback after [sendCommandToServer]\n  /// fails. This method is called only when [sendCommandToServer] throws.\n  ///\n  /// The default behavior is:\n  /// Rollback only if the current value in the store still matches the\n  /// optimistic value created by this dispatch. This avoids rolling back over\n  /// newer changes that may have happened while the request was in flight.\n  ///\n  /// Override this if you need a different safety rule. For example:\n  /// * You want to always rollback, even if something else changed.\n  /// * You want to rollback only if a specific item is still present.\n  /// * You want to rollback only for some errors.\n  ///\n  bool shouldRollback({\n    required Object? currentValue,\n    required Object? initialValue,\n    required Object? optimisticValue,\n    required Object error,\n  }) {\n    // Default: rollback only if we are still seeing our own optimistic value.\n    if (currentValue is ImmutableCollection &&\n        optimisticValue is ImmutableCollection) {\n      return currentValue.same(optimisticValue);\n    } else {\n      return currentValue == optimisticValue;\n    }\n  }\n\n  /// Whether the mixin should call [reloadFromServer].\n  ///\n  /// This method is called in `finally`, both on success and on error, before\n  /// the reload happens.\n  ///\n  /// Parameters:\n  /// * [currentValue] is the value currently in the store (extracted with\n  ///   [getValueFromState]) at the moment we are deciding whether to reload.\n  ///\n  /// * [lastAppliedValue] is the last value this action applied for the same\n  ///   state slice. It is the optimistic value on success, or the rollback value\n  ///   if rollback was applied.\n  ///\n  /// * [optimisticValue] is the value returned by [optimisticValue] and applied\n  ///   optimistically by this dispatch.\n  ///\n  /// * [rollbackValue] is `null` if no rollback state was applied. If rollback\n  ///   was applied, this is the value extracted from the rollback state using\n  ///   [getValueFromState].\n  ///\n  /// * [error] is `null` on success, or the error thrown by [sendCommandToServer]\n  ///   on failure.\n  ///\n  /// Default behavior:\n  /// Returns true, meaning: If [reloadFromServer] is implemented, we reload.\n  ///\n  /// Override this method if you want to skip reloading in some cases.\n  /// For example, reload only on error, or skip reload when the value already\n  /// changed to something else. For example:\n  ///\n  /// ```dart\n  /// bool shouldReload(...) => currentValue == lastAppliedValue;\n  /// bool shouldApplyReload(...) => currentValue == lastAppliedValue;\n  /// ```\n  ///\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error, // null on success\n  }) =>\n      error != null;\n\n  /// Returns true if the mixin should apply the result returned by\n  /// [reloadFromServer] to the state.\n  ///\n  /// This method is called after [reloadFromServer] completes, both on success\n  /// and on error.\n  ///\n  /// Parameters are the same as [shouldReload], plus:\n  /// * [reloadResult] is whatever [reloadFromServer] returned.\n  ///\n  /// Default behavior:\n  /// Always apply the reload result. This matches the common expectation that\n  /// if you chose to reload, the server is the source of truth.\n  ///\n  /// Override this method if you want to avoid overwriting newer local changes,\n  /// or if you need custom rules based on [reloadResult] or [error].\n  ///\n  bool shouldApplyReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? reloadResult,\n    required Object? error, // null on success\n  }) =>\n      true;\n\n  /// Applies the result returned by [reloadFromServer] to the state.\n  ///\n  /// Override this method when [reloadFromServer] returns something that is not\n  /// the same type or shape expected by [applyValueToState], or when applying\n  /// the reload requires updating multiple parts of the state.\n  ///\n  /// Return `null` to ignore the reload result.\n  ///\n  St? applyReloadResultToState(St state, Object? reloadResult) =>\n      applyValueToState(state, reloadResult);\n\n  /// By default the non-reentrant key is based on the action [runtimeType].\n  /// Override [nonReentrantKeyParams] so that actions of the SAME TYPE\n  /// but with different parameters do not block each other.\n  ///\n  /// For example:\n  ///\n  /// ```dart\n  /// class SaveItem extends AppAction with OptimisticCommand {\n  ///   final String itemId;\n  ///   SaveItem(this.itemId);\n  ///\n  ///   Object? nonReentrantKeyParams() => itemId;\n  ///   ...\n  /// }\n  /// ```\n  ///\n  /// Now `SaveItem('A')` and `SaveItem('B')` can run in parallel,\n  /// but two concurrent dispatches of `SaveItem('A')` will not both run.\n  ///\n  Object? nonReentrantKeyParams() => null;\n\n  /// By default the non-reentrant key combines the action [runtimeType]\n  /// with [nonReentrantKeyParams]. Override this method if you want\n  /// different action types to share the same non-reentrant key.\n  ///\n  /// ```dart\n  /// class SaveUser extends ReduxAction<AppState> with OptimisticCommand {\n  ///   final String oderId;\n  ///   SaveUser(this.oderId);\n  ///\n  ///   Object? computeNonReentrantKey() => orderId;\n  ///   ...\n  /// }\n  ///\n  /// class DeleteUser extends ReduxAction<AppState> with OptimisticCommand {\n  ///   final String oderId;\n  ///   DeleteUser(this.oderId);\n  ///\n  ///   Object? computeNonReentrantKey() => orderId;\n  ///   ...\n  /// }\n  /// ```\n  ///\n  /// With this setup, `SaveUser('123')` and `DeleteUser('123')` cannot run\n  /// at the same time because they share the same key.\n  ///\n  Object computeNonReentrantKey() => (runtimeType, nonReentrantKeyParams());\n\n  @override\n  Future<St?> reduce() async {\n    // Updates the value optimistically.\n    final optimistic = optimisticValue();\n    dispatchState(applyValueToState(state, optimistic));\n\n    Object? commandError;\n    Object? lastAppliedValue = optimistic; // what this action last wrote\n    Object? rollbackValue; // value slice after rollback, if any\n\n    try {\n      // Saves the new value to the cloud.\n      // If this action also uses the Retry mixin, we handle retries here\n      // to avoid UI flickering (applying/rolling back on each retry attempt).\n      final serverResponse = await _sendCommandWithRetryIfNeeded(optimistic);\n\n      // Apply server response if not null.\n      if (serverResponse != null) {\n        final St? newState = applyServerResponseToState(state, serverResponse);\n\n        if (newState != null) {\n          dispatchState(newState);\n\n          // Keep lastAppliedValue in sync with what we just wrote for the slice.\n          lastAppliedValue = getValueFromState(newState);\n        }\n      }\n    } catch (error) {\n      commandError = error;\n\n      // Decide if it is safe to rollback (default: only if we are still seeing\n      // our own optimistic value, to avoid undoing newer changes made while\n      // the request was in flight).\n      final currentValue = getValueFromState(state);\n      final initialValue = getValueFromState(initialState);\n\n      if (shouldRollback(\n        currentValue: currentValue,\n        initialValue: initialValue,\n        optimisticValue: optimistic,\n        error: error,\n      )) {\n        final rollback = rollbackState(\n          initialValue: initialValue,\n          optimisticValue: optimistic,\n          error: error,\n        );\n\n        if (rollback != null) {\n          dispatchState(rollback);\n\n          // Update \"lastAppliedValue\" to match what rollback wrote for the value slice.\n          rollbackValue = getValueFromState(rollback);\n          lastAppliedValue = rollbackValue;\n        }\n      }\n\n      rethrow;\n    } finally {\n      try {\n        // Snapshot current value before deciding whether to reload.\n        final Object? currentValueBefore = getValueFromState(state);\n\n        final bool doReload = shouldReload(\n          currentValue: currentValueBefore,\n          lastAppliedValue: lastAppliedValue,\n          optimisticValue: optimistic,\n          rollbackValue: rollbackValue,\n          error: commandError, // null on success\n        );\n\n        if (doReload) {\n          final Object? reloadResult = await reloadFromServer();\n\n          // Re-read after await, because state may have changed while reloading.\n          final Object? currentValueAfter = getValueFromState(state);\n\n          final bool apply = shouldApplyReload(\n            currentValue: currentValueAfter,\n            lastAppliedValue: lastAppliedValue,\n            optimisticValue: optimistic,\n            rollbackValue: rollbackValue,\n            reloadResult: reloadResult,\n            error: commandError, // null on success\n          );\n\n          if (apply) {\n            final St? newState = applyReloadResultToState(state, reloadResult);\n            if (newState != null) dispatchState(newState);\n          }\n        }\n      } on UnimplementedError catch (_) {\n        // If reloadFromServer was not implemented, do nothing.\n      } catch (reloadError) {\n        // Important: Do not let reload failure hide the original command error.\n        if (commandError == null) rethrow;\n      }\n    }\n\n    return null;\n  }\n\n  /// When combined with Retry, this method retries only the [sendCommandToServer]\n  /// call, keeping the optimistic update in place and avoiding UI flickering.\n  Future<Object?> _sendCommandWithRetryIfNeeded(\n      Object? _optimisticValue) async {\n    // If this action doesn't use the Retry mixin,\n    // just call sendCommandToServer directly.\n    if (this is! Retry) {\n      return sendCommandToServer(_optimisticValue);\n    }\n\n    // Access the Retry mixin's properties via casting.\n    final retryMixin = this as Retry<St>;\n\n    while (true) {\n      try {\n        return await sendCommandToServer(_optimisticValue);\n      } catch (error) {\n        retryMixin._attempts++;\n\n        // If maxRetries is reached (and not unlimited), rethrow the error.\n        if ((retryMixin.maxRetries >= 0) &&\n            (retryMixin._attempts > retryMixin.maxRetries)) {\n          rethrow;\n        }\n\n        // Wait before retrying.\n        final currentDelay = retryMixin.nextDelay();\n        await Future.delayed(currentDelay);\n        // Loop continues, retrying sendCommandToServer.\n      }\n    }\n  }\n\n  /// The set of keys that are currently running.\n  Set<Object?> get _nonReentrantCommandKeySet =>\n      store.internalMixinProps.nonReentrantKeySet;\n\n  Object? _nonReentrantCommandKey;\n\n  @override\n  bool abortDispatch() {\n    _cannot_combine_mixins_OptimisticCommand();\n    _cannot_combine_mixins_UnlimitedRetryCheckInternet_OptimisticCommand_OptimisticSync_OptimisticSyncWithPush_ServerPush();\n\n    // First, check the super class/mixin wants to abort.\n    // See the comment in [NonReentrant.abortDispatch].\n    if (super.abortDispatch()) return true;\n\n    _nonReentrantCommandKey = computeNonReentrantKey();\n\n    // If the key is already in the set, abort.\n    if (_nonReentrantCommandKeySet.contains(_nonReentrantCommandKey))\n      return true;\n    //\n    // Otherwise, add the key and allow dispatch.\n    else {\n      _nonReentrantCommandKeySet.add(_nonReentrantCommandKey);\n      return false;\n    }\n  }\n\n  @override\n  void after() {\n    // Remove the key when the action finishes (success or failure).\n    _nonReentrantCommandKeySet.remove(_nonReentrantCommandKey);\n  }\n\n  /// Only [Retry], [CheckInternet] and [AbortWhenNoInternet] can be combined\n  /// with [OptimisticCommand].\n  ///\n  void _cannot_combine_mixins_OptimisticCommand() {\n    _incompatible<OptimisticCommand, NonReentrant>(this);\n    _incompatible<OptimisticCommand, Fresh>(this);\n    _incompatible<OptimisticCommand, Throttle>(this);\n    _incompatible<OptimisticCommand, Debounce>(this);\n    _incompatible<OptimisticCommand, UnlimitedRetries>(this);\n    _incompatible<OptimisticCommand, Polling>(this);\n  }\n\n  void\n      _cannot_combine_mixins_UnlimitedRetryCheckInternet_OptimisticCommand_OptimisticSync_OptimisticSyncWithPush_ServerPush() {\n    _incompatible<OptimisticCommand, UnlimitedRetryCheckInternet>(this);\n    _incompatible<OptimisticCommand, OptimisticSync>(this);\n    _incompatible<OptimisticCommand, OptimisticSyncWithPush>(this);\n    _incompatible<OptimisticCommand, ServerPush>(this);\n  }\n}\n\n/// Mixin [Throttle] ensures the action will be dispatched at most once in the\n/// specified throttle period. It acts as a simple rate limit, so the action\n/// does not run too often.\n///\n/// If an action is dispatched multiple times within a throttle period, only\n/// the first dispatch runs and the others are aborted. After the throttle\n/// period has passed, the next dispatch is allowed to run again, which starts\n/// a new throttle period.\n///\n/// This is useful when an action may be triggered many times in a short time,\n/// for example by fast user input or widget rebuilds, but you only want it to\n/// run from time to time instead of on every dispatch.\n///\n/// For example, if you are using a `StatefulWidget` that needs to load some\n/// information, you can dispatch the loading action when the widget is\n/// created in `initState()` and specify a throttle period so that it does not\n/// reload that information too often:\n///\n/// ```dart\n/// class MyScreen extends StatefulWidget {\n///   State<MyScreen> createState() => _MyScreenState();\n/// }\n///\n/// class _MyScreenState extends State<MyScreen> {\n///\n///   void initState() {\n///     super.initState();\n///     context.dispatch(LoadInformation()); // Here!\n///   }\n///\n///   Widget build(BuildContext context) {\n///     var information = context.state.information;\n///     return Text('Information: $information');\n///   }\n/// }\n/// ```\n///\n/// and then:\n///\n/// ```dart\n/// class LoadInformation extends ReduxAction<AppState> with Throttle {\n///\n///   int throttle = 5000;\n///\n///   Future<AppState> reduce() async {\n///     var information = await loadInformation();\n///     return state.copy(information: information);\n///   }\n/// }\n/// ```\n///\n/// The [throttle] value is given in milliseconds, and the default is 1000\n/// milliseconds (1 second). You can override this default:\n///\n/// ```dart\n/// class MyAction extends ReduxAction<AppState> with Throttle {\n///    int throttle = 500; // Here!\n///    ...\n/// }\n/// ```\n///\n/// You can also override [ignoreThrottle] if you want the action to ignore the\n/// throttle period under some conditions. For example, suppose you want the\n/// action to provide a flag called `force` that will ignore the throttle\n/// period:\n///\n/// ```dart\n/// class MyAction extends ReduxAction<AppState> with Throttle {\n///    final bool force;\n///    MyAction({this.force = false});\n///\n///    bool get ignoreThrottle => force; // Here!\n///\n///    int throttle = 500;\n///    ...\n/// }\n/// ```\n///\n/// # If the action fails\n///\n/// The throttle lock is NOT removed if the action fails. This means that if\n/// the action throws and you dispatch it again within the throttle period, it\n/// will not run a second time.\n///\n/// If you want, you can specify a different behavior by making\n/// [removeLockOnError] true, like this:\n///\n/// ```dart\n/// class MyAction extends ReduxAction<AppState> with Throttle {\n///    bool removeLockOnError = true; // Here!\n///    ...\n/// }\n/// ```\n///\n/// Now, if the action fails, it will remove the lock and allow the action to\n/// be dispatched again right away. Note, this currently implemented in the\n/// [after] method, which means you can override it to customize this behavior:\n///\n/// ```dart\n/// @override\n/// void after() {\n///   if (removeLockOnError && (status.originalError != null)) removeLock();\n/// }\n/// ```\n///\n/// # Advanced usage\n///\n/// The throttle is, by default, based on the action [runtimeType]. This means\n/// it will throttle an action if another action of the same runtimeType was\n/// previously dispatched within the throttle period. In other words, the\n/// runtimeType is the \"lock\". If you want to throttle based on a different\n/// lock, you can override the [lockBuilder] method. For example, here\n/// we throttle two different actions based on the same lock:\n///\n/// ```dart\n/// class MyAction1 extends ReduxAction<AppState> with Throttle {\n///    Object? lockBuilder() => 'myLock';\n///    ...\n/// }\n///\n/// class MyAction2 extends ReduxAction<AppState> with Throttle {\n///    Object? lockBuilder() => 'myLock';\n///    ...\n/// }\n/// ```\n///\n/// Another example is to throttle based on some field of the action:\n///\n/// ```dart\n/// class MyAction extends ReduxAction<AppState> with Throttle {\n///    final String lock;\n///    MyAction(this.lock);\n///    Object? lockBuilder() => lock;\n///    ...\n/// }\n/// ```\n///\n/// Note: Expired locks are removed when expired, to prevent memory leaks.\n///\n/// Notes:\n/// - It should not be combined with other mixins or classes that override [abortDispatch] or [after].\n/// - It should not be combined with [Fresh], [NonReentrant] or [UnlimitedRetryCheckInternet].\n///\nmixin Throttle<St> on ReduxAction<St> {\n  //\n  int get throttle => 1000; // Milliseconds\n\n  bool get removeLockOnError => false;\n\n  bool get ignoreThrottle => false;\n\n  /// The default lock for throttling is the action's [runtimeType],\n  /// meaning it will throttle the dispatch of actions of the same type.\n  /// Override this method to customize the lock to any value.\n  /// For example, you can return a string or an enum, and actions with the\n  /// same lock value will throttle each other.\n  /// Note: Expired locks are removed when expired, to prevent memory leaks.\n  Object? lockBuilder() => runtimeType;\n\n  /// Map that stores the expiry time for each lock.\n  /// The value is the instant when the throttle period ends.\n  Map<Object?, DateTime> get _throttleLockMap =>\n      store.internalMixinProps.throttleLockMap;\n\n  /// Removes the lock, allowing an action of the same type to be dispatched\n  /// again right away. You generally do not need to call this method.\n  void removeLock() => _throttleLockMap.remove(lockBuilder());\n\n  /// Removes all locks, allowing all actions to be dispatched again right away.\n  /// You generally don't need to call this method.\n  void removeAllLocks() => _throttleLockMap.clear();\n\n  @override\n  bool abortDispatch() {\n    _cannot_combine_mixins_Fresh_Throttle_NonReentrant_UnlimitedRetryCheckInternet();\n\n    // First, check the super class/mixin wants to abort.\n    // See the comment in [NonReentrant.abortDispatch].\n    if (super.abortDispatch()) return true;\n\n    final lock = lockBuilder();\n    final now = DateTime.now().toUtc();\n\n    // If should ignore the throttle, then set a new expiry and allow dispatch.\n    if (ignoreThrottle) {\n      _throttleLockMap[lock] = _expiringLockFrom(now);\n      return false;\n    }\n\n    final expiresAt = _throttleLockMap[lock];\n\n    // If there is no lock, or it has expired, set a new expiry and allow.\n    if (expiresAt == null || !expiresAt.isAfter(now)) {\n      _throttleLockMap[lock] = _expiringLockFrom(now);\n      return false;\n    }\n\n    // Still inside the throttle period, abort dispatch.\n    return true;\n  }\n\n  DateTime _expiringLockFrom(DateTime now) =>\n      now.add(Duration(milliseconds: throttle));\n\n  /// Remove locks whose expiry time is in the past or now.\n  void _prune() {\n    final now = DateTime.now().toUtc();\n    _throttleLockMap.removeWhere((_, expiresAt) => !expiresAt.isAfter(now));\n  }\n\n  @override\n  void after() {\n    if (removeLockOnError && (status.originalError != null)) removeLock();\n    _prune();\n  }\n\n  void\n      _cannot_combine_mixins_Fresh_Throttle_NonReentrant_UnlimitedRetryCheckInternet() {\n    _incompatible<Throttle, Fresh>(this);\n    _incompatible<Throttle, NonReentrant>(this);\n    _incompatible<Throttle, UnlimitedRetryCheckInternet>(this);\n    _incompatible<Throttle, OptimisticCommand>(this);\n  }\n}\n\n/// Mixin [Debounce] delays the execution of a function until after a certain\n/// period of inactivity. Each time the debounced function is called,\n/// the period of inactivity (or wait time) is reset.\n///\n/// The function will only execute after it stops being called for the duration\n/// of the wait time. Debouncing is useful in situations where you want to\n/// ensure that a function is not called too frequently and only runs after\n/// some “quiet time.”\n///\n/// For example, it’s commonly used for handling input validation in text fields,\n/// where you might not want to validate the input every time the user presses\n/// a key, but rather after they've stopped typing for a certain amount of time.\n///\n///\n/// The [debounce] value is given in milliseconds, and the default is 333\n/// milliseconds (1/3 of a second). You can override this default:\n///\n/// ```dart\n/// class MyAction extends ReduxAction<AppState> with Debounce {\n///    final int debounce = 1000; // Here!\n///    ...\n/// }\n/// ```\n///\n/// # Advanced usage\n///\n/// The debounce is, by default, based on the action [runtimeType]. This means\n/// it will reset the debounce period when another action of the same\n/// runtimeType was is dispatched within the debounce period. In other words,\n/// the runtimeType is the \"lock\". If you want to debounce based on a different\n/// lock, you can override the [lockBuilder] method. For example, here\n/// we debounce two different actions based on the same lock:\n///\n/// ```dart\n/// class MyAction1 extends ReduxAction<AppState> with Debounce {\n///    Object? lockBuilder() => 'myLock';\n///    ...\n/// }\n///\n/// class MyAction2 extends ReduxAction<AppState> with Debounce {\n///    Object? lockBuilder() => 'myLock';\n///    ...\n/// }\n/// ```\n///\n/// Another example is to debounce based on some field of the action:\n///\n/// ```dart\n/// class MyAction extends ReduxAction<AppState> with Debounce {\n///    final String lock;\n///    MyAction(this.lock);\n///    Object? lockBuilder() => lock;\n///    ...\n/// }\n/// ```\n///\n/// Notes:\n/// - It should not be combined with other mixins or classes that override [wrapReduce].\n/// - It should not be combined with [Retry], [UnlimitedRetries], or [UnlimitedRetryCheckInternet].\n///\nmixin Debounce<St> on ReduxAction<St> {\n  //\n  int get debounce => 333; // Milliseconds\n\n  /// The default lock for debouncing is the action's [runtimeType],\n  /// meaning it will debounce the dispatch of actions of the same type.\n  /// Override this method to customize the lock to any value.\n  /// For example, you can return a string or an enum, and actions with the\n  /// same lock value will debounce each other.\n  Object? lockBuilder() => runtimeType;\n\n  /// Map that stores the run-number for actions with a specific lock.\n  Map<Object?, int> get _debounceLockMap =>\n      store.internalMixinProps.debounceLockMap;\n\n  // A large number that JavaScript can still represent.\n  // In theory, it could be between -9007199254740991 and 9007199254740991.\n  static const _SAFE_INTEGER = 9000000000000000;\n\n  /// Removes all locks, allowing all actions to be dispatched again right away.\n  /// You generally don't need to call this method.\n  void removeAllLocks() => _debounceLockMap.clear();\n\n  @override\n  Future<St?> wrapReduce(Reducer<St> reduce) async {\n    _cannot_combine_mixins_Debounce_Retry_UnlimitedRetryCheckInternet();\n\n    var lock = lockBuilder();\n\n    // Increment and update the map with the new run count.\n    var before = (_debounceLockMap[lock] ?? 0) + 1;\n    if (before > _SAFE_INTEGER) before = 0;\n    _debounceLockMap[lock] = before;\n\n    await Future.delayed(Duration(milliseconds: debounce));\n\n    var after = _debounceLockMap[lock];\n\n    // If the run has changed, it means the action was dispatched again\n    // within the debounce period. So, we abort the reducer.\n    if (after != before)\n      return null;\n    //\n    // Otherwise, we remove the lock and run the reducer.\n    else {\n      _debounceLockMap.remove(lock);\n      return reduce();\n    }\n  }\n\n  void _cannot_combine_mixins_Debounce_Retry_UnlimitedRetryCheckInternet() {\n    _incompatible<Debounce, Retry>(this);\n    _incompatible<Debounce, UnlimitedRetryCheckInternet>(this);\n    _incompatible<Debounce, Polling>(this);\n  }\n}\n\n/// Mixin [UnlimitedRetryCheckInternet] can be used to check if there is\n/// internet when you run some action that needs it. If there is no internet,\n/// the action will abort silently, and then retry the [reduce] method\n/// unlimited times, until there is internet. It will also retry if there\n/// is internet but the action failed.\n///\n/// Just add `with UnlimitedRetryCheckInternet` to your action.\n/// For example:\n///\n/// ```dart\n/// class LoadText extends AppAction UnlimitedRetryCheckInternet {\n///   Future<String> reduce() async {\n///     Response response = await get(Uri.parse(\"https://swapi.dev/api/people/42/\"));\n///     Map<String, dynamic> json = jsonDecode(response.body);\n///     return json['name'] ?? 'Unknown';\n///   }\n/// }\n/// ```\n///\n/// IMPORTANT: This mixin combines [Retry], [UnlimitedRetries],\n/// [AbortWhenNoInternet] and [NonReentrant] mixins, but there is\n/// difference. Combining [Retry] with [CheckInternet] or [AbortWhenNoInternet]\n/// will not retry when there is no internet. It will only retry if there IS\n/// internet but the action fails for some other reason. To retry indefinitely\n/// until internet is available, then you should use [UnlimitedRetryCheckInternet].\n///\n/// IMPORTANT: It only checks if the internet is on or off on the device,\n/// not if the internet provider is really providing the service or if the\n/// server is available. So, it is possible that this function returns true\n/// and the request still fails.\n///\n/// Notes:\n/// - It should not be combined with other mixins or classes that override [wrapReduce] or [abortDispatch].\n/// - It should not be combined with other mixins or classes that check the internet connection.\n/// - Make sure your `before` method does not throw an error, or the retry will NOT happen.\n/// - All retries will be printed to the console.\n///\nmixin UnlimitedRetryCheckInternet<St> on ReduxAction<St> {\n  //\n  @override\n  bool abortDispatch() {\n    _cannot_combine_mixins_Fresh_Throttle_NonReentrant_UnlimitedRetryCheckInternet();\n    _cannot_combine_mixins_CheckInternet_AbortWhenNoInternet_UnlimitedRetryCheckInternet();\n    _cannot_combine_mixins_Debounce_Retry_UnlimitedRetryCheckInternet();\n    _cannot_combine_mixins_UnlimitedRetryCheckInternet_OptimisticCommand_OptimisticSync_OptimisticSyncWithPush_ServerPush();\n\n    // First, check the super class/mixin wants to abort.\n    // See the comment in [NonReentrant.abortDispatch].\n    if (super.abortDispatch()) return true;\n\n    return isWaiting(runtimeType);\n  }\n\n  /// The delay before the first retry attempt.\n  Duration get initialDelay => const Duration(milliseconds: 350);\n\n  /// The factor by which the delay increases for each subsequent retry.\n  /// Must be greater than 1, otherwise it will be set to 2.\n  double get multiplier => 2;\n\n  /// Unlimited retries.\n  int get maxRetries => -1;\n\n  /// The maximum delay between retries to avoid excessively long wait times.\n  /// This is for errors that are not related to the Internet.\n  /// The default is 5 seconds.\n  /// See also: [maxDelayNoInternet]\n  Duration get maxDelay => const Duration(milliseconds: 5000);\n\n  /// The maximum delay between retries when there is no Internet.\n  /// The default is 1 second.\n  /// See also: [maxDelay]\n  Duration get maxDelayNoInternet => const Duration(seconds: 1);\n\n  int _attempts = 0;\n\n  /// The number of retry attempts so far. If the action has not been retried yet, it will be 0.\n  /// If the action finished successfully, it will be equal or less than [maxRetries].\n  /// If the action failed and gave up, it will be equal to [maxRetries] plus 1.\n  int get attempts => _attempts;\n\n  /// This prints the retries, including the action name, the attempt, and if\n  /// the problem was no Internet or not. To remove the print message,\n  /// override with:\n  ///\n  /// ```dart\n  /// void printRetries(String message) {}\n  /// ```\n  void printRetries(String message) => print(message);\n\n  @override\n  Future<St?> wrapReduce(Reducer<St> reduce) async {\n    FutureOr<St?> newState;\n    bool hasInternet = true;\n    try {\n      // Note we don't have side-effects before the first await.\n      var result = await checkConnectivity();\n\n      // IMPORTANT: We throw this exception, but it will not ever be shown,\n      // because we are retrying unlimited times. This simply triggers the next retry.\n      if (result.contains(ConnectivityResult.none)) {\n        hasInternet = false;\n        throw const UserException('');\n      }\n\n      if (attempts == 0)\n        printRetries('Trying $runtimeType.');\n      else\n        printRetries('Retrying $runtimeType (attempt $attempts).');\n\n      newState = reduce();\n      if (newState is Future) newState = await newState;\n    }\n    //\n    catch (error) {\n      //\n      if (!hasInternet) {\n        if (attempts == 0)\n          printRetries('Trying $runtimeType; aborted because of no internet.');\n        else\n          printRetries(\n              'Retrying $runtimeType; aborted because of no internet (attempt $attempts).');\n      }\n\n      _attempts++;\n\n      if ((maxRetries >= 0) && (_attempts > maxRetries)) rethrow;\n\n      var currentDelay = nextDelay(hasInternet: hasInternet);\n      await Future.delayed(currentDelay);\n      return wrapReduce(reduce);\n    }\n    return newState;\n  }\n\n  Duration? _currentDelay;\n\n  /// Start with the [initialDelay], and then increase it by [multiplier] each time this is called.\n  /// If the delay exceeds [maxDelay], it will be set to [maxDelay].\n  Duration nextDelay({required bool hasInternet}) {\n    var _multiplier = multiplier;\n    if (_multiplier <= 1) _multiplier = 2;\n\n    _currentDelay = (_currentDelay == null) //\n        ? initialDelay //\n        : _currentDelay! * _multiplier;\n\n    if (hasInternet) {\n      if (_currentDelay! > maxDelay) _currentDelay = maxDelay;\n    } else {\n      if (_currentDelay! > maxDelayNoInternet)\n        _currentDelay = maxDelayNoInternet;\n    }\n\n    return _currentDelay!;\n  }\n\n  /// If you are running tests, you can override this getter to simulate the\n  /// internet connection as on or off:\n  ///\n  /// - Return `true` if there IS internet.\n  /// - Return `false` if there is NO internet.\n  /// - Return `null` to use the real internet connection status (default).\n  ///\n  /// If you want to change this for all actions using mixins [CheckInternet],\n  /// [AbortWhenNoInternet], and [UnlimitedRetryCheckInternet], you can\n  /// do that at the store level:\n  ///\n  /// ```dart\n  /// store.forceInternetOnOffSimulation = () => false;\n  /// ```\n  ///\n  /// Using [Store.forceInternetOnOffSimulation] is also useful during tests,\n  /// for testing what happens when you have no internet connection. And since\n  /// it's tied to the store, it automatically resets when the store is\n  /// recreated.\n  ///\n  bool? get internetOnOffSimulation => store.forceInternetOnOffSimulation();\n\n  Future<List<ConnectivityResult>> checkConnectivity() async {\n    if (internetOnOffSimulation != null)\n      return internetOnOffSimulation!\n          ? [ConnectivityResult.wifi]\n          : [ConnectivityResult.none];\n\n    return await (Connectivity().checkConnectivity());\n  }\n\n  void\n      _cannot_combine_mixins_Fresh_Throttle_NonReentrant_UnlimitedRetryCheckInternet() {\n    _incompatible<UnlimitedRetryCheckInternet, Fresh>(this);\n    _incompatible<UnlimitedRetryCheckInternet, Throttle>(this);\n    _incompatible<UnlimitedRetryCheckInternet, NonReentrant>(this);\n  }\n\n  void\n      _cannot_combine_mixins_CheckInternet_AbortWhenNoInternet_UnlimitedRetryCheckInternet() {\n    _incompatible<UnlimitedRetryCheckInternet, CheckInternet>(this);\n    _incompatible<UnlimitedRetryCheckInternet, AbortWhenNoInternet>(this);\n  }\n\n  void _cannot_combine_mixins_Debounce_Retry_UnlimitedRetryCheckInternet() {\n    _incompatible<UnlimitedRetryCheckInternet, Debounce>(this);\n    _incompatible<UnlimitedRetryCheckInternet, Retry>(this);\n    _incompatible<UnlimitedRetryCheckInternet, Polling>(this);\n  }\n\n  void\n      _cannot_combine_mixins_UnlimitedRetryCheckInternet_OptimisticCommand_OptimisticSync_OptimisticSyncWithPush_ServerPush() {\n    _incompatible<UnlimitedRetryCheckInternet, OptimisticCommand>(this);\n    _incompatible<UnlimitedRetryCheckInternet, OptimisticSync>(this);\n    _incompatible<UnlimitedRetryCheckInternet, OptimisticSyncWithPush>(this);\n    _incompatible<UnlimitedRetryCheckInternet, ServerPush>(this);\n  }\n}\n\n/// Mixin [Fresh] lets you treat the result of an action as fresh for a\n/// given time period. While the information is fresh, repeated dispatches of\n/// the same action (or other actions with the same \"fresh-key\") are skipped,\n/// because that information is assumed to still be valid in the state.\n///\n/// After the fresh period ends, the information is considered \"stale\".\n/// The next dispatch of an action with the same \"fresh-key\" is allowed to\n/// run again, update the state, and start a new fresh period.\n///\n/// In short, [Fresh] helps you avoid reloading the same information too often.\n///\n///\n/// ## Basic usage\n///\n/// This is often used for actions that load information from a server. You can\n/// think of the fresh period as the time during which the loaded data is still\n/// good to use. After that time, a new dispatch will reload it.\n///\n/// A simple example in a `StatefulWidget` that loads information once when\n/// the widget is created:\n///\n/// ```dart\n/// class MyScreen extends StatefulWidget {\n///   State<MyScreen> createState() => _MyScreenState();\n/// }\n///\n/// class _MyScreenState extends State<MyScreen> {\n///   void initState() {\n///     super.initState();\n///     context.dispatch(LoadInformation()); // Here!\n///   }\n///\n///   Widget build(BuildContext context) {\n///     var information = context.state.information;\n///     return Text('Information: $information');\n///   }\n/// }\n/// ```\n///\n/// Use [Fresh] on the loading action so it does not run again while its data\n/// is still fresh:\n///\n/// ```dart\n/// class LoadInformation extends ReduxAction<AppState> with Fresh {\n///\n///   Future<AppState> reduce() async {\n///     var information = await loadInformation();\n///     return state.copy(information: information);\n///   }\n/// }\n/// ```\n///\n///\n/// ## How fresh-keys work\n///\n/// * Dispatched actions with different fresh-keys are not affected.\n///\n/// * Dispatched actions with the same fresh-key:\n///   - Are aborted while the data is fresh (the fresh period has not passed).\n///   - Run again when the data is stale (after the fresh period has passed).\n///\n/// In other words, freshness is tracked per fresh-key. Any two dispatches that\n/// share the same fresh-key share the same fresh period.\n///\n/// By default, the key is based on:\n/// * The action type (its `runtimeType`), and\n/// * The value returned by [freshKeyParams].\n///\n/// In the previous example, the fresh-key of the `LoadInformation` action is\n/// simply the action [runtimeType], since it did not override [freshKeyParams].\n///\n/// If you dispatch `LoadInformation` many times in a short period, only the\n/// first one runs while the data is fresh. The others are aborted. Later,\n/// when the fresh period ends, the next dispatch will run the action again.\n///\n/// The default [freshKeyParams] returns `null`, so the key is only the action\n/// type. This means all actions of the same type share the same fresh period,\n/// and different action types do not affect each other.\n///\n/// ### Using [freshKeyParams] to separate instances\n///\n/// Many actions need a separate fresh period per id, url, or some other field.\n/// In that case, override [freshKeyParams]. Actions of the same type but with\n/// different [freshKeyParams] values do not affect each other.\n///\n/// ```dart\n/// class LoadUserCart extends ReduxAction<AppState> with Fresh {\n///   final String userId;\n///   LoadUserCart(this.userId);\n///\n///   // The fresh-key parameter here is the `userId`, which means\n///   // each different `(LoadUserCart, userId)` has its own fresh period.\n///   Object? freshKeyParams() => userId;\n///   ...\n/// ```\n///\n/// You can also return more than one field by using a tuple:\n///\n/// ```dart\n/// // Each different `(LoadUserCart, userId, cartId)` has its own fresh period.\n/// Object? freshKeyParams() => (userId, cartId);\n/// ```\n///\n/// ## Configuring how long data stays fresh\n///\n/// The [freshFor] value is given in milliseconds. The default is `1000`\n/// (1 second).\n///\n/// To keep the data fresh for 5 seconds:\n///\n///\n/// ```dart\n/// class LoadInformation extends ReduxAction<AppState> with Fresh {\n///    int freshFor = 500; // Here!\n///    ...\n/// }\n/// ```\n///\n///\n/// ## Forcing the action to run\n///\n/// Sometimes you want to run the action even if the data is still fresh. For\n/// that, you can override [ignoreFresh]. When [ignoreFresh] is `true`, the\n/// action always runs and also starts a new fresh period for its key.\n///\n/// A common pattern is to add a `force` flag:\n///\n/// ```dart\n/// class LoadInformation extends ReduxAction<AppState> with Fresh {\n///    final bool force;\n///    LoadInformation({this.force = false});\n///\n///    bool get ignoreFresh => force; // Here!\n///    ...\n/// }\n/// ```\n///\n/// With this setup:\n/// * `LoadInformation()` runs only when its key is stale.\n/// * `LoadInformation(force: true)` always runs and also refreshes the key.\n///\n///\n/// ## When the action fails\n///\n/// If an action that uses [Fresh] throws an error, the mixin tries to behave\n/// as if that failing run did not make the key stay fresh for longer.\n///\n/// In practice:\n/// * If there was no fresh entry for that key before the action started,\n///   the key is cleared. You can dispatch the action again right away.\n/// * If there was already a fresh time stored for that key, that time is kept.\n/// * If another action using the same key finished after this one started and\n///   changed the fresh time, that newer fresh time is kept as is.\n///\n/// This means:\n/// * Errors never extend the fresh time by themselves.\n/// * A failure from an older action does not cancel a newer successful action\n///   that used the same fresh-key.\n///\n/// You can also control this by hand:\n///\n/// * Call [removeKey] from your action (for example inside [reduce] or\n///   [before]) to remove the key used by that action, so the next dispatch\n///   for that key can run immediately.\n/// * Call [removeAllKeys] from your action to clear all keys and let all\n///   actions run again as if nothing was fresh. This is probably useful\n///   during logout or similar scenarios.\n///\n/// Expired keys are cleaned automatically over time, so you usually do not\n/// need to worry about old entries.\n///\n///\n/// ## Using [computeFreshKey] to share keys across actions\n///\n/// If you want different action types to share the same key, override\n/// [computeFreshKey]. This is useful when several actions read or write the\n/// same logical resource and should respect the same fresh period.\n///\n/// For example, two actions that work on the same user data:\n///\n/// ```dart\n/// class LoadUserProfile extends ReduxAction<AppState> with Fresh {\n///   final String userId;\n///   LoadUserProfile(this.userId);\n///\n///   Object computeFreshKey() => userId; // key is only userId\n///   ...\n/// }\n///\n/// class LoadUserSettings extends ReduxAction<AppState> with Fresh {\n///   final String userId;\n///   LoadUserSettings(this.userId);\n///\n///   Object computeFreshKey() => userId; // same key as above\n///   ...\n/// }\n/// ```\n///\n/// Here:\n/// * `LoadUserProfile('123')` and `LoadUserSettings('123')` share one fresh\n///   period, because they use the same key.\n/// * Any object can be a key, for example an enum or a constant string.\n///\n///\n/// Notes:\n/// - It should not be combined with other mixins or classes that override [abortDispatch] or [after].\n/// - It should not be combined with [Throttle], [NonReentrant] or [UnlimitedRetryCheckInternet].\n///\nmixin Fresh<St> on ReduxAction<St> {\n  //\n  int get freshFor => 1000; // Milliseconds\n\n  bool get ignoreFresh => false;\n\n  /// By default the fresh key is based on the action [runtimeType].\n  /// For example, all actions of type `LoadText` share the same\n  /// freshness:\n  ///\n  /// ```dart\n  /// // This action runs.\n  /// dispatch(LoadText(url: 'https://example.com'));\n  ///\n  /// // This does NOT run, because the previous LoadText is still fresh.\n  /// dispatch(LoadText(url: 'https://another-url.com'));\n  /// ```dart\n  ///\n  /// You can override [freshKeyParams] so that actions of the SAME TYPE\n  /// but with different parameters do not affect each other's freshness.\n  /// In this example, the `url` field becomes part of the fresh-key:\n  ///\n  /// ```dart\n  /// class LoadText extends ReduxAction<AppState> with Fresh {\n  ///   final String url;\n  ///   LoadText(this.url);\n  ///\n  ///   // The fresh-key includes the url.\n  ///   Object? freshKeyParams() => url;\n  ///   ...\n  /// }\n  /// ```\n  ///\n  /// Now, dispatching two `LoadText` actions with different `url` values\n  /// allows both of them to run, because each one uses a different fresh-key:\n  ///\n  /// ```dart\n  /// // This action runs.\n  /// dispatch(LoadText(url: 'https://example.com'));\n  ///\n  /// // This also runs, because the url is different, so it has a different fresh-key.\n  /// dispatch(LoadText(url: 'https://another-url.com'));\n  /// ```\n  ///\n  /// ## In more detail\n  ///\n  /// The default fresh-key, as returned by [computeFreshKey], combines the\n  /// action [runtimeType] with the value returned by [freshKeyParams].\n  ///\n  /// Most of the time you override [freshKeyParams] to return one field,\n  /// or a tuple of fields:\n  ///\n  /// ```dart\n  /// // Fresh-key is runtimeType + url\n  /// Object? freshKeyParams() => url;\n  ///\n  /// // Fresh-key is runtimeType + userId + cartId\n  /// Object? freshKeyParams() => (userId, cartId);\n  /// ```\n  ///\n  /// When [freshKeyParams] returns `null`, the key is just the action type.\n  /// In that case all actions of that type share the same freshness.\n  ///\n  /// See also:\n  /// - [computeFreshKey] if you want full control over how the key is built.\n  ///\n  Object? freshKeyParams() => null;\n\n  /// In most cases you want to use the default fresh-key computation, which\n  /// combines the action's [runtimeType] with the value returned by\n  /// [freshKeyParams]:\n  ///\n  /// ```dart\n  /// Object? computeFreshKey() => (runtimeType, freshKeyParams());\n  /// ```\n  ///\n  /// However, if you want different action types to share the same fresh\n  /// period, you must override [computeFreshKey] and return any key you want.\n  /// Some examples:\n  ///\n  /// ```dart\n  /// // The fresh-key is only the url, without the runtimeType.\n  /// Object? computeFreshKey() => url;\n  ///\n  /// // The fresh-key is a pair of values, without the runtimeType.\n  /// Object? computeFreshKey() => (userId, cartId);\n  ///\n  /// // The fresh-key is a constant string.\n  /// Object? computeFreshKey() => 'myKey';\n  ///\n  /// // The fresh-key is an enum value.\n  /// Object? computeFreshKey() => MyFreshnessKey.myKey;\n  /// ```\n  ///\n  /// For example, suppose you have two different actions, and you want\n  /// them to share the same fresh-key:\n  ///\n  /// ```dart\n  /// class LoadUserProfile extends ReduxAction<AppState> with Fresh {\n  ///   final String userId;\n  ///   LoadUserProfile(this.userId);\n  ///\n  ///   // The key is the userId only, without the runtimeType.\n  ///   Object? computeFreshKey() => userId;\n  ///   ...\n  /// }\n  ///\n  /// class LoadUserSettings extends ReduxAction<AppState> with Fresh {\n  ///   final String userId;\n  ///   LoadUserSettings(this.userId);\n  ///\n  ///   // The key is the userId only, without the runtimeType.\n  ///   Object? computeFreshKey() => userId;\n  ///   ...\n  /// }\n  /// ```\n  ///\n  /// With this setup, if you dispatch `LoadUserProfile('123')`,\n  /// then `LoadUserSettings('123')` will be aborted if dispatched within the\n  /// fresh period of the first action.\n  ///\n  /// See also:\n  /// - [freshKeyParams] when you want to differentiate fresh-keys by some\n  ///   of the fields of the action.\n  ///\n  Object computeFreshKey() => (runtimeType, freshKeyParams());\n\n  /// Map that stores the expiry time and a unique token for each key.\n  /// The value is a record of (expiry DateTime, unique token Object).\n  Map<Object?, (DateTime, Object)> get _freshKeyMap =>\n      store.internalMixinProps.freshKeyMap;\n\n  /// Removes the fresh-key used by this action, allowing an action using the\n  /// same fresh-key to be dispatched and run again, right away.\n  /// Calling this method will make the action stale immediately.\n  /// You generally do not need to call this method, but if you do, use\n  /// it only from your action's [reduce] or [before] methods.\n  void removeKey() {\n    _freshKeyMap.remove(_freshKey);\n    _keysRemoved = true;\n  }\n\n  /// Removes all fresh-key, allowing all actions to be dispatched and\n  /// run again right away.\n  /// Calling this method will make all actions stale immediately.\n  /// You generally do not need to call this method, but if you do, use\n  /// it only from your action's [reduce] or [before] methods.\n  void removeAllKeys() {\n    _freshKeyMap.clear();\n    _keysRemoved = true;\n  }\n\n  (DateTime, Object)? _current;\n  Object? _freshKey;\n  bool _keysRemoved = false;\n  Object? _newToken;\n\n  @override\n  bool abortDispatch() {\n    _cannot_combine_mixins_Fresh_Throttle_NonReentrant_UnlimitedRetryCheckInternet();\n\n    // First, check the super class/mixin wants to abort.\n    // See the comment in [NonReentrant.abortDispatch].\n    if (super.abortDispatch()) return true;\n\n    _keysRemoved = false; // good to reset here\n    _freshKey = computeFreshKey();\n    _current = _freshKeyMap[_freshKey];\n    final now = DateTime.now().toUtc();\n\n    if (ignoreFresh) {\n      final expiry = _expiringKeyFrom(now);\n      final token = Object(); // Unique token for this action invocation.\n      _freshKeyMap[_freshKey] = (expiry, token);\n      _newToken = token;\n      _current = null; // Make it stale if the action fails.\n      return false;\n    }\n\n    final expiresAt = _current?.$1;\n\n    if (expiresAt == null || !expiresAt.isAfter(now)) {\n      final expiry = _expiringKeyFrom(now);\n      final token = Object(); // Unique token for this action invocation.\n      _freshKeyMap[_freshKey] = (expiry, token);\n      _newToken = token;\n      return false;\n    }\n\n    // Still fresh, abort.\n    _newToken = null;\n    return true;\n  }\n\n  void\n      _cannot_combine_mixins_Fresh_Throttle_NonReentrant_UnlimitedRetryCheckInternet() {\n    _incompatible<Fresh, Throttle>(this);\n    _incompatible<Fresh, NonReentrant>(this);\n    _incompatible<Fresh, UnlimitedRetryCheckInternet>(this);\n    _incompatible<Fresh, OptimisticCommand>(this);\n  }\n\n  DateTime _expiringKeyFrom(DateTime now) =>\n      now.add(Duration(milliseconds: freshFor));\n\n  /// Remove keys whose expiry time is in the past or now.\n  void _prune() {\n    final now = DateTime.now().toUtc();\n    _freshKeyMap.removeWhere((_, value) => !value.$1.isAfter(now));\n  }\n\n  @override\n  void after() {\n    if (!_keysRemoved && status.originalError != null && _freshKey != null) {\n      final current = _freshKeyMap[_freshKey];\n\n      // Only rollback if the map still contains the entry written by THIS action.\n      // Use identical() on the token to reliably detect ownership, since DateTime\n      // equality can match different actions that happen in the same millisecond.\n      if (current != null && identical(current.$2, _newToken)) {\n        if (_current == null) {\n          // No previous expiry: remove key (stale).\n          _freshKeyMap.remove(_freshKey);\n        } else {\n          // Restore previous expiry with a new token (previous owner is gone).\n          _freshKeyMap[_freshKey] = _current!;\n        }\n      }\n    }\n\n    _prune();\n  }\n}\n\nvoid _incompatible<T1, T2>(Object instance) {\n  assert(\n    instance is! T2,\n    'The ${T1.toString().split('<').first} mixin '\n    'cannot be combined with the ${T2.toString().split('<').first} mixin.',\n  );\n}\n\n/// Mixin [OptimisticSync] is designed for actions where user interactions\n/// (like toggling a \"like\" button) should update the UI immediately and\n/// send the updated value to the server, making sure the server and the UI\n/// are eventually consistent.\n///\n/// ---\n///\n/// The action is not throttled or debounced in any way, and every dispatch\n/// applies an optimistic update to the state immediately. This guarantees a\n/// very good user experience, because there is immediate feedback on every\n/// interaction.\n///\n/// However, while the first updated value (created by the first time the action\n/// is dispatched) is immediately sent to the server, any other value changes\n/// that occur while the first request is in flight will NOT be sent immediately.\n///\n/// Instead, when the first request completes, it checks if the state is still\n/// the same as the value that was sent. If not, a follow-up request is sent\n/// with the latest value. This process repeats until the state stabilizes.\n///\n/// Note this guarantees that only **one** request is in flight at a time per\n/// key, potentially reducing the number of requests sent to the server while\n/// still coalescing intermediate changes.\n///\n/// Optionally:\n///\n/// * If the server responds with a value, that value is applied to the state.\n///   This is useful when the server normalizes or modifies values.\n///\n/// * When the state finally stabilizes and the request finishes, a callback\n///   function is called, allowing you to perform side-effects.\n///\n/// * In special, if the last request fails, the optimistic state remains, but\n///   in the callback you can then load the current state from the server or\n///   handle the error as you see first by returning a value that will be\n///   applied to the state.\n///\n/// In other words, the mixin makes it easy for you to maintain perfect UI\n/// responsiveness while minimizing server load, and making sure the server and\n/// the UI eventually agree on the same value.\n///\n/// ---\n///\n/// ## How it works\n///\n/// 1. **Immediate UI feedback**: Every dispatch applies [valueToApply] to the\n///    state immediately via [applyOptimisticValueToState].\n///\n/// 2. **Single in-flight request**: Only one request runs at a time per key\n///    (as defined by [optimisticSyncKeyParams]). The first dispatch acquires a lock\n///    and calls [sendValueToServer] to send a request to the server.\n///\n/// 3. **OptimisticSync changes**: If the store state changed while a request started\n///    by [sendValueToServer] was in flight (for example, the user tapped a\n///    \"like\" button again while the first request was pending), a follow-up\n///    request is automatically sent after the current one completes. The change\n///    is detected by comparing [getValueFromState] with the sent value returned\n///    by [valueToApply].\n///\n/// 4. **No unnecessary requests**: If, while the request is in-flight, the\n///    state changes but then returns to the same value as before (for example,\n///    the user tapped a \"like\" button again TWICE while the first request was\n///    pending), [getValueFromState] matches the sent value and no follow-up\n///    request is needed.\n///\n/// 5. **Server response handling**: If [sendValueToServer] returns a non-null\n///    value, it is applied to the state via [applyServerResponseToState] when\n///    the state stabilizes. This is optional but useful.\n///\n/// 6. **Completion callback**: When the synchronization cycle for this key\n///    finishes, [onFinish] is called. On success, it runs after the state is\n///    stable (no follow-up needed) and the lock has been released. On failure,\n///    it runs right after the request fails and the lock is released, and then\n///    the action rethrows the error.\n///\n/// ```\n/// State: liked = false (server confirmed)\n///\n/// User taps LIKE:\n///   → State: liked = true (optimistic)\n///   → Lock acquired, Request 1 sends: setLiked(true)\n///\n/// User taps UNLIKE (Request 1 still in flight):\n///   → State: liked = false (optimistic)\n///   → No request sent (locked)\n///\n/// User taps LIKE (Request 1 still in flight):\n///   → State: liked = true (optimistic)\n///   → No request sent (locked)\n///\n/// Request 1 completes:\n///   → Sent value was `true`, current state is `true`\n///   → They match, no follow-up needed, lock released\n/// ```\n///\n/// If the state had been `false` when Request 1 completed, a follow-up\n/// Request 2 would automatically be sent with `false`.\n///\n/// ## Usage\n///\n/// ```dart\n/// class ToggleLike extends AppAction with OptimisticSync<AppState, bool> {\n///   final String itemId;\n///   ToggleLike(this.itemId);\n///\n///   // Different items can have concurrent requests\n///   @override\n///   Object? optimisticSyncKeyParams() => itemId;\n///\n///   // The new value to apply (toggle current state)\n///   @override\n///   bool valueToApply() => !state.items[itemId].liked;\n///\n///   // Apply the optimistic value to the state\n///   @override\n///   AppState applyOptimisticValueToState(bool isLiked) =>\n///       state.copyWith(items: state.items.setLiked(itemId, isLiked));\n///\n///   // Apply the server response to the state (can be different from optimistic)\n///   @override\n///   AppState? applyServerResponseToState(Object? serverResponse) =>\n///       state.copyWith(items: state.items.setLiked(itemId, serverResponse as bool));\n///\n///   // Read the current value from state (used to detect if follow-up needed)\n///   @override\n///   Object? getValueFromState(AppState state) => state.items[itemId].liked;\n///\n///   // Send the value to the server, optionally return server-confirmed value\n///   @override\n///   Future<Object?> sendValueToServer(Object? optimisticValue) async {\n///     final response = await api.setLiked(itemId, optimisticValue);\n///     return response.liked; // Or return null if server doesn't return a value\n///   }\n///\n///   // Called when state stabilizes (optional). Return state to apply, or null.\n///   @override\n///   Future<AppState?> onFinish(Object? error) async {\n///     if (error != null) {\n///       // Handle error: reload from server to restore correct state\n///       final reloaded = await api.getItem(itemId);\n///       return state.copyWith(items: state.items.update(itemId, reloaded));\n///     }\n///     return null; // Success, no state change needed\n///   }\n/// }\n/// ```\n///\n/// ## Server response handling\n///\n/// [sendValueToServer] can return a value from the server. If non-null, this value is\n/// applied to the state **only when the state stabilizes** (no pending changes).\n/// This is useful when:\n/// - The server normalizes or modifies values\n/// - You want to confirm the server accepted the change\n/// - The server returns the current state after the update\n///\n/// If the server response differs from the current optimistic state when the\n/// state stabilizes, a follow-up request will be sent automatically.\n///\n/// ## Error handling\n///\n/// On failure, the optimistic state remains and [onFinish] is called with\n/// the error.\n///\n/// ## Difference from other mixins\n///\n/// - **vs [Debounce]**: Debounce waits for inactivity before sending *any*\n///   request. OptimisticSync sends the first request immediately and only coalesces\n///   subsequent changes.\n///\n/// - **vs [NonReentrant]**: NonReentrant aborts subsequent dispatches entirely.\n///   OptimisticSync applies the optimistic update and queues a follow-up request.\n///\n/// - **vs [OptimisticCommand]**: OptimisticCommand has rollback logic that breaks\n///   with concurrent dispatches. OptimisticSync is designed for rapid toggling where\n///   only the final state matters.\n///\n/// ## Rollback support\n///\n/// The mixin exposes two fields to help with rollback logic in [onFinish].\n///\n/// - [optimisticValue]: The value returned by [valueToApply] for the current\n///   dispatch. This is set once at the start of reduce() and remains available\n///   throughout the action lifecycle, including in [onFinish].\n///\n/// - [lastSentValue]: The most recent value passed to [sendValueToServer].\n///   Updated right before each server request. Useful for debugging/logging.\n///\n/// Example rollback guard using [optimisticValue]:\n///\n/// ```dart\n/// Future<St?> onFinish(Object? error) async {\n///   if (error != null) {\n///     // Only rollback if the state still reflects our optimistic update.\n///     // If the user made another change, don't overwrite it.\n///     if (getValueFromState(state) == optimisticValue) {\n///       return applyOptimisticValueToState(state, initialValue);\n///     }\n///   }\n///   return null;\n/// }\n/// ```\n///\n/// Another possibility is to use [onFinish] to reload the value from the\n/// server. Here is an example:\n///\n/// ```dart\n/// Future<St?> onFinish(Object? error) async {\n///   try {\n///     final fresh = await api.fetchValue(itemId);\n///     return applyServerResponseToState(state, fresh);\n///   } catch (_) {\n///     return null; // Ignore reload failures and keep the current state.\n///   }\n/// }\n/// ```\n///\n/// Notes:\n/// - It can be combined with [CheckInternet] and [AbortWhenNoInternet].\n/// - It should not be combined with [NonReentrant], [Throttle], [Debounce],\n///   [Fresh], [UnlimitedRetryCheckInternet], [UnlimitedRetries],\n///   [OptimisticCommand], [OptimisticSyncWithPush], or [ServerPush].\n///\nmixin OptimisticSync<St, T> on ReduxAction<St> {\n  //\n  /// The optimistic value that was applied to the state for the current\n  /// dispatch. This is set once at the start of [reduce] to the value returned\n  /// by [valueToApply], and remains available in [onFinish] for rollback logic.\n  late final T optimisticValue;\n\n  /// The most recent value that was passed to [sendValueToServer].\n  /// This is updated right before each server request (including follow-ups).\n  /// Useful for debugging, logging, or implementing custom guards.\n  /// Reset to `null` at the start of each dispatch.\n  T? lastSentValue;\n\n  /// Optionally, override [optimisticSyncKeyParams] to differentiate coalescing by\n  /// action parameters. For example, if you have a like button per item,\n  /// return the item ID so that different items can have concurrent requests:\n  ///\n  /// ```dart\n  /// Object? optimisticSyncKeyParams() => itemId;\n  /// ```\n  ///\n  /// You can also return a record of values:\n  ///\n  /// ```dart\n  /// Object? optimisticSyncKeyParams() => (userId, itemId);\n  /// ```\n  ///\n  /// See also: [computeOptimisticSyncKey], which uses this method by default to\n  /// build the key.\n  ///\n  Object? optimisticSyncKeyParams() => null;\n\n  /// By default the coalescing key combines the action [runtimeType]\n  /// with [optimisticSyncKeyParams]. Override this method if you want\n  /// different action types to share the same coalescing key.\n  Object computeOptimisticSyncKey() => (runtimeType, optimisticSyncKeyParams());\n\n  /// Override [valueToApply] to return the value that should be applied\n  /// optimistically to the state and then sent to the server. This is called\n  /// synchronously and only once per dispatch, when the reducer starts.\n  ///\n  /// The value to apply can be anything, and is usually constructed from the\n  /// action fields, and/or from the current [state]. Valid examples are:\n  ///\n  /// ```dart\n  /// // Set the like button to \"liked\".\n  /// bool valueToApply() => true\n  ///\n  /// // Set the like button to \"liked\" or \"not liked\", according to\n  /// // the field `isLiked` of the action.\n  /// bool valueToApply() => isLiked;\n  ///\n  /// // Toggles the current state of the like button.\n  /// bool valueToApply() => !state.items[itemId].isLiked;\n  /// ```\n  ///\n  T valueToApply();\n\n  /// Override [applyOptimisticValueToState] to return a new state where the\n  /// given [optimisticValue] is applied to the current [state].\n  ///\n  /// Note, AsyncRedux calculates [optimisticValue] by previously\n  /// calling [valueToApply].\n  ///\n  /// ```dart\n  /// AppState applyOptimisticValueToState(state, isLiked) =>\n  ///     state.copyWith(items: state.items.setLiked(itemId, isLiked));\n  /// ```\n  St applyOptimisticValueToState(St state, T optimisticValue);\n\n  /// Override [applyServerResponseToState] to return a new state, where the\n  /// given [serverResponse] (previously received from the server when running\n  /// [sendValueToServer]) is applied to the current [state]. Example:\n  ///\n  /// ```dart\n  /// AppState? applyServerResponseToState(state, serverResponse) =>\n  ///     state.copyWith(items: state.items.setLiked(itemId, serverResponse.isLiked));\n  /// ```\n  ///\n  /// Note [serverResponse] is never `null` here, because this method is only\n  /// called when [sendValueToServer] returned a non-null value.\n  ///\n  /// If you decide you DO NOT want to apply the server response to the state,\n  /// simply return `null`.\n  ///\n  St? applyServerResponseToState(St state, Object serverResponse);\n\n  /// Override [getValueFromState] to extract the value from the current [state].\n  /// This value will be later compared to one returned by [valueToApply] to\n  /// determine if a follow-up request is needed.\n  ///\n  /// Here is the rationale:\n  /// When a request completes, if the value in the state is different from\n  /// the value that was optimistically applied, it means the user changed it\n  /// again while the request was in flight, so a follow-up request is needed\n  /// to sync the latest value with the server.\n  ///\n  /// ```dart\n  /// bool getValueFromState(state) => state.items[itemId].liked;\n  /// ```\n  T getValueFromState(St state);\n\n  /// Override [sendValueToServer] to send the given [optimisticValue] to the\n  /// server, and optionally return the server's response.\n  ///\n  /// Note, AsyncRedux calculates [optimisticValue] by previously\n  /// calling [valueToApply].\n  ///\n  /// If [sendValueToServer] returns a non-null value, that value will be\n  /// applied to the state, but **only when the state stabilizes** (i.e., when\n  /// there are no more pending requests and the lock is about to be released).\n  /// This prevents the server response from overwriting subsequent user\n  /// interactions that occurred while the request was in flight.\n  ///\n  /// The value in the store state may change while the request is in flight.\n  /// For example, if the user presses a like button once, but then\n  /// presses it again before the first request finishes, the value in the\n  /// store state is now different from the optimistic value that was previously\n  /// applied. In this case, [sendValueToServer] will be called again to create\n  /// a follow-up request to sync the updated state with the server.\n  ///\n  /// If [sendValueToServer] returns `null`, the current optimistic state is\n  /// assumed to be correct and valid.\n  ///\n  /// ```dart\n  /// Future<Object?> sendValueToServer(Object? optimisticValue) async {\n  ///   var response = await api.setLiked(itemId, optimisticValue);\n  ///   return response?.liked; // Return server-confirmed value, or null.\n  /// }\n  /// ```\n  Future<Object?> sendValueToServer(Object? optimisticValue);\n\n  /// Optionally, override [onFinish] to run any code after the synchronization\n  /// process completes. For example, you might want to reload related data from\n  /// the server, show a confirmation message, or perform cleanup.\n  ///\n  /// Note [onFinish] is called in both success and failure scenarios.\n  /// On success it runs only after the state is stable for this key.\n  /// On failure it runs immediately after the request fails (there is\n  /// no further stabilization or follow-up).\n  ///\n  /// Important: The synchronization lock is released *before* [onFinish] runs.\n  /// This means new dispatches for the same key may start a new request while\n  /// [onFinish] is still executing.\n  ///\n  /// The [error] parameter will be `null` on success, or contain the error\n  /// object if the request failed.\n  ///\n  /// If [onFinish] returns a non-null state, it will be applied automatically.\n  /// If it returns `null`, no state change is made.\n  ///\n  /// ```dart\n  /// Future<AppState?> onFinish(Object? error) async {\n  ///   if (error == null) {\n  ///     // Success: show confirmation, log analytics, etc.\n  ///     return null;\n  ///   } else {\n  ///     // Failure:\n  ///     // - Show a dialog.\n  ///     // - Reload data from the server.\n  ///     // - Rollback the optimistic update.\n  ///   }\n  /// }\n  /// ```\n  ///\n  /// To show an error dialog in [onFinish]:\n  ///\n  /// ```dart\n  /// dispatch(UserExceptionAction('The server request failed', reason: 'Info reloaded.');\n  /// ```\n  ///\n  /// To reload data from the server in [onFinish]:\n  ///\n  /// ```dart\n  /// return state.copy(info: await api.loadInfo());\n  /// ```\n  ///\n  /// To rollback the optimistic update in [onFinish]:\n  ///\n  /// ```dart\n  /// return state.copy(isLiked: getValueFromState(initialState));\n  /// ```\n  ///\n  /// You can combine the above strategies as needed:\n  ///\n  /// ```dart\n  /// Future<AppState?> onFinish(Object? error) async {\n  ///   if (error == null) return null;\n  ///\n  ///   // 1. Show an error message to the user.\n  ///   dispatch(UserExceptionAction('The server request failed', reason: 'Info reloaded.'));\n  ///\n  ///   // 2. Immediately rollback to the initial state before the action.\n  ///   dispatchState(state.copy(info: await api.loadInfo());\n  ///\n  ///   // 3. Then, to be sure, reload the value from the database.\n  ///   return state.copy(isLiked: getValueFromState(initialState));\n  /// }\n  /// ```\n  ///\n  /// Important:\n  ///\n  /// - If `onFinish(error)` throws, the original [error] is lost and the error\n  ///   thrown by [onFinish] becomes the action error. You can handle it in\n  ///   [wrapError].\n  ///\n  /// - Same on success: If `onFinish(null)` throws, the whole action fails\n  ///   even though the server request succeeded.  You can handle it in\n  ///   [wrapError].\n  ///\n  Future<St?> onFinish(Object? error) async => null;\n\n  @override\n  Future<St?> reduce() async {\n    _cannot_combine_mixins_OptimisticSync();\n    _cannot_combine_mixins_UnlimitedRetryCheckInternet_OptimisticCommand_OptimisticSync_OptimisticSyncWithPush_ServerPush();\n\n    // Reset per-dispatch tracking fields.\n    lastSentValue = null;\n\n    // Compute and cache the key for this dispatch.\n    var _currentKey = computeOptimisticSyncKey();\n\n    final value = valueToApply();\n\n    // Store the optimistic value for this dispatch (available in onFinish).\n    optimisticValue = value;\n\n    // Always apply optimistic update immediately.\n    dispatchState(applyOptimisticValueToState(state, value));\n\n    // If locked, another request is in flight. The optimistic update is\n    // already applied, so just return. When the in-flight request completes,\n    // it will check if a follow-up is needed.\n    if (_optimisticSyncKeySet.contains(_currentKey)) return null;\n\n    // Acquire lock and send request.\n    _optimisticSyncKeySet.add(_currentKey);\n    await _sendAndFollowUp(_currentKey, value);\n\n    return null;\n  }\n\n  /// Set that tracks which keys are currently locked (requests in flight).\n  Set<Object?> get _optimisticSyncKeySet =>\n      store.internalMixinProps.optimisticSyncKeySet;\n\n  /// Sends the request and handles follow-up requests if the state changed\n  /// (by comparing the value returned by [getValueFromState] with [sentValue])\n  /// while the request was in flight.\n  ///\n  Future<void> _sendAndFollowUp(Object? key, T sentValue) async {\n    T _sentValue = sentValue;\n\n    int requestCount = 0;\n\n    while (true) {\n      requestCount++;\n\n      try {\n        // Track the value being sent (for debugging/rollback guards).\n        lastSentValue = _sentValue;\n\n        // Send the value and get the server response (may be null).\n        final Object? serverResponse = await sendValueToServer(_sentValue);\n\n        // Read the current value from the store.\n        // WARNING: In push mode this may reflect a server push, not local intent.\n        final stateValue = getValueFromState(state);\n\n        bool needFollowUp = false;\n\n        // Original value-based behavior (no push compatibility):\n        // If the store value differs from what we sent, send a follow-up with\n        // the current store value.\n        needFollowUp = ifShouldSendAnotherRequest(\n          stateValue: stateValue,\n          sentValue: _sentValue,\n          requestCount: requestCount,\n        );\n\n        if (needFollowUp) _sentValue = stateValue;\n\n        // If we need a follow-up, loop again without applying server response.\n        // The state is not stable yet.\n        if (needFollowUp) continue;\n\n        // State is stable for this key. Now we may apply the server response,\n        // but only if it is not stale relative to newer pushes.\n        if (serverResponse != null) {\n          final newState = applyServerResponseToState(state, serverResponse);\n          if (newState != null) dispatchState(newState);\n        }\n\n        // Release lock and finish.\n        _optimisticSyncKeySet.remove(key);\n        await _callOnFinish(null);\n        break;\n      } catch (error) {\n        // Request failed: release lock, run onFinish(error), then rethrow so the\n        // action still fails as before.\n        _optimisticSyncKeySet.remove(key);\n        await _callOnFinish(error);\n        rethrow;\n      }\n    }\n  }\n\n  /// Calls [onFinish], applying the returned state if non-null.\n  Future<void> _callOnFinish(Object? error) async {\n    final newState = await onFinish(error);\n    if (newState != null) dispatchState(newState);\n  }\n\n  /// If [ifShouldSendAnotherRequest] returns true, the action will perform one\n  /// more request to try and send the value from the state to the server.\n  ///\n  /// The default behavior of this method is to compare:\n  /// - The [stateValue], which is the value currently in the store state.\n  /// - The [sentValue], which is the value that was sent to the server.\n  ///\n  /// If both are different, it means that the state was changed after\n  /// we sent the request, so we should send another request with the new value.\n  ///\n  /// Optionally, override this method if you need custom equality logic.\n  /// The default implementation uses the `==` operator.\n  ///\n  /// The number of follow-up requests is limited at [maxFollowUpRequests] to\n  /// avoid infinite loops. If that limit is exceeded, a [StateError] is thrown.\n  ///\n  bool ifShouldSendAnotherRequest({\n    required T stateValue,\n    required T sentValue,\n    required int requestCount,\n  }) {\n    // Safety check to avoid infinite loops.\n    if ((maxFollowUpRequests != -1) && (requestCount > maxFollowUpRequests)) {\n      throw StateError('Too many follow-up requests '\n          'in action $runtimeType (> $maxFollowUpRequests).');\n    }\n\n    return (stateValue is ImmutableCollection &&\n            sentValue is ImmutableCollection)\n        ? !stateValue.same(sentValue)\n        : stateValue != sentValue;\n  }\n\n  /// Maximum number of follow-up requests to send before throwing an error.\n  /// This is a safety limit to avoid infinite loops. Override if you need a\n  /// different limit. Use `-1` for no limit.\n  int get maxFollowUpRequests => 10000;\n\n  /// Only [CheckInternet] and [AbortWhenNoInternet] can be combined\n  /// with [OptimisticSync].\n  void _cannot_combine_mixins_OptimisticSync() {\n    _incompatible<OptimisticSync, NonReentrant>(this);\n    _incompatible<OptimisticSync, Fresh>(this);\n    _incompatible<OptimisticSync, Throttle>(this);\n    _incompatible<OptimisticSync, Debounce>(this);\n    _incompatible<OptimisticSync, UnlimitedRetries>(this);\n    _incompatible<OptimisticSync, Polling>(this);\n  }\n\n  void\n      _cannot_combine_mixins_UnlimitedRetryCheckInternet_OptimisticCommand_OptimisticSync_OptimisticSyncWithPush_ServerPush() {\n    _incompatible<OptimisticSync, UnlimitedRetryCheckInternet>(this);\n    _incompatible<OptimisticSync, OptimisticCommand>(this);\n    _incompatible<OptimisticSync, OptimisticSyncWithPush>(this);\n    _incompatible<OptimisticSync, ServerPush>(this);\n    _incompatible<OptimisticSync, Retry>(this);\n  }\n}\n\n/// Mixin [OptimisticSyncWithPush] is designed for actions where:\n///\n/// 1. Your app receives server-pushed updates (WebSockets, Server-Sent Events\n///    (SSE), Firebase) that may modify the same state this action controls.\n///    It must be resilient to out-of-order delivery, and multiple devices can\n///    modify the same data.\n///\n/// 2. Non-blocking user interactions (like toggling a \"like\" button) should\n///    update the UI immediately and send the updated value to the server,\n///    making sure the server and the UI are eventually consistent.\n///\n/// 3. You want \"last write wins\" semantics across devices. In other words,\n///    with multiple devices, that's how we decide what truth is when two\n///    devices disagree.\n///\n/// In other words, it allows:\n/// - Optimistic UI\n/// - Multi device writes\n/// - Server push\n/// - Out of order delivery\n///\n/// **IMPORTANT:** If your app does not receive server-pushed updates,\n/// use the [OptimisticSync] mixin instead. In any case, please read the\n/// documentation of [OptimisticSync] first, as this mixin builds upon that\n/// behavior with additional logic to handle server-pushed updates.\n///\n/// ## How it works\n///\n/// 1. **Immediate UI feedback**: The action is not throttled or debounced in\n///    any way, and every dispatch applies an optimistic update to the state\n///    immediately. This guarantees a very good user experience, because there\n///    is immediate feedback on every interaction. Technically, every dispatch\n///    applies [valueToApply] to the state immediately via\n///    [applyOptimisticValueToState].\n///\n/// 2. **Single in-flight request**: The first time the action is dispatched,\n///    the updated value is immediately sent to the server. However, any other\n///    value changes that occur while the first request is in flight will NOT\n///    be sent, at least immediately. In other words, only **one** request is\n///    in flight at a time per key (as defined by [computeOptimisticSyncKey]\n///    and [optimisticSyncKeyParams]), because the first dispatch acquires a\n///    lock on that key, and other dispatches don't send requests when there is\n///    a lock. This potentially reduces the number of requests sent to the\n///    server, while coalescing intermediate changes.\n///\n/// 3. **Follow-up request**: If an action to update the state is dispatched\n///    while a current request started by [sendValueToServer] is in-flight\n///    (for example, the user tapped a \"like\" button again while the first\n///    request was pending), a follow-up request may be automatically sent after\n///    the current one completes. The necessity of a follow-up is decided\n///    automatically when the current request finishes, by internally keeping\n///    a local-revision associated with the dispatch `key`. This process repeats\n///    until the state stabilizes.\n///\n/// 4. **Push handling**: If a server push modifies the same state while a\n///    request is in-flight, when the request completes it checks whether the\n///    most recent change for this key was recorded as coming from a PUSH.\n///    If so, no follow-up request is needed, because the push already came\n///    from the server. This requires server pushes to be applied through\n///    an action that uses the [ServerPush] mixin, with the same `key`\n///    as the corresponding [OptimisticSyncWithPush] action.\n///\n/// 5. **Less intermediate requests**: If the state changes many times while the\n///    request is in-flight, it will coalesce all those changes into a single\n///    follow-up request. However, since [OptimisticSyncWithPush] uses a\n///    local-revision to track changes, it can end up sending a follow-up\n///    request even if the final value is the same as the previously sent value.\n///    This is necessary since here we assume other devices or users could have\n///    changed the value on the server in the meantime. Note this is different\n///    from mixin [OptimisticSync], which assumes only the current user/device\n///    is changing the value, and then compares the sent value with the current\n///    state value to decide if a follow-up request is needed.\n///\n/// 6. **Server response handling**: your implementation of [sendValueToServer]\n///    must call [informServerRevision] with a non-null revision after each\n///    successful request. If the revision is not informed, the mixin throws\n///    a [StateError] at runtime. This is necessary to handle out-of-order pushes\n///    correctly. Also, optionally, if [sendValueToServer] returns a non-null\n///    value, it is applied to the state via [applyServerResponseToState] when\n///    the state stabilizes, unless a newer known server revision for this key\n///    already exists (for example due to a newer push).\n///    Note: If the request started by [sendValueToServer] fails, then\n///    [sendValueToServer] should throw an error, and not call [informServerRevision].\n///\n/// 7. **Completion callback**: When the synchronization cycle for this key\n///    finishes, [onFinish] is called, allowing you to handle errors or perform\n///    side-effects, like showing a message or reloading data. On success, it\n///    runs after the state is stable (no follow-up needed) and the lock has\n///    been released. On failure, it runs right after the request fails and the\n///    lock is released, and then the action rethrows the error.\n///    Note: If the action is dispatched while the key is locked, it still\n///    applies the optimistic update immediately, but it does not call [onFinish].\n///    Only the dispatch that acquired the lock runs the network requests and\n///    calls [onFinish].\n///\n/// 8. **Safety limit**: To avoid infinite loops, the mixin enforces a maximum\n///    number of follow-up requests ([maxFollowUpRequests], default 10000).\n///    If exceeded, it throws a [StateError]. Override to change the limit\n///    or use -1 for no limit.\n///\n/// ## Flow example\n///\n/// ```\n/// State: liked = false\n///\n/// User taps LIKE:\n///   → State: liked = true (optimistic).\n///   → Lock acquired, Request 1 sends: setLiked(true).\n///   → Local-revision is 1.\n///\n/// User taps UNLIKE (Request 1 still in flight):\n///   → State: liked = false (optimistic).\n///   → No request sent (locked).\n///   → Local-revision is 2.\n///\n/// User taps LIKE (Request 1 still in flight):\n///   → State: liked = true (optimistic).\n///   → No request sent (locked).\n///   → Local-revision is 3.\n///\n/// Request 1 completes:\n///   → The last state change was NOT done with a PUSH.\n///   → Compares local-revision of the Request 1 (revision 1) with the current\n///     local-revision (which is revision 3).\n///   → They do NOT match, so a follow-up is needed.\n///   → Request 2 sends: setLiked(true).\n///\n/// Request 2 completes:\n///   → The last state change was NOT done with a PUSH.\n///   → Compares local-revision of the Request 2 (revision 3) with the\n///     current local-revision (which is also revision 3).\n///   → They match, no follow-up needed.\n///   → Lock released.\n/// ```\n///\n/// ## Flow example with PUSH\n///\n/// ```\n/// State: liked = false\n///\n/// User taps LIKE:\n///   → State: liked = true (optimistic)\n///   → Lock acquired, Request 1 sends: setLiked(true)\n///   → Local-revision is 1.\n///\n/// User taps UNLIKE (Request 1 still in flight):\n///   → State: liked = false (optimistic)\n///   → No request sent (locked)\n///   → Local-revision is 2.\n///\n/// A PUSH arrives with liked = false.\n///\n/// Request 1 completes:\n///   → The last state change was done with a PUSH.\n///   → So a follow-up is NOT needed.\n///   → Lock released.\n/// ```\n///\n/// ## Code example\n///\n/// ```dart\n/// class ToggleLikeAction extends ReduxAction<AppState>\n///   with OptimisticSyncWithPush<AppState, bool> {\n///\n///   @override\n///   Future<Object?> sendValueToServer(\n///     Object? optimisticValue,\n///     int localRevision,\n//      int deviceId,\n//      ) async {\n///        var response = await api.setLiked(itemId, optimisticValue, localRevision, deviceId);\n///        if (!response.ok) throw Exception('Server error');\n///        informServerRevision(response.serverRev);\n///        return response.liked;\n///     }\n/// }\n/// ```\n///\n/// Notes:\n/// - It can be combined with [CheckInternet] and [AbortWhenNoInternet].\n/// - It should not be combined with [NonReentrant], [Retry], [Throttle],\n///   [Debounce], [Fresh], [UnlimitedRetryCheckInternet], [UnlimitedRetries],\n///   [OptimisticCommand], [OptimisticSync].\n/// - Do not combine with [ServerPush] in the same action. Use [ServerPush] in\n///   a separate action that only handles server pushes.\n///\nmixin OptimisticSyncWithPush<St, T> on ReduxAction<St> {\n  //\n  /// Optionally, override [optimisticSyncKeyParams] to differentiate coalescing by\n  /// action parameters. For example, if you have a like button per item,\n  /// return the item ID so that different items can have concurrent requests:\n  ///\n  /// ```dart\n  /// Object? optimisticSyncKeyParams() => itemId;\n  /// ```\n  ///\n  /// You can also return a record of values:\n  ///\n  /// ```dart\n  /// Object? optimisticSyncKeyParams() => (userId, itemId);\n  /// ```\n  ///\n  /// See also: [computeOptimisticSyncKey], which uses this method by default to\n  /// build the key.\n  ///\n  Object? optimisticSyncKeyParams() => null;\n\n  /// By default the coalescing key combines the action [runtimeType]\n  /// with [optimisticSyncKeyParams]. Override this method if you want\n  /// different action types to share the same coalescing key.\n  Object computeOptimisticSyncKey() => (runtimeType, optimisticSyncKeyParams());\n\n  /// Override [valueToApply] to return the value that should be applied\n  /// optimistically to the state and then sent to the server. This is called\n  /// synchronously and only once per dispatch, when the reducer starts.\n  ///\n  /// The value to apply can be anything, and is usually constructed from the\n  /// action fields, and/or from the current [state]. Valid examples are:\n  ///\n  /// ```dart\n  /// // Set the like button to \"liked\".\n  /// bool valueToApply() => true\n  ///\n  /// // Set the like button to \"liked\" or \"not liked\", according to\n  /// // the field `isLiked` of the action.\n  /// bool valueToApply() => isLiked;\n  ///\n  /// // Toggles the current state of the like button.\n  /// bool valueToApply() => !state.items[itemId].isLiked;\n  /// ```\n  ///\n  T valueToApply();\n\n  /// Override [applyOptimisticValueToState] to return a new state where the\n  /// given [optimisticValue] is applied to the current [state].\n  ///\n  /// Note, AsyncRedux calculates [optimisticValue] by previously\n  /// calling [valueToApply].\n  ///\n  /// ```dart\n  /// AppState applyOptimisticValueToState(state, isLiked) =>\n  ///     state.copyWith(items: state.items.setLiked(itemId, isLiked));\n  /// ```\n  St applyOptimisticValueToState(St state, T optimisticValue);\n\n  /// Override [applyServerResponseToState] to return a new state, where the\n  /// given [serverResponse] (previously received from the server when running\n  /// [sendValueToServer]) is applied to the current [state]. Example:\n  ///\n  /// ```dart\n  /// AppState? applyServerResponseToState(state, serverResponse) =>\n  ///     state.copyWith(items: state.items.setLiked(itemId, serverResponse.isLiked));\n  /// ```\n  ///\n  /// Note [serverResponse] is never `null` here, because this method is only\n  /// called when [sendValueToServer] returned a non-null value.\n  ///\n  /// If you decide you DO NOT want to apply the server response to the state,\n  /// simply return `null`.\n  ///\n  St? applyServerResponseToState(St state, Object serverResponse);\n\n  /// Override [getValueFromState] to extract the value from the current [state].\n  /// If a follow-up request is needed, the value returned by [getValueFromState]\n  /// is the one that will now be sent to the server.\n  ///\n  /// ```dart\n  /// bool getValueFromState(state) => state.items[itemId].liked;\n  /// ```\n  T getValueFromState(St state);\n\n  /// The device ID is used to differentiate revisions from different devices.\n  /// The default is to use a random integer generated once per app run,\n  /// but you can override this to return a persistent unique ID per device.\n  static int Function() deviceId = () {\n    _deviceId ??=\n        Random().nextInt(4294967296) + (Random().nextInt(10000) * 10000000000);\n    return _deviceId!;\n  };\n\n  static int? _deviceId;\n\n  /// Override [sendValueToServer] to:\n  /// - Send the given [optimisticValue], the [localRevision], and the [deviceId]\n  ///   to the server.\n  /// - Set the current server-revision, by calling [informServerRevision].\n  /// - Optionally, return the server's response.\n  /// - You must throw an error if the request fails (in this case, do not\n  ///   call [informServerRevision]).\n  ///\n  /// Notes:\n  /// - AsyncRedux calculates [optimisticValue] by previously calling [valueToApply].\n  /// - The server must return the server-revision in the response.\n  /// - Server pushes must provide the 3 pieces of information: server-revision,\n  ///   [deviceId], and [localRevision]. See [ServerPush] for details.\n  ///\n  /// If [sendValueToServer] returns a non-null value, that value will be\n  /// applied to the state, but **only when the state stabilizes** (i.e., when\n  /// there are no more pending requests and the lock is about to be released).\n  /// This prevents the server response from overwriting subsequent user\n  /// interactions that occurred while the request was in flight.\n  ///\n  /// The value in the store state may change while the request is in flight,\n  /// both because of user interactions and because of server pushes.\n  /// In case the most recent state change was due to a user interaction,\n  /// (for example, if the user presses a like button once, but then presses\n  /// it again before the first request finishes), then [sendValueToServer] will\n  /// be called again to create a follow-up request to sync the updated state\n  /// with the server. In case the most recent state change was due to a server\n  /// push, no follow-up request is needed.\n  ///\n  /// ```dart\n  /// Future<Object?> sendValueToServer(\n  ///   Object? optimisticValue,\n  ///   int localRevision,\n  ///   int deviceId) async {\n  ///      var response = await api.setLiked(itemId, optimisticValue, localRevision, deviceId);\n  ///      if (!response.ok) throw Exception('Server error');\n  ///      informServerRevision(response.serverRev);\n  ///      return response.liked; // The mixin decides whether to apply this\n  /// }\n  /// ```\n  Future<Object?> sendValueToServer(\n    Object? optimisticValue,\n    int localRevision,\n    int deviceId,\n  );\n\n  /// Each dispatch calls [_localRevision] to increment the revision for\n  /// this key (the first call per dispatch increments; subsequent calls in\n  /// the same dispatch return the same value). The local-revision for the key\n  /// is stored in [_optimisticSyncWithPushRevisionMap].\n  ///\n  /// ## In more detail:\n  ///\n  /// Some state value may change because of:\n  ///\n  /// - An action was dispatched to change the value, in response to a user\n  ///  interaction. This is what happens when the user taps a like button,\n  ///  for example. These dispatched values are put optimistically\n  ///  in the state immediately, and this increments the local-revision.\n  ///\n  /// - A value may have arrived through a server push. These do NOT increment\n  ///   the local-revision.\n  ///\n  /// When a request completes, this is how we decide if we need to send a\n  /// follow-up request:\n  ///\n  /// - If the last applied value is from a PUSH, there is no need to send\n  ///   a follow-up.\n  ///\n  /// - If the last applied value is NOT from a PUSH, then we have to check\n  ///   the local-revision: If the local-revision of the request we sent is\n  ///   less than the current local-revision in the state, it means some other\n  ///   value was dispatched while the request was in flight, so we need to\n  ///   send a follow-up request with the latest value.\n  ///\n  int _localRevision() {\n    final key = _currentKey!;\n\n    if (_lazyLocalRevision == null) {\n      final current = _optimisticSyncWithPushRevisionMap[key];\n\n      // Increment for this dispatch.\n      _lazyLocalRevision = (current?.localRevision ?? 0) + 1;\n\n      final int fromMap = current?.serverRevision ?? -1;\n      final int fromState = getServerRevisionFromState(key);\n      final int seededServerRev = max(fromMap, fromState);\n\n      _optimisticSyncWithPushRevisionMap[key] = (\n        localRevision: _lazyLocalRevision!,\n        serverRevision: seededServerRev,\n        isPush: false,\n      );\n    }\n\n    return _lazyLocalRevision!;\n  }\n\n  int? _lazyLocalRevision;\n\n  /// Tracks the server revision informed by the server during\n  /// [sendValueToServer], which calls [informServerRevision].\n  /// This value is reset before each call to [sendValueToServer], so that\n  /// if it's null we know the server-revision was not informed correctly.\n  int? _informedServerRev;\n\n  /// Cached coalescing key for the current dispatch.\n  /// Computed once and then reused.\n  Object? _currentKey;\n\n  /// You must override this to return the server revision you saved in the\n  /// state in [ServerPush.applyServerPushToState] for the given [key].\n  /// Do return `-1` when unknown.\n  int getServerRevisionFromState(Object? key);\n\n  /// It's mandatory that you call [informServerRevision] from your overridden\n  /// [sendValueToServer], to inform the mixin about the server-revision\n  /// returned in the response.\n  ///\n  /// The server must provide a monotonically increasing revision number,\n  /// (for example, a timestamp, a version number, etc), comparable across\n  /// devices and users, that allows the app to determine the ordering of updates.\n  ///\n  /// The mixin uses this information internally to:\n  /// - Track the latest known server revision (for \"last write wins\" ordering)\n  /// - Determine whether to apply the server response (stale responses are\n  ///   automatically ignored).\n  ///\n  /// **Usage:** Just call this method with the serverRevision from the response.\n  /// The mixin handles all the logic - you don't need to check or compare\n  /// anything yourself. Example:\n  ///\n  /// ```dart\n  ///   @override\n  ///   Future<Object?> sendValueToServer(\n  ///     Object? optimisticValue,\n  ///     int localRevision,\n  ///      int deviceId,\n  ///      ) async {\n  ///        var response = await api.setLiked(itemId, optimisticValue, localRevision, deviceId);\n  ///        if (!response.ok) throw Exception('Server error');\n  ///        informServerRevision(response.serverRev);\n  ///        return response.liked;\n  ///     }\n  /// }\n  /// ```\n  ///\n  /// **Behavior:**\n  ///\n  /// - Only updates the stored serverRevision if `revision` is greater than\n  ///   the newest known serverRevision for this key, considering both:\n  ///   (1) the mixin's internal map entry (if any) and\n  ///   (2) `getServerRevisionFromState(key)` (if you persisted one in state).\n  ///   This prevents regression from stale or out-of-order updates.\n  ///\n  /// - The mixin will only apply the returned server response if this revision\n  ///   is not older than the newest known revision.\n  ///\n  /// See also: [informServerRevisionAsDateTime].\n  ///\n  void informServerRevision(int revision) {\n    _informedServerRev = revision;\n\n    final key = _currentKey!;\n    final entry = _optimisticSyncWithPushRevisionMap[key];\n\n    final int fromMap = entry?.serverRevision ?? -1;\n    final int fromState =\n        getServerRevisionFromState(key); // should return -1 if unknown\n    final int currentServerRev = max(fromMap, fromState);\n\n    // Only move forward, but keep local intent info.\n    if (revision > currentServerRev) {\n      _optimisticSyncWithPushRevisionMap[key] = (\n        localRevision: entry?.localRevision ?? 0,\n        serverRevision: revision,\n        isPush: false,\n      );\n    }\n  }\n\n  /// Convenience method to inform the server revision from a DateTime.\n  /// Uses `millisecondsSinceEpoch` as the revision number.\n  ///\n  /// See also: [informServerRevision].\n  ///\n  void informServerRevisionAsDateTime(DateTime revision) {\n    informServerRevision(revision.millisecondsSinceEpoch);\n  }\n\n  /// Optionally, override [onFinish] to run any code after the synchronization\n  /// process completes. For example, you might want to reload related data from\n  /// the server, show a confirmation message, or perform cleanup.\n  ///\n  /// Note [onFinish] is called in both success and failure scenarios, but only\n  /// after the state stabilizes for this key (that is, after the last request\n  /// finishes and no follow-up request is needed).\n  ///\n  /// Important: The synchronization lock is released *before* [onFinish] runs.\n  /// This means new dispatches for the same key may start a new request while\n  /// [onFinish] is still executing.\n  ///\n  /// The [error] parameter will be `null` on success, or contain the error\n  /// object if the request failed.\n  ///\n  /// If [onFinish] returns a non-null state, it is applied. On success it\n  /// becomes the action's final reduced state. On failure it is dispatched and\n  /// then the original error is rethrown. If it returns `null`, no extra state\n  /// change is made.\n  ///\n  /// ```dart\n  /// Future<St?> onFinish(Object? error) async {\n  ///   if (error == null) {\n  ///     // Success: show confirmation, log analytics, etc.\n  ///     return null;\n  ///   } else {\n  ///     // Failure: reload data from the server.\n  ///     var reloadedInfo = await api.loadInfo();\n  ///     return state.copy(info: reloadedInfo);\n  ///   }\n  /// }\n  /// ```\n  ///\n  /// Important:\n  ///\n  /// - If `onFinish(error)` throws, the original [error] is lost and the error\n  ///   thrown by [onFinish] becomes the action error. You can handle it in\n  ///   [wrapError].\n  ///\n  /// - Same on success: If `onFinish(null)` throws, the whole action fails\n  ///   even though the server request succeeded.  You can handle it in\n  ///   [wrapError].\n  ///\n  Future<St?> onFinish(Object? error) async => null;\n\n  @override\n  Future<St?> reduce() async {\n    _cannot_combine_mixins_OptimisticSyncWithPush();\n    _cannot_combine_mixins_UnlimitedRetryCheckInternet_OptimisticCommand_OptimisticSync_OptimisticSyncWithPush_ServerPush();\n\n    // Compute and cache the key for this dispatch.\n    _currentKey = computeOptimisticSyncKey();\n\n    var localRevision = _localRevision();\n\n    T value = valueToApply();\n\n    // Always apply optimistic update immediately.\n    dispatchState(applyOptimisticValueToState(state, value));\n\n    // If locked, another request is in flight. The optimistic update is\n    // already applied, so just return. When the in-flight request completes,\n    // it will check if a follow-up is needed.\n    if (_optimisticSyncKeySet.contains(_currentKey)) return null;\n\n    // Acquire lock.\n    _optimisticSyncKeySet.add(_currentKey);\n\n    int requestCount = 0;\n\n    while (true) {\n      //\n      // Safety check to avoid infinite loops.\n      requestCount++;\n      if ((maxFollowUpRequests != -1) && (requestCount > maxFollowUpRequests)) {\n        throw StateError('Too many follow-up requests '\n            'in action $runtimeType (> $maxFollowUpRequests).');\n      }\n\n      // Reset before each request so we can detect whether the user called\n      // `informServerRevision()` while executing `sendValueToServer`.\n      _informedServerRev = null;\n\n      try {\n        // Send the value and get the server response (may be null).\n        final Object? serverResponse = await sendValueToServer(\n          value,\n          localRevision,\n          deviceId(),\n        );\n\n        // Validate that the developer called informServerRevision().\n        if (_informedServerRev == null) {\n          throw StateError(\n            'The OptimisticSyncWithPush mixin requires calling '\n            'informServerRevision() inside sendValueToServer(). '\n            'If you don\\'t need server-push handling, use OptimisticSync instead.',\n          );\n        }\n\n        // Revision-based follow-up decision:\n        // If localRevision advanced since this request started, the user changed\n        // intent while the request was in flight, so we may need a follow-up.\n        final entry = _getEntry(_currentKey);\n        final int currentLocalRev = entry.localRevision;\n        final int currentServerRev = entry.serverRevision;\n        final bool isPush = entry.isPush;\n\n        // If the current value was created by the user locally (it's not\n        // from push), and localRevision advanced, we need a follow-up.\n        if (!isPush && (currentLocalRev > localRevision)) {\n          _optimisticSyncWithPushRevisionMap[_currentKey] = (\n            localRevision: currentLocalRev,\n            serverRevision: currentServerRev,\n            isPush: false,\n          );\n\n          // Read the current value from the store.\n          // Will loop one more time, to do the follow-up request.\n          value = getValueFromState(state);\n          localRevision = currentLocalRev;\n        }\n        //\n        // If the state is stable for this key, we may apply the server response,\n        // but only if it is not stale relative to newer pushes.\n        else {\n          // State is stable for this key. Now we may apply the server response,\n          // but only if it is not stale relative to newer pushes.\n          if (serverResponse != null) {\n            // Only apply if the informed server revision still matches the latest\n            // known server revision for this key (i.e., no newer push arrived).\n            final bool shouldApply = _informedServerRev! >= currentServerRev;\n\n            if (shouldApply) {\n              _optimisticSyncWithPushRevisionMap[_currentKey] = (\n                localRevision: currentLocalRev,\n                serverRevision: _informedServerRev!,\n                isPush: false,\n              );\n\n              final newState =\n                  applyServerResponseToState(state, serverResponse);\n              if (newState != null) dispatchState(newState);\n            }\n          }\n\n          // Release lock and finish.\n          _optimisticSyncKeySet.remove(_currentKey);\n          final newState = await onFinish(null);\n\n          // Break the loop.\n          if (newState != null) return newState;\n          break;\n        }\n      }\n      //\n      catch (error) {\n        // Request failed: release lock, run onFinish(error),\n        // then rethrow so the action still fails as before.\n        _optimisticSyncKeySet.remove(_currentKey);\n        final newState = await onFinish(error);\n        if (newState != null) dispatchState(newState);\n        rethrow;\n      }\n    }\n\n    return null;\n  }\n\n  /// Set that tracks which keys are currently locked (requests in flight).\n  Set<Object?> get _optimisticSyncKeySet =>\n      store.internalMixinProps.optimisticSyncKeySet;\n\n  /// Map used by the [OptimisticSyncWithPush] and [ServerPush] mixins.\n  Map<Object?, OptimisticSyncWithPushRevisionEntry>\n      get _optimisticSyncWithPushRevisionMap =>\n          store.internalMixinProps.optimisticSyncWithPushRevisionMap;\n\n  OptimisticSyncWithPushRevisionEntry _getEntry(Object? key) =>\n      _optimisticSyncWithPushRevisionMap[key] ??\n      (\n        localRevision: 0,\n        serverRevision: getServerRevisionFromState(key),\n        isPush: false,\n      );\n\n  /// Maximum number of follow-up requests to send before throwing an error.\n  /// This is a safety limit to avoid infinite loops. Override if you need a\n  /// different limit. Use `-1` for no limit.\n  int get maxFollowUpRequests => 10000;\n\n  /// Only [CheckInternet] and [AbortWhenNoInternet] can be combined\n  /// with [OptimisticSyncWithPush].\n  void _cannot_combine_mixins_OptimisticSyncWithPush() {\n    _incompatible<OptimisticSyncWithPush, NonReentrant>(this);\n    _incompatible<OptimisticSyncWithPush, Fresh>(this);\n    _incompatible<OptimisticSyncWithPush, Throttle>(this);\n    _incompatible<OptimisticSyncWithPush, Debounce>(this);\n    _incompatible<OptimisticSyncWithPush, UnlimitedRetries>(this);\n    _incompatible<OptimisticSyncWithPush, Polling>(this);\n  }\n\n  void\n      _cannot_combine_mixins_UnlimitedRetryCheckInternet_OptimisticCommand_OptimisticSync_OptimisticSyncWithPush_ServerPush() {\n    _incompatible<OptimisticSyncWithPush, UnlimitedRetryCheckInternet>(this);\n    _incompatible<OptimisticSyncWithPush, OptimisticCommand>(this);\n    _incompatible<OptimisticSyncWithPush, OptimisticSync>(this);\n    _incompatible<OptimisticSyncWithPush, ServerPush>(this);\n    _incompatible<OptimisticSyncWithPush, Retry>(this);\n  }\n}\n\ntypedef PushMetadata = ({\n  int serverRevision,\n  int localRevision,\n  int deviceId,\n});\n\n/// Mixin [ServerPush] should be used by actions that put, in the store state,\n/// values that were received by server-push, via WebSockets, Server-Sent\n/// Events (SSE), Firebase, etc.\n///\n/// It works together with [OptimisticSyncWithPush] to ensure that out-of-order\n/// pushes do not corrupt the state, and that local optimistic updates are not\n/// overwritten by stale pushes.\n///\nmixin ServerPush<St> on ReduxAction<St> {\n  /// You must override this to return the type of the action that uses the\n  /// corresponding [OptimisticSyncWithPush] that owns this value (so both\n  /// compute the same stable-sync key).\n  Type associatedAction();\n\n  /// Same meaning as in [OptimisticSyncWithPush]:\n  /// the params that differentiate keys.\n  Object? optimisticSyncKeyParams() => null;\n\n  /// Must match the [OptimisticSyncWithPush] action key computation.\n  /// Default: (associatedActionType, optimisticSyncKeyParams)\n  Object computeOptimisticSyncKey() =>\n      (associatedAction(), optimisticSyncKeyParams());\n\n  /// You must override this to provide the [PushMetadata] that came with the\n  /// push, including:\n  ///\n  /// - The server-revision number.\n  /// - The local-revision number.\n  /// - The device-ID.\n  ///\n  /// For example:\n  ///\n  /// ```dart\n  /// class PushLikeUpdate extends AppAction with ServerPush {\n  ///   final bool liked;\n  ///   final PushMetadata metadata;\n  ///   PushLikeUpdate({required this.liked, required this.metadata});\n  ///\n  ///   Type associatedAction() => ToggleLikeAction;\n  ///\n  ///   PushMetadata pushMetadata() => metadata;\n  ///\n  ///   AppState? applyServerPushToState(AppState state, Object? key, int serverRev)\n  ///     => state.copy(liked: liked, revision: (key, serverRev));\n  /// }\n  /// ```\n  PushMetadata pushMetadata();\n\n  /// You must override this to:\n  /// - Apply the pushed data to [state].\n  /// - Save the [serverRevision] for the current [key] to the [state].\n  ///\n  /// Return `null` to ignore the push.\n  ///\n  St? applyServerPushToState(St state, Object? key, int serverRevision);\n\n  /// You must override this to return the server revision you saved in the\n  /// state in [ServerPush.applyServerPushToState] for the given [key].\n  /// Do return `-1` when unknown.\n  int getServerRevisionFromState(Object? key);\n\n  @override\n  St? reduce() {\n    _cannot_combine_mixins_ServerPush();\n    _cannot_combine_mixins_UnlimitedRetryCheckInternet_OptimisticCommand_OptimisticSync_OptimisticSyncWithPush_ServerPush();\n\n    final key = computeOptimisticSyncKey();\n\n    var (\n      :serverRevision,\n      :localRevision,\n      :deviceId,\n    ) = pushMetadata();\n\n    final current = _optimisticSyncWithPushRevisionMap[key];\n    final int serverRevision_FromMap = current?.serverRevision ?? -1;\n    final int serverRevision_FromState = getServerRevisionFromState(key);\n\n    // Determine the current known server revision for this key.\n    // This is the max of what we have in the map versus what is in the state.\n    final currentServerRev =\n        max(serverRevision_FromMap, serverRevision_FromState);\n\n    // Seed the map from persisted state, if needed.\n    // This is important even when we ignore the push as stale.\n    if ((serverRevision_FromMap == -1) && (serverRevision_FromState >= 0)) {\n      _optimisticSyncWithPushRevisionMap[key] = (\n        localRevision: 0,\n        serverRevision: serverRevision_FromState,\n        isPush: true,\n      );\n    }\n\n    // Ignore stale/out-of-order pushes.\n    if (serverRevision <= currentServerRev) {\n      return null;\n    }\n\n    final entry = _optimisticSyncWithPushRevisionMap[key];\n    final int currentLocalRev = entry?.localRevision ?? 0;\n\n    final bool isSelf = (deviceId == OptimisticSyncWithPush.deviceId());\n\n    // Self-echo of an older request: treat as ACK only.\n    // Do NOT apply and do NOT mark isPush=true (otherwise it cancels follow-ups).\n    if (isSelf && (localRevision < currentLocalRev)) {\n      _optimisticSyncWithPushRevisionMap[key] = (\n        localRevision: currentLocalRev,\n        serverRevision: serverRevision,\n        isPush: false,\n      );\n      return null;\n    }\n\n    // Safe to apply (external push, or self echo that matches latest intent).\n    final newState = applyServerPushToState(state, key, serverRevision);\n\n    // Always record newest known server revision, even if user ignores the push (newState == null).\n    final int storedLocalRev =\n        isSelf ? max(currentLocalRev, localRevision) : currentLocalRev;\n\n    _optimisticSyncWithPushRevisionMap[key] = (\n      localRevision: storedLocalRev,\n      serverRevision: serverRevision,\n      isPush: true,\n    );\n\n    return newState;\n  }\n\n  Map<Object?, OptimisticSyncWithPushRevisionEntry>\n      get _optimisticSyncWithPushRevisionMap =>\n          store.internalMixinProps.optimisticSyncWithPushRevisionMap;\n\n  void _cannot_combine_mixins_ServerPush() {\n    _incompatible<ServerPush, CheckInternet>(this);\n    _incompatible<ServerPush, AbortWhenNoInternet>(this);\n    _incompatible<ServerPush, NonReentrant>(this);\n    _incompatible<ServerPush, Fresh>(this);\n    _incompatible<ServerPush, Throttle>(this);\n    _incompatible<ServerPush, Debounce>(this);\n    _incompatible<ServerPush, UnlimitedRetries>(this);\n    _incompatible<ServerPush, Polling>(this);\n  }\n\n  void\n      _cannot_combine_mixins_UnlimitedRetryCheckInternet_OptimisticCommand_OptimisticSync_OptimisticSyncWithPush_ServerPush() {\n    _incompatible<ServerPush, UnlimitedRetryCheckInternet>(this);\n    _incompatible<ServerPush, OptimisticCommand>(this);\n    _incompatible<ServerPush, OptimisticSync>(this);\n    _incompatible<ServerPush, Retry>(this);\n  }\n}\n\nenum Poll {\n  /// Start polling.\n  /// If polling is already active, does nothing.\n  /// Otherwise, runs the action immediately and starts periodic polling.\n  start,\n\n  /// Stop polling (cancels the timer and does not run the action).\n  stop,\n\n  /// Run the action immediately and restart polling from now.\n  /// If polling is not active, behaves like [Poll.start].\n  runNowAndRestart,\n\n  /// Run the action once immediately.\n  /// Does not start, stop, cancel, or restart polling.\n  once,\n}\n\n/// Mixin [Polling] can be used to periodically dispatch an action at a fixed\n/// interval. Just add `with Polling` to your action. For example:\n///\n/// ```dart\n/// class PollPrices extends AppAction with Polling {\n///   @override final Poll poll;\n///   PollPrices([this.poll = Poll.once]);\n///\n///   @override\n///   ReduxAction<AppState> createPollingAction() => PollPrices();\n///\n///   @override\n///   Future<AppState?> reduce() async {\n///     final prices = await api.getPrices();\n///     return state.copy(prices: prices);\n///   }\n/// }\n/// ```\n///\n/// This is useful when you need to keep data fresh by fetching it from a server\n/// at regular intervals, such as refreshing prices, checking for new messages,\n/// or monitoring wallet balances.\n///\n/// The [pollInterval] is the delay between polling ticks. The default is 10\n/// seconds. You can override it:\n///\n/// ```dart\n/// @override\n/// Duration get pollInterval => const Duration(minutes: 5);\n/// ```\n///\n/// To start polling, dispatch the action with [Poll.start].\n/// To stop, dispatch with [Poll.stop]:\n///\n/// ```dart\n/// // Start polling (also runs reduce immediately):\n/// dispatch(PollPrices(Poll.start));\n///\n/// // Stop polling:\n/// dispatch(PollPrices(Poll.stop));\n/// ```\n///\n/// You can display loading states and errors in your widgets by tracking the\n/// action type that does the work:\n///\n/// ```dart\n/// if (context.isWaiting(PollPrices)) CircularProgressIndicator();\n/// if (context.isFailed(PollPrices)) Text('Failed to load prices');\n/// ```\n///\n/// If you use two separate action types (see Option 2 below), track the worker\n/// action instead of the polling controller.\n///\n/// There are two ways to use this mixin:\n///\n/// ## Option 1: Single action for everything\n///\n/// Use one action class that both controls polling and does the work.\n/// The [createPollingAction] returns the same action type with [Poll.once]\n/// (or with no poll field at all, since [Poll.once] is the default),\n/// so timer ticks run the action without restarting the timer:\n///\n/// ```dart\n/// class LoadBalanceAction extends AppAction with Polling {\n///   final WalletAddress address;\n///   @override final Poll poll;\n///\n///   LoadBalanceAction(this.address, {this.poll = Poll.once});\n///\n///   @override\n///   Duration get pollInterval => const Duration(minutes: 5);\n///\n///   @override\n///   ReduxAction<AppState> createPollingAction() => LoadBalanceAction(address);\n///\n///   @override\n///   Future<AppState?> reduce() async {\n///     final balance = await api.getBalance(address);\n///     return state.copy(balance: balance);\n///   }\n/// }\n///\n/// // Run immediately without affecting the timer\n/// dispatch(LoadBalanceAction(address));\n///\n/// // Start polling\n/// dispatch(LoadBalanceAction(address, poll: Poll.start));\n///\n/// // Stop polling\n/// dispatch(LoadBalanceAction(address, poll: Poll.stop));\n/// ```\n///\n/// ## Option 2: Separate action types\n///\n/// Use one action to control polling, and a different action to do the work.\n/// This is useful when you want `isWaiting` and `isFailed` to track a\n/// different type than the polling controller:\n///\n/// ```dart\n/// class PollBalance extends AppAction with Polling {\n///   final WalletAddress address;\n///   @override final Poll poll;\n///\n///   PollBalance(this.address, {this.poll = Poll.start});\n///\n///   @override\n///   Duration get pollInterval => const Duration(minutes: 5);\n///\n///   @override\n///   ReduxAction<AppState> createPollingAction() => LoadBalanceAction(address);\n///\n///   @override\n///   Future<AppState?> reduce() async {\n///     await dispatchAndWait(LoadBalanceAction(address));\n///     return null;\n///   }\n///\n/// class LoadBalanceAction extends AppAction {\n///   final WalletAddress address;\n///   LoadBalanceAction(this.address);\n///\n///   @override\n///   Future<AppState?> reduce() async {\n///     final balance = await api.getBalance(address);\n///     return state.copy(balance: balance);\n///   }\n/// }\n///\n/// // Start polling:\n/// dispatch(PollBalance(address, poll: Poll.start));\n///\n/// // Check loading state of the worker action:\n/// isWaiting(LoadBalanceAction);\n///\n/// // Stop polling:\n/// dispatch(PollBalance(address, poll: Poll.stop));\n/// ```\n///\n/// ## Polling keys\n///\n/// By default, each action type gets its own independent polling timer,\n/// keyed by its [runtimeType]. This means all instances of the same action\n/// type share one timer.\n///\n/// ### Using [pollingKeyParams] to separate instances\n///\n/// If you need separate polling timers per id, address, or some other field,\n/// override [pollingKeyParams]. Actions of the same type but with different\n/// [pollingKeyParams] values get independent timers.\n///\n/// ```dart\n/// class PollBalance extends AppAction with Polling {\n///   final WalletAddress address;\n///   @override final Poll poll;\n///\n///   PollBalance(this.address, {this.poll = Poll.once});\n///\n///   // Each address gets its own independent polling timer.\n///   @override\n///   Object? pollingKeyParams() => address;\n///\n///   @override\n///   ReduxAction<AppState> createPollingAction() =>\n///       LoadBalanceAction(address);\n///\n///   @override\n///   Future<AppState?> reduce() async {\n///     await dispatchAndWait(LoadBalanceAction(address));\n///     return null;\n///   }\n///\n/// // These start two independent polling timers:\n/// dispatch(PollBalance(address1, poll: Poll.start));\n/// dispatch(PollBalance(address2, poll: Poll.start));\n///\n/// // Stop only address1:\n/// dispatch(PollBalance(address1, poll: Poll.stop));\n/// ```\n///\n/// You can also return more than one field by using a tuple:\n///\n/// ```dart\n/// // Each (userId, walletId) pair gets its own timer.\n/// Object? pollingKeyParams() => (userId, walletId);\n/// ```\n///\n/// ### Using [computePollingKey] to share timers across action types\n///\n/// If you want different action types to share the same polling timer,\n/// override [computePollingKey] and return any key you want:\n///\n/// ```dart\n/// class PollPrices extends AppAction with Polling {\n///   Object computePollingKey() => 'market-data';\n///   ...\n/// }\n///\n/// class PollVolumes extends AppAction with Polling {\n///   Object computePollingKey() => 'market-data'; // same key\n///   ...\n/// }\n/// ```\n///\n/// With this setup, starting `PollPrices` and then `PollVolumes` means\n/// `PollVolumes` is a no-op (the key is already active). Stopping either\n/// one cancels the shared timer.\n///\n/// ## Poll values\n///\n/// - [Poll.start]: Starts polling and runs [reduce] immediately.\n///   If polling is already active for this key, does nothing.\n///\n/// - [Poll.stop]: Cancels the polling for this key and skips [reduce].\n///\n/// - [Poll.runNowAndRestart]: Runs [reduce] immediately and restarts the polling timer\n///   from that moment. If polling is not active, behaves like [Poll.start].\n///\n/// - [Poll.once]: Runs [reduce] immediately, without affecting the polling\n///   (it does not start or stop the polling).\n///\n/// Instead of using a periodic timer, each run schedules the next one,\n/// so the polling interval is measured from the end of each run.\n///\n/// Notes:\n/// - This mixin can be combined with [CheckInternet], [AbortWhenNoInternet],\n///   [NonReentrant], [Throttle], and [Fresh].\n/// - It should not be combined with other mixins or classes that override [wrapReduce].\n/// - It should not be combined with [Retry], [UnlimitedRetries], [Debounce],\n///   [UnlimitedRetryCheckInternet], [OptimisticCommand], [OptimisticSync],\n///   [OptimisticSyncWithPush], or [ServerPush].\n///\n/// See also:\n/// * [Throttle] - If you want to limit how often an action runs, but don't need periodic repetition.\n/// * [Debounce] - If you want to wait for a pause in activity before running the action.\n/// * [NonReentrant] - If you want to prevent overlapping executions of the same action.\n///\nmixin Polling<St> on ReduxAction<St> {\n  Poll get poll;\n\n  Duration get pollInterval => const Duration(seconds: 10);\n\n  /// Must return a new action instance that the timer will dispatch on each\n  /// tick. This can be the same action type with [Poll.once], or a completely\n  /// different action type (see class docs for both patterns).\n  ReduxAction<St> createPollingAction();\n\n  /// By default, the polling key is based on the action [runtimeType].\n  /// All instances of the same action type share one polling timer.\n  ///\n  /// Override this to give each instance its own timer based on some field:\n  ///\n  /// ```dart\n  /// // Each address gets its own polling timer.\n  /// Object? pollingKeyParams() => address;\n  ///\n  /// // Each (userId, walletId) pair gets its own timer.\n  /// Object? pollingKeyParams() => (userId, walletId);\n  /// ```\n  ///\n  /// When [pollingKeyParams] returns `null` (the default), the key is\n  /// just the action type.\n  Object? pollingKeyParams() => null;\n\n  /// Returns the key used to identify this action's polling timer.\n  ///\n  /// The default combines [runtimeType] with [pollingKeyParams]:\n  /// ```dart\n  /// Object computePollingKey() => (runtimeType, pollingKeyParams());\n  /// ```\n  ///\n  /// Override this for full control, for example to share a timer\n  /// across different action types:\n  ///\n  /// ```dart\n  /// Object computePollingKey() => 'shared-market-data';\n  /// ```\n  Object computePollingKey() => (runtimeType, pollingKeyParams());\n\n  Map<Object?, Timer> get _pollingMap => store.internalMixinProps.pollingMap;\n\n  @override\n  Future<St?> wrapReduce(Reducer<St> reduce) async {\n    _cannot_combine_mixins_Polling();\n\n    final key = computePollingKey();\n\n    switch (poll) {\n      case Poll.start:\n      // If polling is already active, don't do anything.\n        if (_pollingMap.containsKey(key)) return null;\n        _scheduleNext(key);\n        return reduce();\n\n      case Poll.stop:\n        _pollingMap.remove(key)?.cancel();\n        return null;\n\n      case Poll.runNowAndRestart:\n        _pollingMap.remove(key)?.cancel();\n        _scheduleNext(key);\n        return reduce();\n\n      case Poll.once:\n        return reduce();\n    }\n  }\n\n  /// Schedules a one-shot timer that dispatches [createPollingAction] and\n  /// then schedules the next tick. The interval is measured from the moment\n  /// the previous tick completes.\n  void _scheduleNext(Object key) {\n    _pollingMap[key] = Timer(pollInterval, () {\n      if (_pollingMap.containsKey(key)) {\n        dispatch(createPollingAction());\n        _scheduleNext(key);\n      }\n    });\n  }\n\n  void _cannot_combine_mixins_Polling() {\n    _incompatible<Polling, Retry>(this);\n    _incompatible<Polling, UnlimitedRetries>(this);\n    _incompatible<Polling, Debounce>(this);\n    _incompatible<Polling, UnlimitedRetryCheckInternet>(this);\n    _incompatible<Polling, OptimisticCommand>(this);\n    _incompatible<Polling, OptimisticSync>(this);\n    _incompatible<Polling, OptimisticSyncWithPush>(this);\n    _incompatible<Polling, ServerPush>(this);\n  }\n}\n"
  },
  {
    "path": "lib/src/action_observer.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'dart:io';\n\nimport 'package:async_redux/async_redux.dart';\n\nabstract class ActionObserver<St> {\n  /// If `ini==true` this is right before the action is dispatched.\n  /// If `ini==false` this is right after the action finishes.\n  void observe(\n    ReduxAction<St> action,\n    int dispatchCount, {\n    required bool ini,\n  });\n}\n\n/// This action-observer will print all actions to the console like so:\n///\n/// ```\n/// I/flutter (15304): | Action MyAction\n/// ```\n///\n/// This helps with development, so you probably don't want to use it in\n/// release mode:\n///\n/// ```\n/// store = Store<AppState>(\n///    ...\n///    actionObservers: kReleaseMode ? null : [ConsoleActionObserver()],\n/// );\n/// ```\n///\n/// If you implement the action's [toString], you can display more information.\n/// For example, suppose a LoginAction which has a username field:\n///\n/// ```\n/// class LoginAction extends ReduxAction {\n///   final String username;\n///   ...\n///   String toString() => super.toString() + '(username)';\n/// }\n/// ```\n///\n/// The above code will print something like this:\n///\n/// ```\n/// I/flutter (15304): | Action LoginAction(user32)\n/// ```\n///\nclass ConsoleActionObserver<St> extends ActionObserver<St> {\n  /// If [useAnsiColors] is `true`, the output will use ANSI escape codes for\n  /// colored output. Defaults to `false`, because not all consoles support\n  /// ANSI colors.\n  final bool? useAnsiColors;\n\n  ConsoleActionObserver({this.useAnsiColors});\n\n  @override\n  void observe(ReduxAction<St> action, int dispatchCount, {required bool ini}) {\n    if (ini) {\n      bool _useAnsiColors;\n\n      // If useAnsiColors is explicitly true, use true.\n      if (useAnsiColors == true)\n        _useAnsiColors = true;\n      //\n      // If useAnsiColors is explicitly false, use false.\n      else if (useAnsiColors == false)\n        _useAnsiColors = false;\n      //\n      // If useAnsiColors is `null`, use ANSI colors on Windows only.\n      // This is because the IntelliJ console on Mac/Linux doesn't support ANSI\n      // colors. Note, ideally we should check if the console itself supports\n      // ANSI colors, but an Android emulator running on Windows doesn't know\n      // where it's running, so we just assume Windows when the target is Windows.\n      else\n        _useAnsiColors = Platform.isWindows;\n\n      print(_useAnsiColors\n          ? '${color(action)}|$italic $action$reset'\n          : '| $action');\n    }\n  }\n\n  /// Callback that chooses the color to print in the console.\n  static String Function(ReduxAction action) color = //\n      (ReduxAction action) => //\n          (action is WaitAction || action is NavigateAction) //\n              ? green\n              : yellow;\n\n  // See ANSI Colors here: https://pub.dev/packages/ansicolor\n  static const white = \"\\x1B[38;5;255m\";\n  static const reversed = \"\\u001b[7m\";\n  static const red = \"\\x1B[38;5;9m\";\n  static const blue = \"\\x1B[38;5;45m\";\n  static const yellow = \"\\x1B[38;5;226m\";\n  static const green = \"\\x1B[38;5;118m\";\n  static const grey = \"\\x1B[38;5;246m\";\n  static const dark = \"\\x1B[38;5;238m\";\n  static const bold = \"\\u001b[1m\";\n  static const italic = \"\\u001b[3m\";\n  static const boldItalic = bold + italic;\n  static const boldItalicOff = boldOff + italicOff;\n  static const boldOff = \"\\u001b[22m\";\n  static const italicOff = \"\\u001b[23m\";\n  static const reset = \"\\u001b[0m\";\n}\n"
  },
  {
    "path": "lib/src/advanced_user_exception.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:fast_immutable_collections/fast_immutable_collections.dart';\nimport 'package:flutter/foundation.dart';\nimport \"package:meta/meta.dart\";\n\n/// Extends the [UserException] to add more features.\n///\n/// The [AdvancedUserException] is not supposed to be instantiated directly. Instead, use\n/// the [addCallbacks], [addCause] and [addProps] extension methods in the [UserException]:\n///\n/// ```dart\n/// UserException(message, code: code, reason: reason)\n///    .addCallbacks(onOk: onOk, onCancel: onCancel)\n///    .addCause(cause)\n///    .addProps(props);\n/// ```\n///\n/// Example:\n///\n/// ```dart\n/// throw UserException('Invalid number', reason: 'Must be less than 42')\n///    .addCallbacks(onOk: () => print('OK'), onCancel: () => print('CANCEL'))\n///    .addCause(FormatException('Invalid input'))\n///    .addProps({'number': 42}));\n/// ```\n///\n/// When the exception is shown to the user in the [UserExceptionDialog], if\n/// callbacks [onOk] and [onCancel] are defined, the dialog will have OK and CANCEL buttons,\n/// and the callbacks will be called when the user taps them.\n///\n/// The [hardCause] is some error which caused the [UserException].\n///\n/// The [props] are any key-value pair properties you'd like to add to the exception.\n///\n@immutable\nclass AdvancedUserException extends UserException {\n  //\n\n  /// Callback to be called after the user views the error, and taps OK in the dialog.\n  final VoidCallback? onOk;\n\n  /// Callback to be called after the user views the error, and taps CANCEL in the dialog.\n  final VoidCallback? onCancel;\n\n  /// The hard cause is some error which caused the [UserException], but that is not\n  /// a [UserException] itself. For example: `int.parse('a')` throws a `FormatException`.\n  /// Then: `throw UserException('Invalid number').addCause(FormatException('Invalid input'))`.\n  /// will have the `FormatException` as the hard cause. Note: If a [UserException] is\n  /// passed as the hard cause, it will be added with [addCause], and will not become the\n  /// hard cause. In other words, a [UserException] will never be a hard cause.\n  final Object? hardCause;\n\n  /// The properties added to the exception, if any.\n  /// They are an immutable-map of type [IMap], of key-value pairs.\n  /// To read the properties, use the `[]` operator, like this:\n  /// ```dart\n  /// var value = exception.props['key'];\n  /// ```\n  /// If the key does not exist, it will return `null`.\n  ///\n  final IMap<String, dynamic> props;\n\n  /// Instead of using this constructor directly, prefer doing:\n  ///\n  /// ```dart\n  /// throw UserException('Invalid number', reason: 'Must be less than 42')\n  ///    .addCallbacks(onOk: () => print('OK'), onCancel: () => print('CANCEL'))\n  ///    .addCause(FormatException('Invalid input'))\n  ///    .addProps({'number': 42}));\n  /// ```\n  ///\n  /// This constructor is public only so that you can subclass [AdvancedUserException].\n  ///\n  const AdvancedUserException(\n    super.message, {\n    required super.reason,\n    required super.code,\n    required super.errorText,\n    required super.ifOpenDialog,\n    required this.onOk,\n    required this.onCancel,\n    required this.hardCause,\n    this.props = const IMapConst<String, dynamic>({}),\n  });\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      super == other &&\n          other is AdvancedUserException &&\n          runtimeType == other.runtimeType &&\n          onOk == other.onOk &&\n          onCancel == other.onCancel &&\n          hardCause == other.hardCause &&\n          props == other.props;\n\n  @override\n  int get hashCode =>\n      super.hashCode ^\n      onOk.hashCode ^\n      onCancel.hashCode ^\n      hardCause.hashCode ^\n      props.hashCode;\n\n  /// Returns a new [UserException], copied from the current one, but adding the given [reason].\n  /// Note the added [reason] won't replace the original reason, but will be added to it.\n  @useResult\n  @mustBeOverridden\n  @override\n  UserException addReason(String? reason) {\n    UserException exception = super.addReason(reason);\n    return AdvancedUserException(\n      exception.message,\n      reason: exception.reason,\n      code: exception.code,\n      onOk: onOk,\n      onCancel: onCancel,\n      hardCause: hardCause,\n      props: props,\n      errorText: errorText,\n      ifOpenDialog: ifOpenDialog,\n    );\n  }\n\n  /// Returns a new [UserException], by merging the current one with the given [anotherUserException].\n  /// This simply means the given [anotherUserException] will be used as part of the [reason] of the\n  /// current one.\n  ///\n  /// Note: If any of the exceptions has [ifOpenDialog] set to `false`, the result will also\n  /// have [ifOpenDialog] set to `false`.\n  ///\n  @useResult\n  @mustBeOverridden\n  @override\n  UserException mergedWith(UserException? anotherUserException) {\n    if (anotherUserException == null)\n      return this;\n    else {\n      UserException exception = super.mergedWith(anotherUserException);\n      return AdvancedUserException(\n        exception.message,\n        reason: exception.reason,\n        code: exception.code,\n        onOk: onOk,\n        onCancel: onCancel,\n        hardCause: hardCause,\n        props: props,\n        errorText: (anotherUserException.errorText?.isNotEmpty ?? false)\n            ? anotherUserException.errorText\n            : errorText,\n        ifOpenDialog: ifOpenDialog && anotherUserException.ifOpenDialog,\n      );\n    }\n  }\n\n  @useResult\n  @mustBeOverridden\n  @override\n  UserException withDialog(bool ifOpenDialog) => AdvancedUserException(\n        message,\n        reason: reason,\n        code: code,\n        onOk: onOk,\n        onCancel: onCancel,\n        props: props,\n        hardCause: hardCause,\n        errorText: errorText,\n        ifOpenDialog: ifOpenDialog,\n      );\n\n  @useResult\n  @mustBeOverridden\n  @override\n  UserException withErrorText(String? newErrorText) => AdvancedUserException(\n        message,\n        reason: reason,\n        code: code,\n        onOk: onOk,\n        onCancel: onCancel,\n        props: props,\n        hardCause: hardCause,\n        errorText: newErrorText,\n        ifOpenDialog: ifOpenDialog,\n      );\n\n  @override\n  String toString() {\n    return super.toString() + (props.isEmpty ? '' : props.toString());\n  }\n}\n\nextension UserExceptionAdvancedExtension on UserException {\n  //\n  /// The `onOk` callback of the exception, or `null` if it was not defined.\n  VoidCallback? get onOk {\n    var exception = this;\n    return (exception is AdvancedUserException) ? exception.onOk : null;\n  }\n\n  /// The `onCancel` callback of the exception, or `null` if it was not defined.\n  VoidCallback? get onCancel {\n    var exception = this;\n    return (exception is AdvancedUserException) ? exception.onCancel : null;\n  }\n\n  /// The hard cause is some error which caused the [UserException], but that is not\n  /// a [UserException] itself. For example: `int.parse('a')` throws a `FormatException`.\n  /// Then: `throw UserException('Invalid number').addCause(FormatException('Invalid input'))`.\n  /// will have the `FormatException` as the hard cause. Note: If a [UserException] is\n  /// passed as the hard cause, it will be added with [addCause], and will not become the\n  /// hard cause. In other words, a [UserException] will never be a hard cause.\n  Object? get hardCause {\n    var exception = this;\n    return (exception is AdvancedUserException) ? exception.hardCause : null;\n  }\n\n  /// The properties added to the exception, if any.\n  /// They are an immutable-map of type [IMap], of key-value pairs.\n  /// To read the properties, use the `[]` operator, like this:\n  /// ```dart\n  /// var value = exception.props['key'];\n  /// ```\n  /// If the key does not exist, it will return `null`.\n  ///\n  IMap<String, dynamic> get props {\n    var exception = this;\n    return (exception is AdvancedUserException)\n        ? exception.props\n        : const IMapConst<String, dynamic>({});\n  }\n\n  /// Returns a [UserException] from the current one, by adding the given [cause].\n  /// Note the added [cause] won't replace the original cause, but will be added to it.\n  ///\n  /// If the added [cause] is a `null`, it will return the current exception, unchanged.\n  ///\n  /// If the added [cause] is a [String], the [addReason] method will be used to\n  /// return the new exception.\n  ///\n  /// If the added [cause] is a [UserException], the [mergedWith] method will be used to\n  /// return the new exception.\n  ///\n  /// If the added [cause] is any other type, including any other error types, it will be\n  /// set as the property [hardCause] of the exception. The hard cause is meant to be some\n  /// error which caused the [UserException], but that is not a [UserException] itself.\n  /// For example, if `int.parse('a')` throws a `FormatException`, then\n  /// `throw UserException('Invalid number').addCause(FormatException('Invalid input'))`.\n  /// will set the `FormatException` as the hard cause.\n  ///\n  @useResult\n  UserException addCause(Object? cause) {\n    //\n    if (cause == null) {\n      return this;\n    }\n    //\n    else if (cause is String) {\n      return addReason(cause);\n    }\n    //\n    else if (cause is UserException) {\n      return mergedWith(cause);\n    }\n    //\n    // Now we're going to set the hard cause.\n    else {\n      return AdvancedUserException(\n        message,\n        reason: reason,\n        code: code,\n        onOk: onOk,\n        onCancel: onCancel,\n        props: props,\n        errorText: errorText,\n        ifOpenDialog: ifOpenDialog,\n        hardCause: cause, // We discard the old hard cause, if any.\n      );\n    }\n  }\n\n  /// Adds callbacks to the [UserException].\n  ///\n  /// This method is used to add `onOk` and `onCancel` callbacks to the [UserException].\n  ///\n  /// The [onOk] callback will be called when the user taps OK in the error dialog.\n  /// The [onCancel] callback will be called when the user taps CANCEL in the error dialog.\n  ///\n  /// If the exception already had callbacks, the new callbacks will be merged with the old ones,\n  /// and the old callbacks will be called before the new ones.\n  ///\n  @useResult\n  UserException addCallbacks({\n    VoidCallback? onOk,\n    VoidCallback? onCancel,\n  }) {\n    var exception = this;\n\n    if (exception is AdvancedUserException) {\n      VoidCallback? _onOk;\n      VoidCallback? _onCancel;\n\n      if (exception.onOk == null)\n        _onOk = onOk;\n      else\n        _onOk = () {\n          exception.onOk?.call();\n          onOk?.call();\n        };\n\n      if (exception.onCancel == null)\n        _onCancel = onCancel;\n      else\n        _onCancel = () {\n          exception.onCancel?.call();\n          onCancel?.call();\n        };\n\n      return AdvancedUserException(\n        message,\n        reason: reason,\n        code: code,\n        onOk: _onOk,\n        onCancel: _onCancel,\n        props: exception.props,\n        hardCause: exception.hardCause,\n        errorText: errorText,\n        ifOpenDialog: ifOpenDialog,\n      );\n    }\n    //\n    else\n      return AdvancedUserException(\n        message,\n        reason: reason,\n        code: code,\n        errorText: errorText,\n        ifOpenDialog: ifOpenDialog,\n        onOk: onOk,\n        onCancel: onCancel,\n        props: const IMapConst<String, dynamic>({}),\n        hardCause: null,\n      );\n  }\n\n  /// Adds [moreProps] to the properties of the [UserException].\n  /// If the exception already had [props], the new [moreProps] will be merged with those.\n  ///\n  @useResult\n  UserException addProps(Map<String, dynamic>? moreProps) {\n    if (moreProps == null)\n      return this;\n    else\n      return AdvancedUserException(\n        message,\n        reason: reason,\n        code: code,\n        onOk: onOk,\n        onCancel: onCancel,\n        props: props.addMap(moreProps),\n        hardCause: hardCause,\n        errorText: errorText,\n        ifOpenDialog: ifOpenDialog,\n      );\n  }\n}\n\n/// If you want the [UserExceptionDialog] to display some [UserException],\n/// you must throw the exception from inside an action's `before` or `reduce`\n/// methods.\n///\n/// However, sometimes you need to create some callback that throws\n/// an [UserException]. If this callback is be called outside of an action,\n/// the dialog will not display the exception. To solve this, the callback\n/// should not throw an exception, but instead call the [UserExceptionAction],\n/// which will then simply throw the exception in its `reduce` method.\n///\nclass UserExceptionAction<St> extends ReduxAction<St> {\n  final UserException exception;\n\n  UserExceptionAction(\n    /// Some message shown to the user.\n    /// Example: `dispatch(UserExceptionAction('Invalid number'))`\n    String? message, {\n    //\n    /// Optionally, instead of [message] we may provide a numeric [code].\n    /// This code may have an associated message which is set in the client.\n    /// Example: `dispatch(UserExceptionAction('', code: 12))`\n    int? code,\n\n    /// Another message which is the reason of the user-exception.\n    /// Example: `dispatch(UserExceptionAction('Invalid number', reason: 'Must be less than 42'))`\n    String? reason,\n\n    /// Callback to be called after the user views the error and taps OK.\n    VoidCallback? onOk,\n\n    /// Callback to be called after the user views the error and taps CANCEL.\n    VoidCallback? onCancel,\n\n    /// Adds the given `cause` to the exception.\n    /// * If the added `cause` is a `String`, the `addReason` method will be used to\n    /// create the exception.\n    /// * If the added `cause` is a `UserException`, the `mergedWith` method will\n    /// be used to create the exception.\n    /// * If the added `cause` is any other type, including any other error types, it will be\n    /// set as the property `hardCause` of the exception. The hard cause is meant to be some\n    /// error which caused the `UserException`, but that is not a `UserException` itself.\n    /// For example: `dispatch(UserException('Invalid number', cause: FormatException('Invalid input'))`.\n    /// will set the `FormatException` as the hard cause.\n    Object? cause,\n\n    /// Any key-value pair properties you'd like to add to the exception.\n    /// For example: `props: {'name': 'John', 'age': 42}`\n    Map<String, dynamic>? props,\n\n    /// If `true`, the [UserExceptionDialog] will show in the dialog or similar UI.\n    /// If `false` you can still show the error in a different way, usually showing [errorText]\n    /// in the UI element that is responsible for the error.\n    final bool ifOpenDialog = true,\n\n    /// Some text to be displayed in the UI element that is responsible for the error.\n    /// For example, a text field could show this text in its `errorText` property.\n    /// When building your widgets, you can get the [errorText] from the failed action:\n    /// `String errorText = context.exceptionFor(MyAction)?.errorText`.\n    final String? errorText,\n    //\n  }) : this.from(\n          UserException(\n            message,\n            reason: reason,\n            code: code,\n            ifOpenDialog: ifOpenDialog,\n            errorText: errorText,\n          ).addCause(cause).addProps(props),\n        );\n\n  UserExceptionAction.from(this.exception);\n\n  @override\n  Future<St> reduce() async => throw exception;\n}\n"
  },
  {
    "path": "lib/src/cache.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:weak_map/weak_map.dart' as c;\n\n/// Cache for 1 immutable state, and no parameters.\n///\n/// The first time this function is called with some state, it will calculate the result from it,\n/// and then return the result. When this function is called again with the same state (compared\n/// with the previous one by identity) it will return the same result, without having to calculate\n/// again. When this function is called again with a different from the previous one, it will\n/// evict the cache, recalculate the result, and cache it.\n///\n/// Example:\n/// ```\n/// var selector = cache1((int limit) =>\n///    () =>\n///    stateNames.take(limit).toList());\n/// ```\nResult Function() Function(State1) cache1state<Result, State1>(\n  Result Function() Function(State1) f,\n) =>\n    c.cache1state(f);\n\n/// Cache for 1 immutable state, and 1 parameter.\n///\n/// When this function is called with some state and some parameter, it will check if it has\n/// the cached result for this state/parameter combination. If so, it will return it from the cache,\n/// without having to recalculate it again. If the result for this state/parameter combination is\n/// not yet cached, it will calculate it, cache it, and then return it. Note: The cache has one\n/// entry for each different parameter (comparing parameters by EQUALITY).\n///\n/// Cache eviction: Each time this function is called with some state, it will compare it (by\n/// IDENTITY) with the state from the previous time the function was called. If the state is\n/// different, the cache (for all parameters) will be evicted. In other words, as soon as the state\n/// changes, it will clear all cached results and start all over again.\n///\n/// Example:\n/// ```\n/// var selector = cache1_1((List<String> state) =>\n///    (String startString) =>\n///    state.where((str) => str.startsWith(startString)).toList());\n/// ```\nResult Function(Param1) Function(State1)\n    cache1state_1param<Result, State1, Param1>(\n  Result Function(Param1) Function(State1) f,\n) =>\n        c.cache1state_1param(f);\n\n/// Cache for 1 immutable state, and 2 parameters.\n///\n/// When this function is called with some state and some parameters, it will check if it has\n/// the cached result for this state/parameters combination. If so, it will return it from the\n/// cache, without having to recalculate it again. If the result for this state/parameters\n/// combination is not yet cached, it will calculate it, cache it, and then return it. Note: The\n/// cache has one entry for each different parameter combination (comparing each parameter in the\n/// combination by EQUALITY).\n///\n/// Cache eviction: Each time this function is called with some state, it will compare it (by\n/// IDENTITY) with the state from the previous time the function was called. If the state is\n/// different, the cache (for all parameters) will be evicted. In other words, as soon as the state\n/// changes, it will clear all cached results and start all over again.\n///\n/// Example:\n/// ```\n/// var selector = cache1_2((List<String> state) => (String startString, String endString) {\n///    return state\n///       .where((str) => str.startsWith(startString) && str.endsWith(endString)).toList();\n///    });\n/// ```\nResult Function(Param1, Param2) Function(State1)\n    cache1state_2params<Result, State1, Param1, Param2>(\n  Result Function(Param1, Param2) Function(State1) f,\n) =>\n        c.cache1state_2params(f);\n\n/// Cache for 2 immutable states, and no parameters.\n///\n/// The first time this function is called with some states, it will calculate the result from them,\n/// and then return the result. When this function is called again with the same states (compared\n/// with the previous ones by IDENTITY) it will return the same result, without having to calculate\n/// again. When this function is called again with any of the states (or both) different from the\n/// previous ones, it will evict the cache, recalculate the result, and cache it.\n///\n/// Example:\n/// ```\n/// var selector = cache2((List<String> names, int limit) =>\n///        () => names.where((str) => str.startsWith(\"A\")).take(limit).toList());\n/// ```\nResult Function() Function(State1, State2) cache2states<Result, State1, State2>(\n  Result Function() Function(State1, State2) f,\n) =>\n    c.cache2states(f);\n\n/// Cache for 2 immutable states, and 1 parameter.\n///\n/// When this function is called with some states and a parameter, it will check if it has\n/// the cached result for this states/parameter combination. If so, it will return it from the\n/// cache, without having to recalculate it again. If the result for this states/parameter\n/// combination is not yet cached, it will calculate it, cache it, and then return it. Note: The\n/// cache has one entry for each different parameter (comparing parameters by EQUALITY).\n///\n/// Cache eviction: Each time this function is called with some states, it will compare them (by\n/// IDENTITY) with the states from the previous time the function was called. If any of the states\n/// is different, the cache (for all parameters) will be evicted. In other words, as soon as one\n/// of the states (or both) change, it will clear all cached results and start all over again.\n///\n/// Example:\n/// ```\n/// var selector = cache2states_1param((List<String> names, int limit) => (String searchString) {\n///    return names.where((str) => str.startsWith(searchString)).take(limit).toList();\n///    });\n/// ```\nResult Function(Param1) Function(State1, State2)\n    cache2states_1param<Result, State1, State2, Param1>(\n  Result Function(Param1) Function(State1, State2) f,\n) =>\n        c.cache2states_1param(f);\n\n/// Cache for 2 immutable states, and 2 parameters.\n///\n/// When this function is called with some states and parameters, it will check if it has\n/// the cached result for this states/parameters combination. If so, it will return it from the\n/// cache, without having to recalculate it again. If the result for this states/parameters\n/// combination is not yet cached, it will calculate it, cache it, and then return it. Note: The\n/// cache has one entry for each different parameter combination (comparing the parameters in the\n/// combination by EQUALITY).\n///\n/// Cache eviction: Each time this function is called with some states, it will compare them (by\n/// IDENTITY) with the states from the previous time the function was called. If any of the states\n/// is different, the cache (for all parameters) will be evicted. In other words, as soon as one\n/// of the states (or both) change, it will clear all cached results and start all over again.\n///\n/// Example:\n/// ```\n/// var selector =\n///    cache2states_2params((List<String> names, int limit) => (String startString, String endString) {\n///       return names\n///          .where((str) => str.startsWith(startString) && str.endsWith(endString))\n///          .take(limit).toList();\n///    });\n/// ```\nResult Function(Param1, Param2) Function(State1, State2)\n    cache2states_2params<Result, State1, State2, Param1, Param2>(\n  Result Function(Param1, Param2) Function(State1, State2) f,\n) =>\n        c.cache2states_2params(f);\n\n/// Cache for 2 immutable states, and 3 parameters.\n///\n/// When this function is called with some states and parameters, it will check if it has\n/// the cached result for this states/parameters combination. If so, it will return it from the\n/// cache, without having to recalculate it again. If the result for this states/parameters\n/// combination is not yet cached, it will calculate it, cache it, and then return it. Note: The\n/// cache has one entry for each different parameter combination (comparing the parameters in the\n/// combination by EQUALITY).\n///\n/// Cache eviction: Each time this function is called with some states, it will compare them (by\n/// IDENTITY) with the states from the previous time the function was called. If any of the states\n/// is different, the cache (for all parameters) will be evicted. In other words, as soon as one\n/// of the states (or both) change, it will clear all cached results and start all over again.\n///\n/// Example:\n/// ```\n/// var selector =\n///    cache2states_3params((List<String> names, int limit) => (String startString, String endString) {\n///       return names\n///          .where((str) => str.startsWith(startString) && str.endsWith(endString))\n///          .take(limit).toList();\n///    });\n/// ```\nResult Function(Param1, Param2, Param3) Function(State1, State2)\n    cache2states_3params<Result, State1, State2, Param1, Param2, Param3>(\n  Result Function(Param1, Param2, Param3) Function(State1, State2) f,\n) =>\n        c.cache2states_3params(f);\n\n/// Cache for 3 immutable states, and no parameters.\n/// Example:\n///\n/// The first time this function is called with some states, it will calculate the result from them,\n/// and then return the result. When this function is called again with the same states (compared\n/// with the previous ones by IDENTITY) it will return the same result, without having to calculate\n/// again. When this function is called again with any of the states (or both) different from the\n/// previous ones, it will evict the cache, recalculate the result, and cache it.\n///\n/// ```\n/// var selector = cache3states((List<String> names, int limit, String prefix) =>\n///        () => names.where((str) => str.startsWith(prefix)).take(limit).toList());\n/// ```\nResult Function() Function(State1, State2, State3)\n    cache3states<Result, State1, State2, State3>(\n  Result Function() Function(State1, State2, State3) f,\n) =>\n        c.cache3states(f);\n\n/// Cache for 1 immutable state, no parameters, and some extra information. This is the same\n/// as [cache1state] but with an extra information. Note: The extra information is not used in\n/// any way to decide whether the cache should be used/recalculated/evicted. It's just passed down\n/// to the [f] function to be used during the result calculation.\nResult Function() Function(State1, Extra)\n    cache1state_0params_x<Result, State1, Extra>(\n  Result Function() Function(State1, Extra) f,\n) =>\n        c.cache1state_0params_x(f);\n\n/// Cache for 2 immutable states, no parameters, and some extra information. This is the same\n/// as [cache1state] but with an extra information. Note: The extra information is not used in\n/// any way to decide whether the cache should be used/recalculated/evicted. It's just passed down\n/// to the [f] function to be used during the result calculation.\nResult Function() Function(State1, State2, Extra)\n    cache2states_0params_x<Result, State1, State2, Extra>(\n  Result Function() Function(State1, State2, Extra) f,\n) =>\n        c.cache2states_0params_x(f);\n\n/// Cache for 3 immutable states, no parameters, and some extra information.This is the same\n/// as [cache1state] but with an extra information. Note: The extra information is not used in\n/// any way to decide whether the cache should be used/recalculated/evicted. It's just passed down\n/// to the [f] function to be used during the result calculation.\nResult Function() Function(State1, State2, State3, Extra)\n    cache3states_0params_x<Result, State1, State2, State3, Extra>(\n  Result Function() Function(State1, State2, State3, Extra) f,\n) =>\n        c.cache3states_0params_x(f);\n"
  },
  {
    "path": "lib/src/cloud_sync.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\n\nabstract class CloudSync<St> extends Persistor<St> {}\n"
  },
  {
    "path": "lib/src/connection_exception.dart",
    "content": "import 'package:async_redux/async_redux.dart';\n\n/// The [ConnectionException] is a type of [UserException] that warns the user when the connection\n/// is not working. Use [ConnectionException.noConnectivity] for a simple version that warns the\n/// users they should check the connection. Use factory [create] to give more complete messages,\n/// indicating the host that is having problems.\n///\nclass ConnectionException extends AdvancedUserException {\n  //\n  // Usage: `throw ConnectionException.noConnectivity`;\n  static const noConnectivity = ConnectionException();\n\n  /// Usage: `throw ConnectionException.noConnectivityWithRetry(() {...})`;\n  ///\n  /// A dialog will open. When the user presses OK or dismisses the dialog in any way,\n  /// the [onRetry] callback will be called.\n  ///\n  static ConnectionException noConnectivityWithRetry(\n          void Function()? onRetry) =>\n      ConnectionException(onRetry: onRetry);\n\n  /// Creates a [ConnectionException].\n  ///\n  /// If you pass it an [onRetry] callback, it will call it when the user presses\n  /// the \"Ok\" button in the dialog. Otherwise, it will just close the dialog.\n  ///\n  /// If you pass it a [host], it will say \"It was not possible to connect to $host\".\n  /// Otherwise, it will simply say \"There is no Internet connection\".\n  ///\n  const ConnectionException({\n    void Function()? onRetry,\n    this.host,\n    String? errorText,\n    bool ifOpenDialog = true,\n  }) : super(\n          (host == null || host == 'null')\n              ? 'There is no Internet'\n              : 'It was not possible to connect to $host.',\n          reason: 'Please, verify your connection.',\n          code: null,\n          onOk: onRetry,\n          onCancel: null,\n          hardCause: null,\n          errorText: errorText ?? 'No Internet connection',\n          ifOpenDialog: ifOpenDialog,\n        );\n\n  final String? host;\n\n  @override\n  UserException addReason(String? reason) {\n    throw UnsupportedError('You cannot use this.');\n  }\n\n  @override\n  UserException mergedWith(UserException? anotherUserException) {\n    throw UnsupportedError('You cannot use this.');\n  }\n\n  @override\n  UserException withErrorText(String? newErrorText) => ConnectionException(\n        host: host,\n        onRetry: onOk,\n        errorText: newErrorText,\n        ifOpenDialog: ifOpenDialog,\n      );\n\n  @override\n  UserException withDialog(bool ifOpenDialog) => ConnectionException(\n        host: host,\n        onRetry: onOk,\n        errorText: errorText,\n        ifOpenDialog: ifOpenDialog,\n      );\n\n  @override\n  UserException get noDialog => ConnectionException(\n        host: host,\n        onRetry: onOk,\n        errorText: errorText,\n        ifOpenDialog: ifOpenDialog,\n      );\n}\n"
  },
  {
    "path": "lib/src/connector_tester.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:flutter/material.dart' hide Action;\n\nimport '../async_redux.dart';\n\n/// Helps testing the `StoreConnector`s methods, such as `onInit`,\n/// `onDispose` and `onWillChange`.\n///\n/// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n///\n/// Example: Suppose you have a `StoreConnector` which dispatches `SomeAction`\n/// on its `onInit`. How could you test that?\n///\n/// ```\n/// class MyConnector extends StatelessWidget {\n///   Widget build(BuildContext context) => StoreConnector<AppState, Vm>(\n///         vm: () => _Factory(),\n///         onInit: _onInit,\n///         builder: (context, vm) { ... }\n///   }\n///\n///   void _onInit(Store<AppState> store) => store.dispatch(SomeAction());\n/// }\n///\n/// var storeTester = StoreTester(...);\n/// ConnectorTester(tester, MyConnector()).runOnInit();\n/// var info = await tester.waitUntil(SomeAction);\n/// ```\n///\nclass ConnectorTester<St, Model> {\n  final Store<St> store;\n  final StatelessWidget widgetConnector;\n\n  StoreConnector<St, Model>? _storeConnector;\n\n  StoreConnector<St, Model> get storeConnector => _storeConnector ??=\n      // ignore: invalid_use_of_protected_member\n      widgetConnector.build(StatelessElement(widgetConnector))\n          as StoreConnector<St, Model>;\n\n  ConnectorTester(this.store, this.widgetConnector);\n\n  void runOnInit() {\n    final OnInitCallback<St>? onInit = storeConnector.onInit;\n    if (onInit != null) onInit(store);\n  }\n\n  void runOnDispose() {\n    final OnDisposeCallback<St>? onDispose = storeConnector.onDispose;\n    if (onDispose != null) onDispose(store);\n  }\n\n  void runOnWillChange(\n    Model previousVm,\n    Model newVm,\n  ) {\n    final OnWillChangeCallback<St, Model>? onWillChange =\n        storeConnector.onWillChange;\n    if (onWillChange != null)\n      onWillChange(StatelessElement(widgetConnector), store, previousVm, newVm);\n  }\n}\n"
  },
  {
    "path": "lib/src/error_observer.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\n\n/// This is DEPRECATED. Use [GlobalErrorObserver] instead.\n///\n/// The [observe] method of the [ErrorObserver] will be given all errors.\n/// It's called after the action's [ReduxAction.wrapError] and the [GlobalWrapError]\n/// have both been called.\n///\n/// The [observe] method should return `true` to throw the error, and `false` to swallow it.\n///\n/// Note: The [ErrorObserver] will be given all errors, including those of type [UserException]\n/// and [AbortDispatchException]. To maintain the default behavior, you should return `false`\n/// (swallow) for both these error types.\n///\n/// Important: Don't use the `store` you get in the [observe] method to dispatch any actions,\n/// as this may have unpredictable results. Also, make sure your errorObserver never throws an\n/// error.\n@Deprecated('Use GlobalErrorObserver instead. This will be removed.')\nabstract class ErrorObserver<St> {\n  //\n\n  /// The [observe] method of the [ErrorObserver] will be given all errors.\n  /// It's called after the action's [ReduxAction.wrapError] and the [GlobalWrapError]\n  /// have both been called.\n  ///\n  /// The [observe] method should return `true` to throw the error, and `false` to swallow it.\n  ///\n  /// Note: The [ErrorObserver] will be given all errors, including those of type [UserException]\n  /// and [AbortDispatchException]. To maintain the default behavior, you should return `false`\n  /// (swallow) for both these error types.\n  ///\n  /// Important: Don't use the `store` you get in the [observe] method to dispatch any actions,\n  /// as this may have unpredictable results. Also, make sure your errorObserver never throws an\n  /// error.\n  bool observe(\n    Object error,\n    StackTrace stackTrace,\n    ReduxAction<St> action,\n    Store<St> store,\n  );\n}\n\n/// During development you may use this error observer if you want all errors to be\n/// shown to the user in a dialog, not only [UserException]s. In more detail:\n/// This will wrap all errors into [UserException]s, and put them all into the\n/// error queue. Note that errors which are NOT originally [UserException]s will\n/// still be thrown, while [UserException]s will still be swallowed.\n///\n/// Passe it to the store like this:\n///\n/// `var store = Store(errorObserver:DevelopmentErrorObserver());`\n@Deprecated('Use GlobalErrorObserverForDevelopment instead. This will be removed.')\nclass DevelopmentErrorObserver<St> implements ErrorObserver<St> {\n  @override\n  bool observe(\n    Object error,\n    StackTrace stackTrace,\n    ReduxAction<St> action,\n    Store store,\n  ) {\n    if (error is UserException)\n      return false;\n    else {\n      // We have to dispatch another action, since we cannot do:\n      // store._addError(errorAsUserException);\n      // store._changeController.add(store.state);\n      Future.microtask(() => store.dispatch(\n            UserExceptionAction(error.toString(), cause: error),\n          ));\n      return true;\n    }\n  }\n}\n\n/// Swallows all errors (not recommended). Passe it to the store like this:\n/// `var store = Store(errorObserver: SwallowErrorObserver());`\n///\n@Deprecated('Use SwallowGlobalErrorObserver instead. This will be removed.')\nclass SwallowErrorObserver<St> implements ErrorObserver<St> {\n  @override\n  bool observe(\n    Object error,\n    StackTrace stackTrace,\n    ReduxAction<St> action,\n    Store store,\n  ) {\n    return false;\n  }\n}\n"
  },
  {
    "path": "lib/src/event_redux.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'dart:math';\n\nimport 'package:flutter/foundation.dart';\n\n/// When the [Event] class was created, Flutter did not have any class named\n/// `Event`. Now there is. For this reason, this typedef allows you to use Evt\n/// instead. You can hide one of them, by importing AsyncRedux like this:\n/// import 'package:async_redux/async_redux.dart' hide Event;\n/// or\n/// import 'package:async_redux/async_redux.dart' hide Evt;\ntypedef Evt<T> = Event<T>;\n\n/// Events are one-time notifications stored in the Redux state, used to trigger\n/// side effects in widgets such as showing dialogs, clearing text fields, or\n/// navigating to new screens.\n///\n/// Unlike regular state values, events are automatically \"consumed\" (marked as\n/// spent) after being read, ensuring they only trigger once.\n///\n/// ## Main Usage: The `event` Extension\n///\n/// The recommended way to use events is with the `context.event()` extension\n/// method. First, define an extension in your code:\n///\n/// ```dart\n/// extension BuildContextExtension on BuildContext {\n///   R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);\n/// }\n/// ```\n///\n/// **Example with a boolean (value-less) event:**\n///\n/// ```dart\n/// // In your state\n/// class AppState {\n///   final Event clearTextEvt;\n///   AppState({required this.clearTextEvt});\n/// }\n///\n/// // In your action\n/// class ClearTextAction extends ReduxAction<AppState> {\n///   @override\n///   AppState reduce() => state.copy(clearTextEvt: Event());\n/// }\n///\n/// // In your widget\n/// Widget build(BuildContext context) {\n///   var clearText = context.event((state) => state.clearTextEvt);\n///   if (clearText) controller.clear();\n///   ...\n/// }\n/// ```\n///\n/// **Example with a typed event:**\n///\n/// ```dart\n/// // In your state\n/// class AppState {\n///   final Event<String> changeTextEvt;\n///   AppState({required this.changeTextEvt});\n/// }\n///\n/// // In your action\n/// class ChangeTextAction extends ReduxAction<AppState> {\n///   @override\n///   Future<AppState> reduce() async {\n///     String newText = await fetchTextFromApi();\n///     return state.copy(changeTextEvt: Event<String>(newText));\n///   }\n/// }\n///\n/// // In your widget\n/// Widget build(BuildContext context) {\n///   var newText = context.event((state) => state.changeTextEvt);\n///   if (newText != null) controller.text = newText;\n///   ...\n/// }\n/// ```\n///\n/// ## Return Values\n///\n/// - For events with **no generic type** (`Event`): `Event.consume()`\n///   returns **true** if the event was dispatched, or **false** if it was\n///   already spent.\n///\n/// - For events with **a value type** (`Event<T>`): `Event.consume()` returns\n///   the **value** if the event was dispatched, or **null** if it was already\n///   spent.\n///\n/// ## Alternative Usage: StoreConnector\n///\n/// Events can also be consumed when creating a `ViewModel` with the `StoreConnector`.\n/// The event is \"consumed\" only once in the converter function, and is then\n/// automatically considered \"spent\".\n///\n/// ## Important Notes\n///\n/// - Events are consumed only once. After consumption, they are marked as \"spent\".\n/// - Each event can be consumed by **one single widget**.\n/// - Always initialize events as spent: `Event.spent()` or `Event<T>.spent()`.\n/// - The widget will rebuild when a new event is dispatched, even if it has the\n///   same internal value as a previous event, because each event instance is\n///   unique.\n///\n/// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n///\n/// Note: For `Event<bool>()` with no value provided, the value defaults to\n/// `true` (not `null`), so that `consume()` returns `true` as expected.\n///\nclass Event<T> {\n  bool _spent;\n  final T? _evtInfo;\n\n  Event([T? evtInfo])\n      : _evtInfo = (T == bool && evtInfo == null) ? (true as T) : evtInfo,\n        _spent = false;\n\n  Event.spent()\n      : _evtInfo = null,\n        _spent = true;\n\n  bool get isSpent => _spent;\n\n  bool get isNotSpent => !isSpent;\n\n  /// Returns the event state and consumes the event.\n  ///\n  /// After consumption, the event is marked as spent and will not trigger again.\n  ///\n  /// - For events with no generic type (`Event`): Returns **true** if the event\n  ///   was dispatched, or **false** if it was already spent.\n  ///\n  /// - For events with a value type (`Event<T>`): Returns the **value** if the\n  ///   event was dispatched, or **null** if it was already spent.\n  ///\n  /// This method is called internally by `context.getEvent()`\n  /// (or `context.event()`) so you usually will not use it directly.\n  /// However, when using the dumb/smart widget pattern, you may use it inside\n  /// a dumb widget (for example, when using a `StoreConnector`) when the event\n  /// was passed as a constructor parameter by a smart widget.\n  T? consume() {\n    T? saveState = state;\n    _spent = true;\n    return saveState;\n  }\n\n  /// Returns the event state without consuming it.\n  ///\n  /// Unlike [consume], this method does not mark the event as spent, so the\n  /// event can be read multiple times.\n  ///\n  /// This is useful in rare cases where you need to check the event value\n  /// without consuming it, but most use cases should use [consume] via\n  /// `context.getEvent()` (or `context.event()`).\n  T? get state {\n    if (T == dynamic && _evtInfo == null) {\n      if (_spent)\n        return (false as T);\n      else {\n        return (true as T);\n      }\n    } else {\n      if (_spent)\n        return null;\n      else {\n        return _evtInfo;\n      }\n    }\n  }\n\n  @override\n  String toString() => 'Event('\n      '${state.toString()}'\n      '${_spent == true ? ', spent' : ''}'\n      ')';\n\n  /// Creates an event which is transformed by a function that usually needs\n  /// the store state.\n  ///\n  /// You must provide the event and a map-function. The map-function must be\n  /// able to deal with the spent state (null or false, accordingly).\n  ///\n  /// This is useful when you need to derive a new event value from an existing\n  /// event, typically by looking up additional data from the state.\n  ///\n  /// **Example:** If `state.indexEvt = Event<int>(5)` and you need to get a\n  /// user from it:\n  ///\n  /// ```dart\n  /// var mapFunction = (int? index) => index == null ? null : state.users[index];\n  /// Event<User> userEvt = Event.map(state.indexEvt, mapFunction);\n  /// ```\n  static Event<T> map<T, V>(Event<V> evt, T? Function(V?) mapFunction) =>\n      MappedEvent<V, T>(evt, mapFunction);\n\n  /// Creates an event which consumes from more than one event.\n  ///\n  /// If the first event is not spent, it will be consumed, and the second\n  /// will not. If the first event is spent, the second one will be consumed.\n  ///\n  /// This is useful when you have multiple sources for the same event and want\n  /// to consume from whichever one is available.\n  ///\n  /// **Note:** If both events are NOT spent, the method will have to be called\n  /// twice to consume both. If both are spent, returns `null`.\n  ///\n  /// **Example:**\n  /// ```dart\n  /// Event<String> combinedEvt = Event.from(localMessageEvt, remoteMessageEvt);\n  /// ```\n  factory Event.from(Event<T> evt1, Event<T> evt2) => EventMultiple(evt1, evt2);\n\n  /// Consumes from more than one event, prioritizing the first event.\n  ///\n  /// If the first event is not spent, it will be consumed, and the second will\n  /// not. If the first event is spent, the second one will be consumed.\n  ///\n  /// This is useful when you have multiple sources for the same event and want\n  /// to consume from whichever one is available.\n  ///\n  /// **Note:** If both events are NOT spent, the method will have to be called\n  /// twice to consume both. If both are spent, returns null.\n  ///\n  /// **Example:**\n  /// ```dart\n  /// String? message = Event.consumeFrom(localMessageEvt, remoteMessageEvt);\n  /// ```\n  static T? consumeFrom<T>(Event<T> evt1, Event<T> evt2) {\n    T? evt = evt1.consume();\n    evt ??= evt2.consume();\n    return evt;\n  }\n\n  /// Special equality implementation for events to ensure correct rebuild\n  /// behavior.\n  ///\n  /// Events use a custom equality check where:\n  /// - **Unspent events** are never considered equal to any other event,\n  ///   ensuring widgets always rebuild when a new event is dispatched.\n  /// - **Spent events** are all considered equal to each other, since they are\n  ///   \"empty\" and should not trigger rebuilds.\n  ///\n  /// This behavior is essential for both the `context.event()` extension and\n  /// `StoreConnector` usage patterns.\n  ///\n  /// ## For StoreConnector Users\n  ///\n  /// When using a [StoreConnector], you must implement equals and hashcode for\n  /// your `ViewModel`. Events included in the ViewModel must follow these rules:\n  ///\n  /// 1) If the **new** ViewModel has an event which is **not spent**, then the\n  /// ViewModel **MUST** be considered distinct, no matter the state of the\n  /// **old** ViewModel, since the new event should fire.\n  ///\n  /// 2) If both the old and new ViewModels have events which **are spent**,\n  /// then these events **MUST NOT** be considered distinct, since spent events\n  /// are considered \"empty\" and should never fire.\n  ///\n  /// 3) If the **new** ViewModel has an event which is **not spent**, and\n  /// the **old** ViewModel has an event which **is spent**, then the new event\n  /// should fire, and for that reason they **MUST** be considered distinct.\n  ///\n  /// 4) If the **new** ViewModel has an event which is **is spent**, and\n  /// the **old** ViewModel has an event which **not spent**, then the new event\n  /// should NOT fire, and for that reason they **SHOULD NOT** be considered\n  /// distinct.\n  ///\n  /// **Note:** To differentiate cases 3 and 4 we would actually be breaking\n  /// the equals contract (which says A==B should be the same as B==A). A safer\n  /// alternative is to always consider events different if any of them is not\n  /// spent. That will, however, fire some unnecessary rebuilds.\n  @override\n  bool operator ==(Object other) {\n    return identical(this, other) ||\n        other is Event &&\n            runtimeType == other.runtimeType\n\n            /// 1) Events not spent are never considered equal to any other,\n            /// and they will always \"fire\", forcing the widget to rebuild.\n            /// 2) Spent events are considered \"empty\", so they are all equal.\n            &&\n            (isSpent && other.isSpent);\n  }\n\n  /// 1) If two objects are equal according to the equals method, then hashcode\n  /// of both must be the same. Since spent events are all equal, they should\n  /// produce the same hashcode.\n  /// 2) If two objects are NOT equal, hashcode may be the same or not, but it's\n  /// better when they are not the same. However, events are mutable, and this\n  /// could mean the hashcode of the state could be changed when an event is\n  /// consumed. To avoid this, we make events always return the same hashCode.\n  @override\n  int get hashCode => 0;\n}\n\n/// An event that combines multiple sub-events, consuming them in priority order.\n///\n/// When consuming this event:\n/// - If the first sub-event is not spent, it will be consumed, and the second\n///   will not.\n/// - If the first sub-event is spent, the second one will be consumed.\n///\n/// This is useful when you have multiple sources for the same event and want\n/// to consume from whichever one is available.\n///\n/// **Note:** If both sub-events are NOT spent, the multiple-event will have to\n/// be consumed twice to consume both sub-events. If both sub-events are spent,\n/// returns null when consumed.\n///\n/// **Example:**\n/// ```dart\n/// Event<String> combinedEvt = EventMultiple(localMessageEvt, remoteMessageEvt);\n/// ```\nclass EventMultiple<T> extends Event<T> {\n  Event<T> evt1;\n  Event<T> evt2;\n\n  EventMultiple(Event? evt1, Event? evt2)\n      : evt1 = evt1 as Event<T>? ?? Event<T>.spent(),\n        evt2 = evt2 as Event<T>? ?? Event<T>.spent();\n\n  // Is spent only if both are spent.\n  @override\n  bool get isSpent => evt1.isSpent && evt2.isSpent;\n\n  /// Returns the event state and consumes the event.\n  ///\n  /// Consumes the first non-spent event. If the first event is not spent, it\n  /// will be consumed and returned. Otherwise, the second event will be\n  /// consumed and returned.\n  @override\n  T? consume() {\n    return Event.consumeFrom(evt1, evt2);\n  }\n\n  /// Returns the event state without consuming it.\n  ///\n  /// Returns the state of the first non-spent event without consuming either event.\n  @override\n  T? get state {\n    T? st = evt1.state;\n    st ??= evt2.state;\n    return st;\n  }\n}\n\n/// An event whose value is transformed by a mapping function.\n///\n/// This is useful when your event value must be transformed by a function that\n/// usually needs the store state. You must provide the event and a map-function.\n/// The map-function must be able to deal with the spent state (null or false,\n/// accordingly).\n///\n/// This is commonly used when you need to derive a new event value from an\n/// existing event, typically by looking up additional data from the state.\n///\n/// **Example:** If `state.indexEvt = Event<int>(5)` and you need to get a user\n/// from it:\n///\n/// ```dart\n/// var mapFunction = (int? index) => index == null ? null : state.users[index];\n/// Event<User> userEvt = MappedEvent<int, User>(state.indexEvt, mapFunction);\n/// ```\nclass MappedEvent<V, T> extends Event<T> {\n  Event<V> evt;\n  T? Function(V?) mapFunction;\n\n  MappedEvent(Event<V>? evt, this.mapFunction) : evt = evt ?? Event<V>.spent();\n\n  @override\n  bool get isSpent => evt.isSpent;\n\n  /// Returns the transformed event state and consumes the underlying event.\n  @override\n  T? consume() => mapFunction(evt.consume());\n\n  /// Returns the transformed event state without consuming it.\n  @override\n  T? get state => mapFunction(evt.state);\n}\n\n/// An event-like class that generates a \"pulse\" to trigger widget updates,\n/// but is NEVER CONSUMED.\n///\n/// Unlike [Event] which is consumed after being read, [EvtState] can be used\n/// with multiple widgets and will trigger rebuilds each time a new instance is\n/// created.\n///\n/// Each [EvtState] instance is unique, even if created with the same value:\n///\n/// ```dart\n/// print(EvtState() == EvtState()); // false\n/// print(EvtState<String>('abc') == EvtState<String>('abc')); // false\n/// ```\n///\n/// **Usage with stateful widgets:**\n///\n/// When a new [EvtState] is created in the state, it will trigger a widget\n/// rebuild. Then, the `didUpdateWidget` method will be called. Since `evt`\n/// is now different from `oldWidget.evt`, it will run your side effect:\n///\n/// ```dart\n/// @override\n/// void didUpdateWidget(MyWidget oldWidget) {\n///   super.didUpdateWidget(oldWidget);\n///\n///   if (evt != oldWidget.evt) doSomethingWith(evt.value);\n/// }\n/// ```\n///\n/// **Key difference from Event:**\n///\n/// The [EvtState] class is never \"consumed\" (like the [Event] class is), which\n/// means you can use it with more than one widget. Use [EvtState] when you need\n/// multiple widgets to react to the same trigger. Use [Event] when you need\n/// one-time consumption by a single widget.\n///\n/// Note: For `Evt<bool>()` with no value provided, the value defaults to\n/// `true` (not `null`), so that `consume()` returns `true` as expected.\n///\n@immutable\nclass EvtState<T> {\n  static final _random = Random.secure();\n\n  final T? value;\n  final int _rand;\n\n  EvtState([this.value]) : _rand = _random.nextInt(1 << 32);\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is EvtState &&\n          runtimeType == other.runtimeType &&\n          value == other.value &&\n          _rand == other._rand;\n\n  @override\n  int get hashCode => value.hashCode ^ _rand.hashCode;\n}\n"
  },
  {
    "path": "lib/src/global_wrap_error.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\n\n/// This is DEPRECATED. Use [GlobalErrorObserver] instead.\n///\n/// This will be given all errors thrown in your actions (including those of type\n/// `UserException`). Then:\n/// * If it returns the same [error] unaltered, this original error will be used.\n/// * If it returns something else, that it will be used instead of the original [error].\n/// * If it returns `null`, the original error will be disabled (swallowed).\n///\n/// IMPORTANT: If instead of RETURNING an error you THROW an error inside the `wrap` function,\n/// AsyncRedux will catch this error and use it instead the original error. In other\n/// words, returning an error or throwing an error has the same effect. However, it is still\n/// recommended to return the error rather than throwing it.\n///\n/// Note this wrapper is called AFTER the action's [ReduxAction.wrapError],\n/// and BEFORE the [ErrorObserver].\n///\n/// A common use case for this is to have a global place to convert some\n/// exceptions into [UserException]s. For example, Firebase may throw some\n/// `PlatformException`s in response to a bad connection to the server.\n/// In this case, you may want to show the user a dialog explaining that the\n/// connection is bad, which you can do by converting it to a [UserException].\n/// Note, this could also be done in the [ReduxAction.wrapError], but then\n/// you'd have to add it to all actions that use Firebase.\n///\n/// Another use case is when you want to throw the [AdvancedUserException.hardCause]\n/// which is not itself an [UserException], and you still want to show\n/// the original [UserException] in a dialog to the user:\n/// ```\n/// Object wrap(Object error, [StackTrace stackTrace, ReduxAction<St> action]) {\n///   if (error is UserException) {\n///     var hardCause = error.hardCause();\n///     if (hardCause != null) {\n///       Future.microtask(() =>\n///         Business.store.dispatch(UserExceptionAction.from(error.withoutHardCause())));\n///       return hardCause;\n///     }}\n///   return null; }\n/// ```\n///\n/// You should not use [GlobalWrapError] to log errors, as the preferred place for\n/// doing that is in the [ErrorObserver].\n@Deprecated('Use GlobalErrorObserver instead. Check the documentation for more details.')\nabstract class GlobalWrapError<St> {\n  Object? wrap(\n    Object error,\n    StackTrace stackTrace,\n    ReduxAction<St> action,\n  );\n}\n\n/// A dummy global wrap error that does nothing.\n@Deprecated('Use GlobalErrorObserver instead. This will be removed.')\nclass GlobalWrapErrorDummy<St> implements GlobalWrapError<St> {\n  @override\n  Object? wrap(error, stackTrace, action) => error;\n}\n"
  },
  {
    "path": "lib/src/local_json_persist.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:async_redux/src/persistor.dart';\nimport 'package:file/file.dart' as f;\nimport 'package:file/local.dart';\nimport 'package:path/path.dart' as p;\n\nimport 'local_persist.dart';\n\n/// Save a simple-object in a file, in UTF-8 Json format.\n///\n/// Use [save] to save as Json:\n///\n/// ```dart\n/// var persist = LocalJsonPersist(\"xyz\");\n/// var simpleObj = \"Hello\";\n/// await persist.saveJson(simpleObj);\n/// ```\n///\n/// Use [load] to load from Json:\n///\n/// ```dart\n/// var persist = LocalJsonPersist(\"xyz\");\n/// Object? decoded = await persist.loadJson();\n/// ```\n///\n/// Examples of valid JSON includes:\n/// 42\n/// 42.5\n/// \"abc\"\n/// [1, 2, 3]\n/// [\"42\", 123]\n/// {\"42\": 123}\n///\n///\n/// Examples of invalid JSON includes:\n/// [1, 2, 3][4, 5, 6] // Not valid because Json does not allow two separate objects.\n/// 1, 2, 3 // Not valid because Json does not allow comma separated objects.\n/// 'abc' // Not valid because string must use double quotes.\n/// {42: \"123\"} // Not valid because a map key must be of type string.\n///\nclass LocalJsonPersist {\n  //\n  /// The default is saving/loading to/from \"appDocsDir/db/\".\n  /// This is not final, so you can change it.\n  /// Make it an empty string to remove it.\n  static String defaultDbSubDir = \"db\";\n\n  /// If running from Flutter, the default base directory is the application's documents dir.\n  /// If running from tests (detected by the `LocalFileSystem` not being present),\n  /// it will use the system's temp directory.\n  ///\n  /// You can change this variable to globally change the directory:\n  /// ```\n  /// // Will use the application's cache directory.\n  /// LocalPersist.useBaseDirectory = LocalPersist.useAppCacheDir;\n  ///\n  /// // Will use the application's downloads directory.\n  /// LocalPersist.useBaseDirectory = LocalPersist.useAppDownloadsDir;\n  ///\n  /// // Will use whatever Directory is given.\n  /// LocalPersist.useBaseDirectory = () => LocalPersist.useCustomBaseDirectory(baseDirectory: myDir);\n  /// ```\n  static Future<void> Function() useBaseDirectory = useAppDocumentsDir;\n\n  /// The default is adding a \".json\" termination to the file name.\n  static const String jsonTermination = \".json\";\n\n  static Directory? get appDocDir => _baseDirectory;\n\n  static Directory? get _baseDirectory => LocalPersist.appDocDir;\n\n  static f.FileSystem get _fileSystem => LocalPersist.getFileSystem();\n\n  final String? dbName, dbSubDir;\n\n  final List<String>? subDirs;\n\n  final f.FileSystem _fileSystemRef;\n\n  File? _file;\n\n  /// Saves to `appDocsDir/db/${dbName}.json`\n  ///\n  /// If [dbName] is a String, it will be used as such.\n  /// If [dbName] is an enum, it will use only the enum value itself.\n  /// For example if `files` is an enum, then `LocalJsonPersist(files.abc)`\n  /// is the same as `LocalJsonPersist(\"abc\")`\n  /// If [dbName] is another object type, its [toString] will be called,\n  /// and then the text after the last dot will be used.\n  ///\n  /// The default database directory [defaultDbSubDir] is `db`.\n  /// You can change this variable to globally change the directory,\n  /// or provide [dbSubDir] in the constructor.\n  ///\n  /// You can also provide other [subDirs] as Strings or enums.\n  /// Example: `LocalJsonPersist(\"photos\", subDirs: [\"article\", \"images\"])`\n  /// saves to `appDocsDir/db/article/images/photos.db`\n  ///\n  /// Important:\n  /// — In tests, instead of using `appDocsDir` it will save to\n  /// the system temp dir.\n  /// — If you mock the file-system (see method `setFileSystem()`)\n  /// it will save to `fileSystem.systemTempDirectory`.\n  ///\n  LocalJsonPersist(Object dbName, {this.dbSubDir, List<Object>? subDirs})\n      : dbName = _getStringFromEnum(dbName),\n        subDirs = subDirs?.map((s) => _getStringFromEnum(s)).toList(),\n        _file = null,\n        _fileSystemRef = _fileSystem;\n\n  /// Saves to the given file.\n  LocalJsonPersist.from(File file)\n      : dbName = null,\n        dbSubDir = null,\n        subDirs = null,\n        _file = file,\n        _fileSystemRef = _fileSystem;\n\n  /// If running from Flutter, this will get the application's documents directory.\n  /// If running from tests, it will use the system's temp directory.\n  static Future<void> useAppDocumentsDir() => LocalPersist.useAppDocumentsDir();\n\n  /// If running from Flutter, this will get the application's cache directory.\n  /// If running from tests, it will use the system's temp directory.\n  static Future<void> useAppCacheDir() => LocalPersist.useAppCacheDir();\n\n  /// If running from Flutter, this will get the application's downloads directory.\n  /// If running from tests, it will use the system's temp directory.\n  static Future<void> useAppDownloadsDir() => LocalPersist.useAppDownloadsDir();\n\n  /// If running from Flutter, the base directory will be the given [baseDirectory].\n  /// If running from tests, it will use the optional [testDirectory], or if this is not provided,\n  /// it will use the system's temp directory.\n  static Future<void> useCustomBaseDirectory({\n    required Directory baseDirectory,\n    Directory? testDirectory,\n  }) =>\n      LocalPersist.useCustomBaseDirectory(\n          baseDirectory: baseDirectory, testDirectory: testDirectory);\n\n  /// Saves the given simple object as JSON.\n  /// If the file exists, it will be overwritten.\n  Future<File> save(Object? simpleObj) async {\n    _checkIfFileSystemIsTheSame();\n\n    Uint8List encoded = encodeJson(simpleObj);\n\n    File file = _file ?? await this.file();\n    await file.create(recursive: true);\n\n    return file.writeAsBytes(\n      encoded,\n      flush: true,\n      mode: FileMode.writeOnly,\n    );\n  }\n\n  /// Loads a simple-object from a JSON file. If the file doesn't exist, returns null.\n  /// A JSON can be a String, a number, null, true, false, '{' (a map) or ']' (a list).\n  /// Note: The file must contain a single JSON, and it can't be empty. It can, however\n  /// simple contain 'null' (without the quotes) which will return null.\n  Future<Object?> load() async {\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n\n    if (!file.existsSync())\n      return null;\n    else {\n      Uint8List encoded;\n      try {\n        encoded = await file.readAsBytes();\n      } catch (error) {\n        if ((error is FileSystemException) && //\n            error.message.contains(\"No such file or directory\")) return null;\n        rethrow;\n      }\n\n      Object? simpleObjs = decodeJson(encoded);\n      return simpleObjs;\n    }\n  }\n\n  /// This method can be used if you were using a Json sequence file with a \".db\" termination,\n  /// and wants to convert it to a regular Json file. This only works if your original \".db\"\n  /// file has a single object.\n  ///\n  /// 1) It first loads a Json file called \"[dbName].json\".\n  /// - If the file exists and is NOT empty, return its content as a single simple object.\n  /// - If the file exists and is empty, returns null.\n  /// - If the file doesn't exist, goes to step 2.\n  ///\n  /// 2) Next, tries loading a Json-SEQUENCE file called \"[dbName].db\".\n  /// - If the file doesn't exist, returns null.\n  /// - If the file exists and is empty, saves it as an empty Json file called \"[dbName].json\"\n  /// - If the file exists with a single object, saves it as a Json file called \"[dbName].json\"\n  /// - If the file exists and has 2 or more objects:\n  ///   * If [isList] is false, throws an exception.\n  ///   * If [isList] is true, wraps the result in a List<Object>.\n  /// - Then deletes the \"[dbName].db\" file (always deletes, no matter what happens).\n  ///\n  /// Note: In effect, this will convert all files it loads from a Json-sequence to Json.\n  /// This only works if the original \".db\" file is a Json-sequence file, and it's on you\n  /// to make sure that's the case.\n  ///\n  Future<Object?> loadConverting({required bool isList}) async {\n    //\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n\n    if (!file.existsSync())\n      return _readsFromJsonSequenceDbFile(isList);\n    else {\n      Uint8List encoded;\n      try {\n        // Loads the '.json' (Json) file.\n        encoded = await file.readAsBytes();\n      } catch (error) {\n        if ((error is FileSystemException) && //\n            error.message.contains(\"No such file or directory\"))\n          return _readsFromJsonSequenceDbFile(isList);\n        rethrow;\n      }\n\n      Object? simpleObjs = decodeJson(encoded);\n      return simpleObjs;\n    }\n  }\n\n  /// Reads a Json-sequence from a '.db' file.\n  Future<Object?> _readsFromJsonSequenceDbFile(bool isList) async {\n    //\n    /// Prepares to open the '.db' file with the same name and location.\n    var jsonSequenceFile =\n        LocalPersist(dbName!, dbSubDir: dbSubDir, subDirs: subDirs);\n\n    // If the '.db' (Json-sequence) file exists,\n    if (await jsonSequenceFile.exists()) {\n      //\n      // Loads the '.db' file into memory.\n      List<Object?>? objs = await jsonSequenceFile.load();\n\n      // Deletes the Json-sequence file.\n      jsonSequenceFile.delete();\n\n      if (isList) {\n        objs ??= const [];\n\n        // Saves the '.json' (Json) file, so that it loads directly, next time.\n        await save(objs);\n\n        return objs;\n      }\n      //\n      // Not a list.\n      else {\n        if (objs != null && objs.length > 1)\n          throw PersistException(\n              \"Json sequence to Json: ${objs.length} objects: $objs.\");\n        //\n        else {\n          // Saves the '.json' (Json) file, so that it loads directly, next time.\n          var obj = (objs == null || objs.isEmpty) ? null : objs[0];\n\n          await save(obj);\n\n          return obj;\n        }\n      }\n    }\n    //\n    else\n      return null;\n  }\n\n  /// Same as [load], but expects the file to be a Map<String, dynamic>\n  /// representing a single object. Will fail if it's not a map. It may return null.\n  Future<Map<String, dynamic>?> loadAsObj() async {\n    Object? simpleObj = await load();\n    if (simpleObj == null) return null;\n    if (simpleObj is! Map<String, dynamic>)\n      throw PersistException(\"Not an object: $simpleObj\");\n    return simpleObj;\n  }\n\n  /// Same as [loadConverting], but expects the file to be a Map<String, dynamic>\n  /// representing a single object. Will fail if it's not a map. It may return null.\n  Future<Map<String, dynamic>?> loadAsObjConverting() async {\n    Object? simpleObj = await loadConverting(isList: false);\n    if (simpleObj == null) return null;\n    if (simpleObj is! Map<String, dynamic>)\n      throw PersistException(\"Not an object: $simpleObj\");\n    return simpleObj;\n  }\n\n  /// Deletes the file.\n  /// If the file was deleted, returns true.\n  /// If the file did not exist, return false.\n  Future<bool> delete() async {\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n\n    if (file.existsSync()) {\n      try {\n        file.deleteSync(recursive: true);\n        return true;\n      } catch (error) {\n        if ((error is FileSystemException) && //\n            error.message.contains(\"No such file or directory\")) return false;\n        rethrow;\n      }\n    } else\n      return false;\n  }\n\n  /// Returns the file length.\n  /// If the file doesn't exist, or exists and is empty, returns 0.\n  Future<int> length() async {\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n\n    if (!file.existsSync())\n      return 0;\n    else {\n      try {\n        return file.length();\n      } catch (error) {\n        if ((error is FileSystemException) && //\n            error.message.contains(\"No such file or directory\")) return 0;\n        rethrow;\n      }\n    }\n  }\n\n  /// Returns true if the file exist. False, otherwise.\n  Future<bool> exists() async {\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n    return file.existsSync();\n  }\n\n  // If the fileSystemRef has changed, files will have to be recreated.\n  void _checkIfFileSystemIsTheSame() {\n    if (!identical(_fileSystemRef, _fileSystem)) _file = null;\n  }\n\n  /// Gets the file.\n  Future<File> file() async {\n    if (_file != null)\n      return _file!;\n    else {\n      if (_baseDirectory == null) await useBaseDirectory();\n      String pathNameStr = pathName(\n        dbName,\n        dbSubDir: dbSubDir,\n        subDirs: subDirs,\n      );\n      _file = _fileSystem.file(pathNameStr);\n      return _file!;\n    }\n  }\n\n  static String? simpleObjsToString(List<Object?>? simpleObjs) => //\n      simpleObjs == null\n          ? simpleObjs as String?\n          : simpleObjs.map((obj) => \"$obj (${obj.runtimeType})\").join(\"\\n\");\n\n  static String pathName(\n    String? dbName, {\n    String? dbSubDir,\n    List<String>? subDirs,\n  }) {\n    return p.joinAll([\n      LocalJsonPersist._baseDirectory!.path,\n      dbSubDir ?? LocalJsonPersist.defaultDbSubDir,\n      if (subDirs != null) ...subDirs,\n      \"$dbName${LocalJsonPersist.jsonTermination}\"\n    ]);\n  }\n\n  static String _getStringFromEnum(Object dbName) =>\n      (dbName is String) ? dbName : dbName.toString().split(\".\").last;\n\n  /// Decodes a single JSON into a simple object, from the given [bytes].\n  static Object? decodeJson(Uint8List bytes) {\n    ByteBuffer buffer = bytes.buffer;\n    Uint8List info = Uint8List.view(buffer);\n    var utf8Decoder = const Utf8Decoder();\n    String json = utf8Decoder.convert(info);\n    var jsonDecoder = const JsonDecoder();\n    return jsonDecoder.convert(json);\n  }\n\n  /// Decodes a single simple object into a JSON, from the given [simpleObj].\n  static Uint8List encodeJson(Object? simpleObj) {\n    var jsonEncoder = const JsonEncoder();\n    String json = jsonEncoder.convert(simpleObj);\n\n    Utf8Encoder encoder = const Utf8Encoder();\n    Uint8List encoded = encoder.convert(json);\n    return encoded;\n  }\n\n  /// You can set a memory file-system in your tests. For example:\n  /// ```\n  /// final mfs = MemoryFileSystem();\n  /// setUpAll(() { LocalJsonPersist.setFileSystem(mfs); });\n  /// tearDownAll(() { LocalJsonPersist.resetFileSystem(); });\n  ///  ...\n  /// expect(mfs.file('myPic.jpg').readAsBytesSync(), List.filled(100, 0));\n  /// ```\n  static void setFileSystem(f.FileSystem fileSystem) {\n    LocalPersist.setFileSystem(fileSystem);\n  }\n\n  static void resetFileSystem() =>\n      LocalPersist.setFileSystem(const LocalFileSystem());\n}\n"
  },
  {
    "path": "lib/src/local_persist.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:file/file.dart' as f;\nimport 'package:file/local.dart';\nimport 'package:flutter/services.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\n/// This will save/load objects into the local disk, as a '.json' file.\n///\n/// =========================================================\n///\n/// 1) Save a simple object in UTF-8 Json format.\n///\n/// Use [saveJson] to save as Json:\n///\n/// ```dart\n/// var persist = LocalPersist(\"xyz\");\n/// var simpleObj = \"Hello\";\n/// await persist.saveJson(simpleObj);\n/// ```\n///\n/// Use [loadJson] to load from Json:\n///\n/// ```dart\n/// var persist = LocalPersist(\"xyz\");\n/// Object? decoded = await persist.loadJson();\n/// ```\n///\n/// Examples of valid JSON includes:\n/// 42\n/// 42.5\n/// \"abc\"\n/// [1, 2, 3]\n/// [\"42\", 123]\n/// {\"42\": 123}\n///\n///\n/// Examples of invalid JSON includes:\n/// [1, 2, 3][4, 5, 6] // Not valid because Json does not allow two separate objects.\n/// 1, 2, 3 // Not valid because Json does not allow comma separated objects.\n/// 'abc' // Not valid because string must use double quotes.\n/// {42: \"123\"} // Not valid because a map key must be of type string.\n///\n/// =========================================================\n///\n/// 2) Save multiple simple objects in a concatenation of UTF-8 Json sequence.\n/// Note: A Json sequence is NOT valid Json.\n///\n/// Use [save] to save a list of objects as a Json sequence:\n///\n/// ```dart\n/// var persist = LocalPersist(\"xyz\");\n/// List<Object> simpleObjs = ['\"Hello\"', '\"How are you?\"', [1, 2, 3], 42];\n/// await persist.save();\n/// ```\n///\n/// The save method has an [append] parameter. If [append] is false (the default),\n/// the file will be overwritten. If [append] is true, it will write to the end\n/// of the file. Being able to append is the only advantage of saving as a Json\n/// sequence instead of saving in regular Json. If you don't need to append,\n/// use [saveJson] instead of [save].\n///\n/// Also, a limitation is that, in a json sequence, each object may have at most\n/// 65.536 bytes. Note this refers to a single json object, not to the total json\n/// sequence file, which may contain many objects.\n///\n/// Use [load] to load a list of objects from a Json sequence:\n///\n/// ```dart\n/// var persist = LocalPersist(\"xyz\");\n/// List<Object> decoded = await persist.load();\n/// ```\n///\nclass LocalPersist {\n  //\n  /// The default is saving/loading to/from \"appDocsDir/db/\".\n  /// This is not final, so you can change it.\n  /// Make it an empty string to remove it.\n  static String defaultDbSubDir = \"db\";\n\n  /// If running from Flutter, the default base directory is the application's documents dir.\n  /// If running from tests (detected by the `LocalFileSystem` not being present),\n  /// it will use the system's temp directory.\n  ///\n  /// You can change this variable to globally change the directory:\n  /// ```\n  /// // Will use the application's cache directory.\n  /// LocalPersist.useBaseDirectory = LocalPersist.useAppCacheDir;\n  ///\n  /// // Will use the application's downloads directory.\n  /// LocalPersist.useBaseDirectory = LocalPersist.useAppDownloadsDir;\n  ///\n  /// // Will use whatever Directory is given.\n  /// LocalPersist.useBaseDirectory = () => LocalPersist.useCustomBaseDirectory(baseDirectory: myDir);\n  /// ```\n  static Future<void> Function() useBaseDirectory =\n      LocalPersist.useAppDocumentsDir;\n\n  /// The default is adding a \".db\" termination to the file name.\n  /// This is not final, so you can change it.\n  static String defaultTermination = \".db\";\n\n  static Directory? get appDocDir => _baseDirectory;\n  static Directory? _baseDirectory;\n\n  // In a json sequence, each object may have at most 65.536 bytes.\n  // Note this refers to a single json object, not to the total json sequence file,\n  // which may contain many objects.\n  static const maxJsonSize = 256 * 256;\n\n  static f.FileSystem _fileSystem = const LocalFileSystem();\n\n  final String? dbName, dbSubDir;\n\n  final List<String>? subDirs;\n\n  final f.FileSystem _fileSystemRef;\n\n  File? _file;\n\n  /// Saves to `appDocsDir/db/${dbName}.db`\n  ///\n  /// If [dbName] is a String, it will be used as such.\n  /// If [dbName] is an enum, it will use only the enum value itself.\n  /// For example if `files` is an enum, then `LocalPersist(files.abc)`\n  /// is the same as `LocalPersist(\"abc\")`\n  /// If [dbName] is another object type, a toString() will be done,\n  /// and then the text after the last dot will be used.\n  ///\n  /// The default database directory [defaultDbSubDir] is `db`.\n  /// You can change this variable to globally change the directory,\n  /// or provide [dbSubDir] in the constructor.\n  ///\n  /// You can also provide other [subDirs] as Strings or enums.\n  /// Example: `LocalPersist(\"photos\", subDirs: [\"article\", \"images\"])`\n  /// saves to `appDocsDir/db/article/images/photos.db`\n  ///\n  /// Important:\n  /// — In tests, instead of using `appDocsDir` it will save to\n  /// the system temp dir.\n  /// — If you mock the file-system (see method `setFileSystem()`)\n  /// it will save to `fileSystem.systemTempDirectory`.\n  ///\n  LocalPersist(Object dbName, {this.dbSubDir, List<Object>? subDirs})\n      : dbName = _getStringFromEnum(dbName),\n        subDirs = subDirs?.map((s) => _getStringFromEnum(s)).toList(),\n        _file = null,\n        _fileSystemRef = _fileSystem;\n\n  /// Saves to the given file.\n  LocalPersist.from(File file)\n      : dbName = null,\n        dbSubDir = null,\n        subDirs = null,\n        _file = file,\n        _fileSystemRef = _fileSystem;\n\n  /// Saves the given simple objects.\n  /// If [append] is false (the default), the file will be overwritten.\n  /// If [append] is true, it will write to the end of the file.\n  Future<File> save(List<Object> simpleObjs, {bool append = false}) async {\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n    await file.create(recursive: true);\n\n    Uint8List encoded = LocalPersist.encode(simpleObjs);\n\n    return file.writeAsBytes(\n      encoded,\n      flush: true,\n      mode: append ? FileMode.writeOnlyAppend : FileMode.writeOnly,\n    );\n  }\n\n  /// Saves the given simple object as JSON (but in a '.db' file).\n  /// If the file exists, it will be overwritten.\n  Future<File> saveJson(Object? simpleObj) async {\n    _checkIfFileSystemIsTheSame();\n\n    Uint8List encoded = encodeJson(simpleObj);\n\n    File file = _file ?? await this.file();\n    await file.create(recursive: true);\n\n    return file.writeAsBytes(\n      encoded,\n      flush: true,\n      mode: FileMode.writeOnly,\n    );\n  }\n\n  /// Loads the simple objects from the file.\n  /// If the file doesn't exist, returns null.\n  /// If the file exists and is empty, returns an empty list.\n  Future<List<Object?>?> load() async {\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n\n    if (!file.existsSync())\n      return null;\n    else {\n      Uint8List encoded;\n      try {\n        encoded = await file.readAsBytes();\n      } catch (error) {\n        if ((error is FileSystemException) && //\n            error.message.contains(\"No such file or directory\")) return null;\n        rethrow;\n      }\n\n      List<Object?> simpleObjs = decode(encoded);\n      return simpleObjs;\n    }\n  }\n\n  /// Loads an object from a JSON file ('.db' file).\n  /// If the file doesn't exist, returns null.\n  /// Note: The file must contain a single JSON, which is NOT\n  /// the default file-format for [LocalPersist].\n  Future<Object?>? loadJson() async {\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n\n    if (!file.existsSync())\n      return null;\n    else {\n      Uint8List encoded;\n      try {\n        encoded = await file.readAsBytes();\n      } catch (error) {\n        if ((error is FileSystemException) && //\n            error.message.contains(\"No such file or directory\")) return null;\n        rethrow;\n      }\n\n      Object? simpleObjs = decodeJson(encoded);\n      return simpleObjs;\n    }\n  }\n\n  /// Same as [load], but expects the file to be a Map<String, dynamic>\n  /// representing a single object. Will fail if it's not a map,\n  /// or if contains more than one single object. It may return null.\n  Future<Map<String, dynamic>?> loadAsObj() async {\n    List<Object?>? simpleObjs = await load();\n    if (simpleObjs == null) return null;\n    if (simpleObjs.length != 1)\n      throw PersistException(\"Not a single object: $simpleObjs\");\n    var simpleObj = simpleObjs[0];\n    if ((simpleObj != null) && (simpleObj is! Map<String, dynamic>))\n      throw PersistException(\"Not an object: $simpleObj\");\n    return simpleObj as FutureOr<Map<String, dynamic>?>;\n  }\n\n  /// Deletes the file.\n  /// If the file was deleted, returns true.\n  /// If the file did not exist, return false.\n  Future<bool> delete() async {\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n\n    if (file.existsSync()) {\n      try {\n        file.deleteSync(recursive: true);\n        return true;\n      } catch (error) {\n        if ((error is FileSystemException) && //\n            error.message.contains(\"No such file or directory\")) return false;\n        rethrow;\n      }\n    } else\n      return false;\n  }\n\n  /// Returns the file length.\n  /// If the file doesn't exist, or exists and is empty, returns 0.\n  Future<int> length() async {\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n\n    if (!file.existsSync())\n      return 0;\n    else {\n      try {\n        return file.length();\n      } catch (error) {\n        if ((error is FileSystemException) && //\n            error.message.contains(\"No such file or directory\")) return 0;\n        rethrow;\n      }\n    }\n  }\n\n  /// Returns true if the file exist. False, otherwise.\n  Future<bool> exists() async {\n    _checkIfFileSystemIsTheSame();\n    File file = _file ?? await this.file();\n    return file.existsSync();\n  }\n\n  // If the fileSystemRef has changed, files will have to be recreated.\n  void _checkIfFileSystemIsTheSame() {\n    if (!identical(_fileSystemRef, _fileSystem)) _file = null;\n  }\n\n  /// Gets the file.\n  Future<File> file() async {\n    if (_file != null)\n      return _file!;\n    else {\n      if (_baseDirectory == null) await useBaseDirectory();\n      String pathNameStr = pathName(\n        dbName,\n        dbSubDir: dbSubDir,\n        subDirs: subDirs,\n      );\n      _file = _fileSystem.file(pathNameStr);\n      return _file!;\n    }\n  }\n\n  static String? simpleObjsToString(List<Object?>? simpleObjs) => //\n      simpleObjs == null\n          ? simpleObjs as String?\n          : simpleObjs.map((obj) => \"$obj (${obj.runtimeType})\").join(\"\\n\");\n\n  static String pathName(\n    String? dbName, {\n    String? dbSubDir,\n    List<String>? subDirs,\n  }) {\n    return p.joinAll([\n      LocalPersist._baseDirectory!.path,\n      dbSubDir ?? LocalPersist.defaultDbSubDir,\n      if (subDirs != null) ...subDirs,\n      \"$dbName${LocalPersist.defaultTermination}\"\n    ]);\n  }\n\n  static String _getStringFromEnum(Object dbName) =>\n      (dbName is String) ? dbName : dbName.toString().split(\".\").last;\n\n  /// If running from Flutter, the base directory will be the application's documents directory.\n  /// If running from tests, it will use the system's temp directory.\n  static Future<void> useAppDocumentsDir() async {\n    if (_baseDirectory != null) return;\n\n    if (_fileSystem == const LocalFileSystem()) {\n      try {\n        _baseDirectory = await getApplicationDocumentsDirectory();\n      } on MissingPluginException catch (_) {\n        _baseDirectory = const LocalFileSystem().systemTempDirectory;\n      }\n    } else\n      _baseDirectory = _fileSystem.systemTempDirectory;\n  }\n\n  /// If running from Flutter, the base directory will be the application's cache directory.\n  /// If running from tests, it will use the system's temp directory.\n  static Future<void> useAppCacheDir() async {\n    if (_baseDirectory != null) return;\n\n    if (_fileSystem == const LocalFileSystem()) {\n      try {\n        _baseDirectory = await getApplicationCacheDirectory();\n      } on MissingPluginException catch (_) {\n        _baseDirectory = const LocalFileSystem().systemTempDirectory;\n      }\n    } else\n      _baseDirectory = _fileSystem.systemTempDirectory;\n  }\n\n  /// If running from Flutter, the base directory will be the application's downloads directory.\n  /// If running from tests, it will use the system's temp directory.\n  static Future<void> useAppDownloadsDir() async {\n    if (_baseDirectory != null) return;\n\n    if (_fileSystem == const LocalFileSystem()) {\n      try {\n        _baseDirectory = await getDownloadsDirectory();\n      } on MissingPluginException catch (_) {\n        _baseDirectory = const LocalFileSystem().systemTempDirectory;\n      }\n    } else\n      _baseDirectory = _fileSystem.systemTempDirectory;\n  }\n\n  /// If running from Flutter, the base directory will be the given [baseDirectory].\n  /// If running from tests, it will use the optional [testDirectory], or if this is not provided,\n  /// it will use the system's temp directory.\n  static Future<void> useCustomBaseDirectory({\n    required Directory baseDirectory,\n    Directory? testDirectory,\n  }) async {\n    if (_baseDirectory != null) return;\n\n    if (_fileSystem == const LocalFileSystem()) {\n      try {\n        // Calling this just to detect if we are running from Flutter or tests.\n        await getDownloadsDirectory();\n        _baseDirectory = baseDirectory;\n      } on MissingPluginException catch (_) {\n        _baseDirectory =\n            testDirectory ?? const LocalFileSystem().systemTempDirectory;\n      }\n    } else\n      _baseDirectory = _fileSystem.systemTempDirectory;\n  }\n\n  static Uint8List encode(List<Object> simpleObjs) {\n    Iterable<String> jsons = objsToJsons(simpleObjs);\n    List<Uint8List> chunks = jsonsToUint8Lists(jsons);\n    Uint8List encoded = concatUint8Lists(chunks);\n    return encoded;\n  }\n\n  static Iterable<String> objsToJsons(List<Object> simpleObjs) {\n    var jsonEncoder = const JsonEncoder();\n    return simpleObjs.map((j) => jsonEncoder.convert(j));\n  }\n\n  static List<Uint8List> jsonsToUint8Lists(Iterable<String> jsons) {\n    List<Uint8List> chunks = [];\n\n    for (String json in jsons) {\n      Utf8Encoder encoder = const Utf8Encoder();\n      Uint8List bytes = encoder.convert(json);\n      var size = bytes.length;\n\n      if (size > maxJsonSize)\n        throw PersistException(\"Size is $size but max is $maxJsonSize bytes.\");\n\n      chunks.add(Uint8List.fromList([size ~/ 256, size % 256]));\n      chunks.add(bytes);\n    }\n\n    return chunks;\n  }\n\n  static Uint8List concatUint8Lists(List<Uint8List> chunks) {\n    return Uint8List.fromList(chunks.expand((x) => (x)).toList());\n  }\n\n  static List<Object?> decode(Uint8List bytes) {\n    List<Uint8List> chunks = bytesToUint8Lists(bytes);\n    Iterable<String> jsons = uint8ListsToJsons(chunks);\n    return toSimpleObjs(jsons).toList();\n  }\n\n  /// Decodes a single JSON into a simple object, from the given [bytes].\n  static Object? decodeJson(Uint8List bytes) {\n    ByteBuffer buffer = bytes.buffer;\n    Uint8List info = Uint8List.view(buffer);\n    var utf8Decoder = const Utf8Decoder();\n    String json = utf8Decoder.convert(info);\n    var jsonDecoder = const JsonDecoder();\n    return jsonDecoder.convert(json);\n  }\n\n  /// Decodes a single simple object into a JSON, from the given [simpleObj].\n  static Uint8List encodeJson(Object? simpleObj) {\n    var jsonEncoder = const JsonEncoder();\n    String json = jsonEncoder.convert(simpleObj);\n\n    Utf8Encoder encoder = const Utf8Encoder();\n    Uint8List encoded = encoder.convert(json);\n    return encoded;\n  }\n\n  static List<Uint8List> bytesToUint8Lists(Uint8List bytes) {\n    List<Uint8List> chunks = [];\n    var buffer = bytes.buffer;\n    int pos = 0;\n    while (pos < bytes.length) {\n      int size = bytes[pos] * 256 + bytes[pos + 1];\n      Uint8List info = Uint8List.view(buffer, pos + 2, size);\n      chunks.add(info);\n      pos += 2 + size;\n    }\n    return chunks;\n  }\n\n  static Iterable<String> uint8ListsToJsons(Iterable<Uint8List> chunks) {\n    var utf8Decoder = const Utf8Decoder();\n    return chunks.map((readChunks) => utf8Decoder.convert(readChunks));\n  }\n\n  static Iterable<Object?> toSimpleObjs(Iterable<String> jsons) {\n    var jsonDecoder = const JsonDecoder();\n    return jsons.map((json) => jsonDecoder.convert(json));\n  }\n\n  /// You can set a memory file-system in your tests. For example:\n  /// ```\n  /// final mfs = MemoryFileSystem();\n  /// setUpAll(() { LocalPersist.setFileSystem(mfs); });\n  /// tearDownAll(() { LocalPersist.resetFileSystem(); });\n  ///  ...\n  /// expect(mfs.file('myPic.jpg').readAsBytesSync(), List.filled(100, 0));\n  /// ```\n  static void setFileSystem(f.FileSystem fileSystem) {\n    _fileSystem = fileSystem;\n  }\n\n  static f.FileSystem getFileSystem() => _fileSystem;\n\n  static void resetFileSystem() => setFileSystem(const LocalFileSystem());\n}\n"
  },
  {
    "path": "lib/src/log.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:logging/logging.dart';\n\n/// Connects a [Logger] to the Redux Store.\n/// Every action that is dispatched will be logged to the Logger, along with the new State\n/// that was created as a result of the action reaching your Store's reducer.\n///\n/// By default, this class does not print anything to your console or to a web\n/// service, such as Fabric or Sentry. It simply logs entries to a Logger instance.\n/// You can then listen to the [Logger.onRecord] Stream, and print to the\n/// console or send these actions to a web service.\n///\n/// Example: To print actions to the console as they are dispatched:\n///\n///     var store = Store(\n///       initialValue: 0,\n///       actionObservers: [Log.printer()]);\n///\n/// Example: If you only want to log actions to a Logger, use the default constructor.\n///\n///     // Create your own Logger and pass it to the Observer.\n///     final logger = new Logger(\"Redux Logger\");\n///     final stateObserver = Log(logger: logger);\n///\n///     final store = new Store<int>(\n///       initialState: 0,\n///       stateObserver: [stateObserver]);\n///\n///     // Note: One quirk about listening to a logger instance is that you're\n///     // actually listening to the Singleton instance of *all* loggers.\n///     logger.onRecord\n///       // Filter down to [LogRecord]s sent to your logger instance\n///       .where((record) => record.loggerName == logger.name)\n///       // Print them out (or do something more interesting!)\n///       .listen((LogRecord) => print(LogRecord));\n///\nclass Log<St> implements ActionObserver<St> {\n  //\n  final Logger logger;\n\n  /// The log Level at which the actions will be recorded\n  final Level level;\n\n  /// A function that formats the String for printing\n  final MessageFormatter<St> formatter;\n\n  /// Logs actions to the given Logger, and does not print anything to the console.\n  Log({\n    Logger? logger,\n    this.level = Level.INFO,\n    this.formatter = singleLineFormatter,\n  }) : logger = logger ?? Logger(\"Log\");\n\n  /// Logs actions to the console.\n  factory Log.printer({\n    Logger? logger,\n    Level level = Level.INFO,\n    MessageFormatter<St> formatter = singleLineFormatter,\n  }) {\n    final log = Log(logger: logger, level: level, formatter: formatter);\n    log.logger.onRecord //\n        .where((record) => record.loggerName == log.logger.name)\n        .listen(print);\n    return log;\n  }\n\n  /// A very simple formatter that writes only the action.\n  static String verySimpleFormatter(\n    dynamic state,\n    ReduxAction action,\n    bool ini,\n    int dispatchCount,\n    DateTime timestamp,\n  ) =>\n      \"$action ${ini ? 'INI' : 'END'}\";\n\n  /// A simple formatter that puts all data on one line.\n  static String singleLineFormatter(\n    dynamic state,\n    ReduxAction action,\n    bool? ini,\n    int dispatchCount,\n    DateTime timestamp,\n  ) {\n    return \"{$action, St: $state, ts: ${DateTime.now()}}\";\n  }\n\n  /// A formatter that puts each attribute on it's own line.\n  static String multiLineFormatter(\n    dynamic state,\n    ReduxAction action,\n    bool ini,\n    int dispatchCount,\n    DateTime timestamp,\n  ) {\n    return \"{\\n\"\n        \"  $dispatchCount) $action,\\n\"\n        \"  St: $state,\\n\"\n        \"  Timestamp: ${DateTime.now()}\\n\"\n        \"}\";\n  }\n\n  @override\n  void observe(ReduxAction<St> action, int dispatchCount, {required bool ini}) {\n    logger.log(\n      level,\n      formatter(null, action, ini, dispatchCount, DateTime.now()),\n    );\n  }\n}\n\n//\n\n/// A function that formats the message that will be logged:\n///\n///   final log = Log(formatter: onlyLogActionFormatter);\n///   var store = new Store(initialState: 0, actionObservers:[log], stateObservers: [...]);\n///\ntypedef MessageFormatter<St> = String Function(\n  St? state,\n  ReduxAction<St> action,\n  bool ini,\n  int dispatchCount,\n  DateTime timestamp,\n);\n"
  },
  {
    "path": "lib/src/mock_build_context.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/foundation.dart' show DiagnosticsTreeStyle;\nimport 'package:flutter/material.dart';\n\n/// A mock BuildContext that holds a Store reference, for testing purposes.\n///\n/// This allows you to test smart widgets created with context extensions.\n/// For example, suppose this is your smart widget:\n///\n/// ```dart\n/// class MyConnector extends StoreConnector<AppState, MyModel> {\n///   Widget build(BuildContext context) {\n///     return MyWidget(name: context.state.name);\n///   }\n/// }\n/// ```\n///\n/// This is how you can test it with:\n///\n/// ```dart\n/// // First, create a Store with the desired initial state.\n/// var store = Store(initialState: AppState(name: 'Mark');\n///\n/// // Then, create a mock BuildContext with that store.\n/// var context = MockBuildContext(store);\n///\n/// // Instantiate your StoreConnector or StoreProvider and build the widget.\n/// var widget = MyConnector().build(context) as MyWidget;\n/// expect(widget.name, 'Mark');\n/// ```\n///\nclass MockBuildContext extends BuildContext {\n  final Store store;\n\n  MockBuildContext(this.store) {\n    // Create the static store backdoor.\n    StoreProvider(store: store, child: const SizedBox());\n  }\n\n  @override\n  Widget get widget => const Placeholder();\n\n  @override\n  bool get debugDoingBuild => true;\n\n  @override\n  InheritedWidget dependOnInheritedElement(InheritedElement ancestor,\n      {Object? aspect}) {\n    throw UnimplementedError('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(\n      {Object? aspect}) {\n    throw UnimplementedError('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  DiagnosticsNode describeElement(String name,\n      {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}) {\n    return DiagnosticsNode.message('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  List<DiagnosticsNode> describeMissingAncestor(\n      {required Type expectedAncestorType}) {\n    return [DiagnosticsNode.message('Not implemented in MockBuildContext.')];\n  }\n\n  @override\n  DiagnosticsNode describeOwnershipChain(String name) {\n    return DiagnosticsNode.message('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  DiagnosticsNode describeWidget(String name,\n      {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}) {\n    return DiagnosticsNode.message('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  void dispatchNotification(Notification notification) {\n    // Do nothing.\n  }\n\n  @override\n  T? findAncestorRenderObjectOfType<T extends RenderObject>() {\n    throw UnimplementedError('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  T? findAncestorStateOfType<T extends State<StatefulWidget>>() {\n    throw UnimplementedError('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  T? findAncestorWidgetOfExactType<T extends Widget>() {\n    throw UnimplementedError('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  RenderObject? findRenderObject() {\n    throw UnimplementedError('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  T? findRootAncestorStateOfType<T extends State<StatefulWidget>>() {\n    throw UnimplementedError('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  InheritedElement?\n      getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {\n    throw UnimplementedError('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  T? getInheritedWidgetOfExactType<T extends InheritedWidget>() {\n    throw UnimplementedError('Not implemented in MockBuildContext.');\n  }\n\n  @override\n  bool get mounted => true;\n\n  @override\n  BuildOwner? get owner => null;\n\n  @override\n  Size? get size =>\n      throw UnimplementedError('Not implemented in MockBuildContext.');\n\n  @override\n  void visitAncestorElements(ConditionalElementVisitor visitor) {\n    // Do nothing.\n  }\n\n  @override\n  void visitChildElements(ElementVisitor visitor) {\n    // Do nothing.\n  }\n}\n"
  },
  {
    "path": "lib/src/mock_store.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\n\n/// Creates a Redux store that lets you mock actions/reducers.\n///\n/// The MockStore lets you define mock actions/reducers for specific actions.\n///\n/// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n///\nclass MockStore<St> extends Store<St> {\n  MockStore({\n    required St initialState,\n    Object? environment,\n    Map<Object?, Object?> props = const {},\n    bool syncStream = false,\n    TestInfoPrinter? testInfoPrinter,\n    List<ActionObserver<St>>? actionObservers,\n    List<StateObserver<St>>? stateObservers,\n    Persistor<St>? persistor,\n    Persistor<St>? cloudSync,\n    ModelObserver? modelObserver,\n    ErrorObserver<St>? errorObserver,\n    WrapReduce<St>? wrapReduce,\n    GlobalErrorObserver<St> Function(Store<St>)? globalErrorObserver,\n    //\n    @Deprecated(\"Use `globalErrorObserver` instead. This will be removed.\")\n    GlobalWrapError<St>? globalWrapError,\n    //\n    bool? defaultDistinct,\n    CompareBy? immutableCollectionEquality,\n    int? maxErrorsQueued,\n    this.mocks,\n  }) : super(\n          initialState: initialState,\n          environment: environment,\n          props: props,\n          syncStream: syncStream,\n          testInfoPrinter: testInfoPrinter,\n          actionObservers: actionObservers,\n          stateObservers: stateObservers,\n          persistor: persistor,\n          cloudSync: cloudSync,\n          modelObserver: modelObserver,\n          errorObserver: errorObserver,\n          wrapReduce: wrapReduce,\n          globalErrorObserver: globalErrorObserver,\n          globalWrapError: globalWrapError,\n          defaultDistinct: defaultDistinct,\n          immutableCollectionEquality: immutableCollectionEquality,\n          maxErrorsQueued: maxErrorsQueued,\n        );\n\n  /// 1) `null` to disable dispatching the action of a certain type.\n  ///\n  /// 2) A `MockAction<St>` instance to dispatch that action instead,\n  /// and provide the original action as a getter to the mocked action.\n  ///\n  /// 3) A `ReduxAction<St>` instance to dispatch that mocked action instead.\n  ///\n  /// 4) `ReduxAction<St> Function(ReduxAction<St>)` to create a mock\n  /// from the original action.\n  ///\n  /// 5) `St Function(ReduxAction<St>, St)` or\n  /// `Future<St> Function(ReduxAction<St>, St)` to modify the state directly.\n  ///\n  Map<Type, dynamic>? mocks;\n\n  MockStore<St> addMock(Type actionType, dynamic mock) {\n    (mocks ??= {})[actionType] = mock;\n    return this;\n  }\n\n  MockStore<St> addMocks(Map<Type, dynamic> mocks) {\n    (this.mocks ??= {}).addAll(mocks);\n    return this;\n  }\n\n  MockStore<St> clearMocks() {\n    mocks = null;\n    return this;\n  }\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// The action may be sync or async.\n  ///\n  /// ```dart\n  /// store.dispatch(MyAction());\n  /// ```\n  ///\n  /// Method [dispatch] is of type [Dispatch].\n  ///\n  /// See also:\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  @override\n  FutureOr<ActionStatus> dispatch(\n    ReduxAction<St> action, {\n    bool notify = true,\n  }) {\n    ReduxAction<St>? _action = _getMockedAction(action);\n\n    return (_action == null) //\n        ? Future.value(ActionStatus(context: (action, this)))\n        : super.dispatch(_action, notify: notify);\n  }\n\n  @Deprecated(\"Use `dispatchAndWait` instead. This will be removed.\")\n  @override\n  Future<ActionStatus> dispatchAsync(ReduxAction<St> action, {bool notify = true}) {\n    return dispatchAndWait(action, notify: notify);\n  }\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// The action may be sync or async. In both cases, it returns a [Future] that resolves when\n  /// the action finishes.\n  ///\n  /// ```dart\n  /// await store.dispatchAndWait(DoThisFirstAction());\n  /// store.dispatch(DoThisSecondAction());\n  /// ```\n  ///\n  /// Note: While the state change from the action's reducer will have been applied when the\n  /// Future resolves, other independent processes that the action may have started may still\n  /// be in progress.\n  ///\n  /// Method [dispatchAndWait] is of type [DispatchAndWait]. It returns `Future<ActionStatus>`,\n  /// which means you can also get the final status of the action after you `await` it:\n  ///\n  /// ```dart\n  /// var status = await store.dispatchAndWait(MyAction());\n  /// ```\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  @override\n  Future<ActionStatus> dispatchAndWait(ReduxAction<St> action, {bool notify = true}) {\n    ReduxAction<St>? _action = _getMockedAction(action);\n\n    return (_action == null) //\n        ? Future.value(ActionStatus(context: (action, this)))\n        : super.dispatchAndWait(_action, notify: notify);\n  }\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// However, if the action is ASYNC, it will throw a [StoreException].\n  ///\n  /// Method [dispatchSync] is of type [DispatchSync].\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  @override\n  ActionStatus dispatchSync(ReduxAction<St> action, {bool notify = true}) {\n    ReduxAction<St>? _action = _getMockedAction(action);\n\n    return (_action == null) //\n        ? ActionStatus(context: (action, this))\n        : super.dispatchSync(_action, notify: notify);\n  }\n\n  ReduxAction<St>? _getMockedAction(ReduxAction<St> action) {\n    if (mocks == null || !mocks!.containsKey(action.runtimeType))\n      return action;\n    else {\n      var mock = mocks![action.runtimeType];\n\n      // 1) `null` to disable dispatching the action of a certain type.\n      if (mock == null)\n        return null;\n      //\n      // 2) A `MockAction<St>` instance to dispatch that action instead,\n      // and provide the original action as a getter to the mocked action.\n      else if (mock is MockAction<St>) {\n        mock._setAction(action);\n        return mock;\n      }\n      //\n      // 3) A `ReduxAction<St>` instance to dispatch that mocked action instead.\n      else if (mock is ReduxAction) {\n        return mock as ReduxAction<St>;\n      }\n      //\n      // 4) `ReduxAction<St> Function(ReduxAction<St>)` to create a mock\n      // from the original action.\n      else if (mock is ReduxAction<St> Function(ReduxAction<St>)) {\n        ReduxAction<St> mockAction = mock(action);\n        return mockAction;\n      }\n      //\n      // 5) `St Function(ReduxAction<St>, St)` or\n      // `Future<St> Function(ReduxAction<St>, St)` to modify the state directly.\n      else if (mock is St Function(ReduxAction<St>, St)) {\n        MockAction<St> mockAction = _GeneralActionSync(mock);\n        mockAction._setAction(action);\n        return mockAction;\n      } else if (mock is Future<St> Function(ReduxAction<St>, St)) {\n        MockAction<St> mockAction = _GeneralActionAsync(mock);\n        mockAction._setAction(action);\n        return mockAction;\n      }\n      //\n      else\n        throw StoreException(\"Action of type `${action.runtimeType}` \"\n            \"can't be mocked by a mock of type \"\n            \"`${mock.runtimeType}`.\\n\"\n            \"Valid mock types are:\\n\"\n            \"`null`\\n\"\n            \"`MockAction<St>`\\n\"\n            \"`ReduxAction<St>`\\n\"\n            \"`ReduxAction<St> Function(ReduxAction<St>)`\\n\"\n            \"`St Function(ReduxAction<St>, St)`\\n\");\n    }\n  }\n}\n\nabstract class MockAction<St> extends ReduxAction<St> {\n  late ReduxAction<St> _action;\n\n  ReduxAction<St> get action => _action;\n\n  void _setAction(ReduxAction<St> action) {\n    _action = action;\n  }\n}\n\nclass _GeneralActionSync<St> extends MockAction<St> {\n  final St Function(ReduxAction<St> action, St state) _reducer;\n\n  _GeneralActionSync(this._reducer);\n\n  @override\n  St reduce() => _reducer(action, state);\n}\n\nclass _GeneralActionAsync<St> extends MockAction<St> {\n  final Future<St> Function(ReduxAction<St> action, St state) _reducer;\n\n  _GeneralActionAsync(this._reducer);\n\n  @override\n  Future<St> reduce() => _reducer(action, state);\n}\n"
  },
  {
    "path": "lib/src/model_observer.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\n\n/// The [ModelObserver] is rarely used. It's goal is to observe and troubleshoot the model changes\n/// causing rebuilds. While you may subclass it to implement its [observe] method, usually you can\n/// just use the provided [DefaultModelObserver] to print the StoreConnector's ViewModel to the\n/// console.\n///\nabstract class ModelObserver<Model> {\n  //\n  /// The [ModelObserver] can be used to observe and troubleshoot the model changes.\n  ///\n  /// The [storeConnector] works by rebuilding the widget when the model changes.\n  /// It needs to compare the [modelPrevious] with the [modelCurrent] to decide if the widget should\n  /// rebuild:\n  ///\n  /// - [isDistinct] is `true` means the widget rebuilt because the model changed.\n  /// - [isDistinct] is `false` means the widget didn't rebuilt because the model hasn't changed.\n  /// - [isDistinct] is `null` means the widget rebuilds everytime (because of\n  ///                the `StoreConnector.distinct` parameter), and the model is not relevant.\n  void observe({\n    required Model? modelPrevious,\n    required Model? modelCurrent,\n    bool? isDistinct,\n    StoreConnectorInterface? storeConnector,\n    int? reduceCount,\n    int? dispatchCount,\n  });\n}\n\n/// The [DefaultModelObserver] prints the StoreConnector's ViewModel to the console.\n///\n/// Passe it to the store like this:\n///\n/// `var store = Store(modelObserver:DefaultModelObserver());`\n///\n/// If you need to print the type of the `StoreConnector` to the console,\n/// make sure to pass `debug:this` as a `StoreConnector` constructor parameter.\n/// Then, optionally, you can also specify a list of `StoreConnector`s to be\n/// observed:\n///\n/// `DefaultModelObserver([MyStoreConnector, SomeOtherStoreConnector]);`\n///\n/// You can also override your `ViewModels.toString()` to print out\n/// any extra info you need.\n///\nclass DefaultModelObserver<Model> implements ModelObserver<Model> {\n  Model? _previous;\n  Model? _current;\n\n  Model? get previous => _previous;\n\n  Model? get current => _current;\n\n  final List<Type> _storeConnectorTypes;\n\n  DefaultModelObserver([this._storeConnectorTypes = const <Type>[]]);\n\n  @override\n  void observe({\n    required Model? modelPrevious,\n    required Model? modelCurrent,\n    bool? isDistinct,\n    StoreConnectorInterface? storeConnector,\n    int? reduceCount,\n    int? dispatchCount,\n  }) {\n    _previous = modelPrevious;\n    _current = modelCurrent;\n\n    var shouldObserve = _storeConnectorTypes.isEmpty ||\n        _storeConnectorTypes.contains(storeConnector!.debug?.runtimeType);\n\n    if (shouldObserve)\n      print(\"Model D:$dispatchCount R:$reduceCount = \"\n          \"Rebuild:${isDistinct == null || isDistinct}, \"\n          \"${storeConnector!.debug == null ? \"\" : //\n              \"Connector:${storeConnector.debug.runtimeType}\"}, \"\n          \"Model:$modelCurrent.\");\n  }\n}\n"
  },
  {
    "path": "lib/src/navigate_action.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:flutter/material.dart';\n\nimport '../async_redux.dart';\n\n/// Available constructors:\n/// `NavigateAction.push()`,\n/// `NavigateAction.pop()`,\n/// `NavigateAction.popAndPushNamed()`,\n/// `NavigateAction.pushNamed()`,\n/// `NavigateAction.pushReplacement()`,\n/// `NavigateAction.pushAndRemoveUntil()`,\n/// `NavigateAction.replace()`,\n/// `NavigateAction.replaceRouteBelow()`,\n/// `NavigateAction.pushReplacementNamed()`,\n/// `NavigateAction.pushNamedAndRemoveUntil()`,\n/// `NavigateAction.pushNamedAndRemoveAll()`,\n/// `NavigateAction.popUntil()`,\n/// `NavigateAction.removeRoute()`,\n/// `NavigateAction.removeRouteBelow()`,\n/// `NavigateAction.popUntilRouteName()`,\n/// `NavigateAction.popUntilRoute()`,\n///\nclass NavigateAction<St> extends ReduxAction<St> {\n  static GlobalKey<NavigatorState>? _navigatorKey;\n\n  static GlobalKey<NavigatorState>? get navigatorKey => _navigatorKey;\n\n  static void setNavigatorKey(GlobalKey<NavigatorState> navigatorKey) =>\n      _navigatorKey = navigatorKey;\n\n  /// Trick explained here: https://github.com/flutter/flutter/issues/20451\n  /// Note 'ModalRoute.of(context).settings.name' doesn't always work.\n  static String? getCurrentNavigatorRouteName(BuildContext context) {\n    late Route currentRoute;\n    Navigator.popUntil(context, (route) {\n      currentRoute = route;\n      return true;\n    });\n    return currentRoute.settings.name;\n  }\n\n  NavigateAction._(this.details);\n\n  final NavigatorDetails details;\n\n  /// This is useful for tests only.\n  /// You can test that some dispatched NavigateAction was of a certain type.\n  NavigateType get type => details.type;\n\n  @override\n  St? reduce() {\n    details.navigate();\n    return null;\n  }\n\n  NavigateAction.push(\n    Route route,\n  ) : this._(NavigatorDetails_Push(route));\n\n  NavigateAction.pop([Object? result]) : this._(NavigatorDetails_Pop(result));\n\n  NavigateAction.popAndPushNamed(\n    String routeName, {\n    Object? result,\n    Object? arguments,\n  }) : this._(NavigatorDetails_PopAndPushNamed(routeName,\n            result: result, arguments: arguments));\n\n  NavigateAction.pushNamed(\n    String routeName, {\n    Object? arguments,\n  }) : this._(NavigatorDetails_PushNamed(routeName, arguments: arguments));\n\n  NavigateAction.pushReplacement(\n    Route route, {\n    Object? result,\n  }) : this._(NavigatorDetails_PushReplacement(route, result: result));\n\n  NavigateAction.pushAndRemoveUntil(\n    Route route,\n    RoutePredicate predicate,\n  ) : this._(NavigatorDetails_PushAndRemoveUntil(route, predicate));\n\n  NavigateAction.replace({\n    Route? oldRoute,\n    Route? newRoute,\n  }) : this._(NavigatorDetails_Replace(\n          oldRoute: oldRoute,\n          newRoute: newRoute,\n        ));\n\n  NavigateAction.replaceRouteBelow({\n    Route? anchorRoute,\n    Route? newRoute,\n  }) : this._(NavigatorDetails_ReplaceRouteBelow(\n          anchorRoute: anchorRoute,\n          newRoute: newRoute,\n        ));\n\n  NavigateAction.pushReplacementNamed(\n    String routeName, {\n    Object? arguments,\n  }) : this._(NavigatorDetails_PushReplacementNamed(routeName,\n            arguments: arguments));\n\n  NavigateAction.pushNamedAndRemoveUntil(\n    String newRouteName,\n    RoutePredicate predicate, {\n    Object? arguments,\n  }) : this._(NavigatorDetails_PushNamedAndRemoveUntil(newRouteName, predicate,\n            arguments: arguments));\n\n  NavigateAction.pushNamedAndRemoveAll(\n    String newRouteName, {\n    Object? arguments,\n  }) : this._(NavigatorDetails_PushNamedAndRemoveAll(newRouteName,\n            arguments: arguments));\n\n  NavigateAction.popUntil(\n    RoutePredicate predicate,\n  ) : this._(NavigatorDetails_PopUntil(predicate));\n\n  NavigateAction.removeRoute(\n    Route route,\n  ) : this._(NavigatorDetails_RemoveRoute(route));\n\n  NavigateAction.removeRouteBelow(\n    Route anchorRoute,\n  ) : this._(NavigatorDetails_RemoveRouteBelow(anchorRoute));\n\n  NavigateAction.popUntilRouteName(\n    String routeName, {\n    bool ifPrintRoutes = false,\n  }) : this._(NavigatorDetails_PopUntilRouteName(routeName,\n            ifPrintRoutes: ifPrintRoutes));\n\n  NavigateAction.popUntilRoute(\n    Route route,\n  ) : this._(NavigatorDetails_PopUntilRoute(route));\n\n  @override\n  String toString() => '${super.toString()}${details.toString()}';\n}\n\nclass NavigatorDetails_Push implements NavigatorDetails {\n  final Route route;\n\n  NavigatorDetails_Push(this.route);\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.push(route);\n  }\n\n  @override\n  NavigateType get type => NavigateType.push;\n\n  @override\n  String toString() => '.push(${route.toStringOrRuntimeType()})';\n}\n\nclass NavigatorDetails_Pop implements NavigatorDetails {\n  final Object? result;\n\n  NavigatorDetails_Pop(this.result);\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.pop(result);\n  }\n\n  @override\n  NavigateType get type => NavigateType.pop;\n\n  @override\n  String toString() =>\n      '.pop(${result == null ? \"\" : result.toStringOrRuntimeType()})';\n}\n\nclass NavigatorDetails_PopAndPushNamed implements NavigatorDetails {\n  final String routeName;\n  final Object? result;\n  final Object? arguments;\n\n  NavigatorDetails_PopAndPushNamed(\n    this.routeName, {\n    this.result,\n    this.arguments,\n  });\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.popAndPushNamed(\n      routeName,\n      result: result,\n      arguments: arguments,\n    );\n  }\n\n  @override\n  NavigateType get type => NavigateType.popAndPushNamed;\n\n  @override\n  String toString() => '.popAndPushNamed('\n      '$routeName'\n      '${result == null ? \"\" : \", result: \" + result.toStringOrRuntimeType()})';\n}\n\nclass NavigatorDetails_PushNamed implements NavigatorDetails {\n  final String routeName;\n  final Object? arguments;\n\n  NavigatorDetails_PushNamed(\n    this.routeName, {\n    this.arguments,\n  });\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState\n        ?.pushNamed(routeName, arguments: arguments);\n  }\n\n  @override\n  NavigateType get type => NavigateType.pushNamed;\n\n  @override\n  String toString() => '.pushNamed($routeName)';\n}\n\nclass NavigatorDetails_PushReplacementNamed implements NavigatorDetails {\n  final String routeName;\n  final Object? arguments;\n\n  NavigatorDetails_PushReplacementNamed(\n    this.routeName, {\n    this.arguments,\n  });\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState\n        ?.pushReplacementNamed(routeName, arguments: arguments);\n  }\n\n  @override\n  NavigateType get type => NavigateType.pushReplacementNamed;\n\n  @override\n  String toString() => '.pushReplacementNamed($routeName)';\n}\n\nclass NavigatorDetails_PushNamedAndRemoveUntil implements NavigatorDetails {\n  final String newRouteName;\n  final Object? arguments;\n  final RoutePredicate predicate;\n\n  NavigatorDetails_PushNamedAndRemoveUntil(\n    this.newRouteName,\n    this.predicate, {\n    this.arguments,\n  });\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.pushNamedAndRemoveUntil(\n        newRouteName, predicate,\n        arguments: arguments);\n  }\n\n  @override\n  NavigateType get type => NavigateType.pushNamedAndRemoveUntil;\n\n  @override\n  String toString() => '.pushNamedAndRemoveUntil($newRouteName, predicate)';\n}\n\nclass NavigatorDetails_PushNamedAndRemoveAll implements NavigatorDetails {\n  final String newRouteName;\n  final Object? arguments;\n\n  NavigatorDetails_PushNamedAndRemoveAll(\n    this.newRouteName, {\n    this.arguments,\n  });\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.pushNamedAndRemoveUntil(\n        newRouteName, (_) => false,\n        arguments: arguments);\n  }\n\n  @override\n  NavigateType get type => NavigateType.pushNamedAndRemoveAll;\n\n  @override\n  String toString() => '.pushNamedAndRemoveAll($newRouteName)';\n}\n\nclass NavigatorDetails_PushReplacement implements NavigatorDetails {\n  final Route route;\n  final Object? result;\n\n  NavigatorDetails_PushReplacement(\n    this.route, {\n    this.result,\n  });\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState\n        ?.pushReplacement(route, result: result);\n  }\n\n  @override\n  NavigateType get type => NavigateType.pushReplacement;\n\n  @override\n  String toString() => '.pushReplacement('\n      '${route.toStringOrRuntimeType()}'\n      '${result == null ? \"\" : \", result: \" + result.toStringOrRuntimeType()})';\n}\n\nclass NavigatorDetails_PushAndRemoveUntil implements NavigatorDetails {\n  final Route route;\n  final RoutePredicate predicate;\n\n  NavigatorDetails_PushAndRemoveUntil(\n    this.route,\n    this.predicate,\n  );\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState\n        ?.pushAndRemoveUntil(route, predicate);\n  }\n\n  @override\n  NavigateType get type => NavigateType.pushAndRemoveUntil;\n\n  @override\n  String toString() => '.pushAndRemoveUntil('\n      '${route.toStringOrRuntimeType()}'\n      ', predicate)';\n}\n\nclass NavigatorDetails_Replace implements NavigatorDetails {\n  final Route? oldRoute;\n  final Route? newRoute;\n\n  NavigatorDetails_Replace({\n    required this.oldRoute,\n    required this.newRoute,\n  });\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.replace(\n      oldRoute: oldRoute!,\n      newRoute: newRoute!,\n    );\n  }\n\n  @override\n  NavigateType get type => NavigateType.replace;\n\n  @override\n  String toString() => '.replace('\n      'oldRoute: ${oldRoute.toStringOrRuntimeType()}, '\n      'newRoute: ${newRoute.toStringOrRuntimeType()})';\n}\n\nclass NavigatorDetails_ReplaceRouteBelow implements NavigatorDetails {\n  final Route? anchorRoute;\n  final Route? newRoute;\n\n  NavigatorDetails_ReplaceRouteBelow({\n    required this.anchorRoute,\n    required this.newRoute,\n  });\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.replaceRouteBelow(\n      anchorRoute: anchorRoute!,\n      newRoute: newRoute!,\n    );\n  }\n\n  @override\n  NavigateType get type => NavigateType.replaceRouteBelow;\n\n  @override\n  String toString() => '.replaceRouteBelow('\n      'anchorRoute: ${anchorRoute.toStringOrRuntimeType()}, '\n      'newRoute: ${newRoute.toStringOrRuntimeType()})';\n}\n\nclass NavigatorDetails_PopUntil implements NavigatorDetails {\n  final RoutePredicate predicate;\n\n  NavigatorDetails_PopUntil(this.predicate);\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.popUntil(predicate);\n  }\n\n  @override\n  NavigateType get type => NavigateType.popUntil;\n\n  @override\n  String toString() => '.popUntil(predicate)';\n}\n\nclass NavigatorDetails_PopUntilRouteName implements NavigatorDetails {\n  final String routeName;\n\n  /// Make this true if you want to see all the routes printed to the console.\n  /// This doesn't affect the navigation itself.\n  final bool ifPrintRoutes;\n\n  NavigatorDetails_PopUntilRouteName(this.routeName,\n      {this.ifPrintRoutes = false});\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.popUntil(((route) {\n      bool result = (route.settings.name == routeName);\n      if (ifPrintRoutes)\n        print('${route.settings.name} == $routeName ($result)');\n      return result;\n    }));\n  }\n\n  @override\n  NavigateType get type => NavigateType.popUntilRouteName;\n\n  @override\n  String toString() => '.popUntilRouteName($routeName)';\n}\n\nclass NavigatorDetails_PopUntilRoute implements NavigatorDetails {\n  final Route route;\n\n  NavigatorDetails_PopUntilRoute(this.route);\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState\n        ?.popUntil(((_route) => _route == route));\n  }\n\n  @override\n  NavigateType get type => NavigateType.popUntilRoute;\n\n  @override\n  String toString() => '.popUntilRoute(${route.toStringOrRuntimeType()})';\n}\n\nclass NavigatorDetails_RemoveRoute implements NavigatorDetails {\n  final Route route;\n\n  NavigatorDetails_RemoveRoute(this.route);\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.removeRoute(route);\n  }\n\n  @override\n  NavigateType get type => NavigateType.removeRoute;\n\n  @override\n  String toString() => '.removeRoute(${route.toStringOrRuntimeType()})';\n}\n\nclass NavigatorDetails_RemoveRouteBelow implements NavigatorDetails {\n  final Route anchorRoute;\n\n  NavigatorDetails_RemoveRouteBelow(this.anchorRoute);\n\n  @override\n  void navigate() {\n    NavigateAction._navigatorKey?.currentState?.removeRouteBelow(anchorRoute);\n  }\n\n  @override\n  NavigateType get type => NavigateType.removeRouteBelow;\n\n  @override\n  String toString() =>\n      '.removeRouteBelow(${anchorRoute.toStringOrRuntimeType()})';\n}\n\nabstract class NavigatorDetails {\n  void navigate();\n\n  NavigateType get type;\n}\n\nenum NavigateType {\n  push,\n  pop,\n  popAndPushNamed,\n  pushNamed,\n  pushReplacement,\n  pushAndRemoveUntil,\n  replace,\n  replaceRouteBelow,\n  pushReplacementNamed,\n  pushNamedAndRemoveUntil,\n  pushNamedAndRemoveAll,\n  popUntil,\n  removeRoute,\n  removeRouteBelow,\n  popUntilRouteName,\n  popUntilRoute,\n}\n\nextension _StringExtension on Object? {\n  /// If the object can be represented with up to 200 chars, we print it.\n  /// Otherwise, we use the object's runtimeType without the generic part.\n  String toStringOrRuntimeType() {\n    String text = toString();\n    if (text.length <= 200)\n      return text;\n    else {\n      text = runtimeType.toString();\n      var pos = text.indexOf('<');\n      return (pos == -1) ? text : text.substring(0, text.indexOf('<'));\n    }\n  }\n}\n"
  },
  {
    "path": "lib/src/persistor.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\n\n/// Use it like this:\n///\n/// ```dart\n/// var persistor = MyPersistor();\n///\n/// var initialState = await persistor.readState();\n///\n/// if (initialState == null) {\n/// initialState = AppState.initialState();\n/// await persistor.saveInitialState(initialState);\n/// }\n///\n/// var store = Store<AppState>(\n///   initialState: initialState,\n///   persistor: persistor,\n/// );\n/// ```\n///\n/// IMPORTANT: When the store is created with a Persistor, the store considers that the\n/// provided initial-state was already persisted. You have to make sure this is the case.\n///\nabstract class Persistor<St> {\n  //\n\n  /// Read the saved state from the persistence. Should return null if the state is not yet\n  /// persisted. This method should be called only once, when the app starts, before the store\n  /// is created. The state it returns may become the store's initial-state. If some error\n  /// occurs while loading the info, we have to deal with it by fixing the problem. In the worse\n  /// case, if we think the state is corrupted and cannot be fixed, one alternative is deleting\n  /// all persisted files and returning null.\n  Future<St?> readState();\n\n  /// Delete the saved state from the persistence.\n  Future<void> deleteState();\n\n  /// Save the new state to the persistence.\n  /// [lastPersistedState] is the last state that was persisted since the app started,\n  /// while [newState] is the new state to be persisted.\n  ///\n  /// Note you have to make sure that [newState] is persisted after this method is called.\n  /// For simpler apps where your state is small, you can just ignore [lastPersistedState]\n  /// and persist the whole [newState] every time. But for larger apps, you should compare\n  /// [lastPersistedState] and [newState], to persist only the difference between them.\n  Future<void> persistDifference({\n    required St? lastPersistedState,\n    required St newState,\n  });\n\n  /// Save an initial-state to the persistence.\n  Future<void> saveInitialState(St state) =>\n      persistDifference(lastPersistedState: null, newState: state);\n\n  /// The default throttle is 2 seconds. Pass null to turn off throttle.\n  Duration? get throttle => const Duration(seconds: 2);\n}\n\n/// A decorator to print persistor information to the console.\n/// Use it like this:\n///\n/// ```dart\n/// var store = Store<AppState>(...,  persistor: PersistorPrinterDecorator(persistor));\n/// ```\n///\nclass PersistorPrinterDecorator<St> extends Persistor<St> {\n  final Persistor<St> _persistor;\n\n  PersistorPrinterDecorator(this._persistor);\n\n  @override\n  Future<St?> readState() async {\n    print(\"Persistor: read state.\");\n    return _persistor.readState();\n  }\n\n  @override\n  Future<void> deleteState() async {\n    print(\"Persistor: delete state.\");\n    return _persistor.deleteState();\n  }\n\n  @override\n  Future<void> persistDifference({\n    required St? lastPersistedState,\n    required St newState,\n  }) async {\n    print(\"Persistor: persist difference:\\n\"\n        \"lastPersistedState = $lastPersistedState\\n\"\n        \"newState = newState\");\n    return _persistor.persistDifference(\n        lastPersistedState: lastPersistedState, newState: newState);\n  }\n\n  @override\n  Future<void> saveInitialState(St state) async {\n    print(\"Persistor: save initial state.\");\n    return _persistor.saveInitialState(state);\n  }\n\n  @override\n  Duration? get throttle => _persistor.throttle;\n}\n\n/// A dummy persistor.\n///\nclass PersistorDummy<T> extends Persistor<T> {\n  @override\n  Future<T?> readState() async => null;\n\n  @override\n  Future<void> deleteState() async => null;\n\n  @override\n  Future<void> persistDifference(\n      {required lastPersistedState, required newState}) async {}\n\n  @override\n  Future<void> saveInitialState(T state) async {}\n\n  @override\n  Duration? get throttle => null;\n}\n\nclass PersistException implements Exception {\n  final Object error;\n\n  PersistException(this.error);\n\n  @override\n  String toString() => error.toString();\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is PersistException //\n          &&\n          runtimeType == other.runtimeType //\n          &&\n          error == other.error;\n\n  @override\n  int get hashCode => error.hashCode;\n}\n\nclass PersistAction<St> extends ReduxAction<St> {\n  @override\n  St? reduce() => null;\n}\n"
  },
  {
    "path": "lib/src/process_persistence.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\n\nclass ProcessPersistence<St> {\n  //\n  ProcessPersistence(this.persistor, this.lastPersistedState)\n      : isPersisting = false,\n        isANewStateAvailable = false,\n        lastPersistTime = DateTime.now().toUtc(),\n        isPaused = false,\n        isInit = false;\n\n  final Persistor persistor;\n  St? lastPersistedState;\n  late St newestState;\n  bool isPersisting;\n  bool isANewStateAvailable;\n  DateTime lastPersistTime;\n  Timer? timer;\n  bool isPaused;\n  bool isInit;\n\n  Duration get throttle => persistor.throttle ?? const Duration();\n\n  /// Same as [Persistor.saveInitialState] but will remember [initialState] as the [lastPersistedState].\n  Future<void> saveInitialState(St initialState) {\n    lastPersistedState = initialState;\n    return persistor.saveInitialState(initialState);\n  }\n\n  /// Same as [Persistor.readState] but will remember the read state as the [lastPersistedState].\n  Future<St?> readState() async {\n    St? state = await persistor.readState();\n    lastPersistedState = state;\n    return state;\n  }\n\n  /// Same as [Persistor.deleteState] but will clear the [lastPersistedState].\n  Future<void> deleteState() async {\n    lastPersistedState = null;\n    return persistor.deleteState();\n  }\n\n  /// 1) If we're still persisting the last time, don't persist no matter what.\n  /// 2) If throttle period is done (or if action is PersistAction), persist.\n  /// 3) If throttle period is NOT done, create a timer to persist as soon as it finishes.\n  ///\n  /// Return true if the persist process started.\n  /// Return false if persistence was postponed.\n  ///\n  bool process(\n    ReduxAction<St>? action,\n    St newState,\n  ) {\n    isInit = true;\n    newestState = newState;\n\n    if (isPaused || identical(lastPersistedState, newState)) return false;\n\n    // 1) If we're still persisting the last time, don't persist no matter what.\n    if (isPersisting) {\n      isANewStateAvailable = true;\n      return false;\n    }\n    //\n    else {\n      //\n      var now = DateTime.now().toUtc();\n\n      // 2) If throttle period is done (or if action is PersistAction), persist.\n      if ( //\n          (now.difference(lastPersistTime) >= throttle) //\n              ||\n              (action is PersistAction) //\n          ) {\n        _cancelTimer();\n        _persist(now, newestState);\n        return true;\n      }\n      //\n      // 3) If throttle period is NOT done, create a timer to persist as soon as it finishes.\n      else {\n        if (timer == null) {\n          //\n          Duration asSoonAsThrottleFinishes =\n              throttle - now.difference(lastPersistTime);\n\n          timer = Timer(asSoonAsThrottleFinishes, () {\n            timer = null;\n            process(null, newestState);\n          });\n        }\n        return false;\n      }\n    }\n  }\n\n  void _cancelTimer() {\n    if (timer != null) {\n      timer!.cancel();\n      timer = null;\n    }\n  }\n\n  void _persist(DateTime now, newState) async {\n    isPersisting = true;\n    lastPersistTime = now;\n    isANewStateAvailable = false;\n\n    try {\n      await persistor.persistDifference(\n        lastPersistedState: lastPersistedState,\n        newState: newState,\n      );\n    }\n    //\n    finally {\n      lastPersistedState = newState;\n      isPersisting = false;\n\n      // If a new state became available while the present state was saving, save again.\n      if (isANewStateAvailable) {\n        isANewStateAvailable = false;\n        process(null, newestState);\n      }\n    }\n  }\n\n  /// Pause the [Persistor] temporarily.\n  ///\n  /// When [pause] is called, the Persistor will not start a new persistence process, until method\n  /// [resume] is called. This will not affect the current persistence process, if one is currently\n  /// running.\n  ///\n  /// Note: A persistence process starts when the [persistDifference] method is called, and\n  /// finishes when the future returned by that method completes.\n  ///\n  void pause() {\n    isPaused = true;\n  }\n\n  /// Persists the current state (if it's not yet persisted), then pauses the [Persistor]\n  /// temporarily.\n  ///\n  ///\n  /// When [persistAndPause] is called, this will not affect the current persistence process, if\n  /// one is currently running. If no persistence process was running, it will immediately start a\n  /// new persistence process (ignoring [throttle]).\n  ///\n  /// Then, the Persistor will not start another persistence process, until method [resume] is\n  /// called.\n  ///\n  /// Note: A persistence process starts when the [persistDifference] method is called, and\n  /// finishes when the future returned by that method completes.\n  ///\n  void persistAndPause() {\n    isPaused = true;\n\n    _cancelTimer();\n\n    if (isInit &&\n        !isPersisting &&\n        !identical(lastPersistedState, newestState)) {\n      var now = DateTime.now().toUtc();\n      _persist(now, newestState);\n    }\n  }\n\n  /// Resumes persistence by the [Persistor], after calling [pause] or [persistAndPause].\n  void resume() {\n    isPaused = false;\n    if (isInit) process(null, newestState);\n  }\n}\n"
  },
  {
    "path": "lib/src/redux_action.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\npart of async_redux_store;\n\n/// All actions you create must extend this class `ReduxAction`.\n///\n/// Important: Do NOT override operator == and hashCode. Actions must retain\n/// their default [Object] comparison by identity, for AsyncRedux to work.\n///\n/// ---\n///\n/// This class comes with a lot of useful fields and methods:\n///\n/// Most important ones are:\n///\n/// > `state` - Returns current state in the store. This is a getter, and can change after every await, for async actions.\n/// > `reduce` - The action reducer that returns the new state. Must be overridden.\n/// > `dispatch` - Dispatches an action (sync or async).\n/// > `dispatchAndWait` - Dispatches an action and returns a `Future` that resolves when it finishes.\n/// > `isWaiting` - Checks if a specific action or action type is currently being processed.\n/// > `isFailed` - Returns true if an action failed with a `UserException`.\n///\n/// Useful ones are:\n///\n/// > `store` - Returns the store instance.\n/// > `before` - Optional method that runs before `reduce` during action dispatching.\n/// > `after` - Optional method that runs after `reduce` during action dispatching.\n/// > `wrapError` - Optionally catches or modifies errors thrown by `reduce` or `before` methods.\n/// > `dispatchAndWaitAll` - Dispatches multiple actions in parallel and waits for all to finish.\n/// > `dispatchAll` - Dispatches multiple actions in parallel.\n/// > `dispatchSync` - Dispatches a sync action, throws if the action is async.\n/// > `exceptionFor` - Returns the `UserException` of the action that failed.\n/// > `clearExceptionFor` - Removes the given action type from the failed actions list.\n/// > `initialState` - Returns the state as it was when the action was dispatched. This does NOT change.\n/// > `waitCondition` - Returns a future that completes when the given state condition is true.\n/// > `waitAllActions` - Returns a future that completes when all given actions finish.\n/// > `status` - Returns the current status of the action (waiting, failed, completed, etc.).\n/// > `prop` - Gets a property from the store (timers, streams, etc.).\n/// > `setProp` - Sets a property in the store.\n/// > `disposeProp` - Disposes a single property by its key.\n/// > `disposeProps` - Disposes all or selected properties (timers, streams, futures).\n/// > `env` - Gets the store environment, useful for global values scoped to the store.\n/// > `microtask` - Returns a future that completes in the next microtask.\n/// > `assertUncompletedFuture` - Asserts that an async reducer has at least one await.\n///\n/// Useful mixins:\n///\n/// > `CheckInternet` - Checks if there is internet before running the action, shows dialog if not.\n/// > `NoDialog` - Used with `CheckInternet` to turn off the dialog when there is no internet.\n/// > `AbortWhenNoInternet` - Silently aborts the action if there is no internet.\n/// > `NonReentrant` - Prevents the action from being dispatched if it's already running.\n/// > `Retry` - Retries the action if it fails, with configurable delays and max retries.\n/// > `UnlimitedRetries` - Used with `Retry` to retry indefinitely.\n/// > `OptimisticCommand` - Updates the state optimistically before saving to the cloud.\n/// > `Throttle` - Ensures the action is dispatched at most once per throttle period.\n/// > `Debounce` - Delays action execution until after a period of inactivity.\n/// > `UnlimitedRetryCheckInternet` - Retries indefinitely with internet checking, prevents reentrant dispatches.\n///\n/// Finally, these are one-off methods that you may use in special situations:\n///\n/// > `stateTimestamp` - Returns the timestamp of the last state change.\n/// > `wrapReduce` - Wraps the `reduce` method for pre/post-processing.\n/// > `abortDispatch` - Returns true to abort the action dispatch before it runs.\n/// > `isSync` - Returns true if the action is sync, false if async.\n/// > `ifWrapReduceOverridden_Sync` - Returns true if `wrapReduce` is overridden synchronously.\n/// > `ifWrapReduceOverridden_Async` - Returns true if `wrapReduce` is overridden asynchronously.\n/// > `ifWrapReduceOverridden` - Returns true if `wrapReduce` is overridden (sync or async).\n/// > `runtimeTypeString` - Returns the `runtimeType` without the generic part.\n///\nabstract class ReduxAction<St> {\n  late Store<St> _store;\n  late St _initialState;\n  ActionStatus _status = ActionStatus(context: null);\n  bool _completedFuture = false;\n\n  @protected\n  void setStore(Store<St> store) {\n    _store = store;\n    _status = _status.copy(context: (this, store));\n    _initialState = _store.state;\n  }\n\n  /// Returns the state as it was when the action was dispatched.\n  ///\n  /// It can be the same or different from `this.state`, which is the current state in the store,\n  /// because other actions may have changed the current state since this action was dispatched.\n  ///\n  /// In the case of SYNC actions that do not dispatch other SYNC actions,\n  /// `this.state` and `this.initialState` will be the same.\n  @protected\n  St get initialState => _initialState;\n\n  @protected\n  Store<St> get store => _store;\n\n  ActionStatus get status => _status;\n\n  /// Gets a property from the store.\n  /// This can be used to save global values, but scoped to the store.\n  /// For example, you could save timers, streams or futures used by actions.\n  ///\n  /// ```dart\n  /// setProp(\"timer\", Timer(Duration(seconds: 1), () => print(\"tick\")));\n  /// var timer = prop<Timer>(\"timer\");\n  /// timer.cancel();\n  /// ```\n  ///\n  /// See also: [setProp] and [env].\n  ///\n  @protected\n  V prop<V>(Object? key) => store.prop<V>(key);\n\n  /// Sets a property in the store.\n  /// This can be used to save global values, but scoped to the store.\n  /// For example, you could save timers, streams or futures used by actions.\n  ///\n  /// ```dart\n  /// setProp(\"timer\", Timer(Duration(seconds: 1), () => print(\"tick\")));\n  /// var timer = prop<Timer>(\"timer\");\n  /// timer.cancel();\n  /// ```\n  ///\n  /// See also: [prop] and [env].\n  ///\n  @protected\n  void setProp(Object? key, Object? value) => store.setProp(key, value);\n\n  /// The [disposeProps] method is used to clean up resources associated with\n  /// the store's properties, by stopping, closing, ignoring and removing timers,\n  /// streams, sinks, and futures that are saved as properties in the store.\n  ///\n  /// In more detail: This method accepts an optional predicate function that\n  /// takes a prop `key` and a `value` as an argument and returns a boolean.\n  ///\n  /// * If you don't provide a predicate function, all properties which are\n  /// `Timer`, `Future`, or `Stream` related will be closed/cancelled/ignored as\n  /// appropriate, and then removed from the props. Other properties will not be\n  /// removed.\n  ///\n  /// * If the predicate function is provided and returns `true` for a given\n  /// property, that property will be removed from the props and, if the property\n  /// is also a `Timer`, `Future`, or `Stream` related, it will be\n  /// closed/cancelled/ignored as appropriate.\n  ///\n  /// * If the predicate function is provided and returns `false` for a given\n  /// property, that property will not be removed from the props, and it will\n  /// not be closed/cancelled/ignored.\n  ///\n  /// This method is particularly useful when the store is being shut down,\n  /// right before or after you called the [Store.shutdown] method.\n  ///\n  /// Example usage:\n  ///\n  /// ```dart\n  /// // Dispose of all Timers, Futures, Stream related etc.\n  /// disposeProps();\n  ///\n  /// // Dispose only Timers.\n  /// disposeProps(({Object? key, Object? value}) => value is Timer);\n  /// ```\n  ///\n  /// Note: The provided mixins, like [Throttle] and [Debounce] also use some\n  /// props that you can dispose by doing `store.internalMixinProps.clear()`;\n  ///\n  /// See also: [disposeProp], to dispose a single property by its key.\n  ///\n  @protected\n  void disposeProps([bool Function({Object? key, Object? value})? predicate]) =>\n      store.disposeProps(predicate);\n\n  /// Uses [disposeProps] to dispose and a single property identified by\n  /// its key [keyToDispose], and remove it from the props.\n  ///\n  /// This method will close/cancel/ignore the property if it's a Timer, Future,\n  /// or Stream related object, and then remove it from the props.\n  ///\n  /// Example usage:\n  ///\n  /// ```dart\n  /// // Dispose a specific timer property\n  /// store.disposeProp(\"myTimer\");\n  /// ```\n  @protected\n  void disposeProp(Object? keyToDispose) => store.disposeProp(keyToDispose);\n\n  /// To wait for the next microtask: `await microtask;`\n  @protected\n  Future get microtask => Future.microtask(() {});\n\n  @protected\n  St get state => _store.state;\n\n  DateTime get stateTimestamp => _store.stateTimestamp;\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// The action may be sync or async.\n  ///\n  /// ```dart\n  /// store.dispatch(MyAction());\n  /// ```\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of this action, even if it changes the state.\n  ///\n  /// Method [dispatch] is of type [Dispatch].\n  ///\n  /// See also:\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchState] which dispatches a sync action that applies a given reducer to the current state.\n  ///\n  @protected\n  Dispatch<St> get dispatch => _store.dispatch;\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// However, if the action is ASYNC, it will throw a [StoreException].\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of this action, even if it changes the state.\n  ///\n  /// Method [dispatchSync] is of type [DispatchSync]. It returns `ActionStatus`,\n  /// which means you can also get the final status of the action:\n  ///\n  /// ```dart\n  /// var status = store.dispatchSync(MyAction());\n  /// ```\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchState] which dispatches a sync action that applies a given reducer to the current state.\n  ///\n  @protected\n  DispatchSync<St> get dispatchSync => _store.dispatchSync;\n\n  @Deprecated(\"Use `dispatchAndWait` instead. This will be removed.\")\n  @protected\n  DispatchAsync<St> get dispatchAsync => _store.dispatchAndWait;\n\n  /// This is a shortcut, equivalent to:\n  ///\n  /// ```dart\n  /// var status = dispatchSync(\n  ///   UpdateStateAction.withReducer(state),\n  /// );\n  /// ```\n  ///\n  /// In other words, it dispatches a sync action that applies the given [state].\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of this action, even if it changes the state.\n  ///\n  /// This dispatch method is to be used ONLY inside other actions, and is not\n  /// available as an widget extension.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  ///\n  @protected\n  ActionStatus dispatchState(St state, {bool notify = true}) =>\n      dispatchSync(UpdateStateAction(state), notify: notify);\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// The action may be sync or async. In both cases, it returns a [Future] that resolves when\n  /// the action finishes.\n  ///\n  /// ```dart\n  /// await store.dispatchAndWait(DoThisFirstAction());\n  /// store.dispatch(DoThisSecondAction());\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild\n  /// because of this action, even if it changes the state.\n  ///\n  /// Note: While the state change from the action's reducer will have been applied when\n  /// the Future resolves, other independent processes that the action may have started\n  /// may still be in progress.\n  ///\n  /// Method [dispatchAndWait] is of type [DispatchAndWait]. It returns `Future<ActionStatus>`,\n  /// which means you can also get the final status of the action after you `await` it:\n  ///\n  /// ```dart\n  /// var status = await store.dispatchAndWait(MyAction());\n  /// ```\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchState] which dispatches a sync action that applies a given reducer to the current state.\n  ///\n  @protected\n  DispatchAndWait<St> get dispatchAndWait => _store.dispatchAndWait;\n\n  /// Dispatches all given [actions] in parallel, applying their reducers, and possibly changing\n  /// the store state. The actions may be sync or async. It returns a [Future] that resolves when\n  /// ALL actions finish.\n  ///\n  /// ```dart\n  /// var actions = await store.dispatchAndWaitAll([BuyAction('IBM'), SellAction('TSLA')]);\n  /// ```\n  ///\n  /// Note this is exactly the same as doing:\n  ///\n  /// ```dart\n  /// var action1 = BuyAction('IBM');\n  /// var action2 = SellAction('TSLA');\n  /// dispatch(action1);\n  /// dispatch(action2);\n  /// await store.waitAllActions([action1, action2], completeImmediately = true);\n  /// var actions = [action1, action2];\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of these actions, even if they change the state.\n  ///\n  /// Note: While the state change from the action's reducers will have been applied when the\n  /// Future resolves, other independent processes that the action may have started may still\n  /// be in progress.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  /// - [dispatchState] which dispatches a sync action that applies a given reducer to the current state.\n  ///\n  @protected\n  Future<List<ReduxAction<St>>> Function(List<ReduxAction<St>> actions, {bool notify})\n      get dispatchAndWaitAll => _store.dispatchAndWaitAll;\n\n  /// Dispatches all given [actions] in parallel, applying their reducer, and possibly changing\n  /// the store state. It returns the same list of [actions], so that you can instantiate them\n  /// inline, but still get a list of them.\n  ///\n  /// ```dart\n  /// var actions = dispatchAll([BuyAction('IBM'), SellAction('TSLA')]);\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of these actions, even if it changes the state.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchState] which dispatches a sync action that applies a given reducer to the current state.\n  ///\n  @protected\n  List<ReduxAction<St>> Function(List<ReduxAction<St>> actions, {bool notify})\n      get dispatchAll => _store.dispatchAll;\n\n  /// This is an optional method that may be overridden to run during action\n  /// dispatching, before `reduce`. If this method throws an error, the\n  /// `reduce` method will NOT run, but the method `after` will.\n  /// It may be synchronous (returning `void`) ou async (returning `Future<void>`).\n  /// You should NOT return `FutureOr`.\n  @protected\n  FutureOr<void> before() {}\n\n  /// This is an optional method that may be overridden to run during action\n  /// dispatching, after `reduce`. If this method throws an error, the\n  /// error will be swallowed (will not throw). So you should only run code that\n  /// can't throw errors. It may be synchronous only.\n  /// Note this method will always be called,\n  /// even if errors were thrown by `before` or `reduce`.\n  ///\n  /// Note: For both synchronous and asynchronous actions, when after runs the store\n  /// already contains the new state returned by reduce, so accessing [state] in [after]\n  /// will return the new state.\n  ///\n  /// Note: Accessing [initialState] in [after] always returns the state as it was when\n  /// the action was dispatched, regardless of when after runs.\n  @protected\n  void after() {}\n\n  /// The `reduce` method is the action reducer. It may read the action state,\n  /// the store state, and then return a new state (or `null` if no state\n  /// change is necessary).\n  ///\n  /// It may be synchronous (returning `AppState` or `null`)\n  /// or async (returning `Future<AppState>` or `Future<null>`).\n  ///\n  /// The `StoreConnector`s may rebuild only if the `reduce` method returns\n  /// a state which is both not `null` and different from the previous one\n  /// (comparing by `identical`, not `equals`).\n  @protected\n  FutureOr<St?> reduce();\n\n  /// You may override [wrapReduce] to wrap the [reduce] method and allow for\n  /// some pre- or post-processing. For example, if you want to prevent an\n  /// async reducer to change the current state in cases where the current\n  /// state has already changed since when the reducer started:\n  ///\n  /// ```dart\n  /// Future<St?> wrapReduce(Reducer<St> reduce) async {\n  ///    var oldState = state;\n  ///    AppState? newState = await reduce();\n  ///    return identical(oldState, state) ? newState : null;\n  /// };\n  /// ```\n  ///\n  /// IMPORTANT:\n  ///\n  /// * Your [wrapReduce] method MUST always return `Future<St?>`. If it\n  /// returns a `FutureOr`, it will NOT be called, and no error will be shown.\n  /// This is because AsyncRedux uses the return type to determine if\n  /// [wrapReduce] was overridden or not.\n  ///\n  /// * If [wrapReduce] returns `St` or `St?`, an error will be thrown.\n  ///\n  /// * Once you override [wrapReduce] the action will always be ASYNC,\n  /// regardless of the [before] and [reduce] methods.\n  ///\n  /// See mixins [Retry], [Throttle], and [Debounce] for real [wrapReduce]\n  /// examples.\n  ///\n  @protected\n  FutureOr<St?> wrapReduce(Reducer<St> reduce) {\n    return null;\n  }\n\n  /// If any error is thrown by `reduce` or `before`, you have the chance\n  /// to further process it by using `wrapError`. Usually this is used to wrap\n  /// the error inside of another that better describes the failed action.\n  /// For example, if some action converts a String into a number, then instead of\n  /// throwing a FormatException you could do:\n  ///\n  /// ```dart\n  /// wrapError(error, _) => UserException(\"Please enter a valid number.\", cause: error)\n  /// ```\n  ///\n  /// If you want to disable the error you can return `null`. For example, if you want\n  /// to disable errors of type `MyException`:\n  ///\n  /// ```dart\n  /// wrapError(error, _) => (error is MyException) ? null : error\n  /// ```\n  ///\n  /// If you don't want to modify the error, just return it unaltered\n  /// (or don't override this method).\n  ///\n  /// See also:\n  /// - [GlobalErrorObserver] which is a global way to wrap errors thrown by actions,\n  ///   and is called after this method.\n  ///\n  @protected\n  Object? wrapError(Object error, StackTrace stackTrace) => error;\n\n  /// If [abortDispatch] returns true, the action will NOT be dispatched:\n  /// `before`, `reduce` and `after` will not be called, and the action will not\n  /// be visible to the store observers.\n  ///\n  /// Note: No observer will be called. It will be as if the action was never\n  /// dispatched. The action status will be `isDispatchAborted: true`.\n  ///\n  /// For example, this mixin prevents reentrant actions (you can only call the\n  /// action if it's not already running):\n  ///\n  /// ```dart\n  /// /// This mixin prevents reentrant actions. You can only call the action if it's not already\n  /// /// running. Example: `class LoadInfo extends ReduxAction<AppState> with NonReentrant { ... }`\n  /// mixin NonReentrant implements ReduxAction<AppState> {\n  ///   bool abortDispatch() => isWaiting(runtimeType);\n  /// }\n  /// ```\n  ///\n  /// Using [abortDispatch] is only useful under rare circumstances, and you should\n  /// only use it if you know what you are doing.\n  ///\n  /// See also:\n  /// - [AbortDispatchException] which is a way to abort the action by throwing an exception.\n  ///\n  @protected\n  bool abortDispatch() => false;\n\n  /// You can use [isWaiting] to check if:\n  /// * A specific async ACTION is currently being processed.\n  /// * An async action of a specific TYPE is currently being processed.\n  /// * If any of a few given async actions or action types is currently being processed.\n  ///\n  /// If you wait for an action TYPE, then it returns false when:\n  /// - The ASYNC action of the type is NOT currently being processed.\n  /// - If the type is not really a type that extends [ReduxAction].\n  /// - The action of the type is a SYNC action (since those finish immediately).\n  ///\n  /// If you wait for an ACTION, then it returns false when:\n  /// - The ASYNC action is NOT currently being processed.\n  /// - If the action is a SYNC action (since those finish immediately).\n  ///\n  /// Trying to wait for any other type of object will return null and throw\n  /// a [StoreException] after the async gap.\n  ///\n  /// Examples:\n  ///\n  /// ```dart\n  /// // Waiting for an action TYPE:\n  /// dispatch(MyAction());\n  /// if (store.isWaiting(MyAction)) { // Show a spinner }\n  ///\n  /// // Waiting for an ACTION:\n  /// var action = MyAction();\n  /// dispatch(action);\n  /// if (store.isWaiting(action)) { // Show a spinner }\n  ///\n  /// // Waiting for any of the given action TYPES:\n  /// dispatch(BuyAction());\n  /// if (store.isWaiting([BuyAction, SellAction])) { // Show a spinner }\n  /// ```\n  @protected\n  bool isWaiting(Object actionOrTypeOrList) => _store.isWaiting(actionOrTypeOrList);\n\n  /// Returns true if an [actionOrTypeOrList] failed with an [UserException].\n  /// Note: This method uses the EXACT type in [actionOrTypeOrList]. Subtypes are not considered.\n  @protected\n  bool isFailed(Object actionOrTypeOrList) => _store.isFailed(actionOrTypeOrList);\n\n  /// Returns the [UserException] of the [actionTypeOrList] that failed.\n  ///\n  /// [actionTypeOrList] can be a [Type], or an Iterable of types. Any other type\n  /// of object will return null and throw a [StoreException] after the async gap.\n  ///\n  /// Note: This method uses the EXACT type in [actionTypeOrList]. Subtypes are not considered.\n  @protected\n  UserException? exceptionFor(Object actionTypeOrList) =>\n      _store.exceptionFor(actionTypeOrList);\n\n  /// Removes the given [actionTypeOrList] from the list of action types that failed.\n  ///\n  /// Note that dispatching an action already removes that action type from the exceptions list.\n  /// This removal happens as soon as the action is dispatched, not when it finishes.\n  ///\n  /// [actionTypeOrList] can be a [Type], or an Iterable of types. Any other type\n  /// of object will return null and throw a [StoreException] after the async gap.\n  ///\n  /// Note: This method uses the EXACT type in [actionTypeOrList]. Subtypes are not considered.\n  @protected\n  void clearExceptionFor(Object actionTypeOrList) =>\n      _store.clearExceptionFor(actionTypeOrList);\n\n  /// Returns a future which will complete when the given state [condition] is true.\n  /// If the condition is already true when the method is called, the future completes immediately.\n  ///\n  /// You may also provide a [timeoutMillis], which by default is 10 minutes.\n  /// To disable the timeout, make it -1.\n  /// If you want, you can modify [Store.defaultTimeoutMillis] to change the default timeout.\n  ///\n  /// ```dart\n  /// var action = await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(action, isA<ChangeNameAction>());\n  /// ```\n  @protected\n  Future<ReduxAction<St>?> waitCondition(\n    bool Function(St) condition, {\n    int? timeoutMillis,\n  }) =>\n      _store.waitCondition(condition, timeoutMillis: timeoutMillis);\n\n  /// Returns a future that completes when ALL given [actions] finished dispatching.\n  /// You MUST provide at list one action, or an error will be thrown.\n  ///\n  /// If [completeImmediately] is `false` (the default), this method will throw [StoreException]\n  /// if none of the given actions are in progress when the method is called. Otherwise, the future\n  /// will complete immediately and throw no error.\n  ///\n  /// Example:\n  ///\n  /// ```ts\n  /// // Dispatching two actions in PARALLEL and waiting for both to finish.\n  /// var action1 = ChangeNameAction('Bill');\n  /// var action2 = ChangeAgeAction(42);\n  /// await waitAllActions([action1, action2]);\n  ///\n  /// // Compare this to dispatching the actions in SERIES:\n  /// await dispatchAndWait(action1);\n  /// await dispatchAndWait(action2);\n  /// ```\n  @protected\n  Future<void> waitAllActions(List<ReduxAction<St>> actions,\n      {bool completeImmediately = false}) {\n    if (actions.isEmpty)\n      throw StoreException('You have to provide a non-empty list of actions.');\n    return _store.waitAllActions(actions, completeImmediately: completeImmediately);\n  }\n\n  /// An async reducer (one that returns Future<AppState?>) must never complete without at least\n  /// one await, because this may result in state changes being lost. It's up to you to make sure\n  /// all code paths in the reducer pass through at least one `await`.\n  ///\n  /// Futures defined by async functions with no `await` are called \"completed futures\".\n  /// It's generally easy to make sure an async reducer does not return a completed future.\n  /// In the rare case when your reducer function is complex and you are unsure that all\n  /// code paths pass through an await, there are 3 possible solutions:\n  ///\n  ///\n  /// * Simplify your reducer, by applying clean-code techniques. That will make it easier for you\n  /// to make sure all code paths have 'await'.\n  ///\n  /// * Add `await microtask;` to the very START of the reducer.\n  ///\n  /// * Call method [assertUncompletedFuture] at the very END of your [reduce] method, right before\n  /// the return. If you do that, an error will be shown in the console in case the reduce method\n  /// ever returns a completed future. Note there is no other way for AsyncRedux to warn you if\n  /// your reducer returned a completed future, because although the completion information exists\n  /// in the `FutureImpl` class, it's not exposed. Also note, the error will be thrown\n  /// asynchronously (will not stop the action from returning a state).\n  ///\n  @protected\n  void assertUncompletedFuture() {\n    scheduleMicrotask(() {\n      _completedFuture = true;\n    });\n  }\n\n  @protected\n  bool ifWrapReduceOverridden_Sync() => wrapReduce is St? Function(Reducer<St>);\n\n  @protected\n  bool ifWrapReduceOverridden_Async() => wrapReduce is Future<St?> Function(Reducer<St>);\n\n  @protected\n  bool ifWrapReduceOverridden() =>\n      ifWrapReduceOverridden_Async() || ifWrapReduceOverridden_Sync();\n\n  /// Returns true if the action is SYNC, and false if the action is ASYNC.\n  /// The action is considered SYNC if the `before` method, the `reduce` method,\n  /// and the `wrapReduce` methods are all synchronous.\n  bool isSync() {\n    //\n    /// Must check that it's NOT `Future<void> Function()`, as `void Function()` doesn't work.\n    bool beforeMethodIsSync = before is! Future<void> Function();\n    if (!beforeMethodIsSync) return false;\n\n    bool reduceMethodIsSync = reduce is St? Function();\n    if (!reduceMethodIsSync) return false;\n\n    // `wrapReduce` is sync if it's not overridden.\n    // `wrapReduce` is sync if it's overridden and SYNC.\n    // `wrapReduce` is NOT sync if it's overridden and ASYNC.\n    return (!ifWrapReduceOverridden_Async());\n  }\n\n  /// Returns the runtimeType, without the generic part.\n  String runtimeTypeString() {\n    var text = runtimeType.toString();\n    var pos = text.indexOf('<');\n    return (pos == -1) ? text : text.substring(0, pos);\n  }\n\n  @override\n  String toString() => 'Action ${runtimeTypeString()}';\n}\n\n/// If an action throws an [AbortDispatchException] the action will abort immediately\n/// (But note the `after` method will still be called no mather what).\n/// The action status will be `isDispatchAborted: true`.\n///\n/// You can use it in the `before` method to abort the action before the `reduce` method\n/// is called. That's similar to throwing an `UserException`, but without showing any\n/// errors to the user.\n///\n/// For example, this mixin prevents reentrant actions (you can only call the action if it's not\n/// already running):\n///\n/// ```dart\n/// /// This mixin prevents reentrant actions. You can only call the action if it's not already\n/// /// running. Example: `class LoadInfo extends ReduxAction<AppState> with NonReentrant { ... }`\n/// mixin NonReentrant implements ReduxAction<AppState> {\n///   bool abortDispatch() => isWaiting(runtimeType);\n/// }\n/// ```\n///\n/// See also:\n/// - [ReduxAction.abortDispatch] which is a way to abort the action's dispatch.\n///\nclass AbortDispatchException implements Exception {\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AbortDispatchException && runtimeType == other.runtimeType;\n\n  @override\n  int get hashCode => 0;\n}\n\n/// The [UpdateStateAction] action is used to update the state of the Redux\n/// store, by applying the given [reducerFunction] to the current state.\n///\n/// Note that inside actions you can directly use [ReduxAction.dispatchState]\n/// which is a shortcut to dispatch an [UpdateStateAction].\n///\nclass UpdateStateAction<St> extends ReduxAction<St> {\n  //\n  final St? Function(St) reducerFunction;\n\n  /// When you don't need to use the current state to create the new state, you\n  /// can use the `UpdateStateAction` factory.\n  ///\n  /// Example:\n  /// ```\n  /// var newState = AppState(...);\n  /// store.dispatch(UpdateStateAction(newState));\n  /// ```\n  factory UpdateStateAction(St state) => UpdateStateAction.withReducer((_) => state);\n\n  /// When you need to use the current state to create the new state, you\n  /// can use `UpdateStateAction.withReducer`.\n  ///\n  /// Example:\n  /// ```\n  /// store.dispatch(UpdateStateAction.withReducer((state) => state.copy(...)));\n  /// ```\n  UpdateStateAction.withReducer(this.reducerFunction);\n\n  @override\n  St? reduce() => reducerFunction(state);\n}\n"
  },
  {
    "path": "lib/src/show_dialog_super.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\n\n/// Displays a Material dialog above the current contents of the app, with\n/// Material entrance and exit animations, modal barrier color, and modal\n/// barrier behavior (dialog is dismissible with a tap on the barrier).\n///\n/// This function takes a [builder] which typically builds a [Dialog] widget.\n/// Content below the dialog is dimmed with a [ModalBarrier]. The widget\n/// returned by the [builder] does not share a context with the location that\n/// [showDialogSuper] is originally called from. Use a [StatefulBuilder] or a\n/// custom [StatefulWidget] if the dialog needs to update dynamically.\n///\n/// The [child] argument is deprecated, and should be replaced with [builder].\n///\n/// The [context] argument is used to look up the [Navigator] and [Theme] for\n/// the dialog. It is only used when the method is called. Its corresponding\n/// widget can be safely removed from the tree before the dialog is closed.\n///\n/// The [onDismissed] callback will be called when the dialog is dismissed.\n/// Note: If the dialog is popped by `Navigator.of(context).pop(result)`,\n/// then the `result` will be available to the callback. That way you can\n/// differentiate between the dialog being dismissed by an Ok or a Cancel\n/// button, for example.\n///\n/// The [barrierDismissible] argument is used to indicate whether tapping on the\n/// barrier will dismiss the dialog. It is `true` by default and can not be `null`.\n///\n/// The [barrierColor] argument is used to specify the color of the modal\n/// barrier that darkens everything below the dialog. If `null` the default color\n/// `Colors.black54` is used.\n///\n/// The [useSafeArea] argument is used to indicate if the dialog should only\n/// display in 'safe' areas of the screen not used by the operating system\n/// (see [SafeArea] for more details). It is `true` by default, which means\n/// the dialog will not overlap operating system areas. If it is set to `false`\n/// the dialog will only be constrained by the screen size. It can not be `null`.\n///\n/// The [useRootNavigator] argument is used to determine whether to push the\n/// dialog to the [Navigator] furthest from or nearest to the given [context].\n/// By default, [useRootNavigator] is `true` and the dialog route created by\n/// this method is pushed to the root navigator. It can not be `null`.\n///\n/// The [routeSettings] argument is passed to [showGeneralDialog],\n/// see [RouteSettings] for details.\n///\n/// If the application has multiple [Navigator] objects, it may be necessary to\n/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the\n/// dialog rather than just `Navigator.pop(context, result)`.\n///\n/// Returns a [Future] that resolves to the value (if any) that was passed to\n/// [Navigator.pop] when the dialog was closed.\n///\n/// ### State Restoration in Dialogs\n///\n/// Using this method will not enable state restoration for the dialog. In order\n/// to enable state restoration for a dialog, use [Navigator.restorablePush]\n/// or [Navigator.restorablePushNamed] with [DialogRoute].\n///\n/// For more information about state restoration, see [RestorationManager].\n///\n/// {@tool sample --template=freeform}\n///\n/// This sample demonstrates how to create a restorable Material dialog. This is\n/// accomplished by enabling state restoration by specifying\n/// [MaterialApp.restorationScopeId] and using [Navigator.restorablePush] to\n/// push [DialogRoute] when the button is tapped.\n///\n/// {@macro flutter.widgets.RestorationManager}\n///\n/// ```dart imports\n/// import 'package:flutter/material.dart';\n/// ```\n///\n/// ```dart\n/// void main() {\n///   runApp(MyApp());\n/// }\n///\n/// class MyApp extends StatelessWidget {\n///   @override\n///   Widget build(BuildContext context) {\n///     return MaterialApp(\n///       restorationScopeId: 'app',\n///       title: 'Restorable Routes Demo',\n///       home: MyHomePage(),\n///     );\n///   }\n/// }\n///\n/// class MyHomePage extends StatelessWidget {\n///   static Route<Object?> _dialogBuilder(BuildContext context, Object? arguments) {\n///     return DialogRoute<void>(\n///       context: context,\n///       builder: (BuildContext context) => const AlertDialog(title: Text('Material Alert!')),\n///     );\n///   }\n///\n///   @override\n///   Widget build(BuildContext context) {\n///     return Scaffold(\n///       body: Center(\n///         child: OutlinedButton(\n///           onPressed: () {\n///             Navigator.of(context).restorablePush(_dialogBuilder);\n///           },\n///           child: const Text('Open Dialog'),\n///         ),\n///       ),\n///     );\n///   }\n/// }\n/// ```\n///\n/// {@end-tool}\n///\n/// See also:\n///\n///  * [AlertDialog], for dialogs that have a row of buttons below a body.\n///  * [SimpleDialog], which handles the scrolling of the contents and does\n///    not show buttons below its body.\n///  * [Dialog], on which [SimpleDialog] and [AlertDialog] are based.\n///  * [showCupertinoDialog], which displays an iOS-style dialog.\n///  * [showGeneralDialog], which allows for customization of the dialog popup.\n///  * <https://material.io/design/components/dialogs.html>\nFuture<T?> showDialogSuper<T>({\n  required BuildContext context,\n  required WidgetBuilder builder,\n  bool barrierDismissible = true,\n  Color? barrierColor = Colors.black54,\n  String? barrierLabel,\n  bool useSafeArea = true,\n  bool useRootNavigator = true,\n  RouteSettings? routeSettings,\n  void Function(T?)? onDismissed,\n}) async {\n  T? result = await showDialog<T>(\n    context: context,\n    builder: builder,\n    barrierDismissible: barrierDismissible,\n    barrierColor: barrierColor,\n    barrierLabel: barrierLabel,\n    useSafeArea: useSafeArea,\n    useRootNavigator: useRootNavigator,\n    routeSettings: routeSettings,\n  );\n\n  if (onDismissed != null) onDismissed(result);\n\n  return result;\n}\n\n/// Displays an iOS-style dialog above the current contents of the app, with\n/// iOS-style entrance and exit animations, modal barrier color, and modal\n/// barrier behavior (by default, the dialog is not dismissible with a tap on\n/// the barrier).\n///\n/// This function takes a [builder] which typically builds a [CupertinoAlertDialog]\n/// widget. Content below the dialog is dimmed with a [ModalBarrier]. The widget\n/// returned by the [builder] does not share a context with the location that\n/// [showCupertinoDialogSuper] is originally called from. Use a [StatefulBuilder]\n/// or a custom [StatefulWidget] if the dialog needs to update dynamically.\n///\n/// The [context] argument is used to look up the [Navigator] for the dialog.\n/// It is only used when the method is called. Its corresponding widget can\n/// be safely removed from the tree before the dialog is closed.\n///\n/// The [onDismissed] callback will be called when the dialog is dismissed.\n/// Note: If the dialog is popped by `Navigator.of(context).pop(result)`,\n/// then the `result` will be available to the callback. That way you can\n/// differentiate between the dialog being dismissed by an Ok or a Cancel\n/// button, for example.\n///\n/// The [useRootNavigator] argument is used to determine whether to push the\n/// dialog to the [Navigator] furthest from or nearest to the given `context`.\n/// By default, `useRootNavigator` is `true` and the dialog route created by\n/// this method is pushed to the root navigator.\n///\n/// If the application has multiple [Navigator] objects, it may be necessary to\n/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the\n/// dialog rather than just `Navigator.pop(context, result)`.\n///\n/// Returns a [Future] that resolves to the value (if any) that was passed to\n/// [Navigator.pop] when the dialog was closed.\n///\n/// ### State Restoration in Dialogs\n///\n/// Using this method will not enable state restoration for the dialog. In order\n/// to enable state restoration for a dialog, use [Navigator.restorablePush]\n/// or [Navigator.restorablePushNamed] with [CupertinoDialogRoute].\n///\n/// For more information about state restoration, see [RestorationManager].\n///\n/// {@tool sample --template=stateless_widget_restoration_cupertino}\n///\n/// This sample demonstrates how to create a restorable Cupertino dialog. This is\n/// accomplished by enabling state restoration by specifying\n/// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to\n/// push [CupertinoDialogRoute] when the [CupertinoButton] is tapped.\n///\n/// {@macro flutter.widgets.RestorationManager}\n///\n/// ```dart\n/// Widget build(BuildContext context) {\n///   return CupertinoPageScaffold(\n///     navigationBar: const CupertinoNavigationBar(\n///       middle: Text('Home'),\n///     ),\n///     child: Center(child: CupertinoButton(\n///       onPressed: () {\n///         Navigator.of(context).restorablePush(_dialogBuilder);\n///       },\n///       child: const Text('Open Dialog'),\n///     )),\n///   );\n/// }\n///\n/// static Route<Object?> _dialogBuilder(BuildContext context, Object? arguments) {\n///   return CupertinoDialogRoute<void>(\n///     context: context,\n///     builder: (BuildContext context) {\n///       return const CupertinoAlertDialog(\n///         title: Text('Title'),\n///         content: Text('Content'),\n///         actions: <Widget>[\n///           CupertinoDialogAction(child: Text('Yes')),\n///           CupertinoDialogAction(child: Text('No')),\n///         ],\n///       );\n///     },\n///   );\n/// }\n/// ```\n///\n/// {@end-tool}\n///\n/// See also:\n///\n///  * [CupertinoAlertDialog], an iOS-style alert dialog.\n///  * [showDialog], which displays a Material-style dialog.\n///  * [showGeneralDialog], which allows for customization of the dialog popup.\n///  * <https://developer.apple.com/ios/human-interface-guidelines/views/alerts/>\nFuture<T?> showCupertinoDialogSuper<T>({\n  required BuildContext context,\n  required WidgetBuilder builder,\n  bool barrierDismissible = true,\n  Color? barrierColor = Colors.black54,\n  String? barrierLabel,\n  bool useSafeArea = true,\n  bool useRootNavigator = true,\n  RouteSettings? routeSettings,\n  void Function(T?)? onDismissed,\n}) async {\n  T? result = await showCupertinoDialog<T>(\n    context: context,\n    builder: builder,\n    barrierDismissible: barrierDismissible,\n    barrierLabel: barrierLabel,\n    useRootNavigator: useRootNavigator,\n    routeSettings: routeSettings,\n  );\n\n  if (onDismissed != null) onDismissed(result);\n\n  return result;\n}\n"
  },
  {
    "path": "lib/src/state_observer.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\n\n/// One or more [StateObserver]s can be set during the [Store] creation. Those observers are\n/// called for all dispatched actions, right after the reducer returns. That happens before the\n/// `after()` method is called, and before the action's `wrapError()` and the global `wrapError()`\n/// methods are called.\n///\n/// The parameters are:\n///\n/// * action = The action itself.\n///\n/// * prevState = The state right before the new state returned by the reducer is applied.\n///               Note this may be different from the state when the reducer was called.\n///\n/// * newState = The state returned by the reducer. Note: If you need to know if the state was\n///            changed or not by the reducer, you can compare both states:\n///            `bool ifStateChanged = !identical(prevState, newState);`\n///\n/// * error = Is null if the reducer completed with no error and returned. Otherwise, will be the\n///           error thrown by the reducer (before any wrapError is applied). Note that, in case of\n///           error, both prevState and newState will be the current store state when the error was\n///           thrown.\n///\n/// * dispatchCount = The sequential number of the dispatch.\n///\n/// <br>\n///\n/// Among other uses, the state-observer is a good place to add METRICS to your application.\n/// For example:\n///\n/// ```\n/// abstract class AppAction extends ReduxAction<AppState> {\n///   void trackEvent(AppState prevState, AppState newState) { // Don't to anything }\n/// }\n///\n/// class AppStateObserver implements StateObserver<AppState> {\n///   @override\n///   void observe(\n///     ReduxAction<AppState> action,\n///     AppState prevState,\n///     AppState newState,\n///     Object? error,\n///     int dispatchCount,\n///   ) {\n///     if (action is AppAction) action.trackEvent(prevState, newState, error);\n///   }\n/// }\n///\n/// class MyAction extends AppAction {\n///    @override\n///    AppState? reduce() { // Do something }\n///\n///    @override\n///    void trackEvent(AppState prevState, AppState newState, Object? error) =>\n///       MyMetrics().track(this, newState, error);\n/// }\n///\n/// ```\n///\nabstract class StateObserver<St> {\n  /// * [action] = The action itself.\n  ///\n  /// * [prevState] = The state right before the new state returned by the reducer is applied.\n  ///               Note this may be different from the state when the reducer was called.\n  ///\n  /// * [newState] = The state returned by the reducer. Note: If you need to know if the state was\n  ///              changed or not by the reducer, you can compare both states:\n  ///              `bool ifStateChanged = !identical(prevState, newState);`\n  ///\n  /// * [error] = Is null if the reducer completed with no error and returned. Otherwise, will be the\n  ///           error thrown by the reducer (before any wrapError is applied). Note that, in case of\n  ///           error, both prevState and newState will be the current store state when the error\n  ///           was thrown.\n  ///\n  /// * [dispatchCount] = The sequential number of the dispatch.\n  ///\n  void observe(\n    ReduxAction<St> action,\n    St prevState,\n    St newState,\n    Object? error,\n    int dispatchCount,\n  );\n}\n"
  },
  {
    "path": "lib/src/store.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlibrary async_redux_store;\n\nimport 'dart:async';\nimport 'dart:collection';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:async_redux/src/process_persistence.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'connector_tester.dart';\n\npart 'redux_action.dart';\n\ntypedef Reducer<St> = FutureOr<St?> Function();\n\ntypedef Dispatch<St> = FutureOr<ActionStatus> Function(\n  ReduxAction<St> action, {\n  bool notify,\n});\n\ntypedef DispatchSync<St> = ActionStatus Function(\n  ReduxAction<St> action, {\n  bool notify,\n});\n\n@Deprecated(\"Use `DispatchAndWait` instead. This will be removed.\")\ntypedef DispatchAsync<St> = Future<ActionStatus> Function(\n  ReduxAction<St> action, {\n  bool notify,\n});\n\ntypedef DispatchAndWait<St> = Future<ActionStatus> Function(\n  ReduxAction<St> action, {\n  bool notify,\n});\n\n/// Creates a Redux store that holds the app state.\n///\n/// The only way to change the state in the store is to dispatch a ReduxAction.\n/// You may implement these methods:\n///\n/// 1) `AppState reduce()` ➜\n///    To run synchronously, just return the state:\n///         AppState reduce() { ... return state; }\n///    To run asynchronously, return a future of the state:\n///         Future<AppState> reduce() async { ... return state; }\n///    Note that changing the state is optional. If you return null (or Future of null)\n///    the state will not be changed. Just the same, if you return the same instance\n///    of state (or its Future) the state will not be changed.\n///\n/// 2) `FutureOr<void> before()` ➜ Runs before the reduce method.\n///    If it throws an error, then `reduce` will NOT run.\n///    To run `before` synchronously, just return void:\n///         void before() { ... }\n///    To run asynchronously, return a future of void:\n///         Future<void> before() async { ... }\n///    Note: If this method runs asynchronously, then `reduce` will also be async,\n///    since it must wait for this one to finish.\n///\n/// 3) `void after()` ➜ Runs after `reduce`, even if an error was thrown by\n/// `before` or `reduce` (akin to a \"finally\" block). If the `after` method itself\n/// throws an error, this error will be \"swallowed\" and ignored. Avoid `after`\n/// methods which can throw errors.\n///\n/// 4) `bool abortDispatch()` ➜ If this returns true, the action will not be\n/// dispatched: `before`, `reduce` and `after` will not be called. This is only useful\n/// under rare circumstances, and you should only use it if you know what you are doing.\n///\n/// 5) `Object? wrapError(error)` ➜ If any error is thrown by `before` or `reduce`,\n/// you have the chance to further process it by using `wrapError`. Usually this\n/// is used to wrap the error inside of another that better describes the failed action.\n/// For example, if some action converts a String into a number, then instead of\n/// throwing a FormatException you could do:\n/// `wrapError(error) => UserException(\"Please enter a valid number.\", cause: error)`\n///\n/// ---\n///\n/// • ActionObserver observes the dispatching of actions,\n///   and may be used to print or log the dispatching of actions.\n///\n/// • StateObservers receive the action, prevState (state right before the new State is\n///   applied), newState (state that was applied), and are used to track metrics and more.\n///\n/// • GlobalErrorObserver may be used to observe, modify, and swallow action errors\n///   globally, as well as log them to monitoring services like Sentry and Firebase\n///   Crashlytics.\n///\n/// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n///\nclass Store<St> {\n  Store({\n    required St initialState,\n    Object? environment,\n    Object? Function(Store<St>)? dependencies,\n    Object? Function(Store<St>)? configuration,\n    Map<Object?, Object?> props = const {},\n    bool syncStream = false,\n    TestInfoPrinter? testInfoPrinter,\n    List<ActionObserver<St>>? actionObservers,\n    List<StateObserver<St>>? stateObservers,\n    Persistor<St>? persistor,\n    Persistor<St>? cloudSync,\n    ModelObserver? modelObserver,\n    WrapReduce<St>? wrapReduce,\n    GlobalErrorObserver<St> Function(Store<St>)? globalErrorObserver,\n    GlobalWrapError<St>? globalWrapError,\n    ErrorObserver<St>? errorObserver,\n    bool? defaultDistinct,\n    CompareBy? immutableCollectionEquality,\n    int? maxErrorsQueued,\n  })  : _state = initialState,\n        _environment = environment,\n        _props = HashMap()..addAll(props),\n        _stateTimestamp = DateTime.now().toUtc(),\n        _changeController = StreamController.broadcast(sync: syncStream),\n        _actionObservers = actionObservers,\n        _stateObservers = stateObservers,\n        _processPersistence = (persistor == null)\n            ? null //\n            : ProcessPersistence(persistor, initialState),\n        _processCloudSync = (cloudSync == null)\n            ? null //\n            : ProcessPersistence(cloudSync, initialState),\n        _modelObserver = modelObserver,\n        _globalErrorObserver = globalErrorObserver,\n        //\n        // Deprecated (will be removed): Use globalErrorObserver instead.\n        _errorObserver = errorObserver,\n        _globalWrapError = globalWrapError,\n        //\n        _wrapReduce = wrapReduce,\n        _defaultDistinct = defaultDistinct ?? true,\n        _immutableCollectionEquality = immutableCollectionEquality,\n        _errors = Queue<UserException>(),\n        _maxErrorsQueued = maxErrorsQueued ?? 10,\n        _dispatchCount = 0,\n        _reduceCount = 0,\n        _shutdown = false,\n        _testInfoPrinter = testInfoPrinter,\n        _testInfoController = (testInfoPrinter == null)\n            ? //\n            null\n            : StreamController.broadcast(sync: syncStream) {\n    // Init the config first, so that it can be used by the dependencies.\n    _configuration = configuration?.call(this);\n    \n    // Dependencies can use `store` and `store.configuration`, if necessary.\n    _dependencies = dependencies?.call(this);\n  }\n\n  St _state;\n\n  final Object? _environment;\n  Object? _configuration;\n  Object? _dependencies;\n\n  final Map<Object?, Object?> _props;\n\n  /// Gets the store environment.\n  /// This can be used to specify if the app is running in production, staging,\n  /// development, test, etc, and to save other global values that are not expected to\n  /// change during the app execution.\n  ///\n  /// If you use this, it's recommended that you make this directly accessible in your\n  /// base action that extends [ReduxAction].\n  ///\n  /// Note the environment is accessible in widgets through\n  /// [BuildContextExtensionForProviderAndConnector.getEnvironment], so that your widgets\n  /// can respond to different environments, if necessary.\n  ///\n  /// See also:\n  /// - [prop] and [setProp], for saving global values that may change during app execution.\n  /// - [dependencies], for injecting dependencies (like services, repositories, etc).\n  /// - [configuration], for setting configuration (like feature flags, etc).\n  ///\n  Object? get environment => _environment;\n\n  /// Gets the store dependencies.\n  /// This can be used to create a global value, but scoped to the store, serving\n  /// in practice as dependency injection.\n  ///\n  /// If you use this, it's recommended that you make this directly accessible in your\n  /// base action that extends [ReduxAction].\n  ///\n  /// Usually, this should not be accessible in widgets, as they should not be aware of\n  /// the dependencies.\n  ///\n  /// See also:\n  /// - [prop] and [setProp], for saving global values that may change during app execution.\n  /// - [env], for specifying if the app is running in production, staging, development, etc.\n  /// - [configuration], for setting configuration (like feature flags, etc).\n  ///\n  Object? get dependencies => _dependencies;\n\n  /// Gets the store configuration.\n  /// This can be used to create a global value, but scoped to the store, serving\n  /// in practice as configuration injection. For example, you could save feature flags\n  /// in the configuration.\n  ///\n  /// If you use this, it's recommended that you make this directly accessible in your\n  /// base action that extends [ReduxAction].\n  ///\n  /// See also:\n  /// - [prop] and [setProp], for saving global values that may change during app execution.\n  /// - [dependencies], for injecting dependencies (like services, repositories, etc).\n  /// - [env], for specifying if the app is running in production, staging, development, etc.\n  ///\n  Object? get configuration => _configuration;\n\n  /// Gets the store properties.\n  @visibleForTesting\n  Map<Object?, Object?> get props => _props;\n\n  /// Gets a property from the store.\n  /// This can be used to save global values, but scoped to the store.\n  /// For example, you could save timers, streams or futures used by actions.\n  ///\n  /// ```dart\n  /// setProp(\"timer\", Timer(Duration(seconds: 1), () => print(\"tick\")));\n  /// var timer = prop<Timer>(\"timer\");\n  /// timer.cancel();\n  /// ```\n  ///\n  /// This is also directly accessible in [ReduxAction] and in [VmFactory], as `prop`.\n  ///\n  /// See also: [setProp] and [env].\n  V prop<V>(Object? key) => _props[key] as V;\n\n  /// Sets a property in the store.\n  /// This can be used to save global values, but scoped to the store.\n  /// For example, you could save timers, streams or futures used by actions.\n  ///\n  /// ```dart\n  /// setProp(\"timer\", Timer(Duration(seconds: 1), () => print(\"tick\")));\n  /// var timer = prop<Timer>(\"timer\");\n  /// timer.cancel();\n  /// ```\n  ///\n  /// This is also directly accessible in [ReduxAction] and in [VmFactory], as `prop`.\n  ///\n  /// See also: [prop] and [env].\n  void setProp(Object? key, Object? value) => _props[key] = value;\n\n  /// The [disposeProps] method is used to clean up resources associated with\n  /// the store's properties, by stopping, closing, ignoring and removing timers,\n  /// streams, sinks, and futures that are saved as properties in the store.\n  ///\n  /// In more detail: This method accepts an optional predicate function that\n  /// takes a prop `key` and a `value` as an argument and returns a boolean.\n  ///\n  /// * If you don't provide a predicate function, all properties which are\n  /// `Timer`, `Future`, or `Stream` related will be closed/cancelled/ignored as\n  /// appropriate, and then removed from the props. Other properties will not be\n  /// removed.\n  ///\n  /// * If the predicate function is provided and returns `true` for a given\n  /// property, that property will be removed from the props and, if the property\n  /// is also a `Timer`, `Future`, or `Stream` related, it will be\n  /// closed/cancelled/ignored as appropriate.\n  ///\n  /// * If the predicate function is provided and returns `false` for a given\n  /// property, that property will not be removed from the props, and it will\n  /// not be closed/cancelled/ignored.\n  ///\n  /// This method is particularly useful when the store is being shut down,\n  /// right before or after you called the [shutdown] method.\n  ///\n  /// Example usage:\n  ///\n  /// ```dart\n  /// // Dispose of all Timers, Futures, Stream related etc.\n  /// store.disposeProps();\n  ///\n  /// // Dispose only Timers.\n  /// store.disposeProps(({Object? key, Object? value}) => value is Timer);\n  /// ```\n  ///\n  /// Note: The provided mixins, like [Throttle] and [Debounce] also use some\n  /// props that you can dispose by doing `store.internalMixinProps.clear()`;\n  ///\n  /// See also: [disposeProp], to dispose a single property by its key.\n  ///\n  void disposeProps([bool Function({Object? key, Object? value})? predicate]) {\n    var keysToRemove = [];\n\n    for (var MapEntry(key: key, value: value) in _props.entries) {\n      final removeIt = predicate?.call(key: key, value: value) ?? true;\n\n      if (removeIt) {\n        final ifTimerFutureStream = _closeTimerFutureStream(value);\n\n        // Removes the key if the predicate was provided and returned true,\n        // or it was not provided but the value is Timer/Future/Stream.\n        if ((predicate != null) || ifTimerFutureStream) keysToRemove.add(key);\n      }\n    }\n\n    // After the iteration, remove all keys at the same time.\n    keysToRemove.forEach((key) => _props.remove(key));\n  }\n\n  /// Uses [disposeProps] to dispose and a single property identified by\n  /// its key [keyToDispose], and remove it from the props.\n  ///\n  /// This method will close/cancel/ignore the property if it's a Timer,\n  /// Future, or Stream related object, and then remove it from the props.\n  ///\n  /// Example usage:\n  ///\n  /// ```dart\n  /// // Dispose a specific timer property\n  /// store.disposeProp(\"myTimer\");\n  /// ```\n  void disposeProp(Object? keyToDispose) {\n    disposeProps(({Object? key, Object? value}) => key == keyToDispose);\n  }\n\n  /// If [obj] is a timer, future or stream related, it will be closed/cancelled/ignored,\n  /// and `true` will be returned. For other object types, the method returns `false`.\n  bool _closeTimerFutureStream(Object? obj) {\n    if (obj is Timer)\n      obj.cancel();\n    else if (obj is Future)\n      obj.ignore();\n    else if (obj is StreamSubscription)\n      obj.cancel();\n    else if (obj is StreamConsumer)\n      obj.close();\n    else if (obj is Sink)\n      obj.close();\n    else\n      return false;\n\n    return true;\n  }\n\n  DateTime _stateTimestamp;\n\n  /// The current state of the app.\n  St get state => _state;\n\n  /// The timestamp of the current state in the store, in UTC.\n  DateTime get stateTimestamp => _stateTimestamp;\n\n  bool get defaultDistinct => _defaultDistinct;\n\n  /// 1) If `null` (the default), view-models which are immutable collections will be compared\n  /// by their default equality.\n  ///\n  /// 2) If `CompareBy.byDeepEquals`, view-models which are immutable collections will be compared\n  /// by their items, one by one (potentially slow comparison).\n  ///\n  /// 3) If `CompareBy.byIdentity`, view-models which are immutable collections will be compared\n  /// by their internals being identical (very fast comparison).\n  ///\n  /// Note: This works with immutable collections `IList`, `ISet`, `IMap` and `IMapOfSets` from\n  /// the https://pub.dev/packages/fast_immutable_collections package.\n  ///\n  CompareBy? get immutableCollectionEquality => _immutableCollectionEquality;\n\n  ModelObserver? get modelObserver => _modelObserver;\n\n  int get dispatchCount => _dispatchCount;\n\n  int get reduceCount => _reduceCount;\n\n  final StreamController<St> _changeController;\n\n  final List<ActionObserver>? _actionObservers;\n\n  final List<StateObserver>? _stateObservers;\n\n  final ProcessPersistence<St>? _processPersistence;\n\n  final ProcessPersistence<St>? _processCloudSync;\n\n  final ModelObserver? _modelObserver;\n\n  final ErrorObserver<St>? _errorObserver;\n\n  final GlobalWrapError<St>? _globalWrapError;\n\n  final GlobalErrorObserver<St> Function(Store<St>)? _globalErrorObserver;\n\n  final WrapReduce<St>? _wrapReduce;\n\n  final bool _defaultDistinct;\n\n  final CompareBy? _immutableCollectionEquality;\n\n  final Queue<UserException> _errors;\n\n  /// [UserException]s may be queued to be shown to the user by a\n  /// [UserExceptionDialog] widgets. Usually, if you are not planning on using\n  /// that dialog (or something similar) you should probably not throw\n  /// [UserException]s, so this should not be a problem. Still, to further\n  /// prevent memory problems, there is a maximum number of exceptions the\n  /// queue can hold.\n  final int _maxErrorsQueued;\n\n  bool _shutdown;\n\n  // For testing:\n  int _dispatchCount;\n  int _reduceCount;\n  TestInfoPrinter? _testInfoPrinter;\n  StreamController<TestInfo<St>>? _testInfoController;\n\n  TestInfoPrinter? get testInfoPrinter => _testInfoPrinter;\n\n  /// A stream that emits the current state when it changes.\n  ///\n  /// # Example\n  ///\n  ///     // Create the Store;\n  ///     final store = new Store<int>(initialState: 0);\n  ///\n  ///     // Listen to the Store's onChange stream, and print the latest\n  ///     // state to the console whenever the reducer produces a new state.\n  ///     // Store StreamSubscription as a variable, so you can stop listening later.\n  ///     final subscription = store.onChange.listen(print);\n  ///\n  ///     // Dispatch some actions, which prints the state.\n  ///     store.dispatch(IncrementAction());\n  ///\n  ///     // When you want to stop printing, cancel the subscription.\n  ///     subscription.cancel();\n  ///\n  Stream<St> get onChange => _changeController.stream;\n\n  /// Used by the storeTester.\n  Stream<TestInfo<St>> get onReduce => (_testInfoController != null)\n      ? //\n      _testInfoController!.stream\n      : Stream<TestInfo<St>>.empty();\n\n  /// Pause the [Persistor] temporarily.\n  ///\n  /// When [pausePersistor] is called, the Persistor will not start a new persistence process,\n  /// until method [resumePersistor] is called. This will not affect the current persistence\n  /// process, if one is currently running.\n  ///\n  /// Note: A persistence process starts when the [Persistor.persistDifference] method is called,\n  /// and finishes when the future returned by that method completes.\n  ///\n  void pausePersistor() {\n    _processPersistence?.pause();\n  }\n\n  /// Pause the [CloudSync] temporarily.\n  ///\n  /// When [pauseCloudSync] is called, the cloud sync will not start a new persistence process,\n  /// until method [resumeCloudSync] is called. This will not affect the current persistence\n  /// process, if one is currently running.\n  ///\n  /// Note: A cloud sync process starts when the [CloudSync.persistDifference] method is called,\n  /// and finishes when the future returned by that method completes.\n  ///\n  void pauseCloudSync() {\n    _processCloudSync?.pause();\n  }\n\n  /// Persists the current state (if it's not yet persisted), then pauses the [Persistor]\n  /// temporarily.\n  ///\n  /// When [persistAndPausePersistor] is called, this will not affect the current persistence\n  /// process, if one is currently running. If no persistence process was running, it will\n  /// immediately start a new persistence process (ignoring [Persistor.throttle]).\n  ///\n  /// Then, the Persistor will not start another persistence process, until method\n  /// [resumePersistor] is called.\n  ///\n  /// Note: A persistence process starts when the [Persistor.persistDifference] method is called,\n  /// and finishes when the future returned by that method completes.\n  ///\n  void persistAndPausePersistor() {\n    _processPersistence?.persistAndPause();\n  }\n\n  /// Saves the current state (if it's not yet saved) to the cloud, then pauses\n  /// the [CloudSync] temporarily.\n  ///\n  /// When [persistAndPauseCloudSync] is called, this will not affect the current cloud save\n  /// process, if one is currently running. If no cloud save process was running, it will\n  /// immediately start a new save process (ignoring [CloudSync.throttle]).\n  ///\n  /// Then, the CloudSync will not start another cloud save process, until method\n  /// [resumeCloudSync] is called.\n  ///\n  /// Note: A cloud save process starts when the [CloudSync.persistDifference] method is called,\n  /// and finishes when the future returned by that method completes.\n  ///\n  void persistAndPauseCloudSync() {\n    _processCloudSync?.persistAndPause();\n  }\n\n  /// Resumes persistence by the [Persistor],\n  /// after calling [pausePersistor] or [persistAndPausePersistor].\n  void resumePersistor() {\n    _processPersistence?.resume();\n  }\n\n  /// Resumes persistence by the [CloudSync],\n  /// after calling [pauseCloudSync] or [persistAndPauseCloudSync].\n  void resumeCloudSync() {\n    _processCloudSync?.resume();\n  }\n\n  /// Asks the [Persistor] to save the [initialState] in the local persistence.\n  Future<void> saveInitialStateInPersistence(St initialState) async =>\n      _processPersistence?.saveInitialState(initialState);\n\n  /// Asks the [CloudSync] to save the [initialState] in the cloud.\n  Future<void> saveInitialStateInCloud(St initialState) async =>\n      _processCloudSync?.saveInitialState(initialState);\n\n  /// Asks the [Persistor] to read the state from the local persistence.\n  /// Important: If you use this, you MUST put this state into the store.\n  /// The Persistor will assume that's the case, and will not work properly otherwise.\n  Future<St?> readStateFromPersistence() async => _processPersistence?.readState();\n\n  /// Asks the [CloudSync] to read the state from the cloud.\n  /// Important: If you use this, you MUST put this state into the store.\n  /// The CloudSync will assume that's the case, and will not work properly otherwise.\n  Future<St?> readStateFromCloudSync() async => _processCloudSync?.readState();\n\n  /// Asks the [Persistor] to delete the saved state from the cloud.\n  Future<void> deleteStateFromPersistence() async => _processPersistence?.deleteState();\n\n  /// Asks the [CloudSync] to delete the saved state from the cloud.\n  Future<void> deleteStateFromCloud() async => _processCloudSync?.deleteState();\n\n  /// Gets, from the [Persistor], the last state that was saved to the local persistence.\n  St? getLastPersistedStateFromPersistor() => _processPersistence?.lastPersistedState;\n\n  /// Gets, from the [CloudSync], the last state that was saved to the cloud.\n  St? getLastPersistedStateFromCloudSync() => _processCloudSync?.lastPersistedState;\n\n  /// Turns on testing capabilities, if not already.\n  void initTestInfoController() {\n    _testInfoController ??= StreamController.broadcast(sync: false);\n  }\n\n  /// Changes the testInfoPrinter.\n  void initTestInfoPrinter(TestInfoPrinter testInfoPrinter) {\n    _testInfoPrinter = testInfoPrinter;\n    initTestInfoController();\n  }\n\n  /// Beware: Changes the state directly. Use only for TESTS.\n  /// This will not notify the listeners nor complete wait conditions.\n  void defineState(St state) {\n    _state = state;\n    _stateTimestamp = DateTime.now().toUtc();\n  }\n\n  /// The global default timeout for the wait functions like [waitCondition] etc\n  /// is 10 minutes. This value is not final and can be modified.\n  /// To disable the timeout, make it -1.\n  static int defaultTimeoutMillis = 60 * 1000 * 10;\n\n  /// Returns a future which will complete when the given state [condition] is true.\n  ///\n  /// If [completeImmediately] is `true` (the default) and the condition was already true when\n  /// the method was called, the future will complete immediately and throw no errors.\n  ///\n  /// If [completeImmediately] is `false` and the condition was already true when\n  /// the method was called, it will throw a [StoreException].\n  ///\n  /// Note: The default here is `true`, while in the other `wait` methods\n  /// like [waitActionCondition] it's `false`. This makes sense because of\n  /// the different use cases for these methods.\n  ///\n  /// You may also provide a [timeoutMillis], which by default is 10 minutes.\n  /// To disable the timeout, make it -1.\n  /// If you want, you can modify [defaultTimeoutMillis] to change the default timeout.\n  ///\n  /// This method is useful in tests, and it returns the action which changed\n  /// the store state into the condition, in case you need it:\n  ///\n  /// ```dart\n  /// var action = await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(action, isA<ChangeNameAction>());\n  /// ```\n  ///\n  /// This method is also eventually useful in production code, in which case you\n  /// should avoid waiting for conditions that may take a very long time to complete,\n  /// as checking the condition is an overhead to every state change.\n  ///\n  /// Examples:\n  ///\n  /// ```ts\n  /// // Dispatches an actions that changes the state, then await for the state change:\n  /// expect(store.state.name, 'John')\n  /// dispatch(ChangeNameAction(\"Bill\"));\n  /// var action = await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Dispatches actions and wait until no actions are in progress.\n  /// dispatch(BuyStock('IBM'));\n  /// dispatch(BuyStock('TSLA'));\n  /// await waitAllActions([]);\n  /// expect(state.stocks, ['IBM', 'TSLA']);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for their TYPES:\n  /// expect(store.state.portfolio, ['TSLA']);\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(SellAction('TSLA'));\n  /// await store.waitAllActionTypes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio, ['IBM']);\n  ///\n  /// // Dispatches actions in PARALLEL and wait until no actions are in progress.\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(BuyAction('TSLA'));\n  /// await store.waitAllActions([]);\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for them:\n  /// let action1 = BuyAction('IBM');\n  /// let action2 = SellAction('TSLA');\n  /// dispatch(action1);\n  /// dispatch(action2);\n  /// await store.waitAllActions([action1, action2]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// expect(store.state.portfolio.contains('TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in SERIES and wait for them:\n  /// await dispatchAndWait(BuyAction('IBM'));\n  /// await dispatchAndWait(SellAction('TSLA'));\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Wait until some action of a given type is dispatched.\n  /// dispatch(DoALotOfStuffAction());\n  /// var action = store.waitActionType(ChangeNameAction);\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(action.status.isCompleteOk, isTrue);\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Wait until some action of the given types is dispatched.\n  /// dispatch(ProcessStocksAction());\n  /// var action = store.waitAnyActionTypeFinishes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// ```\n  ///\n  /// See also:\n  /// [waitCondition] - Waits until the state is in a given condition.\n  /// [waitActionCondition] - Waits until the actions in progress meet a given condition.\n  /// [waitAllActions] - Waits until the given actions are NOT in progress, or no actions are in progress.\n  /// [waitActionType] - Waits until an action of a given type is NOT in progress.\n  /// [waitAllActionTypes] - Waits until all actions of the given type are NOT in progress.\n  /// [waitAnyActionTypeFinishes] - Waits until ANY action of the given types finish dispatching.\n  ///\n  Future<ReduxAction<St>?> waitCondition(\n    bool Function(St) condition, {\n    //\n    /// If `completeImmediately` is `true` (the default) and the condition was already true when\n    /// the method was called, the future will complete immediately and throw no errors.\n    ///\n    /// If `completeImmediately` is `false` and the condition was already true when\n    /// the method was called, it will throw a [StoreException].\n    ///\n    /// Note: The default here is `true`, while in the other `wait` methods\n    /// like [waitActionCondition] it's `false`. This makes sense because of\n    /// the different use cases for these methods.\n    bool completeImmediately = true,\n    //\n    /// The maximum time to wait for the condition to be met. The default is 10 minutes.\n    /// To disable the timeout, make it -1.\n    int? timeoutMillis,\n  }) async {\n    //\n    // If the condition is already true when `waitCondition` is called.\n    if (condition(_state)) {\n      // Complete and return null (no trigger action).\n      if (completeImmediately)\n        return Future.value(null);\n      // else throw an error.\n      else\n        throw StoreException(\"Awaited state condition was already true, \"\n            \"and the future completed immediately.\");\n    }\n    //\n    else {\n      var completer = Completer<ReduxAction<St>?>();\n\n      _stateConditionCompleters[condition] = completer;\n\n      int timeout = timeoutMillis ?? defaultTimeoutMillis;\n      var future = completer.future;\n\n      if (timeout >= 0)\n        future = completer.future.timeout(\n          Duration(milliseconds: timeout),\n          onTimeout: () {\n            _stateConditionCompleters.remove(condition);\n            throw TimeoutException(null, Duration(milliseconds: timeout));\n          },\n        );\n\n      return future;\n    }\n  }\n\n  // This map will hold the completers for each ACTION condition checker function.\n  // 1) The set key is the condition checker function.\n  // 2) The value is the completer, that informs of:\n  //    - The set of actions in progress when the condition is met.\n  //    - The action that triggered the condition.\n  final _actionConditionCompleters = <bool Function(\n          Set<ReduxAction<St>>, ReduxAction<St>?),\n      Completer<(Set<ReduxAction<St>>, ReduxAction<St>?)>>{};\n\n  // This map will hold the completers for each STATE condition checker function.\n  // 1) The set key is the condition checker function.\n  // 2) The value is the completer, that informs the action that triggered the condition.\n  final _stateConditionCompleters = <bool Function(St), Completer<ReduxAction<St>?>>{};\n\n  /// Returns a future that completes when some actions meet the given [condition].\n  ///\n  /// If [completeImmediately] is `false` (the default), this method will throw [StoreException]\n  /// if the condition was already true when the method was called. Otherwise, the future will\n  /// complete immediately and throw no error.\n  ///\n  /// The [condition] is a function that takes the set of actions \"in progress\", as well as an\n  /// action that just entered the set (by being dispatched) or left the set (by finishing\n  /// dispatching). The function should return `true` when the condition is met, and `false`\n  /// otherwise. For example:\n  ///\n  /// ```dart\n  /// var action = await store.waitActionCondition((actionsInProgress, triggerAction) { ... }\n  /// ```\n  ///\n  /// You get back an unmodifiable set of the actions being dispatched that met the condition,\n  /// as well as the action that triggered the condition by being added or removed from the set.\n  ///\n  /// Note: The condition is only checked when some action is dispatched or finishes dispatching.\n  /// It's not checked every time action statuses change.\n  ///\n  /// You may also provide a [timeoutMillis], which by default is 10 minutes.\n  /// To disable the timeout, make it -1.\n  /// If you want, you can modify [defaultTimeoutMillis] to change the default timeout.\n  ///\n  /// Examples:\n  ///\n  /// ```ts\n  /// // Dispatches an actions that changes the state, then await for the state change:\n  /// expect(store.state.name, 'John')\n  /// dispatch(ChangeNameAction(\"Bill\"));\n  /// var action = await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Dispatches actions and wait until no actions are in progress.\n  /// dispatch(BuyStock('IBM'));\n  /// dispatch(BuyStock('TSLA'));\n  /// await waitAllActions([]);\n  /// expect(state.stocks, ['IBM', 'TSLA']);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for their TYPES:\n  /// expect(store.state.portfolio, ['TSLA']);\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(SellAction('TSLA'));\n  /// await store.waitAllActionTypes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio, ['IBM']);\n  ///\n  /// // Dispatches actions in PARALLEL and wait until no actions are in progress.\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(BuyAction('TSLA'));\n  /// await store.waitAllActions([]);\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for them:\n  /// let action1 = BuyAction('IBM');\n  /// let action2 = SellAction('TSLA');\n  /// dispatch(action1);\n  /// dispatch(action2);\n  /// await store.waitAllActions([action1, action2]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// expect(store.state.portfolio.contains('TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in SERIES and wait for them:\n  /// await dispatchAndWait(BuyAction('IBM'));\n  /// await dispatchAndWait(SellAction('TSLA'));\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Wait until some action of a given type is dispatched.\n  /// dispatch(DoALotOfStuffAction());\n  /// var action = store.waitActionType(ChangeNameAction);\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(action.status.isCompleteOk, isTrue);\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Wait until some action of the given types is dispatched.\n  /// dispatch(ProcessStocksAction());\n  /// var action = store.waitAnyActionTypeFinishes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// ```\n  ///\n  /// See also:\n  /// [waitCondition] - Waits until the state is in a given condition.\n  /// [waitActionCondition] - Waits until the actions in progress meet a given condition.\n  /// [waitAllActions] - Waits until the given actions are NOT in progress, or no actions are in progress.\n  /// [waitActionType] - Waits until an action of a given type is NOT in progress.\n  /// [waitAllActionTypes] - Waits until all actions of the given type are NOT in progress.\n  /// [waitAnyActionTypeFinishes] - Waits until ANY action of the given types finish dispatching.\n  ///\n  /// You should only use this method in tests.\n  @visibleForTesting\n  Future<(Set<ReduxAction<St>>, ReduxAction<St>?)> waitActionCondition(\n    //\n    //\n    /// The condition receives the current actions in progress, and the action that triggered the condition.\n    bool Function(Set<ReduxAction<St>> actions, ReduxAction<St>? triggerAction)\n        condition, {\n    //\n    /// If `completeImmediately` is `false` (the default), this method will throw an error if the\n    /// condition is already true when the method is called. Otherwise, the future will complete\n    /// immediately and throw no error.\n    bool completeImmediately = false,\n    //\n    /// Error message in case the condition was already true when the method was called,\n    /// and `completeImmediately` is false.\n    String completedErrorMessage = \"Awaited action condition was already true\",\n    //\n    /// The maximum time to wait for the condition to be met. The default is 10 minutes.\n    /// To disable the timeout, make it -1.\n    int? timeoutMillis,\n  }) {\n    //\n    // If the condition is already true when `waitActionCondition` is called.\n    if (condition(actionsInProgress(), null)) {\n      // Complete and return the actions in progress and the trigger action.\n      if (completeImmediately)\n        return Future.value((actionsInProgress(), null));\n      // else throw an error.\n      else\n        throw StoreException(\n            completedErrorMessage + \", and the future completed immediately.\");\n    }\n    //\n    else {\n      var completer = Completer<(Set<ReduxAction<St>>, ReduxAction<St>?)>();\n\n      _actionConditionCompleters[condition] = completer;\n\n      int timeout = timeoutMillis ?? defaultTimeoutMillis;\n      var future = completer.future;\n\n      if (timeout >= 0)\n        future = completer.future.timeout(\n          Duration(milliseconds: timeout),\n          onTimeout: () {\n            _actionConditionCompleters.remove(condition);\n            throw TimeoutException(null, Duration(milliseconds: timeout));\n          },\n        );\n\n      return future;\n    }\n  }\n\n  /// Returns a future that completes when ALL given [actions] finish dispatching.\n  ///\n  /// If [completeImmediately] is `false` (the default), this method will throw [StoreException]\n  /// if none of the given actions are in progress when the method is called. Otherwise, the future\n  /// will complete immediately and throw no error.\n  ///\n  /// However, if you don't provide any actions (empty list or `null`), the future will complete\n  /// when ALL current actions in progress finish dispatching. In other words, when no actions are\n  /// currently in progress. In this case, if [completeImmediately] is `false`, the method will\n  /// throw an error if no actions are in progress when the method is called.\n  ///\n  /// Note: Waiting until no actions are in progress should only be done in test, never in\n  /// production, as it's very easy to create a deadlock. However, waiting for specific actions to\n  /// finish is safe in production, as long as you're waiting for actions you just dispatched.\n  ///\n  /// You may also provide a [timeoutMillis], which by default is 10 minutes.\n  /// To disable the timeout, make it -1.\n  /// If you want, you can modify [defaultTimeoutMillis] to change the default timeout.\n  ///\n  /// Examples:\n  ///\n  /// ```ts\n  /// // Dispatches an actions that changes the state, then await for the state change:\n  /// expect(store.state.name, 'John')\n  /// dispatch(ChangeNameAction(\"Bill\"));\n  /// var action = await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Dispatches actions and wait until no actions are in progress.\n  /// dispatch(BuyStock('IBM'));\n  /// dispatch(BuyStock('TSLA'));\n  /// await waitAllActions([]);\n  /// expect(state.stocks, ['IBM', 'TSLA']);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for their TYPES:\n  /// expect(store.state.portfolio, ['TSLA']);\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(SellAction('TSLA'));\n  /// await store.waitAllActionTypes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio, ['IBM']);\n  ///\n  /// // Dispatches actions in PARALLEL and wait until no actions are in progress.\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(BuyAction('TSLA'));\n  /// await store.waitAllActions([]);\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for them:\n  /// let action1 = BuyAction('IBM');\n  /// let action2 = SellAction('TSLA');\n  /// dispatch(action1);\n  /// dispatch(action2);\n  /// await store.waitAllActions([action1, action2]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// expect(store.state.portfolio.contains('TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in SERIES and wait for them:\n  /// await dispatchAndWait(BuyAction('IBM'));\n  /// await dispatchAndWait(SellAction('TSLA'));\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Wait until some action of a given type is dispatched.\n  /// dispatch(DoALotOfStuffAction());\n  /// var action = store.waitActionType(ChangeNameAction);\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(action.status.isCompleteOk, isTrue);\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Wait until some action of the given types is dispatched.\n  /// dispatch(ProcessStocksAction());\n  /// var action = store.waitAnyActionTypeFinishes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// ```\n  ///\n  /// See also:\n  /// [waitCondition] - Waits until the state is in a given condition.\n  /// [waitActionCondition] - Waits until the actions in progress meet a given condition.\n  /// [waitAllActions] - Waits until the given actions are NOT in progress, or no actions are in progress.\n  /// [waitActionType] - Waits until an action of a given type is NOT in progress.\n  /// [waitAllActionTypes] - Waits until all actions of the given type are NOT in progress.\n  /// [waitAnyActionTypeFinishes] - Waits until ANY action of the given types finish dispatching.\n  ///\n  Future<void> waitAllActions(\n    List<ReduxAction<St>>? actions, {\n    bool completeImmediately = false,\n    int? timeoutMillis,\n  }) {\n    if (actions == null || actions.isEmpty) {\n      return waitActionCondition(\n          completeImmediately: completeImmediately,\n          completedErrorMessage: \"No actions were in progress\",\n          timeoutMillis: timeoutMillis,\n          (actions, triggerAction) => actions.isEmpty);\n    } else {\n      return waitActionCondition(\n        completeImmediately: completeImmediately,\n        completedErrorMessage: \"None of the given actions were in progress\",\n        timeoutMillis: timeoutMillis,\n        //\n        (actionsInProgress, triggerAction) {\n          for (var action in actions) {\n            if (actionsInProgress.contains(action)) return false;\n          }\n          return true;\n        },\n      );\n    }\n  }\n\n  /// Returns a future that completes when an action of the given type in NOT in progress\n  /// (it's not being dispatched):\n  ///\n  /// - If NO action of the given type is currently in progress when the method is called,\n  ///   and [completeImmediately] is `false` (the default), this method will throw an error.\n  ///\n  /// - If NO action of the given type is currently in progress when the method is called,\n  ///   and [completeImmediately] is `true`, the future completes immediately, returns `null`,\n  ///   and throws no error.\n  ///\n  /// - If an action of the given type is in progress, the future completes when the action\n  ///   finishes, and returns the action. You can use the returned action to check its `status`:\n  ///\n  ///   ```dart\n  ///   var action = await store.waitActionType(MyAction);\n  ///   expect(action.status.originalError, isA<UserException>());\n  ///   ```\n  ///\n  /// You may also provide a [timeoutMillis], which by default is 10 minutes.\n  /// To disable the timeout, make it -1.\n  /// If you want, you can modify [defaultTimeoutMillis] to change the default timeout.\n  ///\n  /// Examples:\n  ///\n  /// ```ts\n  /// // Dispatches an actions that changes the state, then await for the state change:\n  /// expect(store.state.name, 'John')\n  /// dispatch(ChangeNameAction(\"Bill\"));\n  /// var action = await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Dispatches actions and wait until no actions are in progress.\n  /// dispatch(BuyStock('IBM'));\n  /// dispatch(BuyStock('TSLA'));\n  /// await waitAllActions([]);\n  /// expect(state.stocks, ['IBM', 'TSLA']);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for their TYPES:\n  /// expect(store.state.portfolio, ['TSLA']);\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(SellAction('TSLA'));\n  /// await store.waitAllActionTypes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio, ['IBM']);\n  ///\n  /// // Dispatches actions in PARALLEL and wait until no actions are in progress.\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(BuyAction('TSLA'));\n  /// await store.waitAllActions([]);\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for them:\n  /// let action1 = BuyAction('IBM');\n  /// let action2 = SellAction('TSLA');\n  /// dispatch(action1);\n  /// dispatch(action2);\n  /// await store.waitAllActions([action1, action2]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// expect(store.state.portfolio.contains('TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in SERIES and wait for them:\n  /// await dispatchAndWait(BuyAction('IBM'));\n  /// await dispatchAndWait(SellAction('TSLA'));\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Wait until some action of a given type is dispatched.\n  /// dispatch(DoALotOfStuffAction());\n  /// var action = store.waitActionType(ChangeNameAction);\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(action.status.isCompleteOk, isTrue);\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Wait until some action of the given types is dispatched.\n  /// dispatch(ProcessStocksAction());\n  /// var action = store.waitAnyActionTypeFinishes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// ```\n  ///\n  /// See also:\n  /// [waitCondition] - Waits until the state is in a given condition.\n  /// [waitActionCondition] - Waits until the actions in progress meet a given condition.\n  /// [waitAllActions] - Waits until the given actions are NOT in progress, or no actions are in progress.\n  /// [waitActionType] - Waits until an action of a given type is NOT in progress.\n  /// [waitAllActionTypes] - Waits until all actions of the given type are NOT in progress.\n  /// [waitAnyActionTypeFinishes] - Waits until ANY action of the given types finish dispatching.\n  ///\n  /// You should only use this method in tests.\n  @visibleForTesting\n  Future<ReduxAction<St>?> waitActionType(\n    Type actionType, {\n    bool completeImmediately = false,\n    int? timeoutMillis,\n  }) async {\n    var (_, triggerAction) = await waitActionCondition(\n      completeImmediately: completeImmediately,\n      completedErrorMessage: \"No action of the given type was in progress\",\n      timeoutMillis: timeoutMillis,\n      //\n      (actionsInProgress, triggerAction) {\n        return !actionsInProgress.any((action) => action.runtimeType == actionType);\n      },\n    );\n\n    return triggerAction;\n  }\n\n  /// Returns a future that completes when ALL actions of the given types are NOT in progress\n  /// (none of them are being dispatched):\n  ///\n  /// - If NO action of the given types is currently in progress when the method is called,\n  ///   and [completeImmediately] is `false` (the default), this method will throw an error.\n  ///\n  /// - If NO action of the given type is currently in progress when the method is called,\n  ///   and [completeImmediately] is `true`, the future completes immediately and throws no error.\n  ///\n  /// - If any action of the given types is in progress, the future completes only when\n  ///   no action of the given types is in progress anymore.\n  ///\n  /// You may also provide a [timeoutMillis], which by default is 10 minutes.\n  /// To disable the timeout, make it -1.\n  /// If you want, you can modify [defaultTimeoutMillis] to change the default timeout.\n  ///\n  /// Examples:\n  ///\n  /// ```ts\n  /// // Dispatches an actions that changes the state, then await for the state change:\n  /// expect(store.state.name, 'John')\n  /// dispatch(ChangeNameAction(\"Bill\"));\n  /// var action = await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Dispatches actions and wait until no actions are in progress.\n  /// dispatch(BuyStock('IBM'));\n  /// dispatch(BuyStock('TSLA'));\n  /// await waitAllActions([]);\n  /// expect(state.stocks, ['IBM', 'TSLA']);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for their TYPES:\n  /// expect(store.state.portfolio, ['TSLA']);\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(SellAction('TSLA'));\n  /// await store.waitAllActionTypes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio, ['IBM']);\n  ///\n  /// // Dispatches actions in PARALLEL and wait until no actions are in progress.\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(BuyAction('TSLA'));\n  /// await store.waitAllActions([]);\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for them:\n  /// let action1 = BuyAction('IBM');\n  /// let action2 = SellAction('TSLA');\n  /// dispatch(action1);\n  /// dispatch(action2);\n  /// await store.waitAllActions([action1, action2]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// expect(store.state.portfolio.contains('TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in SERIES and wait for them:\n  /// await dispatchAndWait(BuyAction('IBM'));\n  /// await dispatchAndWait(SellAction('TSLA'));\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Wait until some action of a given type is dispatched.\n  /// dispatch(DoALotOfStuffAction());\n  /// var action = store.waitActionType(ChangeNameAction);\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(action.status.isCompleteOk, isTrue);\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Wait until some action of the given types is dispatched.\n  /// dispatch(ProcessStocksAction());\n  /// var action = store.waitAnyActionTypeFinishes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// ```\n  ///\n  /// See also:\n  /// [waitCondition] - Waits until the state is in a given condition.\n  /// [waitActionCondition] - Waits until the actions in progress meet a given condition.\n  /// [waitAllActions] - Waits until the given actions are NOT in progress, or no actions are in progress.\n  /// [waitActionType] - Waits until an action of a given type is NOT in progress.\n  /// [waitAllActionTypes] - Waits until all actions of the given type are NOT in progress.\n  /// [waitAnyActionTypeFinishes] - Waits until ANY action of the given types finish dispatching.\n  ///\n  /// You should only use this method in tests.\n  @visibleForTesting\n  Future<void> waitAllActionTypes(\n    List<Type> actionTypes, {\n    bool completeImmediately = false,\n    int? timeoutMillis,\n  }) async {\n    if (actionTypes.isEmpty) {\n      await waitActionCondition(\n        completeImmediately: completeImmediately,\n        completedErrorMessage: \"No actions are in progress\",\n        timeoutMillis: timeoutMillis,\n        (actions, triggerAction) => actions.isEmpty,\n      );\n    } else {\n      await waitActionCondition(\n        completeImmediately: completeImmediately,\n        completedErrorMessage: \"No action of the given types was in progress\",\n        timeoutMillis: timeoutMillis,\n        //\n        (actionsInProgress, triggerAction) {\n          for (var actionType in actionTypes) {\n            if (actionsInProgress.any((action) => action.runtimeType == actionType))\n              return false;\n          }\n          return true;\n        },\n      );\n    }\n  }\n\n  /// Returns a future which will complete when ANY action of the given types FINISHES\n  /// dispatching. IMPORTANT: This method is different from the other similar methods, because\n  /// it does NOT complete immediately if no action of the given types is in progress. Instead,\n  /// it waits until an action of the given types finishes dispatching, even if they\n  /// were not yet in progress when the method was called.\n  ///\n  /// This method returns the action that completed the future, which you can use to check\n  /// its `status`.\n  ///\n  /// It's useful when the actions you are waiting for are not yet dispatched when you call this\n  /// method. For example, suppose action `StartAction` starts a process that takes some time\n  /// to run and then dispatches an action called `MyFinalAction`. You can then write:\n  ///\n  /// ```dart\n  /// dispatch(StartAction());\n  /// var action = await store.waitAnyActionTypeFinishes([MyFinalAction]);\n  /// expect(action.status.originalError, isA<UserException>());\n  /// ```\n  ///\n  /// You may also provide a [timeoutMillis], which by default is 10 minutes.\n  /// To disable the timeout, make it -1.\n  /// If you want, you can modify [defaultTimeoutMillis] to change the default timeout.\n  ///\n  /// Examples:\n  ///\n  /// ```ts\n  /// // Dispatches an actions that changes the state, then await for the state change:\n  /// expect(store.state.name, 'John')\n  /// dispatch(ChangeNameAction(\"Bill\"));\n  /// var action = await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Dispatches actions and wait until no actions are in progress.\n  /// dispatch(BuyStock('IBM'));\n  /// dispatch(BuyStock('TSLA'));\n  /// await waitAllActions([]);\n  /// expect(state.stocks, ['IBM', 'TSLA']);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for their TYPES:\n  /// expect(store.state.portfolio, ['TSLA']);\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(SellAction('TSLA'));\n  /// await store.waitAllActionTypes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio, ['IBM']);\n  ///\n  /// // Dispatches actions in PARALLEL and wait until no actions are in progress.\n  /// dispatch(BuyAction('IBM'));\n  /// dispatch(BuyAction('TSLA'));\n  /// await store.waitAllActions([]);\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in PARALLEL and wait for them:\n  /// let action1 = BuyAction('IBM');\n  /// let action2 = SellAction('TSLA');\n  /// dispatch(action1);\n  /// dispatch(action2);\n  /// await store.waitAllActions([action1, action2]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// expect(store.state.portfolio.contains('TSLA'), isFalse);\n  ///\n  /// // Dispatches two actions in SERIES and wait for them:\n  /// await dispatchAndWait(BuyAction('IBM'));\n  /// await dispatchAndWait(SellAction('TSLA'));\n  /// expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);\n  ///\n  /// // Wait until some action of a given type is dispatched.\n  /// dispatch(DoALotOfStuffAction());\n  /// var action = store.waitActionType(ChangeNameAction);\n  /// expect(action, isA<ChangeNameAction>());\n  /// expect(action.status.isCompleteOk, isTrue);\n  /// expect(store.state.name, 'Bill');\n  ///\n  /// // Wait until some action of the given types is dispatched.\n  /// dispatch(ProcessStocksAction());\n  /// var action = store.waitAnyActionTypeFinishes([BuyAction, SellAction]);\n  /// expect(store.state.portfolio.contains('IBM'), isTrue);\n  /// ```\n  ///\n  /// See also:\n  /// [waitCondition] - Waits until the state is in a given condition.\n  /// [waitActionCondition] - Waits until the actions in progress meet a given condition.\n  /// [waitAllActions] - Waits until the given actions are NOT in progress, or no actions are in progress.\n  /// [waitActionType] - Waits until an action of a given type is NOT in progress.\n  /// [waitAllActionTypes] - Waits until all actions of the given type are NOT in progress.\n  /// [waitAnyActionTypeFinishes] - Waits until ANY action of the given types finish dispatching.\n  ///\n  /// You should only use this method in tests.\n  @visibleForTesting\n  Future<ReduxAction<St>> waitAnyActionTypeFinishes(\n    List<Type> actionTypes, {\n    int? timeoutMillis,\n  }) async {\n    var (_, triggerAction) = await waitActionCondition(\n      completedErrorMessage: \"Assertion error\",\n      timeoutMillis: timeoutMillis,\n      //\n      (actionsInProgress, triggerAction) {\n        //\n        // If the triggerAction is one of the actionTypes,\n        if ((triggerAction != null) && actionTypes.contains(triggerAction.runtimeType)) {\n          // If the actions in progress do not contain the triggerAction, then the triggerAction has finished.\n          // Otherwise, the triggerAction has just been dispatched, which is not what we want.\n          bool isFinished = !actionsInProgress.contains(triggerAction);\n          return isFinished;\n        }\n        return false;\n      },\n    );\n\n    // Always non-null, because the condition is only met when an action finishes.\n    return triggerAction!;\n  }\n\n  /// Adds an error at the end of the error queue.\n  void _addError(UserException error) {\n    if (_errors.length > _maxErrorsQueued) _errors.removeFirst();\n    _errors.addLast(error);\n  }\n\n  /// Gets the first error from the error queue, and removes it from the queue.\n  UserException? getAndRemoveFirstError() => //\n      (_errors.isEmpty) //\n          ? null\n          : _errors.removeFirst();\n\n  /// Remove an error from the error queue, if it's in the queue.\n  /// Pass it a [source]:\n  /// - A [UserException] object, to remove that error from the queue.\n  /// - An [ActionStatus] object, to remove the error that caused the action to fail.\n  /// - An action ([ReduxAction]), to remove the error that caused the action to fail.\n  ///\n  /// Do nothing if:\n  /// - The error that caused the action to fail is not in the queue.\n  /// - The action did not fail.\n  /// - The status has no [ActionStatus.wrappedError].\n  /// - The [source] is not a [UserException], [ActionStatus], or [ReduxAction].\n  ///\n  /// This is sometimes useful in tests. For example:\n  ///\n  /// ```dart\n  /// // Dispatch some action\n  /// var status = await store.dispatchAndWait(SomeAction());\n  ///\n  /// // Check the action failed as expected\n  /// expect(status.originalError, isError<CloudException>('Insufficient balance.'));\n  ///\n  /// // Make sure there are no more errors\n  /// store.removeError(status);\n  /// expect(store.errors, isEmpty);\n  /// ```\n  ///\n  /// You can also use the action to remove the error:\n  ///\n  /// ```dart\n  /// // Dispatch some action\n  /// var action = SomeAction();\n  /// var status = await store.dispatchAndWait(action);\n  /// store.removeError(action);\n  /// ```\n  ///\n  void removeError(Object source) {\n    Object error;\n    if (source is UserException)\n      error = source;\n    else if (source is ActionStatus && source.wrappedError != null)\n      error = source.wrappedError!;\n    else if (source is ReduxAction && source.status.wrappedError != null)\n      error = source.status.wrappedError!;\n    else\n      return;\n\n    _errors.removeWhere((queuedError) => queuedError == error);\n  }\n\n  /// Call this method to shut down the store.\n  /// It won't accept dispatches or change the state anymore.\n  ///\n  /// See also: [isShutdown] and [disposeProps].\n  void shutdown() {\n    _shutdown = true;\n    internalMixinProps.clear();\n  }\n\n  /// Properties used internally by the provided mixins.\n  /// You should not use this directly.\n  final internalMixinProps = _InternalMixinProps();\n\n  /// If you are running tests, you can change [forceInternetOnOffSimulation] to\n  /// simulate the internet connection as ON or OFF for the provided mixins\n  /// [CheckInternet], [AbortWhenNoInternet], and [UnlimitedRetryCheckInternet].\n  ///\n  /// - Return `true` if there IS internet.\n  /// - Return `false` if there is NO internet.\n  /// - Return `null` to use the real internet connection status (default).\n  ///\n  /// Example:\n  ///\n  /// ```dart\n  /// store.forceInternetOnOffSimulation = () => false;\n  /// ```\n  ///\n  /// This is specially useful during tests, for testing what happens when you\n  /// have no internet connection. And since it's tied to the store, it\n  /// automatically resets when the store is recreated.\n  ///\n  bool? Function() forceInternetOnOffSimulation = () => null;\n\n  bool get isShutdown => _shutdown;\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// The action may be sync or async.\n  ///\n  /// ```dart\n  /// store.dispatch(MyAction());\n  /// ```\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of this action, even if it changes the state.\n  ///\n  /// Method [dispatch] is of type [Dispatch].\n  ///\n  /// See also:\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  /// - [dispatchAndWaitAllActions] which dispatches an action then waits for all actions to finish.\n  ///\n  FutureOr<ActionStatus> dispatch(ReduxAction<St> action, {bool notify = true}) =>\n      _dispatch(action, notify: notify);\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// However, if the action is ASYNC, it will throw a [StoreException].\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of this action, even if it changes the state.\n  ///\n  /// Method [dispatchSync] is of type [DispatchSync]. It returns `ActionStatus`,\n  /// which means you can also get the final status of the action:\n  ///\n  /// ```dart\n  /// var status = store.dispatchSync(MyAction());\n  /// ```\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  /// - [dispatchAndWaitAllActions] which dispatches an action then waits for all actions to finish.\n  ///\n  ActionStatus dispatchSync(ReduxAction<St> action, {bool notify = true}) {\n    if (!action.isSync()) {\n      throw StoreException(\n          \"Can't dispatchSync(${action.runtimeType}) because ${action.runtimeType} is async.\");\n    }\n\n    return _dispatch(action, notify: notify) as ActionStatus;\n  }\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// The action may be sync or async. In both cases, it returns a [Future] that resolves when\n  /// the action finishes.\n  ///\n  /// ```dart\n  /// await store.dispatchAndWait(DoThisFirstAction());\n  /// store.dispatch(DoThisSecondAction());\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of this action, even if it changes the state.\n  ///\n  /// Note: While the state change from the action's reducer will have been applied when the\n  /// Future resolves, other independent processes that the action may have started may still\n  /// be in progress.\n  ///\n  /// Method [dispatchAndWait] is of type [DispatchAndWait]. It returns `Future<ActionStatus>`,\n  /// which means you can also get the final status of the action after you `await` it:\n  ///\n  /// ```dart\n  /// var status = await store.dispatchAndWait(MyAction());\n  /// ```\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  /// - [dispatchAndWaitAllActions] which dispatches an action then waits for all actions to finish.\n  ///\n  Future<ActionStatus> dispatchAndWait(ReduxAction<St> action, {bool notify = true}) =>\n      Future.value(_dispatch(action, notify: notify));\n\n  /// The [dispatchAndWaitAllActions] should be used in tests only.\n  ///\n  /// It first dispatches the given [action], applying its reducer, and\n  /// possibly changing the store state. The action may be sync or async.\n  /// In both cases, it returns a [Future] that resolves when the action\n  /// finishes.\n  ///\n  /// Then, it waits until ALL current actions in progress finish dispatching.\n  /// In other words, when no other actions are currently in progress.\n  ///\n  /// This dispatch method is meant to be used in tests, not in production,\n  /// as it's very easy to create a deadlock. However, if you do use it in\n  /// production, you may provide a [timeoutMillis], which by default is 10\n  /// minutes. To disable the timeout, make it -1. This timeout only starts\n  /// counting after the given [action] finished dispatching. Note: If you want,\n  /// you can modify [defaultTimeoutMillis] to change the default timeout.\n  ///\n  /// ```dart\n  /// await store.dispatchAndWaitAllActions(MyAction());\n  /// ```\n  ///\n  Future<ActionStatus> dispatchAndWaitAllActions(ReduxAction<St> action,\n      {bool notify = true, int? timeoutMillis}) async {\n    var actionStatus = await dispatchAndWait(action, notify: notify);\n    await waitAllActions([], completeImmediately: true, timeoutMillis: timeoutMillis);\n    return actionStatus;\n  }\n\n  /// Dispatches all given [actions] in parallel, applying their reducer, and possibly changing\n  /// the store state. It returns the same list of [actions], so that you can instantiate them\n  /// inline, but still get a list of them.\n  ///\n  /// ```dart\n  /// var actions = dispatchAll([BuyAction('IBM'), SellAction('TSLA')]);\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of these actions, even if it changes the state.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAndWaitAllActions] which dispatches an action then waits for all actions to finish.\n  ///\n  List<ReduxAction<St>> dispatchAll(List<ReduxAction<St>> actions, {bool notify = true}) {\n    for (var action in actions) {\n      dispatch(action, notify: notify);\n    }\n    return actions;\n  }\n\n  /// Dispatches all given [actions] in parallel, applying their reducers, and possibly changing\n  /// the store state. The actions may be sync or async. It returns a [Future] that resolves when\n  /// ALL actions finish.\n  ///\n  /// ```dart\n  /// var actions = await store.dispatchAndWaitAll([BuyAction('IBM'), SellAction('TSLA')]);\n  /// ```\n  ///\n  /// Note this is exactly the same as doing:\n  ///\n  /// ```dart\n  /// var action1 = BuyAction('IBM');\n  /// var action2 = SellAction('TSLA');\n  /// dispatch(action1);\n  /// dispatch(action2);\n  /// await store.waitAllActions([action1, action2], completeImmediately = true);\n  /// var actions = [action1, action2];\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of these actions, even if they change the state.\n  ///\n  /// Note: While the state change from the action's reducers will have been applied when the\n  /// Future resolves, other independent processes that the action may have started may still\n  /// be in progress.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  /// - [dispatchAndWaitAllActions] which dispatches an action then waits for all actions to finish.\n  ///\n  Future<List<ReduxAction<St>>> dispatchAndWaitAll(\n    List<ReduxAction<St>> actions, {\n    bool notify = true,\n  }) async {\n    var futures = <Future<ActionStatus>>[];\n\n    for (var action in actions) {\n      futures.add(dispatchAndWait(action, notify: notify));\n    }\n    await Future.wait(futures);\n\n    return actions;\n  }\n\n  @Deprecated(\"Use `dispatchAndWait` instead. This will be removed.\")\n  Future<ActionStatus> dispatchAsync(ReduxAction<St> action, {bool notify = true}) =>\n      dispatchAndWait(action, notify: notify);\n\n  FutureOr<ActionStatus> _dispatch(ReduxAction<St> action, {required bool notify}) {\n    //\n    // The action may access the store/state/dispatch as fields.\n    action.setStore(this);\n\n    if (_shutdown || action.abortDispatch())\n      return ActionStatus(isDispatchAborted: true, context: (action, this));\n\n    _dispatchCount++;\n\n    if (action.status.isDispatched)\n      throw new StoreException('The action was already dispatched. '\n          'Please, create a new action each time.');\n\n    action._status = action._status.copy(isDispatched: true);\n\n    if (_actionObservers != null)\n      for (ActionObserver observer in _actionObservers) {\n        observer.observe(action, dispatchCount, ini: true);\n      }\n\n    return _processAction(action, notify: notify);\n  }\n\n  void createTestInfoSnapshot(\n    St state,\n    ReduxAction<St> action,\n    Object? error,\n    Object? processedError, {\n    required bool ini,\n  }) {\n    if (_testInfoController != null || testInfoPrinter != null) {\n      var reduceInfo = TestInfo<St>(\n        state,\n        ini,\n        action,\n        error,\n        processedError,\n        dispatchCount,\n        reduceCount,\n        errors,\n      );\n      if (_testInfoController != null) _testInfoController!.add(reduceInfo);\n      if (testInfoPrinter != null) testInfoPrinter!(reduceInfo);\n    }\n  }\n\n  /// Returns a copy of the error queue, containing user exception errors thrown by\n  /// dispatched actions. Note that this is a copy of the queue, so you can't modify the original\n  /// queue here. Instead, use [getAndRemoveFirstError] to consume the errors, one by one.\n  Queue<UserException> get errors => Queue<UserException>.of(_errors);\n\n  /// We check the return type of methods `before` and `reduce` to decide if the\n  /// reducer is synchronous or asynchronous. It's important to run the reducer\n  /// synchronously, if possible.\n  FutureOr<ActionStatus> _processAction(\n    ReduxAction<St> action, {\n    bool notify = true,\n  }) {\n    //\n    _calculateIsWaitingIsFailed(action);\n\n    if (action.isSync())\n      return _processAction_Sync(action, notify: notify);\n    else\n      return _processAction_Async(action, notify: notify);\n  }\n\n  void _calculateIsWaitingIsFailed(ReduxAction<St> action) {\n    //\n    // If the action is fallible (that is to say, we have once called `isFailed` for this action),\n    bool fallible = _actionsWeCanCheckFailed.contains(action.runtimeType);\n\n    bool theUIHasAlreadyUpdated = false;\n\n    if (fallible) {\n      // Dispatch is starting, so we remove the action from the list of failed actions.\n      var removedAction = _failedActions.remove(action.runtimeType);\n\n      // Then we notify the UI. Note we don't notify if the action was never checked.\n      if (removedAction != null) {\n        theUIHasAlreadyUpdated = true;\n        _changeController.add(state);\n      }\n    }\n\n    // Add the action to the list of actions in progress.\n    // Note: We add both SYNC and ASYNC actions. The SYNC actions are important too,\n    // to prevent NonReentrant sync actions, where they call themselves.\n    bool ifWasAdded = _actionsInProgress.add(action);\n    if (ifWasAdded) _checkAllActionConditions(action);\n\n    // Note: If the UI hasn't updated yet, AND\n    // the action is awaitable (that is to say, we have already called `isWaiting` for this action),\n    if (!theUIHasAlreadyUpdated && _awaitableActions.contains(action.runtimeType)) {\n      _changeController.add(state);\n    }\n  }\n\n  /// The [triggerAction] is the action that was just added or removed in the list\n  /// of [_actionsInProgress] that triggered the check.\n  ///\n  void _checkAllActionConditions(ReduxAction<St> triggerAction) {\n    List<bool Function(Set<ReduxAction<St>>, ReduxAction<St>?)> keysToRemove = [];\n\n    _actionConditionCompleters.forEach((condition, completer) {\n      if (condition(actionsInProgress(), triggerAction)) {\n        completer.complete((actionsInProgress(), triggerAction));\n        keysToRemove.add(condition);\n      }\n    });\n\n    keysToRemove.forEach((key) {\n      _actionConditionCompleters.remove(key);\n    });\n  }\n\n  /// The [triggerAction] is the action that modified the state to trigger the condition.\n  void _checkAllStateConditions(ReduxAction<St> triggerAction) {\n    List<bool Function(St)> keysToRemove = [];\n\n    _stateConditionCompleters.forEach((condition, completer) {\n      if (condition(_state)) {\n        completer.complete(triggerAction);\n        keysToRemove.add(condition);\n      }\n    });\n\n    keysToRemove.forEach((key) {\n      _stateConditionCompleters.remove(key);\n    });\n  }\n\n  /// You can use [isWaiting] and pass it [actionOrTypeOrList] to check if:\n  /// * A specific async ACTION is currently being processed.\n  /// * An async action of a specific TYPE is currently being processed.\n  /// * If any of a few given async actions or action types is currently being processed.\n  ///\n  /// If you wait for an action TYPE, then it returns false when:\n  /// - The ASYNC action of the type is NOT currently being processed.\n  /// - If the type is not really a type that extends [ReduxAction].\n  /// - The action of the type is a SYNC action (since those finish immediately).\n  ///\n  /// If you wait for an ACTION, then it returns false when:\n  /// - The ASYNC action is NOT currently being processed.\n  /// - If the action is a SYNC action (since those finish immediately).\n  ///\n  /// Trying to wait for any other type of object will return null and throw\n  /// a [StoreException] after the async gap.\n  ///\n  /// Examples:\n  ///\n  /// ```dart\n  /// // Waiting for an action TYPE:\n  /// dispatch(MyAction());\n  /// if (store.isWaiting(MyAction)) { // Show a spinner }\n  ///\n  /// // Waiting for an ACTION:\n  /// var action = MyAction();\n  /// dispatch(action);\n  /// if (store.isWaiting(action)) { // Show a spinner }\n  ///\n  /// // Waiting for any of the given action TYPES:\n  /// dispatch(BuyAction());\n  /// if (store.isWaiting([BuyAction, SellAction])) { // Show a spinner }\n  /// ```\n  bool isWaiting(Object actionOrTypeOrList) {\n    //\n    // 1) If a type was passed:\n    if (actionOrTypeOrList is Type) {\n      _awaitableActions.add(actionOrTypeOrList);\n      return _actionsInProgress.any((action) => action.runtimeType == actionOrTypeOrList);\n    }\n    //\n    // 2) If an action was passed:\n    else if (actionOrTypeOrList is ReduxAction) {\n      _awaitableActions.add(actionOrTypeOrList.runtimeType);\n      return _actionsInProgress.contains(actionOrTypeOrList);\n    }\n    //\n    // 3) If an iterable was passed:\n    // 3.1) For each action or action type in the iterable...\n    else if (actionOrTypeOrList is Iterable) {\n      bool isWaiting = false;\n      for (var actionOrType in actionOrTypeOrList) {\n        //\n        // 3.2) If it's a type.\n        if (actionOrType is Type) {\n          _awaitableActions.add(actionOrType);\n\n          // 3.2.1) Is waiting if any of the actions in progress has that exact type.\n          if (!isWaiting)\n            isWaiting =\n                _actionsInProgress.any((action) => action.runtimeType == actionOrType);\n        }\n        //\n        // 3.3) If it's an action.\n        else if (actionOrType is ReduxAction) {\n          _awaitableActions.add(actionOrType.runtimeType);\n\n          // 3.3.1) Is waiting if any of the actions in progress is the exact action.\n          if (!isWaiting) isWaiting = _actionsInProgress.contains(actionOrType);\n        }\n        //\n        // 3.4) If it's not an action and not an action type, throw an exception.\n        // The exception is thrown after the async gap, so that it doesn't interrupt the processes.\n        else {\n          Future.microtask(() {\n            throw StoreException(\n                \"You can't do isWaiting([${actionOrTypeOrList.runtimeType}]). \"\n                \"Use only actions, action types, or a list of them.\");\n          });\n        }\n      }\n\n      // 3.5) If the `for` finished without matching any items, return false (it's NOT waiting).\n      return isWaiting;\n    }\n    // 4) If something different was passed, it's an error. We show the error after the\n    // async gap, so we don't interrupt the code. But we return false (not waiting).\n    else {\n      Future.microtask(() {\n        throw StoreException(\"You can't do isWaiting(${actionOrTypeOrList.runtimeType}), \"\n            \"Use only actions, action types, or a list of them.\");\n      });\n\n      return false;\n    }\n  }\n\n  /// Returns true if an [actionOrTypeOrList] failed with an [UserException].\n  /// Note: This method uses the EXACT type in [actionOrTypeOrList]. Subtypes are not considered.\n  bool isFailed(Object actionOrTypeOrList) => exceptionFor(actionOrTypeOrList) != null;\n\n  /// Returns the [UserException] of the [actionTypeOrList] that failed.\n  ///\n  /// [actionTypeOrList] can be a [Type], or an Iterable of types. Any other type\n  /// of object will return null and throw a [StoreException] after the async gap.\n  ///\n  /// Note: This method uses the EXACT type in [actionTypeOrList]. Subtypes are not considered.\n  UserException? exceptionFor(Object actionTypeOrList) {\n    //\n    // 1) If a type was passed:\n    if (actionTypeOrList is Type) {\n      _actionsWeCanCheckFailed.add(actionTypeOrList);\n      var action = _failedActions[actionTypeOrList];\n      var error = action?.status.wrappedError;\n      return (error is UserException) ? error : null;\n    }\n    //\n    // 2) If a list was passed:\n    else if (actionTypeOrList is Iterable) {\n      for (var actionType in actionTypeOrList) {\n        _actionsWeCanCheckFailed.add(actionType);\n        if (actionType is Type) {\n          var error = _failedActions.entries\n              .firstWhereOrNull((entry) => entry.key == actionType)\n              ?.value\n              .status\n              .wrappedError;\n          return (error is UserException) ? error : null;\n        } else {\n          Future.microtask(() {\n            throw StoreException(\n                \"You can't do exceptionFor([${actionTypeOrList.runtimeType}]), \"\n                \"but only an action Type, or a List of types.\");\n          });\n        }\n      }\n      return null;\n    }\n    // 3) If something different was passed, it's an error. We show the error after the\n    // async gap, so we don't interrupt the code. But we return null.\n    else {\n      Future.microtask(() {\n        throw StoreException(\n            \"You can't do exceptionFor(${actionTypeOrList.runtimeType}), \"\n            \"but only an action Type, or a List of types.\");\n      });\n\n      return null;\n    }\n  }\n\n  /// Removes the given [actionTypeOrList] from the list of action types that failed.\n  ///\n  /// Note that dispatching an action already removes that action type from the exceptions list.\n  /// This removal happens as soon as the action is dispatched, not when it finishes.\n  ///\n  /// [actionTypeOrList] can be a [Type], or an Iterable of types. Any other type\n  /// of object will return null and throw a [StoreException] after the async gap.\n  ///\n  /// Note: This method uses the EXACT type in [actionTypeOrList]. Subtypes are not considered.\n  void clearExceptionFor(Object actionTypeOrList) {\n    //\n    // 1) If a type was passed:\n    if (actionTypeOrList is Type) {\n      var result = _failedActions.remove(actionTypeOrList);\n      if (result != null) _changeController.add(state);\n    }\n    //\n    // 2) If a list was passed:\n    else if (actionTypeOrList is Iterable) {\n      Object? result;\n      for (var actionType in actionTypeOrList) {\n        if (actionType is Type) {\n          result = _failedActions.remove(actionType);\n        } else {\n          Future.microtask(() {\n            throw StoreException(\n                \"You can't clearExceptionFor([${actionTypeOrList.runtimeType}]), \"\n                \"but only an action Type, or a List of types.\");\n          });\n        }\n      }\n      if (result != null) _changeController.add(state);\n    }\n    // 3) If something different was passed, it's an error. We show the error after the\n    // async gap, so we don't interrupt the code. But we return null.\n    else {\n      Future.microtask(() {\n        throw StoreException(\n            \"You can't clearExceptionFor(${actionTypeOrList.runtimeType}), \"\n            \"but only an action Type, or a List of types.\");\n      });\n    }\n  }\n\n  /// We check the return type of methods `before` and `reduce` to decide if the\n  /// reducer is synchronous or asynchronous. It's important to run the reducer\n  /// synchronously, if possible.\n  ActionStatus _processAction_Sync(\n    ReduxAction<St> action, {\n    bool notify = true,\n  }) {\n    //\n    // Creates the \"INI\" test snapshot.\n    createTestInfoSnapshot(state!, action, null, null, ini: true);\n\n    // The action may access the store/state/dispatch as fields.\n    assert(action.store == this);\n\n    var afterWasRun = _Flag<bool>(false);\n\n    Object? originalError, processedError;\n\n    try {\n      var result = action.before();\n      if (result is Future) throw StoreException(_beforeTypeErrorMsg);\n\n      action._status = action._status.copy(hasFinishedMethodBefore: true);\n      if (_shutdown) return action._status;\n      _applyReducer(action, notify: notify);\n      action._status = action._status.copy(hasFinishedMethodReduce: true);\n      if (_shutdown) return action._status;\n    }\n    //\n    catch (error, stackTrace) {\n      originalError = error;\n      processedError = _processError(action, error, stackTrace, afterWasRun);\n\n      // Error is meant to be \"swallowed\".\n      if (processedError == null)\n        return action._status;\n      //\n      // Error was not changed. Rethrow.\n      else if (identical(processedError, error))\n        rethrow;\n      //\n      // Error was wrapped. Throw.\n      else\n        Error.throwWithStackTrace(processedError, stackTrace);\n    }\n    //\n    finally {\n      _finalize(action, originalError, processedError, afterWasRun, notify);\n    }\n\n    return action._status;\n  }\n\n  /// We check the return type of methods `before` and `reduce` to decide if the\n  /// reducer is synchronous or asynchronous. It's important to run the reducer\n  /// synchronously, if possible.\n  Future<ActionStatus> _processAction_Async(\n    ReduxAction<St> action, {\n    bool notify = true,\n  }) async {\n    //\n    // Creates the \"INI\" test snapshot.\n    createTestInfoSnapshot(state!, action, null, null, ini: true);\n\n    // The action may access the store/state/dispatch as fields.\n    assert(action.store == this);\n\n    var afterWasRun = _Flag<bool>(false);\n\n    Object? result, originalError, processedError;\n\n    try {\n      result = action.before();\n      if (result is Future) await result;\n      action._status = action._status.copy(hasFinishedMethodBefore: true);\n      if (_shutdown) return action._status;\n      result = _applyReducer(action, notify: notify);\n      if (result is Future) await result;\n      action._status = action._status.copy(hasFinishedMethodReduce: true);\n      if (_shutdown) return action._status;\n    }\n    //\n    catch (error, stackTrace) {\n      originalError = error;\n      processedError = _processError(action, error, stackTrace, afterWasRun);\n      // Error is meant to be \"swallowed\".\n      if (processedError == null)\n        return action._status;\n      // Error was not changed. Rethrows.\n      else if (identical(processedError, error))\n        rethrow;\n      // Error was wrapped. Throw.\n      else\n        Error.throwWithStackTrace(processedError, stackTrace);\n    }\n    //\n    finally {\n      _finalize(action, originalError, processedError, afterWasRun, notify);\n    }\n\n    return action._status;\n  }\n\n  static const _beforeTypeErrorMsg =\n      \"Before should return `void` or `Future<void>`. Do not return `FutureOr`.\";\n\n  static const _reducerTypeErrorMsg = \"Reducer should return `St?` or `Future<St?>`. \";\n\n  void _checkReducerType(FutureOr<St?> Function() reduce) {\n    //\n    // Sync reducer is acceptable.\n    if (reduce is St? Function()) {\n      return;\n    }\n    //\n    // Async reducer is acceptable.\n    else if (reduce is Future<St?> Function()) {\n      return;\n    }\n    //\n    else if (reduce is Future<St>? Function()) {\n      throw StoreException(_reducerTypeErrorMsg + \"Do not return `Future<St>?`.\");\n    }\n    //\n    else if (reduce is Future<St?>? Function()) {\n      throw StoreException(_reducerTypeErrorMsg + \"Do not return `Future<St?>?`.\");\n    }\n    //\n    // ignore: unnecessary_type_check\n    else if (reduce is FutureOr Function()) {\n      throw StoreException(_reducerTypeErrorMsg + \"Do not return `FutureOr`.\");\n    }\n    //\n    else {\n      throw StoreException(\n          _reducerTypeErrorMsg + \"Do not return `${reduce.runtimeType}`.\");\n    }\n  }\n\n  FutureOr<void> _applyReducer(ReduxAction<St> action, {bool notify = true}) {\n    _reduceCount++;\n\n    // Make sure the action reducer returns an acceptable type.\n    _checkReducerType(action.reduce);\n\n    if (action.ifWrapReduceOverridden()) {\n      return _applyReduceAndWrapReduce(action, notify: notify);\n    } else {\n      return _applyReduce(action, notify: notify);\n    }\n  }\n\n  FutureOr<void> _applyReduceAndWrapReduce(ReduxAction<St> action, {bool notify = true}) {\n    //\n    assert(action.ifWrapReduceOverridden());\n\n    if (action.ifWrapReduceOverridden_Sync())\n      throw StoreException(\"The ${action.runtimeType}.wrapReduce method \"\n          \"should return `Future<St?>`, not `<St>` or `<St?>`.\");\n\n    action._completedFuture = false;\n\n    Reducer<St> _reduce = (_wrapReduce != null)\n        ? _wrapReduce.wrapReduce(action.reduce, this)\n        : action.reduce;\n\n    return (action.wrapReduce(_reduce) as Future<St?>).then((state) {\n      _registerState(state, action, notify: notify);\n\n      if (action._completedFuture) {\n        Future.error(\n            \"The reducer of action ${action.runtimeType} returned a completed Future. \"\n            \"This may result in state changes being lost. \"\n            \"Please make sure all code paths in the reducer pass through at least one `await`. \"\n            \"If necessary, add `await microtask;` to the start of the reducer.\");\n      }\n    });\n  }\n\n  FutureOr<void> _applyReduce(ReduxAction<St> action, {bool notify = true}) {\n    //\n    Reducer<St> _reduce = (_wrapReduce != null)\n        ? _wrapReduce.wrapReduce(action.reduce, this)\n        : action.reduce;\n\n    // Sync reducer.\n    if (_reduce is St? Function()) {\n      _registerState(_reduce(), action, notify: notify);\n    }\n    //\n    // Async reducer.\n    else if (_reduce is Future<St?> Function()) {\n      /// When a reducer returns a state, we need to apply that state immediately in the store.\n      /// If we wait even a single microtask, another reducer may change the store-state before we\n      /// have the chance to apply the state. This would result in the later reducer overriding the\n      /// value of the other reducer, and state changes will be lost.\n      ///\n      /// To fix this we'll depend on the behavior described below, which was confirmed by the Dart\n      /// team:\n      ///\n      /// 1) When a future returned by an async function completes, it will call the `then` method\n      /// synchronously (in the same microtask), as long as the function returns a value (not a\n      /// Future) AND this happens AFTER at least one await. This means we then have the chance to\n      /// apply the returned state to the store right away.\n      ///\n      /// 2) When a future returned by an async function completes, it will call the `then` method\n      /// asynchronously (delayed to a later microtask) if there was no await in the async\n      /// function. When that happens, the future is created \"completed\", and Dart will wait for\n      /// the next microtask before calling the `then` method (they do this because they want to\n      /// enforce that a listener on a future is always notified in a later microtask than the one\n      /// where it was registered). This means we will only be able to apply the returned state to\n      /// the store during the next microtask. There is now a chance state will be lost.\n      /// This situation must be avoided at all cost, and it's actually simple to solve it:\n      /// An async reducer must never complete without at least one await.\n      /// Unfortunately, if the developer forgets to add the await, there is no way for AsyncRedux\n      /// to let them know about it, because there is no way for us to know if a Future is\n      /// completed. The completion information exists in the `FutureImpl` class but it's not\n      /// exposed. I have asked the Dart team to expose this information, but they refused. The\n      /// only solution is to document this and trust the developer.\n      ///\n      /// Important: The behavior described above was confirmed by the Dart team, but it's NOT\n      /// documented. In other words, they make no promise that it will be kept in the future.\n      /// If that ever changes, AsyncRedux will need to change too, so that reducers return\n      /// `St? Function(state)` instead of returning `state`. For example, instead of a reducer\n      /// ending with `return state.copy(123)` it would be `return (state) => state.copy(123)`.\n      /// Hopefully, the tests in `sync_async_test.dart` will catch this, if it ever changes.\n\n      action._completedFuture = false;\n\n      return _reduce().then((state) {\n        _registerState(state, action, notify: notify);\n\n        if (action._completedFuture) {\n          Future.error(\n              \"The reducer of action ${action.runtimeType} returned a completed Future. \"\n              \"This may result in state changes being lost. \"\n              \"Please make sure all code paths in the reducer pass through at least one `await`. \"\n              \"If necessary, add `await microtask;` to the start of the reducer.\");\n        }\n      });\n    }\n    //\n    // Invalid reducer (FutureOr is not accepted).\n    else {\n      throw StoreException(\"Reducer should return `St?` or `Future<St?>`. \"\n          \"Do not return `FutureOr<St?>`. \"\n          \"Reduce is of type: '${_reduce.runtimeType}'.\");\n    }\n  }\n\n  /// Adds the state to the changeController, but only if the `reduce` method\n  /// did not return null, and if it did not return the same identical state.\n  ///\n  /// Note: We compare the state using `identical` (which is fast).\n  ///\n  /// The [StateObserver]s are always called (if defined). If you need to know if the state was\n  /// changed or not, you can compare `bool ifStateChanged = identical(prevState, newState)`\n  void _registerState(\n    St? state,\n    ReduxAction<St> action, {\n    bool notify = true,\n  }) {\n    if (_shutdown) return;\n\n    St prevState = _state;\n\n    // Reducers may return null state, or the unaltered state, when they don't want to change the\n    // state. Note: If the action is an \"active action\" it will be removed, so we have to\n    // add the state to _changeController even if it's the same state.\n    if (((state != null) && !identical(_state, state)) ||\n        _actionsInProgress.contains(action)) {\n      _state = state ?? _state;\n      _stateTimestamp = DateTime.now().toUtc();\n\n      if (notify) {\n        _changeController.add(state ?? _state);\n      }\n\n      _checkAllStateConditions(action);\n    }\n    St newState = _state;\n\n    if (_stateObservers != null)\n      for (StateObserver observer in _stateObservers) {\n        observer.observe(action, prevState, newState, null, dispatchCount);\n      }\n\n    if (_processPersistence != null) _processPersistence.process(action, newState);\n    if (_processCloudSync != null) _processCloudSync.process(action, newState);\n  }\n\n  /// The actions that are currently being processed.\n  /// Use [isWaiting] to know if an action is currently being processed.\n  final Set<ReduxAction<St>> _actionsInProgress = HashSet<ReduxAction<St>>.identity();\n\n  /// Returns an unmodifiable set of the actions on progress.\n  Set<ReduxAction<St>> actionsInProgress() {\n    return new UnmodifiableSetView(_actionsInProgress);\n  }\n\n  /// Returns a copy of the set of actions on progress.\n  Set<ReduxAction<St>> copyActionsInProgress() =>\n      HashSet<ReduxAction<St>>.identity()..addAll(actionsInProgress());\n\n  /// Returns true if the actions in progress are equal to the given set.\n  bool actionsInProgressEqualTo(Set<ReduxAction<St>> set) {\n    if (set.length != _actionsInProgress.length) {\n      return false;\n    }\n    return set.containsAll(_actionsInProgress) && _actionsInProgress.containsAll(set);\n  }\n\n  /// Actions that we may put into [_actionsInProgress].\n  /// This helps to know when to rebuild to make [isWaiting] work.\n  final Set<Type> _awaitableActions = HashSet<Type>.identity();\n\n  /// The async actions that have failed recently.\n  /// When an action fails by throwing an UserException, it's added to this map (indexed by its\n  /// action type), and removed when it's dispatched.\n  /// Use [isFailed], [exceptionFor] and [clearExceptionFor] to know if you should display\n  /// some error message due to an action failure.\n  ///\n  /// Note: Throwing an UserException can show a modal dialog to the user, and also show the error\n  /// as a message in the UI. If you don't want to show the dialog you can use the `noDialog`\n  /// getter in the error message: `throw UserException('Invalid input').noDialog`.\n  ///\n  final Map<Type, ReduxAction<St>> _failedActions = HashMap<Type, ReduxAction<St>>();\n\n  /// Async actions that we may put into [_failedActions].\n  /// This helps to know when to rebuild to make [isWaiting] work.\n  final Set<Type> _actionsWeCanCheckFailed = HashSet<Type>.identity();\n\n  /// Returns the processed error. Returns `null` if the error is meant to be \"swallowed\".\n  Object? _processError(\n    ReduxAction<St> action,\n    Object error,\n    StackTrace stackTrace,\n    _Flag<bool> afterWasRun,\n  ) {\n    if (_stateObservers != null)\n      for (StateObserver observer in _stateObservers) {\n        observer.observe(action, _state, _state, error, dispatchCount);\n      }\n\n    Object? errorOrNull = error;\n\n    action._status = action._status.copy(originalError: error);\n\n    try {\n      errorOrNull = action.wrapError(errorOrNull, stackTrace);\n    } catch (_error) {\n      // If the action's wrapError throws an error, it will be used instead\n      // of the original error (but the recommended way is returning the error).\n      errorOrNull = _error;\n    }\n\n    if (errorOrNull != null) {\n      var globalErrorObserver = _globalErrorObserver?.call(this);\n      if (globalErrorObserver != null) {\n        try {\n          globalErrorObserver._init(\n            error: errorOrNull,\n            originalError: action.status.originalError,\n            stackTrace: stackTrace,\n            action: action,\n            store: this,\n          );\n          errorOrNull = globalErrorObserver.observe();\n        } catch (_error) {\n          // If the GlobalErrorObserver throws an error, it will be used instead\n          // of the original error (but the recommended way is returning the error).\n          errorOrNull = _error;\n        }\n      }\n    }\n\n    // This is DEPRECATED and will be removed in the future.\n    // The recommended way is using a GlobalErrorObserver.\n    if (_globalWrapError != null && errorOrNull != null) {\n      try {\n        errorOrNull = _globalWrapError.wrap(errorOrNull, stackTrace, action);\n      } catch (_error) {\n        // If the GlobalWrapError throws an error, it will be used instead\n        // of the original error (but the recommended way is returning the error).\n        errorOrNull = _error;\n      }\n    }\n\n    action._status = action._status.copy(wrappedError: errorOrNull);\n\n    // Memorizes the action that failed. We'll remove it when it's dispatched again.\n    _failedActions[action.runtimeType] = action;\n\n    afterWasRun.value = true;\n    _after(action);\n\n    // Memorizes errors of type UserException (in the error queue).\n    // These errors are usually shown to the user in a modal dialog, and are not logged.\n    if (errorOrNull is UserException) {\n      if (errorOrNull.ifOpenDialog) {\n        _addError(errorOrNull);\n        _changeController.add(state);\n      }\n    } else if (errorOrNull is AbortDispatchException) {\n      action._status = action._status.copy(isDispatchAborted: true);\n    }\n\n    // If an errorObserver was NOT defined, return (to throw) all errors which are\n    // not UserException or AbortDispatchException.\n    if (_errorObserver == null) {\n      if ((errorOrNull is! UserException) && (errorOrNull is! AbortDispatchException))\n        return errorOrNull;\n    }\n    // If an errorObserver was defined, observe the error.\n    // Then, if the observer returns true, return the error to be thrown.\n    else if (errorOrNull != null) {\n      try {\n        if (_errorObserver.observe(errorOrNull, stackTrace, action, this)) //\n          return errorOrNull;\n      } catch (_error) {\n        // The errorObserver should never throw. However, if it does, print the error.\n        _throws(\n            \"Method 'ErrorObserver.observe()' has thrown an error '$_error' \"\n            \"when observing error '$errorOrNull'.\",\n            _error,\n            stackTrace);\n\n        return errorOrNull;\n      }\n    }\n\n    return null;\n  }\n\n  void _finalize(\n    ReduxAction<St> action,\n    Object? error,\n    Object? processedError,\n    _Flag<bool> afterWasRun,\n    bool notify,\n  ) {\n    if (!afterWasRun.value) _after(action);\n\n    bool ifWasRemoved = _actionsInProgress.remove(action);\n    if (ifWasRemoved) _checkAllActionConditions(action);\n\n    // If we'll not be notifying, it's possible we need to trigger the change controller, when the\n    // action is awaitable (that is to say, when we have already called `isWaiting` for this action).\n    if (_awaitableActions.contains(action.runtimeType) && ((error != null) || !notify)) {\n      _changeController.add(state);\n    }\n\n    createTestInfoSnapshot(state!, action, error, processedError, ini: false);\n\n    if (_actionObservers != null)\n      for (ActionObserver observer in _actionObservers) {\n        observer.observe(action, dispatchCount, ini: false);\n      }\n  }\n\n  void _after(ReduxAction<St> action) {\n    try {\n      action.after();\n    } catch (error, stackTrace) {\n      // After should never throw.\n      // However, if it does, prints the error information to the console,\n      // then throw the error after an asynchronous gap.\n      _throws(\n        \"Method '${action.runtimeType}.after()' \"\n        \"has thrown an error:\\n '$error'.\",\n        error,\n        stackTrace,\n      );\n    } finally {\n      action._status = action._status.copy(hasFinishedMethodAfter: true);\n    }\n  }\n\n  /// Closes down the store so it will no longer be operational.\n  /// Only use this if you want to destroy the Store while your app is running.\n  /// Do not use this method as a way to stop listening to onChange state changes.\n  /// For that purpose, view the onChange documentation.\n  Future teardown({St? emptyState}) async {\n    if (emptyState != null) _state = emptyState;\n    _stateTimestamp = DateTime.now().toUtc();\n    return _changeController.close();\n  }\n\n  /// Helps testing the `StoreConnector`s methods, such as `onInit`,\n  /// `onDispose` and `onWillChange`.\n  ///\n  /// For example, suppose you have a `StoreConnector` which dispatches\n  /// `SomeAction` on its `onInit`. How could you test that?\n  ///\n  /// ```\n  /// class MyConnector extends StatelessWidget {\n  ///   Widget build(BuildContext context) => StoreConnector<AppState, Vm>(\n  ///         vm: () => _Factory(),\n  ///         onInit: _onInit,\n  ///         builder: (context, vm) { ... }\n  ///   }\n  ///\n  ///   void _onInit(Store<AppState> store) => store.dispatch(SomeAction());\n  /// }\n  ///\n  /// var store = Store(...);\n  /// var connectorTester = store.getConnectorTester(MyConnector());\n  /// connectorTester.runOnInit();\n  /// var action = await store.waitAnyActionTypeFinishes([SomeAction]);\n  /// expect(action.someValue, 123);\n  /// ```\n  ///\n  ConnectorTester<St, Model> getConnectorTester<Model>(StatelessWidget widgetConnector) =>\n      ConnectorTester<St, Model>(this, widgetConnector);\n\n  /// Throws the error after an asynchronous gap.\n  void _throws(errorMsg, Object? error, StackTrace stackTrace) {\n    Future(() {\n      Error.throwWithStackTrace(\n        (error == null) ? errorMsg : \"$errorMsg:\\n  $error\",\n        stackTrace,\n      );\n    });\n  }\n}\n\nenum CompareBy { byDeepEquals, byIdentity }\n\n@immutable\nclass ActionStatus {\n  ActionStatus({\n    this.isDispatched = false,\n    this.hasFinishedMethodBefore = false,\n    this.hasFinishedMethodReduce = false,\n    this.hasFinishedMethodAfter = false,\n    this.isDispatchAborted = false,\n    this.originalError,\n    this.wrappedError,\n    required this.context,\n  });\n\n  /// Returns true if the action was already dispatched. An action cannot be dispatched\n  /// more than once, which means that you have to create a new action each time.\n  ///\n  /// Note this may be true even if the action has not yet FINISHED dispatching.\n  /// To check if it has finished, use `action.isFinished`.\n  final bool isDispatched;\n\n  /// Is true when the `before` method finished executing normally.\n  /// Is false if it has not yet finished executing or if it threw an error.\n  final bool hasFinishedMethodBefore;\n\n  /// Is true when the `reduce` method finished executing normally, returning a value.\n  /// Is false if it has not yet finished executing or if it threw an error.\n  final bool hasFinishedMethodReduce;\n\n  /// Is true if the `after` method finished executing. Note the `after` method should\n  /// never throw any errors, but if it does the error will be swallowed and ignored.\n  /// Is false if it has not yet finished executing.\n  final bool hasFinishedMethodAfter;\n\n  /// Is true if the action was:\n  /// - Aborted with the [ReduxAction.abortDispatch] method,\n  /// - If an [AbortDispatchException] was thrown by the action's `before` or `reduce`\n  ///   methods (and survived the `wrapError` and `globalErrorObserver`). Or,\n  /// - If the store was being shut down with the [Store.shutdown] method.\n  final bool isDispatchAborted;\n\n  /// Holds the error thrown by the action's before/reduce methods, if any.\n  /// This may or may not be equal to the error thrown by the action, because the original error\n  /// will still be processed by the action's `wrapError` and the `globalErrorObserver`. However,\n  /// if `originalError` is non-null, it means the reducer did not finish running.\n  final Object? originalError;\n\n  /// Holds the error thrown by the action. This may or may not be the same as `originalError`,\n  /// because any errors thrown by the action's before/reduce methods may still be changed\n  /// or cancelled by the action's `wrapError` and the `globalErrorObserver`. This is the\n  /// final error after all these wraps.\n  final Object? wrappedError;\n\n  /// The action and store related to this status.\n  final (ReduxAction, Store)? context;\n\n  /// Returns true only if the action has completed, and none of the 'before' or 'reduce'\n  /// methods have thrown an error. This indicates that the 'reduce' method completed and\n  /// returned a result (even if the result was null). The 'after' method also already ran.\n  ///\n  /// This can be useful if you need to dispatch a second method only if the first method\n  /// succeeded:\n  ///\n  /// ```ts\n  /// let action = new LoadInfo();\n  /// await dispatchAndWait(action);\n  /// if (action.isCompletedOk) dispatch(new ShowInfo());\n  /// ```\n  ///\n  /// Or you can also get the state directly from `dispatchAndWait`:\n  ///\n  /// ```ts\n  /// var status = await dispatchAndWait(LoadInfo());\n  /// if (status.isCompletedOk) dispatch(ShowInfo());\n  /// ```\n  bool get isCompletedOk => isCompleted && (originalError == null);\n\n  /// Returns true only if the action has completed (the 'after' method already ran), but either\n  /// the 'before' or the 'reduce' methods have thrown an error. If this is true, it indicates that\n  /// the reducer could NOT complete, and could not return a value to change the state.\n  bool get isCompletedFailed => isCompleted && (originalError != null);\n\n  /// Returns true only if the action has completed executing, either with or without errors.\n  /// If this is true, the 'after' method already ran.\n  bool get isCompleted => hasFinishedMethodAfter;\n\n  ActionStatus copy({\n    bool? isDispatched,\n    bool? hasFinishedMethodBefore,\n    bool? hasFinishedMethodReduce,\n    bool? hasFinishedMethodAfter,\n    bool? isDispatchAborted,\n    Object? originalError,\n    Object? wrappedError,\n    (ReduxAction, Store)? context,\n  }) =>\n      ActionStatus(\n        isDispatched: isDispatched ?? this.isDispatched,\n        hasFinishedMethodBefore: hasFinishedMethodBefore ?? this.hasFinishedMethodBefore,\n        hasFinishedMethodReduce: hasFinishedMethodReduce ?? this.hasFinishedMethodReduce,\n        hasFinishedMethodAfter: hasFinishedMethodAfter ?? this.hasFinishedMethodAfter,\n        isDispatchAborted: isDispatchAborted ?? this.isDispatchAborted,\n        originalError: originalError ?? this.originalError,\n        wrappedError: wrappedError ?? this.wrappedError,\n        context: context ?? this.context,\n      );\n\n  @override\n  String toString() => 'ActionStatus{'\n      'isDispatched: $isDispatched, '\n      'hasFinishedMethodBefore: $hasFinishedMethodBefore, '\n      'hasFinishedMethodReduce: $hasFinishedMethodReduce, '\n      'hasFinishedMethodAfter: $hasFinishedMethodAfter, '\n      'isDispatchAborted: $isDispatchAborted, '\n      'originalError: $originalError, '\n      'wrappedError: $wrappedError'\n      '}';\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is ActionStatus &&\n          runtimeType == other.runtimeType &&\n          isDispatched == other.isDispatched &&\n          hasFinishedMethodBefore == other.hasFinishedMethodBefore &&\n          hasFinishedMethodReduce == other.hasFinishedMethodReduce &&\n          hasFinishedMethodAfter == other.hasFinishedMethodAfter &&\n          isDispatchAborted == other.isDispatchAborted &&\n          originalError == other.originalError &&\n          wrappedError == other.wrappedError &&\n          context == other.context;\n\n  @override\n  int get hashCode => Object.hash(\n      isDispatched,\n      hasFinishedMethodBefore,\n      hasFinishedMethodReduce,\n      hasFinishedMethodAfter,\n      isDispatchAborted,\n      originalError,\n      wrappedError,\n      context);\n}\n\nclass _Flag<T> {\n  T value;\n\n  _Flag(this.value);\n\n  @override\n  bool operator ==(Object other) => true;\n\n  @override\n  int get hashCode => 0;\n}\n\ntypedef OptimisticSyncWithPushRevisionEntry = ({\n  /// Monotonic counter for *local intents* (dispatches) for this key.\n  /// Incremented once per dispatch of an [OptimisticSyncWithPush] action\n  /// for a given key. Used to decide if a follow-up request may be needed.\n  int localRevision,\n\n  /// Latest known server revision for this key.\n  /// This is updated by both [OptimisticSyncWithPush] and [ServerPush].\n  /// It only moves forward (never regresses) to guard against\n  /// out-of-order responses/pushes. Value `-1` means the app doesn't know\n  /// any server revision yet for this key.\n  int serverRevision,\n\n  /// True if the latest value in memory is from a server push.\n  /// False if it's from a local dispatch.\n  bool isPush,\n});\n\n/// Some properties used by the Mixins. These are scoped to the store, so they\n/// reset when the store is recreated, for example during tests.\nclass _InternalMixinProps {\n  final Map<Object?, DateTime> throttleLockMap = {};\n  final Map<Object?, (DateTime, Object)> freshKeyMap = {};\n  final Map<Object?, int> debounceLockMap = {};\n  final Set<Object?> nonReentrantKeySet = {};\n\n  /// Set for the [OptimisticSync] mixin. Tracks which keys are currently locked.\n  final Set<Object?> optimisticSyncKeySet = {};\n\n  /// Map used by the [OptimisticSyncWithPush] and [ServerPush] mixins.\n  final Map<Object?, OptimisticSyncWithPushRevisionEntry>\n      optimisticSyncWithPushRevisionMap = {};\n\n  /// Map used by the [Polling] mixin. Stores one-shot timers keyed by action runtimeType.\n  final Map<Object?, Timer> pollingMap = {};\n\n  /// Removes the locks for Throttle, Debounce, Fresh, NonReentrant,\n  /// OptimisticSync, OptimisticSyncWithPush, and Polling.\n  void clear() {\n    throttleLockMap.clear();\n    freshKeyMap.clear();\n    debounceLockMap.clear();\n    nonReentrantKeySet.clear();\n    optimisticSyncKeySet.clear();\n    optimisticSyncWithPushRevisionMap.clear();\n    for (final timer in pollingMap.values) timer.cancel();\n    pollingMap.clear();\n  }\n}\n\n/// You may subclass [GlobalErrorObserver] and pass it to the store constructor,\n/// if you want to have a global observer for errors thrown in your actions:\n///\n/// ```dart\n/// var store = Store<AppState>(\n///   initialState: AppState(),\n///   globalErrorObserver: (store) => AppGlobalErrorObserver(),\n/// }\n///\n/// class MyGlobalErrorObserver extends GlobalErrorObserver {\n///   @override\n///   void wrap() {\n///     // Do something.\n///   }\n/// }\n/// ```\n///\n/// Your observer error object will be given all errors thrown in your actions\n/// (including those of type `UserException`). Then:\n/// * If it returns the same [error] unaltered, this original error will be used.\n/// * If it returns something else, that it will be used instead of [error].\n/// * If it returns `null`, [error] will be disabled (swallowed).\n///\n/// IMPORTANT: If instead of RETURNING an error you THROW an error inside the `observe`\n/// method, AsyncRedux will catch this error and use it instead of [error].\n/// In other words, returning an error or throwing an error has the same effect. However,\n/// it is still recommended to return the error rather than throwing it.\n///\n/// Note this observer is called AFTER the action's [ReduxAction.wrapError].\n///\n/// # Use cases\n///\n/// 1. Use this to set up your app to use 3rd-party services like Sentry or Firebase\n/// Crashlytics to monitor your app for errors in production, and print them to the\n/// console in development and testing. Since you are setting it up in a centralized way,\n/// you don't have to \"pollute\" your code with logging calls.\n///\n/// 2. Use this to have a global place to convert some exceptions into [UserException]s.\n/// For example, Firebase may throw some `PlatformException`s in response to a bad\n/// connection to the server. In this case, you may want to show the user a dialog\n/// explaining that the connection is bad, which you can do by converting it to\n/// a [UserException]. Note, this could also be done in the [ReduxAction.wrapError],\n/// but then you'd have to add it to all actions that use Firebase.\n///\n/// # Parameters you can access in the `observe` method:\n///\n/// - `error`: The error thrown by the action, AFTER `wrapError`.\n/// - `originalError`: The action error BEFORE `wrapError`.\n/// - `stackTrace`: The stack trace associated with the error.\n/// - `action`: The action that triggered the error.\n/// - `store`: Use it to read `store.environment` or `store.configuration`.\n///    Do **not** use it to dispatch new actions.\n///\nabstract class GlobalErrorObserver<St> {\n  //\n  /// The error thrown by the action, AFTER being processed by the action's `wrapError`.\n  late final Object error;\n\n  /// The error thrown by the action, BEFORE being processed by the action's `wrapError`.\n  late final Object originalError;\n\n  /// The stack trace of the error.\n  late final StackTrace stackTrace;\n\n  /// The action that threw the error.\n  late final ReduxAction<St> action;\n\n  /// You can access the store, but do NOT use it to dispatch actions,\n  /// because the store is still processing the current action, and dispatching another\n  /// action may cause unexpected behavior. You can use it to read the environment,\n  /// configuration, and state, if they are relevant to the error you want to return.\n  /// Example:\n  ///\n  /// ```dart\n  /// Environment get environment => store.environment;\n  /// Config get configuration => store.configuration;\n  /// AppState get state => store.state;\n  /// ```\n  ///\n  late Store<St> store;\n\n  GlobalErrorObserver();\n\n  /// Override this method to return the error you want to be used\n  /// instead of the original error. Or, if you want to keep the original error,\n  /// return it unaltered. If you want to disable the error, return `null`.\n  Object? observe();\n\n  void _init({\n    required Object error,\n    required Object? originalError,\n    required StackTrace stackTrace,\n    required ReduxAction<St> action,\n    required Store<St> store,\n  }) {\n    this.error = error;\n    this.originalError = originalError ?? '';\n    this.stackTrace = stackTrace;\n    this.action = action;\n  }\n}\n\n/// A dummy global error observer that does nothing (same as not providing it).\n///\n/// See also: [GlobalErrorObserverForDevelopment] and [SwallowGlobalErrorObserver].\n///\nclass GlobalErrorObserverDummy<St> extends GlobalErrorObserver<St> {\n  @override\n  Object? observe() => error;\n}\n\n/// During development you may use this global error observer if you want all errors\n/// to be shown to the user in a dialog, not only [UserException]s.\n///\n/// In more detail:\n///\n/// - Wraps all errors into [UserException]s, and put them all into the error queue.\n/// - Errors which are NOT originally [UserException]s will still be thrown.\n///\n/// Use it in the store like this:\n///\n/// ```dart\n/// var store = Store(\n///    globalErrorObserver: (store) => GlobalErrorObserverForDevelopment()\n/// );\n/// ```\n///\n/// See also: [GlobalErrorObserverDummy] and [SwallowGlobalErrorObserver].\n///\nclass GlobalErrorObserverForDevelopment<St> extends GlobalErrorObserver<St> {\n  @override\n  Object? observe() {\n    if (error is! UserException) {\n      Future.microtask(() => store.dispatch(\n            UserExceptionAction(error.toString(), cause: error),\n          ));\n    }\n\n    return error;\n  }\n}\n\n/// Swallows all errors (not recommended).\n///\n/// Use it in the store like this:\n///\n/// ```dart\n/// var store = Store(\n///    globalErrorObserver: (store) => SwallowGlobalErrorObserver()\n/// );\n/// ```\n/// \n/// See also: [GlobalErrorObserverDummy] and [GlobalErrorObserverForDevelopment].\n///\nclass SwallowGlobalErrorObserver<St> extends GlobalErrorObserver<St> {\n  @override\n  Object? observe() => null;\n}\n"
  },
  {
    "path": "lib/src/store_exception.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\n/// General internal exception for AsyncRedux.\nclass StoreException implements Exception {\n  final String msg;\n\n  StoreException(this.msg);\n\n  @override\n  String toString() => msg;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is StoreException && //\n          runtimeType == other.runtimeType &&\n          msg == other.msg;\n\n  @override\n  int get hashCode => msg.hashCode;\n}\n"
  },
  {
    "path": "lib/src/store_provider_and_connector.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'dart:async';\nimport 'dart:collection';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:collection/collection.dart' show DeepCollectionEquality;\nimport 'package:fast_immutable_collections/fast_immutable_collections.dart';\nimport 'package:flutter/material.dart';\n\n/// Convert the entire [Store] into a [Model]. The [Model] will\n/// be used to build a Widget using the [ViewModelBuilder].\ntypedef StoreConverter<St, Model> = Model Function(Store<St> store);\n\n/// A function that will be run when the [StoreConnector] is initialized (using\n/// the [State.initState] method). This can be useful for dispatching actions\n/// that fetch data for your Widget when it is first displayed.\ntypedef OnInitCallback<St> = void Function(Store<St> store);\n\n/// A function that will be run when the StoreConnector is removed from the Widget Tree.\n/// It is run in the [State.dispose] method.\n/// This can be useful for dispatching actions that remove stale data from your State tree.\ntypedef OnDisposeCallback<St> = void Function(Store<St> store);\n\n/// A test of whether or not your `converter` or `vm` function should run in\n/// response to a State change. For advanced use only.\n/// Some changes to the State of your application will mean your `converter`\n/// or `vm` function can't produce a useful Model. In these cases, such as when\n/// performing exit animations on data that has been removed from your Store,\n/// it can be best to ignore the State change while your animation completes.\n/// To ignore a change, provide a function that returns true or false. If the\n/// returned value is false, the change will be ignored.\n/// If you ignore a change, and the framework needs to rebuild the Widget, the\n/// `builder` function will be called with the latest Model produced\n/// by your `converter` or `vm` functions.\ntypedef ShouldUpdateModel<St> = bool Function(St state);\n\n/// A function that will be run on state change, before the build method.\n/// This function is passed the `Model`, and if `distinct` is `true`,\n/// it will only be called if the `Model` changes.\n/// This is useful for making calls to other classes, such as a\n/// `Navigator` or `TabController`, in response to state changes.\n/// It can also be used to trigger an action based on the previous state.\ntypedef OnWillChangeCallback<St, Model> = void Function(\n    BuildContext? context, Store<St> store, Model previousVm, Model newVm);\n\n/// A function that will be run on State change, after the build method.\n///\n/// This function is passed the `Model`, and if `distinct` is `true`,\n/// it will only be called if the `Model` changes.\n/// This can be useful for running certain animations after the build is complete.\n/// Note: Using a [BuildContext] inside this callback can cause problems if\n/// the callback performs navigation. For navigation purposes, please use\n/// an [OnWillChangeCallback].\ntypedef OnDidChangeCallback<St, Model> = void Function(\n    BuildContext? context, Store<St> store, Model viewModel);\n\n/// A function that will be run after the Widget is built the first time.\n/// This function is passed the store and the initial `Model` created by the [vm]\n/// or the [converter] function. This can be useful for starting certain animations,\n/// such as showing Snackbars, after the Widget is built the first time.\ntypedef OnInitialBuildCallback<St, Model> = void Function(\n    BuildContext? context, Store<St> store, Model viewModel);\n\n/// Build a Widget using the [BuildContext] and [Model].\n/// The [Model] is derived from the [Store] using a [StoreConverter].\ntypedef ViewModelBuilder<Model> = Widget Function(\n  BuildContext context,\n  Model vm,\n);\n\n/// The aspect function type for selectors.\n/// Takes the new state value and returns true if the widget should rebuild.\ntypedef SelectorAspect<St> = bool Function(St? value);\n\n/// Storage class for selector dependencies.\n/// Stores all selector aspects for a single dependent widget.\nclass SelectorDependency<St> {\n  /// Flag indicating selectors should be cleared on next registration\n  bool shouldClearSelectors = false;\n\n  /// Flag tracking if a microtask to clear is scheduled\n  bool shouldClearMutationScheduled = false;\n\n  /// List of all aspect functions registered by this widget\n  final selectors = <SelectorAspect<St>>[];\n}\n\n/// Debug flag to prevent nested select calls.\nbool _debugIsSelecting = false;\n\n// Debug flag to enable logging for `select` mechanism (development only).\nconst bool _debugSelectLogging = false;\n\nabstract class StoreConnectorInterface<St, Model> {\n  VmFactory<St, dynamic, dynamic> Function()? get vm;\n\n  StoreConverter<St, Model>? get converter;\n\n  bool? get distinct;\n\n  OnInitCallback<St>? get onInit;\n\n  OnDisposeCallback<St>? get onDispose;\n\n  bool get rebuildOnChange;\n\n  ShouldUpdateModel<St>? get shouldUpdateModel;\n\n  OnWillChangeCallback<St, Model>? get onWillChange;\n\n  OnDidChangeCallback<St, Model>? get onDidChange;\n\n  OnInitialBuildCallback<St, Model>? get onInitialBuild;\n\n  Object? get debug;\n}\n\n/// Build a widget based on the state of the [Store].\n///\n/// Before the [builder] is run, the [converter] will convert the store into a\n/// more specific `Model` tailored to the Widget being built.\n///\n/// Every time the store changes, the Widget will be rebuilt. As a performance\n/// optimization, the Widget can be rebuilt only when the [Model] changes.\n/// In order for this to work correctly, you must implement [==] and [hashCode] for\n/// the [Model], and set the [distinct] option to true when creating your StoreConnector.\n///\n/// **IMPORTANT:**\n///  With the release of [MockBuildContext], the [StoreConnector] is now\n///  considered deprecated. It will not be marked as deprecated and will not be\n///  removed, but you should avoid it for new code.\n///  For new code, prefer [BuildContext] extensions with [MockBuildContext] for\n///  testing.\n///\n///  The goal of [StoreConnector] was to separate dumb widgets from smart\n///  widgets and let you test the view model without mounting it. Then you could\n///  test the dumb widget with simple presentation tests.\n///  `MockBuildContext` gives you the same benefits, because the dumb widget\n///  itself, when built with a mock context, works as the view model you can\n///  inspect and use to call callbacks.\n///\n///  This makes `StoreConnector` unnecessary. `MockBuildContext` is simpler to\n///  use and avoids extra view model classes and factories.\n///\nclass StoreConnector<St, Model> extends StatelessWidget\n    implements StoreConnectorInterface<St, Model> {\n  //\n  /// Build a Widget using the [BuildContext] and [Model]. The [Model]\n  /// is created by the [vm] or [converter] functions.\n  final ViewModelBuilder<Model> builder;\n\n  /// Convert the [Store] into a [Model]. The resulting [Model] will be\n  /// passed to the [builder] function.\n  @override\n  final VmFactory<St, dynamic, dynamic> Function()? vm;\n\n  /// Convert the [Store] into a [Model]. The resulting [Model] will be\n  /// passed to the [builder] function.\n  @override\n  final StoreConverter<St, Model>? converter;\n\n  /// When [distinct] is true (the default), the Widget is rebuilt only\n  /// when the [Model] changes. In order for this to work correctly, you\n  /// must implement [==] and [hashCode] for the [Model].\n  @override\n  final bool? distinct;\n\n  /// A function that will be run when the StoreConnector is initially created.\n  /// It is run in the [State.initState] method.\n  /// This can be useful for dispatching actions that fetch data for your Widget\n  /// when it is first displayed.\n  @override\n  final OnInitCallback<St>? onInit;\n\n  /// A function that will be run when the StoreConnector is removed from the\n  /// Widget Tree. It is run in the [State.dispose] method.\n  /// This can be useful for dispatching actions that remove stale data from your State tree.\n  @override\n  final OnDisposeCallback<St>? onDispose;\n\n  /// Determines whether the Widget should be rebuilt when the Store emits an onChange event.\n  @override\n  final bool rebuildOnChange;\n\n  /// A test of whether or not your [vm] or [converter] function should run in\n  /// response to a State change. For advanced use only.\n  /// Some changes to the State of your application will mean your [vm] or\n  /// [converter] function can't produce a useful Model. In these cases, such as\n  /// when performing exit animations on data that has been removed from your Store,\n  /// it can be best to ignore the State change while your animation completes.\n  /// To ignore a change, provide a function that returns true or false.\n  /// If the returned value is true, the change will be applied.\n  /// If the returned value is false, the change will be ignored.\n  /// If you ignore a change, and the framework needs to rebuild the Widget,\n  /// the [builder] function will be called with the latest [Model] produced\n  /// by your [vm] or [converter] function.\n  @override\n  final ShouldUpdateModel<St>? shouldUpdateModel;\n\n  /// A function that will be run on State change, before the Widget is built.\n  /// This function is passed the `Model`, and if `distinct` is `true`,\n  /// it will only be called if the `Model` changes.\n  /// This can be useful for imperative calls to things like Navigator, TabController, etc\n  @override\n  final OnWillChangeCallback<St, Model>? onWillChange;\n\n  /// A function that will be run on State change, after the Widget is built.\n  /// This function is passed the `Model`, and if `distinct` is `true`,\n  /// it will only be called if the `Model` changes.\n  /// This can be useful for running certain animations after the build is complete.\n  /// Note: Using a [BuildContext] inside this callback can cause problems if\n  /// the callback performs navigation. For navigation purposes, please use [onWillChange].\n  @override\n  final OnDidChangeCallback<St, Model>? onDidChange;\n\n  /// A function that will be run after the Widget is built the first time.\n  /// This function is passed the store and the initial `Model` created by\n  /// the `vm` or the `converter` function. This can be useful for starting certain\n  /// animations, such as showing snackbars, after the Widget is built the first time.\n  @override\n  final OnInitialBuildCallback<St, Model>? onInitialBuild;\n\n  /// Pass the parameter `debug: this` to get a more detailed error message.\n  @override\n  final Object? debug;\n\n  const StoreConnector({\n    Key? key,\n    required this.builder,\n    this.distinct,\n    this.vm, // Recommended.\n    this.converter, // Can be used instead of `vm`.\n    this.debug,\n    this.onInit,\n    this.onDispose,\n    this.rebuildOnChange = true,\n    this.shouldUpdateModel,\n    this.onWillChange,\n    this.onDidChange,\n    this.onInitialBuild,\n  })  : assert(converter == null || vm == null,\n            \"You can't provide both `converter` and `vm`.\"),\n        assert(converter != null || vm != null,\n            \"You should provide the `converter` or the `vm` parameter.\"),\n        super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return _StoreStreamListener<St, Model>(\n      store: StoreProvider.backdoorInheritedWidget<St>(context, debug: debug),\n      debug: debug,\n      storeConnector: this,\n      builder: builder,\n      converter: converter,\n      vm: vm,\n      distinct: distinct,\n      onInit: onInit,\n      onDispose: onDispose,\n      rebuildOnChange: rebuildOnChange,\n      shouldUpdateModel: shouldUpdateModel,\n      onWillChange: onWillChange,\n      onDidChange: onDidChange,\n      onInitialBuild: onInitialBuild,\n    );\n  }\n\n  /// This is not used directly by the store, but may be used in tests.\n  /// If you have a store and a StoreConnector, and you want its associated\n  /// ViewModel, you can do:\n  /// `Model viewModel = storeConnector.getLatestModel(store);`\n  ///\n  /// And if you want to build the widget:\n  /// `var widget = (storeConnector as dynamic).builder(context, viewModel);`\n  ///\n  Model getLatestModel(Store store) {\n    //\n    // The `vm` parameter is recommended.\n    if (vm != null) {\n      var factory = vm!();\n      internalsVmFactoryInject(factory, store.state, store);\n      return internalsVmFactoryFromStore(factory) as Model;\n    }\n    //\n    // The `converter` parameter can be used instead of `vm`.\n    else if (converter != null) {\n      return converter!(store as Store<St>);\n    }\n    //\n    else\n      throw AssertionError(\"View-model can't be created. \"\n          \"Please provide the vm or the converter parameter.\");\n  }\n}\n\n/// Listens to the store and calls builder whenever the store changes.\nclass _StoreStreamListener<St, Model> extends StatefulWidget {\n  final ViewModelBuilder<Model> builder;\n  final StoreConverter<St, Model>? converter;\n  final VmFactory<St, dynamic, dynamic> Function()? vm;\n  final Store<St> store;\n  final Object? debug;\n  final StoreConnectorInterface storeConnector;\n  final bool rebuildOnChange;\n  final bool? distinct;\n  final OnInitCallback<St>? onInit;\n  final OnDisposeCallback<St>? onDispose;\n  final ShouldUpdateModel<St>? shouldUpdateModel;\n  final OnWillChangeCallback<St, Model>? onWillChange;\n  final OnDidChangeCallback<St, Model>? onDidChange;\n  final OnInitialBuildCallback<St, Model>? onInitialBuild;\n\n  const _StoreStreamListener({\n    Key? key,\n    required this.builder,\n    required this.store,\n    required this.debug,\n    required this.converter,\n    required this.vm,\n    required this.storeConnector,\n    this.distinct,\n    this.onInit,\n    this.onDispose,\n    this.rebuildOnChange = true,\n    this.onWillChange,\n    this.onDidChange,\n    this.onInitialBuild,\n    this.shouldUpdateModel,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() {\n    return _StoreStreamListenerState<St, Model>();\n  }\n}\n\n/// If the StoreConnector throws an error.\nclass _ConverterError extends Error {\n  final Object? debug;\n\n  /// The error thrown while running the [StoreConnector.converter] function.\n  final Object error;\n\n  /// The stacktrace that accompanies the [error]\n  @override\n  final StackTrace stackTrace;\n\n  /// Creates a ConverterError with the relevant error and stacktrace.\n  _ConverterError(this.error, this.stackTrace, this.debug);\n\n  @override\n  String toString() {\n    return \"Error creating the view model\"\n        \"${debug == null ? '' : ' (${debug.runtimeType})'}: \"\n        \"$error\\n\\n\"\n        \"$stackTrace\\n\\n\";\n  }\n}\n\nclass _StoreStreamListenerState<St, Model> //\n    extends State<_StoreStreamListener<St, Model>> {\n  Stream<Model>? _stream;\n  Model? _latestModel;\n  _ConverterError? _latestError;\n\n  // If `widget.distinct` was passed, use it. Otherwise, use the store default.\n  bool get _distinct => widget.distinct ?? widget.store.defaultDistinct;\n\n  /// if [StoreConnector.shouldUpdateModel] returns false, we need to know the\n  /// most recent VALID state (it was valid when [StoreConnector.shouldUpdateModel]\n  /// returned true). We save all valid states into [_mostRecentValidState], and\n  /// when we need to use it we put it into [_forceLastValidStreamState].\n  St? _mostRecentValidState, _forceLastValidStreamState;\n\n  @override\n  void initState() {\n    if (widget.onInit != null) {\n      widget.onInit!(widget.store);\n    }\n\n    _computeLatestModel();\n    if (widget.shouldUpdateModel != null) {\n      // The initial state has to be valid at this point.\n      // This is needed so that the first stream event\n      // can be compared against a baseline.\n      _mostRecentValidState = widget.store.state;\n    }\n\n    if ((widget.onInitialBuild != null) && (_latestModel != null)) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        widget.onInitialBuild!(\n          mounted ? context : null,\n          widget.store,\n          _latestModel!,\n        );\n      });\n    }\n\n    _createStream();\n\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    if (widget.onDispose != null) {\n      widget.onDispose!(widget.store);\n    }\n\n    super.dispose();\n  }\n\n  @override\n  void didUpdateWidget(_StoreStreamListener<St, Model> oldWidget) {\n    _computeLatestModel();\n\n    if (widget.store != oldWidget.store) {\n      _createStream();\n    }\n\n    super.didUpdateWidget(oldWidget);\n  }\n\n  void _computeLatestModel() {\n    try {\n      _latestError = null;\n      _latestModel =\n          getLatestModel(_forceLastValidStreamState ?? widget.store.state);\n    } catch (error, stacktrace) {\n      _latestModel = null;\n      _latestError = _ConverterError(error, stacktrace, widget.debug);\n    }\n  }\n\n  void _createStream() => _stream = widget.store.onChange\n      // This prevents unnecessary calculations of the view-model.\n      .where(_stateChanged)\n      // Discards invalid states.\n      .where(_shouldUpdateModel)\n      // Calculates the view-model using the `vm` or `converter` functions.\n      .map(_calculateModel)\n      // Don't use `Stream.distinct` because it cannot capture the initial\n      // ViewModel produced by the `converter`.\n      .where(_whereDistinct)\n      // Updates the latest-model with the calculated vm.\n      // Important: This must be done after all other optional\n      // transformations, such as shouldUpdateModel.\n      .transform(StreamTransformer.fromHandlers(\n        handleData: _handleData as void Function(Model?, EventSink<Model>)?,\n        handleError: _handleError,\n      ));\n\n  // This prevents unnecessary calculations of the view-model.\n  bool _stateChanged(St state) {\n    return !identical(_mostRecentValidState, widget.store.state) ||\n        _actionsInProgressHaveChanged();\n  }\n\n  /// Used by [_actionsInProgressHaveChanged].\n  Set<ReduxAction<St>> _lastActionsInProgress =\n      HashSet<ReduxAction<St>>.identity();\n\n  /// Returns true if the actions in progress have changed since the last time we checked.\n  bool _actionsInProgressHaveChanged() {\n    if (widget.store.actionsInProgressEqualTo(_lastActionsInProgress))\n      return false;\n    else {\n      _lastActionsInProgress = widget.store.copyActionsInProgress();\n      return true;\n    }\n  }\n\n  // If `shouldUpdateModel` is provided, it will calculate if the STORE state contains\n  // a valid state which may be used to calculate the view-model. If this is not the\n  // case, we revert to the last known valid state, which may be a STORE state or a\n  // STREAM state. Note the view-model is always calculated from the STORE state,\n  // which is always the same or more recent than the STREAM state. We could greatly\n  // simplify all of this if the view-model used the STREAM state. However, this would\n  // mean some small delay in the UI, and there is also the problem that the converter\n  // parameter uses the STORE.\n  bool _shouldUpdateModel(St state) {\n    if (widget.shouldUpdateModel == null)\n      return true;\n    else {\n      _forceLastValidStreamState = null;\n      bool ifStoreHasValidModel = widget.shouldUpdateModel!(widget.store.state);\n      if (ifStoreHasValidModel) {\n        _mostRecentValidState = widget.store.state;\n        return true;\n      }\n      //\n      else {\n        //\n        bool ifStreamHasValidModel = widget.shouldUpdateModel!(state);\n        if (ifStreamHasValidModel) {\n          _mostRecentValidState = state;\n          return false;\n        } else {\n          if (identical(state, widget.store.state)) {\n            _forceLastValidStreamState = _mostRecentValidState;\n          }\n        }\n      }\n\n      return (_forceLastValidStreamState != null);\n    }\n  }\n\n  Model? _calculateModel(St state) =>\n      getLatestModel(_forceLastValidStreamState ?? widget.store.state);\n\n  // Don't use `Stream.distinct` since it can't capture the initial vm.\n  bool _whereDistinct(Model? vm) {\n    if (_distinct) {\n      bool isDistinct = _isDistinct(vm);\n\n      _observeWithTheModelObserver(\n        modelPrevious: _latestModel,\n        modelCurrent: vm,\n        isDistinct: isDistinct,\n      );\n\n      return isDistinct;\n    } else\n      return true;\n  }\n\n  bool _isDistinct(Model? vm) {\n    if ((vm is ImmutableCollection) &&\n        (_latestModel is ImmutableCollection) &&\n        widget.store.immutableCollectionEquality != null) {\n      if (widget.store.immutableCollectionEquality == CompareBy.byIdentity)\n        return areSameImmutableCollection(\n            vm, _latestModel as ImmutableCollection?);\n      if (widget.store.immutableCollectionEquality == CompareBy.byDeepEquals) {\n        return areImmutableCollectionsWithEqualItems(\n            vm, _latestModel as ImmutableCollection?);\n      } else\n        throw AssertionError(widget.store.immutableCollectionEquality);\n    } else\n      return vm != _latestModel;\n  }\n\n  void _handleData(Model vm, EventSink<Model> sink) {\n    //\n    if (!_distinct)\n      _observeWithTheModelObserver(\n        modelPrevious: _latestModel,\n        modelCurrent: vm,\n        isDistinct: _distinct,\n      );\n\n    _latestError = null;\n\n    if ((widget.onWillChange != null) && (_latestModel != null)) {\n      widget.onWillChange!(\n        mounted ? context : null,\n        widget.store,\n        _latestModel!,\n        vm,\n      );\n    }\n\n    _latestModel = vm;\n\n    if ((widget.onDidChange != null) && (_latestModel != null)) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        widget.onDidChange!(\n          mounted ? context : null,\n          widget.store,\n          _latestModel!,\n        );\n      });\n    }\n\n    sink.add(vm);\n  }\n\n  // If the view-model construction failed.\n  void _handleError(\n    Object error,\n    StackTrace stackTrace,\n    EventSink<Model> sink,\n  ) {\n    _latestModel = null;\n    _latestError = _ConverterError(error, stackTrace, widget.debug);\n    sink.addError(error, stackTrace);\n  }\n\n  // If there is a ModelObserver, observe.\n  // Note: This observer is only useful for tests.\n  void _observeWithTheModelObserver<Model>({\n    required Model? modelPrevious,\n    required Model? modelCurrent,\n    required bool isDistinct,\n  }) {\n    try {\n      widget.store.modelObserver?.observe(\n        modelPrevious: modelPrevious,\n        modelCurrent: modelCurrent,\n        isDistinct: isDistinct,\n        storeConnector: widget.storeConnector,\n        reduceCount: widget.store.reduceCount,\n        dispatchCount: widget.store.dispatchCount,\n      );\n    } catch (error, stackTrace) {\n      // The errorObserver should never throw. However, if it does, print the error.\n      _throws(\"Method 'ModelObserver.observe()' has thrown an error\", error,\n          stackTrace);\n    }\n  }\n\n  /// Throws the error after an asynchronous gap.\n  void _throws(errorMsg, Object? error, StackTrace stackTrace) {\n    Future(() {\n      Error.throwWithStackTrace(\n        (error == null) ? errorMsg : \"$errorMsg:\\n  $error\",\n        stackTrace,\n      );\n    });\n  }\n\n  /// The StoreConnector needs the converter or vm parameter (only one of them):\n  /// 1) Converter gets the `store`.\n  /// 2) Vm gets `state` and `dispatch`, so it's easier to use.\n  ///\n  Model getLatestModel(St state) {\n    //\n    // The `vm` parameter is recommended.\n    if (widget.vm != null) {\n      var factory = widget.vm!();\n      internalsVmFactoryInject(factory, state, widget.store);\n      return internalsVmFactoryFromStore(factory) as Model;\n    }\n    //\n    // The `converter` parameter can be used instead of `vm`.\n    else if (widget.converter != null) {\n      return widget.converter!(widget.store);\n    }\n    //\n    else\n      throw AssertionError(\"View-model can't be created. \"\n          \"Please provide vm or converter parameter.\");\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return widget.rebuildOnChange\n        ? StreamBuilder<Model>(\n            stream: _stream,\n            builder: (context, snapshot) => (_latestError != null)\n                ? throw _latestError!\n                : widget.builder(context, _latestModel as Model),\n          )\n        : _latestError != null\n            ? throw _latestError!\n            : widget.builder(context, _latestModel as Model);\n  }\n}\n\n/// Provides a Redux [Store] to all ancestors of this Widget.\n/// This should generally be a root widget in your App.\n///\n/// Then, you have two alternatives to access the store:\n///\n/// 1) Connect to the provided store by using a [StoreConnector], and\n/// the [StoreConnector.vm] parameter:\n///\n/// ```dart\n/// StoreConnector(\n///    vm: () => Factory(this),\n///    builder: (context, vm) => MyHomePage(user: vm.user)\n/// );\n/// ```\n///\n/// See the documentation for more information on how to create the view-model using the `vm`\n/// parameter and a `VmFactory` class.\n///\n/// 2) Connect to the provided store by using a [StoreConnector], and\n/// the [StoreConnector.converter] parameter:\n///\n/// ```dart\n/// StoreConnector(\n///    converter: (Store<AppState> store) => store.state.counter,\n///    builder: (context, value) => Text('$value', style: const TextStyle(fontSize: 30)),\n/// );\n/// ```\n/// See the documentation for more information on how to use the `converter` parameter.\n///\n/// 3) Use the extension methods on [BuildContext], like explained below:\n///\n/// You can read the state of the store using the `context.state` method:\n///\n/// ```dart\n/// var state = context.state;\n/// ```\n///\n/// You can dispatch actions using the [dispatch], [dispatchAll], [dispatchAndWait],\n/// [dispatchAndWaitAll] and [dispatchSync] methods:\n///\n/// ```dart\n/// context.dispatch(action);\n/// context.dispatchAll([action1, action2]);\n/// context.dispatchAndWait(action);\n/// context.dispatchAndWaitAll([action1, action2]);\n/// context.dispatchSync(action);\n/// ```\n///\n/// You can also use `context.isWaiting`, `context.isFailed()`, `context.exceptionFor()`\n/// and `context.clearExceptionFor()`.\n///\n/// IMPORTANT: You need to define this extension in your own code:\n///\n/// ```dart\n/// extension BuildContextExtension on BuildContext {\n///   AppState get state => getState<AppState>();\n/// ```\nclass StoreProvider<St> extends InheritedWidget {\n  final Store<St> _store;\n\n  // Explanation\n  // -----------\n  //\n  // The hierarchy is:\n  // StoreProvider -> _InheritedUntypedDoesNotRebuild -> _WidgetListensOnChange -> _InheritedUntypedRebuilds\n  //\n  // Where:\n  // * StoreProvider is a public, <St> TYPED inherited widget, from where we read\n  //       the `state` of type `St`.\n  //\n  // * _InheritedUntypedDoesNotRebuild is an UNTYPED inherited widget used by `dispatch`,\n  //       `dispatchAndWait` and `dispatchSync`. That's useful because they can dispatch without\n  //       the knowing the St type, but it DOES NOT REBUILD.\n  //\n  // * _WidgetListensOnChange is a StatefulWidget that listens to the store (onChange) and\n  //       rebuilds the whenever there is a new state available.\n  //\n  // * _InheritedUntypedRebuilds is an UNTYPED inherited widget that is used by `isWaiting`,\n  //       `isFailed` and `exceptionFor`. That's useful because these methods can find it without\n  //       the knowing the St type, but it REBUILDS. Note: `_InheritedUntypedRebuilds._isOn` is\n  //       true only after `state`, `isWaiting`, `isFailed` and `exceptionFor` are used for the\n  //       first time. This is to make it faster by avoiding `updateShouldNotify` before this\n  //       inner provider is necessary.\n\n  StoreProvider({\n    Key? key,\n    required Store<St> store,\n    required Widget child,\n  })  : _store = _init(store),\n        super(\n          key: key,\n          child: _InheritedUntypedDoesNotRebuild(store: store, child: child),\n        );\n\n  /// Provides easy access to the AsyncRedux store state from a BuildContext.\n  ///\n  /// Use this in your widget's build method to read the current store state.\n  /// Any widget that calls this WILL rebuild automatically when the state\n  /// changes (unless you pass the [notify] parameter as `false`).\n  ///\n  /// For convenience, it's recommended that you define this extension in your\n  /// own code:\n  /// ```dart\n  /// extension BuildContextExtension on BuildContext {\n  ///   AppState get state => getState<AppState>();\n  /// }\n  /// ```\n  ///\n  /// And then use it like this:\n  ///\n  /// ```dart\n  /// var state = context.state;\n  /// ```\n  static St state<St>(BuildContext context,\n      {bool notify = true, Object? debug}) {\n    if (notify) {\n      final _InheritedUntypedRebuilds? provider = context\n          .dependOnInheritedWidgetOfExactType<_InheritedUntypedRebuilds>();\n\n      if (provider == null)\n        throw throw _exceptionForWrongStoreType(\n            _typeOf<_InheritedUntypedRebuilds>(),\n            debug: debug);\n\n      St state;\n      try {\n        state = provider._store.state as St;\n      } catch (error) {\n        throw _exceptionForWrongStateType(provider._store.state, St);\n      }\n\n      // We only turn on rebuilds when this `state` method is used for the first time.\n      // This is to make it faster when this method is not used, which is the\n      // case if the state is only accessed via StoreConnector.\n      _InheritedUntypedRebuilds._isOn = true;\n\n      return state;\n    }\n    // Get the state without rebuilding when the state later changes.\n    else {\n      return backdoorInheritedWidget<St>(context, debug: debug).state;\n    }\n  }\n\n  /// This WILL create a dependency, and WILL potentially rebuild the state.\n  /// You don't need `St` to call this method.\n  static Store<St> _getStoreWithDependency_Untyped<St>(BuildContext context,\n      {Object? debug}) {\n    //\n    final _InheritedUntypedRebuilds? provider =\n        context.dependOnInheritedWidgetOfExactType<_InheritedUntypedRebuilds>();\n\n    if (provider == null)\n      throw _exceptionForWrongStoreType(_typeOf<_InheritedUntypedRebuilds>(),\n          debug: debug);\n\n    // We only turn on rebuilds when this `state` method is used for the first\n    // time. This is to make it faster when this method is not used, which is\n    // the case if the state is only accessed via StoreConnector.\n    _InheritedUntypedRebuilds._isOn = true;\n\n    return provider._store as Store<St>;\n  }\n\n  /// This WILL NOT create a dependency, and may NOT rebuild the state.\n  /// You don't need `St` to call this method.\n  static Store<St> _getStoreNoDependency_Untyped<St>(BuildContext context,\n      {Object? debug}) {\n    //\n    try {\n      // Try to get the store from the dependency.\n      final element = context.getElementForInheritedWidgetOfExactType<\n          _InheritedUntypedDoesNotRebuild>();\n\n      if (element == null)\n        throw _exceptionForWrongStoreType(StoreException, debug: debug);\n\n      final widget = element.widget as _InheritedUntypedDoesNotRebuild;\n      return widget._store as Store<St>;\n    }\n    //\n    // Try to get the store from the static global backdoor. Only works in\n    // production, since in tests there may be more than one store-provider.\n    catch (error) {\n      try {\n        return backdoorStaticGlobal<St>();\n      } catch (e) {\n        // Swallow.\n      }\n\n      // Rethrow the original error when getting the store from the dependency.\n      rethrow;\n    }\n  }\n\n  /// Workaround to capture generics.\n  static Type _typeOf<T>() => T;\n\n  /// Dispatch an action with [ReduxAction.dispatch]\n  /// without needing a `StoreConnector`. Example:\n  ///\n  /// ```dart\n  /// StoreProvider.dispatch(context, MyAction());\n  /// ```\n  ///\n  /// However, it's recommended that you use the built-in `BuildContext` extension instead:\n  ///\n  /// ```dart\n  /// context.dispatch(action)`.\n  /// ```\n  static FutureOr<ActionStatus> dispatch<St>(\n          BuildContext context, ReduxAction<St> action,\n          {Object? debug, bool notify = true}) =>\n      _getStoreNoDependency_Untyped(context, debug: debug)\n          .dispatch(action, notify: notify);\n\n  /// Dispatch an action with [ReduxAction.dispatchSync]\n  /// without needing a `StoreConnector`. Example:\n  ///\n  /// ```dart\n  /// StoreProvider.dispatchSync(context, MyAction());\n  /// ```\n  ///\n  /// However, it's recommended that you use the built-in `BuildContext` extension instead:\n  ///\n  /// ```dart\n  /// context.dispatchSync(action)`.\n  /// ```\n  static ActionStatus dispatchSync<St>(\n          BuildContext context, ReduxAction<St> action,\n          {Object? debug, bool notify = true}) =>\n      _getStoreNoDependency_Untyped(context, debug: debug)\n          .dispatchSync(action, notify: notify);\n\n  /// Dispatch an action with [ReduxAction.dispatchAndWait]\n  /// without needing a `StoreConnector`. Example:\n  ///\n  /// ```dart\n  /// var status = await StoreProvider.dispatchAndWait(context, MyAction());\n  /// ```\n  ///\n  /// However, it's recommended that you use the built-in `BuildContext` extension instead:\n  ///\n  /// ```dart\n  /// var status = await context.dispatchAndWait(action)`.\n  /// ```\n  static Future<ActionStatus> dispatchAndWait<St>(\n          BuildContext context, ReduxAction<St> action,\n          {Object? debug, bool notify = true}) =>\n      _getStoreNoDependency_Untyped(context, debug: debug)\n          .dispatchAndWait(action, notify: notify);\n\n  /// Dispatch a list of actions with [ReduxAction.dispatchAll]\n  /// without needing a `StoreConnector`. Example:\n  ///\n  /// ```dart\n  /// StoreProvider.dispatchAll(context, [Action1(), Action2()]);\n  /// ```\n  ///\n  /// However, it's recommended that you use the built-in `BuildContext` extension instead:\n  ///\n  /// ```dart\n  /// context.dispatchAll([Action1(), Action2()])`.\n  /// ```\n  static List<ReduxAction<St>> dispatchAll<St>(\n    BuildContext context,\n    List<ReduxAction<St>> actions, {\n    Object? debug,\n    bool notify = true,\n  }) =>\n      _getStoreNoDependency_Untyped<St>(context, debug: debug)\n          .dispatchAll(actions, notify: notify);\n\n  /// Dispatch a list of actions with [ReduxAction.dispatchAndWaitAll]\n  /// without needing a `StoreConnector`. Example:\n  ///\n  /// ```dart\n  /// var status = await StoreProvider.dispatchAndWaitAll(context, [Action1(), Action2()]);\n  /// ```\n  ///\n  /// However, it's recommended that you use the built-in `BuildContext` extension instead:\n  ///\n  /// ```dart\n  /// var status = await context.dispatchAndWaitAll([Action1(), Action2()])`.\n  /// ```\n  static Future<List<ReduxAction<St>>> dispatchAndWaitAll<St>(\n    BuildContext context,\n    List<ReduxAction<St>> actions, {\n    Object? debug,\n    bool notify = true,\n  }) =>\n      _getStoreNoDependency_Untyped<St>(context, debug: debug)\n          .dispatchAndWaitAll(actions, notify: notify);\n\n  /// Returns a future which will complete when the given state [condition] is true.\n  /// If the condition is already true when the method is called, the future completes immediately.\n  ///\n  /// You may also provide a [timeoutMillis], which by default is 10 minutes.\n  /// To disable the timeout, make it -1.\n  /// If you want, you can modify [Store.defaultTimeoutMillis] to change the default timeout.\n  ///\n  /// ```dart\n  /// var action = await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(action, isA<ChangeNameAction>());\n  /// ```\n  static Future<ReduxAction<St>?> waitCondition<St>(\n    BuildContext context,\n    bool Function(St) condition, {\n    int? timeoutMillis,\n  }) =>\n      backdoorInheritedWidget<St>(context)\n          .waitCondition(condition, timeoutMillis: timeoutMillis);\n\n  /// Returns a future that completes when ALL given [actions] finished dispatching.\n  ///\n  /// Example:\n  ///\n  /// ```ts\n  /// // Dispatching two actions in PARALLEL and waiting for both to finish.\n  /// var action1 = ChangeNameAction('Bill');\n  /// var action2 = ChangeAgeAction(42);\n  /// await waitAllActions([action1, action2]);\n  ///\n  /// // Compare this to dispatching the actions in SERIES:\n  /// await dispatchAndWait(action1);\n  /// await dispatchAndWait(action2);\n  /// ```\n  static Future<void> waitAllActions<St>(\n      BuildContext context, List<ReduxAction<St>> actions) {\n    if (actions.isEmpty)\n      throw StoreException('You have to provide a non-empty list of actions.');\n    return backdoorInheritedWidget<St>(context).waitAllActions(actions);\n  }\n\n  /// You can use [isWaiting] and pass it [actionOrTypeOrList] to check if:\n  /// * A specific async ACTION is currently being processed.\n  /// * An async action of a specific TYPE is currently being processed.\n  /// * If any of a few given async actions or action types is currently being processed.\n  ///\n  /// If you wait for an action TYPE, then it returns false when:\n  /// - The ASYNC action of the type is NOT currently being processed.\n  /// - If the type is not really a type that extends [ReduxAction].\n  /// - The action of the type is a SYNC action (since those finish immediately).\n  ///\n  /// If you wait for an ACTION, then it returns false when:\n  /// - The ASYNC action is NOT currently being processed.\n  /// - If the action is a SYNC action (since those finish immediately).\n  ///\n  /// Trying to wait for any other type of object will return null and throw\n  /// a [StoreException] after the async gap.\n  ///\n  /// Widgets that use this method WILL rebuild whenever the state changes\n  /// (unless you pass the [notify] parameter as `false`).\n  ///\n  static bool isWaiting(\n    BuildContext context,\n    Object actionOrTypeOrList, {\n    bool notify = true,\n  }) =>\n      (notify\n              ? _getStoreWithDependency_Untyped\n              : _getStoreNoDependency_Untyped)(context)\n          .isWaiting(actionOrTypeOrList);\n\n  /// Returns true if an [actionOrTypeOrList] failed with an [UserException].\n  ///\n  /// It's recommended that you use the BuildContext extension instead:\n  ///\n  /// ```dart\n  /// if (context.isFailed(MyAction)) { // Show an error message. }\n  /// ```\n  ///\n  /// Widgets that use this method WILL rebuild whenever the state changes\n  /// (unless you pass the [notify] parameter as `false`).\n  ///\n  static bool isFailed(\n    BuildContext context,\n    Object actionOrTypeOrList, {\n    bool notify = true,\n  }) =>\n      (notify\n              ? _getStoreWithDependency_Untyped\n              : _getStoreNoDependency_Untyped)(context)\n          .isFailed(actionOrTypeOrList);\n\n  /// Returns the [UserException] of the [actionTypeOrList] that failed.\n  ///\n  /// The [actionTypeOrList] can be a [Type], or an Iterable of types. Any other type\n  /// of object will return null and throw a [StoreException] after the async gap.\n  ///\n  /// It's recommended that you use the BuildContext extension instead:\n  ///\n  /// ```dart\n  /// if (context.isFailed(SaveUserAction)) Text(context.exceptionFor(SaveUserAction)!.reason ?? '');\n  /// ```\n  ///\n  /// Widgets that use this method WILL rebuild whenever the state changes\n  /// (unless you pass the [notify] parameter as `false`).\n  ///\n  static UserException? exceptionFor(\n    BuildContext context,\n    Object actionOrTypeOrList, {\n    bool notify = true,\n  }) =>\n      (notify\n              ? _getStoreWithDependency_Untyped\n              : _getStoreNoDependency_Untyped)(context)\n          .exceptionFor(actionOrTypeOrList);\n\n  /// Removes the given [actionTypeOrList] from the list of action types that failed.\n  ///\n  /// Note that dispatching an action already removes that action type from the exceptions list.\n  /// This removal happens as soon as the action is dispatched, not when it finishes.\n  ///\n  /// [actionTypeOrList] can be a [Type], or an Iterable of types. Any other type\n  /// of object will return null and throw a [StoreException] after the async gap.\n  ///\n  /// Widgets that use this method WILL rebuild whenever the state changes\n  /// (unless you pass the [notify] parameter as `false`).\n  ///\n  static void clearExceptionFor(\n    BuildContext context,\n    Object actionOrTypeOrList, {\n    bool notify = true,\n  }) =>\n      (notify\n              ? _getStoreWithDependency_Untyped\n              : _getStoreNoDependency_Untyped)(context)\n          .clearExceptionFor(actionOrTypeOrList);\n\n  /// Avoid using if you don't have a good reason to do so.\n  ///\n  /// The [backdoorInheritedWidget] gives you direct access to the store for advanced\n  /// use-cases. It does NOT create a dependency like [_getStoreWithDependency_Untyped] does,\n  /// and it does NOT rebuild the state when the state changes, when you access it like this:\n  /// `var state = StoreProvider.backdoorInheritedWidget(context, this).state;`.\n  ///\n  static Store<St> backdoorInheritedWidget<St>(BuildContext context,\n      {Object? debug}) {\n    //\n    final element =\n        context.getElementForInheritedWidgetOfExactType<StoreProvider<St>>();\n    final StoreProvider<St>? provider = element?.widget as StoreProvider<St>?;\n\n    if (provider == null)\n      throw _exceptionForWrongStoreType(_typeOf<StoreProvider<St>>(),\n          debug: debug);\n\n    return provider._store;\n  }\n\n  /// Avoid using this if you don't have a good reason to do so.\n  ///\n  /// The [backdoorStaticGlobal] gives you direct access to the store for\n  /// advanced use-cases. It does NOT need the context, as it gets the store\n  /// from the static field [_staticStoreBackdoor].\n  ///\n  /// Note this field is set when the [StoreProvider] is created, which assumes\n  /// the [StoreProvider] is used only once in your app. This is usually a\n  /// reasonable assumption in production, but can break in tests.\n  ///\n  /// It is similar to [_getStoreNoDependency_Untyped] in that is does not\n  /// create a dependency, but it does not need the context, which means\n  /// you can use it anywhere, even outside of the widget tree.\n  ///\n  /// Use it like this:\n  ///\n  /// ```dart\n  /// var state = StoreProvider.backdoorStaticGlobal<AppState>().state;`.\n  /// ```\n  ///\n  static Store<St> backdoorStaticGlobal<St>() {\n    if (_staticStoreBackdoor == null)\n      throw StoreException('Error: No Redux store found. '\n          'Did you forget to use the StoreProvider?');\n\n    if (_staticStoreBackdoor is! Store<St>) {\n      var type = _typeOf<Store<St>>;\n      throw StoreException(\n          'Error: Store is of type ${_staticStoreBackdoor.runtimeType} '\n          'and not of type $type. Please provide the correct type.');\n    }\n\n    return _staticStoreBackdoor as Store<St>;\n  }\n\n  static Store backdoorStaticGlobalUntyped() {\n    if (_staticStoreBackdoor == null)\n      throw StoreException('Error: No Redux store found. '\n          'Did you forget to use the StoreProvider?');\n\n    return _staticStoreBackdoor!;\n  }\n\n  /// See [backdoorStaticGlobal].\n  static Store? _staticStoreBackdoor;\n\n  static Store<St> _init<St>(Store<St> store) {\n    _staticStoreBackdoor = store;\n    return store;\n  }\n\n  @override\n  bool updateShouldNotify(StoreProvider<St> oldWidget) {\n    // Only notify dependents if the store instance changes,\n    // not on every state change within the store.\n    return _store != oldWidget._store;\n  }\n}\n\n/// An UNTYPED inherited widget used by `dispatch`, `dispatchAndWait` and\n/// `dispatchSync`. That's useful because they can dispatch without the knowing\n/// the St type, but it DOES NOT REBUILD.\nclass _InheritedUntypedDoesNotRebuild extends InheritedWidget {\n  final Store _store;\n\n  _InheritedUntypedDoesNotRebuild({\n    Key? key,\n    required Store store,\n    required Widget child,\n  })  : _store = store,\n        super(\n          key: key,\n          child: _WidgetListensOnChange(store: store, child: child),\n        );\n\n  @override\n  bool updateShouldNotify(_InheritedUntypedDoesNotRebuild oldWidget) {\n    // Only notify dependents if the store instance changes,\n    // not on every state change within the store.\n    return _store != oldWidget._store;\n  }\n}\n\n/// A StatefulWidget that listens to the store (onChange) and\n/// rebuilds the whenever there is a new state available.\nclass _WidgetListensOnChange extends StatefulWidget {\n  final Widget child;\n  final Store store;\n\n  _WidgetListensOnChange({required this.store, required this.child});\n\n  @override\n  _WidgetListensOnChangeState createState() => _WidgetListensOnChangeState();\n}\n\nclass _WidgetListensOnChangeState extends State<_WidgetListensOnChange> {\n  @override\n  void initState() {\n    super.initState();\n    widget.store.onChange.listen((state) {\n      if (mounted) {\n        setState(() {});\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // The Inner InheritedWidget is rebuilt whenever the store's state changes,\n    // triggering rebuilds for widgets that depend on the specific parts of the state.\n    return _InheritedUntypedRebuilds(\n      store: widget.store,\n      child: widget.child,\n    );\n  }\n}\n\n/// An UNTYPED inherited widget that is used by `isWaiting`, `isFailed` and `exceptionFor`.\n/// That's useful because these methods can find it without the knowing the St type, but\n/// it REBUILDS. Note: `_InheritedUntypedRebuilds._isOn` is true only after `state`, `isWaiting`,\n/// `isFailed` and `exceptionFor` are used for the first time. This is to make it faster by\n/// avoiding `updateShouldNotify` before this inner provider is necessary.\n/// This class now also supports selector-based rebuilds for fine-grained state subscriptions.\nclass _InheritedUntypedRebuilds extends InheritedWidget {\n  static var _isOn = false;\n  final Store _store;\n\n  _InheritedUntypedRebuilds({\n    Key? key,\n    required Store store,\n    required Widget child,\n  })  : _store = store,\n        super(key: key, child: child);\n\n  @override\n  _InheritedUntypedRebuildsElement createElement() {\n    return _InheritedUntypedRebuildsElement(this);\n  }\n\n  @override\n  bool updateShouldNotify(_InheritedUntypedRebuilds oldWidget) {\n    return _isOn;\n  }\n}\n\n/// Custom InheritedElement that supports selector-based rebuilds.\nclass _InheritedUntypedRebuildsElement extends InheritedElement {\n  _InheritedUntypedRebuildsElement(_InheritedUntypedRebuilds widget)\n      : super(widget);\n\n  @override\n  _InheritedUntypedRebuilds get widget =>\n      super.widget as _InheritedUntypedRebuilds;\n\n  @override\n  void updateDependencies(Element dependent, Object? aspect) {\n    // We need the state type, but this is untyped. We'll handle dynamic selectors.\n    final dependencies = getDependencies(dependent);\n\n    // DEBUG: Log dependency registration\n    if (_debugSelectLogging) {\n      print('[UPDATE_DEPS] Widget: ${dependent.widget.runtimeType}, '\n          'Has existing deps: ${dependencies != null}, '\n          'Deps type: ${dependencies?.runtimeType}, '\n          'Aspect type: ${aspect?.runtimeType}');\n    }\n\n    // Already listening to everything - don't override with selector.\n    if (dependencies != null && dependencies is! SelectorDependency) {\n      if (_debugSelectLogging) {\n        print('[UPDATE_DEPS] Already listening to everything, returning');\n      }\n      return;\n    }\n\n    if (aspect is SelectorAspect) {\n      // Get or create the dependency object.\n      final selectorDependency =\n          (dependencies ?? SelectorDependency()) as SelectorDependency;\n\n      if (_debugSelectLogging) {\n        print('[UPDATE_DEPS] Selector aspect detected. '\n            'Creating new dependency: ${dependencies == null}, '\n            'Current selector count: ${selectorDependency.selectors.length}');\n      }\n\n      // Clear selectors if flagged (from previous build).\n      if (selectorDependency.shouldClearSelectors) {\n        if (_debugSelectLogging) {\n          print(\n              '[UPDATE_DEPS] Clearing ${selectorDependency.selectors.length} old selectors');\n        }\n        selectorDependency.shouldClearSelectors = false;\n        selectorDependency.selectors.clear();\n      }\n\n      // Schedule selector clearing for next tick.\n      if (selectorDependency.shouldClearMutationScheduled == false) {\n        selectorDependency.shouldClearMutationScheduled = true;\n        if (_debugSelectLogging) {\n          print('[UPDATE_DEPS] Scheduling selector clear for next microtask');\n        }\n        Future.microtask(() {\n          if (_debugSelectLogging) {\n            print(\n                '[UPDATE_DEPS] Microtask executed - marking selectors for clearing');\n          }\n          selectorDependency\n            ..shouldClearMutationScheduled = false\n            ..shouldClearSelectors = true;\n        });\n      }\n\n      // Add the new selector.\n      selectorDependency.selectors.add(aspect);\n      setDependencies(dependent, selectorDependency);\n\n      if (_debugSelectLogging) {\n        print(\n            '[UPDATE_DEPS] Added selector. New count: ${selectorDependency.selectors.length}');\n      }\n    } else {\n      // No aspect = listen to everything (context.state behavior).\n      setDependencies(dependent, const Object());\n      if (_debugSelectLogging) {\n        print('[UPDATE_DEPS] No aspect - listening to everything');\n      }\n    }\n  }\n\n  @override\n  void notifyDependent(InheritedWidget oldWidget, Element dependent) {\n    final dependencies = getDependencies(dependent);\n\n    if (_debugSelectLogging) {\n      print('[NOTIFY] Widget: ${dependent.widget.runtimeType}, '\n          'Has deps: ${dependencies != null}, '\n          'Deps type: ${dependencies?.runtimeType}, '\n          'Is dirty: ${dependent.dirty}');\n    }\n\n    var shouldNotify = false;\n    if (dependencies != null) {\n      if (dependencies is SelectorDependency) {\n        // OPTIMIZATION: Skip if widget is already being rebuilt.\n        if (dependent.dirty) {\n          if (_debugSelectLogging) {\n            print('[NOTIFY] Widget already dirty, skipping');\n          }\n          return;\n        }\n\n        if (_debugSelectLogging) {\n          print('[NOTIFY] Checking ${dependencies.selectors.length} selectors');\n        }\n\n        // Check each selector.\n        int selectorIndex = 0;\n        for (final updateShouldNotify in dependencies.selectors) {\n          try {\n            assert(() {\n              _debugIsSelecting = true;\n              return true;\n            }());\n\n            // Call the aspect function with new value.\n            shouldNotify = updateShouldNotify(widget._store.state);\n\n            if (_debugSelectLogging) {\n              print('[NOTIFY] Selector $selectorIndex returned: $shouldNotify');\n            }\n          } finally {\n            assert(() {\n              _debugIsSelecting = false;\n              return true;\n            }());\n          }\n\n          // OPTIMIZATION: Short-circuit on first true.\n          if (shouldNotify) {\n            if (_debugSelectLogging) {\n              print('[NOTIFY] Selector triggered rebuild, stopping check');\n            }\n            break;\n          }\n          selectorIndex++;\n        }\n      } else {\n        // No selectors = watch everything.\n        shouldNotify = true;\n        if (_debugSelectLogging) {\n          print('[NOTIFY] No selectors - watching everything');\n        }\n      }\n    } else {\n      // If no dependencies registered yet, notify by default.\n      shouldNotify = true;\n      if (_debugSelectLogging) {\n        print(\n            '[NOTIFY] WARNING: No dependencies registered! Notifying by default');\n      }\n    }\n\n    if (shouldNotify) {\n      if (_debugSelectLogging) {\n        print('[NOTIFY] >>> REBUILDING ${dependent.widget.runtimeType}');\n      }\n      dependent.didChangeDependencies();\n    } else {\n      if (_debugSelectLogging) {\n        print('[NOTIFY] Not rebuilding ${dependent.widget.runtimeType}');\n      }\n    }\n  }\n}\n\nStoreException _exceptionForWrongStoreType(Type type, {Object? debug}) {\n  return StoreException(\n      '''Error: No $type found. (debug info: ${debug.runtimeType})\n\n    To fix, please try:\n  \n  * Wrapping your MaterialApp with the StoreProvider<St>, rather than an individual Route\n  * Providing full type information to your Store<St>, StoreProvider<St> and StoreConnector<St, Model>\n  * Ensure you are using consistent and complete imports. E.g. always use `import 'package:my_app/app_state.dart';\n      ''');\n}\n\nStoreException _exceptionForWrongStateType(Object? state, Type wrongType) {\n  return StoreException(\n      'Error: State is of type ${state.runtimeType} but you typed it as $wrongType.');\n}\n\nextension BuildContextExtensionForProviderAndConnector<St> on BuildContext {\n  //\n  /// Provides easy access to the AsyncRedux store state from a BuildContext.\n  ///\n  /// Use this in your widget's build method to watch the current store state.\n  /// Any widget that calls this will rebuild automatically when the state\n  /// changes in any way (even if the part of the state we are actually using\n  /// did not change).\n  ///\n  /// You cannot use [getState] in your `initState` method. If you do, it will\n  /// throw an exception. See [getRead] for an alternative that can be used in\n  /// `initState`.\n  ///\n  /// For convenience, it's recommended that you define this extension in your\n  /// own code:\n  ///\n  /// ```dart\n  /// extension BuildContextExtension on BuildContext {\n  ///   AppState get state => getState<AppState>();\n  ///   AppState read() => getRead<AppState>();\n  ///   R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n  ///   R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);\n  /// }\n  /// ```\n  ///\n  /// Then use it like this:\n  ///\n  /// ```dart\n  /// var state = context.state;\n  /// ```\n  ///\n  /// See also:\n  ///\n  /// - [getRead] if you don't want the widget to rebuild automatically when\n  ///   the state changes (use it with `context.read()`). This is useful when\n  ///   you want to read the state once, for example inside an event handler,\n  ///   or in your `initState` method.\n  ///\n  /// - [getSelect] to select a specific part of the state and only rebuild\n  ///   when that part changes (use it with `context.select()`).\n  ///\n  St getState<St>() => _isMock //\n      ? (_store.state as St) //\n      : StoreProvider.state<St>(this);\n\n  /// Provides easy access to the AsyncRedux store state from a BuildContext.\n  ///\n  /// This is useful when you want to read the state once, for example\n  /// inside an event handler, or in your `initState` method.\n  /// Widgets using this will NOT rebuild automatically when the state changes.\n  ///\n  /// For convenience, it's recommended that you define this extension in your\n  /// own code:\n  ///\n  /// ```dart\n  /// extension BuildContextExtension on BuildContext {\n  ///   AppState get state => getState<AppState>();\n  ///   AppState read() => getRead<AppState>();\n  ///   R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n  ///   R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);\n  /// }\n  /// ```\n  ///\n  /// Then use it like this:\n  ///\n  /// ```dart\n  /// var state = context.read();\n  /// ```\n  ///\n  /// See also:\n  ///\n  /// - [getState] if you want the widget to rebuild automatically on any state\n  ///   change (use it with `context.state`).\n  ///\n  /// - [getSelect] to select a specific part of the state and only rebuild\n  ///   when that part changes (use it with `context.select()`).\n  ///\n  St getRead<St>() => _isMock\n      ? (_store.state as St)\n      : StoreProvider.state<St>(this, notify: false);\n\n  /// Consume an event from the state, and rebuild the widget when the event is\n  /// dispatched.\n  ///\n  /// Events are one-time notifications that can be used to trigger side effects\n  /// in widgets, such as showing a dialog, clearing a text field, or navigating\n  /// to a new screen. Unlike regular state values, events are automatically\n  /// \"consumed\" (marked as spent) after being read, ensuring they only trigger\n  /// once.\n  ///\n  /// This method selects an event from the state using the provided [selector]\n  /// function, consumes it, and returns its value. The widget will rebuild\n  /// whenever a new (unspent) event is dispatched to the store.\n  ///\n  /// **Return value:**\n  /// - For events with no generic type (`Evt`): Returns `true` if the event\n  ///   was dispatched, or `false` if it was already spent.\n  /// - For events with a value type (`Evt<R>`): Returns the event's value if\n  ///   it was dispatched, or `null` if it was already spent.\n  ///\n  /// For convenience, it's recommended that you define this extension in your\n  /// own code:\n  ///\n  /// ```dart\n  /// extension BuildContextExtension on BuildContext {\n  ///   AppState get state => getState<AppState>();\n  ///   AppState read() => getRead<AppState>();\n  ///   R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n  ///   R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);\n  /// }\n  /// ```\n  ///\n  /// **Example with a boolean (value-less) event (clear text field):**\n  ///\n  /// In your state:\n  /// ```dart\n  /// class AppState {\n  ///   final Event clearTextEvt;\n  ///   AppState({required this.clearTextEvt});\n  /// }\n  /// ```\n  ///\n  /// In your action:\n  /// ```dart\n  /// class ClearTextAction extends ReduxAction<AppState> {\n  ///   AppState reduce() => state.copy(clearTextEvt: Event());\n  /// }\n  /// ```\n  ///\n  /// In your widget:\n  /// ```dart\n  /// Widget build(BuildContext context) {\n  ///   var clearText = context.event((state) => state.clearTextEvt);\n  ///   if (clearText) controller.clear();\n  ///   ...\n  /// }\n  /// ```\n  ///\n  /// **Example with a typed event (display text in text field):**\n  ///\n  /// In your state:\n  /// ```dart\n  /// class AppState {\n  ///   final Event<String> changeTextEvt;\n  ///   AppState({required this.changeTextEvt});\n  /// }\n  /// ```\n  ///\n  /// In your action:\n  /// ```dart\n  /// class ChangeTextAction extends ReduxAction<AppState> {\n  ///   Future<AppState> reduce() async {\n  ///     String newText = await fetchTextFromApi();\n  ///     return state.copy(changeTextEvt: Event<String>(newText));\n  ///   }\n  /// }\n  /// ```\n  ///\n  /// In your widget:\n  /// ```dart\n  /// Widget build(BuildContext context) {\n  ///   var newText = context.event((state) => state.changeTextEvt);\n  ///   if (newText != null) controller.text = newText;\n  ///   ...\n  /// }\n  /// ```\n  ///\n  /// **Important notes:**\n  /// - Events are consumed only once. After consumption, they are marked as\n  ///   \"spent\" and won't trigger again until a new event is dispatched.\n  /// - Each event can be consumed by **only one widget**. If you need multiple\n  ///   widgets to react to the same trigger, use separate events in the state\n  ///   or consider using [EvtState] instead (which is not consumed).\n  /// - Initialize events in the state as spent: `Event.spent()` or\n  ///   `Event<T>.spent()`.\n  /// - The widget will rebuild when a new event is dispatched, even if it has\n  ///   the same internal value as a previous event, because each event instance\n  ///   is unique.\n  /// - The [selector] function must be pure and not cause side effects.\n  ///\n  /// See also:\n  ///\n  /// - [getState] to access the state and rebuild on any state change.\n  /// - [getRead] to read the state without triggering rebuilds.\n  /// - [getSelect] to select specific parts of the state and rebuild only when those parts change.\n  /// - [Event] class documentation for more details on event behavior and lifecycle.\n  ///\n  R? getEvent<St, R>(Evt<R> Function(St state) selector, {bool debug = true}) {\n    _assertEvent(debug);\n\n    var evt = getSelect<St, Evt<R>>(selector, debug: debug);\n    return evt.consume();\n  }\n\n  /// Select a specific part of the state and only rebuild when that part changes.\n  ///\n  /// This method allows fine-grained subscriptions to the state, rebuilding the widget\n  /// only when the selected value actually changes, not on every state update.\n  ///\n  /// For convenience, it's recommended that you define this extension in your\n  /// own code:\n  ///\n  /// ```dart\n  /// extension BuildContextExtension on BuildContext {\n  ///   AppState get state => getState<AppState>();\n  ///   AppState read() => getRead<AppState>();\n  ///   R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n  ///   R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);\n  /// }\n  /// ```\n  ///\n  /// Then use it like this:\n  /// ```dart\n  /// final userName = context.select((state) => state.user.name);\n  /// ```\n  ///\n  /// The widget will only rebuild when `state.user.name` changes, not when other\n  /// parts of the state change.\n  ///\n  /// The comparison uses deep equality checking, so it works correctly with:\n  /// - Primitive values (int, String, bool, etc.)\n  /// - Lists (element-by-element comparison)\n  /// - Maps (key-value pair comparison)\n  /// - Sets (membership comparison)\n  /// - Custom classes with proper `==` operator\n  /// - IList, ISet, IMap from fast_immutable_collections\n  ///\n  /// IMPORTANT: The selector function must be pure and not cause side effects.\n  /// Do not call other provider methods or dispatch actions inside the selector.\n  ///\n  /// See also:\n  ///\n  /// - [getState] if you want the widget to rebuild automatically on any state\n  ///   change (use it with `context.state`).\n  ///\n  /// - [getRead] if you don't want the widget to rebuild automatically when\n  ///   the state changes (use it with `context.read()`).\n  ///\n  /// The [debug] parameter, when true (the default), will throw an error if you\n  /// try to use `context.select` outside the widget's `build` method. Set it to\n  /// false to also allow usage in `didChangeDependencies`. Use this with care:\n  /// once the debug check is off, invalid usage in methods like `initState` will\n  /// no longer be detected.\n  ///\n  R getSelect<St, R>(R Function(St state) selector, {bool debug = true}) {\n    if (_isMock) return selector(_store.state as St);\n    _assertSelect(debug);\n\n    // Get the InheritedElement WITHOUT creating a dependency yet.\n    final inheritedElement =\n        getElementForInheritedWidgetOfExactType<_InheritedUntypedRebuilds>();\n\n    if (inheritedElement == null) {\n      throw _exceptionForWrongStoreType(_typeOf<_InheritedUntypedRebuilds>());\n    }\n\n    final provider = inheritedElement.widget as _InheritedUntypedRebuilds;\n\n    St state;\n    try {\n      state = provider._store.state as St;\n    } catch (error) {\n      throw _exceptionForWrongStateType(provider._store.state, St);\n    }\n\n    // We only turn on rebuilds when select is used for the first time\n    // (similar to how state() method works).\n    _InheritedUntypedRebuilds._isOn = true;\n\n    // Execute selector with debug tracking\n    assert(() {\n      _debugIsSelecting = true;\n      return true;\n    }());\n\n    final selected = selector(state);\n\n    assert(() {\n      _debugIsSelecting = false;\n      return true;\n    }());\n\n    if (_debugSelectLogging) {\n      print('[SELECT] ${widget.runtimeType} selected value: $selected');\n    }\n\n    // Register the dependency with an aspect function.\n    dependOnInheritedElement(\n      inheritedElement as _InheritedUntypedRebuildsElement,\n      aspect: (dynamic newValue) {\n        if (newValue == null) {\n          return false;\n        }\n\n        // Re-run selector with new value and compare.\n        assert(() {\n          _debugIsSelecting = true;\n          return true;\n        }());\n\n        St newState;\n        try {\n          newState = newValue as St;\n        } catch (_) {\n          return false;\n        }\n\n        final newSelected = selector(newState);\n\n        assert(() {\n          _debugIsSelecting = false;\n          return true;\n        }());\n\n        // Use deep equality to compare selected values.\n        return !const DeepCollectionEquality().equals(newSelected, selected);\n      },\n    );\n\n    return selected;\n  }\n\n  void _assertSelect(bool debug) {\n    assert(() {\n      final widget = this.widget;\n\n      // Check for unsupported contexts.\n      if (widget is SliverWithKeepAliveWidget ||\n          widget is AutomaticKeepAliveClientMixin) {\n        throw FlutterError(\n            'Tried to use `context.select` (or `context.getSelect`) '\n            'inside a SliverList/SliderGridView.'\n            '\\n\\n'\n            'This is likely a mistake, as instead of rebuilding only the item that cares '\n            'about the selected value, this would rebuild the entire list/grid.'\n            '\\n\\n'\n            'To fix, add a `Builder` or extract the content of `itemBuilder` in a separate widget:'\n            '\\n\\n'\n            'ListView.builder(\\n'\n            '  itemBuilder: (context, index) {\\n'\n            '    return Builder(builder: (context) {\\n'\n            '      final todo = context.select((st) => st.list[index]);\\n'\n            '      return Text(todo.name);\\n'\n            '    });\\n'\n            '  },\\n'\n            ');\\n');\n      }\n\n      // Check we're in a build method.\n      if (debug &&\n          !debugDoingBuild &&\n          widget is! LayoutBuilder &&\n          widget is! SliverLayoutBuilder) {\n        throw FlutterError(\n            'Tried to use `context.select` (or `context.getSelect`) '\n            'outside the widget `build` method.'\n            '\\n\\n'\n            'See also: `context.read()` which you can use in `initState` and events handlers, '\n            'because it will not rebuild widgets automatically when the state changes.\\n');\n      }\n\n      // Check for nested select calls.\n      if (_debugIsSelecting) {\n        throw FlutterError(\n            'Cannot call `context.select` inside the selector of another `context.select`.'\n            '\\n\\n'\n            'The selector function must return a value immediately, without calling other selectors.\\n');\n      }\n\n      return true;\n    }());\n  }\n\n  void _assertEvent(bool debug) {\n    assert(() {\n      final widget = this.widget;\n\n      // Check for unsupported contexts.\n      if (widget is SliverWithKeepAliveWidget ||\n          widget is AutomaticKeepAliveClientMixin) {\n        throw FlutterError(\n            'Tried to use `context.event` (or `context.getEvent`) '\n            'inside a SliverList/SliderGridView.'\n            '\\n\\n'\n            'This is likely a mistake, as instead of rebuilding only the item that cares '\n            'about the selected value, this would rebuild the entire list/grid.'\n            '\\n\\n'\n            'To fix, add a `Builder` or extract the content of `itemBuilder` in a separate widget:'\n            '\\n\\n'\n            'ListView.builder(\\n'\n            '  itemBuilder: (context, index) {\\n'\n            '    return Builder(builder: (context) {\\n'\n            '      var clearText = context.event((state) => state.clearTextEvt);\\n'\n            '      if (clearText) controller.clear();\\n'\n            '      return TextField(controller: controller);\\n'\n            '    });\\n'\n            '  },\\n'\n            ');\\n');\n      }\n\n      // Check we're in a build method.\n      if (debug &&\n          !debugDoingBuild &&\n          widget is! LayoutBuilder &&\n          widget is! SliverLayoutBuilder) {\n        throw FlutterError(\n            'Tried to use `context.event` (or `context.getEvent`) '\n            'outside the widget `build` method.'\n            '\\n\\n'\n            'Note: If you also want to allow the usage in '\n            '`didChangeDependencies`, set `debug` to false in `context.getEvent`. '\n            'Use with care, as invalid usage in methods like `initState` will '\n            'no longer be detected once the debug check is off.\\n');\n      }\n\n      // Check for nested select calls.\n      if (_debugIsSelecting) {\n        throw FlutterError(\n            'Cannot call `context.event` inside the selector of another `context.event`.'\n            '\\n\\n'\n            'The selector function must return a value immediately, without calling other selectors.\\n');\n      }\n\n      return true;\n    }());\n  }\n\n  /// Workaround to capture generics (used internally).\n  static Type _typeOf<T>() => T;\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// The action may be sync or async.\n  ///\n  /// ```dart\n  /// context.dispatch(MyAction());\n  /// ```\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of this action, even if it changes the state.\n  ///\n  /// Method [dispatch] is of type [Dispatch].\n  ///\n  /// See also:\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  ///\n  FutureOr<ActionStatus> dispatch(ReduxAction<St> action,\n          {bool notify = true}) =>\n      _isMock\n          ? _store.dispatch(action, notify: notify)\n          : StoreProvider.dispatch(this, action, notify: notify);\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// The action may be sync or async. In both cases, it returns a [Future] that resolves when\n  /// the action finishes.\n  ///\n  /// ```dart\n  /// await context.dispatchAndWait(DoThisFirstAction());\n  /// context.dispatch(DoThisSecondAction());\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of this action, even if it changes the state.\n  ///\n  /// Note: While the state change from the action's reducer will have been applied when the\n  /// Future resolves, other independent processes that the action may have started may still\n  /// be in progress.\n  ///\n  /// Method [dispatchAndWait] is of type [DispatchAndWait]. It returns `Future<ActionStatus>`,\n  /// which means you can also get the final status of the action after you `await` it:\n  ///\n  /// ```dart\n  /// var status = await context.dispatchAndWait(MyAction());\n  /// ```\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  ///\n  Future<ActionStatus> dispatchAndWait(ReduxAction<St> action,\n          {bool notify = true}) =>\n      _isMock\n          ? _store.dispatchAndWait(action, notify: notify)\n          : StoreProvider.dispatchAndWait(this, action, notify: notify);\n\n  /// Dispatches all given [actions] in parallel, applying their reducer, and\n  /// possibly changing the store state.\n  ///\n  /// ```dart\n  /// dispatchAll([BuyAction('IBM'), SellAction('TSLA')]);\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of these actions, even if it changes the state.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  ///\n  List<ReduxAction<St>> dispatchAll<St>(List<ReduxAction<St>> actions,\n      {bool notify = true}) {\n    return _isMock\n        ? _store.dispatchAll(actions, notify: notify) as List<ReduxAction<St>>\n        : StoreProvider.dispatchAll<St>(this, actions, notify: notify);\n  }\n\n  /// Dispatches all given [actions] in parallel, applying their reducers, and\n  /// possibly changing the store state. The actions may be sync or async.\n  /// It returns a [Future] that resolves when ALL actions finish.\n  ///\n  /// ```dart\n  /// await context.dispatchAndWaitAll([BuyAction('IBM'), SellAction('TSLA')]);\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily\n  /// rebuild because of these actions, even if they change the state.\n  ///\n  /// Note: While the state change from the action's reducers will have been\n  /// applied when the Future resolves, other independent processes that the\n  /// action may have started may still be in progress.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  ///\n  Future<List<ReduxAction<St>>> dispatchAndWaitAll<St>(\n    List<ReduxAction<St>> actions, {\n    bool notify = true,\n  }) =>\n      _isMock\n          ? _store.dispatchAndWaitAll(actions, notify: notify)\n              as Future<List<ReduxAction<St>>>\n          : StoreProvider.dispatchAndWaitAll(this, actions, notify: notify);\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// However, if the action is ASYNC, it will throw a [StoreException].\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily\n  /// rebuild because of this action, even if it changes the state.\n  ///\n  /// Method [dispatchSync] is of type [DispatchSync]. It returns `ActionStatus`,\n  /// which means you can also get the final status of the action:\n  ///\n  /// ```dart\n  /// var status = context.dispatchSync(MyAction());\n  /// ```\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  ///\n  ActionStatus dispatchSync(ReduxAction<St> action, {bool notify = true}) =>\n      _isMock\n          ? _store.dispatchSync(action, notify: notify)\n          : StoreProvider.dispatchSync(this, action, notify: notify);\n\n  /// You can use [isWaiting] and pass it [actionOrTypeOrList] to check if:\n  /// * A specific async ACTION is currently being processed.\n  /// * An async action of a specific TYPE is currently being processed.\n  /// * If any of a few given async actions or action types is currently being\n  ///   processed.\n  ///\n  /// If you wait for an action TYPE, then it returns false when:\n  /// - The ASYNC action of the type is NOT currently being processed.\n  /// - If the type is not really a type that extends [ReduxAction].\n  /// - The action of the type is a SYNC action (since those finish immediately).\n  ///\n  /// If you wait for an ACTION, then it returns false when:\n  /// - The ASYNC action is NOT currently being processed.\n  /// - If the action is a SYNC action (since those finish immediately).\n  ///\n  /// Trying to wait for any other type of object will return null and throw\n  /// a [StoreException] after the async gap.\n  ///\n  /// Examples:\n  ///\n  /// ```dart\n  /// // Waiting for an action TYPE:\n  /// dispatch(MyAction());\n  /// if (context.isWaiting(MyAction)) { // Show a spinner }\n  ///\n  /// // Waiting for an ACTION:\n  /// var action = MyAction();\n  /// dispatch(action);\n  /// if (context.isWaiting(action)) { // Show a spinner }\n  ///\n  /// // Waiting for any of the given action TYPES:\n  /// dispatch(BuyAction());\n  /// if (context.isWaiting([BuyAction, SellAction])) { // Show a spinner }\n  /// ```\n  bool isWaiting(Object actionOrTypeOrList) => _isMock\n      ? _store.isWaiting(actionOrTypeOrList)\n      : StoreProvider.isWaiting(this, actionOrTypeOrList);\n\n  /// Returns true if an [actionOrTypeOrList] failed with an [UserException].\n  ///\n  /// Example:\n  ///\n  /// ```dart\n  /// if (context.isFailed(MyAction)) { // Show an error message. }\n  /// ```\n  bool isFailed(Object actionOrTypeOrList) => _isMock\n      ? _store.isFailed(actionOrTypeOrList)\n      : StoreProvider.isFailed(this, actionOrTypeOrList);\n\n  /// Returns the [UserException] of the [actionTypeOrList] that failed.\n  ///\n  /// The [actionTypeOrList] can be a [Type], or an Iterable of types. Any other type\n  /// of object will return null and throw a [StoreException] after the async gap.\n  ///\n  /// Example:\n  ///\n  /// ```dart\n  /// if (context.isFailed(SaveUserAction)) Text(context.exceptionFor(SaveUserAction)!.reason ?? '');\n  /// ```\n  UserException? exceptionFor(Object actionOrTypeOrList) => _isMock\n      ? _store.exceptionFor(actionOrTypeOrList)\n      : StoreProvider.exceptionFor(this, actionOrTypeOrList);\n\n  /// Removes the given [actionTypeOrList] from the list of action types that failed.\n  ///\n  /// Note that dispatching an action already removes that action type from the\n  /// exceptions list. This removal happens as soon as the action is dispatched,\n  /// not when it finishes.\n  ///\n  /// [actionTypeOrList] can be a [Type], or an Iterable of types. Any other type\n  /// of object will return null and throw a [StoreException] after the async gap.\n  ///\n  void clearExceptionFor(Object actionOrTypeOrList) => _isMock\n      ? _store.clearExceptionFor(actionOrTypeOrList)\n      : StoreProvider.clearExceptionFor(this, actionOrTypeOrList);\n\n  /// Given the BuildContext, provides easy access to the optional AsyncRedux\n  /// store \"environment\" that you may have defined. The environment is considered\n  /// immutable and should not change during the app's lifecycle.\n  ///\n  /// This allows you to show different UI based on the environment (if the app is running\n  /// in production, staging, development, testing), for example showing a debug banner\n  /// in development environment but not in production.\n  ///\n  /// Note that accessing the environment does not trigger any widget rebuilds.\n  ///\n  /// For convenience, given that you will have your own `Environment` class,\n  /// it's recommended that you define this extension in your own code:\n  ///\n  /// ```dart\n  /// extension BuildContextExtension on BuildContext {\n  ///   Environment get env => getEnvironment<AppState>() as Environment;\n  /// }\n  /// ```\n  ///\n  /// Then use it like this:\n  ///\n  /// ```dart\n  /// var environment = context.env;\n  /// ```\n  ///\n  /// Or else you can directly create a boolean getter:\n  ///\n  /// ```dart\n  /// extension BuildContextExtension on BuildContext {\n  ///   Environment get _env => getEnvironment<AppState>() as Environment;\n  ///   bool get isProduction => _env.isProduction;\n  ///   bool get isStaging => _env.isStaging;\n  ///   bool get isDevelopment => _env.isDevelopment\n  ///   bool get isTesting => _env.isTesting;\n  /// }\n  /// ```\n  ///\n  /// Then use it like this:\n  ///\n  /// ```dart\n  /// if (context.isProduction) return Text('Welcome to the app!');\n  /// else return Text('Welcome to the development version of the app!');\n  /// ```\n  Object? getEnvironment<St>() {\n    if (_isMock) return _store.environment;\n\n    Store<St> store = StoreProvider.backdoorInheritedWidget<St>(this);\n    return store.environment;\n  }\n\n  /// Given the BuildContext, provides easy access to the optional AsyncRedux\n  /// store \"configuration\" that you may have defined.\n  ///\n  /// This allows you to show different UI based on the configuration (if the app should\n  /// show certain features, or use certain API endpoints, etc.). The configuration is\n  /// considered immutable and should not change during the app's lifecycle.\n  ///\n  /// Note that accessing the configuration does not trigger any widget rebuilds.\n  ///\n  /// For convenience, given that you will have your own `Configuration` class,\n  /// it's recommended that you define this extension in your own code:\n  ///\n  /// ```dart\n  /// extension BuildContextExtension on BuildContext {\n  ///   Configuration get config => getConfiguration<AppState>() as Configuration;\n  /// }\n  /// ```\n  ///\n  /// Then use it like this:\n  ///\n  /// ```dart\n  /// var configuration = context.config;\n  /// ```\n  ///\n  /// Or else you can directly create a boolean getter:\n  ///\n  /// ```dart\n  /// extension BuildContextExtension on BuildContext {\n  ///   Configuration get _config => getConfiguration<AppState>() as Configuration;\n  ///   bool get isA => _config.abTesting.a;\n  ///   bool get isB => _config.abTesting.b;\n  ///   bool get showAdminFeatures => _config.showAdminFeatures;\n  /// }\n  /// ```\n  ///\n  /// Then use it like this:\n  ///\n  /// ```dart\n  /// if (context.isA) return Text('Welcome', style: TextStyle(color: Colors.blue));\n  /// else return Text('Welcome', style: TextStyle(color: Colors.red));\n  /// ```\n  Object? getConfiguration<St>() {\n    if (_isMock) return _store.configuration;\n\n    Store<St> store = StoreProvider.backdoorInheritedWidget<St>(this);\n    return store.configuration;\n  }\n\n  /// Allows [MockBuildContext] to be used for testing.\n  bool get _isMock => this is MockBuildContext;\n\n  /// Only use this after checking [_isMock].\n  Store get _store => (this as MockBuildContext).store;\n}\n\n/// This extension allows you to write `dispatch()` instead of\n/// `context.dispatch()` inside the [State] of a [StatefulWidget]. It\n/// also works for `dispatchAndWait()`, `dispatchAll()`, `dispatchAndWaitAll()`,\n/// `dispatchSync()`, `isWaiting()`, `isFailed()`, `exceptionFor()`, and\n/// `clearExceptionFor()`.\n///\n/// - It is compatible with testing with [MockBuildContext].\n///\n/// - If your app has multiple [StoreProvider] widgets, you should continue\n/// using `context.dispatch()`.\n///\nextension StatefulWidgetExtensionForProviderAndConnector<St> on State {\n  //\n  /// Dispatches the action, applying its reducer, and possibly changing the\n  /// store state. The action may be sync or async.\n  ///\n  /// ```dart\n  /// dispatch(MyAction());\n  /// ```\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of this action, even if it changes the state.\n  ///\n  /// Method [dispatch] is of type [Dispatch].\n  ///\n  /// IMPORTANT: You can use `dispatch()` only when your app has a single\n  /// [StoreProvider], which is almost always true. Otherwise, you need to use\n  /// `context.dispatch()` instead.\n  ///\n  /// See also:\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  ///\n  FutureOr<ActionStatus> dispatch(ReduxAction<St> action,\n          {bool notify = true}) =>\n      StoreProvider.backdoorStaticGlobal().dispatch(action, notify: notify);\n\n  /// Dispatches the action, applying its reducer, and possibly changing the\n  /// store state. The action may be sync or async. In both cases, it returns a\n  /// [Future] that resolves when the action finishes.\n  ///\n  /// ```dart\n  /// await dispatchAndWait(DoThisFirstAction());\n  /// dispatch(DoThisSecondAction());\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of this action, even if it changes the state.\n  ///\n  /// Note: While the state change from the action's reducer will have been\n  /// applied when the Future resolves, other independent processes that the\n  /// action may have started may still be in progress.\n  ///\n  /// Method [dispatchAndWait] is of type [DispatchAndWait]. It returns\n  /// `Future<ActionStatus>`, which means you can also get the final status of\n  /// the action after you `await` it:\n  ///\n  /// ```dart\n  /// var status = await dispatchAndWait(MyAction());\n  /// ```\n  ///\n  /// IMPORTANT: You can use `dispatchAndWait()` only when your app has a single\n  /// [StoreProvider], which is almost always true. Otherwise, you need to use\n  /// `context.dispatchAndWait()` instead.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  ///\n  Future<ActionStatus> dispatchAndWait(ReduxAction<St> action,\n          {bool notify = true}) =>\n      StoreProvider.backdoorStaticGlobal()\n          .dispatchAndWait(action, notify: notify);\n\n  /// Dispatches all given [actions] in parallel, applying their reducer, and\n  /// possibly changing the store state.\n  ///\n  /// ```dart\n  /// dispatchAll([BuyAction('IBM'), SellAction('TSLA')]);\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of these actions, even if it changes the state.\n  ///\n  /// IMPORTANT: You can use `dispatchAll()` only when your app has a single\n  /// [StoreProvider], which is almost always true. Otherwise, you need to use\n  /// `context.dispatchAll()` instead.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  ///\n  List<ReduxAction<St>> dispatchAll<St>(List<ReduxAction<St>> actions,\n          {bool notify = true}) =>\n      StoreProvider.backdoorStaticGlobal().dispatchAll(actions, notify: notify)\n          as List<ReduxAction<St>>;\n\n  /// Dispatches all given [actions] in parallel, applying their reducers, and\n  /// possibly changing the store state. The actions may be sync or async.\n  /// It returns a [Future] that resolves when ALL actions finish.\n  ///\n  /// ```dart\n  /// await dispatchAndWaitAll([BuyAction('IBM'), SellAction('TSLA')]);\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of these actions, even if they change the state.\n  ///\n  /// Note: While the state change from the action's reducers will have been\n  /// applied when the Future resolves, other independent processes that the\n  /// action may have started may still be in progress.\n  ///\n  /// IMPORTANT: You can use `dispatchAndWaitAll()` only when your app has a single\n  /// [StoreProvider], which is almost always true. Otherwise, you need to use\n  /// `context.dispatchAndWaitAll()` instead.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  ///\n  Future<List<ReduxAction<St>>> dispatchAndWaitAll<St>(\n    List<ReduxAction<St>> actions, {\n    bool notify = true,\n  }) =>\n      StoreProvider.backdoorStaticGlobal().dispatchAndWaitAll(actions,\n          notify: notify) as Future<List<ReduxAction<St>>>;\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store\n  /// state. However, if the action is ASYNC, it will throw a [StoreException].\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of this action, even if it changes the state.\n  ///\n  /// Method [dispatchSync] is of type [DispatchSync]. It returns `ActionStatus`,\n  /// which means you can also get the final status of the action:\n  ///\n  /// ```dart\n  /// var status = dispatchSync(MyAction());\n  /// ```\n  ///\n  /// IMPORTANT: You can use `dispatchSync()` only when your app has a single\n  /// [StoreProvider], which is almost always true. Otherwise, you need to use\n  /// `context.dispatchSync()` instead.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  ///\n  ActionStatus dispatchSync(ReduxAction<St> action, {bool notify = true}) =>\n      StoreProvider.backdoorStaticGlobal().dispatchSync(action, notify: notify);\n}\n\n/// This extension allows you to write `dispatch()` instead of\n/// `context.dispatch()` inside a [StatelessWidget]. It also works for\n/// `dispatchAndWait()`, `dispatchAll()`, `dispatchAndWaitAll()`,\n/// `dispatchSync()`, `isWaiting()`, `isFailed()`, `exceptionFor()`, and\n/// `clearExceptionFor()`.\n///\n/// - It is compatible with testing with [MockBuildContext].\n///\n/// - If your app has multiple [StoreProvider] widgets, you should continue\n/// using `context.dispatch()`.\n///\nextension StatelessWidgetExtensionForProviderAndConnector<St>\n    on StatelessWidget {\n  //\n  /// Dispatches the action, applying its reducer, and possibly changing the\n  /// store state. The action may be sync or async.\n  ///\n  /// ```dart\n  /// dispatch(MyAction());\n  /// ```\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of this action, even if it changes the state.\n  ///\n  /// Method [dispatch] is of type [Dispatch].\n  ///\n  /// IMPORTANT: You can use `dispatch()` only when your app has a single\n  /// [StoreProvider], which is almost always true. Otherwise, you need to use\n  /// `context.dispatch()` instead.\n  ///\n  /// See also:\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  ///\n  FutureOr<ActionStatus> dispatch(ReduxAction<St> action,\n          {bool notify = true}) =>\n      StoreProvider.backdoorStaticGlobal().dispatch(action, notify: notify);\n\n  /// Dispatches the action, applying its reducer, and possibly changing the\n  /// store state. The action may be sync or async. In both cases, it returns a\n  /// [Future] that resolves when the action finishes.\n  ///\n  /// ```dart\n  /// await dispatchAndWait(DoThisFirstAction());\n  /// dispatch(DoThisSecondAction());\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of this action, even if it changes the state.\n  ///\n  /// Note: While the state change from the action's reducer will have been\n  /// applied when the Future resolves, other independent processes that the\n  /// action may have started may still be in progress.\n  ///\n  /// Method [dispatchAndWait] is of type [DispatchAndWait]. It returns\n  /// `Future<ActionStatus>`, which means you can also get the final status of\n  /// the action after you `await` it:\n  ///\n  /// ```dart\n  /// var status = await dispatchAndWait(MyAction());\n  /// ```\n  ///\n  /// IMPORTANT: You can use `dispatchAndWait()` only when your app has a single\n  /// [StoreProvider], which is almost always true. Otherwise, you need to use\n  /// `context.dispatchAndWait()` instead.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  ///\n  Future<ActionStatus> dispatchAndWait(ReduxAction<St> action,\n          {bool notify = true}) =>\n      StoreProvider.backdoorStaticGlobal()\n          .dispatchAndWait(action, notify: notify);\n\n  /// Dispatches all given [actions] in parallel, applying their reducer, and\n  /// possibly changing the store state.\n  ///\n  /// ```dart\n  /// dispatchAll([BuyAction('IBM'), SellAction('TSLA')]);\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of these actions, even if it changes the state.\n  ///\n  /// IMPORTANT: You can use `dispatchAll()` only when your app has a single\n  /// [StoreProvider], which is almost always true. Otherwise, you need to use\n  /// `context.dispatchAll()` instead.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchAndWaitAll] which dispatches all given actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  ///\n  List<ReduxAction<St>> dispatchAll<St>(List<ReduxAction<St>> actions,\n          {bool notify = true}) =>\n      StoreProvider.backdoorStaticGlobal().dispatchAll(actions, notify: notify)\n          as List<ReduxAction<St>>;\n\n  /// Dispatches all given [actions] in parallel, applying their reducers, and\n  /// possibly changing the store state. The actions may be sync or async.\n  /// It returns a [Future] that resolves when ALL actions finish.\n  ///\n  /// ```dart\n  /// await dispatchAndWaitAll([BuyAction('IBM'), SellAction('TSLA')]);\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of these actions, even if they change the state.\n  ///\n  /// Note: While the state change from the action's reducers will have been\n  /// applied when the Future resolves, other independent processes that the\n  /// action may have started may still be in progress.\n  ///\n  /// IMPORTANT: You can use `dispatchAndWaitAll()` only when your app has a single\n  /// [StoreProvider], which is almost always true. Otherwise, you need to use\n  /// `context.dispatchAndWaitAll()` instead.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAll] which dispatches all given actions in parallel.\n  ///\n  Future<List<ReduxAction<St>>> dispatchAndWaitAll<St>(\n    List<ReduxAction<St>> actions, {\n    bool notify = true,\n  }) =>\n      StoreProvider.backdoorStaticGlobal().dispatchAndWaitAll(actions,\n          notify: notify) as Future<List<ReduxAction<St>>>;\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store\n  /// state. However, if the action is ASYNC, it will throw a [StoreException].\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not\n  /// necessarily rebuild because of this action, even if it changes the state.\n  ///\n  /// Method [dispatchSync] is of type [DispatchSync]. It returns `ActionStatus`,\n  /// which means you can also get the final status of the action:\n  ///\n  /// ```dart\n  /// var status = dispatchSync(MyAction());\n  /// ```\n  ///\n  /// IMPORTANT: You can use `dispatchSync()` only when your app has a single\n  /// [StoreProvider], which is almost always true. Otherwise, you need to use\n  /// `context.dispatchSync()` instead.\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  ///\n  ActionStatus dispatchSync(ReduxAction<St> action, {bool notify = true}) =>\n      StoreProvider.backdoorStaticGlobal().dispatchSync(action, notify: notify);\n}\n"
  },
  {
    "path": "lib/src/store_tester.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'dart:async';\nimport 'dart:collection';\n\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart' hide Action;\n\nimport '../async_redux.dart';\nimport 'connector_tester.dart';\n\n/// Predicate used in [StoreTester.waitCondition].\n/// Return true to stop waiting, and get the last state.\ntypedef StateCondition<St> = bool Function(TestInfo<St> info);\n\n/// Helps testing the store, actions, and sync/async reducers.\n///\n/// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n///\nclass StoreTester<St> {\n  //\n  /// The default timeout in seconds is 10 minutes.\n  /// This value is not final and can be modified.\n  static int defaultTimeout = 60 * 10;\n\n  /// If the default debug info should be printed to the console or not.\n  static bool printDefaultDebugInfo = true;\n\n  static TestInfoPrinter defaultTestInfoPrinter = (TestInfo info) {\n    if (printDefaultDebugInfo) print(info);\n  };\n\n  static VoidCallback defaultNewStorePrinter = () {\n    if (printDefaultDebugInfo) print(\"New StoreTester.\");\n  };\n\n  final Store<St> _store;\n  final List<Type> _ignore;\n  late StreamSubscription _subscription;\n  late Completer<TestInfo<St>> _completer;\n  late Queue<Future<TestInfo<St>>> _futures;\n\n  Store<St> get store => _store;\n\n  St get state => _store.state;\n\n  /// The last TestInfo read after some wait method.\n  late TestInfo<St> lastInfo;\n\n  /// The current TestInfo.\n  TestInfo<St> get currentTestInfo => _currentTestInfo;\n  late TestInfo<St> _currentTestInfo;\n\n  /// The [StoreTester] makes it easy to test both sync and async reducers.\n  /// You may dispatch some action, wait for it to finish or wait until some\n  /// arbitrary condition is met, and then check the resulting state.\n  ///\n  /// The [StoreTester] will, by default, print some default debug\n  /// information to the console. You can disable these prints globally\n  /// by making `StoreTester.printDefaultDebugInfo = false`.\n  /// Note you can also provide your own custom [testInfoPrinter].\n  ///\n  /// If [shouldThrowUserExceptions] is true, all errors will be thrown,\n  /// and not swallowed, including UserExceptions. Use this in all tests\n  /// that should throw no errors. Pass [shouldThrowUserExceptions] as\n  /// false when you are testing code that should throw UserExceptions.\n  /// These exceptions will then silently go to the `errors` queue,\n  /// where you can assert they exist with the right error messages.\n  ///\n  StoreTester({\n    required St initialState,\n    TestInfoPrinter? testInfoPrinter,\n    List<Type>? ignore,\n    bool syncStream = false,\n    ErrorObserver<St>? errorObserver,\n    bool shouldThrowUserExceptions = false,\n    Map<Type, dynamic>? mocks,\n  }) : this.from(\n            MockStore(\n              initialState: initialState,\n              syncStream: syncStream,\n              errorObserver: errorObserver ?? //\n                  (shouldThrowUserExceptions ? TestErrorObserver() : null),\n              mocks: mocks,\n            ),\n            testInfoPrinter: testInfoPrinter,\n            ignore: ignore);\n\n  /// Create a StoreTester from a store that already exists.\n  StoreTester.from(\n    Store<St> store, {\n    TestInfoPrinter? testInfoPrinter,\n    List<Type>? ignore,\n  })  : _ignore = ignore ?? const [],\n        _store = store {\n    if (testInfoPrinter != null)\n      _store.initTestInfoPrinter(testInfoPrinter);\n    else if (_store.testInfoPrinter == null) //\n      _store.initTestInfoPrinter(defaultTestInfoPrinter);\n\n    _listen();\n    defaultNewStorePrinter();\n  }\n\n  /// Create a StoreTester from a store that already exists,\n  /// but don't print anything to the console.\n  StoreTester.simple(this._store) : _ignore = const [] {\n    _listen();\n  }\n\n  Map<Type, dynamic>? get mocks => (store as MockStore).mocks;\n\n  set mocks(Map<Type, dynamic>? _mocks) => (store as MockStore).mocks = _mocks;\n\n  MockStore<St> addMock(Type actionType, dynamic mock) {\n    (store as MockStore).addMock(actionType, mock);\n    return store as MockStore<St>;\n  }\n\n  MockStore<St> addMocks(Map<Type, dynamic> mocks) {\n    (store as MockStore).addMocks(mocks);\n    return store as MockStore<St>;\n  }\n\n  MockStore<St> clearMocks() {\n    (store as MockStore).clearMocks();\n    return store as MockStore<St>;\n  }\n\n  FutureOr<ActionStatus> dispatch(ReduxAction<St> action,\n          {bool notify = true}) =>\n      store.dispatch(action, notify: notify);\n\n  ActionStatus dispatchSync(ReduxAction<St> action, {bool notify = true}) =>\n      store.dispatchSync(action, notify: notify);\n\n  @Deprecated(\"Use `dispatchAndWait` instead. This will be removed.\")\n  Future<ActionStatus> dispatchAsync(ReduxAction<St> action,\n          {bool notify = true}) =>\n      store.dispatchAndWait(action, notify: notify);\n\n  Future<ActionStatus> dispatchAndWait(ReduxAction<St> action,\n          {bool notify = true}) =>\n      store.dispatchAndWait(action, notify: notify);\n\n  /// Dispatches [action], and then waits until it finishes.\n  /// Returns the info after the action finishes. **Ignores other** actions.\n  ///\n  /// Example use:\n  ///\n  ///   var action = MyAction();\n  ///   await storeTester.dispatchAndWait(action);\n  ///\n  /// Note, this is the same as doing:\n  ///\n  ///   var action = MyAction();\n  ///   storeTester.dispatch(action);\n  ///   await storeTester.wait(action);\n  ///\n  Future<TestInfo<St>> dispatchAndWaitGetInfo(ReduxAction<St> action) {\n    store.dispatch(action);\n    return waitUntilAction(action);\n  }\n\n  void defineState(St state) => _store.defineState(state);\n\n  /// Dispatches an action that changes the current state to the one provided by you.\n  /// Then, runs until that action is dispatched and finished (ignoring other actions).\n  /// Returns the info after the action finishes, containing the given state.\n  ///\n  /// Example use:\n  ///\n  ///   var info = await storeTester.dispatchState(MyState(123));\n  ///   expect(info.state, MyState(123));\n  ///\n  Future<TestInfo<St>> dispatchState(St state) async {\n    var action = _NewStateAction(state);\n    dispatch(action);\n\n    TestInfo<St>? testInfo;\n\n    while (testInfo == null ||\n        !identical(testInfo.action, action) ||\n        testInfo.isINI) {\n      testInfo = await _next();\n    }\n\n    lastInfo = testInfo;\n\n    return testInfo;\n  }\n\n  /// Returns a mutable copy of the global ignore list.\n  List<Type> get ignore => List.of(_ignore);\n\n  /// Runs until the predicate function [condition] returns true.\n  /// This function will receive each testInfo, from where it can\n  /// access the state, action, errors etc.\n  /// When [testImmediately] is true (the default), it will test the condition\n  /// immediately when the method is called. If the condition is true, the\n  /// method will return immediately, without waiting for any actions to be\n  /// dispatched.\n  /// When [testImmediately] is false, it will only test\n  /// the condition once an action is dispatched.\n  /// Only END states will be received, unless you pass [ignoreIni] as false.\n  /// Returns the info after the condition is met.\n  ///\n  Future<TestInfo<St>> waitConditionGetLast(\n    StateCondition<St> condition, {\n    bool testImmediately = true,\n    bool ignoreIni = true,\n    int? timeoutInSeconds,\n  }) async {\n    timeoutInSeconds ??= defaultTimeout;\n\n    var infoList = await waitCondition(\n      condition,\n      testImmediately: testImmediately,\n      ignoreIni: ignoreIni,\n      timeoutInSeconds: timeoutInSeconds,\n    );\n\n    return infoList.last;\n  }\n\n  /// Runs until the predicate function [condition] returns true.\n  /// This function will receive each testInfo, from where it can\n  /// access the state, action, errors etc.\n  /// When [testImmediately] is true (the default), it will test the condition\n  /// immediately when the method is called. If the condition is true, the\n  /// method will return immediately, without waiting for any actions to be\n  /// dispatched.\n  /// When [testImmediately] is false, it will only test\n  /// the condition once an action is dispatched.\n  /// Only END states will be received, unless you pass [ignoreIni] as false.\n  /// Returns a list with all info until the condition is met.\n  ///\n  Future<TestInfoList<St>> waitCondition(\n    StateCondition<St> condition, {\n    bool testImmediately = true,\n    bool ignoreIni = true,\n    int? timeoutInSeconds,\n  }) async {\n    timeoutInSeconds ??= defaultTimeout;\n\n    TestInfoList<St> infoList = TestInfoList<St>();\n\n    if (testImmediately) {\n      var currentTestInfoWithoutAction = TestInfo<St>(\n        _currentTestInfo.state,\n        false,\n        null,\n        null,\n        null,\n        _currentTestInfo.dispatchCount,\n        _currentTestInfo.reduceCount,\n        _currentTestInfo.errors,\n      );\n      if (condition(currentTestInfoWithoutAction)) {\n        infoList._add(currentTestInfoWithoutAction);\n        lastInfo = infoList.last;\n        return infoList;\n      }\n    }\n\n    TestInfo<St> testInfo = await _next(timeoutInSeconds: timeoutInSeconds);\n\n    while (true) {\n      if (ignoreIni)\n        while (testInfo.ini)\n          testInfo = await (_next(\n            timeoutInSeconds: timeoutInSeconds,\n          ));\n\n      infoList._add(testInfo);\n\n      if (condition(testInfo))\n        break;\n      else\n        testInfo = await _next(timeoutInSeconds: timeoutInSeconds);\n    }\n\n    lastInfo = infoList.last;\n    return infoList;\n  }\n\n  /// If [error] is a Type, runs until after an action throws an error of this exact type.\n  /// If [error] is NOT a Type, runs until after an action throws this [error] (using equals).\n  ///\n  /// You can also, instead, define [processedError], which is the error after wrapped by the\n  /// action's wrapError() method. Note, if you define both [error] and [processedError],\n  /// both need to match.\n  ///\n  /// Returns the info after the error condition is met.\n  ///\n  Future<TestInfo<St>> waitUntilErrorGetLast({\n    Object? error,\n    Object? processedError,\n    int? timeoutInSeconds,\n  }) async {\n    timeoutInSeconds ??= defaultTimeout;\n\n    var infoList = await waitUntilError(\n      error: error,\n      processedError: processedError,\n      timeoutInSeconds: timeoutInSeconds,\n    );\n\n    return infoList.last;\n  }\n\n  /// If [error] is a Type, runs until after an action throws an error of this exact type.\n  /// If [error] is NOT a Type, runs until after an action throws this [error] (using equals).\n  ///\n  /// You can also, instead, define [processedError], which is the error after wrapped by the\n  /// action's wrapError() method. Note, if you define both [error] and [processedError],\n  /// both need to match.\n  ///\n  /// Returns a list with all info until the error condition is met.\n  ///\n  Future<TestInfoList<St>> waitUntilError({\n    Object? error,\n    Object? processedError,\n    int? timeoutInSeconds,\n  }) async {\n    timeoutInSeconds ??= defaultTimeout;\n\n    assert(error != null || processedError != null);\n\n    var condition = (TestInfo<St> info) =>\n        (error == null ||\n            (error is Type && info.error.runtimeType == error) ||\n            (error is! Type && info.error == error)) &&\n        (processedError == null ||\n            (processedError is Type && //\n                info.processedError.runtimeType == processedError) ||\n            (processedError is! Type && //\n                info.processedError == processedError));\n\n    var infoList = await waitCondition(\n      condition,\n      ignoreIni: true,\n      timeoutInSeconds: timeoutInSeconds,\n    );\n\n    lastInfo = infoList.last;\n\n    return infoList;\n  }\n\n  /// Expects **one action** of the given type to be dispatched, and waits until it finishes.\n  /// Returns the info after the action finishes.\n  /// Will fail with an exception if an unexpected action is seen.\n  Future<TestInfo<St>> wait(Type actionType) async => //\n      waitAllGetLast([actionType]);\n\n  /// Runs until an action of the given type is dispatched, and then waits until it finishes.\n  /// Returns the info after the action finishes. **Ignores other** actions types.\n  ///\n  Future<TestInfo<St>> waitUntil(\n    Type actionType, {\n    int? timeoutInSeconds,\n  }) async {\n    timeoutInSeconds ??= defaultTimeout;\n\n    TestInfo<St>? testInfo;\n\n    while (\n        (testInfo == null) || (testInfo.type != actionType) || testInfo.isINI) {\n      testInfo = await _next(timeoutInSeconds: timeoutInSeconds);\n    }\n\n    lastInfo = testInfo;\n\n    return testInfo;\n  }\n\n  /// Runs until an action of the given types is dispatched, and then waits until it\n  /// finishes. Returns the info after the action finishes. **Ignores other** actions types.\n  ///\n  Future<TestInfo<St>> waitUntilAny(\n    List<Type> actionTypes, {\n    int? timeoutInSeconds,\n  }) async {\n    timeoutInSeconds ??= defaultTimeout;\n\n    TestInfo<St>? testInfo;\n\n    while ((testInfo == null) ||\n        (!actionTypes.contains(testInfo.type)) ||\n        testInfo.isINI) {\n      testInfo = await _next(timeoutInSeconds: timeoutInSeconds);\n    }\n\n    lastInfo = testInfo;\n\n    return testInfo;\n  }\n\n  /// Runs until all actions of the given types are dispatched and finish, in any order.\n  /// Returns a list with all info until the last action finishes. **Ignores other** actions types.\n  ///\n  Future<TestInfoList<St>> waitUntilAll(\n    List<Type> actionTypes, {\n    bool ignoreIni = true,\n    int? timeoutInSeconds,\n  }) async {\n    assert(actionTypes.isNotEmpty);\n\n    timeoutInSeconds ??= defaultTimeout;\n\n    TestInfoList<St> infoList = TestInfoList<St>();\n    Set<Type> actionsIni = Set.from(actionTypes);\n    Set<Type> actionsEnd = {};\n    TestInfo<St>? testInfo;\n\n    while (actionsIni.isNotEmpty || actionsEnd.isNotEmpty) {\n      testInfo = await _next(timeoutInSeconds: timeoutInSeconds);\n      if (!ignoreIni || testInfo.isEND) infoList._add(testInfo);\n      Type actionType = testInfo.action.runtimeType;\n      if (testInfo.isINI) {\n        if (actionsIni.remove(actionType)) {\n          actionsEnd.add(actionType);\n        }\n      } else\n        actionsEnd.remove(actionType);\n    }\n\n    lastInfo = infoList.last;\n    return infoList;\n  }\n\n  /// Runs until all actions of the given types are dispatched and finish, in any order.\n  /// Returns the info after they all finish. **Ignores other** actions types.\n  ///\n  Future<TestInfo<St>> waitUntilAllGetLast(\n    List<Type> actionTypes, {\n    bool ignoreIni = true,\n    int? timeoutInSeconds,\n  }) async {\n    timeoutInSeconds ??= defaultTimeout;\n\n    var infoList = await waitUntilAll(\n      actionTypes,\n      ignoreIni: ignoreIni,\n      timeoutInSeconds: timeoutInSeconds,\n    );\n\n    return infoList.last;\n  }\n\n  /// Runs until the exact given action is dispatched, and then waits until it finishes.\n  /// Returns the info after the action finishes. **Ignores other** actions.\n  ///\n  /// Example use:\n  ///\n  ///   var action = MyAction();\n  ///   storeTester.dispatch(action);\n  ///   await storeTester.waitUntilAction(action);\n  ///\n  Future<TestInfo<St>> waitUntilAction(\n    ReduxAction<St> action, {\n    int? timeoutInSeconds,\n  }) async {\n    timeoutInSeconds ??= defaultTimeout;\n\n    TestInfo<St>? testInfo;\n\n    while (testInfo == null || testInfo.action != action || testInfo.isINI) {\n      testInfo = await _next(timeoutInSeconds: timeoutInSeconds);\n    }\n\n    lastInfo = testInfo;\n\n    return testInfo;\n  }\n\n  /// Runs until **all** given actions types are dispatched, **in order**.\n  /// Waits until all of them are finished.\n  /// Returns the info after all actions finish.\n  /// Will fail with an exception if an unexpected action is seen,\n  /// or if any of the expected actions are dispatched in the wrong order.\n  ///\n  /// If you pass action types to [ignore], they will be ignored (the test won't fail when\n  /// encountering them, and won't collect testInfo for them). However, if an action type\n  /// exists both in [actionTypes] and [ignore], it will be expected in that particular order,\n  /// and the others of that type will be ignored. This method will remember all ignored actions\n  /// and wait for them to finish, so that they don't \"leak\" to the next wait.\n  ///\n  /// If [ignore] is null, it will use the global ignore provided in the\n  /// [StoreTester] constructor, if any. If [ignore] is an empty list, it\n  /// will disable that global ignore.\n  ///\n  Future<TestInfo<St>> waitAllGetLast(\n    List<Type> actionTypes, {\n    List<Type>? ignore,\n  }) async {\n    assert(actionTypes.isNotEmpty);\n    ignore ??= _ignore;\n\n    var infoList = await waitAll(actionTypes, ignore: ignore);\n\n    lastInfo = infoList.last;\n\n    return infoList.last;\n  }\n\n  /// Runs until **all** given actions types are dispatched, in **any order**.\n  /// Waits until all of them are finished. Returns the info after all actions finish.\n  /// Will fail with an exception if an unexpected action is seen.\n  ///\n  /// If you pass action types to [ignore], they will be ignored (the test won't fail when\n  /// encountering them, and won't collect testInfo for them). This method will remember all\n  /// ignored actions and wait for them to finish, so that they don't \"leak\" to the next wait.\n  /// An action type cannot exist in both [actionTypes] and [ignore] lists.\n  ///\n  Future<TestInfo<St>> waitAllUnorderedGetLast(\n    List<Type> actionTypes, {\n    int? timeoutInSeconds,\n    List<Type>? ignore,\n  }) async {\n    timeoutInSeconds ??= defaultTimeout;\n\n    return (await waitAllUnordered(\n      actionTypes,\n      timeoutInSeconds: timeoutInSeconds,\n      ignore: ignore,\n    ))\n        .last;\n  }\n\n  /// Runs until **all** given actions types are dispatched, **in order**.\n  /// Waits until all of them are finished.\n  /// Returns the info after all actions finish.\n  /// Will fail with an exception if an unexpected action is seen,\n  /// or if any of the expected actions are dispatched in the wrong order.\n  ///\n  /// If you pass action types to [ignore], they will be ignored (the test won't fail when\n  /// encountering them, and won't collect testInfo for them). However, if an action type\n  /// exists both in [actionTypes] and [ignore], it will be expected in that particular order,\n  /// and the others of that type will be ignored. This method will remember all ignored actions\n  /// and wait for them to finish, so that they don't \"leak\" to the next wait.\n  ///\n  /// If [ignore] is null, it will use the global ignore provided in the\n  /// [StoreTester] constructor, if any. If [ignore] is an empty list, it\n  /// will disable that global ignore.\n  ///\n  /// This method is the same as `waitAllGetLast`, but instead of returning\n  /// just the last info, it returns a list with the end info for each action.\n  ///\n  Future<TestInfoList<St>> waitAll(\n    List<Type> actionTypes, {\n    List<Type>? ignore,\n  }) async {\n    assert(actionTypes.isNotEmpty);\n    ignore ??= _ignore;\n\n    TestInfoList<St> infoList = TestInfoList<St>();\n\n    TestInfo<St>? testInfo;\n\n    Queue<Type> expectedActionTypesINI = Queue.from(actionTypes);\n\n    // These are for better error messages only.\n    List<Type> obtainedIni = [];\n    List<Type> ignoredIni = [];\n\n    List<ReduxAction?> expectedActionsEND = [];\n    List<ReduxAction?> expectedActionsENDIgnored = [];\n\n    while (expectedActionTypesINI.isNotEmpty ||\n        expectedActionsEND.isNotEmpty ||\n        expectedActionsENDIgnored.isNotEmpty) {\n      //\n      testInfo = await _next();\n\n      // Action INI must all exist, in order.\n      if (testInfo.isINI) {\n        //\n        bool wasIgnored = ignore.contains(testInfo.type) &&\n            (expectedActionTypesINI.isEmpty || //\n                expectedActionTypesINI.first != testInfo.type);\n\n        /// Record this action, so that later we can wait until it ends.\n        if (wasIgnored) {\n          expectedActionsENDIgnored.add(testInfo.action);\n          ignoredIni.add(testInfo.type); // // For better error messages only.\n        }\n        //\n        else {\n          expectedActionsEND.add(testInfo.action);\n          obtainedIni.add(testInfo.type); // For better error messages only.\n\n          Type? expectedActionTypeINI = expectedActionTypesINI.isEmpty\n              ? //\n              null\n              : expectedActionTypesINI.removeFirst();\n\n          if (testInfo.type != expectedActionTypeINI)\n            throw StoreException(\"Got this unexpected action: \"\n                \"${testInfo.type} INI.\\n\"\n                \"Was expecting: $expectedActionTypeINI INI.\\n\"\n                \"obtainedIni: $obtainedIni\\n\"\n                \"ignoredIni: $ignoredIni\");\n        }\n      }\n      //\n      // Action END must all exist, but the order doesn't matter.\n      else {\n        bool wasRemoved = expectedActionsEND.remove(testInfo.action);\n\n        if (wasRemoved)\n          infoList._add(testInfo);\n        else\n          wasRemoved = expectedActionsENDIgnored.remove(testInfo.action);\n\n        if (!wasRemoved)\n          throw StoreException(\"Got this unexpected action: \"\n              \"${testInfo.type} END.\\n\"\n              \"obtainedIni: $obtainedIni\\n\"\n              \"ignoredIni: $ignoredIni\");\n      }\n    }\n\n    lastInfo = infoList.last;\n\n    return infoList;\n  }\n\n  /// The same as `waitAllUnorderedGetLast`, but instead of returning just the last info,\n  /// it returns a list with the end info for each action.\n  ///\n  /// If you pass action types to [ignore], they will be ignored (the test won't fail when\n  /// encountering them, and won't collect testInfo for them). This method will remember all\n  /// ignored actions and wait for them to finish, so that they don't \"leak\" to the next wait.\n  /// An action type cannot exist in both [actionTypes] and [ignore] lists.\n  ///\n  /// If [ignore] is null, it will use the global ignore provided in the\n  /// [StoreTester] constructor, if any. If [ignore] is an empty list, it\n  /// will disable that global ignore.\n  ///\n  Future<TestInfoList<St>> waitAllUnordered(\n    List<Type> actionTypes, {\n    int? timeoutInSeconds,\n    List<Type>? ignore,\n  }) async {\n    assert(actionTypes.isNotEmpty);\n\n    timeoutInSeconds ??= defaultTimeout;\n\n    ignore ??= _ignore;\n\n    // Actions which are expected can't also be ignored.\n    var intersection = ignore.toSet().intersection(actionTypes.toSet());\n    if (intersection.isNotEmpty)\n      throw StoreException(\"Actions $intersection \"\n          \"should not be expected and ignored.\");\n\n    TestInfoList<St> infoList = TestInfoList<St>();\n    List<Type> actionsIni = List.from(actionTypes);\n    List<Type> actionsEnd = List.from(actionTypes);\n\n    TestInfo<St>? testInfo;\n\n    // Saves ignored actions INI.\n    // Note: This relies on Actions not overriding operator ==.\n    List<ReduxAction?> ignoredActions = [];\n\n    while (actionsIni.isNotEmpty || actionsEnd.isNotEmpty) {\n      try {\n        testInfo = await _next(timeoutInSeconds: timeoutInSeconds);\n\n        while (ignore.contains(testInfo!.type)) {\n          //\n          // Saves ignored actions.\n          if (ignore.contains(testInfo.type)) {\n            if (testInfo.isINI)\n              ignoredActions.add(testInfo.action);\n            else\n              ignoredActions.remove(testInfo.action);\n          }\n\n          testInfo = await (_next(timeoutInSeconds: timeoutInSeconds));\n        }\n      } on StoreExceptionTimeout catch (error) {\n        error.addDetail(\"These actions were not dispatched: \"\n            \"$actionsIni INI.\");\n        error.addDetail(\"These actions haven't finished: \"\n            \"$actionsEnd END.\");\n        rethrow;\n      }\n\n      var action = testInfo.type;\n\n      if (testInfo.isINI) {\n        if (!actionsIni.remove(action))\n          throw StoreException(\"Unexpected action was dispatched: \"\n              \"$action INI.\");\n      } else {\n        if (!actionsEnd.remove(action))\n          throw StoreException(\"Unexpected action was dispatched: \"\n              \"$action END.\");\n\n        // Only save the END states.\n        infoList._add(testInfo);\n      }\n    }\n\n    // Wait for all ignored actions to finish, so that they don't \"leak\" to the next wait.\n    while (ignoredActions.isNotEmpty) {\n      testInfo = await _next();\n\n      var wasIgnored = ignoredActions.remove(testInfo.action);\n\n      if (!wasIgnored && ignore.contains(testInfo.type)) {\n        if (testInfo.isINI)\n          ignoredActions.add(testInfo.action);\n        else\n          ignoredActions.remove(testInfo.action);\n        continue;\n      }\n\n      if (!testInfo.isEND || !wasIgnored)\n        throw StoreException(\"Got this unexpected action: \"\n            \"${testInfo.type} ${testInfo.ini ? \"INI\" : \"END\"}.\");\n    }\n\n    lastInfo = infoList.last;\n\n    return infoList;\n  }\n\n  void _listen() {\n    _store.initTestInfoController();\n    _subscription = _store.onReduce.listen(_completeFuture);\n    _completer = Completer();\n    _futures = Queue()..addLast(_completer.future);\n\n    _currentTestInfo = TestInfo<St>(\n      state,\n      false,\n      null,\n      null,\n      null,\n      store.dispatchCount,\n      store.reduceCount,\n      store.errors,\n    );\n  }\n\n  Future<TestInfo<St>> _next({\n    int? timeoutInSeconds,\n  }) async {\n    timeoutInSeconds ??= defaultTimeout;\n\n    if (_futures.isEmpty) {\n      _completer = Completer();\n      _futures.addLast(_completer.future);\n    }\n\n    var result = _futures.removeFirst();\n\n    _currentTestInfo = await result.timeout(\n      Duration(seconds: timeoutInSeconds),\n      onTimeout: (() => throw StoreExceptionTimeout()),\n    );\n\n    return _currentTestInfo;\n  }\n\n  void _completeFuture(TestInfo<St> reduceInfo) {\n    _completer.complete(reduceInfo);\n    _completer = Completer();\n    _futures.addLast(_completer.future);\n  }\n\n  Future cancel() async => await _subscription.cancel();\n\n  /// Helps testing the `StoreConnector`s methods, such as `onInit`,\n  /// `onDispose` and `onWillChange`.\n  ///\n  /// For example, suppose you have a `StoreConnector` which dispatches\n  /// `SomeAction` on its `onInit`. How could you test that?\n  ///\n  /// ```\n  /// class MyConnector extends StatelessWidget {\n  ///   Widget build(BuildContext context) => StoreConnector<AppState, Vm>(\n  ///         vm: () => _Factory(),\n  ///         onInit: _onInit,\n  ///         builder: (context, vm) { ... }\n  ///   }\n  ///\n  ///   void _onInit(Store<AppState> store) => store.dispatch(SomeAction());\n  /// }\n  ///\n  /// var storeTester = StoreTester(...);\n  /// var connectorTester = storeTester.getConnectorTester(MyConnector());\n  /// connectorTester.runOnInit();\n  /// var info = await tester.waitUntil(SomeAction);\n  /// ```\n  ///\n  ConnectorTester<St, Model> getConnectorTester<Model>(\n          StatelessWidget widgetConnector) =>\n      ConnectorTester<St, Model>(store, widgetConnector);\n}\n\n/// List of test information, before or after some actions are dispatched.\nclass TestInfoList<St> {\n  final List<TestInfo<St>> _info = [];\n\n  TestInfo<St> get last => _info.last;\n\n  TestInfo<St> get first => _info.first;\n\n  /// The number of dispatched actions.\n  int get length => _info.length;\n\n  /// Returns info corresponding to the end of the index-th dispatched action type.\n  TestInfo<St> getIndex(int index) => _info[index];\n\n  /// Returns the first info corresponding to the end of the given action type.\n  TestInfo<St>? operator [](Type actionType) =>\n      _info.firstWhereOrNull((info) => info.type == actionType);\n\n  /// Returns the n-th info corresponding to the end of the given action type\n  /// Note: N == 1 is the first one.\n  TestInfo<St>? get(Type actionType, [int n = 1]) => _info.firstWhereOrNull(\n        (info) {\n          var ifFound = (info.type == actionType);\n          if (ifFound) n--;\n          return ifFound && (n == 0);\n        },\n      );\n\n  /// Returns all info corresponding to the action type.\n  List<TestInfo<St>> getAll(Type actionType) {\n    return _info.where((info) => info.type == actionType).toList();\n  }\n\n  void forEach(void action(TestInfo<St> element)) => _info.forEach(action);\n\n  TestInfo<St> firstWhere(\n    bool test(TestInfo<St> element), {\n    TestInfo<St> orElse()?,\n  }) =>\n      _info.firstWhere(test, orElse: orElse);\n\n  TestInfo<St> lastWhere(\n    bool test(TestInfo<St> element), {\n    TestInfo<St> orElse()?,\n  }) =>\n      _info.lastWhere(test, orElse: orElse);\n\n  TestInfo<St> singleWhere(\n    bool test(TestInfo<St> element), {\n    TestInfo<St> orElse()?,\n  }) =>\n      _info.singleWhere(test, orElse: orElse);\n\n  Iterable<TestInfo<St>> where(\n          bool test(\n            TestInfo<St> element,\n          )) =>\n      _info.where(test);\n\n  Iterable<T> map<T>(T f(TestInfo<St> element)) => _info.map(f);\n\n  List<TestInfo<St>> toList({\n    bool growable = true,\n  }) =>\n      _info.toList(growable: growable);\n\n  Set<TestInfo<St>> toSet() => _info.toSet();\n\n  bool get isEmpty => length == 0;\n\n  bool get isNotEmpty => !isEmpty;\n\n  void _add(TestInfo<St> info) => _info.add(info);\n}\n\n/// Note: The [StoreExceptionTimeout] is used in the [StoreTester] only.\n/// In the wait methods of the [Store] (like [Store.waitCondition] etc),\n/// the timeouts throw [TimeoutException].\n///\nclass StoreExceptionTimeout extends StoreException {\n  StoreExceptionTimeout() : super(\"Timeout.\");\n\n  final List<String> _details = <String>[];\n\n  List<String> get details => _details;\n\n  void addDetail(String detail) => _details.add(detail);\n\n  @override\n  String toString() => (details.isEmpty)\n      ? msg\n      : //\n      msg + \"\\nDetails:\\n\" + details.map((d) => \"- $d\").join(\"\\n\");\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) || //\n      other is StoreExceptionTimeout && runtimeType == other.runtimeType;\n\n  @override\n  int get hashCode => 0;\n}\n\n/// During tests, use this error observer if you want all errors to be thrown,\n/// and not swallowed, including UserExceptions. You should probably use this\n/// in all tests that you don't expect to throw any errors, including\n/// UserExceptions.\n///\n/// On the contrary, when you are actually testing that some code throws\n/// specific UserExceptions, you should NOT use this error observer, but\n/// should instead let the UserExceptions go silently to the error queue\n/// (the `errors` field in the store), and then assert that the queue\n/// actually contains those errors.\n///\nclass TestErrorObserver<St> implements ErrorObserver<St> {\n  @override\n  bool observe(\n    Object error,\n    StackTrace stackTrace,\n    ReduxAction<St> action,\n    Store store,\n  ) =>\n      true;\n}\n\nclass _NewStateAction<St> extends ReduxAction<St> {\n  final St newState;\n\n  _NewStateAction(this.newState);\n\n  @override\n  St reduce() => newState;\n}\n"
  },
  {
    "path": "lib/src/test_info.dart",
    "content": "import 'dart:collection';\n\nimport 'package:async_redux/async_redux.dart';\n\ntypedef TestInfoPrinter = void Function(TestInfo);\n\nclass TestInfo<St> {\n  final St state;\n  final bool ini;\n  final ReduxAction<St>? action;\n  final int dispatchCount;\n  final int reduceCount;\n\n  /// List of all UserException's waiting to be displayed in the error dialog.\n  Queue<UserException> errors;\n\n  /// The error thrown by the action, if any,\n  /// before being processed by the action's wrapError() method.\n  final Object? error;\n\n  /// The error thrown by the action,\n  /// after being processed by the action's wrapError() method.\n  final Object? processedError;\n\n  bool get isINI => ini;\n\n  bool get isEND => !ini;\n\n  Type get type {\n    // Removes the generic type from UserExceptionAction, WaitAction,\n    // NavigateAction and PersistAction.\n    // For example UserExceptionAction<AppState> becomes UserExceptionAction<dynamic>.\n    if (action is UserExceptionAction) {\n      if (action.runtimeType.toString().split('<')[0] ==\n          'UserExceptionAction') //\n        return UserExceptionAction;\n    } else if (action is WaitAction) {\n      if (action.runtimeType.toString().split('<')[0] == 'WaitAction') //\n        return WaitAction;\n    } else if (action is NavigateAction) {\n      if (action.runtimeType.toString().split('<')[0] == 'NavigateAction') //\n        return NavigateAction;\n    } else if (action is PersistAction) {\n      if (action.runtimeType.toString().split('<')[0] == 'PersistAction') //\n        return PersistAction;\n    }\n\n    return action.runtimeType;\n  }\n\n  TestInfo(\n    this.state,\n    this.ini,\n    this.action,\n    this.error,\n    this.processedError,\n    this.dispatchCount,\n    this.reduceCount,\n    this.errors,\n  ) : assert(state != null);\n\n  @override\n  String toString() => 'D:$dispatchCount '\n      'R:$reduceCount '\n      '= $action ${ini ? \"INI\" : \"END\"}\\n';\n}\n"
  },
  {
    "path": "lib/src/user_exception_dialog.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'dart:collection';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\n\nimport 'show_dialog_super.dart';\n\n/// Use it like this:\n///\n/// ```\n/// class MyApp extends StatelessWidget {\n///   @override\n///   Widget build(BuildContext context)\n///     => StoreProvider<AppState>(\n///       store: store,\n///       child: MaterialApp(\n///           home: UserExceptionDialog<AppState>(\n///             child: MyHomePage(),\n///           )));\n/// }\n///\n/// ```\n///\n/// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n///\nclass UserExceptionDialog<St> extends StatelessWidget {\n  final Widget child;\n  final ShowUserExceptionDialog? onShowUserExceptionDialog;\n\n  /// If false (the default), the dialog will appear in the context of the\n  /// [NavigateAction.navigatorKey]. If you don't set up that key, or if you\n  /// pass `true` here, it will use the local context of the\n  /// [UserExceptionDialog] widget.\n  ///\n  /// Make sure this is `false` if you are putting the [UserExceptionDialog] in\n  /// the `builder` parameter of the [MaterialApp] widget, because in this case\n  /// the [UserExceptionDialog] will be above the app's [Navigator], and if\n  /// you open the dialog in the local context you won't be able to use the\n  /// Android back-button to close it.\n  final bool useLocalContext;\n\n  UserExceptionDialog({\n    required this.child,\n    this.onShowUserExceptionDialog,\n    this.useLocalContext = false,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    //\n    return StoreConnector<St, _Vm>(\n      vm: () => _Factory<St>(),\n      builder: (context, vm) {\n        //\n        Event<UserException>? errorEvent = //\n            (_Factory._errorEvents.isEmpty) //\n                ? null\n                : _Factory._errorEvents.removeFirst();\n\n        return _UserExceptionDialogWidget(\n          child,\n          errorEvent,\n          onShowUserExceptionDialog,\n          useLocalContext,\n        );\n      },\n    );\n  }\n}\n\nclass _UserExceptionDialogWidget extends StatefulWidget {\n  final Widget child;\n  final Event<UserException>? errorEvent;\n  final ShowUserExceptionDialog onShowUserExceptionDialog;\n  final bool useLocalContext;\n\n  _UserExceptionDialogWidget(\n    this.child,\n    this.errorEvent,\n    ShowUserExceptionDialog? onShowUserExceptionDialog,\n    this.useLocalContext,\n  ) : onShowUserExceptionDialog = //\n            onShowUserExceptionDialog ?? _defaultUserExceptionDialog;\n\n  static void _defaultUserExceptionDialog(\n    BuildContext context,\n    UserException userException,\n    bool useLocalContext,\n  ) {\n    if (!useLocalContext) {\n      var navigatorContext = NavigateAction.navigatorKey?.currentContext;\n      if (navigatorContext != null) context = navigatorContext;\n    }\n\n    defaultTargetPlatform;\n    if (!kIsWeb && (defaultTargetPlatform == TargetPlatform.iOS)) {\n      showCupertinoDialogSuper<int>(\n        context: context,\n        onDismissed: (int? result) {\n          if (result == 1)\n            userException.onOk?.call();\n          else if (result == 2)\n            userException.onCancel?.call();\n          else {\n            if (userException.onCancel == null)\n              userException.onOk?.call();\n            else\n              userException.onCancel?.call();\n          }\n        },\n        builder: (BuildContext context) {\n          var (title, content) = userException.titleAndContent();\n          return CupertinoAlertDialog(\n            title: Text(title),\n            content: Text(content),\n            actions: [\n              CupertinoDialogAction(\n                child: const Text(\"OK\"),\n                onPressed: () {\n                  Navigator.of(context).pop(1);\n                },\n              ),\n              if (userException.onCancel != null)\n                CupertinoDialogAction(\n                  child: const Text(\"CANCEL\"),\n                  onPressed: () {\n                    Navigator.of(context).pop(2);\n                  },\n                )\n            ],\n          );\n        },\n      );\n    } else\n      showDialogSuper<int>(\n        context: context,\n        onDismissed: (int? result) {\n          if (result == 1)\n            userException.onOk?.call();\n          else if (result == 2)\n            userException.onCancel?.call();\n          else {\n            if (userException.onCancel == null)\n              userException.onOk?.call();\n            else\n              userException.onCancel?.call();\n          }\n        },\n        builder: (BuildContext context) {\n          var (title, content) = userException.titleAndContent();\n          return AlertDialog(\n            title: Text(title),\n            content: Text(content),\n            actions: [\n              if (userException.onCancel != null)\n                TextButton(\n                  child: const Text(\"CANCEL\"),\n                  onPressed: () {\n                    Navigator.of(context).pop(2);\n                  },\n                ),\n              TextButton(\n                child: const Text(\"OK\"),\n                onPressed: () {\n                  Navigator.of(context).pop(1);\n                },\n              )\n            ],\n          );\n        },\n      );\n  }\n\n  @override\n  _UserExceptionDialogState createState() => _UserExceptionDialogState();\n}\n\nclass _UserExceptionDialogState extends State<_UserExceptionDialogWidget> {\n  @override\n  void didUpdateWidget(_UserExceptionDialogWidget oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    UserException? userException = widget.errorEvent?.consume();\n\n    if (userException != null)\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        widget.onShowUserExceptionDialog(\n            context, userException, widget.useLocalContext);\n      });\n  }\n\n  @override\n  Widget build(BuildContext context) => widget.child;\n}\n\nclass _Factory<St> extends VmFactory<St, UserExceptionDialog, _Vm> {\n  static final Queue<Event<UserException>> _errorEvents = Queue();\n\n  @override\n  _Vm fromStore() {\n    UserException? error = getAndRemoveFirstError();\n\n    if (error != null) _errorEvents.add(Event(error));\n\n    return _Vm(\n      rebuild: (error != null),\n    );\n  }\n}\n\nclass _Vm extends Vm {\n  //\n  final bool rebuild;\n\n  _Vm({required this.rebuild});\n\n  /// Does not respect equals contract:\n  /// Is not equal when it should rebuild.\n  @override\n  bool operator ==(Object other) => !rebuild;\n\n  @override\n  int get hashCode => rebuild.hashCode;\n}\n\ntypedef ShowUserExceptionDialog = void Function(\n  BuildContext context,\n  UserException userException,\n  bool useLocalContext,\n);\n"
  },
  {
    "path": "lib/src/view_model.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlibrary async_redux_view_model;\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\n\n/// Each state passed in the [Vm.equals] parameter in the in view-model will be\n/// compared by equality (==), unless it is of type [VmEquals], when it will be\n/// compared by the [VmEquals.vmEquals] method, which by default is a comparison\n/// by identity (but can be overridden).\nabstract class VmEquals<T> {\n  bool vmEquals(T other) => identical(this, other);\n}\n\n/// [Vm] is a base class for your view-models.\n///\n/// A view-model is a helper object to a [StoreConnector] widget. It holds the\n/// part of the Store state the corresponding dumb-widget needs, and may also\n/// convert this state part into a more convenient format for the dumb-widget\n/// to work with.\n///\n/// Each time the state changes, all [StoreConnector]s in the widget tree will\n/// create a view-model, and compare it with the view-model they created with\n/// the previous state. Only if the view-model changed, the [StoreConnector]\n/// will rebuild. For this to work, you must implement equals/hashcode for the\n/// view-model class. Otherwise, the [StoreConnector] will think the view-model\n/// changed everytime, and thus will rebuild everytime. This wouldn't create any\n/// visible problems to your app, but would be inefficient and maybe slow.\n///\n/// Using the [Vm] class you can implement equals/hashcode without having to\n/// override these methods. Instead, simply list all fields (which are not\n/// immutable, like functions) to the [equals] parameter in the constructor.\n/// For example:\n///\n/// ```\n/// ViewModel({this.counter, this.onIncrement}) : super(equals: [counter]);\n/// ```\n///\n/// Each listed state will be compared by equality (==), unless it is of type\n/// [VmEquals], when it will be compared by the [VmEquals.vmEquals] method,\n/// which by default is a comparison by identity (but can be overridden).\n///\n@immutable\nabstract class Vm {\n  //\n\n  /// To test the view-model generated by a Factory, use [createFrom] and pass it the\n  /// [store] and the [factory]. Note this method must be called in a recently\n  /// created factory, as it can only be called once per factory instance.\n  ///\n  /// The method will return the view-model, which you can use to:\n  ///\n  /// * Inspect the view-model properties directly, or\n  ///\n  /// * Call any of the view-model callbacks. If the callbacks dispatch actions,\n  /// you use `await store.waitActionType(MyAction)`,\n  /// or `await store.waitAllActionTypes([MyAction, OtherAction])`,\n  /// or `await store.waitCondition((state) => ...)`, or if necessary you can even\n  /// record all dispatched actions and state changes with `Store.record.start()`\n  /// and `Store.record.stop()`.\n  ///\n  /// Example:\n  /// ```\n  /// var store = Store(initialState: User(\"Mary\"));\n  /// var vm = Vm.createFrom(store, MyFactory());\n  ///\n  /// // Checking a view-model property.\n  /// expect(vm.user.name, \"Mary\");\n  ///\n  /// // Calling a view-model callback and waiting for the action to finish.\n  /// vm.onChangeNameTo(\"Bill\"); // Dispatches SetNameAction(\"Bill\").\n  /// await store.waitActionType(SetNameAction);\n  /// expect(store.state.name, \"Bill\");\n  ///\n  /// // Calling a view-model callback and waiting for the state to change.\n  /// vm.onChangeNameTo(\"Bill\"); // Dispatches SetNameAction(\"Bill\").\n  /// await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(store.state.name, \"Bill\");\n  /// ```\n  ///\n  @visibleForTesting\n  static Model createFrom<St, T extends Widget?, Model extends Vm>(\n    Store<St> store,\n    VmFactory<St, T, Model> factory,\n  ) {\n    internalsVmFactoryInject(factory, store.state, store);\n    return internalsVmFactoryFromStore(factory) as Model;\n  }\n\n  /// The List of properties which will be used to determine whether two BaseModels are equal.\n  final List<Object?> equals;\n\n  /// The constructor takes an optional List of fields which will be used\n  /// to determine whether two [Vm] are equal.\n  Vm({this.equals = const []})\n      : assert(_onlyContainFieldsOfAllowedTypes(equals));\n\n  /// Fields should not contain functions.\n  static bool _onlyContainFieldsOfAllowedTypes(List equals) {\n    equals.forEach((Object? field) {\n      if (field is Function)\n        throw StoreException(\"ViewModel equals \"\n            \"can't contain field of type Function: ${field.runtimeType}.\");\n    });\n\n    return true;\n  }\n\n  @override\n  bool operator ==(Object other) {\n    return identical(this, other) ||\n        other is Vm &&\n            runtimeType == other.runtimeType &&\n            _listEquals(\n              equals,\n              other.equals,\n            );\n  }\n\n  bool _listEquals<T>(List<T>? list1, List<T>? list2) {\n    if (list1 == null) return list2 == null;\n    if (list2 == null || list1.length != list2.length) return false;\n    if (identical(list1, list2)) return true;\n    for (int index = 0; index < list1.length; index++) {\n      var item1 = list1[index];\n      var item2 = list2[index];\n\n      if ((item1 is VmEquals<T>) &&\n          (item2 is VmEquals<T>) //\n          &&\n          !item1.vmEquals(item2)) return false;\n\n      if (item1 != item2) return false;\n    }\n    return true;\n  }\n\n  @override\n  int get hashCode => runtimeType.hashCode ^ _propsHashCode;\n\n  int get _propsHashCode {\n    int hashCode = 0;\n    equals.forEach((Object? prop) => hashCode = hashCode ^ prop.hashCode);\n    return hashCode;\n  }\n\n  @override\n  String toString() => '$runtimeType{${equals.join(', ')}}';\n}\n\n/// Factory that creates a view-model of type [Vm], for the [StoreConnector]:\n///\n/// ```\n/// return StoreConnector<AppState, _ViewModel>(\n///      vm: _Factory(),\n///      builder: ...\n/// ```\n///\n/// You must override the [fromStore] method:\n///\n/// ```\n/// class _Factory extends VmFactory {\n///    _ViewModel fromStore() => _ViewModel(\n///        counter: state,\n///        onIncrement: () => dispatch(IncrementAction(amount: 1)));\n/// }\n/// ```\n///\n/// If necessary, you can pass the [StoreConnector] widget to the factory:\n///\n/// ```\n/// return StoreConnector<AppState, _ViewModel>(\n///      vm: _Factory(this),\n///      builder: ...\n///\n/// ...\n/// class _Factory extends VmFactory<AppState, MyHomePageConnector> {\n///    _Factory(connector) : super(connector);\n///    _ViewModel fromStore() => _ViewModel(\n///        counter: state,\n///        onIncrement: () => dispatch(IncrementAction(amount: widget.amount)));\n/// }\n/// ```\n///\nabstract class VmFactory<St, T extends Widget?, Model extends Vm> {\n  /// You need to pass the connector widget only if the view-model needs any info from it.\n  VmFactory([this._connector]);\n\n  Model? fromStore();\n\n  final T? _connector;\n\n  /// The connector widget that will instantiate the view-model.\n  @Deprecated(\"Use `connector` instead\")\n  T? get widget => _connector;\n\n  /// The connector widget that will instantiate the view-model.\n  T get connector {\n    if (_connector == null)\n      throw StoreException(\n          \"To use the `connector` field you must pass it to the factory constructor:\"\n          \"\\n\\n\"\n          \"return StoreConnector<AppState, _Vm>(\\n\"\n          \"   vm: () => Factory(this),\\n\"\n          \"   ...\"\n          \"\\n\\n\"\n          \"class Factory extends VmFactory<_Vm, MyConnector> {\\n\"\n          \"   Factory(Widget widget) : super(widget);\");\n    else\n      return _connector;\n  }\n\n  late final Store<St> _store;\n  late final St _state;\n\n  /// Once the Vm is created, we save it so that it can be used by factory methods.\n  Model? _vm;\n  bool _vmCreated = false;\n\n  /// Once the view-model is created, and as long as it's not null, you can reference\n  /// it by using the [vm] getter. This is meant to be used inside of Factory methods.\n  ///\n  /// Example:\n  ///\n  /// ```\n  /// ViewModel fromStore() =>\n  ///   ViewModel(\n  ///     value: _calculateValue(),\n  ///     onTap: _onTap);\n  ///   }\n  ///\n  /// // Here we use the value, without having to recalculate it.\n  /// void _onTap() => dispatch(SaveValueAction(vm.value));\n  /// ```\n  ///\n  Model get vm {\n    if (!_vmCreated)\n      throw StoreException(\"You can't reference the view-model \"\n          \"before it's created and returned by the fromStore method.\");\n\n    if (_vm == null)\n      throw StoreException(\"You can't reference the view-model, \"\n          \"because it's null.\");\n\n    return _vm!;\n  }\n\n  bool get ifVmIsNull {\n    if (!_vmCreated)\n      throw StoreException(\"You can't reference the view-model \"\n          \"before it's created and returned by the fromStore method.\");\n\n    return (_vm == null);\n  }\n\n  void _setStore(St state, Store store) {\n    _store = store as Store<St>;\n    _state = state;\n  }\n\n  /// The state the store was holding when the factory and the view-model were created.\n  /// This state is final inside of the factory.\n  St get state => _state;\n\n  /// Gets a property from the store.\n  /// This can be used to save global values, but scoped to the store.\n  /// For example, you could save timers, streams or futures used by actions.\n  ///\n  /// ```dart\n  /// setProp(\"timer\", Timer(Duration(seconds: 1), () => print(\"tick\")));\n  /// var timer = prop<Timer>(\"timer\");\n  /// timer.cancel();\n  /// ```\n  ///\n  /// See also: [setProp].\n  V prop<V>(Object? key) => _store.prop<V>(key);\n\n  /// Sets a property in the store.\n  /// This can be used to save global values, but scoped to the store.\n  /// For example, you could save timers, streams or futures used by actions.\n  ///\n  /// ```dart\n  /// setProp(\"timer\", Timer(Duration(seconds: 1), () => print(\"tick\")));\n  /// var timer = prop<Timer>(\"timer\");\n  /// timer.cancel();\n  /// ```\n  ///\n  /// See also: [prop] and [env].\n  void setProp(Object? key, Object? value) => _store.setProp(key, value);\n\n  /// The current (most recent) store state.\n  /// This will return the current state the store holds at the time the method is called.\n  St currentState() => _store.state;\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// The action may be sync or async.\n  ///\n  /// ```dart\n  /// store.dispatch(MyAction());\n  /// ```\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of this action, even if it changes the state.\n  ///\n  /// Method [dispatch] is of type [Dispatch].\n  ///\n  /// See also:\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  ///\n  Dispatch<St> get dispatch => _store.dispatch;\n\n  @Deprecated(\"Use `dispatchAndWait` instead. This will be removed.\")\n  DispatchAsync<St> get dispatchAsync => _store.dispatchAndWait;\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// The action may be sync or async. In both cases, it returns a [Future] that resolves when\n  /// the action finishes.\n  ///\n  /// ```dart\n  /// await store.dispatchAndWait(DoThisFirstAction());\n  /// store.dispatch(DoThisSecondAction());\n  /// ```\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of this action, even if it changes the state.\n  ///\n  /// Note: While the state change from the action's reducer will have been applied when the\n  /// Future resolves, other independent processes that the action may have started may still\n  /// be in progress.\n  ///\n  /// Method [dispatchAndWait] is of type [DispatchAndWait]. It returns `Future<ActionStatus>`,\n  /// which means you can also get the final status of the action after you `await` it:\n  ///\n  /// ```dart\n  /// var status = await store.dispatchAndWait(MyAction());\n  /// ```\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchSync] which dispatches sync actions, and throws if the action is async.\n  ///\n  DispatchAndWait<St> get dispatchAndWait => _store.dispatchAndWait;\n\n  /// Dispatches the action, applying its reducer, and possibly changing the store state.\n  /// However, if the action is ASYNC, it will throw a [StoreException].\n  ///\n  /// If you pass the [notify] parameter as `false`, widgets will not necessarily rebuild because\n  /// of this action, even if it changes the state.\n  ///\n  /// Method [dispatchSync] is of type [DispatchSync]. It returns `ActionStatus`,\n  /// which means you can also get the final status of the action:\n  ///\n  /// ```dart\n  /// var status = store.dispatchSync(MyAction());\n  /// ```\n  ///\n  /// See also:\n  /// - [dispatch] which dispatches both sync and async actions.\n  /// - [dispatchAndWait] which dispatches both sync and async actions, and returns a Future.\n  ///\n  DispatchSync<St> get dispatchSync => _store.dispatchSync;\n\n  /// You can use [isWaiting] to check if:\n  /// * A specific async ACTION is currently being processed.\n  /// * An async action of a specific TYPE is currently being processed.\n  /// * If any of a few given async actions or action types is currently being processed.\n  ///\n  /// If you wait for an action TYPE, then it returns false when:\n  /// - The ASYNC action of type [actionType] is NOT currently being processed.\n  /// - If [actionType] is not really a type that extends [ReduxAction].\n  /// - The action of type [actionType] is a SYNC action (since those finish immediately).\n  ///\n  /// If you wait for an ACTION, then it returns false when:\n  /// - The ASYNC [action] is NOT currently being processed.\n  /// - If [action] is a SYNC action (since those finish immediately).\n  //\n  /// Examples:\n  ///\n  /// ```dart\n  /// // Waiting for an action TYPE:\n  /// dispatch(MyAction());\n  /// if (isWaiting(MyAction)) { // Show a spinner }\n  ///\n  /// // Waiting for an ACTION:\n  /// var action = MyAction();\n  /// dispatch(action);\n  /// if (isWaiting(action)) { // Show a spinner }\n  ///\n  /// // Waiting for any of the given action TYPES:\n  /// dispatch(BuyAction());\n  /// if (isWaiting([BuyAction, SellAction])) { // Show a spinner }\n  /// ```\n  bool isWaiting(Object actionOrTypeOrList) =>\n      _store.isWaiting(actionOrTypeOrList);\n\n  /// Returns true if an [actionOrTypeOrList] failed with an [UserException].\n  /// Note: This method uses the EXACT type in [actionOrTypeOrList]. Subtypes are not considered.\n  bool isFailed(Object actionOrTypeOrList) =>\n      _store.isFailed(actionOrTypeOrList);\n\n  /// Returns the [UserException] of the [actionTypeOrList] that failed.\n  ///\n  /// [actionTypeOrList] can be a [Type], or an Iterable of types. Any other type\n  /// of object will return null and throw a [StoreException] after the async gap.\n  ///\n  /// Note: This method uses the EXACT type in [actionTypeOrList]. Subtypes are not considered.\n  UserException? exceptionFor(Object actionTypeOrList) =>\n      _store.exceptionFor(actionTypeOrList);\n\n  /// Removes the given [actionTypeOrList] from the list of action types that failed.\n  ///\n  /// Note that dispatching an action already removes that action type from the exceptions list.\n  /// This removal happens as soon as the action is dispatched, not when it finishes.\n  ///\n  /// [actionTypeOrList] can be a [Type], or an Iterable of types. Any other type\n  /// of object will return null and throw a [StoreException] after the async gap.\n  ///\n  /// Note: This method uses the EXACT type in [actionTypeOrList]. Subtypes are not considered.\n  void clearExceptionFor(Object actionTypeOrList) =>\n      _store.clearExceptionFor(actionTypeOrList);\n\n  /// Returns a future which will complete when the given state [condition] is true.\n  /// If the condition is already true when the method is called, the future completes immediately.\n  ///\n  /// You may also provide a [timeoutMillis], which by default is 10 minutes.\n  /// To disable the timeout, make it -1.\n  /// If you want, you can modify [Store.defaultTimeoutMillis] to change the default timeout.\n  ///\n  /// ```dart\n  /// var action = await store.waitCondition((state) => state.name == \"Bill\");\n  /// expect(action, isA<ChangeNameAction>());\n  /// ```\n  Future<ReduxAction<St>?> waitCondition(\n    bool Function(St) condition, {\n    int? timeoutMillis,\n  }) =>\n      _store.waitCondition(condition, timeoutMillis: timeoutMillis);\n\n  /// Returns a future that completes when ALL given [actions] finished dispatching.\n  /// You MUST provide at list one action, or an error will be thrown.\n  ///\n  /// If [completeImmediately] is `false` (the default), this method will throw [StoreException]\n  /// if none of the given actions are in progress when the method is called. Otherwise, the future\n  /// will complete immediately and throw no error.\n  ///\n  /// Example:\n  ///\n  /// ```ts\n  /// // Dispatching two actions in PARALLEL and waiting for both to finish.\n  /// var action1 = ChangeNameAction('Bill');\n  /// var action2 = ChangeAgeAction(42);\n  /// await waitAllActions([action1, action2]);\n  ///\n  /// // Compare this to dispatching the actions in SERIES:\n  /// await dispatchAndWait(action1);\n  /// await dispatchAndWait(action2);\n  /// ```\n  Future<void> waitAllActions(List<ReduxAction<St>> actions,\n      {bool completeImmediately = false}) {\n    if (actions.isEmpty)\n      throw StoreException('You have to provide a non-empty list of actions.');\n    return _store.waitAllActions(actions,\n        completeImmediately: completeImmediately);\n  }\n\n  /// Gets the first error from the error queue, and removes it from the queue.\n  UserException? getAndRemoveFirstError() => _store.getAndRemoveFirstError();\n}\n\n/// For internal use only. Please don't use this.\nVm? internalsVmFactoryFromStore(\n    VmFactory<dynamic, dynamic, dynamic> vmFactory) {\n  vmFactory._vm = vmFactory.fromStore();\n  vmFactory._vmCreated = true;\n  return vmFactory._vm;\n}\n\n/// For internal use only. Please don't use this.\nvoid internalsVmFactoryInject<St>(\n    VmFactory<St, dynamic, dynamic> vmFactory, St state, Store store) {\n  vmFactory._setStore(state, store);\n}\n"
  },
  {
    "path": "lib/src/wait.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:flutter/foundation.dart';\n\nenum WaitOperation { add, remove, clear }\n\n/// Immutable object to keep track of boolean flags that indicate if some\n/// process is in progress (the user is \"waiting\").\n///\n/// The flags and flag-references can be any immutable object.\n/// They must be immutable to make sure [Wait] is also immutable.\n///\n/// Use it in Redux store states, like this:\n/// * To add a flag: state.copy(wait: state.wait.add(flag: myFlag));\n/// * To remove a flag: state.copy(wait: state.wait.remove(flag: myFlag));\n/// * To clear all flags: state.copy(wait: state.wait.clear());\n///\n/// If can also use have a flag with a reference, like this:\n/// * To add a flag with reference: state.copy(wait: state.wait.add(flag: myFlag, ref:MyRef));\n/// * To remove a flag with reference: state.copy(wait: state.wait.remove(flag: myFlag, ref:MyRef));\n/// * To clear all references for a flag: state.copy(wait: state.wait.clear(flag: myFlag));\n///\n/// In the ViewModel, you can check the flags/references, like this:\n///\n/// * To check if there is any waiting: state.wait.isWaitingAny\n/// * To check if is waiting a specific flag: state.wait.isWaiting(myFlag);\n/// * To check if is waiting a specific flag/reference: state.wait.isWaiting(myFlag, ref: myRef);\n///\n@immutable\nclass Wait {\n  final Map<Object?, Set<Object?>> _flags;\n\n  static const Wait empty = Wait._({});\n\n  factory Wait() => empty;\n\n  /// Convenience flag that you can use when a `null` value means ALL.\n  /// For example, suppose if you want until an async process schedules an `appointment`\n  /// for specific `time`. However, if no time is selected, you want to schedule the whole\n  /// day (all \"times\"). You can do:\n  /// `dispatch(WaitAction.add(appointment, ref: time ?? Wait.ALL));`\n  ///\n  /// And then later check if you are waiting for a specific time:\n  /// `if (wait.isWaiting(appointment, ref: time) { ... }`\n  ///\n  /// Or if you are waiting for the whole day:\n  /// `if (wait.isWaiting(appointment, ref: Wait.ALL) { ... }`\n  ///\n  static const ALL = Object();\n\n  const Wait._(Map<Object?, Set<Object?>> flags) : _flags = flags;\n\n  Wait add({required Object? flag, Object? ref}) {\n    Map<Object?, Set<Object?>> newFlags = _deepCopy();\n\n    Set<Object?>? refs = newFlags[flag];\n    if (refs == null) {\n      refs = {};\n      newFlags[flag] = refs;\n    }\n    refs.add(ref);\n\n    return Wait._(newFlags);\n  }\n\n  Wait remove({required Object? flag, Object? ref}) {\n    if (_flags.isEmpty)\n      return this;\n    else {\n      Map<Object?, Set<Object?>> newFlags = _deepCopy();\n\n      if (ref == null) {\n        newFlags.remove(flag);\n      } else {\n        Set<Object?> refs = newFlags[flag] ?? {};\n        refs.remove(ref);\n        if (refs.isEmpty) newFlags.remove(flag);\n      }\n\n      if (newFlags.isEmpty)\n        return empty;\n      else\n        return Wait._(newFlags);\n    }\n  }\n\n  Wait process(\n    WaitOperation operation, {\n    required Object? flag,\n    Object? ref,\n  }) {\n    if (operation == WaitOperation.add)\n      return add(flag: flag, ref: ref);\n    else if (operation == WaitOperation.remove)\n      return remove(flag: flag, ref: ref);\n    else if (operation == WaitOperation.clear)\n      return clear(flag: flag);\n    else\n      throw AssertionError(operation);\n  }\n\n  /// Return true if there is any waiting (any flag).\n  bool get isWaitingAny => _flags.isNotEmpty;\n\n  /// Return true if is waiting for a specific flag.\n  /// If [ref] is null, it returns true if it's waiting for any reference of the flag.\n  /// If [ref] is not null, it returns true if it's waiting for that specific reference of the flag.\n  bool isWaiting(Object? flag, {Object? ref}) {\n    Set? refs = _flags[flag];\n\n    return (ref == null) //\n        ? (refs != null) && refs.isNotEmpty //\n        : (refs != null) && refs.contains(ref);\n  }\n\n  /// Return true if is waiting for ANY flag of the specific type.\n  ///\n  /// This is useful when you want to wait for an Action to finish. For example:\n  ///\n  /// ```\n  /// class MyAction extends ReduxAction<AppState> {\n  ///   Future<AppState?> reduce() async {\n  ///     await doSomething();\n  ///     return null;\n  ///   }\n  ///\n  ///   void before() => dispatch(WaitAction.add(this));\n  ///   void after() => dispatch(WaitAction.remove(this));\n  /// }\n  ///\n  /// // Then, in some widget or connector:\n  /// if (wait.isWaitingForType<MyAction>()) { ... }\n  /// ```\n  bool isWaitingForType<T>() {\n    for (Object? flag in _flags.keys) if (flag is T) return true;\n    return false;\n  }\n\n  Wait clear({Object? flag}) {\n    if (flag == null)\n      return empty;\n    else {\n      Map<Object?, Set<Object?>> newFlags = _deepCopy();\n      newFlags.remove(flag);\n      return Wait._(newFlags);\n    }\n  }\n\n  void clearWhere(\n          bool Function(\n            Object? flag,\n            Set<Object?> refs,\n          ) test) =>\n      _flags.removeWhere(test);\n\n  Map<Object?, Set<Object?>> _deepCopy() {\n    Map<Object?, Set<Object?>> newFlags = {};\n\n    for (var MapEntry(key: key, value: value) in _flags.entries) {\n      newFlags[key] = Set.of(value);\n    }\n\n    return newFlags;\n  }\n}\n"
  },
  {
    "path": "lib/src/wait_action.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:flutter/cupertino.dart';\n\nimport '../async_redux.dart';\n\n/// [WaitAction] and [Wait] work together to help you create boolean flags that\n/// indicate some process is currently running. For this to work your store state\n/// must have a `Wait` field named `wait`, and then:\n///\n/// 1) The state must have a `copy` or `copyWith` method that copies this\n/// field as a named parameter. For example:\n///\n/// ```\n/// class AppState {\n///   final Wait wait;\n///   AppState({this.wait});\n///   AppState copy({Wait wait}) => AppState(wait: wait);\n///   }\n/// ```\n///\n/// OR:\n///\n/// 2) You must use the BuiltValue package https://pub.dev/packages/built_value,\n/// which automatically creates a `rebuild` method.\n///\n/// OR:\n///\n/// 3) You must use the Freezed package https://pub.dev/packages/freezed,\n/// which automatically creates the `copyWith` method.\n///\n/// OR:\n///\n/// 4) Inject your own [WaitReducer] implementation into [WaitAction]\n/// by replacing the static variable [WaitAction.reducer] with a callback\n/// that changes the wait object as you see fit.\n///\n/// OR:\n///\n/// 5) Don't use the [WaitAction], but instead create your own `MyWaitAction`\n/// that uses the [Wait] object in whatever way you want.\n///\nclass WaitAction<St> extends ReduxAction<St> {\n  //\n\n  /// Works out-of-the-box for most use cases, but you can inject your\n  /// own reducer here during your app's initialization, if necessary.\n  static WaitReducer reducer = _defaultReducer;\n\n  /// The default is to choose a reducer that is compatible with your AppState class.\n  static final WaitReducer _defaultReducer = (\n    state,\n    operation,\n    flag,\n    ref,\n  ) {\n    try {\n      return _copyReducer(state, operation, flag, ref);\n    } on NoSuchMethodError catch (_) {\n      try {\n        return _builtValueReducer(state, operation, flag, ref);\n      } on NoSuchMethodError catch (_) {\n        try {\n          return _freezedReducer(state, operation, flag, ref);\n        } on NoSuchMethodError catch (_) {\n          throw AssertionError(\"The store state \"\n              \"is not compatible with WaitAction.\");\n        }\n      }\n    }\n  };\n\n  /// For this to work, your state class must have a [copy] method.\n  static final WaitReducer _copyReducer = (state, operation, flag, ref) {\n    Wait wait = (state as dynamic).wait ?? Wait();\n    return (state as dynamic).copy(\n        wait: wait.process(\n      operation,\n      flag: flag,\n      ref: ref,\n    ));\n  };\n\n  /// For this to work, your state class must have a suitable [rebuild] method.\n  /// This happens automatically when you use the BuiltValue package.\n  static final WaitReducer _builtValueReducer = (state, operation, flag, ref) {\n    Wait wait = (state as dynamic).wait ?? Wait();\n    return (state as dynamic).rebuild((state) => state\n      ..wait = wait.process(\n        operation,\n        flag: flag,\n        ref: ref,\n      ));\n  };\n\n  /// For this to work, your state class must have a [copyWith] method.\n  /// This happens automatically when you use the Freezed package.\n  static final WaitReducer _freezedReducer = (state, operation, flag, ref) {\n    Wait wait = (state as dynamic).wait ?? Wait();\n    return (state as dynamic).copyWith(\n        wait: wait.process(\n      operation,\n      flag: flag,\n      ref: ref,\n    ));\n  };\n\n  final WaitOperation operation;\n\n  final Object? flag, ref;\n  final Duration? delay;\n\n  /// Adds a [flag] that indicates some process is currently running.\n  /// Optionally, you can also have a flag-reference called [ref].\n  ///\n  /// Note: [flag] and [ref] must be immutable objects.\n  ///\n  /// ```\n  /// // Add a wait state, using this as the flag.\n  /// dispatch(WaitAction.add(this));\n  ///\n  /// // Add a wait state, using this as the flag, and 123 as a reference.\n  /// dispatch(WaitAction.add(this, ref: 123));\n  /// ```\n  /// Note: When the process finishes running, you will have to remove\n  /// the [flag] by using the [remove] or [clear] methods.\n  ///\n  /// If you pass a [delay], the flag will be added only after that\n  /// duration has passed, after the [add] method is called.\n  ///\n  WaitAction.add(\n    this.flag, {\n    this.ref,\n    this.delay,\n  }) : operation = WaitOperation.add;\n\n  /// Removes a [flag] previously added with the [add] method.\n  /// Removing the flag indicating some process finished running.\n  ///\n  /// If you added the flag with a reference [ref], you must also pass the\n  /// same reference here to remove it. Alternatively, if you want to\n  /// remove all references to that flag, use the [clear] method instead.\n  ///\n  /// ```\n  /// // Add and remove a wait state, using this as the flag.\n  /// dispatch(WaitAction.add(this));\n  /// dispatch(WaitAction.remove(this));\n  ///\n  /// // Adds and remove a wait state, using this as the flag, and 123 as a reference.\n  /// dispatch(WaitAction.add(this, ref: 123));\n  /// dispatch(WaitAction.remove(this, ref: 123));\n  /// ```\n  ///\n  /// If you pass a [delay], the flag will be removed only after that\n  /// duration has passed, after the [add] method is called. Example:\n  ///\n  /// ```\n  /// // Add a wait state that will be automatically removed after 3 seconds.\n  /// dispatch(WaitAction.add(this));\n  /// dispatch(WaitAction.remove(this, delay: Duration(seconds: 3)));\n  /// ```\n  ///\n  WaitAction.remove(\n    this.flag, {\n    this.ref,\n    this.delay,\n  }) : operation = WaitOperation.remove;\n\n  /// Clears (removes) the [flag], with all its references.\n  /// Removing the flag indicating some process finished running.\n  ///\n  /// ```\n  /// dispatch(WaitAction.add(this, flag: 123));\n  /// dispatch(WaitAction.add(this, flag: \"xyz\"));\n  /// dispatch(WaitAction.clear(this);\n  /// ```\n  WaitAction.clear([\n    this.flag,\n  ])  : operation = WaitOperation.clear,\n        delay = null,\n        ref = null;\n\n  @override\n  St? reduce() {\n    if (delay == null)\n      return reducer(state, operation, flag, ref);\n    else {\n      Future.delayed(delay!, () {\n        reducer(state, operation, flag, ref);\n      });\n      return null;\n    }\n  }\n\n  @override\n  String toString() => 'WaitAction.${operation.name}('\n      'flag: ${flag.toStringLimited()}, '\n      'ref: ${ref.toStringLimited()})';\n}\n\ntypedef WaitReducer<St> = St? Function(\n  St? state,\n  WaitOperation operation,\n  Object? flag,\n  Object? ref,\n);\n\nextension _StringExtension on Object? {\n  /// If the object can be represented with up to 50 chars, we print it.\n  /// Otherwise, we cut the text (using the Characters lib) and add an ellipsis.\n  String toStringLimited() {\n    String text = toString();\n    return (text.length <= 50) ? text : \"${Characters(text).take(49)}…\";\n  }\n}\n"
  },
  {
    "path": "lib/src/wrap_reduce.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// Based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan.\n// Uses code from package equatable by Felix Angelov.\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\n\n/// You may globally wrap the reducer to allow for some pre or post-processing.\n/// Note: if the action also have a [ReduxAction.wrapReduce] method, this global\n/// wrapper will be called AFTER (it will wrap the action's wrapper which wraps\n/// the action's reducer).\n///\n/// If [ifShouldProcess] is overridden to return `false`, the wrapper will\n/// be turned of.\n///\n/// The [process] method gets the old-state and the new-state, and returns\n/// the end state that you want to send to the store. Note: In sync reducers,\n/// the old-state is the state before the reducer is called. However, in\n/// async reducers, the old-state is the state AFTER the reducer returns\n/// but before the reducer's result is committed to the store.\n///\n/// For example, this wrapper checks if `newState.someInfo` is out of range,\n/// and if that's the case it's logged and changed to some valid value:\n///\n/// ```\n/// class MyWrapReduce extends WrapReduce<AppState> {\n///   St process({required St oldState, required St newState}) {\n///     if (identical(newState.someInfo, oldState.someInfo) || oldState.someInfo.isWithRange())\n///     return newState;\n///     else {\n///       Logger.log('Invalid value: ${oldState.someInfo}');\n///       return newState.copy(someInfo: newState.someInfo.copy(SomeInfo(validValue)));\n///       }}}\n/// ```\n///\n/// Note the [wrapReduce] method encapsulates the complexities of\n/// differentiating sync and async reducers. However, you can override it\n/// to provide your own implementation if necessary.\n///\nabstract class WrapReduce<St> {\n  //\n  bool ifShouldProcess() => true;\n\n  St process({\n    required St oldState,\n    required St newState,\n  });\n\n  Reducer<St> wrapReduce(\n    Reducer<St> reduce,\n    Store<St> store,\n  ) {\n    //\n    if (!ifShouldProcess())\n      return reduce;\n    //\n    // 1) Sync reducer.\n    else {\n      if (reduce is St? Function()) {\n        return () {\n          //\n          // The old-state right before calling the sync reducer.\n          St oldState = store.state;\n\n          // This is the state returned by the reducer.\n          St? newState = reduce();\n\n          // If the reducer returned null, or the same instance, do nothing.\n          if (newState == null || identical(store.state, newState))\n            return newState;\n\n          return process(oldState: oldState, newState: newState);\n        };\n      }\n      //\n      // 2) Async reducer.\n      else if (reduce is Future<St?> Function()) {\n        return () async {\n          //\n          // The is the state returned by the reducer.\n          St? newState = await reduce();\n\n          // This is the state right after the reducer returns,\n          // but before it's committed to the store.\n          St oldState = store.state;\n\n          // If the reducer returned null, or returned the same instance, don't do anything.\n          if (newState == null || identical(store.state, newState))\n            return newState;\n\n          return process(oldState: oldState, newState: newState);\n        };\n      }\n      // Not defined.\n      else {\n        throw StoreException(\"Reducer should return `St?` or `Future<St?>`. \"\n            \"Do not return `FutureOr<St?>`. \"\n            \"Reduce is of type: '${reduce.runtimeType}'.\");\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "mixin_compatibility.md",
    "content": "# Mixin Compatibility Matrix\n\nThis document describes the compatibility between AsyncRedux action mixins.\n\n## Mixins Overview\n\n| Mixin                         | Purpose                                                                   | Overrides                     |\n|-------------------------------|---------------------------------------------------------------------------|-------------------------------|\n| `CheckInternet`               | Checks internet before action; shows dialog if no connection              | `before`                      |\n| `NoDialog`                    | Modifier for `CheckInternet` to suppress dialog                           | (requires `CheckInternet`)    |\n| `AbortWhenNoInternet`         | Checks internet before action; aborts silently if no connection           | `before`                      |\n| `NonReentrant`                | Aborts if the same action is already running                              | `abortDispatch`               |\n| `Retry`                       | Retries the action on error with exponential backoff                      | `wrapReduce`                  |\n| `UnlimitedRetries`            | Modifier for `Retry` to retry indefinitely                                | (requires `Retry`)            |\n| `OptimisticCommand`           | Applies state changes optimistically, rolls back on error                 | `reduce`                      |\n| `OptimisticSync`              | Optimistic updates with coalescing; merges rapid dispatches into one sync | `reduce`                      |\n| `OptimisticSyncWithPush`      | Like `OptimisticSync` but with revision tracking for server pushes        | `reduce`                      |\n| `ServerPush`                  | Handles server-pushed updates for `OptimisticSyncWithPush`                | `reduce`                      |\n| `Throttle`                    | Limits action execution to at most once per throttle period               | `abortDispatch`, `after`      |\n| `Debounce`                    | Delays execution until after a period of inactivity                       | `wrapReduce`                  |\n| `UnlimitedRetryCheckInternet` | Combines internet check + unlimited retry + non-reentrant                 | `abortDispatch`, `wrapReduce` |\n| `Fresh`                       | Skips action if data is still fresh (not stale)                           | `abortDispatch`, `after`      |\n| `Polling`                     | Adds periodic polling to any action                                       | `wrapReduce`                  |\n\n## Compatibility Matrix\n\n|                                 | CheckInternet | NoDialog | AbortWhenNoInternet | NonReentrant | Retry | UnlimitedRetries | UnlimitedRetryCheckInternet | Throttle | Debounce | Fresh | OptimisticCommand | OptimisticSync | OptimisticSyncWithPush | ServerPush | Polling |\n|---------------------------------|:-------------:|:--------:|:-------------------:|:------------:|:-----:|:----------------:|:---------------------------:|:--------:|:--------:|:-----:|:-----------------:|:--------------:|:----------------------:|:----------:|:-------:|\n| **CheckInternet**               |       —       |    ✅     |          ❌          |      ✅       |  ✅️   |        ✅️        |              ❌              |    ✅     |    ✅     |   ✅   |         ✅         |       ✅        |           ✅            |     ❌      |    ✅    |\n| **NoDialog**                    |      ➡️       |    —     |          ❌          |      ✅       |  ✅️   |        ✅️        |              ❌              |    ✅     |    ✅     |   ✅   |         ✅         |       ✅        |           ✅            |     ❌      |    ✅    |\n| **AbortWhenNoInternet**         |       ❌       |    ❌     |          —          |      ✅       |  ✅️   |        ✅️        |              ❌              |    ✅     |    ✅     |   ✅   |         ✅         |       ✅        |           ✅            |     ❌      |    ✅    |\n| **NonReentrant**                |       ✅       |    ✅     |          ✅          |      —       |   ✅   |        ✅         |              ❌              |    ❌     |    ✅     |   ❌   |         ❌         |       ❌        |           ❌            |     ❌      |    ✅    |\n| **Retry**                       |      ✅️       |    ✅️    |         ✅️          |      ✅       |   —   |        ✅         |              ❌              |    ✅     |    ❌     |   ✅   |         ✅         |       ❌        |           ❌            |     ❌      |    ❌    |\n| **UnlimitedRetries**            |      ✅️       |    ✅️    |         ✅️          |      ✅       |  ➡️   |        —         |              ❌              |    ✅     |    ❌     |   ✅   |         ❌         |       ❌        |           ❌            |     ❌      |    ❌    |\n| **UnlimitedRetryCheckInternet** |       ❌       |    ❌     |          ❌          |      ❌       |   ❌   |        ❌         |              —              |    ❌     |    ❌     |   ❌   |         ❌         |       ❌        |           ❌            |     ❌      |    ❌    |\n| **Throttle**                    |       ✅       |    ✅     |          ✅          |      ❌       |   ✅   |        ✅         |              ❌              |    —     |    ✅     |   ❌   |         ❌         |       ❌        |           ❌            |     ❌      |    ✅    |\n| **Debounce**                    |       ✅       |    ✅     |          ✅          |      ✅       |   ❌   |        ❌         |              ❌              |    ✅     |    —     |   ✅   |         ❌         |       ❌        |           ❌            |     ❌      |    ❌    |\n| **Fresh**                       |       ✅       |    ✅     |          ✅          |      ❌       |   ✅   |        ✅         |              ❌              |    ❌     |    ✅     |   —   |         ❌         |       ❌        |           ❌            |     ❌      |    ✅    |\n| **OptimisticCommand**           |       ✅       |    ✅     |          ✅          |      ❌       |   ✅   |        ❌         |              ❌              |    ❌     |    ❌     |   ❌   |         —         |       ❌        |           ❌            |     ❌      |    ❌    |\n| **OptimisticSync**              |       ✅       |    ✅     |          ✅          |      ❌       |   ❌   |        ❌         |              ❌              |    ❌     |    ❌     |   ❌   |         ❌         |       —        |           ❌            |     ❌      |    ❌    |\n| **OptimisticSyncWithPush**      |       ✅       |    ✅     |          ✅          |      ❌       |   ❌   |        ❌         |              ❌              |    ❌     |    ❌     |   ❌   |         ❌         |       ❌        |           —            |     ❌      |    ❌    |\n| **ServerPush**                  |       ❌       |    ❌     |          ❌          |      ❌       |   ❌   |        ❌         |              ❌              |    ❌     |    ❌     |   ❌   |         ❌         |       ❌        |           ❌            |     —      |    ❌    |\n| **Polling**                     |       ✅       |    ✅     |          ✅          |      ✅       |   ❌   |        ❌         |              ❌              |    ✅     |    ❌     |   ✅   |         ❌         |       ❌        |           ❌            |     ❌      |    —    |\n\n- ✅ = Compatible (can be combined)\n- ❌ = Incompatible (cannot be combined)\n- ➡️ = Requires (must be used together)\n\n## Incompatibility Groups\n\n### Group 1: Internet Checking Mixins\n\nThese mixins all check internet connectivity and cannot be combined with each\nother:\n\n- `CheckInternet`\n- `AbortWhenNoInternet`\n- `UnlimitedRetryCheckInternet`\n\n### Group 2: abortDispatch Mixins\n\nThese mixins override `abortDispatch` and cannot be combined with each other:\n\n- `NonReentrant`\n- `Throttle`\n- `UnlimitedRetryCheckInternet`\n- `Fresh`\n\n### Group 3: wrapReduce Mixins\n\nThese mixins override `wrapReduce` and cannot be combined with each other:\n\n- `Retry` / `UnlimitedRetries`\n- `Debounce`\n- `UnlimitedRetryCheckInternet`\n- `Polling`\n\n### Group 4: Optimistic Update Mixins\n\nThese mixins handle optimistic state updates and cannot be combined with each\nother:\n\n- `OptimisticCommand`\n- `OptimisticSync`\n- `OptimisticSyncWithPush`\n- `ServerPush` (used alongside `OptimisticSyncWithPush`, but not combined with it in the\n  same action)\n\n## Notes\n\n### CheckInternet / AbortWhenNoInternet + Retry\n\nCombining `Retry` with `CheckInternet` or `AbortWhenNoInternet`\nwill not retry when there is no internet. It will only retry if there **is**\ninternet but the action fails for some other reason. To retry indefinitely until\ninternet is available, use `UnlimitedRetryCheckInternet` instead.\n\n### NoDialog\n\n`NoDialog` is a modifier mixin that **requires** `CheckInternet`. It cannot be\nused alone:\n\n```dart\nclass MyAction extends ReduxAction<AppState> with CheckInternet, NoDialog { ... }\n```\n\n### UnlimitedRetries\n\n`UnlimitedRetries` is a modifier mixin that **requires** `Retry`. It cannot be\nused alone:\n\n```dart\nclass MyAction extends ReduxAction<AppState> with Retry, UnlimitedRetries { ... }\n```\n\n### Recommended Combinations\n\n- `Retry` + `NonReentrant`: Recommended to avoid multiple instances running\n  simultaneously.\n- `CheckInternet` + `NonReentrant`: Safe combination for internet-dependent actions.\n- `CheckInternet` + `Throttle`: Safe combination (but not with `NonReentrant` at the same\n  time)\n- `AbortWhenNoInternet` + `NonReentrant`: Safe combination.\n- `AbortWhenNoInternet` + `Throttle`: Safe combination (but not with `NonReentrant` at the\n  same time)\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: async_redux\ndescription: The modern version of Redux. State management that's simple to learn and easy to use; Powerful enough to handle complex applications with millions of users; Testable.\nversion: 28.0.0-dev.3\n# author: Marcelo Glasberg <marcglasberg@gmail.com>\nrepository: https://github.com/marcglasberg/async_redux\nissue_tracker: https://github.com/marcglasberg/async_redux/issues\nhomepage: https://asyncredux.com\ndocumentation: https://asyncredux.com\ntopics:\n  - redux\n  - state-management\n  - ui\n  - reactive-programming\n  - testing\n\nenvironment:\n  sdk: '>=3.5.0 <4.0.0'\n  flutter: \">=3.16.0\"\n\ndependencies:\n  async_redux_core: ^1.4.1\n  fast_immutable_collections: ^11.1.0\n  weak_map: ^4.0.1\n  connectivity_plus: \">=6.0.3 <8.0.0\"\n  collection: ^1.18.0\n  logging: ^1.3.0\n  path: ^1.9.1\n  file: ^7.0.0\n  path_provider: ^2.1.5\n  meta: ^1.11.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  fake_async: ^1.3.3\n  bdd_framework: ^4.0.6\n"
  },
  {
    "path": "test/abort_dispatch_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nList<String>? info;\n\nvoid main() {\n  var feature = BddFeature('Abort dispatch of actions');\n\n  test('Test aborting an action.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    store.dispatch(ActionA(abort: false));\n    expect(store.state, \"X\");\n    expect(info, ['1', '2', '3']);\n\n    store.dispatch(ActionA(abort: false));\n    expect(store.state, \"XX\");\n    expect(info, ['1', '2', '3', '1', '2', '3']);\n\n    // Won't dispatch, because abortDispatch checks the abort flag.\n    store.dispatch(ActionA(abort: true));\n    expect(store.state, \"XX\");\n    expect(info, ['1', '2', '3', '1', '2', '3']);\n  });\n\n  test('Test aborting an action, where the abortDispatch method accesses the state.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    store.dispatch(ActionB());\n    expect(store.state, \"X\");\n    expect(info, ['1', '2', '3']);\n\n    store.dispatch(ActionB());\n    expect(store.state, \"XX\");\n    expect(info, ['1', '2', '3', '1', '2', '3']);\n\n    // Won't dispatch, because abortDispatch checks that the state has length 2.\n    store.dispatch(ActionB());\n    expect(store.state, \"XX\");\n    expect(info, ['1', '2', '3', '1', '2', '3']);\n  });\n\n  Bdd(feature)\n      .scenario('The action can abort its own dispatch.')\n      .given('An action that returns true (or false) in its abortDispatch method.')\n      .when('The action is dispatched (with dispatch, or dispatchSync, or dispatchAndWait).')\n      .then('It is aborted (or is not aborted, respectively).')\n      .note(\n          'We have to test dispatch/dispatchSync/dispatchAndWait separately, because they abort in different ways.')\n      .run((_) async {\n    // Dispatch\n    var store = Store<State>(initialState: State(1));\n\n    // Doesn't abort, so it increments.\n    store.dispatch(Increment(false));\n    expect(store.state.count, 2);\n\n    // Aborts, so it doesn't change.\n    store.dispatch(Increment(true));\n    expect(store.state.count, 2);\n\n    // Doesn't abort, so it increments again.\n    store.dispatch(Increment(false));\n    expect(store.state.count, 3);\n\n    // DispatchSync\n    store = Store<State>(initialState: State(1));\n\n    // Doesn't abort, so it increments.\n    store.dispatchSync(Increment(false));\n    expect(store.state.count, 2);\n\n    // Aborts, so it doesn't change.\n    store.dispatchSync(Increment(true));\n    expect(store.state.count, 2);\n\n    // Doesn't abort, so it increments again.\n    store.dispatchSync(Increment(false));\n    expect(store.state.count, 3);\n\n    // DispatchAndWait\n    store = Store<State>(initialState: State(1));\n\n    // Doesn't abort, so it increments.\n    await store.dispatchAndWait(Increment(false));\n    expect(store.state.count, 2);\n\n    // Aborts, so it doesn't change.\n    await store.dispatchAndWait(Increment(true));\n    expect(store.state.count, 2);\n\n    // Doesn't abort, so it increments again.\n    await store.dispatchAndWait(Increment(false));\n    expect(store.state.count, 3);\n  });\n}\n\nclass ActionA extends ReduxAction<String> {\n  bool abort;\n\n  ActionA({required this.abort});\n\n  @override\n  bool abortDispatch() => abort;\n\n  @override\n  void before() {\n    info!.add('1');\n  }\n\n  @override\n  String reduce() {\n    info!.add('2');\n    return state + 'X';\n  }\n\n  @override\n  void after() {\n    info!.add('3');\n  }\n}\n\nclass ActionB extends ReduxAction<String> {\n  @override\n  bool abortDispatch() => state.length >= 2;\n\n  @override\n  void before() {\n    info!.add('1');\n  }\n\n  @override\n  String reduce() {\n    info!.add('2');\n    return state + 'X';\n  }\n\n  @override\n  void after() {\n    info!.add('3');\n  }\n}\n\nclass State {\n  final int count;\n\n  State(this.count);\n\n  @override\n  String toString() => 'State($count)';\n}\n\nclass Increment extends ReduxAction<State> {\n  final bool ifAbort;\n\n  Increment(this.ifAbort);\n\n  @override\n  bool abortDispatch() => ifAbort;\n\n  @override\n  State reduce() => State(state.count + 1);\n}\n"
  },
  {
    "path": "test/action_initial_state_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass State {\n  final int count;\n\n  State(this.count);\n\n  @override\n  String toString() => 'State($count)';\n}\n\nclass ChangeAction extends ReduxAction<State> {\n  final int newValue;\n\n  ChangeAction(this.newValue);\n\n  @override\n  State reduce() => State(newValue);\n}\n\nclass IncrementSync extends ReduxAction<State> {\n  String result = '';\n\n  @override\n  void before() {\n    result += 'before initialState: $initialState|';\n    result += 'before state: $state|';\n    dispatch(ChangeAction(42));\n    result += 'before initialState: $initialState|';\n    result += 'before state: $state|';\n  }\n\n  @override\n  State reduce() {\n    result += 'reduce initialState: $initialState|';\n    result += 'reduce state: $state|';\n    dispatch(ChangeAction(100));\n    result += 'reduce initialState: $initialState|';\n    result += 'reduce state: $state|';\n    return State(state.count + 1);\n  }\n\n  @override\n  void after() {\n    result += 'after initialState: $initialState|';\n    result += 'after state: $state|';\n    dispatch(ChangeAction(1350));\n    result += 'after initialState: $initialState|';\n    result += 'after state: $state|';\n  }\n}\n\nclass IncrementAsync extends ReduxAction<State> {\n  String result = '';\n\n  @override\n  Future<void> before() async {\n    result += 'before initialState: $initialState|';\n    result += 'before state: $state|';\n    dispatch(ChangeAction(42));\n    result += 'before initialState: $initialState|';\n    result += 'before state: $state|';\n  }\n\n  @override\n  Future<State> reduce() async {\n    result += 'reduce initialState: $initialState|';\n    result += 'reduce state: $state|';\n    dispatch(ChangeAction(100));\n    result += 'reduce initialState: $initialState|';\n    result += 'reduce state: $state|';\n    return State(state.count + 1);\n  }\n\n  @override\n  void after() {\n    result += 'after initialState: $initialState|';\n    result += 'after state: $state|';\n    dispatch(ChangeAction(1350));\n    result += 'after initialState: $initialState|';\n    result += 'after state: $state|';\n  }\n}\n\nvoid main() {\n  var feature = BddFeature('Action initial state');\n\n  Bdd(feature)\n      .scenario('The action has access to its initial state.')\n      .given('SYNC and ASYNC actions.')\n      .when('The \"before\" and \"reduce\" and \"after\" methods are called.')\n      .then('They have access to the store state as it was when the action was dispatched.')\n      .note('The action initial state has nothing to do with the store initial state.')\n      .run((_) async {\n    // SYNC\n    var store = Store<State>(initialState: State(1));\n\n    var actionSync = IncrementSync();\n    store.dispatch(actionSync);\n\n    expect(\n        actionSync.result,\n        'before initialState: State(1)|'\n        'before state: State(1)|'\n        'before initialState: State(1)|'\n        'before state: State(42)|'\n        'reduce initialState: State(1)|'\n        'reduce state: State(42)|'\n        'reduce initialState: State(1)|'\n        'reduce state: State(100)|'\n        'after initialState: State(1)|'\n        'after state: State(101)|'\n        'after initialState: State(1)|'\n        'after state: State(1350)|');\n\n    // ASYNC\n    store = Store<State>(initialState: State(1));\n\n    var actionAsync = IncrementAsync();\n    await store.dispatchAndWait(actionAsync);\n\n    expect(\n        actionAsync.result,\n        'before initialState: State(1)|'\n        'before state: State(1)|'\n        'before initialState: State(1)|'\n        'before state: State(42)|'\n        'reduce initialState: State(1)|'\n        'reduce state: State(42)|'\n        'reduce initialState: State(1)|'\n        'reduce state: State(100)|'\n        'after initialState: State(1)|'\n        'after state: State(101)|'\n        'after initialState: State(1)|'\n        'after state: State(1350)|');\n  });\n}\n"
  },
  {
    "path": "test/action_status_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate List<String> info;\n\nenum When { before, reduce, after }\n\n/// IMPORTANT:\n/// These tests may print errors to the console. This is normal.\n///\nvoid main() {\n  test('Test detecting that the BEFORE method of an action threw an error.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    var actionA = MyAction(whenToThrow: When.before);\n    store.dispatch(actionA);\n\n    expect(actionA.status.hasFinishedMethodBefore, false);\n    expect(actionA.status.hasFinishedMethodReduce, false);\n    expect(actionA.status.hasFinishedMethodAfter, true);\n    expect(actionA.status.isCompleted, true);\n    expect(actionA.status.isCompletedOk, false);\n    expect(actionA.status.isCompletedFailed, true);\n    expect(actionA.status.originalError, const UserException('During before'));\n    expect(actionA.status.wrappedError, const UserException('During before'));\n  });\n\n  test('Test detecting that the REDUCE method of an action threw an error.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    var actionA = MyAction(whenToThrow: When.reduce);\n    store.dispatch(actionA);\n\n    expect(actionA.status.hasFinishedMethodBefore, true);\n    expect(actionA.status.hasFinishedMethodReduce, false);\n    expect(actionA.status.hasFinishedMethodAfter, true);\n    expect(actionA.status.isCompleted, true);\n    expect(actionA.status.isCompletedOk, false);\n    expect(actionA.status.isCompletedFailed, true);\n    expect(actionA.status.originalError, const UserException('During reduce'));\n    expect(actionA.status.wrappedError, const UserException('During reduce'));\n  });\n\n  test('Test wrapping the error in the action.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    var actionA = MyActionWithWrapError(whenToThrow: When.reduce);\n    try {\n      store.dispatch(actionA);\n    } catch (e) {\n      // This is expected.\n    }\n\n    expect(actionA.status.hasFinishedMethodBefore, true);\n    expect(actionA.status.hasFinishedMethodReduce, false);\n    expect(actionA.status.hasFinishedMethodAfter, true);\n    expect(actionA.status.isCompleted, true);\n    expect(actionA.status.isCompletedOk, false);\n    expect(actionA.status.isCompletedFailed, true);\n    expect(actionA.status.originalError, const UserException('During reduce'));\n    expect(actionA.status.wrappedError, 'wrapped error in action: UserException{During reduce}');\n  });\n\n  test('Test wrapping the error globally with the globalWrapError (Store constructor).', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(\n      initialState: \"\",\n      globalWrapError: MyGlobalWrapError<String>(),\n    );\n\n    var actionA = MyAction(whenToThrow: When.reduce);\n    try {\n      store.dispatch(actionA);\n    } catch (e) {\n      // This is expected.\n    }\n\n    expect(actionA.status.hasFinishedMethodBefore, true);\n    expect(actionA.status.hasFinishedMethodReduce, false);\n    expect(actionA.status.hasFinishedMethodAfter, true);\n    expect(actionA.status.isCompleted, true);\n    expect(actionA.status.isCompletedOk, false);\n    expect(actionA.status.isCompletedFailed, true);\n    expect(actionA.status.originalError, const UserException('During reduce'));\n    expect(actionA.status.wrappedError, 'global wrapped error: UserException{During reduce}');\n  });\n\n  test(\n      \"Test detecting that the AFTER method of an action threw an error. \"\n      \"An AFTER method shouldn't throw. But if it does, the error will be \"\n      \"thrown asynchronously (after the async gap).\", () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    var hasThrown = false;\n    runZonedGuarded(() {\n      var actionA = MyAction(whenToThrow: When.after);\n      store.dispatch(actionA);\n\n      expect(actionA.status.hasFinishedMethodBefore, true);\n      expect(actionA.status.hasFinishedMethodReduce, true);\n      expect(actionA.status.hasFinishedMethodAfter, true);\n      expect(actionA.status.isCompleted, true);\n      expect(actionA.status.isCompletedOk, true);\n      expect(actionA.status.isCompletedFailed, false);\n      expect(actionA.status.originalError, isNull);\n      expect(actionA.status.wrappedError, isNull);\n    }, (error, stackTrace) {\n      hasThrown = true;\n\n      expect(\n          error,\n          \"Method 'MyAction.after()' has thrown an error:\\n\"\n          \" 'UserException{During after}'.:\\n\"\n          \"  UserException{During after}\");\n    });\n\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(hasThrown, isTrue);\n  });\n\n  test('Test detecting that the action threw no errors.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    var actionA = MyAction(whenToThrow: null);\n    store.dispatch(actionA);\n\n    expect(actionA.status.hasFinishedMethodBefore, true);\n    expect(actionA.status.hasFinishedMethodReduce, true);\n    expect(actionA.status.hasFinishedMethodAfter, true);\n    expect(actionA.status.isCompleted, true);\n    expect(actionA.status.isCompletedOk, true);\n    expect(actionA.status.isCompletedFailed, false);\n    expect(actionA.status.originalError, isNull);\n    expect(actionA.status.wrappedError, isNull);\n  });\n\n  test('The status.context contains the action and store after a successful dispatch.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    var actionA = MyAction(whenToThrow: null);\n    store.dispatch(actionA);\n\n    expect(actionA.status.context, isNotNull);\n    var (action, ctxStore) = actionA.status.context!;\n    expect(action, same(actionA));\n    expect(ctxStore, same(store));\n  });\n\n  test('The status.context contains the action and store when the action threw an error.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    var actionA = MyAction(whenToThrow: When.before);\n    store.dispatch(actionA);\n    expect(actionA.status.isCompletedFailed, true);\n\n    expect(actionA.status.context, isNotNull);\n    var (action1, store1) = actionA.status.context!;\n    expect(action1, same(actionA));\n    expect(store1, same(store));\n\n    var actionB = MyAction(whenToThrow: When.reduce);\n    store.dispatch(actionB);\n    expect(actionB.status.isCompletedFailed, true);\n\n    expect(actionB.status.context, isNotNull);\n    var (action2, store2) = actionB.status.context!;\n    expect(action2, same(actionB));\n    expect(store2, same(store));\n  });\n\n  test('The status.context contains the action and store when dispatch is aborted.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    var actionA = MyAbortAction();\n    ActionStatus returnedStatus = await store.dispatchAndWait(actionA);\n    expect(returnedStatus.isDispatchAborted, true);\n\n    expect(returnedStatus.context, isNotNull);\n    var (action, ctxStore) = returnedStatus.context!;\n    expect(action, same(actionA));\n    expect(ctxStore, same(store));\n  });\n\n  test('The status.context is null before the action is dispatched.', () async {\n    //\n    var actionA = MyAction(whenToThrow: null);\n    expect(actionA.status.context, isNull);\n  });\n}\n\nclass MyAction extends ReduxAction<String> {\n  When? whenToThrow;\n\n  MyAction({this.whenToThrow});\n\n  @override\n  void before() {\n    info.add('1');\n    if (whenToThrow == When.before) throw const UserException(\"During before\");\n  }\n\n  @override\n  String reduce() {\n    info.add('2');\n    if (whenToThrow == When.reduce) throw const UserException(\"During reduce\");\n    return state + 'X';\n  }\n\n  @override\n  void after() {\n    if (whenToThrow == When.after) throw const UserException(\"During after\");\n    info.add('3');\n  }\n}\n\nclass MyActionWithWrapError extends ReduxAction<String> {\n  When? whenToThrow;\n\n  MyActionWithWrapError({this.whenToThrow});\n\n  @override\n  void before() {\n    info.add('1');\n    if (whenToThrow == When.before) throw const UserException(\"During before\");\n  }\n\n  @override\n  String reduce() {\n    info.add('2');\n    if (whenToThrow == When.reduce) throw const UserException(\"During reduce\");\n    return state + 'X';\n  }\n\n  @override\n  void after() {\n    if (whenToThrow == When.after) throw const UserException(\"During after\");\n    info.add('3');\n  }\n\n  @override\n  Object? wrapError(Object error, StackTrace stackTrace) => 'wrapped error in action: $error';\n}\n\nclass MyAbortAction extends ReduxAction<String> {\n  @override\n  bool abortDispatch() => true;\n\n  @override\n  String reduce() => state;\n}\n\nclass MyGlobalWrapError<St> implements GlobalWrapError<St> {\n  @override\n  Object? wrap(error, stackTrace, action) => 'global wrapped error: $error';\n}\n"
  },
  {
    "path": "test/action_to_string_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nvoid main() {\n  test('NavigateAction toString() and type.', () async {\n    //\n    var route1 = MaterialPageRoute(builder: (BuildContext ctx) => Container());\n    var route2 = CupertinoPageRoute(builder: (BuildContext ctx) => Container());\n\n    // ---\n\n    var action = NavigateAction.push(route1);\n    expect(action.toString(),\n        'Action NavigateAction.push(MaterialPageRoute<dynamic>(RouteSettings(none, null), animation: null))');\n    expect(action.type, NavigateType.push);\n\n    // ---\n\n    action = NavigateAction.pop();\n    expect(action.toString(), 'Action NavigateAction.pop()');\n    expect(action.type, NavigateType.pop);\n\n    action = NavigateAction.pop(true);\n    expect(action.toString(), 'Action NavigateAction.pop(true)');\n    expect(action.type, NavigateType.pop);\n\n    // ---\n\n    action = NavigateAction.popAndPushNamed(\"routeName\");\n    expect(action.toString(), 'Action NavigateAction.popAndPushNamed(routeName)');\n    expect(action.type, NavigateType.popAndPushNamed);\n\n    action = NavigateAction.popAndPushNamed(\"routeName\", result: true);\n    expect(action.toString(), 'Action NavigateAction.popAndPushNamed(routeName, result: true)');\n    expect(action.type, NavigateType.popAndPushNamed);\n\n    // ---\n\n    action = NavigateAction.pushNamed(\"routeName\");\n    expect(action.toString(), 'Action NavigateAction.pushNamed(routeName)');\n    expect(action.type, NavigateType.pushNamed);\n\n    // ---\n\n    action = NavigateAction.pushReplacement(route1);\n    expect(\n        action.toString(),\n        'Action NavigateAction.pushReplacement(MaterialPageRoute<dynamic>('\n        'RouteSettings(none, null), animation: null)'\n        ')');\n    expect(action.type, NavigateType.pushReplacement);\n\n    action = NavigateAction.pushReplacement(route1, result: true);\n    expect(\n        action.toString(),\n        'Action NavigateAction.pushReplacement(MaterialPageRoute<dynamic>('\n        'RouteSettings(none, null), animation: null), result: true'\n        ')');\n    expect(action.type, NavigateType.pushReplacement);\n\n    // ---\n\n    action = NavigateAction.pushAndRemoveUntil(route1, (_) => true);\n    expect(\n        action.toString(),\n        'Action NavigateAction.pushAndRemoveUntil('\n        'MaterialPageRoute<dynamic>(RouteSettings(none, null), animation: null), predicate'\n        ')');\n    expect(action.type, NavigateType.pushAndRemoveUntil);\n\n    // ---\n\n    action = NavigateAction.replace(oldRoute: route1, newRoute: route2);\n    expect(\n        action.toString(),\n        'Action NavigateAction.replace('\n        'oldRoute: MaterialPageRoute<dynamic>(RouteSettings(none, null), animation: null), newRoute: CupertinoPageRoute<dynamic>(RouteSettings(none, null), animation: null)'\n        ')');\n    expect(action.type, NavigateType.replace);\n\n    action = NavigateAction.replace(oldRoute: null, newRoute: null);\n    expect(action.toString(), 'Action NavigateAction.replace(oldRoute: null, newRoute: null)');\n    expect(action.type, NavigateType.replace);\n\n    // ---\n\n    action = NavigateAction.replaceRouteBelow(anchorRoute: route1, newRoute: route2);\n    expect(\n        action.toString(),\n        'Action NavigateAction.replaceRouteBelow('\n        'anchorRoute: MaterialPageRoute<dynamic>(RouteSettings(none, null), animation: null), newRoute: CupertinoPageRoute<dynamic>(RouteSettings(none, null), animation: null)'\n        ')');\n    expect(action.type, NavigateType.replaceRouteBelow);\n\n    action = NavigateAction.replaceRouteBelow(anchorRoute: null, newRoute: null);\n    expect(action.toString(),\n        'Action NavigateAction.replaceRouteBelow(anchorRoute: null, newRoute: null)');\n    expect(action.type, NavigateType.replaceRouteBelow);\n\n    // ---\n\n    action = NavigateAction.pushReplacementNamed(\"routeName\");\n    expect(action.toString(), 'Action NavigateAction.pushReplacementNamed(routeName)');\n    expect(action.type, NavigateType.pushReplacementNamed);\n\n    // ---\n\n    action = NavigateAction.pushNamedAndRemoveUntil(\"routeName\", (_) => true);\n    expect(\n        action.toString(), 'Action NavigateAction.pushNamedAndRemoveUntil(routeName, predicate)');\n    expect(action.type, NavigateType.pushNamedAndRemoveUntil);\n\n    // ---\n\n    action = NavigateAction.pushNamedAndRemoveAll(\"routeName\");\n    expect(action.toString(), 'Action NavigateAction.pushNamedAndRemoveAll(routeName)');\n    expect(action.type, NavigateType.pushNamedAndRemoveAll);\n\n    // ---\n\n    action = NavigateAction.popUntil((_) => true);\n    expect(action.toString(), 'Action NavigateAction.popUntil(predicate)');\n    expect(action.type, NavigateType.popUntil);\n\n    // ---\n\n    action = NavigateAction.removeRoute(route1);\n    expect(\n        action.toString(),\n        'Action NavigateAction.removeRoute('\n        'MaterialPageRoute<dynamic>(RouteSettings(none, null), animation: null)'\n        ')');\n    expect(action.type, NavigateType.removeRoute);\n\n    // ---\n\n    action = NavigateAction.removeRouteBelow(route1);\n    expect(\n        action.toString(),\n        'Action NavigateAction.removeRouteBelow('\n        'MaterialPageRoute<dynamic>(RouteSettings(none, null), animation: null)'\n        ')');\n    expect(action.type, NavigateType.removeRouteBelow);\n\n    // ---\n\n    action = NavigateAction.popUntilRouteName(\"routeName\");\n    expect(action.toString(), 'Action NavigateAction.popUntilRouteName(routeName)');\n    expect(action.type, NavigateType.popUntilRouteName);\n\n    // ---\n\n    action = NavigateAction.popUntilRoute(route1);\n    expect(\n        action.toString(),\n        'Action NavigateAction.popUntilRoute('\n        'MaterialPageRoute<dynamic>(RouteSettings(none, null), animation: null)'\n        ')');\n    expect(action.type, NavigateType.popUntilRoute);\n  });\n}\n"
  },
  {
    "path": "test/action_wrap_reduce2_test.dart",
    "content": "import 'dart:async' show FutureOr;\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test('Knowing if wrapReduce is overridden, sync, or async', () async {\n    //\n    var xN = ActionNullableX();\n    var yN = ActionNullableY();\n    var x = ActionX();\n    var y = ActionY();\n    var z = ActionZ();\n\n    print(x.wrapReduce.runtimeType);\n    print(xN.wrapReduce.runtimeType);\n    print(y.wrapReduce.runtimeType);\n    print(yN.wrapReduce.runtimeType);\n    print(z.wrapReduce.runtimeType);\n\n    print('\\nActionX -> true / false / true');\n    print('ifWrapReduceOverridden ${x.ifWrapReduceOverridden()}');\n    print('ifWrapReduceSync ${x.ifWrapReduceOverridden_Sync()}');\n    print('ifWrapReduceAsync ${x.ifWrapReduceOverridden_Async()}');\n    expect(x.ifWrapReduceOverridden(), true);\n    expect(x.ifWrapReduceOverridden_Sync(), false);\n    expect(x.ifWrapReduceOverridden_Async(), true);\n\n    print('\\nActionNullableX -> true / false / true');\n    print('ifWrapReduceOverridden ${xN.ifWrapReduceOverridden()}');\n    print('ifWrapReduceSync ${xN.ifWrapReduceOverridden_Sync()}');\n    print('ifWrapReduceAsync ${xN.ifWrapReduceOverridden_Async()}');\n    expect(xN.ifWrapReduceOverridden(), true);\n    expect(xN.ifWrapReduceOverridden_Sync(), false);\n    expect(xN.ifWrapReduceOverridden_Async(), true);\n\n    print('\\nActionY -> true / true / false');\n    print('ifWrapReduceOverridden ${y.ifWrapReduceOverridden()}');\n    print('ifWrapReduceSync ${y.ifWrapReduceOverridden_Sync()}');\n    print('ifWrapReduceAsync ${y.ifWrapReduceOverridden_Async()}');\n    expect(y.ifWrapReduceOverridden(), true);\n    expect(y.ifWrapReduceOverridden_Sync(), true);\n    expect(y.ifWrapReduceOverridden_Async(), false);\n\n    print('\\nActionNullableY -> true / true / false');\n    print('ifWrapReduceOverridden ${yN.ifWrapReduceOverridden()}');\n    print('ifWrapReduceSync ${yN.ifWrapReduceOverridden_Sync()}');\n    print('ifWrapReduceAsync ${yN.ifWrapReduceOverridden_Async()}');\n    expect(yN.ifWrapReduceOverridden(), true);\n    expect(yN.ifWrapReduceOverridden_Sync(), true);\n    expect(yN.ifWrapReduceOverridden_Async(), false);\n\n    print('\\nActionZ => false false false');\n    print('ifWrapReduceOverridden ${z.ifWrapReduceOverridden()}');\n    print('ifWrapReduceSync ${z.ifWrapReduceOverridden_Sync()}');\n    print('ifWrapReduceAsync ${z.ifWrapReduceOverridden_Async()}');\n    expect(z.ifWrapReduceOverridden(), false);\n    expect(z.ifWrapReduceOverridden_Sync(), false);\n    expect(z.ifWrapReduceOverridden_Async(), false);\n  });\n}\n\nabstract class BaseAction<St> {\n  // static const _wrapReduceFlag = Object();\n\n  FutureOr<St?> reduce();\n\n  FutureOr<St?> wrapReduce(Reducer<St> reduce) {\n    return null;\n  }\n\n  bool ifWrapReduceOverridden_Sync() => wrapReduce is St? Function(Reducer<St>);\n\n  bool ifWrapReduceOverridden_Async() =>\n      wrapReduce is Future<St?> Function(Reducer<St>);\n\n  bool ifWrapReduceOverridden() =>\n      ifWrapReduceOverridden_Async() || ifWrapReduceOverridden_Sync();\n}\n\n/// SYNC: This action overrides the [wrapReduce] method.\nclass ActionNullableX extends BaseAction<int> {\n  @override\n  int? reduce() => 123;\n\n  @override\n  Future<int?> wrapReduce(Reducer<int> reduce) async {\n    print('overriden');\n    return null;\n  }\n}\n\n/// SYNC: This action overrides the [wrapReduce] method.\nclass ActionX extends BaseAction<int> {\n  @override\n  int? reduce() => 123;\n\n  @override\n  Future<int> wrapReduce(Reducer<int> reduce) async {\n    print('overriden');\n    return 0;\n  }\n}\n\n/// This action does NOT override the [wrapReduce] method.\nclass ActionNullableY extends BaseAction<int> {\n  @override\n  int reduce() => 456;\n\n  @override\n  int? wrapReduce(Reducer<int> reduce) {\n    print('overriden');\n    return null;\n  }\n}\n\n/// This action does NOT override the [wrapReduce] method.\nclass ActionY extends BaseAction<int> {\n  @override\n  int reduce() => 456;\n\n  @override\n  int wrapReduce(Reducer<int> reduce) {\n    print('overriden');\n    return 0;\n  }\n}\n\n/// This action does NOT override the [wrapReduce] method.\nclass ActionZ extends BaseAction<int> {\n  @override\n  FutureOr<int?> reduce() => 456;\n}\n"
  },
  {
    "path": "test/action_wrap_reduce_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test('action.isSync', () async {\n    expect(IncrementReduceSyncNoBeforeNoWrap().isSync(), isTrue);\n    expect(IncrementReduceAsyncNoBeforeNoWrap().isSync(), isFalse);\n\n    expect(IncrementReduceSyncBeforeSyncNoWrap().isSync(), isTrue);\n    expect(IncrementReduceSyncBeforeAsyncNoWrap().isSync(), isFalse);\n\n    expect(IncrementReduceSyncNoBeforeWrapSync().isSync(), isTrue);\n    expect(IncrementReduceSyncNoBeforeWrapSync2().isSync(), isTrue);\n    expect(IncrementReduceSyncNoBeforeWrapSync3().isSync(), isTrue);\n\n    expect(IncrementReduceSyncNoBeforeWrapAsync().isSync(), isFalse);\n    expect(IncrementReduceSyncNoBeforeWrapAsync2().isSync(), isFalse);\n    expect(IncrementReduceSyncNoBeforeWrapAsync3().isSync(), isFalse);\n  });\n}\n\nclass State {\n  final int count;\n\n  State(this.count);\n}\n\nclass IncrementReduceSyncNoBeforeNoWrap extends ReduxAction<State> {\n  @override\n  State reduce() => State(state.count + 1);\n}\n\nclass IncrementReduceAsyncNoBeforeNoWrap extends ReduxAction<State> {\n  @override\n  Future<State> reduce() async {\n    return State(state.count + 1);\n  }\n}\n\nclass IncrementReduceSyncBeforeSyncNoWrap extends ReduxAction<State> {\n  @override\n  void before() {}\n\n  @override\n  State reduce() => State(state.count + 1);\n}\n\nclass IncrementReduceSyncBeforeAsyncNoWrap extends ReduxAction<State> {\n  @override\n  Future<void> before() async {\n    await Future.delayed(const Duration(milliseconds: 1));\n  }\n\n  @override\n  State reduce() => State(state.count + 1);\n}\n\nclass IncrementReduceSyncNoBeforeWrapSync extends ReduxAction<State> {\n  @override\n  State reduce() => State(state.count + 1);\n\n  @override\n  State? wrapReduce(Reducer<State> reduce) {\n    return reduce() as State?;\n  }\n}\n\nclass IncrementReduceSyncNoBeforeWrapSync2 extends ReduxAction<State> {\n  @override\n  State reduce() => State(state.count + 1);\n\n  @override\n  State wrapReduce(Reducer<State> reduce) {\n    return reduce() as State;\n  }\n}\n\nclass IncrementReduceSyncNoBeforeWrapSync3 extends ReduxAction<State> {\n  @override\n  State reduce() => State(state.count + 1);\n\n  @override\n  State wrapReduce(Reducer<State> reduce) {\n    return reduce() as State;\n  }\n}\n\nclass IncrementReduceSyncNoBeforeWrapAsync extends ReduxAction<State> {\n  @override\n  State reduce() => State(state.count + 1);\n\n  @override\n  Future<State?> wrapReduce(Reducer<State> reduce) async {\n    await microtask;\n    return reduce();\n  }\n}\n\nclass IncrementReduceSyncNoBeforeWrapAsync2 extends ReduxAction<State> {\n  @override\n  State reduce() => State(state.count + 1);\n\n  @override\n  Future<State> wrapReduce(Reducer<State> reduce) async {\n    return reduce() as Future<State>;\n  }\n}\n\nclass IncrementReduceSyncNoBeforeWrapAsync3 extends ReduxAction<State> {\n  @override\n  State reduce() => State(state.count + 1);\n\n  @override\n  Future<State> wrapReduce(Reducer<State> reduce) async {\n    return reduce() as Future<State>;\n  }\n}\n"
  },
  {
    "path": "test/after_throws_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate List<String> info;\n\n/// IMPORTANT:\n/// These tests may print errors to the console. This is normal.\n///\nvoid main() {\n  test('If the after method throws, the error will be thrown asynchronously.', () async {\n    //\n    dynamic error;\n    dynamic asyncError;\n    late Store<String> store;\n\n    await runZonedGuarded(() async {\n      info = [];\n      store = Store<String>(initialState: \"\");\n\n      try {\n        store.dispatch(ActionA());\n      } catch (_error) {\n        error = _error;\n      }\n      await Future.delayed(const Duration(seconds: 1));\n    }, (_asyncError, s) {\n      asyncError = _asyncError;\n    });\n\n    expect(store.state, \"A\");\n\n    expect(info, [\n      'A.before state=\"\"',\n      'A.reduce state=\"\"',\n      'A.after state=\"A\"',\n    ]);\n\n    expect(error, isNull);\n\n    expect(\n        asyncError,\n        \"Method 'ActionA.after()' has thrown an error:\\n\"\n        \" 'some-error'.:\\n\"\n        \"  some-error\");\n  });\n}\n\nclass ActionA extends ReduxAction<String> {\n  @override\n  void before() {\n    info.add('A.before state=\"$state\"');\n  }\n\n  @override\n  String reduce() {\n    info.add('A.reduce state=\"$state\"');\n    return state + 'A';\n  }\n\n  @override\n  void after() {\n    info.add('A.after state=\"$state\"');\n    throw \"some-error\";\n  }\n}\n"
  },
  {
    "path": "test/before_reduce_after_order_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nlate List<String> info;\n\nvoid main() {\n  //\n  test('Method call sequence for sync reducer.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n    store.dispatch(ActionA());\n    expect(store.state, \"A\");\n    expect(info, [\n      'A.before state=\"\"',\n      'A.reduce state=\"\"',\n      'A.after state=\"A\"',\n    ]);\n  });\n\n  test(\n      'Method call sequence for async reducer. '\n      'The reducer is async because the method returns Future.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n    await store.dispatch(ActionB());\n    expect(store.state, \"B\");\n    expect(info, [\n      'B.before state=\"\"',\n      'B.reduce1 state=\"\"',\n      'B.reduce2 state=\"\"',\n      'B.after state=\"B\"',\n    ]);\n  });\n\n  test(\n      'Method call sequence for async reducer. '\n      'The reducer is async because the REDUCE method returns Future.',\n      () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    // B is dispatched first, but will finish last, because it's async.\n    var f1 = store.dispatchAndWait(ActionB());\n    var f2 = store.dispatchAndWait(ActionA());\n\n    await Future.wait([f1, f2]);\n    expect(store.state, \"AB\");\n    expect(info, [\n      'B.before state=\"\"',\n      'B.reduce1 state=\"\"',\n      'A.before state=\"\"',\n      'A.reduce state=\"\"',\n      'A.after state=\"A\"',\n      'B.reduce2 state=\"A\"',\n      'B.after state=\"AB\"'\n    ]);\n  });\n\n  test(\n      'Method call sequence for async reducer. '\n      'The reducer is async because the BEFORE method returns Future.',\n      () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    // C is dispatched first, but will finish last, because it's async.\n    var f1 = store.dispatchAndWait(ActionC());\n    var f2 = store.dispatchAndWait(ActionA());\n\n    await Future.wait([f1, f2]);\n    expect(store.state, \"AC\");\n    expect(info, [\n      'C.before state=\"\"',\n      'A.before state=\"\"',\n      'A.reduce state=\"\"',\n      'A.after state=\"A\"',\n      'C.reduce state=\"A\"',\n      'C.after state=\"AC\"'\n    ]);\n  });\n\n  test(\n      'Method call sequence for async reducer. '\n      'The reducer is async because the BEFORE method returns Future.'\n      'Shows what happens if the before method actually awaits.', () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n\n    // D is dispatched first, but will finish last, because it's async.\n    var f1 = store.dispatchAndWait(ActionD());\n    var f2 = store.dispatchAndWait(ActionA());\n\n    await Future.wait([f1, f2]);\n    expect(store.state, \"AD\");\n    expect(info, [\n      'D.before1 state=\"\"',\n      'A.before state=\"\"',\n      'A.reduce state=\"\"',\n      'A.after state=\"A\"',\n      'D.before2 state=\"A\"',\n      'D.reduce state=\"A\"',\n      'D.after state=\"AD\"'\n    ]);\n  });\n\n  test(\n      'What happens when the after method of a sync reducer dispatches another action? '\n      'The state is changed by the reduce method before the after method is executed.',\n      () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n    await store.dispatch(ActionE());\n\n    //\n    expect(store.state, \"EA\");\n    expect(info, [\n      'E.before state=\"\"',\n      'E.reduce state=\"\"',\n      'E.after1 state=\"E\"',\n      'A.before state=\"E\"',\n      'A.reduce state=\"E\"',\n      'A.after state=\"EA\"',\n      'E.after2 state=\"EA\"'\n    ]);\n  });\n\n  test(\n      'What happens when the after method of a async reducer dispatches another action? '\n      'The state is changed by the reduce method before the after method is executed.',\n      () async {\n    //\n    info = [];\n    Store<String> store = Store<String>(initialState: \"\");\n    await store.dispatch(ActionF());\n\n    //\n    expect(store.state, \"FA\");\n    expect(info, [\n      'F.before state=\"\"',\n      'F.reduce1 state=\"\"',\n      'F.reduce2 state=\"\"',\n      'F.after1 state=\"F\"',\n      'A.before state=\"F\"',\n      'A.reduce state=\"F\"',\n      'A.after state=\"FA\"',\n      'F.after2 state=\"FA\"'\n    ]);\n  });\n}\n\nclass ActionA extends ReduxAction<String> {\n  @override\n  void before() {\n    info.add('A.before state=\"$state\"');\n  }\n\n  @override\n  String reduce() {\n    info.add('A.reduce state=\"$state\"');\n    return state + 'A';\n  }\n\n  @override\n  void after() {\n    info.add('A.after state=\"$state\"');\n  }\n}\n\nclass ActionB extends ReduxAction<String> {\n  @override\n  void before() {\n    info.add('B.before state=\"$state\"');\n  }\n\n  @override\n  Future<String> reduce() async {\n    info.add('B.reduce1 state=\"$state\"');\n    await Future.delayed(const Duration(milliseconds: 50));\n    info.add('B.reduce2 state=\"$state\"');\n    return state + 'B';\n  }\n\n  @override\n  void after() {\n    info.add('B.after state=\"$state\"');\n  }\n}\n\nclass ActionC extends ReduxAction<String> {\n  @override\n  Future<void> before() async {\n    info.add('C.before state=\"$state\"');\n  }\n\n  @override\n  String reduce() {\n    info.add('C.reduce state=\"$state\"');\n    return state + 'C';\n  }\n\n  @override\n  void after() {\n    info.add('C.after state=\"$state\"');\n  }\n}\n\nclass ActionD extends ReduxAction<String> {\n  @override\n  Future<void> before() async {\n    info.add('D.before1 state=\"$state\"');\n    await Future.delayed(const Duration(milliseconds: 10));\n    info.add('D.before2 state=\"$state\"');\n  }\n\n  @override\n  String reduce() {\n    info.add('D.reduce state=\"$state\"');\n    return state + 'D';\n  }\n\n  @override\n  void after() {\n    info.add('D.after state=\"$state\"');\n  }\n}\n\nclass ActionE extends ReduxAction<String> {\n  @override\n  void before() {\n    info.add('E.before state=\"$state\"');\n  }\n\n  @override\n  String reduce() {\n    info.add('E.reduce state=\"$state\"');\n    return state + 'E';\n  }\n\n  @override\n  void after() {\n    info.add('E.after1 state=\"$state\"');\n    store.dispatch(ActionA());\n    info.add('E.after2 state=\"$state\"');\n  }\n}\n\nclass ActionF extends ReduxAction<String> {\n  @override\n  void before() {\n    info.add('F.before state=\"$state\"');\n  }\n\n  @override\n  Future<String> reduce() async {\n    info.add('F.reduce1 state=\"$state\"');\n    await Future.delayed(const Duration(milliseconds: 10));\n    info.add('F.reduce2 state=\"$state\"');\n    return state + 'F';\n  }\n\n  @override\n  void after() {\n    info.add('F.after1 state=\"$state\"');\n    store.dispatch(ActionA());\n    info.add('F.after2 state=\"$state\"');\n  }\n}\n"
  },
  {
    "path": "test/before_throwing_errors_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\n/// This is meant to solve this issue:\n/// - BEFORE() SWALLOWS REDUCER() ERRORS\n///   https://github.com/marcglasberg/async_redux/issues/105\n///\nvoid main() {\n  test('1).', () async {\n    //\n    Store<String> store = Store<String>(initialState: \"\");\n\n    Object? error;\n\n    try {\n      await store.dispatch(ActionBeforeFutureOr());\n    } catch (_error) {\n      error = _error;\n    }\n\n    expect(store.state, \"\");\n    expect(\n        error,\n        StoreException(\"Before should return `void` or `Future<void>`. \"\n            \"Do not return `FutureOr`.\"));\n  });\n\n  test('1).', () async {\n    //\n    Store<String> store = Store<String>(initialState: \"\");\n\n    Object? error;\n\n    try {\n      await store.dispatch(ActionSyncBeforeThrowsError());\n    } catch (_error) {\n      error = _error;\n    }\n\n    expect(store.state, \"\");\n    expect(error, StoreException(\"ERROR 1\"));\n  });\n\n  test('2).', () async {\n    //\n    Store<String> store = Store<String>(initialState: \"\");\n\n    Object? error;\n\n    try {\n      await store.dispatch(ActionAsyncBeforeThrowsError());\n    } catch (_error) {\n      error = _error;\n    }\n\n    expect(store.state, \"\");\n    expect(error, StoreException(\"ERROR 2\"));\n  });\n\n  test('3).', () async {\n    //\n    Store<String> store = Store<String>(initialState: \"\");\n\n    Object? error;\n\n    try {\n      await store.dispatch(ActionAsyncBeforeThrowsErrorAsync());\n    } catch (_error) {\n      error = _error;\n    }\n\n    expect(store.state, \"\");\n    expect(error, StoreException(\"ERROR B\"));\n  });\n\n  test('4).', () async {\n    //\n    Store<String> store = Store<String>(initialState: \"\");\n\n    Object? error;\n\n    try {\n      await store.dispatch(ActionSyncBeforeThrowsErrorWithWrapError());\n    } catch (_error) {\n      error = _error;\n    }\n\n    expect(store.state, \"\");\n    expect(error, WrappedError(StoreException(\"ERROR 4\")));\n  });\n\n  test('5).', () async {\n    //\n    Store<String> store = Store<String>(initialState: \"\");\n\n    Object? error;\n\n    try {\n      await store.dispatch(ActionAsyncBeforeThrowsErrorWithWrapError());\n    } catch (_error) {\n      error = _error;\n    }\n\n    expect(store.state, \"\");\n    expect(error, WrappedError(StoreException(\"ERROR 5\")));\n  });\n\n  test('6).', () async {\n    //\n    Store<String> store = Store<String>(initialState: \"\");\n\n    Object? error;\n\n    try {\n      await store.dispatch(ActionAsyncBeforeThrowsErrorAsyncWithWrapError());\n    } catch (_error) {\n      error = _error;\n    }\n\n    expect(store.state, \"\");\n    expect(error, WrappedError(StoreException(\"ERROR B\")));\n  });\n\n  test('7).', () async {\n    //\n    Store<String> store = Store<String>(initialState: \"\");\n\n    Object? error;\n\n    try {\n      await store.dispatch(ActionWithBeforeAndReducerThatThrowsErrorWithWrapError());\n    } catch (_error) {\n      error = _error;\n    }\n\n    expect(store.state, \"C\");\n    expect(error, WrappedError(StoreException(\"ERROR 7\")));\n  });\n}\n\nclass ActionBeforeFutureOr extends ReduxAction<String> {\n  @override\n  FutureOr<void> before() async {\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n\n  @override\n  String reduce() {\n    return state + '0';\n  }\n}\n\nclass ActionSyncBeforeThrowsError extends ReduxAction<String> {\n  @override\n  void before() {\n    throw StoreException(\"ERROR 1\");\n  }\n\n  @override\n  String reduce() {\n    return state + '1';\n  }\n}\n\nclass ActionAsyncBeforeThrowsError extends ReduxAction<String> {\n  @override\n  Future<void> before() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    throw StoreException(\"ERROR 2\");\n  }\n\n  @override\n  String reduce() {\n    return state + '2';\n  }\n}\n\nclass ActionAsyncBeforeThrowsErrorAsync extends ReduxAction<String> {\n  @override\n  Future<void> before() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    dispatch(ActionB());\n  }\n\n  @override\n  String reduce() {\n    return state + '3';\n  }\n}\n\nclass ActionB extends ReduxAction<String> {\n  @override\n  String reduce() {\n    throw StoreException(\"ERROR B\");\n  }\n}\n\nclass ActionC extends ReduxAction<String> {\n  @override\n  String reduce() {\n    return state + 'C';\n  }\n}\n\nclass ActionSyncBeforeThrowsErrorWithWrapError extends ReduxAction<String> {\n  @override\n  void before() {\n    throw StoreException(\"ERROR 4\");\n  }\n\n  @override\n  String reduce() {\n    return state + '4';\n  }\n\n  @override\n  Object? wrapError(Object error, StackTrace stackTrace) {\n    return WrappedError(error);\n  }\n}\n\nclass ActionAsyncBeforeThrowsErrorWithWrapError extends ReduxAction<String> {\n  @override\n  Future<void> before() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    throw StoreException(\"ERROR 5\");\n  }\n\n  @override\n  String reduce() {\n    return state + '5';\n  }\n\n  @override\n  Object? wrapError(Object error, StackTrace stackTrace) {\n    return WrappedError(error);\n  }\n}\n\nclass ActionAsyncBeforeThrowsErrorAsyncWithWrapError extends ReduxAction<String> {\n  @override\n  Future<void> before() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    dispatch(ActionB());\n  }\n\n  @override\n  String reduce() {\n    return state + '6';\n  }\n\n  @override\n  Object? wrapError(Object error, StackTrace stackTrace) => WrappedError(error);\n}\n\nclass ActionWithBeforeAndReducerThatThrowsErrorWithWrapError extends ReduxAction<String> {\n  @override\n  void before() => dispatch(ActionC());\n\n  @override\n  Future<String> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 100));\n    throw StoreException(\"ERROR 7\");\n  }\n\n  @override\n  Object? wrapError(Object error, StackTrace stackTrace) => WrappedError(error);\n}\n\nclass WrappedError {\n  final Object? error;\n\n  WrappedError(this.error);\n\n  @override\n  String toString() {\n    return 'WrappedError{error: $error | ${error.runtimeType}}';\n  }\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is WrappedError && runtimeType == other.runtimeType && error == other.error;\n\n  @override\n  int get hashCode => error.hashCode;\n}\n"
  },
  {
    "path": "test/cache_test.dart",
    "content": "import 'package:async_redux/src/cache.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var stateNames = List<String>.unmodifiable([\"Juan\", \"Anna\", \"Bill\", \"Zack\", \"Arnold\", \"Amanda\"]);\n\n  test('Test 1 state with 0 parameters.', () {\n    //\n    var selector = cache1state((int limit) => () => stateNames.take(limit).toList());\n\n    var memoA1 = selector(1)();\n    var memoA2 = selector(1)();\n    expect(memoA1, [\"Juan\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    var memoB1 = selector(2)();\n    var memoB2 = selector(2)();\n    expect(memoB1, [\"Juan\", \"Anna\"]);\n    expect(identical(memoB1, memoB2), isTrue);\n  });\n\n  test('Test results are forgotten when the state changes (1 state with 0 parameters).', () {\n    //\n    var selector = cache1state((int limit) => () => stateNames.take(limit).toList());\n\n    var memoA1 = selector(1)();\n    var memoA2 = selector(1)();\n    expect(memoA1, [\"Juan\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    // Another state with another parameter.\n    selector(2)();\n\n    // Try reading the previous state, with the same parameter as before.\n    var memoA5 = selector(1)();\n    expect(memoA5, [\"Juan\"]);\n    expect(identical(memoA5, memoA1), isFalse);\n  });\n\n  test('Test 1 state with 1 parameter.', () {\n    //\n    var selector = cache1state_1param((List<String> state) =>\n        (String startString) => state.where((str) => str.startsWith(startString)).toList());\n\n    var memoA1 = selector(stateNames)(\"A\");\n    var memoA2 = selector(stateNames)(\"A\");\n    expect(memoA1, [\"Anna\", \"Arnold\", \"Amanda\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    selector(stateNames)(\"B\");\n\n    var memoA3 = selector(stateNames)(\"A\");\n    expect(memoA3, [\"Anna\", \"Arnold\", \"Amanda\"]);\n    expect(identical(memoA1, memoA3), isTrue);\n  });\n\n  test('Test results are forgotten when the state changes (1 state with 1 parameter).', () {\n    //\n    var selector = cache1state_1param((List<String> state) => (String startString) {\n          return state.where((str) => str.startsWith(startString)).toList();\n        });\n\n    var memoA1 = selector(stateNames)(\"A\");\n    var memoA2 = selector(stateNames)(\"A\");\n    expect(memoA1, [\"Anna\", \"Arnold\", \"Amanda\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    // Another state with another parameter.\n    selector(List.of(stateNames))(\"B\");\n    selector(stateNames)(\"B\");\n\n    // Try reading the previous state, with the same parameter as before.\n    var memoA5 = selector(stateNames)(\"A\");\n    expect(memoA5, [\"Anna\", \"Arnold\", \"Amanda\"]);\n    expect(identical(memoA5, memoA1), isFalse);\n  });\n\n  test('Test 1 state with 2 parameters.', () {\n    //\n    var selector =\n        cache1state_2params((List<String> state) => (String startString, String endString) {\n              return state\n                  .where((str) => str.startsWith(startString) && str.endsWith(endString))\n                  .toList();\n            });\n\n    // Concatenate.\n    String otherA = \"a\" + \"\"; // ignore: prefer_adjacent_string_concatenation\n    expect(identical(\"a\", otherA), isFalse);\n\n    var memoA1 = selector(stateNames)(\"A\", \"a\");\n    var memoA2 = selector(stateNames)(\"A\", otherA);\n    expect(memoA1, [\"Anna\", \"Amanda\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    var memoB1 = selector(stateNames)(\"A\", \"d\");\n    var memoB2 = selector(stateNames)(\"A\", \"d\");\n    expect(memoB1, [\"Arnold\"]);\n    expect(identical(memoB1, memoB2), isTrue);\n\n    var memoA3 = selector(stateNames)(\"A\", \"a\");\n    expect(memoA1, [\"Anna\", \"Amanda\"]);\n    expect(identical(memoA1, memoA3), isTrue);\n  });\n\n  test('Test results are forgotten when the state changes (1 state with 2 parameters).', () {\n    //\n    var selector =\n        cache1state_2params((List<String> state) => (String startString, String endString) {\n              return state\n                  .where((str) => str.startsWith(startString) && str.endsWith(endString))\n                  .toList();\n            });\n\n    var memoA1 = selector(stateNames)(\"A\", \"a\");\n    var memoA2 = selector(stateNames)(\"A\", \"a\");\n    expect(memoA1, [\"Anna\", \"Amanda\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    // Another state with another parameter.\n    selector(List.of(stateNames))(\"B\", \"l\");\n    selector(stateNames)(\"B\", \"l\");\n\n    // Try reading the previous state, with the same parameter as before.\n    var memoA5 = selector(stateNames)(\"A\", \"a\");\n    expect(memoA5, [\"Anna\", \"Amanda\"]);\n    expect(identical(memoA5, memoA1), isFalse);\n  });\n\n  test('Test 2 states with 0 parameters.', () {\n    //\n    var selector = cache2states((List<String> names, int limit) =>\n        () => names.where((str) => str.startsWith(\"A\")).take(limit).toList());\n\n    var memoA1 = selector(stateNames, 1)();\n    var memoA2 = selector(stateNames, 1)();\n    expect(memoA1, [\"Anna\"]);\n    expect(memoA2, [\"Anna\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    var memoB1 = selector(stateNames, 2)();\n    var memoB2 = selector(stateNames, 2)();\n    expect(memoB1, [\"Anna\", \"Arnold\"]);\n    expect(identical(memoB1, memoB2), isTrue);\n  });\n\n  test('Test results are forgotten when the state changes (2 states with 0 parameters).', () {\n    //\n    var selector = cache2states((List<String> names, int limit) => () {\n          return names.where((str) => str.startsWith(\"A\")).take(limit).toList();\n        });\n\n    var memoA1 = selector(stateNames, 1)();\n    var memoA2 = selector(stateNames, 1)();\n    expect(memoA1, [\"Anna\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    // Another state with another parameter.\n    selector(stateNames, 2)();\n\n    // Try reading the previous state, with the same parameter as before.\n    var memoA5 = selector(stateNames, 1)();\n    expect(memoA5, [\"Anna\"]);\n    expect(identical(memoA5, memoA1), isFalse);\n  });\n\n  test('Test 2 states with 1 parameter.', () {\n    //\n    var selector = cache2states_1param((List<String> names, int limit) => (String searchString) {\n          return names.where((str) => str.startsWith(searchString)).take(limit).toList();\n        });\n\n    var memoA1 = selector(stateNames, 1)(\"A\");\n    var memoA2 = selector(stateNames, 1)(\"A\");\n    expect(memoA1, [\"Anna\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    var memoB1 = selector(stateNames, 2)(\"A\");\n    var memoB2 = selector(stateNames, 2)(\"A\");\n    expect(memoB1, [\"Anna\", \"Arnold\"]);\n    expect(identical(memoB1, memoB2), isTrue);\n\n    var memoC = selector(stateNames, 2)(\"B\");\n    expect(memoC, [\"Bill\"]);\n\n    var memoD = selector(stateNames, 2)(\"A\");\n    expect(identical(memoD, memoB1), isTrue);\n\n    // Has to forget, because the state changed.\n    selector(stateNames, 1)(\"A\");\n    expect(identical(memoA1, memoC), isFalse);\n  });\n\n  test('Test results are forgotten when the state changes (2 states with 1 parameter).', () {\n    //\n    var selector = cache2states_1param((List<String> names, int limit) => (String searchString) {\n          return names.where((str) => str.startsWith(searchString)).take(limit).toList();\n        });\n\n    var memoA1 = selector(stateNames, 1)(\"A\");\n    var memoA2 = selector(stateNames, 1)(\"A\");\n    expect(memoA1, [\"Anna\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    // Another state with another parameter.\n    selector(stateNames, 2)(\"B\");\n    selector(stateNames, 1)(\"B\");\n\n    // Try reading the previous state, with the same parameter as before.\n    var memoA5 = selector(stateNames, 1)(\"A\");\n    expect(memoA5, [\"Anna\"]);\n    expect(identical(memoA5, memoA1), isFalse);\n  });\n\n  test('Test 2 states with 2 parameters.', () {\n    //\n    var selector = cache2states_2params(\n        (List<String> names, int limit) => (String startString, String endString) {\n              return names\n                  .where((str) => str.startsWith(startString) && str.endsWith(endString))\n                  .take(limit)\n                  .toList();\n            });\n\n    var memoA1 = selector(stateNames, 1)(\"A\", \"a\");\n    var memoA2 = selector(stateNames, 1)(\"A\", \"a\");\n    expect(memoA1, [\"Anna\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    var memoB1 = selector(stateNames, 2)(\"A\", \"a\");\n    var memoB2 = selector(stateNames, 2)(\"A\", \"a\");\n    expect(memoB1, [\"Anna\", \"Amanda\"]);\n    expect(identical(memoB1, memoB2), isTrue);\n  });\n\n  test('Test results are forgotten when the state changes (2 states with 2 parameters).', () {\n    //\n    var selector = cache2states_2params(\n        (List<String> names, int limit) => (String startString, String endString) {\n              return names\n                  .where((str) => str.startsWith(startString) && str.endsWith(endString))\n                  .take(limit)\n                  .toList();\n            });\n\n    var memoA1 = selector(stateNames, 1)(\"A\", \"a\");\n    var memoA2 = selector(stateNames, 1)(\"A\", \"a\");\n    expect(memoA1, [\"Anna\"]);\n    expect(identical(memoA1, memoA2), isTrue);\n\n    // Another state with another parameter.\n    selector(stateNames, 2)(\"B\", \"l\");\n    selector(stateNames, 1)(\"B\", \"l\");\n\n    // Try reading the previous state, with the same parameter as before.\n    var memoA5 = selector(stateNames, 1)(\"A\", \"a\");\n    expect(memoA5, [\"Anna\"]);\n    expect(identical(memoA5, memoA1), isFalse);\n  });\n\n  test('Changing the second or the first state, it should forget the cached value.', () {\n    //\n    var stateNames1 = List<String>.unmodifiable([\"A1a\", \"A2a\", \"A3x\", \"B4a\", \"B5a\", \"B6x\"]);\n\n    var selector = cache2states_2params(\n        (List<String> names, int limit) => (String startString, String endString) {\n              return names\n                  .where((str) => str.startsWith(startString) && str.endsWith(endString))\n                  .take(limit)\n                  .toList();\n            });\n\n    var memo1 = selector(stateNames1, 1)(\"A\", \"a\");\n    expect(memo1, [\"A1a\"]);\n\n    var memo2 = selector(stateNames1, 2)(\"A\", \"a\");\n    expect(memo2, [\"A1a\", \"A2a\"]);\n\n    var memo3 = selector(stateNames1, 1)(\"A\", \"a\");\n    expect(memo3, [\"A1a\"]);\n\n    var memo4 = selector(stateNames1, 2)(\"A\", \"a\");\n    expect(memo4, [\"A1a\", \"A2a\"]);\n\n    expect(identical(memo1, memo3), isFalse);\n    expect(identical(memo2, memo4), isFalse);\n\n    // ---\n\n    var stateNames2 = List<String>.unmodifiable([\"A1a\", \"A2a\", \"A3x\", \"B4a\", \"B5a\", \"B6x\"]);\n\n    var memo5 = selector(stateNames1, 1)(\"A\", \"a\");\n    expect(memo5, [\"A1a\"]);\n\n    var memo6 = selector(stateNames2, 1)(\"A\", \"a\");\n    expect(memo6, [\"A1a\"]);\n\n    var memo7 = selector(stateNames1, 1)(\"A\", \"a\");\n    expect(memo7, [\"A1a\"]);\n\n    var memo8 = selector(stateNames2, 1)(\"A\", \"a\");\n    expect(memo8, [\"A1a\"]);\n\n    expect(identical(memo5, memo7), isFalse);\n    expect(identical(memo6, memo8), isFalse);\n  });\n}\n"
  },
  {
    "path": "test/check_internet_mixin_test.dart",
    "content": "import 'package:bdd_framework/bdd_framework.dart';\n\nvoid main() {\n  var feature = BddFeature('Check internet actions');\n  // TODO: Test mixins: CheckInternet, NoDialog, AbortWhenNoInternet\n}\n"
  },
  {
    "path": "test/context_environment_test.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('AsyncRedux context.env functionality', () {\n    testWidgets('Environment is accessible via context.env',\n        (WidgetTester tester) async {\n      final initialState = AppState(counter: 0);\n      final environment = TestEnvironment(\n        apiUrl: 'https://api.example.com',\n        apiKey: 'test-key-123',\n      );\n\n      final store = Store<AppState>(\n        initialState: initialState,\n        environment: environment,\n      );\n\n      TestEnvironment? capturedEnv;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                capturedEnv = context.env;\n                return Text('API: ${capturedEnv!.apiUrl}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(capturedEnv, isNotNull);\n      expect(capturedEnv!.apiUrl, 'https://api.example.com');\n      expect(capturedEnv!.apiKey, 'test-key-123');\n      expect(find.text('API: https://api.example.com'), findsOneWidget);\n    });\n\n    testWidgets('Accessing environment does not trigger rebuilds',\n        (WidgetTester tester) async {\n      final initialState = AppState(counter: 0);\n      final environment = TestEnvironment(\n        apiUrl: 'https://api.example.com',\n        apiKey: 'test-key-123',\n      );\n\n      final store = Store<AppState>(\n        initialState: initialState,\n        environment: environment,\n      );\n\n      int buildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                // Access environment - should not cause rebuilds\n                var env = context.env;\n                return Text('API: ${env.apiUrl}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(buildCount, 1);\n\n      // Dispatch action that changes state\n      store.dispatch(IncrementAction());\n      await tester.pump();\n      await tester.pump();\n\n      // Widget should NOT rebuild because it only accessed env, not state\n      expect(buildCount, 1);\n\n      // Dispatch another action\n      store.dispatch(IncrementAction());\n      await tester.pump();\n      await tester.pump();\n\n      // Still no rebuild\n      expect(buildCount, 1);\n    });\n\n    testWidgets('Environment access combined with state access',\n        (WidgetTester tester) async {\n      final initialState = AppState(counter: 0);\n      final environment = TestEnvironment(\n        apiUrl: 'https://api.example.com',\n        apiKey: 'test-key-123',\n      );\n\n      final store = Store<AppState>(\n        initialState: initialState,\n        environment: environment,\n      );\n\n      int envOnlyBuildCount = 0;\n      int stateAndEnvBuildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Column(\n                children: [\n                  // Widget that only accesses env\n                  Builder(builder: (context) {\n                    envOnlyBuildCount++;\n                    var env = context.env;\n                    return Text('URL: ${env.apiUrl}');\n                  }),\n                  // Widget that accesses both env and state\n                  Builder(builder: (context) {\n                    stateAndEnvBuildCount++;\n                    var env = context.env;\n                    var counter = context.select((st) => st.counter);\n                    return Text('${env.apiKey}: $counter');\n                  }),\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n\n      expect(envOnlyBuildCount, 1);\n      expect(stateAndEnvBuildCount, 1);\n\n      // Dispatch action\n      store.dispatch(IncrementAction());\n      await tester.pump();\n      await tester.pump();\n\n      // Only the widget with state access should rebuild\n      expect(envOnlyBuildCount, 1); // No rebuild\n      expect(stateAndEnvBuildCount, 2); // Rebuilt due to state change\n    });\n\n    testWidgets('Environment is same instance across widgets',\n        (WidgetTester tester) async {\n      final initialState = AppState(counter: 0);\n      final environment = TestEnvironment(\n        apiUrl: 'https://api.example.com',\n        apiKey: 'test-key-123',\n      );\n\n      final store = Store<AppState>(\n        initialState: initialState,\n        environment: environment,\n      );\n\n      TestEnvironment? env1;\n      TestEnvironment? env2;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Column(\n                children: [\n                  Builder(builder: (context) {\n                    env1 = context.env;\n                    return const Text('Widget 1');\n                  }),\n                  Builder(builder: (context) {\n                    env2 = context.env;\n                    return const Text('Widget 2');\n                  }),\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n\n      expect(env1, isNotNull);\n      expect(env2, isNotNull);\n      expect(identical(env1, env2), true);\n      expect(identical(env1, environment), true);\n    });\n\n    testWidgets('Null environment returns null', (WidgetTester tester) async {\n      final initialState = AppState(counter: 0);\n\n      // Store without environment\n      final store = Store<AppState>(initialState: initialState);\n\n      Object? capturedEnv;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                capturedEnv = context.getEnvironment<AppState>();\n                return Text('Env: $capturedEnv');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(capturedEnv, isNull);\n      expect(find.text('Env: null'), findsOneWidget);\n    });\n\n    testWidgets('Environment accessible in nested widgets',\n        (WidgetTester tester) async {\n      final initialState = AppState(counter: 0);\n      final environment = TestEnvironment(\n        apiUrl: 'https://api.example.com',\n        apiKey: 'nested-test',\n      );\n\n      final store = Store<AppState>(\n        initialState: initialState,\n        environment: environment,\n      );\n\n      TestEnvironment? deepNestedEnv;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Container(\n                child: Column(\n                  children: [\n                    Card(\n                      child: Padding(\n                        padding: const EdgeInsets.all(8.0),\n                        child: Builder(builder: (context) {\n                          deepNestedEnv = context.env;\n                          return Text('Key: ${deepNestedEnv!.apiKey}');\n                        }),\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ),\n      );\n\n      expect(deepNestedEnv, isNotNull);\n      expect(deepNestedEnv!.apiKey, 'nested-test');\n      expect(find.text('Key: nested-test'), findsOneWidget);\n    });\n  });\n}\n\n// Extension for BuildContext\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n  TestEnvironment get env => getEnvironment<AppState>() as TestEnvironment;\n}\n\n// Test environment class\nclass TestEnvironment {\n  final String apiUrl;\n  final String apiKey;\n\n  TestEnvironment({required this.apiUrl, required this.apiKey});\n}\n\n// Test state class\nclass AppState {\n  final int counter;\n\n  AppState({required this.counter});\n\n  AppState copyWith({int? counter}) {\n    return AppState(counter: counter ?? this.counter);\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is AppState && other.counter == counter;\n  }\n\n  @override\n  int get hashCode => counter.hashCode;\n}\n\n// Test action\nclass IncrementAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(counter: state.counter + 1);\n  }\n}\n"
  },
  {
    "path": "test/context_event_test.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('AsyncRedux context.event() functionality', () {\n    testWidgets('Basic event consumption - value-less event (Event)',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int buildCount = 0;\n      bool? lastEventValue;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                var clearText = context.event((state) => state.clearTextEvt);\n                lastEventValue = clearText;\n                return Text('Clear: $clearText', key: const Key('clearText'));\n              }),\n            ),\n          ),\n        ),\n      );\n\n      // Initial build - event is spent, should return false\n      expect(buildCount, 1);\n      expect(lastEventValue, false);\n      expect(find.text('Clear: false'), findsOneWidget);\n\n      // Dispatch event - should rebuild and return true\n      store.dispatch(ClearTextAction());\n      await tester.pump();\n      await tester.pump();\n\n      expect(buildCount, 2);\n      expect(lastEventValue, true);\n      expect(find.text('Clear: true'), findsOneWidget);\n\n      // Event is now spent - dispatching unrelated action should NOT rebuild\n      // because the selected event hasn't changed\n      store.dispatch(IncrementCounterAction());\n      await tester.pump();\n      await tester.pump();\n\n      expect(buildCount, 2); // No rebuild - event didn't change\n      expect(find.text('Clear: true'), findsOneWidget); // Still shows last rendered value\n    });\n\n    testWidgets('Typed event consumption - Event<String>',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int buildCount = 0;\n      String? lastEventValue;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                var newText = context.event((state) => state.changeTextEvt);\n                lastEventValue = newText;\n                return Text('Text: ${newText ?? \"none\"}',\n                    key: const Key('changeText'));\n              }),\n            ),\n          ),\n        ),\n      );\n\n      // Initial build - event is spent, should return null\n      expect(buildCount, 1);\n      expect(lastEventValue, null);\n      expect(find.text('Text: none'), findsOneWidget);\n\n      // Dispatch event with value - should rebuild and return value\n      store.dispatch(ChangeTextAction('Hello World'));\n      await tester.pump();\n      await tester.pump();\n\n      expect(buildCount, 2);\n      expect(lastEventValue, 'Hello World');\n      expect(find.text('Text: Hello World'), findsOneWidget);\n\n      // Event is now spent - dispatching unrelated action should NOT rebuild\n      store.dispatch(IncrementCounterAction());\n      await tester.pump();\n      await tester.pump();\n\n      expect(buildCount, 2); // No rebuild - event didn't change\n      expect(find.text('Text: Hello World'), findsOneWidget); // Still shows last value\n    });\n\n    testWidgets('Event consumed only once per dispatch',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      List<String?> eventHistory = [];\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                var newText = context.event((state) => state.changeTextEvt);\n                eventHistory.add(newText);\n                return Text('Text: ${newText ?? \"none\"}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(eventHistory, [null]); // Initial - spent\n\n      // First event\n      store.dispatch(ChangeTextAction('First'));\n      await tester.pump();\n      await tester.pump();\n      expect(eventHistory, [null, 'First']);\n\n      // Second event - overwrites the spent first event\n      store.dispatch(ChangeTextAction('Second'));\n      await tester.pump();\n      await tester.pump();\n      expect(eventHistory, [null, 'First', 'Second']);\n\n      // Third event\n      store.dispatch(ChangeTextAction('Third'));\n      await tester.pump();\n      await tester.pump();\n      expect(eventHistory, [null, 'First', 'Second', 'Third']);\n    });\n\n    testWidgets('Multiple events in same widget', (WidgetTester tester) async {\n      final initialState = AppState(\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int buildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                var clear = context.event((state) => state.clearTextEvt);\n                var change = context.event((state) => state.changeTextEvt);\n                return Column(\n                  children: [\n                    Text('Clear: $clear'),\n                    Text('Change: ${change ?? \"none\"}'),\n                  ],\n                );\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(buildCount, 1);\n      expect(find.text('Clear: false'), findsOneWidget);\n      expect(find.text('Change: none'), findsOneWidget);\n\n      // Dispatch clear event only\n      store.dispatch(ClearTextAction());\n      await tester.pump();\n      await tester.pump();\n\n      expect(buildCount, 2);\n      expect(find.text('Clear: true'), findsOneWidget);\n      expect(find.text('Change: none'), findsOneWidget);\n\n      // Dispatch change event only\n      store.dispatch(ChangeTextAction('New Text'));\n      await tester.pump();\n      await tester.pump();\n\n      expect(buildCount, 3);\n      expect(find.text('Clear: false'), findsOneWidget); // Clear was consumed\n      expect(find.text('Change: New Text'), findsOneWidget);\n\n      // Dispatch both events\n      store.dispatch(ClearTextAction());\n      store.dispatch(ChangeTextAction('Both Events'));\n      await tester.pump();\n      await tester.pump();\n\n      expect(buildCount, 4);\n      expect(find.text('Clear: true'), findsOneWidget);\n      expect(find.text('Change: Both Events'), findsOneWidget);\n    });\n\n    testWidgets('Event triggers rebuild even with same value',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int buildCount = 0;\n      List<String?> values = [];\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                var newText = context.event((state) => state.changeTextEvt);\n                values.add(newText);\n                return Text('Text: ${newText ?? \"none\"}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(buildCount, 1);\n\n      // Dispatch event with value\n      store.dispatch(ChangeTextAction('Same'));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 2);\n      expect(values.last, 'Same');\n\n      // Dispatch same value again - should still trigger rebuild\n      // because each Event instance is unique\n      store.dispatch(ChangeTextAction('Same'));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 3);\n      expect(values.last, 'Same');\n\n      // Dispatch same value a third time\n      store.dispatch(ChangeTextAction('Same'));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 4);\n      expect(values.last, 'Same');\n    });\n\n    testWidgets('Event with null value vs spent event',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      List<String?> values = [];\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                var newText = context.event((state) => state.changeTextEvt);\n                values.add(newText);\n                return Text('Text: ${newText ?? \"none\"}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(values, [null]); // Spent event returns null\n\n      // Dispatch event with null value\n      store.dispatch(ChangeTextAction(null));\n      await tester.pump();\n      await tester.pump();\n      // Event with null value also returns null, but event was consumed\n      expect(values, [null, null]);\n\n      // Dispatch another event with actual value\n      store.dispatch(ChangeTextAction('actual'));\n      await tester.pump();\n      await tester.pump();\n      expect(values, [null, null, 'actual']);\n    });\n\n    testWidgets('Event with various types - int', (WidgetTester tester) async {\n      final initialState = AppStateWithIntEvent(\n        numberEvt: Event<int>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppStateWithIntEvent>(initialState: initialState);\n      List<int?> values = [];\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppStateWithIntEvent>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                var number = context.getEvent<AppStateWithIntEvent, int>(\n                    (state) => state.numberEvt);\n                values.add(number);\n                return Text('Number: ${number ?? \"none\"}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(values, [null]);\n\n      store.dispatch(SetNumberAction(42));\n      await tester.pump();\n      await tester.pump();\n      expect(values, [null, 42]);\n\n      store.dispatch(SetNumberAction(0));\n      await tester.pump();\n      await tester.pump();\n      expect(values, [null, 42, 0]);\n    });\n\n    testWidgets('Event not consumed when widget disposed',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      bool showWidget = true;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: StatefulBuilder(\n              builder: (context, setState) {\n                return Scaffold(\n                  body: Column(\n                    children: [\n                      if (showWidget)\n                        Builder(builder: (context) {\n                          var newText =\n                              context.event((state) => state.changeTextEvt);\n                          return Text('Text: ${newText ?? \"none\"}');\n                        }),\n                      ElevatedButton(\n                        onPressed: () => setState(() => showWidget = false),\n                        child: const Text('Hide'),\n                      ),\n                    ],\n                  ),\n                );\n              },\n            ),\n          ),\n        ),\n      );\n\n      expect(find.text('Text: none'), findsOneWidget);\n\n      // Dispatch event\n      store.dispatch(ChangeTextAction('Hello'));\n      await tester.pump();\n      await tester.pump();\n      expect(find.text('Text: Hello'), findsOneWidget);\n\n      // Hide widget\n      await tester.tap(find.text('Hide'));\n      await tester.pump();\n      expect(find.text('Text: Hello'), findsNothing);\n\n      // Dispatch another event while widget is hidden\n      store.dispatch(ChangeTextAction('World'));\n      await tester.pump();\n      // Event is in state but not consumed (no widget to consume it)\n      expect(store.state.changeTextEvt.isNotSpent, true);\n    });\n\n    testWidgets('Rapid event dispatching', (WidgetTester tester) async {\n      final initialState = AppState(\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      List<String?> values = [];\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                var newText = context.event((state) => state.changeTextEvt);\n                values.add(newText);\n                return Text('Text: ${newText ?? \"none\"}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(values, [null]);\n\n      // Dispatch multiple events rapidly\n      store.dispatch(ChangeTextAction('First'));\n      store.dispatch(ChangeTextAction('Second'));\n      store.dispatch(ChangeTextAction('Third'));\n      await tester.pump();\n      await tester.pump();\n\n      // Only the last event should be consumed (events overwrite each other)\n      expect(values.last, 'Third');\n    });\n\n    testWidgets('Event with complex type - List', (WidgetTester tester) async {\n      final initialState = AppStateWithListEvent(\n        itemsEvt: Event<List<String>>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppStateWithListEvent>(initialState: initialState);\n      List<List<String>?> values = [];\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppStateWithListEvent>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                var items = context.getEvent<AppStateWithListEvent, List<String>>(\n                    (state) => state.itemsEvt);\n                values.add(items);\n                return Text('Items: ${items?.join(\", \") ?? \"none\"}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(values, [null]);\n      expect(find.text('Items: none'), findsOneWidget);\n\n      store.dispatch(SetItemsAction(['apple', 'banana', 'cherry']));\n      await tester.pump();\n      await tester.pump();\n      expect(values.last, ['apple', 'banana', 'cherry']);\n      expect(find.text('Items: apple, banana, cherry'), findsOneWidget);\n    });\n\n    testWidgets('Event combined with select - independent rebuilds',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        clearTextEvt: Event.spent(),\n        changeTextEvt: Event<String>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int eventWidgetBuilds = 0;\n      int selectWidgetBuilds = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Column(\n                children: [\n                  // Widget using event\n                  Builder(builder: (context) {\n                    eventWidgetBuilds++;\n                    var newText = context.event((state) => state.changeTextEvt);\n                    return Text('Event: ${newText ?? \"none\"}');\n                  }),\n                  // Widget using select\n                  Builder(builder: (context) {\n                    selectWidgetBuilds++;\n                    var counter = context.select((st) => st.counter);\n                    return Text('Counter: $counter');\n                  }),\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n\n      expect(eventWidgetBuilds, 1);\n      expect(selectWidgetBuilds, 1);\n\n      // Dispatch event - should rebuild event widget\n      store.dispatch(ChangeTextAction('Hello'));\n      await tester.pump();\n      await tester.pump();\n      expect(eventWidgetBuilds, 2);\n      // Select widget may or may not rebuild depending on implementation\n\n      // Increment counter - should rebuild select widget\n      store.dispatch(IncrementCounterAction());\n      await tester.pump();\n      await tester.pump();\n      expect(selectWidgetBuilds, greaterThan(1));\n    });\n\n    testWidgets('Event.from - consuming from multiple events',\n        (WidgetTester tester) async {\n      final initialState = AppStateWithTwoEvents(\n        event1: Event<String>.spent(),\n        event2: Event<String>.spent(),\n        counter: 0,\n      );\n\n      final store = Store<AppStateWithTwoEvents>(initialState: initialState);\n      List<String?> values = [];\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppStateWithTwoEvents>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                var combined = context.getEvent<AppStateWithTwoEvents, String>(\n                    (state) => Event.from(state.event1, state.event2));\n                values.add(combined);\n                return Text('Combined: ${combined ?? \"none\"}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(values, [null]);\n\n      // Dispatch to first event\n      store.dispatch(SetEvent1Action('From Event 1'));\n      await tester.pump();\n      await tester.pump();\n      expect(values.last, 'From Event 1');\n\n      // Dispatch to second event (first is now spent, so second is consumed)\n      store.dispatch(SetEvent2Action('From Event 2'));\n      await tester.pump();\n      await tester.pump();\n      expect(values.last, 'From Event 2');\n\n      // Dispatch to first event again\n      store.dispatch(SetEvent1Action('From Event 1 Again'));\n      await tester.pump();\n      await tester.pump();\n      expect(values.last, 'From Event 1 Again');\n    });\n\n    testWidgets('MappedEvent - transforming event values',\n        (WidgetTester tester) async {\n      final initialState = AppStateWithMappedEvent(\n        indexEvt: Event<int>.spent(),\n        users: ['Alice', 'Bob', 'Charlie'],\n        counter: 0,\n      );\n\n      final store = Store<AppStateWithMappedEvent>(initialState: initialState);\n      List<String?> values = [];\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppStateWithMappedEvent>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                var user = context.getEvent<AppStateWithMappedEvent, String>(\n                    (state) => Event.map(\n                        state.indexEvt,\n                        (int? index) =>\n                            index == null ? null : state.users[index]));\n                values.add(user);\n                return Text('User: ${user ?? \"none\"}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(values, [null]);\n\n      // Dispatch index 1 -> should return \"Bob\"\n      store.dispatch(SetIndexAction(1));\n      await tester.pump();\n      await tester.pump();\n      expect(values.last, 'Bob');\n      expect(find.text('User: Bob'), findsOneWidget);\n\n      // Dispatch index 2 -> should return \"Charlie\"\n      store.dispatch(SetIndexAction(2));\n      await tester.pump();\n      await tester.pump();\n      expect(values.last, 'Charlie');\n      expect(find.text('User: Charlie'), findsOneWidget);\n\n      // Dispatch index 0 -> should return \"Alice\"\n      store.dispatch(SetIndexAction(0));\n      await tester.pump();\n      await tester.pump();\n      expect(values.last, 'Alice');\n      expect(find.text('User: Alice'), findsOneWidget);\n    });\n\n    testWidgets('Event equality - spent events are equal',\n        (WidgetTester tester) async {\n      final evt1 = Event<String>.spent();\n      final evt2 = Event<String>.spent();\n      expect(evt1 == evt2, true);\n\n      final evt3 = Event<String>('value');\n      final evt4 = Event<String>('value');\n      // Unspent events are never equal\n      expect(evt3 == evt4, false);\n\n      // Consume evt3\n      evt3.consume();\n      // Now evt3 is spent but evt4 is not\n      expect(evt3 == evt4, false);\n\n      // Consume evt4\n      evt4.consume();\n      // Both spent, should be equal\n      expect(evt3 == evt4, true);\n    });\n\n    testWidgets('Event isSpent and isNotSpent properties',\n        (WidgetTester tester) async {\n      final evt = Event<String>('test');\n      expect(evt.isSpent, false);\n      expect(evt.isNotSpent, true);\n\n      evt.consume();\n      expect(evt.isSpent, true);\n      expect(evt.isNotSpent, false);\n    });\n  });\n}\n\n// Extension for BuildContext\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n  AppState read() => getRead<AppState>();\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n}\n\n// Test state classes\nclass AppState {\n  final Event clearTextEvt;\n  final Event<String> changeTextEvt;\n  final int counter;\n\n  AppState({\n    required this.clearTextEvt,\n    required this.changeTextEvt,\n    required this.counter,\n  });\n\n  AppState copyWith({\n    Event? clearTextEvt,\n    Event<String>? changeTextEvt,\n    int? counter,\n  }) {\n    return AppState(\n      clearTextEvt: clearTextEvt ?? this.clearTextEvt,\n      changeTextEvt: changeTextEvt ?? this.changeTextEvt,\n      counter: counter ?? this.counter,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is AppState &&\n        other.clearTextEvt == clearTextEvt &&\n        other.changeTextEvt == changeTextEvt &&\n        other.counter == counter;\n  }\n\n  @override\n  int get hashCode => Object.hash(clearTextEvt, changeTextEvt, counter);\n}\n\nclass AppStateWithIntEvent {\n  final Event<int> numberEvt;\n  final int counter;\n\n  AppStateWithIntEvent({required this.numberEvt, required this.counter});\n\n  AppStateWithIntEvent copyWith({Event<int>? numberEvt, int? counter}) {\n    return AppStateWithIntEvent(\n      numberEvt: numberEvt ?? this.numberEvt,\n      counter: counter ?? this.counter,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is AppStateWithIntEvent &&\n        other.numberEvt == numberEvt &&\n        other.counter == counter;\n  }\n\n  @override\n  int get hashCode => Object.hash(numberEvt, counter);\n}\n\nclass AppStateWithListEvent {\n  final Event<List<String>> itemsEvt;\n  final int counter;\n\n  AppStateWithListEvent({required this.itemsEvt, required this.counter});\n\n  AppStateWithListEvent copyWith({Event<List<String>>? itemsEvt, int? counter}) {\n    return AppStateWithListEvent(\n      itemsEvt: itemsEvt ?? this.itemsEvt,\n      counter: counter ?? this.counter,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is AppStateWithListEvent &&\n        other.itemsEvt == itemsEvt &&\n        other.counter == counter;\n  }\n\n  @override\n  int get hashCode => Object.hash(itemsEvt, counter);\n}\n\nclass AppStateWithTwoEvents {\n  final Event<String> event1;\n  final Event<String> event2;\n  final int counter;\n\n  AppStateWithTwoEvents({\n    required this.event1,\n    required this.event2,\n    required this.counter,\n  });\n\n  AppStateWithTwoEvents copyWith({\n    Event<String>? event1,\n    Event<String>? event2,\n    int? counter,\n  }) {\n    return AppStateWithTwoEvents(\n      event1: event1 ?? this.event1,\n      event2: event2 ?? this.event2,\n      counter: counter ?? this.counter,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is AppStateWithTwoEvents &&\n        other.event1 == event1 &&\n        other.event2 == event2 &&\n        other.counter == counter;\n  }\n\n  @override\n  int get hashCode => Object.hash(event1, event2, counter);\n}\n\nclass AppStateWithMappedEvent {\n  final Event<int> indexEvt;\n  final List<String> users;\n  final int counter;\n\n  AppStateWithMappedEvent({\n    required this.indexEvt,\n    required this.users,\n    required this.counter,\n  });\n\n  AppStateWithMappedEvent copyWith({\n    Event<int>? indexEvt,\n    List<String>? users,\n    int? counter,\n  }) {\n    return AppStateWithMappedEvent(\n      indexEvt: indexEvt ?? this.indexEvt,\n      users: users ?? this.users,\n      counter: counter ?? this.counter,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is AppStateWithMappedEvent &&\n        other.indexEvt == indexEvt &&\n        other.counter == counter;\n  }\n\n  @override\n  int get hashCode => Object.hash(indexEvt, counter);\n}\n\n// Test actions\nclass ClearTextAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(clearTextEvt: Event());\n  }\n}\n\nclass ChangeTextAction extends ReduxAction<AppState> {\n  final String? text;\n  ChangeTextAction(this.text);\n\n  @override\n  AppState reduce() {\n    return state.copyWith(changeTextEvt: Event<String>(text));\n  }\n}\n\nclass IncrementCounterAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(counter: state.counter + 1);\n  }\n}\n\nclass SetNumberAction extends ReduxAction<AppStateWithIntEvent> {\n  final int number;\n  SetNumberAction(this.number);\n\n  @override\n  AppStateWithIntEvent reduce() {\n    return state.copyWith(numberEvt: Event<int>(number));\n  }\n}\n\nclass SetItemsAction extends ReduxAction<AppStateWithListEvent> {\n  final List<String> items;\n  SetItemsAction(this.items);\n\n  @override\n  AppStateWithListEvent reduce() {\n    return state.copyWith(itemsEvt: Event<List<String>>(items));\n  }\n}\n\nclass SetEvent1Action extends ReduxAction<AppStateWithTwoEvents> {\n  final String value;\n  SetEvent1Action(this.value);\n\n  @override\n  AppStateWithTwoEvents reduce() {\n    return state.copyWith(event1: Event<String>(value));\n  }\n}\n\nclass SetEvent2Action extends ReduxAction<AppStateWithTwoEvents> {\n  final String value;\n  SetEvent2Action(this.value);\n\n  @override\n  AppStateWithTwoEvents reduce() {\n    return state.copyWith(event2: Event<String>(value));\n  }\n}\n\nclass IncrementCounter2Action extends ReduxAction<AppStateWithTwoEvents> {\n  @override\n  AppStateWithTwoEvents reduce() {\n    return state.copyWith(counter: state.counter + 1);\n  }\n}\n\nclass SetIndexAction extends ReduxAction<AppStateWithMappedEvent> {\n  final int index;\n  SetIndexAction(this.index);\n\n  @override\n  AppStateWithMappedEvent reduce() {\n    return state.copyWith(indexEvt: Event<int>(index));\n  }\n}\n\nclass IncrementCounter3Action extends ReduxAction<AppStateWithMappedEvent> {\n  @override\n  AppStateWithMappedEvent reduce() {\n    return state.copyWith(counter: state.counter + 1);\n  }\n}\n"
  },
  {
    "path": "test/context_select_advanced_test.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:fast_immutable_collections/fast_immutable_collections.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('AsyncRedux select() functionality', () {\n    testWidgets('Basic selection - widget only rebuilds when selected value changes',\n        (WidgetTester tester) async {\n      // Create initial state\n      final initialState = AppState(\n        user: User(name: 'Alice', age: 25, email: 'alice@example.com'),\n        counter: 0,\n        items: IList<String>(['item1']),\n        settings: Settings(darkMode: false, language: 'en'),\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n\n      // Track build counts for each widget\n      Map<String, int> buildCounts = {\n        'userName': 0,\n        'userAge': 0,\n        'counter': 0,\n        'itemsCount': 0,\n        'darkMode': 0,\n      };\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Column(\n                children: [\n                  Builder(builder: (context) {\n                    buildCounts['userName'] = buildCounts['userName']! + 1;\n                    final userName = context.select((st) => st.user.name);\n                    return Text('Name: $userName', key: const Key('userName'));\n                  }),\n                  Builder(builder: (context) {\n                    buildCounts['userAge'] = buildCounts['userAge']! + 1;\n                    final userAge = context.select((st) => st.user.age);\n                    return Text('Age: $userAge', key: const Key('userAge'));\n                  }),\n                  Builder(builder: (context) {\n                    buildCounts['counter'] = buildCounts['counter']! + 1;\n                    final counter = context.select((st) => st.counter);\n                    return Text('Counter: $counter', key: const Key('counter'));\n                  }),\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n\n      // Initial build\n      expect(buildCounts['userName'], 1);\n      expect(buildCounts['userAge'], 1);\n      expect(buildCounts['counter'], 1);\n      expect(find.text('Name: Alice'), findsOneWidget);\n      expect(find.text('Age: 25'), findsOneWidget);\n      expect(find.text('Counter: 0'), findsOneWidget);\n\n      // Update user name - only userName widget should rebuild\n      store.dispatch(UpdateUserNameAction('Bob'));\n      await tester.pump();\n      await tester.pump();\n\n      expect(buildCounts['userName'], 2); // Rebuilt\n      expect(buildCounts['userAge'], 1); // Not rebuilt\n      expect(buildCounts['counter'], 1); // Not rebuilt\n      expect(find.text('Name: Bob'), findsOneWidget);\n\n      // Update user age - only userAge widget should rebuild\n      store.dispatch(UpdateUserAgeAction(30));\n      await tester.pump();\n      await tester.pump();\n\n      expect(buildCounts['userName'], 2); // Not rebuilt\n      expect(buildCounts['userAge'], 2); // Rebuilt\n      expect(buildCounts['counter'], 1); // Not rebuilt\n      expect(find.text('Age: 30'), findsOneWidget);\n\n      // Update counter - only counter widget should rebuild\n      store.dispatch(IncrementCounterAction());\n      await tester.pump();\n      await tester.pump();\n\n      expect(buildCounts['userName'], 2); // Not rebuilt\n      expect(buildCounts['userAge'], 2); // Not rebuilt\n      expect(buildCounts['counter'], 2); // Rebuilt\n      expect(find.text('Counter: 1'), findsOneWidget);\n    });\n\n    testWidgets('Deep equality checking for collections', (WidgetTester tester) async {\n      final initialState = AppState(\n        user: User(name: 'Alice', age: 25, email: 'alice@example.com'),\n        counter: 0,\n        items: IList<String>(['apple', 'banana']),\n        settings: Settings(darkMode: false, language: 'en'),\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int buildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                // Select filtered list\n                final filteredItems = context.select(\n                  (state) => state.items.where((item) => item.startsWith('a')).toList(),\n                );\n                return Text('Items: ${filteredItems.join(', ')}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(buildCount, 1);\n      expect(find.text('Items: apple'), findsOneWidget);\n\n      // Add item that doesn't match filter - should NOT rebuild\n      store.dispatch(AddItemAction('cherry'));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 1); // No rebuild\n\n      // Add item that matches filter - should rebuild\n      store.dispatch(AddItemAction('apricot'));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 2); // Rebuilt\n      expect(find.text('Items: apple, apricot'), findsOneWidget);\n    });\n\n    testWidgets('Multiple selects in one widget', (WidgetTester tester) async {\n      final initialState = AppState(\n        user: User(name: 'Alice', age: 25, email: 'alice@example.com'),\n        counter: 0,\n        items: IList<String>(),\n        settings: Settings(darkMode: false, language: 'en'),\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int buildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                // Multiple selects in one build\n                final userName = context.select((st) => st.user.name);\n                final userAge = context.select((st) => st.user.age);\n                final isDarkMode = context.select((st) => st.settings.darkMode);\n\n                return Column(\n                  children: [\n                    Text('User: $userName, $userAge'),\n                    Text('Dark Mode: $isDarkMode'),\n                  ],\n                );\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(buildCount, 1);\n\n      // Change only counter (not selected) - should NOT rebuild\n      store.dispatch(IncrementCounterAction());\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 1);\n\n      // Change any selected value - should rebuild\n      store.dispatch(UpdateUserNameAction('Bob'));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 2);\n\n      // Change another selected value - should rebuild\n      store.dispatch(ToggleDarkModeAction());\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 3);\n    });\n\n    testWidgets('Complex computed values', (WidgetTester tester) async {\n      final initialState = AppState(\n        user: User(name: 'Alice', age: 17, email: 'alice@example.com'),\n        counter: 5,\n        items: IList<String>(['a', 'b', 'c']),\n        settings: Settings(darkMode: false, language: 'en'),\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int buildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                // Select computed summary.\n                final summary = context.select((st) => {\n                  'isAdult': st.user.age >= 18,\n                  'hasMany': st.items.length > 5,\n                  'score': st.counter * st.items.length,\n                });\n\n                return Text('Adult: ${summary['isAdult']}, Many: ${summary['hasMany']}, Score: ${summary['score']}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(buildCount, 1);\n      expect(find.text('Adult: false, Many: false, Score: 15'), findsOneWidget);\n\n      // Change age but still minor - computed value same, should NOT rebuild\n      store.dispatch(UpdateUserAgeAction(16));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 1);\n\n      // Change age to adult - computed value changes, should rebuild\n      store.dispatch(UpdateUserAgeAction(18));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 2);\n      expect(find.text('Adult: true, Many: false, Score: 15'), findsOneWidget);\n\n      // Add items to change score\n      store.dispatch(AddItemAction('d'));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 3);\n      expect(find.text('Adult: true, Many: false, Score: 20'), findsOneWidget);\n    });\n\n    testWidgets('Selector clearing between builds', (WidgetTester tester) async {\n      final initialState = AppState(\n        user: User(name: 'Alice', age: 25, email: 'alice@example.com'),\n        counter: 0,\n        items: IList<String>(),\n        settings: Settings(darkMode: false, language: 'en'),\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      bool showAge = true;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: StatefulBuilder(\n                builder: (context, setState) {\n                  return Column(\n                    children: [\n                      if (showAge)\n                        Builder(builder: (context) {\n                          final userAge = context.select((st) => st.user.age);\n                          return Text('Age: $userAge');\n                        })\n                      else\n                        Builder(builder: (context) {\n                          final userName = context.select((st) => st.user.name);\n                          return Text('Name: $userName');\n                        }),\n                      ElevatedButton(\n                        onPressed: () => setState(() => showAge = !showAge),\n                        child: const Text('Toggle'),\n                      ),\n                    ],\n                  );\n                },\n              ),\n            ),\n          ),\n        ),\n      );\n\n      expect(find.text('Age: 25'), findsOneWidget);\n      expect(find.text('Name: Alice'), findsNothing);\n\n      // Toggle to show name\n      await tester.tap(find.text('Toggle'));\n      await tester.pump();\n      await tester.pump(); // Extra pump for microtask\n\n      expect(find.text('Age: 25'), findsNothing);\n      expect(find.text('Name: Alice'), findsOneWidget);\n\n      // Change age (should not trigger rebuild since we're now selecting name)\n      store.dispatch(UpdateUserAgeAction(30));\n      await tester.pump();\n      expect(find.text('Name: Alice'), findsOneWidget); // Still showing name\n\n      // Change name (should trigger rebuild)\n      store.dispatch(UpdateUserNameAction('Bob'));\n      await tester.pump();\n      expect(find.text('Name: Bob'), findsOneWidget);\n    });\n\n    testWidgets('Comparing to regular state access', (WidgetTester tester) async {\n      final initialState = AppState(\n        user: User(name: 'Alice', age: 25, email: 'alice@example.com'),\n        counter: 0,\n        items: IList<String>(),\n        settings: Settings(darkMode: false, language: 'en'),\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int regularBuildCount = 0;\n      int selectBuildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Column(\n                children: [\n                  // Widget using regular state access\n                  Builder(builder: (context) {\n                    regularBuildCount++;\n                    final state = context.state;\n                    return Text('Regular: ${state.user.name}');\n                  }),\n                  // Widget using select.\n                  Builder(builder: (context) {\n                    selectBuildCount++;\n                    final userName = context.select((st) => st.user.name);\n                    return Text('Select: $userName');\n                  }),\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n\n      expect(regularBuildCount, 1);\n      expect(selectBuildCount, 1);\n\n      // Change unrelated state - regular rebuilds, select doesn't\n      store.dispatch(IncrementCounterAction());\n      await tester.pump();\n      expect(regularBuildCount, 2); // Rebuilt\n      expect(selectBuildCount, 1); // Not rebuilt\n\n      // Change selected state - both rebuild\n      store.dispatch(UpdateUserNameAction('Bob'));\n      await tester.pump();\n      await tester.pump();\n      expect(regularBuildCount, 3); // Rebuilt\n      expect(selectBuildCount, 2); // Rebuilt\n    });\n  });\n\n  group('Error handling and edge cases', () {\n    testWidgets('Using select outside build method throws error', (WidgetTester tester) async {\n      final initialState = AppState(\n        user: User(name: 'Alice', age: 25, email: 'alice@example.com'),\n        counter: 0,\n        items: IList<String>(),\n        settings: Settings(darkMode: false, language: 'en'),\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(\n                builder: (context) {\n                  return ElevatedButton(\n                    onPressed: () {\n                      // This should throw an error.\n                      expect(\n                        () => context.select((st) => st.user.name),\n                        throwsA(isA<FlutterError>()),\n                      );\n                    },\n                    child: const Text('Click me'),\n                  );\n                },\n              ),\n            ),\n          ),\n        ),\n      );\n\n      await tester.tap(find.text('Click me'));\n    });\n\n    testWidgets('Nested select calls throw error', (WidgetTester tester) async {\n      final initialState = AppState(\n        user: User(name: 'Alice', age: 25, email: 'alice@example.com'),\n        counter: 0,\n        items: IList<String>(),\n        settings: Settings(darkMode: false, language: 'en'),\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      bool errorThrown = false;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(\n                builder: (context) {\n                  try {\n                    context.select((st) {\n                      // Nested select - should throw.\n                      context.select((s) => s.counter);\n                      return st.user.name;\n                    });\n                  } catch (e) {\n                    errorThrown = true;\n                    return const Text('Error caught');\n                  }\n                  return const Text('No error');\n                },\n              ),\n            ),\n          ),\n        ),\n      );\n\n      expect(errorThrown, true);\n      expect(find.text('Error caught'), findsOneWidget);\n    });\n  });\n}\n\n// Recommended to create this extension.\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n  AppState read() => getRead<AppState>();\n  R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector);\n  R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);\n}\n\n// Test state classes\nclass AppState {\n  final User user;\n  final int counter;\n  final IList<String> items;\n  final Settings settings;\n\n  AppState({\n    required this.user,\n    required this.counter,\n    required this.items,\n    required this.settings,\n  });\n\n  AppState copyWith({\n    User? user,\n    int? counter,\n    IList<String>? items,\n    Settings? settings,\n  }) {\n    return AppState(\n      user: user ?? this.user,\n      counter: counter ?? this.counter,\n      items: items ?? this.items,\n      settings: settings ?? this.settings,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is AppState &&\n        other.user == user &&\n        other.counter == counter &&\n        other.items == items &&\n        other.settings == settings;\n  }\n\n  @override\n  int get hashCode => Object.hash(user, counter, items, settings);\n}\n\nclass User {\n  final String name;\n  final int age;\n  final String email;\n\n  User({required this.name, required this.age, required this.email});\n\n  User copyWith({String? name, int? age, String? email}) {\n    return User(\n      name: name ?? this.name,\n      age: age ?? this.age,\n      email: email ?? this.email,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is User &&\n        other.name == name &&\n        other.age == age &&\n        other.email == email;\n  }\n\n  @override\n  int get hashCode => Object.hash(name, age, email);\n}\n\nclass Settings {\n  final bool darkMode;\n  final String language;\n\n  Settings({required this.darkMode, required this.language});\n\n  Settings copyWith({bool? darkMode, String? language}) {\n    return Settings(\n      darkMode: darkMode ?? this.darkMode,\n      language: language ?? this.language,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is Settings &&\n        other.darkMode == darkMode &&\n        other.language == language;\n  }\n\n  @override\n  int get hashCode => Object.hash(darkMode, language);\n}\n\n// Test actions\nclass UpdateUserNameAction extends ReduxAction<AppState> {\n  final String name;\n  UpdateUserNameAction(this.name);\n\n  @override\n  AppState reduce() {\n    return state.copyWith(user: state.user.copyWith(name: name));\n  }\n}\n\nclass UpdateUserAgeAction extends ReduxAction<AppState> {\n  final int age;\n  UpdateUserAgeAction(this.age);\n\n  @override\n  AppState reduce() {\n    return state.copyWith(user: state.user.copyWith(age: age));\n  }\n}\n\nclass IncrementCounterAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(counter: state.counter + 1);\n  }\n}\n\nclass AddItemAction extends ReduxAction<AppState> {\n  final String item;\n  AddItemAction(this.item);\n\n  @override\n  AppState reduce() {\n    return state.copyWith(items: state.items.add(item));\n  }\n}\n\nclass ToggleDarkModeAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(\n      settings: state.settings.copyWith(darkMode: !state.settings.darkMode),\n    );\n  }\n}\n\n// Test widgets\nclass SelectTestWidget extends StatefulWidget {\n  final Store<AppState> store;\n  final int Function()? onBuildCounter;\n\n  const SelectTestWidget({\n    Key? key,\n    required this.store,\n    this.onBuildCounter,\n  }) : super(key: key);\n\n  @override\n  State<SelectTestWidget> createState() => _SelectTestWidgetState();\n}\n\nclass _SelectTestWidgetState extends State<SelectTestWidget> {\n  int buildCount = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    buildCount++;\n    widget.onBuildCounter?.call();\n\n    return StoreProvider<AppState>(\n      store: widget.store,\n      child: Builder(\n        builder: (context) {\n          return Column(\n            children: [\n              UserNameWidget(\n                key: const Key('userName'),\n                onBuild: widget.onBuildCounter,\n              ),\n              UserAgeWidget(\n                key: const Key('userAge'),\n                onBuild: widget.onBuildCounter,\n              ),\n              CounterWidget(\n                key: const Key('counter'),\n                onBuild: widget.onBuildCounter,\n              ),\n              ItemsCountWidget(\n                key: const Key('itemsCount'),\n                onBuild: widget.onBuildCounter,\n              ),\n              DarkModeWidget(\n                key: const Key('darkMode'),\n                onBuild: widget.onBuildCounter,\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass UserNameWidget extends StatelessWidget {\n  final Function()? onBuild;\n\n  const UserNameWidget({Key? key, this.onBuild}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    onBuild?.call();\n    final userName = context.select((st) => st.user.name);\n    return Text('Name: $userName');\n  }\n}\n\nclass UserAgeWidget extends StatelessWidget {\n  final Function()? onBuild;\n\n  const UserAgeWidget({Key? key, this.onBuild}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    onBuild?.call();\n    final userAge = context.select((st) => st.user.age);\n    return Text('Age: $userAge');\n  }\n}\n\nclass CounterWidget extends StatelessWidget {\n  final Function()? onBuild;\n\n  const CounterWidget({Key? key, this.onBuild}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    onBuild?.call();\n    final counter = context.select((st) => st.counter);\n    return Text('Counter: $counter');\n  }\n}\n\nclass ItemsCountWidget extends StatelessWidget {\n  final Function()? onBuild;\n\n  const ItemsCountWidget({Key? key, this.onBuild}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    onBuild?.call();\n    final itemCount = context.select((st) => st.items.length);\n    return Text('Items: $itemCount');\n  }\n}\n\nclass DarkModeWidget extends StatelessWidget {\n  final Function()? onBuild;\n\n  const DarkModeWidget({Key? key, this.onBuild}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    onBuild?.call();\n    final isDarkMode = context.select((st) => st.settings.darkMode);\n    return Text('Dark Mode: $isDarkMode');\n  }\n}\n\n// Widget that uses multiple selects\nclass MultiSelectWidget extends StatelessWidget {\n  final Function()? onBuild;\n\n  const MultiSelectWidget({Key? key, this.onBuild}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    onBuild?.call();\n\n    // Multiple selects in one build\n    final userName = context.select((st) => st.user.name);\n    final userAge = context.select((st) => st.user.age);\n    final isDarkMode = context.select((st) => st.settings.darkMode);\n\n    return Column(\n      children: [\n        Text('User: $userName, $userAge'),\n        Text('Dark Mode: $isDarkMode'),\n      ],\n    );\n  }\n}\n\n// Widget that selects complex computed values\nclass ComputedSelectWidget extends StatelessWidget {\n  final Function()? onBuild;\n\n  const ComputedSelectWidget({Key? key, this.onBuild}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    onBuild?.call();\n\n    // Select computed/derived values\n    final summary = context.select((st) => {\n      'userName': st.user.name,\n      'itemCount': st.items.length,\n      'isAdult': st.user.age >= 18,\n    });\n\n    return Text('Summary: $summary');\n  }\n}\n\n// Widget that selects lists\nclass ListSelectWidget extends StatelessWidget {\n  final Function()? onBuild;\n\n  const ListSelectWidget({Key? key, this.onBuild}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    onBuild?.call();\n\n    // Select filtered list\n    final longItems = context.select(\n          (state) => state.items.where((item) => item.length > 5).toList(),\n    );\n\n    return Text('Long items: ${longItems.join(', ')}');\n  }\n}\n"
  },
  {
    "path": "test/context_select_test.dart",
    "content": "// Exploratory test to debug the select() rebuild issue\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Select Debug Investigation', () {\n    testWidgets('Track dependency lifecycle and rebuild behavior',\n        (WidgetTester tester) async {\n      //\n      print('\\n' + '=' * 80);\n      print('STARTING SELECT DEBUG TEST');\n      print('=' * 80 + '\\n');\n\n      // Create initial state\n      final initialState = TestState(counter: 0, text: 'hello', flag: false);\n      final store = Store<TestState>(initialState: initialState);\n\n      // Track builds\n      final counterBuilds = <String>[];\n      final flagBuilds = <String>[];\n      final regularBuilds = <String>[];\n\n      print('>>> INITIAL WIDGET TREE BUILD\\n');\n\n      // Build the widget tree\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<TestState>(\n            store: store,\n            child: Scaffold(\n              body: Column(\n                children: [\n                  CounterSelectWidget(buildLog: counterBuilds),\n                  FlagSelectWidget(buildLog: flagBuilds),\n                  RegularStateWidget(buildLog: regularBuilds),\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n\n      await tester.pump();\n      await tester.pump();\n\n      print('\\n>>> INITIAL BUILD COMPLETE');\n      print('Counter builds: ${counterBuilds.length}');\n      print('Flag builds: ${flagBuilds.length}');\n      print('Regular builds: ${regularBuilds.length}');\n\n      expect(counterBuilds.length, 1);\n      expect(flagBuilds.length, 1);\n      expect(regularBuilds.length, 1);\n\n      // Clear build logs for next phase\n      counterBuilds.clear();\n      flagBuilds.clear();\n      regularBuilds.clear();\n\n      // ---------------\n\n      print('\\n' + '-' * 80);\n      print('>>> ACTION 1: INCREMENT COUNTER');\n      print('-' * 80 + '\\n');\n\n      // Dispatch increment action\n      store.dispatch(IncrementAction());\n      await tester.pump();\n      await tester.pump();\n\n      print('\\n>>> AFTER INCREMENT:');\n      print(\n          'Counter rebuilds: ${counterBuilds.length} ${counterBuilds.isNotEmpty ? \"✓\" : \"✗\"}');\n      print(\n          'Flag rebuilds: ${flagBuilds.length} ${flagBuilds.isEmpty ? \"✓\" : \"✗ UNEXPECTED!\"}');\n      print(\n          'Regular rebuilds: ${regularBuilds.length} ${regularBuilds.isNotEmpty ? \"✓\" : \"✗\"}');\n\n      expect(counterBuilds.length, 1);\n      expect(flagBuilds.length, 0);\n      expect(regularBuilds.length, 1);\n\n      // Clear for next action\n      counterBuilds.clear();\n      flagBuilds.clear();\n      regularBuilds.clear();\n\n      // ---------------\n\n      print('\\n' + '-' * 80);\n      print('>>> ACTION 2: TOGGLE FLAG');\n      print('-' * 80 + '\\n');\n\n      // Dispatch toggle action\n      store.dispatch(ToggleFlagAction());\n      await tester.pump();\n      await tester.pump();\n\n      print('\\n>>> AFTER TOGGLE:');\n      print(\n          'Counter rebuilds: ${counterBuilds.length} ${counterBuilds.isEmpty ? \"✓\" : \"✗ UNEXPECTED!\"}');\n      print(\n          'Flag rebuilds: ${flagBuilds.length} ${flagBuilds.isNotEmpty ? \"✓\" : \"✗\"}');\n      print(\n          'Regular rebuilds: ${regularBuilds.length} ${regularBuilds.isNotEmpty ? \"✓\" : \"✗\"}');\n\n      expect(counterBuilds.length, 0);\n      expect(flagBuilds.length, 1);\n      expect(regularBuilds.length, 1);\n\n      // Clear for next action\n      counterBuilds.clear();\n      flagBuilds.clear();\n      regularBuilds.clear();\n\n      // ---------------\n\n      print('\\n' + '-' * 80);\n      print('>>> ACTION 3: INCREMENT AGAIN');\n      print('-' * 80 + '\\n');\n\n      // Dispatch another increment\n      store.dispatch(IncrementAction());\n      await tester.pump();\n      await tester.pump();\n\n      print('\\n>>> AFTER SECOND INCREMENT:');\n      print(\n          'Counter rebuilds: ${counterBuilds.length} ${counterBuilds.isNotEmpty ? \"✓\" : \"✗\"}');\n      print(\n          'Flag rebuilds: ${flagBuilds.length} ${flagBuilds.isEmpty ? \"✓\" : \"✗ UNEXPECTED!\"}');\n      print(\n          'Regular rebuilds: ${regularBuilds.length} ${regularBuilds.isNotEmpty ? \"✓\" : \"✗\"}');\n\n      expect(counterBuilds.length, 1);\n      expect(flagBuilds.length, 0);\n      expect(regularBuilds.length, 1);\n\n      // ---------------\n\n      print('\\n' + '-' * 80);\n      print('TEST COMPLETE');\n      print('-' * 80 + '\\n');\n    });\n\n    testWidgets('Select works with inline Builder widgets',\n        (WidgetTester tester) async {\n      // Enable debug logging\n      print('\\n' + '=' * 80);\n      print('TESTING DIFFERENT WIDGET PATTERNS');\n      print('=' * 80 + '\\n');\n\n      final initialState = TestState(counter: 0, text: 'hello', flag: false);\n      final store = Store<TestState>(initialState: initialState);\n\n      // Track builds\n      final counterBuilds = <String>[];\n      final flagBuilds = <String>[];\n\n      // Test with Builder widgets\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<TestState>(\n            store: store,\n            child: Scaffold(\n              body: Column(\n                children: [\n                  // Pattern 1: Direct widget\n                  Builder(\n                    builder: (context) {\n                      counterBuilds.add('counter');\n                      print('[BUILDER 1] Building counter selector');\n                      final counter = context.select((s) => s.counter);\n                      return Text('Direct: $counter');\n                    },\n                  ),\n                  // Pattern 2: Wrapped in another Builder\n                  Builder(\n                    builder: (context) => Builder(\n                      builder: (context) {\n                        flagBuilds.add('flag');\n                        print('[BUILDER 2] Building flag selector');\n                        final flag = context.select((s) => s.flag);\n                        return Text('Nested: $flag');\n                      },\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n\n      await tester.pump();\n      await tester.pump();\n\n      // Initial build\n      expect(counterBuilds.length, 1);\n      expect(flagBuilds.length, 1);\n\n      counterBuilds.clear();\n      flagBuilds.clear();\n\n      print('\\n>>> DISPATCHING INCREMENT IN BUILDER TEST');\n      store.dispatch(IncrementAction());\n      await tester.pump();\n      await tester.pump();\n\n      // Only counter should rebuild\n      expect(counterBuilds.length, 1);\n      expect(flagBuilds.length, 0);\n\n      counterBuilds.clear();\n      flagBuilds.clear();\n\n      print('\\n>>> DISPATCHING TOGGLE IN BUILDER TEST');\n      store.dispatch(ToggleFlagAction());\n\n      await tester.pump();\n      await tester.pump();\n\n      // Only flag should rebuild\n      expect(counterBuilds.length, 0);\n      expect(flagBuilds.length, 1);\n\n      print('\\n' + '-' * 80);\n      print('BUILDER TEST COMPLETE');\n      print('-' * 80 + '\\n');\n    });\n  });\n}\n\n// Recommended to create this extension.\nextension BuildContextExtension on BuildContext {\n  R select<R>(R Function(TestState state) selector) =>\n      getSelect<TestState, R>(selector);\n}\n\n// Simple test state\nclass TestState {\n  final int counter;\n  final String text;\n  final bool flag;\n\n  TestState({\n    required this.counter,\n    required this.text,\n    required this.flag,\n  });\n\n  TestState copyWith({\n    int? counter,\n    String? text,\n    bool? flag,\n  }) {\n    return TestState(\n      counter: counter ?? this.counter,\n      text: text ?? this.text,\n      flag: flag ?? this.flag,\n    );\n  }\n\n  @override\n  String toString() => 'TestState(counter: $counter, text: $text, flag: $flag)';\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is TestState &&\n        other.counter == counter &&\n        other.text == text &&\n        other.flag == flag;\n  }\n\n  @override\n  int get hashCode => Object.hash(counter, text, flag);\n}\n\n// Test actions -------------------\n\nclass IncrementAction extends ReduxAction<TestState> {\n  @override\n  TestState reduce() => state.copyWith(counter: state.counter + 1);\n}\n\nclass ChangeTextAction extends ReduxAction<TestState> {\n  final String text;\n\n  ChangeTextAction(this.text);\n\n  @override\n  TestState reduce() => state.copyWith(text: text);\n}\n\nclass ToggleFlagAction extends ReduxAction<TestState> {\n  @override\n  TestState reduce() => state.copyWith(flag: !state.flag);\n}\n\n// Test widgets with tracking -------------------\n\nclass CounterSelectWidget extends StatelessWidget {\n  final List<String> buildLog;\n\n  const CounterSelectWidget({Key? key, required this.buildLog})\n      : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    buildLog.add('CounterSelectWidget.build()');\n    print('\\n=== CounterSelectWidget BUILD ===');\n\n    final counter = context.select((st) {\n      print('  [Selector executing] Selecting counter: ${st.counter}');\n      return st.counter;\n    });\n\n    print('  Selected value: $counter');\n    print('=== CounterSelectWidget BUILD END ===\\n');\n\n    return Text('Counter: $counter');\n  }\n}\n\nclass FlagSelectWidget extends StatelessWidget {\n  final List<String> buildLog;\n\n  const FlagSelectWidget({Key? key, required this.buildLog}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    buildLog.add('FlagSelectWidget.build()');\n    print('\\n=== FlagSelectWidget BUILD ===');\n\n    final flag = context.select((st) {\n      print('  [Selector executing] Selecting flag: ${st.flag}');\n      return st.flag;\n    });\n\n    print('  Selected value: $flag');\n    print('=== FlagSelectWidget BUILD END ===\\n');\n\n    return Text('Flag: $flag');\n  }\n}\n\nclass RegularStateWidget extends StatelessWidget {\n  final List<String> buildLog;\n\n  const RegularStateWidget({Key? key, required this.buildLog})\n      : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    buildLog.add('RegularStateWidget.build()');\n    print('\\n=== RegularStateWidget BUILD ===');\n\n    final state = context.getState<TestState>();\n\n    print('  Got state: $state');\n    print('=== RegularStateWidget BUILD END ===\\n');\n\n    return Text('State: $state');\n  }\n}\n"
  },
  {
    "path": "test/context_state_test.dart",
    "content": "// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('AsyncRedux context.state functionality', () {\n    testWidgets('context.state rebuilds on any state change',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        name: 'Alice',\n        counter: 0,\n        flag: false,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int buildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                var state = context.state;\n                return Text('Name: ${state.name}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(buildCount, 1);\n      expect(find.text('Name: Alice'), findsOneWidget);\n\n      // Change name - should rebuild\n      store.dispatch(ChangeNameAction('Bob'));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 2);\n      expect(find.text('Name: Bob'), findsOneWidget);\n\n      // Change counter (unrelated to displayed value) - should still rebuild\n      store.dispatch(IncrementCounterAction());\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 3);\n\n      // Change flag (also unrelated) - should still rebuild\n      store.dispatch(ToggleFlagAction());\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 4);\n    });\n\n    testWidgets(\n        'context.state always rebuilds even when accessing single field',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        name: 'Alice',\n        counter: 0,\n        flag: false,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int buildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                // Only accessing name, but using context.state\n                var name = context.state.name;\n                return Text('Name: $name');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(buildCount, 1);\n\n      // Change counter (not name) - should still rebuild because we used context.state\n      store.dispatch(IncrementCounterAction());\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 2);\n\n      // Compare with select - only rebuilds when selected value changes\n      // (This is tested in context_select_test.dart)\n    });\n  });\n\n  group('AsyncRedux context.read() functionality', () {\n    testWidgets('context.read() does not trigger rebuilds',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        name: 'Alice',\n        counter: 0,\n        flag: false,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int buildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                buildCount++;\n                var state = context.read();\n                return Text('Name: ${state.name}');\n              }),\n            ),\n          ),\n        ),\n      );\n\n      expect(buildCount, 1);\n      expect(find.text('Name: Alice'), findsOneWidget);\n\n      // Change name - should NOT rebuild\n      store.dispatch(ChangeNameAction('Bob'));\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 1);\n      // Still shows old value because widget didn't rebuild\n      expect(find.text('Name: Alice'), findsOneWidget);\n\n      // Change counter - should NOT rebuild\n      store.dispatch(IncrementCounterAction());\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 1);\n\n      // Change flag - should NOT rebuild\n      store.dispatch(ToggleFlagAction());\n      await tester.pump();\n      await tester.pump();\n      expect(buildCount, 1);\n    });\n\n    testWidgets('context.read() can be used in initState',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        name: 'Alice',\n        counter: 42,\n        flag: false,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int? capturedCounter;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: InitStateTestWidget(\n                onInitState: (context) {\n                  // This should work - reading state in initState\n                  capturedCounter = context.read().counter;\n                },\n              ),\n            ),\n          ),\n        ),\n      );\n\n      expect(capturedCounter, 42);\n    });\n\n    testWidgets('context.read() returns current state value',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        name: 'Alice',\n        counter: 0,\n        flag: false,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      List<String> readValues = [];\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                return ElevatedButton(\n                  onPressed: () {\n                    // Read current state on button press\n                    readValues.add(context.read().name);\n                  },\n                  child: const Text('Read'),\n                );\n              }),\n            ),\n          ),\n        ),\n      );\n\n      // Read initial value\n      await tester.tap(find.text('Read'));\n      expect(readValues, ['Alice']);\n\n      // Change state\n      store.dispatch(ChangeNameAction('Bob'));\n      await tester.pump();\n      await tester.pump();\n\n      // Read new value\n      await tester.tap(find.text('Read'));\n      expect(readValues, ['Alice', 'Bob']);\n\n      // Change again\n      store.dispatch(ChangeNameAction('Charlie'));\n      await tester.pump();\n      await tester.pump();\n\n      // Read newest value\n      await tester.tap(find.text('Read'));\n      expect(readValues, ['Alice', 'Bob', 'Charlie']);\n    });\n  });\n\n  group('context.state vs context.read() comparison', () {\n    testWidgets('state rebuilds, read does not', (WidgetTester tester) async {\n      final initialState = AppState(\n        name: 'Alice',\n        counter: 0,\n        flag: false,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int stateBuildCount = 0;\n      int readBuildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Column(\n                children: [\n                  // Widget using context.state\n                  Builder(builder: (context) {\n                    stateBuildCount++;\n                    var state = context.state;\n                    return Text('State: ${state.name}');\n                  }),\n                  // Widget using context.read()\n                  Builder(builder: (context) {\n                    readBuildCount++;\n                    var state = context.read();\n                    return Text('Read: ${state.name}');\n                  }),\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n\n      expect(stateBuildCount, 1);\n      expect(readBuildCount, 1);\n\n      // Change state\n      store.dispatch(ChangeNameAction('Bob'));\n      await tester.pump();\n      await tester.pump();\n\n      expect(stateBuildCount, 2); // Rebuilt\n      expect(readBuildCount, 1); // Not rebuilt\n\n      // Change again\n      store.dispatch(IncrementCounterAction());\n      await tester.pump();\n      await tester.pump();\n\n      expect(stateBuildCount, 3); // Rebuilt again\n      expect(readBuildCount, 1); // Still not rebuilt\n    });\n\n    testWidgets('Multiple dispatches - state rebuilds each time, read never',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        name: 'Alice',\n        counter: 0,\n        flag: false,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      int stateBuildCount = 0;\n      int readBuildCount = 0;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Column(\n                children: [\n                  Builder(builder: (context) {\n                    stateBuildCount++;\n                    return Text('Counter: ${context.state.counter}');\n                  }),\n                  Builder(builder: (context) {\n                    readBuildCount++;\n                    return Text('Read Counter: ${context.read().counter}');\n                  }),\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n\n      expect(stateBuildCount, 1);\n      expect(readBuildCount, 1);\n\n      // Dispatch 5 actions\n      for (int i = 0; i < 5; i++) {\n        store.dispatch(IncrementCounterAction());\n        await tester.pump();\n        await tester.pump();\n      }\n\n      expect(stateBuildCount, 6); // 1 initial + 5 rebuilds\n      expect(readBuildCount, 1); // Never rebuilt\n    });\n  });\n\n  group('Edge cases and error handling', () {\n    testWidgets('context.state throws when used in initState',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        name: 'Alice',\n        counter: 0,\n        flag: false,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      Object? caughtError;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: InitStateTestWidget(\n                onInitState: (context) {\n                  try {\n                    // This should throw - using context.state in initState\n                    context.state;\n                  } catch (e) {\n                    caughtError = e;\n                  }\n                },\n              ),\n            ),\n          ),\n        ),\n      );\n\n      // context.state should throw when used in initState\n      expect(caughtError, isNotNull);\n    });\n\n    testWidgets('context.read() works in callbacks',\n        (WidgetTester tester) async {\n      final initialState = AppState(\n        name: 'Alice',\n        counter: 0,\n        flag: false,\n      );\n\n      final store = Store<AppState>(initialState: initialState);\n      String? capturedName;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: store,\n            child: Scaffold(\n              body: Builder(builder: (context) {\n                return ElevatedButton(\n                  onPressed: () {\n                    capturedName = context.read().name;\n                  },\n                  child: const Text('Capture'),\n                );\n              }),\n            ),\n          ),\n        ),\n      );\n\n      // Change state before pressing button\n      store.dispatch(ChangeNameAction('Bob'));\n      await tester.pump();\n      await tester.pump();\n\n      // Now press button to read current state\n      await tester.tap(find.text('Capture'));\n      expect(capturedName, 'Bob');\n    });\n\n    testWidgets('context.state in nested StoreProvider uses correct store',\n        (WidgetTester tester) async {\n      final outerState = AppState(name: 'Outer', counter: 1, flag: false);\n      final innerState = AppState(name: 'Inner', counter: 2, flag: true);\n\n      final outerStore = Store<AppState>(initialState: outerState);\n      final innerStore = Store<AppState>(initialState: innerState);\n\n      String? outerName;\n      String? innerName;\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: StoreProvider<AppState>(\n            store: outerStore,\n            child: Column(\n              children: [\n                Builder(builder: (context) {\n                  outerName = context.state.name;\n                  return Text('Outer: $outerName');\n                }),\n                StoreProvider<AppState>(\n                  store: innerStore,\n                  child: Builder(builder: (context) {\n                    innerName = context.state.name;\n                    return Text('Inner: $innerName');\n                  }),\n                ),\n              ],\n            ),\n          ),\n        ),\n      );\n\n      expect(outerName, 'Outer');\n      expect(innerName, 'Inner');\n    });\n  });\n}\n\n// Extension for BuildContext\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n}\n\n// Test state class\nclass AppState {\n  final String name;\n  final int counter;\n  final bool flag;\n\n  AppState({\n    required this.name,\n    required this.counter,\n    required this.flag,\n  });\n\n  AppState copyWith({\n    String? name,\n    int? counter,\n    bool? flag,\n  }) {\n    return AppState(\n      name: name ?? this.name,\n      counter: counter ?? this.counter,\n      flag: flag ?? this.flag,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is AppState &&\n        other.name == name &&\n        other.counter == counter &&\n        other.flag == flag;\n  }\n\n  @override\n  int get hashCode => Object.hash(name, counter, flag);\n}\n\n// Test actions\nclass ChangeNameAction extends ReduxAction<AppState> {\n  final String name;\n\n  ChangeNameAction(this.name);\n\n  @override\n  AppState reduce() {\n    return state.copyWith(name: name);\n  }\n}\n\nclass IncrementCounterAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(counter: state.counter + 1);\n  }\n}\n\nclass ToggleFlagAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copyWith(flag: !state.flag);\n  }\n}\n\n// Widget to test initState behavior\nclass InitStateTestWidget extends StatefulWidget {\n  final void Function(BuildContext context) onInitState;\n\n  const InitStateTestWidget({\n    Key? key,\n    required this.onInitState,\n  }) : super(key: key);\n\n  @override\n  State<InitStateTestWidget> createState() => _InitStateTestWidgetState();\n}\n\nclass _InitStateTestWidgetState extends State<InitStateTestWidget> {\n  @override\n  void initState() {\n    super.initState();\n    widget.onInitState(context);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return const Text('InitState Test');\n  }\n}\n"
  },
  {
    "path": "test/debounce_mixin_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var feature = BddFeature('Debounce actions');\n\n  Bdd(feature)\n      .scenario(\n          'Sync action is debounced when dispatched multiple times quickly')\n      .given('A sync action with the Debounce mixin')\n      .when(\n          'The action is dispatched multiple times within the debounce period')\n      .then('It should only execute once after the debounce period')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n    store.dispatch(DebounceAction());\n    store.dispatch(DebounceAction());\n    store.dispatch(DebounceAction());\n    expect(store.state.count, 0);\n\n    // Wait for a bit more than the debounce period (150 ms).\n    await Future.delayed(const Duration(milliseconds: 150));\n    expect(store.state.count, 1);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Async action is debounced when dispatched multiple times quickly')\n      .given('An async action with the Debounce mixin')\n      .when(\n          'The action is dispatched multiple times within the debounce period')\n      .then('It should only execute once after the debounce period')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n    store.dispatch(DebounceActionAsync());\n    store.dispatch(DebounceActionAsync());\n    store.dispatch(DebounceActionAsync());\n    expect(store.state.count, 0);\n\n    // Wait for a bit more than the debounce period (150 ms).\n    await Future.delayed(const Duration(milliseconds: 150));\n    expect(store.state.count, 1);\n  });\n\n  Bdd(feature)\n      .scenario('A sync action executes again after debounce period expires')\n      .given('A sync action with the Debounce mixin')\n      .when('The action is dispatched, '\n          'then after waiting for the debounce period, dispatched again')\n      .then('Each dispatch should execute after the debounce period')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n    store.dispatch(DebounceAction());\n    expect(store.state.count, 0);\n\n    // Wait for a bit more than the debounce period (150 ms).\n    await Future.delayed(const Duration(milliseconds: 150));\n    expect(store.state.count, 1);\n\n    store.dispatch(DebounceAction());\n    expect(store.state.count, 1);\n\n    // Wait for a bit more than the debounce period (150 ms).\n    await Future.delayed(const Duration(milliseconds: 150));\n    expect(store.state.count, 2);\n  });\n\n  Bdd(feature)\n      .scenario('An async action executes again after debounce period expires')\n      .given('An async action with the Debounce mixin')\n      .when('The action is dispatched, '\n          'then after waiting for the debounce period, dispatched again')\n      .then('Each dispatch should execute after the debounce period')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n    store.dispatch(DebounceActionAsync());\n    expect(store.state.count, 0);\n\n    // Wait for a bit more than the debounce period (150 ms).\n    await Future.delayed(const Duration(milliseconds: 150));\n    expect(store.state.count, 1);\n\n    store.dispatch(DebounceActionAsync());\n    expect(store.state.count, 1);\n\n    // Wait for a bit more than the debounce period (150 ms).\n    await Future.delayed(const Duration(milliseconds: 150));\n    expect(store.state.count, 2);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Sync actions with different runtime types are not debounced together')\n      .given(\n          'Two sync actions with the Debounce mixin but different runtime types')\n      .when('Both actions are dispatched in quick succession')\n      .then(\n          'Each action should execute independently after their debounce periods')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n    store.dispatch(DebounceActionA());\n    store.dispatch(DebounceActionB());\n    expect(store.state.count, 0);\n\n    // The debounce period is 200ms.\n    // Wait for a bit more than that, but less than double that: 300ms.\n    await Future.delayed(const Duration(milliseconds: 300));\n    expect(store.state.count, 2);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Async actions with different runtime types are not debounced together')\n      .given(\n          'Two async actions with the Debounce mixin but different runtime types')\n      .when('Both actions are dispatched in quick succession')\n      .then(\n          'Each action should execute independently after their debounce periods')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n    store.dispatch(DebounceActionAAsync());\n    store.dispatch(DebounceActionBAsync());\n    expect(store.state.count, 0);\n\n    // The debounce period is 200ms.\n    // Wait for a bit more than that, but less than double that: 300ms.\n    await Future.delayed(const Duration(milliseconds: 300));\n    expect(store.state.count, 2);\n  });\n}\n\n// A simple state that holds a count.\nclass AppState {\n  final int count;\n\n  AppState(this.count);\n\n  AppState copy({int? count}) => AppState(count ?? this.count);\n\n  @override\n  String toString() => 'AppState($count)';\n}\n\n// An action that uses the Debounce mixin to increment the state.\nclass DebounceAction extends ReduxAction<AppState> with Debounce {\n  @override\n  int debounce = 100;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Two actions that override lockBuilder to return the same lock.\nclass DebounceAction1 extends ReduxAction<AppState> with Debounce {\n  @override\n  int debounce = 100;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass DebounceAction2 extends ReduxAction<AppState> with Debounce {\n  @override\n  int debounce = 100;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Two actions with default lock (their runtime types differ).\nclass DebounceActionA extends ReduxAction<AppState> with Debounce {\n  @override\n  int debounce = 200;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass DebounceActionB extends ReduxAction<AppState> with Debounce {\n  @override\n  int debounce = 200;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Async versions:\n\n// An action that uses the Debounce mixin to increment the state.\nclass DebounceActionAsync extends ReduxAction<AppState> with Debounce {\n  @override\n  int debounce = 100;\n\n  @override\n  Future<AppState> reduce() async {\n    await microtask;\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Two actions that override lockBuilder to return the same lock.\nclass DebounceAction1Async extends ReduxAction<AppState> with Debounce {\n  @override\n  int debounce = 100;\n\n  @override\n  Future<AppState> reduce() async {\n    await microtask;\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass DebounceAction2Async extends ReduxAction<AppState> with Debounce {\n  @override\n  int debounce = 100;\n\n  @override\n  Future<AppState> reduce() async {\n    await microtask;\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Two actions with default lock (their runtime types differ).\nclass DebounceActionAAsync extends ReduxAction<AppState> with Debounce {\n  @override\n  int debounce = 200;\n\n  @override\n  Future<AppState> reduce() async {\n    await microtask;\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass DebounceActionBAsync extends ReduxAction<AppState> with Debounce {\n  @override\n  int debounce = 200;\n\n  @override\n  Future<AppState> reduce() async {\n    await microtask;\n    return state.copy(count: state.count + 1);\n  }\n}\n"
  },
  {
    "path": "test/dispatch_and_wait_all_actions_test.dart",
    "content": "// File: test/dispatch_and_wait_all_actions_test.dart\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  //\n  test('Completes for a sync action', () async {\n    final store = Store<State>(initialState: State(1));\n    final status = await store.dispatchAndWaitAllActions(IncrementSync());\n    expect(status.isCompletedOk, true);\n    expect(store.state.count, 2);\n  });\n\n  test('Completes for an async action', () async {\n    final store = Store<State>(initialState: State(1));\n    final status = await store.dispatchAndWaitAllActions(IncrementAsync());\n    expect(status.isCompletedOk, true);\n    expect(store.state.count, 2);\n  });\n\n  test('Waits for nested async dispatch in reduce', () async {\n    var store = Store<State>(initialState: State(1));\n\n    var status =\n        await store.dispatchAndWaitAllActions(DispatchMultipleActions());\n\n    expect(status.isCompletedOk, true);\n\n    // 1 → DispatchMultipleActions.reduce → 2 → then IncrementAsync → 3\n    expect(store.state.count, 3);\n\n    // ---\n\n    // Compare it to a normal dispatchAndWait:\n\n    store = Store<State>(initialState: State(1));\n\n    status =\n    await store.dispatchAndWait(DispatchMultipleActions());\n\n    expect(status.isCompletedOk, true);\n\n    // 1 → DispatchMultipleActions.reduce → 2 → then IncrementAsync → 3\n    expect(store.state.count, 2);\n  });\n}\n\nclass State {\n  final int count;\n\n  State(this.count);\n}\n\nclass IncrementSync extends ReduxAction<State> {\n  @override\n  State reduce() => State(state.count + 1);\n}\n\nclass IncrementAsync extends ReduxAction<State> {\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    return State(state.count + 1);\n  }\n}\n\nclass DispatchMultipleActions extends ReduxAction<State> {\n  @override\n  Future<State> reduce() async {\n    // First, update the state synchronously.\n    final updated = State(state.count + 1);\n\n    // Then dispatch another async action.\n    dispatch(IncrementAsync());\n    \n    return updated;\n  }\n}\n"
  },
  {
    "path": "test/dispatch_and_wait_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var feature = BddFeature('Dispatch and wait');\n\n  Bdd(feature)\n      .scenario('Waiting for a dispatchAndWait to end.')\n      .given('A SYNC or ASYNC action.')\n      .when('The action is dispatched with `dispatchAndWait(action)`.')\n      .then('It returns a `Promise` that resolves when the action finishes.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n    await store.dispatch(IncrementSync());\n    expect(store.state.count, 2);\n\n    await store.dispatch(IncrementAsync());\n    expect(store.state.count, 3);\n  });\n\n  Bdd(feature)\n      .scenario('Knowing when some action dispatched with `dispatchAndWait` is being processed.')\n      .given('A SYNC or ASYNC action.')\n      .when('The action is dispatched.')\n      .then('We can check if the action is processing with `Store.isWaiting(actionType)`.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    // SYNC ACTION: isWaiting is always false.\n\n    expect(store.isWaiting(IncrementSync), false);\n    expect(store.state.count, 1);\n\n    var actionSync = IncrementSync();\n    expect(actionSync.status.isDispatched, false);\n    var promise1 = store.dispatch(actionSync);\n    expect(actionSync.status.isDispatched, true);\n\n    expect(store.isWaiting(IncrementSync), false);\n    expect(store.state.count, 2);\n\n    await promise1; // Since it's SYNC, it's already finished when dispatched.\n\n    expect(store.isWaiting(IncrementSync), false);\n    expect(store.state.count, 2);\n\n    // ASYNC ACTION: isWaiting is true while we wait for it to finish.\n\n    expect(store.isWaiting(IncrementAsync), false);\n    expect(store.state.count, 2);\n\n    var actionAsync = IncrementAsync();\n    expect(actionAsync.status.isDispatched, false);\n\n    var promise2 = store.dispatch(actionAsync);\n    expect(actionAsync.status.isDispatched, true);\n\n    expect(store.isWaiting(IncrementAsync), true); // True!\n    expect(store.state.count, 2);\n\n    await promise2; // Since it's ASYNC, it really waits until it finishes.\n\n    expect(store.isWaiting(IncrementAsync), false);\n    expect(store.state.count, 3);\n  });\n\n  Bdd(feature)\n      .scenario('Reading the ActionStatus of the action.')\n      .given('A SYNC or ASYNC action.')\n      .when('The action is dispatched.')\n      .and('The action finishes without any errors.')\n      .then('We can check the action status, which says the action completed OK (no errors).')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    // SYNC ACTION\n    var actionSync = IncrementSync();\n    var status = actionSync.status;\n\n    expect(status.isDispatched, false);\n    expect(status.hasFinishedMethodBefore, false);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, false);\n    expect(status.isCompleted, false);\n    expect(status.isCompletedOk, false);\n    expect(status.isCompletedFailed, false);\n\n    status = await store.dispatchAndWait(actionSync);\n\n    expect(status, actionSync.status);\n    expect(status.isDispatched, true);\n    expect(status.hasFinishedMethodBefore, true);\n    expect(status.hasFinishedMethodReduce, true);\n    expect(status.hasFinishedMethodAfter, true); // After is like a \"finally\" block. It always runs.\n    expect(status.isCompleted, true);\n    expect(status.isCompletedOk, true);\n    expect(status.isCompletedFailed, false);\n\n    // ASYNC ACTION\n    var actionAsync = IncrementAsync();\n    status = actionAsync.status;\n\n    expect(status.isDispatched, false);\n    expect(status.hasFinishedMethodBefore, false);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, false);\n    expect(status.isCompleted, false);\n    expect(status.isCompletedOk, false);\n    expect(status.isCompletedFailed, false);\n\n    status = await store.dispatchAndWait(actionAsync);\n\n    expect(status, actionAsync.status);\n    expect(status.isDispatched, true);\n    expect(status.hasFinishedMethodBefore, true);\n    expect(status.hasFinishedMethodReduce, true);\n    expect(status.hasFinishedMethodAfter, true); // After is like a \"finally\" block. It always runs.\n    expect(status.isCompleted, true);\n    expect(status.isCompletedOk, true);\n    expect(status.isCompletedFailed, false);\n  });\n\n  Bdd(feature)\n      .scenario('Reading the ActionStatus of the action.')\n      .given('A SYNC or ASYNC action.')\n      .when('The action is dispatched.')\n      .and('The action fails in the \"before\" method.')\n      .then('We can check the action status, which says the action completed with errors.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    // SYNC ACTION\n    var actionSync = IncrementSyncBeforeFails();\n    var status = actionSync.status;\n\n    expect(status.isDispatched, false);\n    expect(status.hasFinishedMethodBefore, false);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, false);\n    expect(status.isCompleted, false);\n\n    status = await store.dispatchAndWait(actionSync);\n\n    expect(status, actionSync.status);\n    expect(status.isDispatched, true);\n    expect(status.hasFinishedMethodBefore, false);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, true); // After is like a \"finally\" block. It always runs.\n    expect(status.isCompleted, true);\n    expect(status.isCompletedOk, false);\n    expect(status.isCompletedFailed, true);\n\n    // ASYNC ACTION\n    var actionAsync = IncrementAsyncBeforeFails();\n    status = actionAsync.status;\n\n    expect(status.isDispatched, false);\n    expect(status.hasFinishedMethodBefore, false);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, false);\n    expect(status.isCompleted, false);\n\n    status = await store.dispatchAndWait(actionAsync);\n\n    expect(status, actionAsync.status);\n    expect(status.isDispatched, true);\n    expect(status.hasFinishedMethodBefore, false);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, true); // After is like a \"finally\" block. It always runs.\n    expect(status.isCompleted, true);\n    expect(status.isCompletedOk, false);\n    expect(status.isCompletedFailed, true);\n  });\n\n  Bdd(feature)\n      .scenario('Reading the ActionStatus of the action.')\n      .given('A SYNC or ASYNC action.')\n      .when('The action is dispatched.')\n      .and('The action fails in the \"reduce\" method.')\n      .then('We can check the action status, which says the action completed with errors.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    // SYNC ACTION\n    var actionSync = IncrementSyncReduceFails();\n    var status = actionSync.status;\n\n    expect(status.isDispatched, false);\n    expect(status.hasFinishedMethodBefore, false);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, false);\n    expect(status.isCompleted, false);\n\n    status = await store.dispatchAndWait(actionSync);\n\n    expect(status, actionSync.status);\n    expect(status.isDispatched, true);\n    expect(status.hasFinishedMethodBefore, true);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, true); // After is like a \"finally\" block. It always runs.\n    expect(status.isCompleted, true);\n    expect(status.isCompletedOk, false);\n    expect(status.isCompletedFailed, true);\n\n    // ASYNC ACTION\n    var actionAsync = IncrementAsyncReduceFails();\n    status = actionAsync.status;\n\n    expect(status.isDispatched, false);\n    expect(status.hasFinishedMethodBefore, false);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, false);\n    expect(status.isCompleted, false);\n\n    status = await store.dispatchAndWait(actionAsync);\n\n    expect(status, actionAsync.status);\n    expect(status.isDispatched, true);\n    expect(status.hasFinishedMethodBefore, true);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, true); // After is like a \"finally\" block. It always runs.\n    expect(status.isCompleted, true);\n    expect(status.isCompletedOk, false);\n    expect(status.isCompletedFailed, true);\n  });\n\n  Bdd(feature)\n      .scenario('Reading the ActionStatus of the action.')\n      .given('A SYNC or ASYNC action.')\n      .when('The action is dispatched.')\n      .and('The action fails in the \"reduce\" method.')\n      .then('We can check the action status, which says the action completed with errors.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    // SYNC ACTION\n    var actionSync = IncrementSyncReduceFails();\n    var status = actionSync.status;\n\n    expect(status.isDispatched, false);\n    expect(status.hasFinishedMethodBefore, false);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, false);\n    expect(status.isCompleted, false);\n\n    status = await store.dispatchAndWait(actionSync);\n\n    expect(status, actionSync.status);\n    expect(status.isDispatched, true);\n    expect(status.hasFinishedMethodBefore, true);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, true); // After is like a \"finally\" block. It always runs.\n    expect(status.isCompleted, true);\n    expect(status.isCompletedOk, false);\n    expect(status.isCompletedFailed, true);\n\n    // ASYNC ACTION\n    var actionAsync = IncrementAsyncReduceFails();\n    status = actionAsync.status;\n\n    expect(status.isDispatched, false);\n    expect(status.hasFinishedMethodBefore, false);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, false);\n    expect(status.isCompleted, false);\n\n    status = await store.dispatchAndWait(actionAsync);\n\n    expect(status, actionAsync.status);\n    expect(status.isDispatched, true);\n    expect(status.hasFinishedMethodBefore, true);\n    expect(status.hasFinishedMethodReduce, false);\n    expect(status.hasFinishedMethodAfter, true); // After is like a \"finally\" block. It always runs.\n    expect(status.isCompleted, true);\n    expect(status.isCompletedOk, false);\n    expect(status.isCompletedFailed, true);\n  });\n}\n\nclass State {\n  final int count;\n\n  State(this.count);\n}\n\nclass IncrementSync extends ReduxAction<State> {\n  @override\n  State reduce() {\n    return State(state.count + 1);\n  }\n}\n\nclass IncrementAsync extends ReduxAction<State> {\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    return State(state.count + 1);\n  }\n}\n\nclass IncrementSyncBeforeFails extends ReduxAction<State> {\n  @override\n  void before() {\n    throw const UserException('Before failed');\n  }\n\n  @override\n  State reduce() {\n    return State(state.count + 1);\n  }\n}\n\nclass IncrementSyncReduceFails extends ReduxAction<State> {\n  @override\n  State reduce() {\n    throw const UserException('Reduce failed');\n  }\n}\n\nclass IncrementSyncAfterFails extends ReduxAction<State> {\n  @override\n  State reduce() {\n    return State(state.count + 1);\n  }\n\n  @override\n  void after() {\n    throw const UserException('After failed');\n  }\n}\n\nclass IncrementAsyncBeforeFails extends ReduxAction<State> {\n  @override\n  Future<void> before() async {\n    throw const UserException('Before failed');\n  }\n\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    return State(state.count + 1);\n  }\n}\n\nclass IncrementAsyncReduceFails extends ReduxAction<State> {\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    throw const UserException('Reduce failed');\n  }\n}\n\nclass IncrementAsyncAfterFails extends ReduxAction<State> {\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    return State(state.count + 1);\n  }\n\n  @override\n  Future<void> after() async {\n    throw const UserException('After failed');\n  }\n}\n"
  },
  {
    "path": "test/dispatch_sync_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var feature = BddFeature('DispatchSync');\n\n  Bdd(feature)\n      .scenario('DispatchSync only dispatches SYNC actions.')\n      .given('A SYNC or ASYNC action.')\n      .when('The action is dispatched with `dispatchSync(action)`.')\n      .then('It throws a `StoreException` when the action is ASYNC.')\n      .and('It fails synchronously.')\n      .note('We have to separately test with async \"before\", async \"reduce\", '\n          'and both \"before\" and \"reduce\" being async, because they fail in different ways.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    // Works\n    store.dispatchSync(IncrementSync());\n\n    // Fails synchronously with a `StoreException`.\n    expect(() => store.dispatchSync(IncrementAsyncBefore()), throwsA(isA<StoreException>()));\n    expect(() => store.dispatchSync(IncrementAsyncReduce()), throwsA(isA<StoreException>()));\n    expect(() => store.dispatchSync(IncrementAsyncBeforeReduce()), throwsA(isA<StoreException>()));\n  });\n}\n\nclass State {\n  final int count;\n\n  State(this.count);\n}\n\nclass IncrementSync extends ReduxAction<State> {\n  @override\n  State reduce() => State(state.count + 1);\n}\n\nclass IncrementAsyncBefore extends ReduxAction<State> {\n  @override\n  Future<void> before() async {\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n\n  @override\n  State reduce() => State(state.count + 1);\n}\n\nclass IncrementAsyncReduce extends ReduxAction<State> {\n  @override\n  Future<State> reduce() async {\n    return State(state.count + 1);\n  }\n}\n\nclass IncrementAsyncBeforeReduce extends ReduxAction<State> {\n  @override\n  Future<void> before() async {\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n\n  @override\n  Future<State> reduce() async {\n    return State(state.count + 1);\n  }\n}\n"
  },
  {
    "path": "test/dispatch_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var feature = BddFeature('Dispatch');\n\n  Bdd(feature)\n      .scenario('Waiting for a dispatch to end.')\n      .given('A SYNC or ASYNC action.')\n      .when('The action is dispatched with `dispatch(action)`.')\n      .then('The SYNC action changes the state synchronously.')\n      .and('The ASYNC action changes the state asynchronously.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    // The SYNC action changes the state synchronously.\n    expect(store.state.count, 1);\n    store.dispatch(IncrementSync());\n    expect(store.state.count, 2);\n\n    // The ASYNC action does NOT change the state synchronously.\n    store.dispatch(IncrementAsync());\n    expect(store.state.count, 2);\n\n    // But the ASYNC action changes the state asynchronously.\n    await Future.delayed(const Duration(milliseconds: 50));\n    expect(store.state.count, 3);\n  });\n\n  Bdd(feature)\n      .scenario('Knowing when some action dispatched with `dispatch` is being processed.')\n      .given('A SYNC or ASYNC action.')\n      .when('The action is dispatched.')\n      .then('We can check if the action is processing with `Store.isWaiting(action)`.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    // SYNC ACTION: isWaiting is always false.\n    expect(store.isWaiting(IncrementSync), false);\n    expect(store.state.count, 1);\n\n    var actionSync = IncrementSync();\n    store.dispatch(actionSync);\n    expect(store.isWaiting(IncrementSync), false);\n    expect(store.state.count, 2);\n\n    // ASYNC ACTION: isWaiting is true while we wait for it to finish.\n    expect(store.isWaiting(IncrementAsync), false);\n    expect(store.state.count, 2);\n\n    var actionAsync = IncrementAsync();\n    store.dispatch(actionAsync);\n    expect(store.isWaiting(IncrementAsync), true); // True!\n    expect(store.state.count, 2);\n\n    // Since it's ASYNC, it really waits until it finishes.\n    await Future.delayed(const Duration(milliseconds: 50));\n\n    expect(store.isWaiting(IncrementAsync), false);\n    expect(store.state.count, 3);\n  });\n}\n\nclass State {\n  final int count;\n\n  State(this.count);\n}\n\nclass IncrementSync extends ReduxAction<State> {\n  @override\n  State reduce() {\n    return State(state.count + 1);\n  }\n}\n\nclass IncrementAsync extends ReduxAction<State> {\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 1));\n    return State(state.count + 1);\n  }\n}\n"
  },
  {
    "path": "test/event_redux_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test('Typedef Evt.', () {\n    expect(Event.spent(), Evt.spent());\n    expect((Event).toString(), 'Event<dynamic>');\n    expect((Evt).toString(), 'Event<dynamic>');\n  });\n\n  test('Boolean event equals.', () {\n    // Spent events are always equal.\n    expect(Event.spent(), Event.spent());\n    expect(Event.spent(), Evt.spent());\n\n    // Not-spent events are always different.\n    expect(Event(), isNot(Event()));\n\n    // An event not-spent is always different from a spent event.\n    expect(Event.spent(), isNot(Event()));\n  });\n\n  test('String event equals.', () {\n    // Spent events are always equal.\n    expect(Event<String>.spent(), Event<String>.spent());\n\n    // Not-spent events are always different.\n    expect(Event<String>('String'), isNot(Event<String>('String')));\n\n    // An event not-spent is always different from a spent event.\n    expect(Event<String>.spent(), isNot(Event<String>()));\n  });\n\n  test('Number event equals.', () {\n    // Spent events are always equal.\n    expect(Event<int>.spent(), Event<int>.spent());\n\n    // Not-spent events are always different.\n    expect(Event<int>(123), isNot(Event<int>(123)));\n\n    // An event not-spent is always different from a spent event.\n    expect(Event<int>.spent(), isNot(Event<int>()));\n  });\n\n  test('EventMultiple', () {\n    Event<String> evt1 = Event<String>(\"Mary\");\n    Event<String> evt2 = Event<String>(\"Anna\");\n    EventMultiple<String> evt = EventMultiple(evt1, evt2);\n\n    expect(evt.isSpent, false);\n    expect(evt.isNotSpent, true);\n    expect(evt.state, \"Mary\");\n    expect(evt.state, \"Mary\");\n    expect(evt.isSpent, false);\n    expect(evt.isNotSpent, true);\n\n    expect(evt.consume(), \"Mary\");\n    expect(evt.state, \"Anna\");\n    expect(evt.isSpent, false);\n    expect(evt.isNotSpent, true);\n\n    expect(evt.consume(), \"Anna\");\n    expect(evt.state, null);\n    expect(evt.isSpent, true);\n    expect(evt.isNotSpent, false);\n\n    expect(evt.consume(), null);\n    expect(evt.isSpent, true);\n    expect(evt.isNotSpent, false);\n  });\n\n  test('MappedEvent', () {\n    List<String> users = [\"Mary\", \"Anna\", \"Arnold\", \"Jake\", \"Frank\", \"Suzy\"];\n    String? Function(int?) mapFunction = (index) => index == null ? null : users[index];\n    Event<String> userEvt1 = Event.map(Event<int>(3), mapFunction);\n    Event<String> userEvt2 = MappedEvent<int, String>(Event<int>(2), mapFunction);\n\n    // Consume the event.\n    expect(userEvt1.consume(), \"Jake\");\n    expect(userEvt1.consume(), null);\n    expect(userEvt1.isSpent, true);\n    expect(userEvt1.isNotSpent, false);\n\n    // Don't consume the event.\n    expect(userEvt2.state, \"Arnold\");\n    expect(userEvt2.state, \"Arnold\");\n    expect(userEvt2.isSpent, false);\n    expect(userEvt2.isNotSpent, true);\n\n    // A spent event is different from a not-spent one.\n    expect(userEvt1 == userEvt2, isFalse);\n\n    // Now consume the second event.\n    expect(userEvt2.consume(), \"Arnold\");\n    expect(userEvt2.isSpent, true);\n    expect(userEvt2.isNotSpent, false);\n\n    // A spent event is equal to a spent one.\n    expect(userEvt1 == userEvt2, isTrue);\n  });\n\n  test('Typedef EvtState.', () {\n    expect((EvtState).toString(), 'EvtState<dynamic>');\n    expect((EvtState<String>()).runtimeType.toString(), 'EvtState<String>');\n  });\n\n  test('Boolean event equals.', () {\n    expect(EvtState() == EvtState(), isFalse);\n    expect(EvtState<String>('abc') == EvtState<String>('abc'), isFalse);\n    expect(EvtState<String>('abc') == EvtState<String>('123'), isFalse);\n\n    expect(EvtState().hashCode == EvtState().hashCode, isFalse);\n    expect(EvtState<String>('abc').hashCode == EvtState<String>('abc').hashCode, isFalse);\n    expect(EvtState<String>('abc').hashCode == EvtState<String>('123').hashCode, isFalse);\n\n    var x = EvtState();\n    var y = x;\n    expect(x == y, isTrue);\n    expect(x.hashCode == y.hashCode, isTrue);\n  });\n\n  test('Getting the value. It is not consumed.', () {\n    expect(EvtState().value, isNull);\n\n    expect(EvtState<String>('abc').value, 'abc');\n    expect(EvtState<String>('abc').value, 'abc');\n\n    expect(EvtState<int>(123).value, 123);\n    expect(EvtState<int>(123).value, 123);\n  });\n}\n"
  },
  {
    "path": "test/failed_action_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var feature = BddFeature('Failed action');\n\n  Bdd(feature)\n      .scenario('Checking if a SYNC action has failed.')\n      .given('A SYNC action.')\n      .when('The action is dispatched twice with `dispatch(action)`.')\n      .and('The action fails the first time, but not the second time.')\n      .then('We can check that the action failed the first time, but not the second.')\n      .and('We can get the action exception the first time, but null the second time.')\n      .and('We can clear the failing flag.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    // When the SYNC action fails, the failed flag is set.\n    expect(store.isFailed(SyncActionThatFails), false);\n    var actionFail = SyncActionThatFails(true);\n    store.dispatch(actionFail);\n    expect(store.isFailed(SyncActionThatFails), true);\n    expect(store.exceptionFor(SyncActionThatFails), const UserException('Yes, it failed.'));\n\n    // When the same action is dispatched and does not fail, the failed flag is cleared.\n    var actionSuccess = SyncActionThatFails(false);\n    store.dispatch(actionSuccess);\n    expect(store.isFailed(SyncActionThatFails), false);\n    expect(store.exceptionFor(SyncActionThatFails), null);\n\n    // Test clearing the exception.\n\n    // Fail it again.\n    store.dispatch(SyncActionThatFails(true));\n    expect(store.isFailed(SyncActionThatFails), true);\n    expect(store.exceptionFor(SyncActionThatFails), const UserException('Yes, it failed.'));\n\n    // We clear the exception for ANOTHER action. It doesn't clear anything.\n    store.clearExceptionFor(AsyncActionThatFails);\n    expect(store.isFailed(SyncActionThatFails), true);\n    expect(store.exceptionFor(SyncActionThatFails), const UserException('Yes, it failed.'));\n\n    // We clear the exception for the correct action. Now it's NOT failing anymore.\n    store.clearExceptionFor(SyncActionThatFails);\n    expect(store.isFailed(SyncActionThatFails), false);\n    expect(store.exceptionFor(SyncActionThatFails), null);\n  });\n\n  Bdd(feature)\n      .scenario('Checking if an ASYNC action has failed.')\n      .given('An ASYNC action.')\n      .when('The action is dispatched twice with `dispatch(action)`.')\n      .and('The action fails the first time, but not the second time.')\n      .then('We can check that the action failed the first time, but not the second.')\n      .and('We can get the action exception the first time, but null the second time.')\n      .and('We can clear the failing flag.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    // Initially, flag tells us it's NOT failing.\n    expect(store.isFailed(AsyncActionThatFails), false);\n    var actionFail = AsyncActionThatFails(true);\n\n    // The action is dispatched, but it's ASYNC. We wait for it.\n    await store.dispatch(actionFail);\n\n    // Now it's failed.\n    expect(store.isFailed(AsyncActionThatFails), true);\n    expect(store.exceptionFor(AsyncActionThatFails), const UserException('Yes, it failed.'));\n\n    // We clear the exception, so that it's NOT failing.\n    store.clearExceptionFor(AsyncActionThatFails);\n    expect(store.isFailed(AsyncActionThatFails), false);\n    actionFail = AsyncActionThatFails(true);\n\n    // The action is dispatched, but it's ASYNC.\n    store.dispatch(actionFail);\n\n    // So, there was no time to fail.\n    expect(store.isFailed(AsyncActionThatFails), false);\n\n    // We wait until it really finishes.\n    await Future.delayed(const Duration(milliseconds: 50));\n\n    // Now it's failed.\n    expect(store.isFailed(AsyncActionThatFails), true);\n    expect(store.exceptionFor(AsyncActionThatFails), const UserException('Yes, it failed.'));\n\n    // We dispatch the same action type again.\n    actionFail = AsyncActionThatFails(true);\n    store.dispatch(actionFail);\n\n    // This act of dispatching it cleared the flag.\n    expect(store.isFailed(AsyncActionThatFails), false);\n\n    // We wait until it really finishes, again.\n    await Future.delayed(const Duration(milliseconds: 500));\n\n    // Not it's failed, again.\n    expect(store.isFailed(AsyncActionThatFails), true);\n    expect(store.exceptionFor(AsyncActionThatFails), const UserException('Yes, it failed.'));\n  });\n}\n\nclass State {\n  final int count;\n\n  State(this.count);\n}\n\nclass SyncActionThatFails extends ReduxAction<State> {\n  final bool ifFails;\n\n  SyncActionThatFails(this.ifFails);\n\n  @override\n  State? reduce() {\n    if (ifFails) throw const UserException('Yes, it failed.');\n    return null;\n  }\n}\n\nclass AsyncActionThatFails extends ReduxAction<State> {\n  final bool ifFails;\n\n  AsyncActionThatFails(this.ifFails);\n\n  @override\n  Future<State?> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 1));\n    if (ifFails) throw const UserException('Yes, it failed.');\n    return null;\n  }\n}\n"
  },
  {
    "path": "test/fresh_mixin_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var feature = BddFeature('Fresh mixin');\n\n  // ==========================================================================\n  // Case 1: Initial no key, ignoreFresh == false, success\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Action succeeds when no fresh key exists')\n      .given('No fresh key exists for the action')\n      .when('The action is dispatched')\n      .then('It should execute and create a fresh key')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // First dispatch - should run since no key exists\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 1);\n\n    // Dispatch again immediately - should abort (key is fresh)\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 1);\n  });\n\n  // ==========================================================================\n  // Case 2: Initial no key, ignoreFresh == false, error\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Action fails when no fresh key exists - key should be removed')\n      .given('No fresh key exists for the action')\n      .when('The action is dispatched and fails')\n      .then('It should execute, then remove the fresh key on error')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // First dispatch - should run and fail, key should be removed\n    await store.dispatchAndWait(\n      FreshAction(\n        shouldFail: true, // True!\n        ignoreFresh: false,\n      ),\n    );\n\n    // The action failed, so the state should not have changed\n    expect(store.state.count, 0);\n\n    // Dispatch again - should run because the key was removed after error\n    // This time it succeeds, proving it actually ran\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false, // False!\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 1); // Proves the second dispatch ran\n  });\n\n  // ==========================================================================\n  // Case 3: Initial stale key, ignoreFresh == false, success\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Action succeeds when a stale key exists')\n      .given('A stale fresh key exists for the action')\n      .when('The action is dispatched')\n      .then('It should execute and update the fresh key')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // First dispatch with short freshFor\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 1);\n\n    // Wait for the fresh period to expire (150ms freshFor + buffer)\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    // Now the key is stale, so it should run again\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 2);\n  });\n\n  // ==========================================================================\n  // Case 4: Initial stale key, ignoreFresh == false, error\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Action fails when a stale key exists - restores stale state')\n      .given('A stale fresh key exists for the action')\n      .when('The action is dispatched and fails')\n      .then('It should restore the old stale expiry')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Create a stale key by dispatching and waiting\n    await store.dispatch(FreshAction(\n      shouldFail: false,\n      ignoreFresh: false,\n    ));\n    expect(store.state.count, 1);\n\n    // Wait for the key to become stale\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    // Now dispatch with failure - it should run (stale), fail, and restore stale state\n    await store.dispatchAndWait(FreshAction(\n      shouldFail: true, // Fail!\n      ignoreFresh: false,\n    ));\n    expect(store.state.count, 1); // No change due to failure\n\n    // The key should still be stale, so we can dispatch again\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 2);\n  });\n\n  // ==========================================================================\n  // Case 5: Initial fresh key, ignoreFresh == false\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Action aborts when fresh key exists')\n      .given('A fresh key exists for the action')\n      .when('The action is dispatched again')\n      .then('It should abort without executing')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // First dispatch - should run\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 1);\n\n    // Dispatch again immediately - should abort (key is still fresh)\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 1);\n\n    // Wait for fresh period to expire\n    await Future.delayed(\n      const Duration(milliseconds: 200),\n    );\n\n    // Now it should run again\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 2);\n  });\n\n  // ==========================================================================\n  // Case 6: ignoreFresh == true, success\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Action with ignoreFresh=true always runs and stays fresh on success')\n      .given('An action with ignoreFresh set to true')\n      .when('The action is dispatched even when fresh')\n      .then('It should execute and create a new fresh period')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // First dispatch with ignoreFresh - should run\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: true,\n      ),\n    );\n    expect(store.state.count, 1);\n\n    // Dispatch again immediately with ignoreFresh - should run again\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: true,\n      ),\n    );\n    expect(store.state.count, 2);\n\n    // After success, the key should be fresh\n    // So a normal action (without ignoreFresh) should be aborted\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 2); // Aborted\n\n    // Dispatch again immediately with ignoreFresh - should run again\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: true,\n      ),\n    );\n    expect(store.state.count, 3);\n  });\n\n  // ==========================================================================\n  // Case 7: ignoreFresh == true, error\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Action with ignoreFresh=true runs but removes key on error')\n      .given('An action with ignoreFresh set to true')\n      .when('The action is dispatched and fails')\n      .then('It should execute and remove the key on error')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Dispatch with ignoreFresh and failure - should run and then remove key\n    await store.dispatchAndWait(\n      FreshAction(\n        shouldFail: true, // Fail!\n        ignoreFresh: true, // True!\n      ),\n    );\n\n    expect(store.state.count, 0); // No change due to failure\n\n    // The key should be removed, so a normal action should run.\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 1);\n  });\n\n  // ==========================================================================\n  // Case 8: removeKey in reduce or before\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Action calls removeKey - no rollback on error')\n      .given('An action that calls removeKey in reduce')\n      .when('The action fails')\n      .then('The key should remain removed (no rollback)')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Dispatch action that removes key and fails\n    await store.dispatchAndWait(\n      FreshAction(\n        shouldRemoveKey: true, // Remove key!\n        shouldFail: true, // Fail!\n        ignoreFresh: false,\n      ),\n    );\n\n    expect(store.state.count, 0);\n\n    // The key was manually removed, so it should run again\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        shouldRemoveKey: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 1);\n  });\n\n  // ==========================================================================\n  // Case 9: removeKey in reduce or before\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Action calls removeKey - allows immediate re-dispatch')\n      .given('An action that calls removeKey in reduce')\n      .when('The action succeeds')\n      .then('The key should be removed and action can run again')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Dispatch action that removes its own key\n    await store.dispatch(\n      FreshAction(\n        shouldRemoveKey: true, // Remove key!\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 1);\n\n    // The key was removed, so it should run again immediately\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        shouldRemoveKey: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 2);\n  });\n\n  // ==========================================================================\n  // Case 10: removeAllKeys in reduce or before\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Action calls removeAllKeys - clears all fresh keys')\n      .given('An action that calls removeAllKeys in reduce')\n      .when('The action is dispatched')\n      .then('All fresh keys should be removed')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Create fresh keys for different actions\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    await store.dispatch(FreshAction2());\n    expect(store.state.count, 2);\n\n    // Both should be fresh, so re-dispatch should abort\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    await store.dispatch(FreshAction2());\n    expect(store.state.count, 2);\n\n    // Now dispatch an action that removes all keys\n    // Use ignoreFresh so it runs even though the key is fresh\n    await store.dispatch(\n      FreshAction(\n        shouldRemoveAllKeys: true, // Remove all keys!\n        shouldFail: false,\n        ignoreFresh: true, // Force run even if fresh!\n      ),\n    );\n    expect(store.state.count, 3);\n\n    // Now both actions should run again (keys removed)\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    await store.dispatch(FreshAction2());\n    expect(store.state.count, 5);\n  });\n\n  // ==========================================================================\n  // Case 11: Two actions A then B, same key, B dispatched while K is fresh from A\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Two actions with same key - second aborts when first is fresh')\n      .given('Two actions that share the same fresh key')\n      .when('Both are dispatched while the key is fresh')\n      .then('Only the first action should execute')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Dispatch first action with shared key\n    await store.dispatch(FreshActionSharedKey1());\n    expect(store.state.count, 1);\n\n    // Dispatch second action with same key - should abort\n    await store.dispatch(FreshActionSharedKey2());\n    expect(store.state.count, 1);\n\n    // Wait for fresh period to expire\n    await Future.delayed(const Duration(milliseconds: 1100));\n\n    // Now second action should run\n    await store.dispatch(FreshActionSharedKey2());\n    expect(store.state.count, 2);\n  });\n\n  // ========================================================================\n  // Case 12: Two actions A then B, same key, B dispatched after expiry\n  // ========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Two actions A then B - B failure does not block future shared-key runs')\n      .given('Action A runs successfully, then B runs and fails for same key')\n      .when('The shared key was already stale when B ran')\n      .then(\n          'B\\'s failure should not stop another shared-key action from running')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Action A runs successfully and sets the shared key as fresh.\n    await store.dispatch(FreshActionSharedKey1());\n    expect(store.state.count, 1);\n\n    // Wait for the shared key to become stale (freshFor == 1000ms).\n    await Future.delayed(const Duration(milliseconds: 1100));\n\n    // Action B runs and fails. This should not change the state.\n    await store.dispatchAndWait(FreshActionSharedKey2Fails());\n\n    expect(store.state.count, 1);\n\n    // After B's failure, the shared key should be stale again.\n    // So another shared-key action should run and change the state.\n    await store.dispatch(FreshActionSharedKey1());\n    expect(store.state.count, 2);\n  });\n\n  // ==========================================================================\n  // Case 13: Tests different runtime types\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Actions with different runtime types have independent freshness')\n      .given('Two actions with Fresh mixin but different runtime types')\n      .when('Both actions are dispatched in quick succession')\n      .then('Both should execute independently')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 1);\n\n    await store.dispatch(FreshAction2());\n    expect(store.state.count, 2);\n\n    // Both should be fresh now\n    await store.dispatch(\n      FreshAction(\n        shouldFail: false,\n        ignoreFresh: false,\n      ),\n    );\n    expect(store.state.count, 2); // Aborted\n\n    await store.dispatch(FreshAction2());\n    expect(store.state.count, 2); // Aborted\n  });\n\n  // ==========================================================================\n  // Case 14: Test freshKeyParams\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Actions with freshKeyParams differentiate by parameters')\n      .given('Actions that use freshKeyParams to differentiate instances')\n      .when('Actions with different params are dispatched')\n      .then('They should have independent freshness')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Dispatch with param \"A\"\n    await store.dispatch(\n      FreshActionWithParams('A'),\n    );\n    expect(store.state.count, 1);\n\n    // Dispatch with param \"B\" - different key, should run\n    await store.dispatch(\n      FreshActionWithParams('B'),\n    );\n    expect(store.state.count, 2);\n\n    // Dispatch with param \"A\" again - same key, should abort\n    await store.dispatch(\n      FreshActionWithParams('A'),\n    );\n    expect(store.state.count, 2);\n\n    // Dispatch with param \"B\" again - same key, should abort\n    await store.dispatch(\n      FreshActionWithParams('B'),\n    );\n    expect(store.state.count, 2);\n  });\n\n  // ==========================================================================\n  // Case 15: Test freshKeyParams\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Actions with freshKeyParams share freshness for same params')\n      .given('Actions with the same freshKeyParams value')\n      .when('Dispatched in quick succession')\n      .then('The second should abort')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Dispatch with param \"X\"\n    await store.dispatch(\n      FreshActionWithParams('X'),\n    );\n    expect(store.state.count, 1);\n\n    // Dispatch with same param \"X\" - should abort\n    await store.dispatch(\n      FreshActionWithParams('X'),\n    );\n    expect(store.state.count, 1);\n  });\n\n  // ==========================================================================\n  // Case 16: Concurrency protection: A fails, B succeeds, no previous key\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Concurrency: A fails after B succeeds - B\\'s freshness is preserved')\n      .given('Action A dispatched first but takes time to complete')\n      .and('Action B dispatched after A\\'s freshness expires, succeeds quickly')\n      .when('A finishes and fails after B has already succeeded')\n      .then('A\\'s failure should NOT remove the fresh key set by B')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Dispatch action A that:\n    // - Uses a shared key\n    // - Has a short freshFor (100ms) so it expires quickly\n    // - Takes a long time to execute (300ms delay)\n    // - Fails at the end\n    // Don't await - let it run in background\n    store.dispatch(\n      FreshActionConcurrentSlow(\n        shouldFail: true,\n        delayMillis: 300,\n        freshForMillis: 100,\n      ),\n    );\n\n    // Wait for A's freshness to expire (100ms + buffer)\n    await Future.delayed(const Duration(milliseconds: 150));\n\n    // At this point:\n    // - A is still running (only 150ms passed, A needs 300ms)\n    // - A's freshness has expired (100ms freshFor)\n    // - Map[K] = expiryA (stale)\n\n    // Dispatch action B that:\n    // - Uses the same shared key\n    // - Executes quickly\n    // - Succeeds\n    await store.dispatch(\n      FreshActionConcurrentFast(\n        shouldFail: false,\n      ),\n    );\n\n    // B has succeeded and set Map[K] = expiryB (fresh)\n    // Count should be 1 from B's success (A hasn't finished yet)\n    expect(store.state.count, 1);\n\n    // Wait for A to finish (it takes 300ms total, we've waited 150ms + dispatch time)\n    // So wait another 200ms to be safe\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    // A has now failed\n    // The critical assertion: A's failure should NOT have removed the key\n    // Because A's after() sees current = expiryB != _newExpiryA\n    // So the rollback is skipped\n\n    // Dispatch another action with the same key\n    // If B's freshness is preserved, this should abort\n    await store.dispatch(\n      FreshActionConcurrentFast(\n        shouldFail: false,\n      ),\n    );\n\n    // Count should still be 1 (second dispatch was aborted because key is fresh from B)\n    expect(store.state.count, 1);\n\n    // Wait for B's freshness to expire\n    await Future.delayed(const Duration(milliseconds: 1100));\n\n    // Now the key should be stale, so dispatch should succeed\n    await store.dispatch(\n      FreshActionConcurrentFast(\n        shouldFail: false,\n      ),\n    );\n    expect(store.state.count, 2);\n  });\n\n  // ==========================================================================\n  // Case 17: Concurrency protection: Previous stale expiry exists, A fails, B succeeds\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Concurrency: Previous stale expiry, A fails after B succeeds - B\\'s freshness is preserved')\n      .given('An initial action creates a stale expiry for the key')\n      .and('A slow failing action A starts and updates the expiry')\n      .and('A fast succeeding action B runs after A\\'s freshness expires')\n      .when('A later fails after B has already succeeded')\n      .then(\n          'A\\'s failure should NOT restore the old stale expiry or remove B\\'s freshness')\n      .run((_) async {\n    final store = Store<AppState>(initialState: AppState(0));\n\n    // Step 1: Initial action C writes the first expiry and succeeds.\n    // This gives us a \"previous expiry\" (_current != null for the next action).\n    await store.dispatch(\n      FreshActionConcurrentSlow(\n        shouldFail: false,\n        delayMillis: 0,\n        freshForMillis: 100, // short fresh period\n      ),\n    );\n    // C succeeded once\n    expect(store.state.count, 1);\n\n    // Wait for C's freshness to expire so its expiry is stale but still in the map.\n    await Future.delayed(const Duration(milliseconds: 150));\n\n    // Step 2: Dispatch A (slow, failing) with the same key and same freshFor.\n    // At this point:\n    // - Map['concurrentKey'] = prevExpiry (from C), which is stale.\n    // - A.abortDispatch sees _current != null and writes a new expiryA.\n    store.dispatch(\n      FreshActionConcurrentSlow(\n        shouldFail: true,\n        delayMillis: 300, // long running, will fail later\n        freshForMillis: 100,\n      ),\n    );\n\n    // Wait long enough for A's fresh window (100ms) to expire,\n    // but not long enough for A to finish (300ms total).\n    await Future.delayed(const Duration(milliseconds: 150));\n\n    // Step 3: Dispatch B (fast, succeeding) with the same key.\n    // Now:\n    // - Map['concurrentKey'] = expiryA (from A), which is stale at this point.\n    // - B.abortDispatch sees stale expiry and writes expiryB.\n    await store.dispatch(\n      FreshActionConcurrentFast(\n        shouldFail: false,\n      ),\n    );\n\n    // C and B have succeeded, A is still running and will fail later.\n    // Count: 1 (C) + 1 (B) = 2.\n    expect(store.state.count, 2);\n\n    // Step 4: Wait for A to finish and fail.\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    // At this time:\n    // - Map['concurrentKey'] == expiryB (written by B).\n    // - A.after sees status.originalError != null,\n    //   current = expiryB, _newExpiryA = expiryA, _currentA = prevExpiry.\n    // - Since current != _newExpiryA, rollback is skipped.\n    //   So A must NOT restore prevExpiry or remove the key.\n\n    // Step 5: Dispatch B again while its freshFor (1000ms) has not expired.\n    // If B's freshness is preserved, this dispatch should abort\n    // and the reducer should NOT run.\n    await store.dispatch(\n      FreshActionConcurrentFast(\n        shouldFail: false,\n      ),\n    );\n    expect(store.state.count, 2);\n\n    // Optional: Wait for B's freshness to expire and confirm that the key\n    // becomes stale and a new dispatch can run.\n    await Future.delayed(const Duration(milliseconds: 1100));\n\n    await store.dispatch(\n      FreshActionConcurrentFast(\n        shouldFail: false,\n      ),\n    );\n    expect(store.state.count, 3);\n  });\n\n  // ==========================================================================\n  // Case 18: Concurrency + ignoreFresh: A fails after B succeeds - B's freshness preserved\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Concurrency with ignoreFresh: A fails after B succeeds - B\\'s freshness is preserved')\n      .given(\n          'A slow action A with ignoreFresh=true starts and reserves freshness')\n      .and('After A\\'s freshFor expires, a fast action B runs and succeeds')\n      .when('A later fails after B has already succeeded')\n      .then('A\\'s failure should NOT remove or revert B\\'s freshness')\n      .run((_) async {\n    final store = Store<AppState>(initialState: AppState(0));\n\n    // Step 1: Dispatch A (slow, failing, ignoreFresh=true).\n    //\n    // A characteristics:\n    // - freshFor = 100ms\n    // - ignoreFresh = true\n    // - delay = 300ms, then fails\n    //\n    // At A.abortDispatch (t0):\n    // - Map['concurrentKey'] is probably null (no previous key)\n    // - ignoreFresh branch:\n    //     Map['concurrentKey'] = expiryA = t0 + 100ms\n    //     _newExpiryA = expiryA\n    //     _currentA = null\n    //\n    // Do NOT await: let A run in background.\n    store.dispatch(\n      FreshActionConcurrentSlowIgnoreFresh(\n        shouldFail: true,\n        delayMillis: 300,\n        freshForMillis: 100,\n      ),\n    );\n\n    // Step 2: Wait until A's fresh window expires, but A is still running.\n    //\n    // After 150ms:\n    // - expiryA (t0 + 100ms) is in the past => stale\n    // - A is still running (needs 300ms)\n    await Future.delayed(const Duration(milliseconds: 150));\n\n    // Step 3: Dispatch B (fast, succeeds) with the same key.\n    //\n    // At B.abortDispatch (t1 ~ t0 + 150ms):\n    // - Map['concurrentKey'] = expiryA (stale)\n    // - B sees stale, writes:\n    //     expiryB = t1 + 1000ms\n    //     Map['concurrentKey'] = expiryB\n    //     _newExpiryB = expiryB\n    //\n    // Then B.reduce succeeds and increments count.\n    await store.dispatch(\n      FreshActionConcurrentFast(\n        shouldFail: false,\n      ),\n    );\n\n    // So far:\n    // - Only B has succeeded => count = 1\n    // - Map['concurrentKey'] = expiryB (fresh)\n    expect(store.state.count, 1);\n\n    // Step 4: Wait for A to finish and fail.\n    //\n    // After another 200ms:\n    // - Total since A started ~350ms > 300ms => A finishes and fails.\n    //\n    // In A.after:\n    // - status.originalError != null\n    // - current = Map['concurrentKey'] = expiryB\n    // - _newExpiryA = expiryA\n    // - current != _newExpiryA => rollback block is SKIPPED\n    //   So A does NOT remove the key (even though _currentA == null).\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    // Step 5: Dispatch B again while expiryB is still in the future.\n    //\n    // If B's freshness was preserved, this dispatch must abort\n    // (abortDispatch returns true) and NOT increment the counter.\n    await store.dispatch(\n      FreshActionConcurrentFast(\n        shouldFail: false,\n      ),\n    );\n\n    // Still only one successful run from B.\n    expect(store.state.count, 1);\n\n    // Optional: Wait for B's freshness to expire, then B should run again.\n    await Future.delayed(const Duration(milliseconds: 1100));\n\n    await store.dispatch(\n      FreshActionConcurrentFast(\n        shouldFail: false,\n      ),\n    );\n    expect(store.state.count, 2);\n  });\n\n  // ==========================================================================\n  // Case 19: Scenario 4: Nested override between abort and after, failing outer action\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Nested override: failing outer action must not revert newer freshness')\n      .given('OuterActionNested reserves freshness for a key')\n      .and('OverrideAction runs inside its reduce and sets a newer freshness')\n      .when('OuterActionNested later fails and after() runs')\n      .then('The newer freshness from OverrideAction must be preserved')\n      .run((_) async {\n    final store = Store<AppState>(initialState: AppState(0));\n\n    // Step 1: Dispatch the outer action that will:\n    // - In abortDispatch: reserve expiryA for \"nestedKey\".\n    // - In reduce: dispatch OverrideAction, which:\n    //     * runs (ignoreFresh = true),\n    //     * sets a newer expiryB for \"nestedKey\",\n    //     * increments count to 1.\n    // - Then OuterActionNested throws.\n    try {\n      await store.dispatch(OuterActionNested());\n      fail('Expected OuterActionNested to throw');\n    } catch (_) {\n      // Expected failure from OuterActionNested.\n    }\n\n    // At this point:\n    // - OverrideAction has succeeded exactly once => count should be 1.\n    expect(store.state.count, 1);\n\n    // In after() of OuterActionNested:\n    // - status.originalError != null\n    // - current = Map['nestedKey'] is the expiry set by OverrideAction\n    // - _newExpiry (from OuterActionNested) is the earlier expiryA\n    // - current != _newExpiry => rollback is skipped\n    //\n    // So the key must still be fresh according to OverrideAction.\n\n    // Step 2: Dispatch CheckAction with the same key.\n    //\n    // If the newer freshness from OverrideAction is preserved:\n    // - abortDispatch of CheckAction sees a fresh key and returns true\n    // - reduce() is NOT called and count stays 1.\n    //\n    // If OuterActionNested had reverted or removed the key on error:\n    // - the key would be stale\n    // - CheckAction would run and increment count to 2.\n    await store.dispatch(CheckAction());\n\n    // Assert that CheckAction was aborted (did not increment the count).\n    expect(store.state.count, 1);\n  });\n\n  // ---------------------------------------------------------------------------\n\n  // ==========================================================================\n  // Case 20: Fresh mixin cannot be combined with Throttle\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Fresh mixin cannot be combined with Throttle')\n      .given('An action that combines Fresh and Throttle mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () => store.dispatch(FreshWithThrottleAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The Fresh mixin cannot be combined with the Throttle mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 21: Fresh mixin cannot be combined with NonReentrant\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Fresh mixin cannot be combined with NonReentrant')\n      .given('An action that combines Fresh and NonReentrant mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () => store.dispatch(FreshWithNonReentrantAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The Fresh mixin cannot be combined with the NonReentrant mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 22: Fresh mixin cannot be combined with UnlimitedRetryCheckInternet\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Fresh mixin cannot be combined with UnlimitedRetryCheckInternet')\n      .given(\n          'An action that combines Fresh and UnlimitedRetryCheckInternet mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () => store.dispatch(FreshWithUnlimitedRetryAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The Fresh mixin cannot be combined with the UnlimitedRetryCheckInternet mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 23: Throttle mixin cannot be combined with NonReentrant\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Throttle mixin cannot be combined with NonReentrant')\n      .given('An action that combines Throttle and NonReentrant mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // NonReentrant.abortDispatch() runs and detects Throttle\n    expect(\n      () => store.dispatch(ThrottleWithNonReentrantAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The NonReentrant mixin cannot be combined with the Throttle mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 24: Throttle mixin cannot be combined with UnlimitedRetryCheckInternet\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Throttle mixin cannot be combined with UnlimitedRetryCheckInternet')\n      .given(\n          'An action that combines Throttle and UnlimitedRetryCheckInternet mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // UnlimitedRetryCheckInternet.abortDispatch() runs and detects Throttle\n    expect(\n      () => store.dispatch(ThrottleWithUnlimitedRetryAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The UnlimitedRetryCheckInternet mixin cannot be combined with the Throttle mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 25: NonReentrant mixin cannot be combined with UnlimitedRetryCheckInternet\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'NonReentrant mixin cannot be combined with UnlimitedRetryCheckInternet')\n      .given(\n          'An action that combines NonReentrant and UnlimitedRetryCheckInternet mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // UnlimitedRetryCheckInternet.abortDispatch() runs and detects NonReentrant\n    expect(\n      () => store.dispatch(NonReentrantWithUnlimitedRetryAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The UnlimitedRetryCheckInternet mixin cannot be combined with the NonReentrant mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 26: CheckInternet mixin cannot be combined with AbortWhenNoInternet\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'CheckInternet mixin cannot be combined with AbortWhenNoInternet')\n      .given(\n          'An action that combines CheckInternet and AbortWhenNoInternet mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // AbortWhenNoInternet.before() runs and detects CheckInternet\n    expect(\n      () => store.dispatch(CheckInternetWithAbortWhenNoInternetAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The AbortWhenNoInternet mixin cannot be combined with the CheckInternet mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 27: CheckInternet mixin cannot be combined with UnlimitedRetryCheckInternet\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'CheckInternet mixin cannot be combined with UnlimitedRetryCheckInternet')\n      .given(\n          'An action that combines CheckInternet and UnlimitedRetryCheckInternet mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // UnlimitedRetryCheckInternet.abortDispatch() runs first and detects CheckInternet\n    expect(\n      () => store.dispatch(CheckInternetWithUnlimitedRetryAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The UnlimitedRetryCheckInternet mixin cannot be combined with the CheckInternet mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 28: AbortWhenNoInternet mixin cannot be combined with UnlimitedRetryCheckInternet\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'AbortWhenNoInternet mixin cannot be combined with UnlimitedRetryCheckInternet')\n      .given(\n          'An action that combines AbortWhenNoInternet and UnlimitedRetryCheckInternet mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // UnlimitedRetryCheckInternet.abortDispatch() runs first and detects AbortWhenNoInternet\n    expect(\n      () => store.dispatch(AbortWhenNoInternetWithUnlimitedRetryAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The UnlimitedRetryCheckInternet mixin cannot be combined with the AbortWhenNoInternet mixin.',\n      )),\n    );\n  });\n\n  // ---------------------------------------------------------------------------\n\n  // ==========================================================================\n  // Case 29: Rapid consecutive dispatches get different tokens (regression test)\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Rapid consecutive dispatches get different tokens')\n      .given('Two actions dispatched in rapid succession (same millisecond)')\n      .when('The first action fails after the second succeeds')\n      .then('The second action freshness should be preserved')\n      .note('This is a regression test for the DateTime equality bug')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Dispatch a slow action that will fail\n    var slowFuture = store.dispatchAndWait(\n      RapidTestActionSlow(shouldFail: true),\n    );\n\n    // Immediately dispatch another action with ignoreFresh\n    // This happens within the same millisecond\n    await store.dispatch(RapidTestActionFast());\n    expect(store.state.count, 1);\n\n    // Wait for the slow action to complete (and fail)\n    await slowFuture;\n\n    // The fast action's freshness should be preserved\n    // If it was incorrectly reverted, this action would run\n    await store.dispatch(RapidTestActionCheck());\n    expect(store.state.count, 1); // Should still be 1 (check was aborted)\n  });\n\n  // ==========================================================================\n  // Case 30: Triple nesting - innermost ignoreFresh failure removes key\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Triple nesting: A->B->C where A and C fail, B succeeds')\n      .given('Action A dispatches B, B dispatches C')\n      .and('B succeeds, but A and C both fail')\n      .when('All actions complete')\n      .then('Key is removed because C (last ignoreFresh writer) failed')\n      .note('C overwrote B entry, then C failed and removed key')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    try {\n      await store.dispatch(TripleNestOuterAction(\n        middleShouldFail: false,\n        innerShouldFail: true,\n        outerShouldFail: true,\n      ));\n      fail('Expected outer action to throw');\n    } catch (_) {}\n\n    // B succeeded and incremented count\n    expect(store.state.count, 1);\n\n    // C (ignoreFresh) failed and removed the key, so check action runs\n    await store.dispatch(TripleNestCheckAction());\n    expect(store.state.count, 2); // Runs because C's failure removed key\n  });\n\n  // ==========================================================================\n  // Case 31: Triple nesting - middle fails, outer and inner succeed\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Triple nesting: A->B->C where B fails, A and C succeed')\n      .given('Action A dispatches B, B dispatches C')\n      .and('C succeeds, B fails after dispatching C, A succeeds after B')\n      .when('All actions complete')\n      .then('C freshness should be preserved')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Note: A catches B's failure internally and succeeds\n    await store.dispatch(TripleNestOuterAction(\n      middleShouldFail: true,\n      innerShouldFail: false,\n      outerShouldFail: false,\n    ));\n\n    // C succeeded and incremented count, A succeeded and incremented count\n    // B failed so it didn't increment\n    expect(store.state.count, 2);\n\n    // Check if freshness is preserved from C\n    await store.dispatch(TripleNestCheckAction());\n    expect(store.state.count, 2); // Should still be 2\n  });\n\n  // ==========================================================================\n  // Case 32: Restore of stale entry still results in stale state\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Restored stale freshness allows subsequent actions')\n      .given('Action A sets freshness with short duration')\n      .and('Action B runs after A expires and sets new freshness')\n      .and('B fails and restores A previous (now stale) freshness')\n      .when('Action C is dispatched')\n      .then('C should run because restored freshness is stale')\n      .note('When B restores A expired entry, C still runs since its stale')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // A runs and sets freshness with short duration (50ms)\n    await store.dispatch(RestoreTestActionA());\n    expect(store.state.count, 1);\n\n    // Wait for A's freshness to expire\n    await Future.delayed(const Duration(milliseconds: 60));\n\n    // B runs (A's freshness expired), sets new freshness, then fails\n    await store.dispatchAndWait(RestoreTestActionB());\n    // B failed, so count stays at 1\n    expect(store.state.count, 1);\n\n    // B restored A's old entry, but that entry is stale (expired)\n    // C should run because the restored freshness is already expired\n    await store.dispatch(RestoreTestActionC());\n    expect(store.state.count, 2); // C runs because restored entry is stale\n  });\n\n  // ==========================================================================\n  // Case 33: Sequential failures - each removes key, allowing next to run\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Sequential failures each remove key')\n      .given('Action A fails (removes key)')\n      .and('Action B runs and fails (removes key)')\n      .and('Action C runs and fails (removes key)')\n      .when('Action D is dispatched')\n      .then('D should run because key was removed')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // A fails\n    await store.dispatchAndWait(SequentialFailAction(id: 'A'));\n    expect(store.state.count, 0);\n\n    // B runs (key was removed) and fails\n    await store.dispatchAndWait(SequentialFailAction(id: 'B'));\n    expect(store.state.count, 0);\n\n    // C runs (key was removed) and fails\n    await store.dispatchAndWait(SequentialFailAction(id: 'C'));\n    expect(store.state.count, 0);\n\n    // D runs (key was removed) and succeeds\n    await store.dispatch(SequentialSucceedAction());\n    expect(store.state.count, 1);\n  });\n\n  // ==========================================================================\n  // Case 34: Nested ignoreFresh failure removes key even if outer succeeds\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Nested ignoreFresh failure removes freshness key')\n      .given('Action A (normal) dispatches B (ignoreFresh=true)')\n      .and('B fails and removes its key entry')\n      .when('A succeeds')\n      .then('Key should be removed because ignoreFresh failure makes it stale')\n      .note('ignoreFresh with _current=null removes key on failure by design')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // A dispatches B inside, B fails (removes key), A succeeds\n    await store.dispatch(OuterNormalInnerIgnoreFreshAction());\n    expect(store.state.count, 1); // Only A incremented\n\n    // Check freshness - B's failure removed the key, so this should run\n    await store.dispatch(OuterNormalCheckAction());\n    expect(store.state.count, 2); // Runs because key was removed by B's failure\n  });\n\n  // ==========================================================================\n  // Case 35: Concurrent ignoreFresh - last writer's failure removes key\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Concurrent ignoreFresh actions - last writer determines outcome')\n      .given('Three ignoreFresh actions start concurrently')\n      .and('First fails, second succeeds, third fails')\n      .when('All complete')\n      .then('Key is removed because last abortDispatch writer failed')\n      .note('With concurrent ignoreFresh, the last to call abortDispatch owns '\n          'the entry. If that action fails, the key is removed.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // Start three concurrent actions\n    // All call abortDispatch() at roughly the same time\n    // The last one to write to the map \"owns\" the entry\n    var future1 = store.dispatchAndWait(ConcurrentIgnoreFreshAction(\n      id: 1,\n      delayMs: 100,\n      shouldFail: true,\n    ));\n\n    var future2 = store.dispatchAndWait(ConcurrentIgnoreFreshAction(\n      id: 2,\n      delayMs: 50,\n      shouldFail: false,\n    ));\n\n    var future3 = store.dispatchAndWait(ConcurrentIgnoreFreshAction(\n      id: 3,\n      delayMs: 150,\n      shouldFail: true,\n    ));\n\n    await Future.wait([future1, future2, future3]);\n\n    // Only action 2 succeeded\n    expect(store.state.count, 1);\n\n    // Action 3 was likely the last to write (or one of the failing actions was)\n    // When the last writer fails, it removes the key because ignoreFresh\n    // sets _current = null, so failure removes the entry\n    await store.dispatch(ConcurrentIgnoreFreshCheck());\n    expect(store.state.count, 2); // Runs because key was removed\n  });\n\n  // ==========================================================================\n  // Case 36: Aborted action should not affect freshness on failure path\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Aborted action does not interfere with freshness')\n      .given('Action A sets freshness')\n      .and('Action B is aborted (key is fresh)')\n      .when('A check action runs')\n      .then('Freshness from A should still be valid')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    // A runs and sets freshness\n    await store.dispatch(AbortTestActionA());\n    expect(store.state.count, 1);\n\n    // B is aborted (doesn't run)\n    await store.dispatch(AbortTestActionB());\n    expect(store.state.count, 1); // Still 1\n\n    // Freshness should still be valid\n    await store.dispatch(AbortTestActionCheck());\n    expect(store.state.count, 1); // Should abort\n  });\n\n  // ---------------------------------------------------------------------------\n}\n\n// =============================================================================\n// Test state and actions\n// =============================================================================\n\nclass AppState {\n  final int count;\n\n  AppState(this.count);\n\n  AppState copy({int? count}) => AppState(count ?? this.count);\n\n  @override\n  String toString() => 'AppState($count)';\n}\n\n// Action with ignoreFresh\nclass FreshAction extends ReduxAction<AppState> with Fresh {\n  final bool shouldFail;\n  final bool shouldRemoveKey;\n  final bool shouldRemoveAllKeys;\n  final bool _ignoreFresh;\n  final int _freshFor;\n\n  FreshAction({\n    required this.shouldFail,\n    required bool ignoreFresh,\n    this.shouldRemoveKey = false,\n    this.shouldRemoveAllKeys = false,\n    int freshFor = 150,\n  })  : _ignoreFresh = ignoreFresh,\n        _freshFor = freshFor;\n\n  @override\n  int get freshFor => _freshFor;\n\n  @override\n  bool get ignoreFresh => _ignoreFresh;\n\n  @override\n  AppState reduce() {\n    if (shouldFail) {\n      throw const UserException('Intentional failure');\n    }\n    if (shouldRemoveKey) {\n      removeKey();\n    }\n    if (shouldRemoveAllKeys) {\n      removeAllKeys();\n    }\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Second action type for testing different runtime types\nclass FreshAction2 extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Actions with shared key\nclass FreshActionSharedKey1 extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'sharedKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass FreshActionSharedKey2 extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'sharedKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass FreshActionSharedKey2Fails extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'sharedKey';\n\n  @override\n  AppState reduce() {\n    throw const UserException('Intentional failure');\n  }\n}\n\n// Action with freshKeyParams\nclass FreshActionWithParams extends ReduxAction<AppState> with Fresh {\n  final String param;\n\n  FreshActionWithParams(this.param);\n\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object? freshKeyParams() => param;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Actions for concurrency test - slow action that takes time to complete\nclass FreshActionConcurrentSlow extends ReduxAction<AppState> with Fresh {\n  final bool shouldFail;\n  final int delayMillis;\n  final int freshForMillis;\n\n  FreshActionConcurrentSlow({\n    required this.shouldFail,\n    required this.delayMillis,\n    required this.freshForMillis,\n  });\n\n  @override\n  int get freshFor => freshForMillis;\n\n  @override\n  Object computeFreshKey() => 'concurrentKey';\n\n  @override\n  Future<AppState> reduce() async {\n    // Simulate long-running operation\n    await Future.delayed(Duration(milliseconds: delayMillis));\n\n    if (shouldFail) {\n      throw const UserException('Intentional failure from slow action');\n    }\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Fast action for concurrency test\nclass FreshActionConcurrentFast extends ReduxAction<AppState> with Fresh {\n  final bool shouldFail;\n\n  FreshActionConcurrentFast({required this.shouldFail});\n\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'concurrentKey';\n\n  @override\n  AppState reduce() {\n    if (shouldFail) {\n      throw const UserException('Intentional failure from fast action');\n    }\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Slow action with ignoreFresh=true for concurrency tests\nclass FreshActionConcurrentSlowIgnoreFresh extends ReduxAction<AppState>\n    with Fresh {\n  final bool shouldFail;\n  final int delayMillis;\n  final int freshForMillis;\n\n  FreshActionConcurrentSlowIgnoreFresh({\n    required this.shouldFail,\n    required this.delayMillis,\n    required this.freshForMillis,\n  });\n\n  @override\n  int get freshFor => freshForMillis;\n\n  @override\n  bool get ignoreFresh => true;\n\n  @override\n  Object computeFreshKey() => 'concurrentKey';\n\n  @override\n  Future<AppState> reduce() async {\n    // Simulate long-running operation\n    await Future.delayed(Duration(milliseconds: delayMillis));\n\n    if (shouldFail) {\n      throw const UserException(\n          'Intentional failure from slow ignoreFresh action');\n    }\n\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Outer action: reserves freshness, then dispatches OverrideAction, then fails.\nclass OuterActionNested extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000; // Long enough so it won't expire during the test.\n\n  @override\n  Object computeFreshKey() => 'nestedKey';\n\n  @override\n  bool get ignoreFresh => false;\n\n  @override\n  Future<AppState> reduce() async {\n    // \"External\" writer: overrides the freshness while this action is running.\n    await dispatch(OverrideAction());\n\n    // Now fail, after OverrideAction has already succeeded.\n    throw Exception('OuterActionNested fails after override');\n  }\n}\n\n// Override action: always runs, sets freshness, increments count.\nclass OverrideAction extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'nestedKey';\n\n  @override\n  bool get ignoreFresh => true; // Always run and reset freshness.\n\n  @override\n  AppState reduce() {\n    // This is our \"external\" writer that sets the final freshness.\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Check action: normal Fresh semantics, used to verify freshness state.\nclass CheckAction extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'nestedKey';\n\n  @override\n  bool get ignoreFresh => false;\n\n  @override\n  AppState reduce() {\n    // Should only run if the key is stale.\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Action that combines Fresh with Throttle (incompatible)\nclass FreshWithThrottleAction extends ReduxAction<AppState>\n    with\n        Throttle,\n        // ignore: private_collision_in_mixin_application\n        Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  int get throttle => 1000;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Action that combines Fresh with NonReentrant (incompatible)\nclass FreshWithNonReentrantAction extends ReduxAction<AppState>\n    with\n        NonReentrant,\n        // ignore: private_collision_in_mixin_application\n        Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Action that combines Fresh with UnlimitedRetryCheckInternet (incompatible)\nclass FreshWithUnlimitedRetryAction extends ReduxAction<AppState>\n    with\n        UnlimitedRetryCheckInternet,\n        // ignore: private_collision_in_mixin_application\n        Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Action that combines Throttle with NonReentrant (incompatible)\nclass ThrottleWithNonReentrantAction extends ReduxAction<AppState>\n    with\n        Throttle,\n        // ignore: private_collision_in_mixin_application\n        NonReentrant {\n  @override\n  int get throttle => 1000;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Action that combines Throttle with UnlimitedRetryCheckInternet (incompatible)\nclass ThrottleWithUnlimitedRetryAction extends ReduxAction<AppState>\n    with\n        Throttle,\n        // ignore: private_collision_in_mixin_application\n        UnlimitedRetryCheckInternet {\n  @override\n  int get throttle => 1000;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Action that combines NonReentrant with UnlimitedRetryCheckInternet (incompatible)\nclass NonReentrantWithUnlimitedRetryAction extends ReduxAction<AppState>\n    with\n        NonReentrant,\n        // ignore: private_collision_in_mixin_application\n        UnlimitedRetryCheckInternet {\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Action that combines CheckInternet with AbortWhenNoInternet (incompatible)\nclass CheckInternetWithAbortWhenNoInternetAction extends ReduxAction<AppState>\n    with\n        CheckInternet,\n        // ignore: private_collision_in_mixin_application\n        AbortWhenNoInternet {\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Action that combines CheckInternet with UnlimitedRetryCheckInternet (incompatible)\nclass CheckInternetWithUnlimitedRetryAction extends ReduxAction<AppState>\n    with\n        CheckInternet,\n        // ignore: private_collision_in_mixin_application\n        UnlimitedRetryCheckInternet {\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Action that combines AbortWhenNoInternet with UnlimitedRetryCheckInternet (incompatible)\nclass AbortWhenNoInternetWithUnlimitedRetryAction extends ReduxAction<AppState>\n    with\n        AbortWhenNoInternet,\n        // ignore: private_collision_in_mixin_application\n        UnlimitedRetryCheckInternet {\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// =============================================================================\n// Actions for Case 29: Rapid consecutive dispatches (regression test)\n// =============================================================================\n\nclass RapidTestActionSlow extends ReduxAction<AppState> with Fresh {\n  final bool shouldFail;\n\n  RapidTestActionSlow({required this.shouldFail});\n\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'rapidKey';\n\n  @override\n  Future<AppState> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    if (shouldFail) {\n      throw const UserException('Slow action fails');\n    }\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass RapidTestActionFast extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'rapidKey';\n\n  @override\n  bool get ignoreFresh => true;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass RapidTestActionCheck extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'rapidKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// =============================================================================\n// Actions for Cases 30-31: Triple nesting tests\n// =============================================================================\n\nclass TripleNestOuterAction extends ReduxAction<AppState> with Fresh {\n  final bool middleShouldFail;\n  final bool innerShouldFail;\n  final bool outerShouldFail;\n\n  TripleNestOuterAction({\n    required this.middleShouldFail,\n    required this.innerShouldFail,\n    required this.outerShouldFail,\n  });\n\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'tripleNestKey';\n\n  @override\n  Future<AppState?> reduce() async {\n    try {\n      await dispatch(TripleNestMiddleAction(\n        shouldFail: middleShouldFail,\n        innerShouldFail: innerShouldFail,\n      ));\n    } catch (_) {\n      // Middle failed, but outer continues\n    }\n\n    if (outerShouldFail) {\n      throw const UserException('Outer fails');\n    }\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass TripleNestMiddleAction extends ReduxAction<AppState> with Fresh {\n  final bool shouldFail;\n  final bool innerShouldFail;\n\n  TripleNestMiddleAction({\n    required this.shouldFail,\n    required this.innerShouldFail,\n  });\n\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'tripleNestKey';\n\n  @override\n  bool get ignoreFresh => true;\n\n  @override\n  Future<AppState?> reduce() async {\n    try {\n      await dispatch(TripleNestInnerAction(shouldFail: innerShouldFail));\n    } catch (_) {\n      // Inner failed, but middle continues\n    }\n\n    if (shouldFail) {\n      throw const UserException('Middle fails');\n    }\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass TripleNestInnerAction extends ReduxAction<AppState> with Fresh {\n  final bool shouldFail;\n\n  TripleNestInnerAction({required this.shouldFail});\n\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'tripleNestKey';\n\n  @override\n  bool get ignoreFresh => true;\n\n  @override\n  AppState reduce() {\n    if (shouldFail) {\n      throw const UserException('Inner fails');\n    }\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass TripleNestCheckAction extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'tripleNestKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// =============================================================================\n// Actions for Case 32: Restore preserves freshness\n// =============================================================================\n\nclass RestoreTestActionA extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 50; // Short, so B can run after it expires\n\n  @override\n  Object computeFreshKey() => 'restoreKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass RestoreTestActionB extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 500; // Longer fresh period\n\n  @override\n  Object computeFreshKey() => 'restoreKey';\n\n  @override\n  AppState reduce() {\n    // This will set new freshness, then fail, which should restore A's\n    throw const UserException('B fails intentionally');\n  }\n}\n\nclass RestoreTestActionC extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'restoreKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// =============================================================================\n// Actions for Case 33: Sequential failures\n// =============================================================================\n\nclass SequentialFailAction extends ReduxAction<AppState> with Fresh {\n  final String id;\n\n  SequentialFailAction({required this.id});\n\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'sequentialKey';\n\n  @override\n  AppState reduce() {\n    throw UserException('Action $id fails');\n  }\n}\n\nclass SequentialSucceedAction extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'sequentialKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// =============================================================================\n// Actions for Case 34: Nested ignoreFresh failure\n// =============================================================================\n\nclass OuterNormalInnerIgnoreFreshAction extends ReduxAction<AppState>\n    with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'outerNormalKey';\n\n  @override\n  Future<AppState?> reduce() async {\n    try {\n      await dispatch(InnerIgnoreFreshFailAction());\n    } catch (_) {\n      // Inner failed, outer continues\n    }\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass InnerIgnoreFreshFailAction extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'outerNormalKey';\n\n  @override\n  bool get ignoreFresh => true;\n\n  @override\n  AppState reduce() {\n    throw const UserException('Inner ignoreFresh fails');\n  }\n}\n\nclass OuterNormalCheckAction extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'outerNormalKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// =============================================================================\n// Actions for Case 35: Multiple concurrent ignoreFresh\n// =============================================================================\n\nclass ConcurrentIgnoreFreshAction extends ReduxAction<AppState> with Fresh {\n  final int id;\n  final int delayMs;\n  final bool shouldFail;\n\n  ConcurrentIgnoreFreshAction({\n    required this.id,\n    required this.delayMs,\n    required this.shouldFail,\n  });\n\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'concurrentIgnoreFreshKey';\n\n  @override\n  bool get ignoreFresh => true;\n\n  @override\n  Future<AppState> reduce() async {\n    await Future.delayed(Duration(milliseconds: delayMs));\n    if (shouldFail) {\n      throw UserException('Action $id fails');\n    }\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass ConcurrentIgnoreFreshCheck extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'concurrentIgnoreFreshKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// =============================================================================\n// Actions for Case 36: Aborted action doesn't interfere\n// =============================================================================\n\nclass AbortTestActionA extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'abortTestKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass AbortTestActionB extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'abortTestKey';\n\n  // Will be aborted because A's key is fresh\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass AbortTestActionCheck extends ReduxAction<AppState> with Fresh {\n  @override\n  int get freshFor => 1000;\n\n  @override\n  Object computeFreshKey() => 'abortTestKey';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n"
  },
  {
    "path": "test/local_json_persist_test.dart",
    "content": "// Please run this test file by itself, not together with other tests.\nimport 'dart:io';\nimport 'dart:math';\nimport 'dart:typed_data';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:async_redux/local_json_persist.dart';\nimport 'package:async_redux/src/local_persist.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'test_utils.dart';\n\nenum files { abcd, xyzk }\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  group('Do not run on CI', skip: isCI, () {\n    test('Encode and decode state.', () async {\n      //\n      List<Object> simpleObj = [\n        'Hello',\n        'How are you?',\n        [\n          1,\n          2,\n          3,\n          {'name': 'John'}\n        ],\n        42,\n        true,\n        false\n      ];\n\n      Uint8List encoded = LocalJsonPersist.encodeJson(simpleObj);\n      Object? decoded = LocalJsonPersist.decodeJson(encoded);\n      expect(decoded, simpleObj);\n\n      expect(\n          (decoded as List)\n              .map((obj) => \"$obj (${obj.runtimeType})\")\n              .join(\"\\n\"),\n          'Hello (String)\\n'\n          'How are you? (String)\\n'\n          '[1, 2, 3, {name: John}] (List<dynamic>)\\n'\n          '42 (int)\\n'\n          'true (bool)\\n'\n          'false (bool)');\n    });\n\n    test('Save and load state.', () async {\n      //\n      // Use a random number to make sure it's not checking already saved files.\n      int randNumber = Random().nextInt(100000);\n\n      List<Object> simpleObj = [\n        'Goodbye',\n        '\"Life is what happens\\n\\rwhen you\\'re busy making other plans.\" -John Lennon',\n        [\n          100,\n          200,\n          {\"name\": \"João\"}\n        ],\n        true,\n        randNumber,\n      ];\n\n      var persist = LocalJsonPersist(\"abcd\");\n\n      await persist.save(simpleObj);\n\n      Object? decoded = await persist.load();\n\n      expect(decoded, simpleObj);\n\n      expect(\n          (decoded as List)\n              .map((obj) => \"$obj (${obj.runtimeType})\")\n              .join(\"\\n\"),\n          'Goodbye (String)\\n'\n          '\"Life is what happens\\n\\rwhen you\\'re busy making other plans.\" -John Lennon (String)\\n'\n          '[100, 200, {name: João}] (List<dynamic>)\\n'\n          'true (bool)\\n'\n          '$randNumber (int)');\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Test file can be defined by String or enum.', () async {\n      //\n      File file = await (LocalJsonPersist(\"abcd\").file());\n      expect(\n          file.path.endsWith(\"\\\\db\\\\abcd.json\") ||\n              file.path.endsWith(\"/db/abcd.json\"),\n          isTrue);\n\n      file = await (LocalJsonPersist(files.abcd).file());\n      expect(\n          file.path.endsWith(\"\\\\db\\\\abcd.json\") ||\n              file.path.endsWith(\"/db/abcd.json\"),\n          isTrue);\n\n      file = await (LocalJsonPersist(files.xyzk, dbSubDir: \"kkk\").file());\n      expect(\n          file.path.endsWith(\"\\\\kkk\\\\xyzk.json\") ||\n              file.path.endsWith(\"/kkk/xyzk.json\"),\n          isTrue);\n    });\n\n    test('Test dbDir and subDirs.', () async {\n      //\n      File file = await (LocalJsonPersist(\"xyzk\").file());\n      expect(\n          file.path.endsWith(\"\\\\xyzk.json\") || file.path.endsWith(\"/xyzk.json\"),\n          isTrue);\n\n      file = await (LocalJsonPersist(\"xyzk\", dbSubDir: \"kkk\").file());\n      expect(\n          file.path.endsWith(\"\\\\kkk\\\\xyzk.json\") ||\n              file.path.endsWith(\"/kkk/xyzk.json\"),\n          isTrue);\n\n      file = await (LocalJsonPersist(\"xyzk\", dbSubDir: \"kkk\", subDirs: [\"mno\"])\n          .file());\n      expect(\n          file.path.endsWith(\"\\\\kkk\\\\mno\\\\xyzk.json\") ||\n              file.path.endsWith(\"/kkk/mno/xyzk.json\"),\n          isTrue);\n\n      file = await (LocalJsonPersist(\"xyzk\",\n          dbSubDir: \"kkk\", subDirs: [\"m\", \"n\", \"o\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\kkk\\\\m\\\\n\\\\o\\\\xyzk.json\") ||\n              file.path.endsWith(\"/kkk/m/n/o/xyzk.json\"),\n          isTrue);\n\n      file = await (LocalJsonPersist(\"xyzk\", subDirs: [\"mno\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\db\\\\mno\\\\xyzk.json\") ||\n              file.path.endsWith(\"/db/mno/xyzk.json\"),\n          isTrue);\n\n      file = await (LocalJsonPersist(\"xyzk\", subDirs: [\"m\", \"n\", \"o\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\db\\\\m\\\\n\\\\o\\\\xyzk.json\") ||\n              file.path.endsWith(\"/db/m/n/o/xyzk.json\"),\n          isTrue);\n\n      String saveDefaultDbSubDir = LocalJsonPersist.defaultDbSubDir;\n\n      LocalJsonPersist.defaultDbSubDir = \"myDir\";\n\n      file = await (LocalJsonPersist(\"xyzk\", subDirs: [\"mno\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\myDir\\\\mno\\\\xyzk.json\") ||\n              file.path.endsWith(\"/myDir/mno/xyzk.json\"),\n          isTrue);\n\n      file = await (LocalJsonPersist(\"xyzk\", subDirs: [\"m\", \"n\", \"o\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\myDir\\\\m\\\\n\\\\o\\\\xyzk.json\") ||\n              file.path.endsWith(\"/myDir/m/n/o/xyzk.json\"),\n          isTrue);\n\n      LocalJsonPersist.defaultDbSubDir = \"\";\n\n      file = await (LocalJsonPersist(\"xyzk\", subDirs: [\"mno\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\mno\\\\xyzk.json\") ||\n              file.path.endsWith(\"/mno/xyzk.json\"),\n          isTrue);\n      expect(\n          file.path.endsWith(\"\\\\db\\\\mno\\\\xyzk.json\") ||\n              file.path.endsWith(\"/db/mno/xyzk.json\"),\n          isFalse);\n\n      print('file.path = ${file.path}');\n      file = await (LocalJsonPersist(\"xyzk\", subDirs: [\"m\", \"n\", \"o\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\m\\\\n\\\\o\\\\xyzk.json\") ||\n              file.path.endsWith(\"/m/n/o/xyzk.json\"),\n          isTrue);\n      expect(\n          file.path.endsWith(\"\\\\db\\\\m\\\\n\\\\o\\\\xyzk.json\") ||\n              file.path.endsWith(\"/db/m/n/o/xyzk.json\"),\n          isFalse);\n\n      LocalJsonPersist.defaultDbSubDir = \"\";\n\n      file = await (LocalJsonPersist(\"xyzk\", subDirs: [\"mno\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\mno\\\\xyzk.json\") ||\n              file.path.endsWith(\"/mno/xyzk.json\"),\n          isTrue);\n      expect(\n          file.path.endsWith(\"\\\\db\\\\mno\\\\xyzk.json\") ||\n              file.path.endsWith(\"/db/mno/xyzk.json\"),\n          isFalse);\n\n      print('file.path = ${file.path}');\n      file = await (LocalJsonPersist(\"xyzk\", subDirs: [\"m\", \"n\", \"o\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\m\\\\n\\\\o\\\\xyzk.json\") ||\n              file.path.endsWith(\"/m/n/o/xyzk.json\"),\n          isTrue);\n      expect(\n          file.path.endsWith(\"\\\\db\\\\m\\\\n\\\\o\\\\xyzk.json\") ||\n              file.path.endsWith(\"/db/m/n/o/xyzk.json\"),\n          isFalse);\n\n      LocalJsonPersist.defaultDbSubDir = saveDefaultDbSubDir;\n    });\n\n    test('Add objects to save, and load from file name.', () async {\n      //\n      // User random numbers to make sure it's not checking already saved files.\n      var rand = Random();\n      int randNumber1 = rand.nextInt(1000);\n      int randNumber2 = rand.nextInt(1000);\n      int randNumber3 = rand.nextInt(1000);\n\n      var persist = LocalJsonPersist(\"xyzk\");\n      await persist.save([randNumber1, randNumber2, randNumber3]);\n\n      Object? decoded = await persist.load();\n\n      expect(decoded, [randNumber1, randNumber2, randNumber3]);\n\n      expect(\n          (decoded as List)\n              .map((obj) => \"$obj (${obj.runtimeType})\")\n              .join(\"\\n\"),\n          '$randNumber1 (int)\\n'\n          '$randNumber2 (int)\\n'\n          '$randNumber3 (int)');\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Test create, overwrite and delete the file.', () async {\n      //\n      var persist = LocalJsonPersist(\"klm\");\n\n      // Create.\n      await persist.save([123]);\n      var decoded = await persist.load();\n      expect(decoded, [123]);\n\n      // Overwrite.\n      await persist.save([789]);\n      decoded = await persist.load();\n      expect(decoded, [789]);\n\n      // Delete.\n      File file = await (persist.file());\n      expect(file.existsSync(), true);\n      await persist.delete();\n      expect(file.existsSync(), false);\n    });\n\n    test(\"Load/Length/Exists file that doesn't exist, or exists and is empty.\",\n        () async {\n      //\n      // File doesn't exist.\n      var persist = LocalJsonPersist(\"doesNotExist\");\n      expect(await persist.load(), isNull);\n      expect(await persist.length(), 0);\n      expect(await persist.exists(), false);\n\n      // File exists and is empty.\n      persist = LocalJsonPersist(\"my_file\");\n      await persist.save([]);\n      expect(await persist.load(), []);\n      expect(await persist.length(), 2);\n      expect(await persist.exists(), true);\n\n      // File exists and contains Json null, which is 4 chars: n, u, l and l.\n      persist = LocalJsonPersist(\"my_file\");\n      await persist.save(null);\n      expect(await persist.load(), null);\n      expect(await persist.length(), 4);\n      expect(await persist.exists(), true);\n    });\n\n    test(\"Deletes a file that exists or doesn't exist.\", () async {\n      //\n      // File doesn't exist.\n      var persist = LocalJsonPersist(\"doesNotExist\");\n      expect(await persist.delete(), isFalse);\n\n      // File exists and is deleted.\n      persist = LocalJsonPersist(\"my_file\");\n      await persist.save([]);\n      expect(await persist.delete(), isTrue);\n    });\n\n    test('Load as object.', () async {\n      //\n      // Use a random number to make sure it's not checking already saved files.\n      int randNumber = Random().nextInt(100000);\n\n      Map<String, dynamic> simpleObj = {\n        \"one\": 1,\n        \"two\": randNumber,\n      };\n\n      var persist = LocalJsonPersist(\"obj\");\n      await persist.save(simpleObj);\n\n      Map<String, dynamic>? decoded = await persist.loadAsObj();\n\n      expect(decoded, simpleObj);\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Loading an object-which-is-not-a-map as single object, fails.',\n        () async {\n      //\n      List<Object> simpleObj = [\n        {\n          \"one\": 1,\n          \"two\": 2,\n        },\n        {\n          \"three\": 1,\n          \"four\": 2,\n        }\n      ];\n\n      var persist = LocalJsonPersist(\"obj\");\n      await persist.save(simpleObj);\n\n      dynamic error;\n      try {\n        await persist.loadAsObj();\n      } catch (_error) {\n        error = _error;\n      }\n      expect(\n          error,\n          PersistException(\n              \"Not an object: [{one: 1, two: 2}, {three: 1, four: 2}]\"));\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Load as object (map) something which is not an object.', () async {\n      //\n      List<Object> simpleObj = [\"hey\"];\n\n      var persist = LocalJsonPersist(\"obj\");\n      await persist.save(simpleObj);\n\n      dynamic error;\n      try {\n        await persist.loadAsObj();\n      } catch (_error) {\n        error = _error;\n      }\n      expect(error, PersistException(\"Not an object: [hey]\"));\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Encode and decode as JSON.', () async {\n      //\n      List<Object> simpleObj = [\n        'Hello',\n        'How are you?',\n        [\n          1,\n          2,\n          3,\n          {'name': 'John'}\n        ],\n        42,\n        true,\n        false\n      ];\n\n      Uint8List encoded = LocalJsonPersist.encodeJson(simpleObj);\n      Object? decoded = LocalJsonPersist.decodeJson(encoded);\n      expect(decoded, simpleObj);\n\n      expect(\n          (decoded as List)\n              .map((obj) => \"$obj (${obj.runtimeType})\")\n              .join(\"\\n\"),\n          'Hello (String)\\n'\n          'How are you? (String)\\n'\n          '[1, 2, 3, {name: John}] (List<dynamic>)\\n'\n          '42 (int)\\n'\n          'true (bool)\\n'\n          'false (bool)');\n    });\n\n    test('Save and load a single string into/from JSON.', () async {\n      //\n      Object simpleObj = 'Goodbye';\n      var persist = LocalJsonPersist(\"abcd\");\n      await persist.save(simpleObj);\n      Object? decoded = await persist.load();\n      expect(decoded, simpleObj);\n      expect(decoded, 'Goodbye');\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('loadConverting from .json file', () async {\n      var simpleObj = {'Hello': 123};\n      var persist = LocalJsonPersist(\"abcd\");\n      await persist.save(simpleObj);\n      Object? decoded = await persist.loadConverting(isList: false);\n      expect(decoded, simpleObj);\n\n      expect(await persist.exists(), isTrue);\n      expect((await persist.file()).toString(), endsWith('\\\\db\\\\abcd.json\\''));\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('loadConverting from .db (json-sequence) file', () async {\n      //\n      var simpleObj = {'Hello': 123};\n\n      // Save inside a List.\n      var listWithOneElement = [simpleObj];\n      var persistSequence = LocalPersist(\"abcd\");\n      await persistSequence.save(listWithOneElement);\n\n      // The '.db' file exists.\n      expect(await persistSequence.exists(), isTrue);\n      expect((await persistSequence.file()).toString(),\n          endsWith('\\\\db\\\\abcd.db\\''));\n\n      // ---\n\n      // The '.json' file does NOT exist.\n      var persist = LocalJsonPersist(\"abcd\");\n      expect(await persist.exists(), isFalse);\n\n      // When we load converting...\n      Object? decoded = await persist.loadConverting(isList: false);\n      expect(decoded, simpleObj);\n\n      // The '.json' file now exists.\n      expect(await persist.exists(), isTrue);\n      expect((await persist.file()).toString(), endsWith('\\\\db\\\\abcd.json\\''));\n\n      // But the '.db' file was deleted.\n      expect(await persistSequence.exists(), isFalse);\n\n      // ---\n\n      // We now can read the '.json' file again.\n      persist = LocalJsonPersist(\"abcd\");\n      expect(await persist.exists(), isTrue);\n\n      // And it works just the same.\n      decoded = await persist.loadConverting(isList: false);\n      expect(decoded, simpleObj);\n\n      // ---\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test(\n        'loadConverting from .db (json-sequence) file fails for more than 1 object',\n        () async {\n      var simpleObj = ['Hello', 123];\n      var persistSequence = LocalPersist(\"abcd\");\n      await persistSequence.save(simpleObj);\n\n      dynamic _error;\n      var persist = LocalJsonPersist(\"abcd\");\n      try {\n        await persist.loadConverting(isList: false);\n      } catch (error) {\n        _error = error;\n        expect(error is PersistException, isTrue);\n        expect(error.toString(),\n            'Json sequence to Json: 2 objects: [Hello, 123].');\n      }\n\n      expect(_error, isNot(null));\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('loadAsObjConverting from .db (json-sequence) file', () async {\n      //\n      var simpleObj = {'Hello': 123};\n\n      // Save inside a List.\n      var listWithOneElement = [simpleObj];\n      var persistSequence = LocalPersist(\"abcd\");\n      await persistSequence.save(listWithOneElement);\n\n      // The '.db' file exists.\n      expect(await persistSequence.exists(), isTrue);\n      expect((await persistSequence.file()).toString(),\n          endsWith('\\\\db\\\\abcd.db\\''));\n\n      // ---\n\n      // The '.json' file does NOT exist.\n      var persist = LocalJsonPersist(\"abcd\");\n      expect(await persist.exists(), isFalse);\n\n      // When we load converting...\n      Map<String, dynamic>? decoded = await persist.loadAsObjConverting();\n      expect(decoded, simpleObj);\n\n      // The '.json' file now exists.\n      expect(await persist.exists(), isTrue);\n      expect((await persist.file()).toString(), endsWith('\\\\db\\\\abcd.json\\''));\n\n      // But the '.db' file was deleted.\n      expect(await persistSequence.exists(), isFalse);\n\n      // ---\n\n      // We now can read the '.json' file again.\n      persist = LocalJsonPersist(\"abcd\");\n      expect(await persist.exists(), isTrue);\n\n      // And it works just the same.\n      decoded = await persist.loadAsObjConverting();\n      expect(decoded, simpleObj);\n\n      // ---\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('loadConverting from .json file', () async {\n      var simpleObj = {'Hello': 123};\n      var persist = LocalJsonPersist(\"abcd\");\n      await persist.save(simpleObj);\n      Object? decoded = await persist.loadConverting(isList: false);\n      expect(decoded, simpleObj);\n\n      expect(await persist.exists(), isTrue);\n      expect((await persist.file()).toString(), endsWith('\\\\db\\\\abcd.json\\''));\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('loadConverting from .db (json-sequence) file', () async {\n      //\n      var simpleObj = {'Hello': 123};\n\n      // Save inside a List.\n      var listWithOneElement = [simpleObj];\n      var persistSequence = LocalPersist(\"abcd\");\n      await persistSequence.save(listWithOneElement);\n\n      // The '.db' file exists.\n      expect(await persistSequence.exists(), isTrue);\n      expect((await persistSequence.file()).toString(),\n          endsWith('\\\\db\\\\abcd.db\\''));\n\n      // ---\n\n      // The '.json' file does NOT exist.\n      var persist = LocalJsonPersist(\"abcd\");\n      expect(await persist.exists(), isFalse);\n\n      // When we load converting...\n      Object? decoded = await persist.loadConverting(isList: true);\n      expect(decoded, [simpleObj]);\n\n      // The '.json' file now exists.\n      expect(await persist.exists(), isTrue);\n      expect((await persist.file()).toString(), endsWith('\\\\db\\\\abcd.json\\''));\n\n      // But the '.db' file was deleted.\n      expect(await persistSequence.exists(), isFalse);\n\n      // ---\n\n      // We now can read the '.json' file again.\n      persist = LocalJsonPersist(\"abcd\");\n      expect(await persist.exists(), isTrue);\n\n      // And it works just the same.\n      decoded = await persist.loadConverting(isList: true);\n      expect(decoded, [simpleObj]);\n\n      // ---\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('loadConverting from .db (json-sequence) file for a single object',\n        () async {\n      //\n      var simpleObj = ['Hello'];\n      var persistSequence = LocalPersist(\"abcd\");\n      await persistSequence.save(simpleObj);\n\n      var persist = LocalJsonPersist(\"abcd\");\n      var decoded = await persist.loadConverting(isList: true);\n\n      expect(decoded, simpleObj);\n\n      // ---\n\n      var persistJson = LocalJsonPersist(simpleObj);\n      await persistJson.save(simpleObj);\n\n      decoded = await persistJson.load();\n      expect(decoded, simpleObj);\n\n      decoded = await persistJson.loadConverting(isList: true);\n      expect(decoded, simpleObj);\n\n      // ---\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('loadConverting from .db (json-sequence) file for more than 1 object',\n        () async {\n      //\n      var simpleObj = ['Hello', 123];\n      var persistSequence = LocalPersist(\"abcd\");\n      await persistSequence.save(simpleObj);\n\n      var persist = LocalJsonPersist(\"abcd\");\n      var decoded = await persist.loadConverting(isList: true);\n\n      expect(decoded, simpleObj);\n\n      // ---\n\n      var persistJson = LocalJsonPersist(simpleObj);\n      await persistJson.save(simpleObj);\n\n      decoded = await persistJson.load();\n      expect(decoded, simpleObj);\n\n      decoded = await persistJson.loadConverting(isList: true);\n      expect(decoded, simpleObj);\n\n      // ---\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test(\n        'loadConverting from .db (json-sequence) file for a list inside a list',\n        () async {\n      //\n      var simpleObj = [\n        ['Hello', 123]\n      ];\n\n      var persistSequence = LocalPersist(\"abcd\");\n      await persistSequence.save(simpleObj);\n\n      var persist = LocalJsonPersist(\"abcd\");\n      var decoded = await persist.loadConverting(isList: true);\n\n      expect(decoded, simpleObj);\n\n      // ---\n\n      var persistJson = LocalJsonPersist(simpleObj);\n      await persistJson.save(simpleObj);\n\n      decoded = await persistJson.load();\n      expect(decoded, simpleObj);\n\n      decoded = await persistJson.loadConverting(isList: true);\n      expect(decoded, simpleObj);\n\n      // ---\n\n      // Cleans up test.\n      await persist.delete();\n    });\n  });\n}\n"
  },
  {
    "path": "test/local_persist_test.dart",
    "content": "// Please run this test file by itself, not together with other tests.\nimport 'dart:io';\nimport 'dart:math';\nimport 'dart:typed_data';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:async_redux/local_persist.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'test_utils.dart';\n\nenum files { abc, xyz }\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  group('Do not run on CI', skip: isCI, () {\n    test('Encode and decode state.', () async {\n      //\n      List<Object> simpleObjs = [\n        'Hello',\n        'How are you?',\n        [\n          1,\n          2,\n          3,\n          {'name': 'John'}\n        ],\n        42,\n        true,\n        false\n      ];\n\n      Uint8List encoded = LocalPersist.encode(simpleObjs);\n      List<Object?> decoded = LocalPersist.decode(encoded);\n      expect(decoded, simpleObjs);\n\n      expect(\n          decoded.map((obj) => \"$obj (${obj.runtimeType})\").join(\"\\n\"),\n          'Hello (String)\\n'\n          'How are you? (String)\\n'\n          '[1, 2, 3, {name: John}] (List<dynamic>)\\n'\n          '42 (int)\\n'\n          'true (bool)\\n'\n          'false (bool)');\n    });\n\n    test('Save and load state.', () async {\n      //\n      // Use a random number to make sure it's not checking already saved files.\n      int randNumber = Random().nextInt(100000);\n\n      List<Object> simpleObjs = [\n        'Goodbye',\n        '\"Life is what happens\\n\\rwhen you\\'re busy making other plans.\" -John Lennon',\n        [\n          100,\n          200,\n          {\"name\": \"João\"}\n        ],\n        true,\n        randNumber,\n      ];\n\n      var persist = LocalPersist(\"abc\");\n\n      await persist.save(simpleObjs);\n\n      List<Object?> decoded = (await persist.load())!;\n\n      expect(decoded, simpleObjs);\n\n      expect(\n          decoded.map((obj) => \"$obj (${obj.runtimeType})\").join(\"\\n\"),\n          'Goodbye (String)\\n'\n          '\"Life is what happens\\n\\rwhen you\\'re busy making other plans.\" -John Lennon (String)\\n'\n          '[100, 200, {name: João}] (List<dynamic>)\\n'\n          'true (bool)\\n'\n          '$randNumber (int)');\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Test file can be defined by String or enum.', () async {\n      //\n      File file = await (LocalPersist(\"abc\").file());\n      expect(\n          file.path.endsWith(\"\\\\db\\\\abc.db\") ||\n              file.path.endsWith(\"/db/abc.db\"),\n          isTrue);\n\n      file = await (LocalPersist(files.abc).file());\n      expect(\n          file.path.endsWith(\"\\\\db\\\\abc.db\") ||\n              file.path.endsWith(\"/db/abc.db\"),\n          isTrue);\n\n      file = await (LocalPersist(files.xyz, dbSubDir: \"kkk\").file());\n      expect(\n          file.path.endsWith(\"\\\\kkk\\\\xyz.db\") ||\n              file.path.endsWith(\"/kkk/xyz.db\"),\n          isTrue);\n    });\n\n    test('Test dbDir and subDirs.', () async {\n      //\n      File file = await (LocalPersist(\"xyz\").file());\n      expect(file.path.endsWith(\"\\\\xyz.db\") || file.path.endsWith(\"/xyz.db\"),\n          isTrue);\n\n      file = await (LocalPersist(\"xyz\", dbSubDir: \"kkk\").file());\n      expect(\n          file.path.endsWith(\"\\\\kkk\\\\xyz.db\") ||\n              file.path.endsWith(\"/kkk/xyz.db\"),\n          isTrue);\n\n      file =\n          await (LocalPersist(\"xyz\", dbSubDir: \"kkk\", subDirs: [\"mno\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\kkk\\\\mno\\\\xyz.db\") ||\n              file.path.endsWith(\"/kkk/mno/xyz.db\"),\n          isTrue);\n\n      file =\n          await (LocalPersist(\"xyz\", dbSubDir: \"kkk\", subDirs: [\"m\", \"n\", \"o\"])\n              .file());\n      expect(\n          file.path.endsWith(\"\\\\kkk\\\\m\\\\n\\\\o\\\\xyz.db\") ||\n              file.path.endsWith(\"/kkk/m/n/o/xyz.db\"),\n          isTrue);\n\n      file = await (LocalPersist(\"xyz\", subDirs: [\"mno\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\db\\\\mno\\\\xyz.db\") ||\n              file.path.endsWith(\"/db/mno/xyz.db\"),\n          isTrue);\n\n      file = await (LocalPersist(\"xyz\", subDirs: [\"m\", \"n\", \"o\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\db\\\\m\\\\n\\\\o\\\\xyz.db\") ||\n              file.path.endsWith(\"/db/m/n/o/xyz.db\"),\n          isTrue);\n\n      String saveDefaultDbSubDir = LocalPersist.defaultDbSubDir;\n\n      LocalPersist.defaultDbSubDir = \"myDir\";\n\n      file = await (LocalPersist(\"xyz\", subDirs: [\"mno\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\myDir\\\\mno\\\\xyz.db\") ||\n              file.path.endsWith(\"/myDir/mno/xyz.db\"),\n          isTrue);\n\n      file = await (LocalPersist(\"xyz\", subDirs: [\"m\", \"n\", \"o\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\myDir\\\\m\\\\n\\\\o\\\\xyz.db\") ||\n              file.path.endsWith(\"/myDir/m/n/o/xyz.db\"),\n          isTrue);\n\n      LocalPersist.defaultDbSubDir = \"\";\n\n      file = await (LocalPersist(\"xyz\", subDirs: [\"mno\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\mno\\\\xyz.db\") ||\n              file.path.endsWith(\"/mno/xyz.db\"),\n          isTrue);\n      expect(\n          file.path.endsWith(\"\\\\db\\\\mno\\\\xyz.db\") ||\n              file.path.endsWith(\"/db/mno/xyz.db\"),\n          isFalse);\n\n      print('file.path = ${file.path}');\n      file = await (LocalPersist(\"xyz\", subDirs: [\"m\", \"n\", \"o\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\m\\\\n\\\\o\\\\xyz.db\") ||\n              file.path.endsWith(\"/m/n/o/xyz.db\"),\n          isTrue);\n      expect(\n          file.path.endsWith(\"\\\\db\\\\m\\\\n\\\\o\\\\xyz.db\") ||\n              file.path.endsWith(\"/db/m/n/o/xyz.db\"),\n          isFalse);\n\n      LocalPersist.defaultDbSubDir = \"\";\n\n      file = await (LocalPersist(\"xyz\", subDirs: [\"mno\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\mno\\\\xyz.db\") ||\n              file.path.endsWith(\"/mno/xyz.db\"),\n          isTrue);\n      expect(\n          file.path.endsWith(\"\\\\db\\\\mno\\\\xyz.db\") ||\n              file.path.endsWith(\"/db/mno/xyz.db\"),\n          isFalse);\n\n      print('file.path = ${file.path}');\n      file = await (LocalPersist(\"xyz\", subDirs: [\"m\", \"n\", \"o\"]).file());\n      expect(\n          file.path.endsWith(\"\\\\m\\\\n\\\\o\\\\xyz.db\") ||\n              file.path.endsWith(\"/m/n/o/xyz.db\"),\n          isTrue);\n      expect(\n          file.path.endsWith(\"\\\\db\\\\m\\\\n\\\\o\\\\xyz.db\") ||\n              file.path.endsWith(\"/db/m/n/o/xyz.db\"),\n          isFalse);\n\n      LocalPersist.defaultDbSubDir = saveDefaultDbSubDir;\n    });\n\n    test('Add objects to save, and load from file name.', () async {\n      //\n      // User random numbers to make sure it's not checking already saved files.\n      var rand = Random();\n      int randNumber1 = rand.nextInt(1000);\n      int randNumber2 = rand.nextInt(1000);\n      int randNumber3 = rand.nextInt(1000);\n\n      var persist = LocalPersist(\"xyz\");\n      await persist.save([randNumber1, randNumber2, randNumber3]);\n\n      List<Object?> decoded = (await persist.load())!;\n\n      expect(decoded, [randNumber1, randNumber2, randNumber3]);\n\n      expect(\n          decoded.map((obj) => \"$obj (${obj.runtimeType})\").join(\"\\n\"),\n          '$randNumber1 (int)\\n'\n          '$randNumber2 (int)\\n'\n          '$randNumber3 (int)');\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Test appending, then loading.', () async {\n      //\n      // User random numbers to make sure it's not checking already saved files.\n      var rand = Random();\n      int randNumber1 = rand.nextInt(1000);\n      int randNumber2 = rand.nextInt(1000);\n\n      var persist = LocalPersist(\"lmn\");\n\n      await persist.save([\"Hello\", randNumber1], append: false);\n      await persist.save([\"There\", randNumber2], append: true);\n\n      var simpleObjs = [\n        35,\n        false,\n        {\n          \"x\": 1,\n          \"y\": [1, 2]\n        }\n      ];\n      await persist.save(simpleObjs, append: true);\n\n      List<Object?> decoded = (await persist.load())!;\n\n      expect(decoded, [\n        \"Hello\",\n        randNumber1,\n        \"There\",\n        randNumber2,\n        35,\n        false,\n        {\n          \"x\": 1,\n          \"y\": [1, 2]\n        }\n      ]);\n\n      expect(\n          LocalPersist.simpleObjsToString(decoded),\n          'Hello (String)\\n'\n          '$randNumber1 (int)\\n'\n          'There (String)\\n'\n          '$randNumber2 (int)\\n'\n          '35 (int)\\n'\n          'false (bool)\\n'\n          '{x: 1, y: [1, 2]} (_Map<String, dynamic>)');\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Test create, append, overwrite and delete the file.', () async {\n      //\n      var persist = LocalPersist(\"klm\");\n\n      // Create.\n      await persist.save([123], append: false);\n      var decoded = await persist.load();\n      expect(decoded, [123]);\n\n      // Append.\n      await persist.save([456], append: true);\n      decoded = await persist.load();\n      expect(decoded, [123, 456]);\n\n      // Overwrite.\n      await persist.save([789], append: false);\n      decoded = await persist.load();\n      expect(decoded, [789]);\n\n      // Delete.\n      File file = await (persist.file());\n      expect(file.existsSync(), true);\n      await persist.delete();\n      expect(file.existsSync(), false);\n    });\n\n    test(\"Load/Length/Exists file that doesn't exist, or exists and is empty.\",\n        () async {\n      //\n      // File doesn't exist.\n      var persist = LocalPersist(\"doesNotExist\");\n      expect(await persist.load(), isNull);\n      expect(await persist.length(), 0);\n      expect(await persist.exists(), false);\n\n      // File exists and is empty.\n      persist = LocalPersist(\"my_file\");\n      await persist.save([]);\n      expect(await persist.load(), []);\n      expect(await persist.length(), 0);\n      expect(await persist.exists(), true);\n    });\n\n    test(\"Deletes a file that exists or doesn't exist.\", () async {\n      //\n      // File doesn't exist.\n      var persist = LocalPersist(\"doesNotExist\");\n      expect(await persist.delete(), isFalse);\n\n      // File exists and is deleted.\n      persist = LocalPersist(\"my_file\");\n      await persist.save([]);\n      expect(await persist.delete(), isTrue);\n    });\n\n    test('Load as object.', () async {\n      //\n      // Use a random number to make sure it's not checking already saved files.\n      int randNumber = Random().nextInt(100000);\n\n      List<Object> simpleObjs = [\n        {\n          \"one\": 1,\n          \"two\": randNumber,\n        }\n      ];\n\n      var persist = LocalPersist(\"obj\");\n      await persist.save(simpleObjs);\n\n      Map<String, dynamic> decoded = (await persist.loadAsObj())!;\n\n      expect(decoded, simpleObjs[0]);\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Load many object as single object.', () async {\n      //\n      List<Object> simpleObjs = [\n        {\n          \"one\": 1,\n          \"two\": 2,\n        },\n        {\n          \"three\": 1,\n          \"four\": 2,\n        }\n      ];\n\n      var persist = LocalPersist(\"obj\");\n      await persist.save(simpleObjs);\n\n      dynamic error;\n      try {\n        await persist.loadAsObj();\n      } catch (_error) {\n        error = _error;\n      }\n      expect(\n          error,\n          PersistException(\n              \"Not a single object: [{one: 1, two: 2}, {three: 1, four: 2}]\"));\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Load as object (map) something which is not an object.', () async {\n      //\n      List<Object> simpleObjs = [\"hey\"];\n\n      var persist = LocalPersist(\"obj\");\n      await persist.save(simpleObjs);\n\n      dynamic error;\n      try {\n        await persist.loadAsObj();\n      } catch (_error) {\n        error = _error;\n      }\n      expect(error, PersistException(\"Not an object: hey\"));\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Encode and decode as JSON.', () async {\n      //\n      List<Object> simpleObjs = [\n        'Hello',\n        'How are you?',\n        [\n          1,\n          2,\n          3,\n          {'name': 'John'}\n        ],\n        42,\n        true,\n        false\n      ];\n\n      Uint8List encoded = LocalPersist.encodeJson(simpleObjs);\n      Object? decoded = LocalPersist.decodeJson(encoded);\n      expect(decoded, simpleObjs);\n\n      expect(\n          (decoded as List)\n              .map((obj) => \"$obj (${obj.runtimeType})\")\n              .join(\"\\n\"),\n          'Hello (String)\\n'\n          'How are you? (String)\\n'\n          '[1, 2, 3, {name: John}] (List<dynamic>)\\n'\n          '42 (int)\\n'\n          'true (bool)\\n'\n          'false (bool)');\n    });\n\n    test('Save and load state into/from JSON.', () async {\n      //\n      // Use a random number to make sure it's not checking already saved files.\n      int randNumber = Random().nextInt(100000);\n\n      List<Object> simpleObjs = [\n        'Goodbye',\n        '\"Life is what happens\\n\\rwhen you\\'re busy making other plans.\" -John Lennon',\n        [\n          100,\n          200,\n          {\"name\": \"João\"}\n        ],\n        true,\n        randNumber,\n      ];\n\n      var persist = LocalPersist(\"abc\");\n\n      await persist.saveJson(simpleObjs);\n\n      Object? decoded = await persist.loadJson();\n\n      expect(decoded, simpleObjs);\n\n      expect(\n          (decoded as List)\n              .map((obj) => \"$obj (${obj.runtimeType})\")\n              .join(\"\\n\"),\n          'Goodbye (String)\\n'\n          '\"Life is what happens\\n\\rwhen you\\'re busy making other plans.\" -John Lennon (String)\\n'\n          '[100, 200, {name: João}] (List<dynamic>)\\n'\n          'true (bool)\\n'\n          '$randNumber (int)');\n\n      // Cleans up test.\n      await persist.delete();\n    });\n\n    test('Save and load a single string into/from JSON.', () async {\n      //\n      Object simpleObjs = 'Goodbye';\n      var persist = LocalPersist(\"abc\");\n      await persist.saveJson(simpleObjs);\n      Object? decoded = await persist.loadJson();\n      expect(decoded, simpleObjs);\n      expect(decoded, 'Goodbye');\n\n      // Cleans up test.\n      await persist.delete();\n    });\n  });\n}\n"
  },
  {
    "path": "test/mock_build_context_test.dart",
    "content": "import \"package:async_redux/async_redux.dart\";\nimport \"package:flutter/material.dart\";\nimport \"package:flutter_test/flutter_test.dart\";\n\nvoid main() {\n  group('MockBuildContext', () {\n    test('allows testing widgets with context.state extension', () {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'Mark', age: 30));\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n      expect(widget.name, 'Mark');\n    });\n\n    test('onChange callback dispatches ChangeName action', () async {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'Initial', age: 25));\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n      expect(store.state.name, 'Initial');\n\n      // Trigger action `ChangeName('John')` via onChange callback.\n      widget.onChange();\n\n      // State should be updated (synchronous action completes immediately).\n      expect(store.state.name, 'John');\n    });\n\n    test('rebuilding widget after state change shows new name', () async {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'Original', age: 20));\n      var context = MockBuildContext(store);\n\n      // Build widget with original state.\n      var widget1 = MyConnector().build(context) as MyWidget;\n      expect(widget1.name, 'Original');\n\n      // Dispatch action to change name (synchronous action completes immediately).\n      await store.dispatchAndWait(ChangeName('Updated'));\n\n      // Rebuild widget - should reflect new state.\n      var widget2 = MyConnector().build(context) as MyWidget;\n      expect(widget2.name, 'Updated');\n    });\n\n    test('context.read() returns state without rebuilding', () {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'ReadTest', age: 35));\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n\n      // In MockBuildContext, read() should work the same as state.\n      expect(widget.nameFromRead, 'ReadTest');\n      expect(widget.nameFromRead, widget.name);\n    });\n\n    test('context.select() selects specific part of state', () {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'SelectTest', age: 40));\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n\n      // select should return the selected part of the state\n      expect(widget.nameFromSelect, 'SelectTest');\n      expect(widget.nameFromSelect, widget.name);\n    });\n\n    test('context.event() returns null when event is spent', () {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'EventTest', age: 50));\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n\n      // Initial event is spent, should return null\n      expect(widget.nameFromEvent, null);\n    });\n\n    test('context.event() returns event value when event is dispatched',\n        () async {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'Initial', age: 18));\n      var context = MockBuildContext(store);\n\n      // Build widget - event is spent\n      var widget1 = MyConnector().build(context) as MyWidget;\n      expect(widget1.nameFromEvent, null);\n\n      // Dispatch action with event\n      await store.dispatchAndWait(ChangeNameWithEvent('NewName'));\n\n      // Rebuild widget - event should be consumed and return the value\n      var widget2 = MyConnector().build(context) as MyWidget;\n      expect(widget2.nameFromEvent, 'NewName');\n      expect(widget2.name, 'NewName');\n    });\n\n    test('context.event() consumes event only once', () async {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'Initial', age: 22));\n      var context = MockBuildContext(store);\n\n      // Dispatch action with event\n      await store.dispatchAndWait(ChangeNameWithEvent('FirstEvent'));\n\n      // First build - event should be consumed\n      var widget1 = MyConnector().build(context) as MyWidget;\n      expect(widget1.nameFromEvent, 'FirstEvent');\n\n      // Second build - event is now spent\n      var widget2 = MyConnector().build(context) as MyWidget;\n      expect(widget2.nameFromEvent, null);\n    });\n\n    test('all context methods work together in MyConnector', () async {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'AllMethods', age: 28));\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n\n      // All methods should return the same name\n      expect(widget.name, 'AllMethods');\n      expect(widget.nameFromRead, 'AllMethods');\n      expect(widget.nameFromSelect, 'AllMethods');\n      expect(widget.nameFromEvent, null); // Event is spent\n\n      // Dispatch action with event\n      await store.dispatchAndWait(ChangeNameWithEvent('UpdatedWithEvent'));\n\n      // Rebuild and verify all methods work\n      var widget2 = MyConnector().build(context) as MyWidget;\n      expect(widget2.name, 'UpdatedWithEvent');\n      expect(widget2.nameFromRead, 'UpdatedWithEvent');\n      expect(widget2.nameFromSelect, 'UpdatedWithEvent');\n      expect(widget2.nameFromEvent, 'UpdatedWithEvent');\n    });\n\n    test(\n        'context.read() and context.state return same value after state change',\n        () async {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'Before', age: 33));\n      var context = MockBuildContext(store);\n\n      // Change state\n      await store.dispatchAndWait(ChangeName('After'));\n\n      // Build widget and verify both methods return updated state\n      var widget = MyConnector().build(context) as MyWidget;\n      expect(widget.name, 'After');\n      expect(widget.nameFromRead, 'After');\n      expect(widget.name, widget.nameFromRead);\n    });\n\n    test('context.dispatchAll() dispatches multiple actions', () async {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'Initial', age: 0));\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n\n      // Dispatch multiple actions via MyConnector\n      widget.onDispatchAll();\n\n      // Both actions should be applied\n      expect(store.state.name, 'Updated');\n      expect(store.state.age, 42);\n    });\n\n    test('context.dispatchSync() dispatches synchronous action', () {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'Initial', age: 10));\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n\n      // Dispatch sync action via MyConnector\n      widget.onDispatchSync();\n\n      // Action completes immediately\n      expect(store.state.name, 'Sync');\n    });\n\n    test('context.dispatchAndWait() waits for async action then dispatches',\n        () async {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'Initial', age: 0));\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n\n      // Age should be 0 initially\n      expect(store.state.age, 0);\n\n      // Dispatch async action via MyConnector - waits for WaitAndChangeAge(10), then DuplicateAge\n      await widget.onDispatchAndWait();\n\n      // After waiting, age should be 10 * 2 = 20\n      expect(store.state.age, 20);\n    });\n\n    test('context.dispatchAndWaitAll() waits for all actions then dispatches',\n        () async {\n      var store =\n          Store<AppState>(initialState: AppState(name: 'Initial', age: 0));\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n\n      // Initial values\n      expect(store.state.age, 0);\n      expect(store.state.name, 'Initial');\n\n      // Dispatch multiple async actions - waits for both, then DuplicateAge\n      await widget.onDispatchAndWaitAll();\n\n      // After waiting, age should be 5 * 2 = 10, name should be 'AsyncAll'\n      expect(store.state.age, 10);\n      expect(store.state.name, 'AsyncAll');\n    });\n\n    test('context.env returns store environment', () {\n      var env = TestEnvironment(apiUrl: 'https://api.test.com');\n      var store = Store<AppState>(\n        initialState: AppState(name: 'Test', age: 45),\n        environment: env,\n      );\n      var context = MockBuildContext(store);\n      var widget = MyConnector().build(context) as MyWidget;\n\n      // Get environment via MyConnector\n      expect(widget.environment, env);\n      expect(widget.environment?.apiUrl, 'https://api.test.com');\n    });\n  });\n}\n\n// Define AppState with name, age fields and event\nclass AppState {\n  final String name;\n  final int age;\n  final Event<String> nameChangedEvent;\n\n  AppState({\n    required this.name,\n    required this.age,\n    Event<String>? nameChangedEvent,\n  }) : nameChangedEvent = nameChangedEvent ?? Event<String>.spent();\n\n  AppState copy({\n    String? name,\n    int? age,\n    Event<String>? nameChangedEvent,\n  }) =>\n      AppState(\n        name: name ?? this.name,\n        age: age ?? this.age,\n        nameChangedEvent: nameChangedEvent ?? this.nameChangedEvent,\n      );\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          name == other.name &&\n          age == other.age &&\n          nameChangedEvent == other.nameChangedEvent;\n\n  @override\n  int get hashCode => name.hashCode ^ age.hashCode ^ nameChangedEvent.hashCode;\n}\n\n// Define extension for BuildContext\nextension BuildContextExtension on BuildContext {\n  AppState get state => getState<AppState>();\n\n  AppState read() => getRead<AppState>();\n\n  R select<R>(R Function(AppState state) selector) =>\n      getSelect<AppState, R>(selector);\n\n  R? event<R>(Evt<R> Function(AppState state) selector) =>\n      getEvent<AppState, R>(selector);\n\n  TestEnvironment? get env => getEnvironment<AppState>() as TestEnvironment?;\n}\n\n// Define ChangeName action\nclass ChangeName extends ReduxAction<AppState> {\n  final String newName;\n\n  ChangeName(this.newName);\n\n  @override\n  AppState reduce() => state.copy(name: newName);\n}\n\n// Define ChangeAge action\nclass ChangeAge extends ReduxAction<AppState> {\n  final int newAge;\n\n  ChangeAge(this.newAge);\n\n  @override\n  AppState reduce() => state.copy(age: newAge);\n}\n\n// Define WaitAndChangeAge - async action that waits 200ms\nclass WaitAndChangeAge extends ReduxAction<AppState> {\n  final int newAge;\n\n  WaitAndChangeAge(this.newAge);\n\n  @override\n  Future<AppState> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 200));\n    return state.copy(age: newAge);\n  }\n}\n\n// Define DuplicateAge - doubles the current age\nclass DuplicateAge extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => state.copy(age: state.age * 2);\n}\n\n// Define ChangeNameWithEvent action that also triggers an event\nclass ChangeNameWithEvent extends ReduxAction<AppState> {\n  final String newName;\n\n  ChangeNameWithEvent(this.newName);\n\n  @override\n  AppState reduce() => state.copy(\n        name: newName,\n        nameChangedEvent: Event<String>(newName),\n      );\n}\n\n// Define MyWidget - the dumb widget\nclass MyWidget extends StatelessWidget {\n  final String name;\n  final String nameFromRead;\n  final String nameFromSelect;\n  final String? nameFromEvent;\n  final VoidCallback onChange;\n  final VoidCallback onDispatchAll;\n  final VoidCallback onDispatchSync;\n  final Future<void> Function() onDispatchAndWait;\n  final Future<void> Function() onDispatchAndWaitAll;\n  final TestEnvironment? environment;\n\n  const MyWidget({\n    Key? key,\n    required this.name,\n    required this.nameFromRead,\n    required this.nameFromSelect,\n    required this.nameFromEvent,\n    required this.onChange,\n    required this.onDispatchAll,\n    required this.onDispatchSync,\n    required this.onDispatchAndWait,\n    required this.onDispatchAndWaitAll,\n    required this.environment,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Text(name),\n        Text(nameFromRead),\n        Text(nameFromSelect),\n        if (nameFromEvent != null) Text(nameFromEvent!),\n        ElevatedButton(\n          onPressed: onChange,\n          child: const Text('Change Name'),\n        ),\n      ],\n    );\n  }\n}\n\n// Define MyConnector - the smart widget using context extensions\nclass MyConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MyWidget(\n      name: context.state.name,\n      nameFromRead: context.read().name,\n      nameFromSelect: context.select((AppState state) => state.name),\n      nameFromEvent: context.event((AppState state) => state.nameChangedEvent),\n      onChange: () => context.dispatch(ChangeName('John')),\n      onDispatchAll: () => context.dispatchAll([\n        ChangeName('Updated'),\n        ChangeAge(42),\n      ]),\n      onDispatchSync: () => context.dispatchSync(ChangeName('Sync')),\n      onDispatchAndWait: () async {\n        await context.dispatchAndWait(WaitAndChangeAge(10));\n        context.dispatch(DuplicateAge());\n      },\n      onDispatchAndWaitAll: () async {\n        await context.dispatchAndWaitAll([\n          WaitAndChangeAge(5),\n          ChangeName('AsyncAll'),\n        ]);\n        context.dispatch(DuplicateAge());\n      },\n      environment: context.env,\n    );\n  }\n}\n\n// Define TestEnvironment for testing getEnvironment\nclass TestEnvironment {\n  final String apiUrl;\n\n  TestEnvironment({required this.apiUrl});\n}\n"
  },
  {
    "path": "test/mock_store_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n@immutable\nclass AppState {\n  final String text;\n\n  AppState(this.text);\n}\n\nclass MyAction extends ReduxAction<AppState> {\n  int value;\n\n  MyAction(this.value);\n\n  @override\n  AppState reduce() => AppState(state.text + value.toString());\n}\n\nclass MyAction1 extends MyAction {\n  MyAction1() : super(1);\n}\n\nclass MyAction2 extends MyAction {\n  MyAction2() : super(2);\n}\n\nclass MyAction3 extends MyAction {\n  MyAction3() : super(3);\n}\n\nclass MyAction4 extends MyAction {\n  MyAction4() : super(4);\n}\n\nclass MyAction5 extends MyAction {\n  MyAction5() : super(5);\n}\n\nclass MyMockAction extends MockAction<AppState> {\n  @override\n  AppState reduce() => AppState(state.text + '[' + (action as MyAction).value.toString() + ']');\n}\n\nvoid main() {\n  StoreTester<AppState> createMockStoreTester() {\n    var store = MockStore<AppState>(initialState: AppState(\"0\"));\n    return StoreTester.from(store);\n  }\n\n  test('Store: mock a single sync action.', () async {\n    var store = MockStore<AppState>(initialState: AppState(\"0\"));\n    expect(store.state.text, \"0\");\n    store.dispatch(MyAction1());\n    expect(store.state.text, \"01\");\n\n    // With mock:\n    store = MockStore<AppState>(initialState: AppState(\"0\"));\n    expect(store.state.text, \"0\");\n    store.addMock(\n      MyAction1,\n      (ReduxAction<AppState> action, AppState state) => AppState(state.text + 'A'),\n    );\n    store.dispatch(MyAction1());\n    expect(store.state.text, \"0A\");\n  });\n\n  test('StoreTester: mock a single sync action.', () async {\n    // Without mock:\n    var storeTester = createMockStoreTester();\n    expect(storeTester.state.text, \"0\");\n    storeTester.dispatch(MyAction1());\n    expect(storeTester.state.text, \"01\");\n\n    // With mock:\n    storeTester = createMockStoreTester();\n    expect(storeTester.state.text, \"0\");\n    storeTester.addMock(\n        MyAction1, (ReduxAction<AppState> action, AppState state) => AppState(state.text + 'A'));\n    storeTester.dispatch(MyAction1());\n    expect(storeTester.state.text, \"0A\");\n  });\n\n  test('Store: mock sync actions in different ways.', () async {\n    // Without mock:\n    var store = MockStore<AppState>(initialState: AppState(\"0\"));\n    expect(store.state.text, \"0\");\n    store.dispatch(MyAction1());\n    store.dispatch(MyAction2());\n    store.dispatch(MyAction3());\n    store.dispatch(MyAction4());\n    store.dispatch(MyAction5());\n    expect(store.state.text, \"012345\");\n\n    // With mock:\n    store = MockStore<AppState>(initialState: AppState(\"0\"));\n    expect(store.state.text, \"0\");\n    store.addMocks({\n      /// 1) `null` to disable dispatching the action of a certain type.\n      MyAction1: null,\n\n      /// 2) A `MockAction<St>` instance to dispatch that action instead,\n      /// and provide the original action as a getter to the mocked action.\n      MyAction2: MyMockAction(),\n\n      /// 3) A `ReduxAction<St>` instance to dispatch that mocked action instead.\n      MyAction3: MyAction(7),\n\n      /// 4) `ReduxAction<St> Function(ReduxAction<St>)` to create a mock\n      /// from the original action,\n      MyAction4: (ReduxAction<AppState> action) => MyAction((action as MyAction).value + 4),\n\n      /// 5) `St Function(ReduxAction<St>, St)` to modify the state directly.\n      MyAction5: (ReduxAction<AppState> action, AppState state) =>\n          AppState(state.text + '|' + (action as MyAction).value.toString()),\n    });\n    store.dispatch(MyAction1());\n    store.dispatch(MyAction2());\n    store.dispatch(MyAction3());\n    store.dispatch(MyAction4());\n    store.dispatch(MyAction5());\n    expect(store.state.text, \"0[2]78|5\");\n  });\n\n  test(\"Mock can't be of invalid type.\", () async {\n    var store = MockStore<AppState>(initialState: AppState(\"0\"));\n    expect(store.state.text, \"0\");\n    store.addMocks({MyAction1: 123});\n\n    Object? error;\n    try {\n      store.dispatch(MyAction1());\n    } catch (_error) {\n      error = _error;\n    }\n\n    expect(error, isNotNull);\n    expect(error, const TypeMatcher<StoreException>());\n\n    expect(\n        error.toString(),\n        \"Action of type `MyAction1` can't be mocked by a mock of type `int`.\\n\"\n        \"Valid mock types are:\\n\"\n        \"`null`\\n\"\n        \"`MockAction<St>`\\n\"\n        \"`ReduxAction<St>`\\n\"\n        \"`ReduxAction<St> Function(ReduxAction<St>)`\\n\"\n        \"`St Function(ReduxAction<St>, St)`\\n\");\n  });\n}\n"
  },
  {
    "path": "test/model_observer_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  //\n  testWidgets(\n    //\n    \"ModelObserver.\",\n    //\n    (WidgetTester tester) async {\n      //\n      var modelObserver = DefaultModelObserver<String>();\n\n      Store<_StateTest> store = Store<_StateTest>(\n        initialState: _StateTest(\"A\", 1),\n        modelObserver: modelObserver,\n      );\n\n      StoreProvider<_StateTest> provider = StoreProvider<_StateTest>(\n        store: store,\n        child: const _MyWidgetConnector(),\n      );\n\n      await tester.pumpWidget(_TestApp(provider));\n\n      // A ➜ B\n      expect(store.state.text, \"A\");\n      store.dispatch(_MyAction(\"B\", 1));\n      expect(store.state.text, \"B\");\n\n      await tester.pump();\n\n      expect(modelObserver.previous, \"A\");\n      expect(modelObserver.current, \"B\");\n\n      // ---\n\n      // B ➜ B\n      expect(store.state.text, \"B\");\n      store.dispatch(_MyAction(\"B\", 2));\n      expect(store.state.text, \"B\");\n\n      await tester.pump();\n\n      expect(modelObserver.previous, \"B\");\n      expect(modelObserver.current, \"B\");\n\n      // ---\n\n      // B ➜ C\n      expect(store.state.text, \"B\");\n      store.dispatch(_MyAction(\"C\", 1));\n      expect(store.state.text, \"C\");\n\n      await tester.pump();\n\n      expect(modelObserver.previous, \"B\");\n      expect(modelObserver.current, \"C\");\n\n      // ---\n\n      // C ➜ A\n      expect(store.state.text, \"C\");\n      store.dispatch(_MyAction(\"D\", 1));\n      expect(store.state.text, \"D\");\n\n      await tester.pump();\n\n      expect(modelObserver.previous, \"C\");\n      expect(modelObserver.current, \"D\");\n    },\n  );\n}\n\nclass _TestApp extends StatelessWidget {\n  final StoreProvider<_StateTest> provider;\n\n  _TestApp(this.provider);\n\n  @override\n  Widget build(BuildContext context) => provider;\n}\n\n@immutable\nclass _StateTest {\n  final String text;\n  final int number;\n\n  _StateTest(this.text, this.number);\n}\n\nclass _MyWidgetConnector extends StatelessWidget {\n  const _MyWidgetConnector();\n\n  @override\n  Widget build(BuildContext context) => StoreConnector<_StateTest, String>(\n        debug: this,\n        converter: (Store<_StateTest> store) => store.state.text,\n        builder: (BuildContext context, String model) => Container(),\n      );\n}\n\nclass _MyAction extends ReduxAction<_StateTest> {\n  String text;\n  int number;\n\n  _MyAction(this.text, this.number);\n\n  @override\n  _StateTest reduce() => _StateTest(text, number);\n}\n"
  },
  {
    "path": "test/navigate_action_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nlate Store<AppState> store;\n\nfinal navigatorKey = GlobalKey<NavigatorState>();\n\nfinal routes = {\n  \"/\": (BuildContext context) => MyPage(const Key(\"page1\")),\n  \"/page2\": (BuildContext context) => MyPage(const Key(\"page2\")),\n  \"/page3\": (BuildContext context) => MyPage(const Key(\"page3\")),\n};\n\nclass AppState {}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreProvider<AppState>(\n      store: store,\n      child: MaterialApp(\n        initialRoute: \"/\",\n        routes: routes,\n        navigatorKey: navigatorKey,\n      ),\n    );\n  }\n}\n\nclass MyPage extends StatelessWidget {\n  MyPage(Key key) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Column(\n        children: <Widget>[\n          Text(\"Current route: ${NavigateAction.getCurrentNavigatorRouteName(context)}\"),\n          //\n          RawMaterialButton(\n              key: const Key(\"pushNamedPage2\"),\n              onPressed: () => store.dispatch(NavigateAction.pushNamed(\"/page2\"))),\n          //\n          RawMaterialButton(\n              key: const Key(\"pushNamedPage3\"),\n              onPressed: () => store.dispatch(NavigateAction.pushNamed(\"/page3\"))),\n          //\n          RawMaterialButton(\n              key: const Key(\"pushNamedAndRemoveAllPage1\"),\n              onPressed: () => store.dispatch(NavigateAction.pushNamedAndRemoveAll(\"/\"))),\n          //\n          RawMaterialButton(\n              key: const Key(\"pushReplacementNamedPage2\"),\n              onPressed: () => store.dispatch(NavigateAction.pushReplacementNamed(\"/page2\"))),\n          //\n          RawMaterialButton(\n              key: const Key(\"pushNamedAndRemoveUntilPage2\"),\n              onPressed: () => store.dispatch(\n                      NavigateAction.pushNamedAndRemoveUntil(\"/page2\", (Route<dynamic> route) {\n                    return route.settings.name == \"/\";\n                  }))),\n          //\n          RawMaterialButton(\n              key: const Key(\"popUntilPage1\"),\n              onPressed: () => store.dispatch(NavigateAction.popUntilRouteName(\"/\"))),\n          //\n          RawMaterialButton(\n            key: const Key(\"pop\"),\n            onPressed: () => store.dispatch(NavigateAction.pop()),\n          ),\n          //\n        ],\n      ),\n    );\n  }\n}\n\nvoid main() {\n  setUp(() async {\n    NavigateAction.setNavigatorKey(navigatorKey);\n    store = Store<AppState>(initialState: AppState());\n  });\n\n  final Finder page1Finder = find.byKey(const Key(\"page1\"));\n  final Finder page1IncludeIfOffstageFinder = find.byKey(const Key(\"page1\"), skipOffstage: false);\n  final Finder pushAndRemoveAllPage1Finder = find.byKey(const Key(\"pushNamedAndRemoveAllPage1\"));\n  final Finder popUntilPage1Finder = find.byKey(const Key(\"popUntilPage1\"));\n\n  final Finder page2Finder = find.byKey(const Key(\"page2\"));\n  final Finder page2IncludeIfOffstageFinder = find.byKey(const Key(\"page2\"), skipOffstage: false);\n  final Finder pushPage2Finder = find.byKey(const Key(\"pushNamedPage2\"));\n  final Finder pushReplacementPage2Finder = find.byKey(const Key(\"pushReplacementNamedPage2\"));\n  final Finder pushNamedAndRemoveUntilPage2Finder =\n      find.byKey(const Key(\"pushNamedAndRemoveUntilPage2\"));\n\n  final Finder page3Finder = find.byKey(const Key(\"page3\"));\n  final Finder page3IncludeIfOffstageFinder = find.byKey(const Key(\"page3\"), skipOffstage: false);\n  final Finder pushPage3Finder = find.byKey(const Key(\"pushNamedPage3\"));\n\n  final Finder popFinder = find.byKey(const Key(\"pop\"));\n\n  testWidgets(\"pushNamed\", (WidgetTester tester) async {\n    await tester.pumpWidget(MyApp());\n    await tester.pumpAndSettle();\n\n    // check if initial page corresponds to initialRoute\n    expect(find.text(\"Current route: /\"), findsOneWidget);\n    expect(page1Finder, findsOneWidget);\n    expect(page2Finder, findsNothing);\n    expect(page3Finder, findsNothing);\n\n    // pushNamed to page 2\n    await tester.tap(pushPage2Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page2\"), findsOneWidget);\n    expect(page1Finder, findsNothing);\n    expect(page2Finder, findsOneWidget);\n    expect(page3Finder, findsNothing);\n\n    // pushNamed to page 3\n    await tester.tap(pushPage3Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page3\"), findsOneWidget);\n    expect(page1Finder, findsNothing);\n    expect(page2Finder, findsNothing);\n    expect(page3Finder, findsOneWidget);\n  });\n\n  testWidgets(\"pushNamedAndRemoveAll\", (WidgetTester tester) async {\n    await tester.pumpWidget(MyApp());\n    await tester.pumpAndSettle();\n\n    // initial route\n    expect(find.text(\"Current route: /\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsNothing);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // push page 2\n    await tester.tap(pushPage2Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page2\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // push page 3\n    await tester.tap(pushPage3Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page3\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsOneWidget);\n\n    // for fun, push page 3 again\n    await tester.tap(pushPage3Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page3\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsNWidgets(2));\n\n    // pushNamedAndRemoveAll back to page 1\n    await tester.tap(pushAndRemoveAllPage1Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsNothing);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n  });\n\n  testWidgets(\"pushReplacementNamed\", (WidgetTester tester) async {\n    await tester.pumpWidget(MyApp());\n    await tester.pumpAndSettle();\n\n    // initial route\n    expect(find.text(\"Current route: /\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsNothing);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // push page 2\n    await tester.tap(pushPage2Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page2\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // push page 3\n    await tester.tap(pushPage3Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page3\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsOneWidget);\n\n    // push page 2 and replace page 3\n    await tester.tap(pushReplacementPage2Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page2\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsNWidgets(2));\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n  });\n\n  testWidgets(\"pushNamedAndRemoveUntil\", (WidgetTester tester) async {\n    await tester.pumpWidget(MyApp());\n    await tester.pumpAndSettle();\n\n    // initial route\n    expect(find.text(\"Current route: /\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsNothing);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // push page 2\n    await tester.tap(pushPage2Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page2\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // push page 3\n    await tester.tap(pushPage3Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page3\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsOneWidget);\n\n    // the current stack should be:\n    // page 3\n    // page 2\n    // page 1\n\n    // if we push page 2 and replace until page 1,then the stack should be:\n    // page 2 (pushed)\n    // page 3 (removed)\n    // page 2 (removed)\n    // page 1\n\n    // which would result in a stack of\n    // page 2\n    // page 1\n    await tester.tap(pushNamedAndRemoveUntilPage2Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page2\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n  });\n\n  testWidgets(\"pop\", (WidgetTester tester) async {\n    await tester.pumpWidget(MyApp());\n    await tester.pumpAndSettle();\n\n    // initial route\n    expect(find.text(\"Current route: /\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsNothing);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // push page 2\n    await tester.tap(pushPage2Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page2\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // push page 3\n    await tester.tap(pushPage3Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page3\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsOneWidget);\n\n    // pop page 3\n    await tester.tap(popFinder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page2\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // pop page 2\n    await tester.tap(popFinder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsNothing);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n  });\n  //\n  testWidgets(\"popUntil\", (WidgetTester tester) async {\n    await tester.pumpWidget(MyApp());\n    await tester.pumpAndSettle();\n\n    // initial route\n    expect(find.text(\"Current route: /\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsNothing);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // push page 2\n    await tester.tap(pushPage2Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page2\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n\n    // push page 3\n    await tester.tap(pushPage3Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /page3\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsOneWidget);\n    expect(page3IncludeIfOffstageFinder, findsOneWidget);\n\n    // pop until page 1\n    await tester.tap(popUntilPage1Finder);\n    await tester.pumpAndSettle();\n    expect(find.text(\"Current route: /\"), findsOneWidget);\n    expect(page1IncludeIfOffstageFinder, findsOneWidget);\n    expect(page2IncludeIfOffstageFinder, findsNothing);\n    expect(page3IncludeIfOffstageFinder, findsNothing);\n  });\n}\n"
  },
  {
    "path": "test/non_reentrant_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var feature = BddFeature('Non reentrant actions');\n\n  // ==========================================================================\n  // Case 1: Sync action non-reentrant does not call itself\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Sync action non-reentrant does not call itself.')\n      .given('A SYNC action that calls itself.')\n      .and('The action is non-reentrant.')\n      .when('The action is dispatched.')\n      .then('It runs once.')\n      .and('Does not result in a stack overflow.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n    store.dispatchSync(NonReentrantSyncActionCallsItself());\n    expect(store.state.count, 2);\n  });\n\n  // ==========================================================================\n  // Case 2: Async action non-reentrant does not call itself\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Async action non-reentrant does not call itself.')\n      .given('An ASYNC action that calls itself.')\n      .and('The action is non-reentrant.')\n      .when('The action is dispatched.')\n      .then('It runs once.')\n      .and('Does not result in a stack overflow.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n    store.dispatch(NonReentrantAsyncActionCallsItself());\n    expect(store.state.count, 2);\n  });\n\n  // ==========================================================================\n  // Case 3: Async action non-reentrant blocks concurrent dispatches\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Async action non-reentrant does not start before an action of the same type finished.')\n      .given('An ASYNC action takes some time to finish.')\n      .and('The action is non-reentrant.')\n      .when('The action is dispatched.')\n      .and('Another action of the same type is dispatched before the previous one finished.')\n      .then('It runs only once.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    // We start with count 1.\n    expect(store.state.count, 1);\n    expect(store.isWaiting(NonReentrantAsyncAction), false);\n\n    // We dispatch an action that will wait for 100 millis and increment 10.\n    store.dispatch(NonReentrantAsyncAction(10, 100));\n    expect(store.isWaiting(NonReentrantAsyncAction), true);\n\n    // So far, we still have count 1.\n    expect(store.state.count, 1);\n\n    // We wait a little bit and dispatch ANOTHER action that will wait for 10 millis and increment 50.\n    await Future.delayed(const Duration(milliseconds: 10));\n    store.dispatch(NonReentrantAsyncAction(50, 10));\n    expect(store.isWaiting(NonReentrantAsyncAction), true);\n\n    // We wait for all actions to finish dispatching.\n    await store.waitAllActions([]);\n    expect(store.isWaiting(NonReentrantAsyncAction), false);\n\n    // The only action that ran was the first one, which incremented by 10 (1+10 = 11).\n    // The second action was aborted.\n    expect(store.state.count, 11);\n  });\n\n  // ==========================================================================\n  // Case 4: NonReentrant allows dispatch after action completes\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('NonReentrant allows dispatch after action completes.')\n      .given('An ASYNC non-reentrant action has completed.')\n      .when('The same action type is dispatched again.')\n      .then('It should run successfully.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    // Dispatch first action\n    await store.dispatchAndWait(NonReentrantAsyncAction(10, 50));\n    expect(store.state.count, 11);\n\n    // After completion, we can dispatch again\n    await store.dispatchAndWait(NonReentrantAsyncAction(5, 50));\n    expect(store.state.count, 16);\n  });\n\n  // ==========================================================================\n  // Case 5: NonReentrant releases key even on failure\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('NonReentrant releases key even when action fails.')\n      .given('A non-reentrant action that throws an error.')\n      .when('The action is dispatched and fails.')\n      .then('A subsequent dispatch of the same action type should run.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    // Dispatch action that will fail\n    await store.dispatchAndWait(NonReentrantFailingAction());\n    expect(store.state.count, 1); // No change due to failure\n\n    // After failure, we can dispatch again (key was released in after())\n    await store.dispatchAndWait(NonReentrantAsyncAction(10, 10));\n    expect(store.state.count, 11);\n  });\n\n  // ==========================================================================\n  // Case 6: Actions with nonReentrantKeyParams can run in parallel\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Actions with different nonReentrantKeyParams can run in parallel.')\n      .given('A non-reentrant action that uses nonReentrantKeyParams.')\n      .when('Two actions with different params are dispatched concurrently.')\n      .then('Both actions should run.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(0));\n\n    // Dispatch two actions with different itemIds - they should both run\n    store.dispatch(NonReentrantWithParams('A', 10, 100));\n    store.dispatch(NonReentrantWithParams('B', 20, 100));\n\n    // Wait for both to complete\n    await store.waitAllActions([]);\n\n    // Both should have run: 0 + 10 + 20 = 30\n    expect(store.state.count, 30);\n  });\n\n  // ==========================================================================\n  // Case 7: Actions with same nonReentrantKeyParams block each other\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Actions with same nonReentrantKeyParams block each other.')\n      .given('A non-reentrant action that uses nonReentrantKeyParams.')\n      .when('Two actions with the same params are dispatched concurrently.')\n      .then('Only the first action should run.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(0));\n\n    // Dispatch two actions with the same itemId\n    store.dispatch(NonReentrantWithParams('A', 10, 100));\n\n    // Wait a bit to ensure first action started\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // This should be aborted because 'A' is already running\n    store.dispatch(NonReentrantWithParams('A', 50, 10));\n\n    // Wait for all to complete\n    await store.waitAllActions([]);\n\n    // Only first should have run: 0 + 10 = 10\n    expect(store.state.count, 10);\n  });\n\n  // ==========================================================================\n  // Case 8: Different action types with same computeNonReentrantKey block each other\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Different action types with same computeNonReentrantKey block each other.')\n      .given('Two different action types that share the same non-reentrant key.')\n      .when('Both actions are dispatched concurrently.')\n      .then('Only the first action should run.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(0));\n\n    // Dispatch first action with shared key\n    store.dispatch(NonReentrantSharedKey1(10, 100));\n\n    // Wait a bit to ensure first action started\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Dispatch second action type with same shared key - should be aborted\n    store.dispatch(NonReentrantSharedKey2(50, 10));\n\n    // Wait for all to complete\n    await store.waitAllActions([]);\n\n    // Only first should have run: 0 + 10 = 10\n    expect(store.state.count, 10);\n  });\n\n  // ==========================================================================\n  // Case 9: After first action completes, second action type with shared key can run\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'After first action completes, second action type with shared key can run.')\n      .given('Two different action types that share the same non-reentrant key.')\n      .when('The first action completes.')\n      .then('The second action type can run.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(0));\n\n    // Dispatch and wait for first action\n    await store.dispatchAndWait(NonReentrantSharedKey1(10, 50));\n    expect(store.state.count, 10);\n\n    // Now second action type with same key should run\n    await store.dispatchAndWait(NonReentrantSharedKey2(20, 50));\n    expect(store.state.count, 30);\n  });\n\n  // ==========================================================================\n  // Case 10: Multiple concurrent dispatches with various params\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Multiple concurrent dispatches with various params.')\n      .given('Multiple non-reentrant actions with different params.')\n      .when('They are dispatched concurrently.')\n      .then(\n          'Actions with different params run, actions with same params are blocked.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(0));\n\n    // Dispatch multiple actions:\n    // - Two with param 'A' (second should be blocked)\n    // - Two with param 'B' (second should be blocked)\n    // - One with param 'C' (should run)\n    store.dispatch(NonReentrantWithParams('A', 1, 100));\n    store.dispatch(NonReentrantWithParams('B', 2, 100));\n    store.dispatch(NonReentrantWithParams('C', 4, 100));\n\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    store.dispatch(NonReentrantWithParams('A', 8, 10)); // blocked\n    store.dispatch(NonReentrantWithParams('B', 16, 10)); // blocked\n\n    await store.waitAllActions([]);\n\n    // Only A(1), B(2), C(4) should have run: 0 + 1 + 2 + 4 = 7\n    expect(store.state.count, 7);\n  });\n\n  // ==========================================================================\n  // Case 11: NonReentrant action key is released after error in reduce\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('NonReentrant action key is released after error in reduce.')\n      .given('A non-reentrant action with params that throws.')\n      .when('The action fails.')\n      .then('The key is released and another action with same params can run.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(0));\n\n    // Dispatch action with param 'X' that fails\n    await store.dispatchAndWait(NonReentrantWithParamsFails('X'));\n    expect(store.state.count, 0); // No change\n\n    // Now dispatch another action with same param - should run\n    await store.dispatchAndWait(NonReentrantWithParams('X', 10, 10));\n    expect(store.state.count, 10);\n  });\n\n  // ==========================================================================\n  // Case 12: Default nonReentrantKeyParams returns null\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Default nonReentrantKeyParams returns null.')\n      .given('A non-reentrant action without overriding nonReentrantKeyParams.')\n      .when('The action is dispatched twice concurrently.')\n      .then('The second dispatch is blocked based on runtimeType.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    // These use default key (runtimeType, null)\n    store.dispatch(NonReentrantAsyncAction(10, 100));\n\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    store.dispatch(NonReentrantAsyncAction(50, 10)); // Should be blocked\n\n    await store.waitAllActions([]);\n\n    // Only first ran: 1 + 10 = 11\n    expect(store.state.count, 11);\n  });\n}\n\n// =============================================================================\n// Test state and actions\n// =============================================================================\n\nclass State {\n  final int count;\n\n  State(this.count);\n\n  @override\n  String toString() => 'State($count)';\n}\n\nclass NonReentrantSyncActionCallsItself extends ReduxAction<State>\n    with NonReentrant {\n  @override\n  State reduce() {\n    dispatch(NonReentrantSyncActionCallsItself());\n    return State(state.count + 1);\n  }\n}\n\nclass NonReentrantAsyncActionCallsItself extends ReduxAction<State>\n    with NonReentrant {\n  @override\n  Future<State> reduce() async {\n    dispatch(NonReentrantSyncActionCallsItself());\n    return State(state.count + 1);\n  }\n}\n\nclass NonReentrantAsyncAction extends ReduxAction<State> with NonReentrant {\n  final int increment;\n  final int delayMillis;\n\n  NonReentrantAsyncAction(this.increment, this.delayMillis);\n\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(Duration(milliseconds: delayMillis));\n    return State(state.count + increment);\n  }\n}\n\n/// Action that always fails - used to test that key is released on error.\nclass NonReentrantFailingAction extends ReduxAction<State> with NonReentrant {\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Intentional failure');\n  }\n}\n\n/// Action that uses nonReentrantKeyParams to differentiate by itemId.\nclass NonReentrantWithParams extends ReduxAction<State> with NonReentrant {\n  final String itemId;\n  final int increment;\n  final int delayMillis;\n\n  NonReentrantWithParams(this.itemId, this.increment, this.delayMillis);\n\n  @override\n  Object? nonReentrantKeyParams() => itemId;\n\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(Duration(milliseconds: delayMillis));\n    return State(state.count + increment);\n  }\n}\n\n/// Action that uses nonReentrantKeyParams and always fails.\nclass NonReentrantWithParamsFails extends ReduxAction<State> with NonReentrant {\n  final String itemId;\n\n  NonReentrantWithParamsFails(this.itemId);\n\n  @override\n  Object? nonReentrantKeyParams() => itemId;\n\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Intentional failure');\n  }\n}\n\n/// First action type that uses a shared non-reentrant key via computeNonReentrantKey.\nclass NonReentrantSharedKey1 extends ReduxAction<State> with NonReentrant {\n  final int increment;\n  final int delayMillis;\n\n  NonReentrantSharedKey1(this.increment, this.delayMillis);\n\n  @override\n  Object computeNonReentrantKey() => 'sharedKey';\n\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(Duration(milliseconds: delayMillis));\n    return State(state.count + increment);\n  }\n}\n\n/// Second action type that uses the same shared non-reentrant key.\nclass NonReentrantSharedKey2 extends ReduxAction<State> with NonReentrant {\n  final int increment;\n  final int delayMillis;\n\n  NonReentrantSharedKey2(this.increment, this.delayMillis);\n\n  @override\n  Object computeNonReentrantKey() => 'sharedKey';\n\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(Duration(milliseconds: delayMillis));\n    return State(state.count + increment);\n  }\n}\n"
  },
  {
    "path": "test/optimistic_command_mixin_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart' hide Retry;\n\nvoid main() {\n  var feature = BddFeature('OptimisticCommand mixin');\n\n  Bdd(feature)\n      .scenario('OptimisticCommand applies value, saves, and reloads.')\n      .given('An action with OptimisticCommand mixin.')\n      .when('The action is dispatched and sendCommandToServer succeeds.')\n      .then('The optimistic value is applied immediately.')\n      .and('The reloaded value is applied after save completes.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemAction('new_item');\n\n    // Track state changes during the action.\n    action.stateChanges.add(store.state.items);\n\n    await store.dispatchAndWait(action);\n\n    // Final state should have the reloaded items.\n    expect(store.state.items, ['reloaded']);\n    expect(action.status.isCompletedOk, isTrue);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand + Retry: retries sendCommandToServer only, no UI flickering.')\n      .given('An action with both OptimisticCommand and Retry mixins.')\n      .and('sendCommandToServer fails the first 2 times, then succeeds.')\n      .when('The action is dispatched.')\n      .then('The optimistic value is applied only once at the start.')\n      .and('sendCommandToServer is retried until it succeeds.')\n      .and('No rollback/re-apply flickering occurs during retries.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithRetry('new_item', failCount: 2);\n    await store.dispatchAndWait(action);\n\n    // Final state should have the reloaded items.\n    expect(store.state.items, ['reloaded']);\n    expect(action.status.isCompletedOk, isTrue);\n    expect(action.attempts, 2); // Failed 2 times, then succeeded\n\n    // CRITICAL: Verify NO flickering - optimistic value applied only once.\n    // State changes tracked inside the action should show:\n    // [optimistic] then [reloaded].\n    // NOT: [optimistic] [rollback] [optimistic] [rollback] [optimistic] [reloaded]\n    expect(action.stateChangesLog.length,\n        2); // Only 2 state changes, no flickering\n    expect(action.stateChangesLog[0], ['initial', 'new_item']); // Optimistic\n    expect(action.stateChangesLog[1], ['reloaded']); // Reloaded\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand + Retry: rolls back only after all retries fail.')\n      .given('An action with both OptimisticCommand and Retry mixins.')\n      .and('sendCommandToServer always fails (maxRetries = 3).')\n      .when('The action is dispatched.')\n      .then('The optimistic value stays in place during all retry attempts.')\n      .and('Rollback happens only after all retries are exhausted.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithRetryThatAlwaysFails('new_item');\n    await store.dispatchAndWait(action);\n\n    // Final state should be rolled back (reloadFromServer doesn't throw, but rollback happens).\n    // Note: The finally block still runs reloadFromServer even on failure.\n    expect(action.status.isCompletedFailed, isTrue);\n    expect(action.attempts, 4); // Initial + 3 retries\n\n    // CRITICAL: Verify NO flickering - optimistic applied once, then reload runs in finally.\n    // Without our fix, it would be: [opt] [roll] [opt] [roll] [opt] [roll] [opt] [roll] [reload]\n    // With our fix: [opt] [roll] [reload] (rollback + reload in finally)\n    expect(action.stateChangesLog.length, 3);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']); // Optimistic\n    expect(action.stateChangesLog[1],\n        ['initial']); // Rolled back after all retries failed\n    expect(action.stateChangesLog[2], ['reloaded']); // Reload in finally\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand without Retry: normal behavior, no retry logic.')\n      .given('An action with only OptimisticCommand mixin (no Retry).')\n      .and('sendCommandToServer fails.')\n      .when('The action is dispatched.')\n      .then('The action fails immediately without retrying.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionThatFails('new_item');\n    await store.dispatchAndWait(action);\n\n    // Note: reloadFromServer still runs in finally even on failure\n    expect(action.status.isCompletedFailed, isTrue);\n    expect(action.saveAttempts, 1); // Only 1 attempt, no retries\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand rolls back on failure.')\n      .given('An action with OptimisticCommand mixin.')\n      .when('The action is dispatched and sendCommandToServer fails.')\n      .then('The optimistic value is rolled back to the initial value.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionThatFailsWithStateLog('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // Verify the sequence: optimistic update, then rollback, then reload in finally.\n    expect(action.stateChangesLog.length, 3);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']); // Optimistic\n    expect(action.stateChangesLog[1], ['initial']); // Rolled back\n    expect(action.stateChangesLog[2], ['reloaded']); // Reload in finally\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand does NOT rollback if state changed by another action.')\n      .given('An action with OptimisticCommand mixin.')\n      .and('Another action modifies the state during sendCommandToServer.')\n      .when('The action fails.')\n      .then('The optimistic value is NOT rolled back because state changed.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionThatFailsAfterStateChange('new_item', store);\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // State should NOT be rolled back because another action changed it.\n    // Final state should be 'changed_by_other' (from the other action) then 'reloaded'.\n    // The rollback was skipped because state != optimistic value.\n    expect(action.stateChangesLog[0], ['initial', 'new_item']); // Optimistic\n    // No rollback occurred because state was changed by another action.\n    // Finally block still runs reloadFromServer.\n    expect(action.stateChangesLog.last, ['reloaded']); // Reload in finally\n    expect(action.rollbackOccurred, isFalse);\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand without reloadFromServer implementation.')\n      .given(\n          'An action with OptimisticCommand that does not implement reloadFromServer.')\n      .when('The action is dispatched and sendCommandToServer succeeds.')\n      .then('The reload step is skipped (no error).')\n      .and('The state keeps the optimistic value.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithoutReload('new_item');\n    await store.dispatchAndWait(action);\n\n    // Final state should keep the optimistic value since reload was not implemented.\n    expect(store.state.items, ['initial', 'new_item']);\n    expect(action.status.isCompletedOk, isTrue);\n\n    // Only one state change: the optimistic update.\n    expect(action.stateChangesLog.length, 1);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand without reloadFromServer: rollback on failure.')\n      .given(\n          'An action with OptimisticCommand that does not implement reloadFromServer.')\n      .when('The action is dispatched and sendCommandToServer fails.')\n      .then('The optimistic value is rolled back.')\n      .and('The reload step is skipped (no error).')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithoutReloadThatFails('new_item');\n    await store.dispatchAndWait(action);\n\n    // Final state should be rolled back to initial since save failed.\n    expect(store.state.items, ['initial']);\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // Two state changes: optimistic update, then rollback.\n    expect(action.stateChangesLog.length, 2);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']); // Optimistic\n    expect(action.stateChangesLog[1], ['initial']); // Rolled back\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand + Retry without reloadFromServer: no flickering.')\n      .given(\n          'An action with OptimisticCommand and Retry, but no reloadFromServer.')\n      .and('sendCommandToServer fails the first 2 times, then succeeds.')\n      .when('The action is dispatched.')\n      .then('No flickering occurs and state keeps optimistic value.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithRetryNoReload('new_item', failCount: 2);\n    await store.dispatchAndWait(action);\n\n    // Final state should keep the optimistic value since reload was not implemented.\n    expect(store.state.items, ['initial', 'new_item']);\n    expect(action.status.isCompletedOk, isTrue);\n    expect(action.attempts, 2);\n\n    // Only one state change: the optimistic update (no reload).\n    expect(action.stateChangesLog.length, 1);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']);\n  });\n\n  // ---------------------------------------------------------------------------\n  // Tests for overriding rollbackState\n  // ---------------------------------------------------------------------------\n\n  Bdd(feature)\n      .scenario(\n          'Custom rollbackState marks item as failed instead of removing it.')\n      .given('An action with OptimisticCommand that overrides rollbackState.')\n      .when('The action fails.')\n      .then('The custom rollback is applied (item marked as failed).')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithCustomRollback('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // State should have the item marked as failed.\n    expect(store.state.items, ['initial', 'new_item (FAILED)']);\n\n    // Verify error was passed to rollbackState.\n    expect(action.capturedError, isA<UserException>());\n\n    // Note: stateChangesLog only captures calls through applyValueToState.\n    // Custom rollbackState returns a state directly, bypassing applyValueToState.\n    // So we only see the optimistic update in the log.\n    expect(action.stateChangesLog.length, 1);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']);\n  });\n\n  Bdd(feature)\n      .scenario('rollbackState returning null skips rollback.')\n      .given(\n          'An action with OptimisticCommand that overrides rollbackState to return null.')\n      .when('The action fails.')\n      .then('No rollback occurs and state keeps the optimistic value.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithRollbackReturningNull('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // State should keep the optimistic value (no rollback).\n    expect(store.state.items, ['initial', 'new_item']);\n\n    // Only one state change: the optimistic update.\n    expect(action.stateChangesLog.length, 1);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']);\n  });\n\n  // ---------------------------------------------------------------------------\n  // Tests for overriding shouldRollback\n  // ---------------------------------------------------------------------------\n\n  Bdd(feature)\n      .scenario('shouldRollback always true: rollback even when state changed.')\n      .given(\n          'An action with OptimisticCommand that overrides shouldRollback to always return true.')\n      .and('Another action modifies the state during sendCommandToServer.')\n      .when('The action fails.')\n      .then('The rollback happens even though state changed.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithAlwaysRollback('new_item', store);\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // State should be rolled back to initial, overwriting the other action's change.\n    expect(store.state.items, ['initial']);\n\n    // State changes: optimistic, then rollback.\n    expect(action.stateChangesLog.length, 2);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']);\n    expect(action.stateChangesLog[1], ['initial']);\n  });\n\n  Bdd(feature)\n      .scenario('shouldRollback always false: never rollback.')\n      .given(\n          'An action with OptimisticCommand that overrides shouldRollback to always return false.')\n      .when('The action fails.')\n      .then('No rollback occurs and state keeps the optimistic value.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithNeverRollback('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // State should keep the optimistic value (no rollback).\n    expect(store.state.items, ['initial', 'new_item']);\n\n    // Only one state change: the optimistic update.\n    expect(action.stateChangesLog.length, 1);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'shouldRollback conditional: rollback only for validation errors.')\n      .given(\n          'An action with shouldRollback that returns false for network errors.')\n      .when('The action fails with a network error.')\n      .then('No rollback occurs.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithConditionalRollback('new_item',\n        throwNetworkError: true);\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // No rollback for network error.\n    expect(store.state.items, ['initial', 'new_item']);\n    expect(action.stateChangesLog.length, 1);\n  });\n\n  Bdd(feature)\n      .scenario('shouldRollback conditional: rollback for validation errors.')\n      .given(\n          'An action with shouldRollback that returns true for validation errors.')\n      .when('The action fails with a validation error.')\n      .then('Rollback occurs.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithConditionalRollback('new_item',\n        throwNetworkError: false);\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // Rollback for validation error.\n    expect(store.state.items, ['initial']);\n    expect(action.stateChangesLog.length, 2);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']);\n    expect(action.stateChangesLog[1], ['initial']);\n  });\n\n  // ---------------------------------------------------------------------------\n  // Tests for overriding shouldReload\n  // ---------------------------------------------------------------------------\n\n  Bdd(feature)\n      .scenario('shouldReload returns false on success: no reload.')\n      .given('An action with shouldReload that returns true only on error.')\n      .when('The action succeeds.')\n      .then('reloadFromServer is not called.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action =\n        SaveItemActionWithConditionalReload('new_item', shouldFail: false);\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n    expect(action.reloadWasCalled, isFalse);\n\n    // State keeps optimistic value.\n    expect(store.state.items, ['initial', 'new_item']);\n    expect(action.stateChangesLog.length, 1);\n  });\n\n  Bdd(feature)\n      .scenario('shouldReload returns true on error: reload happens.')\n      .given('An action with shouldReload that returns true only on error.')\n      .when('The action fails.')\n      .then('reloadFromServer is called and applied.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action =\n        SaveItemActionWithConditionalReload('new_item', shouldFail: true);\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n    expect(action.reloadWasCalled, isTrue);\n\n    // State is reloaded.\n    expect(store.state.items, ['reloaded']);\n  });\n\n  // ---------------------------------------------------------------------------\n  // Tests for overriding shouldApplyReload\n  // ---------------------------------------------------------------------------\n\n  Bdd(feature)\n      .scenario(\n          'shouldApplyReload returns true when state unchanged: reload applied.')\n      .given(\n          'An action with shouldApplyReload that checks if state is unchanged.')\n      .and('No other action modifies state during reload.')\n      .when('The action succeeds.')\n      .then('Reload result is applied.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithConditionalApplyReload(\n      'new_item',\n      store,\n      changeStateDuringReload: false,\n    );\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n\n    // Reload was applied.\n    expect(store.state.items, ['reloaded']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'shouldApplyReload returns false when state changed: reload skipped.')\n      .given(\n          'An action with shouldApplyReload that checks if state is unchanged.')\n      .and('Another action modifies state during reload.')\n      .when('The action succeeds.')\n      .then('Reload result is NOT applied to avoid overwriting newer changes.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithConditionalApplyReload(\n      'new_item',\n      store,\n      changeStateDuringReload: true,\n    );\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n\n    // State was changed by other action, so reload was NOT applied.\n    // The state should be 'changed_by_other' from ChangeStateAction.\n    expect(store.state.items, ['changed_by_other']);\n  });\n\n  // ---------------------------------------------------------------------------\n  // Tests for overriding applyReloadResultToState\n  // ---------------------------------------------------------------------------\n\n  Bdd(feature)\n      .scenario('Custom applyReloadResultToState transforms reload result.')\n      .given(\n          'An action with applyReloadResultToState that transforms the reload result.')\n      .and('reloadFromServer returns a map instead of a list.')\n      .when('The action succeeds.')\n      .then('The custom transformation is applied.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithCustomApplyReload('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n\n    // The custom applyReloadResultToState transformed the map and added 'TRANSFORMED'.\n    expect(store.state.items, ['server_item1', 'server_item2', 'TRANSFORMED']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'applyReloadResultToState returning null skips applying reload.')\n      .given('An action with applyReloadResultToState that returns null.')\n      .when('The action succeeds and reload completes.')\n      .then(\n          'The reload result is NOT applied and state keeps optimistic value.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithApplyReloadReturningNull('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n    expect(action.reloadWasCalled, isTrue);\n\n    // Reload was called but NOT applied (applyReloadResultToState returned null).\n    // State keeps the optimistic value.\n    expect(store.state.items, ['initial', 'new_item']);\n    expect(action.stateChangesLog.length, 1);\n  });\n\n  // ---------------------------------------------------------------------------\n  // Missing edge cases / invariants\n  // ---------------------------------------------------------------------------\n\n  Bdd(feature)\n      .scenario('OptimisticCommand: if reloadFromServer throws on success, '\n          'the action fails with the reload error.')\n      .given('An action with OptimisticCommand mixin.')\n      .and('sendCommandToServer succeeds.')\n      .and('reloadFromServer throws.')\n      .when('The action is dispatched.')\n      .then('The action fails (reload error is not swallowed).')\n      .and(\n          'The optimistic value remains applied (reload did not overwrite it).')\n      .note(\n          'This locks the intended behavior when reload fails but there was no prior error.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithReloadThatThrows('new_item');\n    await store.dispatchAndWait(action);\n\n    // The action should fail because reloadFromServer threw.\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // The error should be the reload error, not swallowed.\n    expect(action.status.originalError.toString(), contains('Reload failed'));\n\n    // The optimistic value remains applied (reload did not overwrite it).\n    expect(store.state.items, ['initial', 'new_item']);\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand: if reloadFromServer throws on failure, '\n          'the action fails with the original command error.')\n      .given('An action with OptimisticCommand mixin.')\n      .and('sendCommandToServer throws.')\n      .and('reloadFromServer also throws.')\n      .when('The action is dispatched.')\n      .then('The action fails with the original sendCommandToServer error '\n          '(reload error does not replace it).')\n      .and('Rollback behavior follows shouldRollback/rollbackState as usual.')\n      .note('This ensures reload failure never hides the real command failure.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithBothCommandAndReloadThatThrow('new_item');\n    await store.dispatchAndWait(action);\n\n    // The action should fail.\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // The error should be the ORIGINAL command error, not the reload error.\n    expect(action.status.originalError.toString(), contains('Command failed'));\n\n    // Rollback should have happened (state rolled back to initial).\n    expect(store.state.items, ['initial']);\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand: shouldReload can skip reload on error.')\n      .given('An action with OptimisticCommand mixin.')\n      .and('sendCommandToServer throws.')\n      .and('shouldReload returns false when error != null.')\n      .when('The action is dispatched.')\n      .then('reloadFromServer is not called.')\n      .and('Rollback behavior is still evaluated normally.')\n      .note('This is different from \"reload not implemented\". '\n          'It is \"reload intentionally disabled by policy\".')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithShouldReloadFalseOnError('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // reloadFromServer should NOT have been called.\n    expect(action.reloadWasCalled, isFalse);\n\n    // Rollback should still have happened normally.\n    expect(store.state.items, ['initial']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand: shouldApplyReload can use the error parameter '\n          'to skip applying reload on failure.')\n      .given('An action with OptimisticCommand mixin.')\n      .and('sendCommandToServer throws.')\n      .and('reloadFromServer returns a value.')\n      .and('shouldApplyReload returns false when error != null.')\n      .when('The action is dispatched.')\n      .then('reloadFromServer is called (because shouldReload returned true).')\n      .and(\n          'The reload result is not applied (because shouldApplyReload returned false).')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithShouldApplyReloadFalseOnError('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // reloadFromServer WAS called.\n    expect(action.reloadWasCalled, isTrue);\n\n    // But the reload result was NOT applied (shouldApplyReload returned false).\n    // State should be rolled back to initial, not 'reloaded'.\n    expect(store.state.items, ['initial']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand: lastAppliedValue passed to shouldReload/shouldApplyReload '\n          'is the rollbackValue when rollback was applied.')\n      .given('An action with OptimisticCommand mixin.')\n      .and('sendCommandToServer throws.')\n      .and(\n          'Rollback is applied (shouldRollback returns true and rollbackState returns a non-null state).')\n      .and(\n          'shouldReload captures the received lastAppliedValue and rollbackValue.')\n      .when('The action is dispatched.')\n      .then('lastAppliedValue equals rollbackValue when rollback happened.')\n      .note(\n          'This verifies your bookkeeping: on error, the \"last thing we applied\" '\n          'should reflect rollback, not the optimistic value.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionCaptureLastAppliedOnError('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n\n    // Verify the captured values.\n    expect(action.capturedLastAppliedValue, isNotNull);\n    expect(action.capturedRollbackValue, isNotNull);\n\n    // lastAppliedValue should equal rollbackValue when rollback happened.\n    expect(action.capturedLastAppliedValue, action.capturedRollbackValue);\n\n    // And both should be the initial value (what we rolled back to).\n    expect(action.capturedLastAppliedValue, ['initial']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand: lastAppliedValue passed to shouldReload/shouldApplyReload '\n          'is the optimisticValue on success.')\n      .given('An action with OptimisticCommand mixin.')\n      .and('sendCommandToServer succeeds.')\n      .and('shouldReload captures the received lastAppliedValue.')\n      .when('The action is dispatched.')\n      .then('lastAppliedValue equals optimisticValue.')\n      .note(\n          'Ensures lastAppliedValue semantics are stable across success vs failure.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionCaptureLastAppliedOnSuccess('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n\n    // Verify the captured lastAppliedValue equals the optimisticValue.\n    expect(action.capturedLastAppliedValue, isNotNull);\n    expect(action.capturedOptimisticValue, isNotNull);\n    expect(action.capturedLastAppliedValue, action.capturedOptimisticValue);\n\n    // And rollbackValue should be null on success.\n    expect(action.capturedRollbackValue, isNull);\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand: optimisticValue is computed exactly once, '\n          'even when Retry retries sendCommandToServer.')\n      .given('An action with OptimisticCommand and Retry mixins.')\n      .and('optimisticValue increments a counter each time it is called.')\n      .and('sendCommandToServer fails N times then succeeds.')\n      .when('The action is dispatched.')\n      .then('optimisticValue was called exactly once.')\n      .and('sendCommandToServer was called N+1 times.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action =\n        SaveItemActionWithOptimisticValueCounter('new_item', failCount: 3);\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n\n    // optimisticValue should have been called exactly once.\n    expect(action.optimisticValueCallCount, 1);\n\n    // sendCommandToServer should have been called N+1 times (3 failures + 1 success = 4).\n    expect(action.sendCommandCallCount, 4);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand: sendCommandToServer receives the same optimisticValue '\n          'instance that was applied to state.')\n      .given('An action with OptimisticCommand mixin.')\n      .and('optimisticValue returns an object whose identity can be checked.')\n      .when('The action is dispatched.')\n      .then(\n          'sendCommandToServer receives the same object instance returned by optimisticValue.')\n      .note(\n          'This is useful if users build an optimistic payload object and want to reuse it in the command.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionCheckIdentity('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n\n    // The object passed to sendCommandToServer should be identical to the one returned by optimisticValue.\n    expect(action.receivedValueInSendCommand, isNotNull);\n    expect(action.createdOptimisticValue, isNotNull);\n    expect(\n        identical(\n            action.receivedValueInSendCommand, action.createdOptimisticValue),\n        isTrue);\n  });\n\n  // ---------------------------------------------------------------------------\n  // Tests for built-in non-reentrant behavior\n  // ---------------------------------------------------------------------------\n\n  Bdd(feature)\n      .scenario('OptimisticCommand blocks concurrent dispatches.')\n      .given('An OptimisticCommand action that takes some time to finish.')\n      .when('The action is dispatched.')\n      .and(\n          'Another action of the same type is dispatched before the previous one finished.')\n      .then('The second dispatch is aborted.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    // Dispatch first action that takes 100ms.\n    store.dispatch(OptimisticCommandSlowAction('item1', delayMillis: 100));\n    expect(store.isWaiting(OptimisticCommandSlowAction), true);\n\n    // Wait a bit and dispatch another action of the same type.\n    await Future.delayed(const Duration(milliseconds: 10));\n    store.dispatch(OptimisticCommandSlowAction('item2', delayMillis: 10));\n\n    // Wait for all actions to finish.\n    await store.waitAllActions([]);\n    expect(store.isWaiting(OptimisticCommandSlowAction), false);\n\n    // Only the first action ran, adding 'item1'.\n    // The second action was aborted.\n    expect(store.state.items, ['initial', 'item1']);\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand allows dispatch after action completes.')\n      .given('An OptimisticCommand action has completed.')\n      .when('The same action type is dispatched again.')\n      .then('It should run successfully.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    // Dispatch first action and wait for completion.\n    await store\n        .dispatchAndWait(OptimisticCommandSlowAction('item1', delayMillis: 10));\n    expect(store.state.items, ['initial', 'item1']);\n\n    // After completion, we can dispatch again.\n    await store\n        .dispatchAndWait(OptimisticCommandSlowAction('item2', delayMillis: 10));\n    expect(store.state.items, ['initial', 'item1', 'item2']);\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand releases key even when action fails.')\n      .given('An OptimisticCommand action that throws an error.')\n      .when('The action is dispatched and fails.')\n      .then('A subsequent dispatch of the same action type should run.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    // Dispatch action that will fail.\n    await store.dispatchAndWait(OptimisticCommandFailingAction());\n    expect(store.state.items, ['initial']); // Rolled back due to failure.\n\n    // After failure, we can dispatch again (key was released in after()).\n    await store\n        .dispatchAndWait(OptimisticCommandSlowAction('item1', delayMillis: 10));\n    expect(store.state.items, ['initial', 'item1']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand with different nonReentrantKeyParams can run in parallel.')\n      .given('An OptimisticCommand action that uses nonReentrantKeyParams.')\n      .when('Two actions with different params are dispatched concurrently.')\n      .then('Both actions should run.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: []));\n\n    // Dispatch two actions with different itemIds - they should both run.\n    store\n        .dispatch(OptimisticCommandWithParams('A', 'valueA', delayMillis: 100));\n    store\n        .dispatch(OptimisticCommandWithParams('B', 'valueB', delayMillis: 100));\n\n    // Wait for both to complete.\n    await store.waitAllActions([]);\n\n    // Both should have run.\n    expect(store.state.items.length, 2);\n    expect(store.state.items.contains('valueA'), isTrue);\n    expect(store.state.items.contains('valueB'), isTrue);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand with same nonReentrantKeyParams blocks each other.')\n      .given('An OptimisticCommand action that uses nonReentrantKeyParams.')\n      .when('Two actions with the same params are dispatched concurrently.')\n      .then('Only the first action should run.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: []));\n\n    // Dispatch two actions with the same itemId.\n    store.dispatch(\n        OptimisticCommandWithParams('A', 'valueA1', delayMillis: 100));\n\n    // Wait a bit to ensure first action started.\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // This should be aborted because 'A' is already running.\n    store\n        .dispatch(OptimisticCommandWithParams('A', 'valueA2', delayMillis: 10));\n\n    // Wait for all to complete.\n    await store.waitAllActions([]);\n\n    // Only first should have run.\n    expect(store.state.items, ['valueA1']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Different OptimisticCommand action types with same computeNonReentrantKey block each other.')\n      .given(\n          'Two different OptimisticCommand action types that share the same non-reentrant key.')\n      .when('Both actions are dispatched concurrently.')\n      .then('Only the first action should run.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: []));\n\n    // Dispatch first action with shared key.\n    store.dispatch(OptimisticCommandSharedKey1('value1', delayMillis: 100));\n\n    // Wait a bit to ensure first action started.\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Dispatch second action type with same shared key - should be aborted.\n    store.dispatch(OptimisticCommandSharedKey2('value2', delayMillis: 10));\n\n    // Wait for all to complete.\n    await store.waitAllActions([]);\n\n    // Only first should have run.\n    expect(store.state.items, ['value1']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'After first OptimisticCommand completes, second action type with shared key can run.')\n      .given(\n          'Two different OptimisticCommand action types that share the same non-reentrant key.')\n      .when('The first action completes.')\n      .then('The second action type can run.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: []));\n\n    // Dispatch and wait for first action.\n    await store.dispatchAndWait(\n        OptimisticCommandSharedKey1('value1', delayMillis: 10));\n    expect(store.state.items, ['value1']);\n\n    // Now second action type with same key should run.\n    await store.dispatchAndWait(\n        OptimisticCommandSharedKey2('value2', delayMillis: 10));\n    expect(store.state.items, ['value1', 'value2']);\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand key is released after error in reduce.')\n      .given('An OptimisticCommand action with params that throws.')\n      .when('The action fails.')\n      .then('The key is released and another action with same params can run.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: []));\n\n    // Dispatch action with param 'X' that fails.\n    await store.dispatchAndWait(OptimisticCommandWithParamsThatFails('X'));\n    expect(store.state.items, []); // Rolled back.\n\n    // Now dispatch another action with same param - should run.\n    await store.dispatchAndWait(\n        OptimisticCommandWithParams('X', 'valueX', delayMillis: 10));\n    expect(store.state.items, ['valueX']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Multiple OptimisticCommand concurrent dispatches with various params.')\n      .given('Multiple OptimisticCommand actions with different params.')\n      .when('They are dispatched concurrently.')\n      .then(\n          'Actions with different params run, actions with same params are blocked.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: []));\n\n    // Dispatch multiple actions:\n    // - Two with param 'A' (second should be blocked)\n    // - Two with param 'B' (second should be blocked)\n    // - One with param 'C' (should run)\n    store.dispatch(OptimisticCommandWithParams('A', 'A1', delayMillis: 100));\n    store.dispatch(OptimisticCommandWithParams('B', 'B1', delayMillis: 100));\n    store.dispatch(OptimisticCommandWithParams('C', 'C1', delayMillis: 100));\n\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    store.dispatch(\n        OptimisticCommandWithParams('A', 'A2', delayMillis: 10)); // blocked\n    store.dispatch(\n        OptimisticCommandWithParams('B', 'B2', delayMillis: 10)); // blocked\n\n    await store.waitAllActions([]);\n\n    // Only A1, B1, C1 should have run.\n    expect(store.state.items.length, 3);\n    expect(store.state.items.contains('A1'), isTrue);\n    expect(store.state.items.contains('B1'), isTrue);\n    expect(store.state.items.contains('C1'), isTrue);\n    expect(store.state.items.contains('A2'), isFalse);\n    expect(store.state.items.contains('B2'), isFalse);\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand cannot be combined with NonReentrant mixin.')\n      .given('An action that combines OptimisticCommand and NonReentrant.')\n      .when('The action is dispatched.')\n      .then('An assertion error is thrown.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    // This should throw an assertion error due to incompatible mixins.\n    expect(\n      () => store.dispatch(OptimisticCommandWithNonReentrant('item')),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        contains('OptimisticCommand'),\n      )),\n    );\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand cannot be combined with Throttle mixin.')\n      .given('An action that combines OptimisticCommand and Throttle.')\n      .when('The action is dispatched.')\n      .then('An assertion error is thrown.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    // This should throw an assertion error due to incompatible mixins.\n    expect(\n      () => store.dispatch(OptimisticCommandWithThrottle('item')),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        contains('OptimisticCommand'),\n      )),\n    );\n  });\n\n  Bdd(feature)\n      .scenario('OptimisticCommand cannot be combined with Fresh mixin.')\n      .given('An action that combines OptimisticCommand and Fresh.')\n      .when('The action is dispatched.')\n      .then('An assertion error is thrown.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    // This should throw an assertion error due to incompatible mixins.\n    expect(\n      () => store.dispatch(OptimisticCommandWithFresh('item')),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        contains('OptimisticCommand'),\n      )),\n    );\n  });\n\n  // ---------------------------------------------------------------------------\n  // Tests for DEFAULT shouldReload behavior (only reload on error)\n  // ---------------------------------------------------------------------------\n\n  Bdd(feature)\n      .scenario('Default shouldReload: does NOT reload on success.')\n      .given('An action with OptimisticCommand using default shouldReload.')\n      .and('sendCommandToServer succeeds.')\n      .when('The action is dispatched.')\n      .then('reloadFromServer is NOT called.')\n      .and('State keeps the optimistic value.')\n      .note(\n          'The default shouldReload returns (error != null), so it skips reload on success.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithDefaultShouldReloadOnSuccess('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n    expect(action.reloadWasCalled, isFalse);\n\n    // State keeps the optimistic value.\n    expect(store.state.items, ['initial', 'new_item']);\n    expect(action.stateChangesLog.length, 1);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']);\n  });\n\n  Bdd(feature)\n      .scenario('Default shouldReload: DOES reload on failure.')\n      .given('An action with OptimisticCommand using default shouldReload.')\n      .and('sendCommandToServer fails.')\n      .when('The action is dispatched.')\n      .then('reloadFromServer IS called.')\n      .and('State is reloaded after rollback.')\n      .note(\n          'The default shouldReload returns (error != null), so it reloads on failure.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithDefaultShouldReloadOnFailure('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n    expect(action.reloadWasCalled, isTrue);\n\n    // State was reloaded after rollback.\n    expect(store.state.items, ['reloaded']);\n\n    // State changes: optimistic, rollback, reload.\n    expect(action.stateChangesLog.length, 3);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']); // Optimistic\n    expect(action.stateChangesLog[1], ['initial']); // Rolled back\n    expect(action.stateChangesLog[2], ['reloaded']); // Reloaded\n  });\n\n  // ---------------------------------------------------------------------------\n  // Tests for sendCommandToServer returning a value (applyServerResponseToState)\n  // ---------------------------------------------------------------------------\n\n  Bdd(feature)\n      .scenario(\n          'sendCommandToServer returns value: applyServerResponseToState is called.')\n      .given('An action with OptimisticCommand.')\n      .and('sendCommandToServer returns a non-null value.')\n      .when('The action is dispatched.')\n      .then('applyServerResponseToState is called with the returned value.')\n      .and('The server response is applied to the state.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithServerResponse('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n    expect(action.serverResponseApplied, isTrue);\n    expect(action.receivedServerResponse,\n        ['initial', 'new_item', 'server_confirmed']);\n\n    // Final state should have the server-confirmed value.\n    expect(store.state.items, ['initial', 'new_item', 'server_confirmed']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'sendCommandToServer returns null: applyServerResponseToState is NOT called.')\n      .given('An action with OptimisticCommand.')\n      .and('sendCommandToServer returns null.')\n      .when('The action is dispatched.')\n      .then('applyServerResponseToState is NOT called.')\n      .and('State keeps the optimistic value.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithNullServerResponse('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n    expect(action.applyServerResponseCalled, isFalse);\n\n    // State keeps the optimistic value.\n    expect(store.state.items, ['initial', 'new_item']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'applyServerResponseToState returns null: server response is NOT applied.')\n      .given('An action with OptimisticCommand.')\n      .and('sendCommandToServer returns a value.')\n      .and('applyServerResponseToState returns null.')\n      .when('The action is dispatched.')\n      .then('The server response is received but NOT applied.')\n      .and('State keeps the optimistic value.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithServerResponseReturningNull('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n    expect(action.applyServerResponseCalled, isTrue);\n\n    // State keeps the optimistic value (server response was not applied).\n    expect(store.state.items, ['initial', 'new_item']);\n  });\n\n  Bdd(feature)\n      .scenario('Server response is applied before reload.')\n      .given('An action with OptimisticCommand.')\n      .and(\n          'Both sendCommandToServer returns a value and reloadFromServer is implemented.')\n      .when('The action is dispatched.')\n      .then('applyServerResponseToState is called first.')\n      .and('reloadFromServer is called second (if shouldReload returns true).')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithServerResponseAndReload('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n\n    // Verify the order: server response applied first, then reload.\n    expect(action.operationOrder, ['applyServerResponse', 'reload']);\n\n    // Final state should have the reloaded value (reload happens after server response).\n    expect(store.state.items, ['reloaded']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'sendCommandToServer fails: applyServerResponseToState is NOT called.')\n      .given('An action with OptimisticCommand.')\n      .and('sendCommandToServer throws an error.')\n      .when('The action is dispatched.')\n      .then(\n          'applyServerResponseToState is NOT called (no server response on error).')\n      .and('Rollback happens as usual.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithServerResponseThatFails('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedFailed, isTrue);\n    expect(action.applyServerResponseCalled, isFalse);\n\n    // State was rolled back.\n    expect(store.state.items, ['initial']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticCommand + Retry: server response is applied after successful retry.')\n      .given('An action with OptimisticCommand and Retry mixins.')\n      .and(\n          'sendCommandToServer fails 2 times, then succeeds and returns a value.')\n      .when('The action is dispatched.')\n      .then(\n          'applyServerResponseToState is called with the value from the successful attempt.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action =\n        SaveItemActionWithRetryAndServerResponse('new_item', failCount: 2);\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n    expect(action.serverResponseApplied, isTrue);\n\n    // Final state should have the server-confirmed value.\n    expect(store.state.items, ['initial', 'new_item', 'server_confirmed']);\n\n    // Only one state change for optimistic update (no flickering).\n    expect(action.stateChangesLog.length, 1);\n    expect(action.stateChangesLog[0], ['initial', 'new_item']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Server response can be transformed in applyServerResponseToState.')\n      .given('An action with OptimisticCommand.')\n      .and('sendCommandToServer returns a complex object (map).')\n      .and('applyServerResponseToState transforms the response.')\n      .when('The action is dispatched.')\n      .then('The transformed response is applied to state.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action = SaveItemActionWithServerResponseTransformation('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n\n    // State should have the transformed server response.\n    expect(store.state.items, ['new_item (confirmed: 2024-01-01)']);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Default shouldReload with server response: only server response is applied on success.')\n      .given('An action with OptimisticCommand using default shouldReload.')\n      .and('sendCommandToServer succeeds and returns a value.')\n      .and('reloadFromServer is implemented.')\n      .when('The action is dispatched.')\n      .then('applyServerResponseToState is called.')\n      .and(\n          'reloadFromServer is NOT called (default shouldReload skips reload on success).')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(items: ['initial']));\n\n    var action =\n        SaveItemActionWithDefaultShouldReloadAndServerResponse('new_item');\n    await store.dispatchAndWait(action);\n\n    expect(action.status.isCompletedOk, isTrue);\n    expect(action.serverResponseApplied, isTrue);\n    expect(action.reloadWasCalled, isFalse);\n\n    // State should have the server-confirmed value (not reloaded).\n    expect(store.state.items, ['initial', 'new_item', 'server_confirmed']);\n  });\n}\n\n// -----------------------------------------------------------------------------\n// State\n// -----------------------------------------------------------------------------\n\nclass AppState {\n  final List<String> items;\n\n  AppState({required this.items});\n\n  AppState copy({List<String>? items}) => AppState(items: items ?? this.items);\n\n  @override\n  String toString() => 'AppState(items: $items)';\n}\n\n// -----------------------------------------------------------------------------\n// Actions\n// -----------------------------------------------------------------------------\n\n/// Basic OptimisticCommand action that succeeds.\nclass SaveItemAction extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChanges = [];\n\n  SaveItemAction(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChanges.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    return ['reloaded'];\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error, // null on success\n  }) =>\n      true;\n}\n\n/// OptimisticCommand action that always fails sendCommandToServer.\nclass SaveItemActionThatFails extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  int saveAttempts = 0;\n\n  SaveItemActionThatFails(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    saveAttempts++;\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Save failed');\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    return ['reloaded'];\n  }\n}\n\n/// OptimisticCommand action that fails and tracks state changes (for rollback test).\nclass SaveItemActionThatFailsWithStateLog extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionThatFailsWithStateLog(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Save failed');\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    return ['reloaded'];\n  }\n}\n\n/// OptimisticCommand action that fails after another action changes the state.\n/// This tests the conditional rollback logic.\nclass SaveItemActionThatFailsAfterStateChange extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final Store<AppState> _store;\n  final List<List<String>> stateChangesLog = [];\n  bool rollbackOccurred = false;\n\n  SaveItemActionThatFailsAfterStateChange(this.newItem, this._store);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    // Track if this is a rollback (going back to initial).\n    if (stateChangesLog.isNotEmpty &&\n        newItems.length == 1 &&\n        newItems[0] == 'initial') {\n      rollbackOccurred = true;\n    }\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    // Another action changes the state during save.\n    _store.dispatch(ChangeStateAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Save failed');\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    return ['reloaded'];\n  }\n}\n\n/// Action that changes the state (used to simulate concurrent modification).\nclass ChangeStateAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => state.copy(items: ['changed_by_other']);\n}\n\n/// OptimisticCommand action that does NOT implement reloadFromServer.\nclass SaveItemActionWithoutReload extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithoutReload(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n\n// reloadFromServer is intentionally NOT overridden - uses default that throws UnimplementedError.\n}\n\n/// OptimisticCommand action that does NOT implement reloadFromServer and fails.\nclass SaveItemActionWithoutReloadThatFails extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithoutReloadThatFails(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Save failed');\n  }\n\n// reloadFromServer is intentionally NOT overridden - uses default that throws UnimplementedError.\n}\n\n/// OptimisticCommand + Retry action without reloadFromServer implementation.\nclass SaveItemActionWithRetryNoReload extends ReduxAction<AppState>\n    with OptimisticCommand<AppState>, Retry<AppState> {\n  final String newItem;\n  final int failCount;\n  int _saveAttemptCount = 0;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithRetryNoReload(this.newItem, {required this.failCount});\n\n  @override\n  Duration get initialDelay => const Duration(milliseconds: 5);\n\n  @override\n  int get maxRetries => 10;\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    _saveAttemptCount++;\n    await Future.delayed(const Duration(milliseconds: 5));\n    if (_saveAttemptCount <= failCount) {\n      throw UserException('Save failed: attempt $_saveAttemptCount');\n    }\n  }\n\n// reloadFromServer is intentionally NOT overridden - uses default that throws UnimplementedError.\n}\n\n/// OptimisticCommand + Retry action that fails [failCount] times then succeeds.\nclass SaveItemActionWithRetry extends ReduxAction<AppState>\n    with OptimisticCommand<AppState>, Retry<AppState> {\n  final String newItem;\n  final int failCount;\n  int _saveAttemptCount = 0;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithRetry(this.newItem, {required this.failCount});\n\n  @override\n  Duration get initialDelay => const Duration(milliseconds: 5);\n\n  @override\n  int get maxRetries => 10;\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    _saveAttemptCount++;\n    await Future.delayed(const Duration(milliseconds: 5));\n    if (_saveAttemptCount <= failCount) {\n      throw UserException('Save failed: attempt $_saveAttemptCount');\n    }\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    await Future.delayed(const Duration(milliseconds: 5));\n    return ['reloaded'];\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error, // null on success\n  }) =>\n      true;\n}\n\n/// OptimisticCommand + Retry action that always fails (tests rollback after exhausting retries).\nclass SaveItemActionWithRetryThatAlwaysFails extends ReduxAction<AppState>\n    with OptimisticCommand<AppState>, Retry<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithRetryThatAlwaysFails(this.newItem);\n\n  @override\n  Duration get initialDelay => const Duration(milliseconds: 5);\n\n  @override\n  int get maxRetries => 3;\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 5));\n    throw const UserException('Save always fails');\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    return ['reloaded'];\n  }\n}\n\n// -----------------------------------------------------------------------------\n// Actions for testing override methods\n// -----------------------------------------------------------------------------\n\n/// Action that overrides rollbackState to mark the item as failed instead of removing it.\nclass SaveItemActionWithCustomRollback extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n  Object? capturedError;\n\n  SaveItemActionWithCustomRollback(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Save failed');\n  }\n\n  @override\n  AppState? rollbackState({\n    required Object? initialValue,\n    required Object? optimisticValue,\n    required Object error,\n  }) {\n    capturedError = error;\n    // Instead of restoring initial value, mark the item as failed.\n    final items = optimisticValue as List<String>;\n    final markedItems =\n        items.map((item) => item == newItem ? '$item (FAILED)' : item).toList();\n    return state.copy(items: markedItems);\n  }\n\n// No reload to keep test simple.\n}\n\n/// Action that overrides rollbackState to return null (skip rollback).\nclass SaveItemActionWithRollbackReturningNull extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithRollbackReturningNull(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Save failed');\n  }\n\n  @override\n  AppState? rollbackState({\n    required Object? initialValue,\n    required Object? optimisticValue,\n    required Object error,\n  }) {\n    // Return null to skip rollback.\n    return null;\n  }\n\n// No reload to keep test simple.\n}\n\n/// Action that overrides shouldRollback to always rollback (even if state changed).\nclass SaveItemActionWithAlwaysRollback extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final Store<AppState> _store;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithAlwaysRollback(this.newItem, this._store);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    // Another action changes the state during save.\n    _store.dispatch(ChangeStateAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Save failed');\n  }\n\n  @override\n  bool shouldRollback({\n    required Object? currentValue,\n    required Object? initialValue,\n    required Object? optimisticValue,\n    required Object error,\n  }) {\n    // Always rollback, regardless of whether state changed.\n    return true;\n  }\n\n// No reload to keep test simple.\n}\n\n/// Action that overrides shouldRollback to never rollback.\nclass SaveItemActionWithNeverRollback extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithNeverRollback(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Save failed');\n  }\n\n  @override\n  bool shouldRollback({\n    required Object? currentValue,\n    required Object? initialValue,\n    required Object? optimisticValue,\n    required Object error,\n  }) {\n    // Never rollback.\n    return false;\n  }\n\n// No reload to keep test simple.\n}\n\n/// Action that overrides shouldRollback based on error type.\nclass SaveItemActionWithConditionalRollback extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final bool throwNetworkError;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithConditionalRollback(this.newItem,\n      {required this.throwNetworkError});\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    if (throwNetworkError) {\n      throw const UserException('Network error');\n    } else {\n      throw const UserException('Validation error');\n    }\n  }\n\n  @override\n  bool shouldRollback({\n    required Object? currentValue,\n    required Object? initialValue,\n    required Object? optimisticValue,\n    required Object error,\n  }) {\n    // Only rollback for validation errors, not network errors (might retry later).\n    if (error is UserException && error.toString().contains('Network error')) {\n      return false;\n    }\n    return true;\n  }\n\n// No reload to keep test simple.\n}\n\n/// Action that overrides shouldReload to skip reload on success.\nclass SaveItemActionWithConditionalReload extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final bool shouldFail;\n  final List<List<String>> stateChangesLog = [];\n  bool reloadWasCalled = false;\n\n  SaveItemActionWithConditionalReload(this.newItem, {required this.shouldFail});\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    if (shouldFail) throw const UserException('Save failed');\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error,\n  }) {\n    // Only reload on error, not on success.\n    return error != null;\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    reloadWasCalled = true;\n    return ['reloaded'];\n  }\n}\n\n/// Action that overrides shouldApplyReload to skip applying if state changed.\nclass SaveItemActionWithConditionalApplyReload extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final Store<AppState> _store;\n  final bool changeStateDuringReload;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithConditionalApplyReload(\n    this.newItem,\n    this._store, {\n    required this.changeStateDuringReload,\n  });\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n\n  @override\n  bool shouldApplyReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? reloadResult,\n    required Object? error,\n  }) {\n    // Only apply reload if state hasn't changed since we applied our value.\n    return currentValue == lastAppliedValue;\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    if (changeStateDuringReload) {\n      // Simulate another action changing state while we're reloading.\n      _store.dispatch(ChangeStateAction());\n      await Future.delayed(const Duration(milliseconds: 10));\n    }\n    return ['reloaded'];\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error, // null on success\n  }) =>\n      true;\n}\n\n/// Action that overrides applyReloadResultToState to transform reload result.\nclass SaveItemActionWithCustomApplyReload extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithCustomApplyReload(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    // Return a map instead of a list (different shape than expected by applyValueToState).\n    return {\n      'items': ['server_item1', 'server_item2'],\n      'count': 2\n    };\n  }\n\n  @override\n  AppState? applyReloadResultToState(AppState state, Object? reloadResult) {\n    // Transform the map result into what we need.\n    final map = reloadResult as Map<String, dynamic>;\n    final items = (map['items'] as List).cast<String>();\n    // Add a marker to show we transformed the data.\n    return state.copy(items: [...items, 'TRANSFORMED']);\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error, // null on success\n  }) =>\n      true;\n}\n\n/// Action that overrides applyReloadResultToState to return null (skip applying).\nclass SaveItemActionWithApplyReloadReturningNull extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n  bool reloadWasCalled = false;\n\n  SaveItemActionWithApplyReloadReturningNull(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    reloadWasCalled = true;\n    return ['reloaded'];\n  }\n\n  @override\n  AppState? applyReloadResultToState(AppState state, Object? reloadResult) {\n    // Return null to skip applying the reload result.\n    return null;\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error, // null on success\n  }) =>\n      true;\n}\n\n// -----------------------------------------------------------------------------\n// Actions for edge case tests\n// -----------------------------------------------------------------------------\n\n/// Action where sendCommandToServer succeeds but reloadFromServer throws.\nclass SaveItemActionWithReloadThatThrows extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n\n  SaveItemActionWithReloadThatThrows(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    // Succeeds - no throw.\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    throw const UserException('Reload failed');\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error, // null on success\n  }) =>\n      true;\n}\n\n/// Action where both sendCommandToServer and reloadFromServer throw.\nclass SaveItemActionWithBothCommandAndReloadThatThrow\n    extends ReduxAction<AppState> with OptimisticCommand<AppState> {\n  final String newItem;\n\n  SaveItemActionWithBothCommandAndReloadThatThrow(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Command failed');\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    throw const UserException('Reload failed');\n  }\n}\n\n/// Action that overrides shouldReload to return false when there's an error.\nclass SaveItemActionWithShouldReloadFalseOnError extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  bool reloadWasCalled = false;\n\n  SaveItemActionWithShouldReloadFalseOnError(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Command failed');\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error,\n  }) {\n    // Skip reload when there's an error.\n    return error == null;\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    reloadWasCalled = true;\n    return ['reloaded'];\n  }\n}\n\n/// Action that overrides shouldApplyReload to return false when there's an error.\nclass SaveItemActionWithShouldApplyReloadFalseOnError\n    extends ReduxAction<AppState> with OptimisticCommand<AppState> {\n  final String newItem;\n  bool reloadWasCalled = false;\n\n  SaveItemActionWithShouldApplyReloadFalseOnError(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Command failed');\n  }\n\n  @override\n  bool shouldApplyReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? reloadResult,\n    required Object? error,\n  }) {\n    // Skip applying reload when there's an error.\n    return error == null;\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    reloadWasCalled = true;\n    return ['reloaded'];\n  }\n}\n\n/// Action that captures lastAppliedValue and rollbackValue on error.\nclass SaveItemActionCaptureLastAppliedOnError extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  Object? capturedLastAppliedValue;\n  Object? capturedRollbackValue;\n\n  SaveItemActionCaptureLastAppliedOnError(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Command failed');\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error,\n  }) {\n    // Capture the values for testing.\n    capturedLastAppliedValue = lastAppliedValue;\n    capturedRollbackValue = rollbackValue;\n    return false; // Skip reload to simplify test.\n  }\n}\n\n/// Action that captures lastAppliedValue on success.\nclass SaveItemActionCaptureLastAppliedOnSuccess extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  Object? capturedLastAppliedValue;\n  Object? capturedOptimisticValue;\n  Object? capturedRollbackValue;\n\n  SaveItemActionCaptureLastAppliedOnSuccess(this.newItem);\n\n  @override\n  Object? optimisticValue() {\n    final value = [...state.items, newItem];\n    capturedOptimisticValue = value;\n    return value;\n  }\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    // Succeeds - no throw.\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error,\n  }) {\n    // Capture the values for testing.\n    capturedLastAppliedValue = lastAppliedValue;\n    capturedRollbackValue = rollbackValue;\n    return false; // Skip reload to simplify test.\n  }\n}\n\n/// Action with Retry that counts optimisticValue and sendCommandToServer calls.\nclass SaveItemActionWithOptimisticValueCounter extends ReduxAction<AppState>\n    with\n        OptimisticCommand<AppState>,\n        // ignore: private_collision_in_mixin_application\n        Retry<AppState> {\n  final String newItem;\n  final int failCount;\n  int optimisticValueCallCount = 0;\n  int sendCommandCallCount = 0;\n\n  SaveItemActionWithOptimisticValueCounter(this.newItem,\n      {required this.failCount});\n\n  @override\n  Duration get initialDelay => const Duration(milliseconds: 5);\n\n  @override\n  int get maxRetries => 10;\n\n  @override\n  Object? optimisticValue() {\n    optimisticValueCallCount++;\n    return [...state.items, newItem];\n  }\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    sendCommandCallCount++;\n    await Future.delayed(const Duration(milliseconds: 5));\n    if (sendCommandCallCount <= failCount) {\n      throw UserException('Failed: attempt $sendCommandCallCount');\n    }\n  }\n\n// No reload to keep test simple.\n}\n\n/// Action that checks identity of optimisticValue passed to sendCommandToServer.\nclass SaveItemActionCheckIdentity extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  Object? createdOptimisticValue;\n  Object? receivedValueInSendCommand;\n\n  SaveItemActionCheckIdentity(this.newItem);\n\n  @override\n  Object? optimisticValue() {\n    // Create a new list and store reference for identity check.\n    createdOptimisticValue = [...state.items, newItem];\n    return createdOptimisticValue;\n  }\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    receivedValueInSendCommand = newValue;\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n\n// No reload to keep test simple.\n}\n\n// -----------------------------------------------------------------------------\n// Actions for non-reentrant behavior tests\n// -----------------------------------------------------------------------------\n\n/// OptimisticCommand action with configurable delay (for testing non-reentrant behavior).\nclass OptimisticCommandSlowAction extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final int delayMillis;\n\n  OptimisticCommandSlowAction(this.newItem, {required this.delayMillis});\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(Duration(milliseconds: delayMillis));\n  }\n\n// No reload to keep test simple.\n}\n\n/// OptimisticCommand action that always fails (for testing key release on failure).\nclass OptimisticCommandFailingAction extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  @override\n  Object? optimisticValue() => [...state.items, 'failing_item'];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Intentional failure');\n  }\n\n// No reload to keep test simple.\n}\n\n/// OptimisticCommand action that uses nonReentrantKeyParams to differentiate by itemId.\nclass OptimisticCommandWithParams extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String itemId;\n  final String value;\n  final int delayMillis;\n\n  OptimisticCommandWithParams(this.itemId, this.value,\n      {required this.delayMillis});\n\n  @override\n  Object? nonReentrantKeyParams() => itemId;\n\n  @override\n  Object? optimisticValue() => [...state.items, value];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(Duration(milliseconds: delayMillis));\n  }\n\n// No reload to keep test simple.\n}\n\n/// OptimisticCommand action that uses nonReentrantKeyParams and always fails.\nclass OptimisticCommandWithParamsThatFails extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String itemId;\n\n  OptimisticCommandWithParamsThatFails(this.itemId);\n\n  @override\n  Object? nonReentrantKeyParams() => itemId;\n\n  @override\n  Object? optimisticValue() => [...state.items, 'failing_$itemId'];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Intentional failure');\n  }\n\n// No reload to keep test simple.\n}\n\n/// First OptimisticCommand action type that uses a shared non-reentrant key via computeNonReentrantKey.\nclass OptimisticCommandSharedKey1 extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String value;\n  final int delayMillis;\n\n  OptimisticCommandSharedKey1(this.value, {required this.delayMillis});\n\n  @override\n  Object computeNonReentrantKey() => 'sharedOptimisticKey';\n\n  @override\n  Object? optimisticValue() => [...state.items, value];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(Duration(milliseconds: delayMillis));\n  }\n\n// No reload to keep test simple.\n}\n\n/// Second OptimisticCommand action type that uses the same shared non-reentrant key.\nclass OptimisticCommandSharedKey2 extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String value;\n  final int delayMillis;\n\n  OptimisticCommandSharedKey2(this.value, {required this.delayMillis});\n\n  @override\n  Object computeNonReentrantKey() => 'sharedOptimisticKey';\n\n  @override\n  Object? optimisticValue() => [...state.items, value];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(Duration(milliseconds: delayMillis));\n  }\n\n// No reload to keep test simple.\n}\n\n/// OptimisticCommand action that also uses NonReentrant (should throw assertion error).\nclass OptimisticCommandWithNonReentrant extends ReduxAction<AppState>\n    with OptimisticCommand<AppState>, NonReentrant<AppState> {\n  final String newItem;\n\n  OptimisticCommandWithNonReentrant(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n}\n\n/// OptimisticCommand action that also uses Throttle (should throw assertion error).\nclass OptimisticCommandWithThrottle extends ReduxAction<AppState>\n    with OptimisticCommand<AppState>, Throttle<AppState> {\n  final String newItem;\n\n  OptimisticCommandWithThrottle(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<void> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n}\n\n/// OptimisticCommand action that also uses Fresh (should throw assertion error).\nclass OptimisticCommandWithFresh extends ReduxAction<AppState>\n    with OptimisticCommand<AppState>, Fresh<AppState> {\n  final String newItem;\n\n  OptimisticCommandWithFresh(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) =>\n      state.copy(items: value as List<String>);\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    return null;\n  }\n}\n\n// -----------------------------------------------------------------------------\n// Actions for testing DEFAULT shouldReload behavior (only reload on error)\n// -----------------------------------------------------------------------------\n\n/// Action that uses default shouldReload (only reload on error) - success case.\n/// reloadFromServer should NOT be called on success.\nclass SaveItemActionWithDefaultShouldReloadOnSuccess\n    extends ReduxAction<AppState> with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n  bool reloadWasCalled = false;\n\n  SaveItemActionWithDefaultShouldReloadOnSuccess(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    return null; // Success, no error\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    reloadWasCalled = true;\n    return ['reloaded'];\n  }\n\n// NOTE: We do NOT override shouldReload - it uses the default (error != null)\n}\n\n/// Action that uses default shouldReload (only reload on error) - failure case.\n/// reloadFromServer SHOULD be called on failure.\nclass SaveItemActionWithDefaultShouldReloadOnFailure\n    extends ReduxAction<AppState> with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n  bool reloadWasCalled = false;\n\n  SaveItemActionWithDefaultShouldReloadOnFailure(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Save failed');\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    reloadWasCalled = true;\n    return ['reloaded'];\n  }\n\n// NOTE: We do NOT override shouldReload - it uses the default (error != null)\n}\n\n// -----------------------------------------------------------------------------\n// Actions for testing sendCommandToServer returning a value\n// -----------------------------------------------------------------------------\n\n/// Action where sendCommandToServer returns a server response that is applied.\nclass SaveItemActionWithServerResponse extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n  bool serverResponseApplied = false;\n  Object? receivedServerResponse;\n\n  SaveItemActionWithServerResponse(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    // Server returns a confirmed response with additional data\n    return ['initial', newItem, 'server_confirmed'];\n  }\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    serverResponseApplied = true;\n    receivedServerResponse = serverResponse;\n    return state.copy(items: serverResponse as List<String>);\n  }\n\n// No reload - we just want to test server response\n}\n\n/// Action where sendCommandToServer returns null (no server response to apply).\nclass SaveItemActionWithNullServerResponse extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n  bool applyServerResponseCalled = false;\n\n  SaveItemActionWithNullServerResponse(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    return null; // No server response\n  }\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    applyServerResponseCalled = true;\n    return state.copy(items: serverResponse as List<String>);\n  }\n}\n\n/// Action where applyServerResponseToState returns null (skip applying).\nclass SaveItemActionWithServerResponseReturningNull\n    extends ReduxAction<AppState> with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n  bool applyServerResponseCalled = false;\n\n  SaveItemActionWithServerResponseReturningNull(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    return ['from_server']; // Server returns a value\n  }\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    applyServerResponseCalled = true;\n    return null; // Explicitly decide not to apply the server response\n  }\n}\n\n/// Action with both server response and reload to test ordering.\nclass SaveItemActionWithServerResponseAndReload extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<String> operationOrder = [];\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithServerResponseAndReload(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    return ['server_response'];\n  }\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    operationOrder.add('applyServerResponse');\n    return state.copy(items: serverResponse as List<String>);\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    operationOrder.add('reload');\n    return ['reloaded'];\n  }\n\n  @override\n  bool shouldReload({\n    required Object? currentValue,\n    required Object? lastAppliedValue,\n    required Object? optimisticValue,\n    required Object? rollbackValue,\n    required Object? error,\n  }) =>\n      true; // Always reload to test ordering\n}\n\n/// Action that fails in sendCommandToServer - server response should NOT be applied.\nclass SaveItemActionWithServerResponseThatFails extends ReduxAction<AppState>\n    with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n  bool applyServerResponseCalled = false;\n\n  SaveItemActionWithServerResponseThatFails(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    throw const UserException('Save failed');\n  }\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    applyServerResponseCalled = true;\n    return state.copy(items: serverResponse as List<String>);\n  }\n}\n\n/// Action with Retry that tests server response is applied after successful retry.\nclass SaveItemActionWithRetryAndServerResponse extends ReduxAction<AppState>\n    with OptimisticCommand<AppState>, Retry<AppState> {\n  final String newItem;\n  final int failCount;\n  int _saveAttemptCount = 0;\n  final List<List<String>> stateChangesLog = [];\n  bool serverResponseApplied = false;\n\n  SaveItemActionWithRetryAndServerResponse(this.newItem,\n      {required this.failCount});\n\n  @override\n  Duration get initialDelay => const Duration(milliseconds: 5);\n\n  @override\n  int get maxRetries => 10;\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    _saveAttemptCount++;\n    await Future.delayed(const Duration(milliseconds: 5));\n    if (_saveAttemptCount <= failCount) {\n      throw UserException('Save failed: attempt $_saveAttemptCount');\n    }\n    // Return server response on success\n    return ['initial', newItem, 'server_confirmed'];\n  }\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    serverResponseApplied = true;\n    return state.copy(items: serverResponse as List<String>);\n  }\n}\n\n/// Action that tests server response transformation.\nclass SaveItemActionWithServerResponseTransformation\n    extends ReduxAction<AppState> with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n\n  SaveItemActionWithServerResponseTransformation(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    // Server returns a map with more complex data\n    return {\n      'items': [newItem],\n      'timestamp': '2024-01-01',\n      'confirmed': true,\n    };\n  }\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    final map = serverResponse as Map<String, dynamic>;\n    final items = (map['items'] as List).cast<String>();\n    // Transform: add confirmed items with timestamp\n    return state.copy(\n        items:\n            items.map((i) => '$i (confirmed: ${map['timestamp']})').toList());\n  }\n}\n\n/// Action that uses default shouldReload with server response.\n/// Tests that server response is applied but reload is skipped on success.\nclass SaveItemActionWithDefaultShouldReloadAndServerResponse\n    extends ReduxAction<AppState> with OptimisticCommand<AppState> {\n  final String newItem;\n  final List<List<String>> stateChangesLog = [];\n  bool serverResponseApplied = false;\n  bool reloadWasCalled = false;\n\n  SaveItemActionWithDefaultShouldReloadAndServerResponse(this.newItem);\n\n  @override\n  Object? optimisticValue() => [...state.items, newItem];\n\n  @override\n  Object? getValueFromState(AppState state) => state.items;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) {\n    final newItems = value as List<String>;\n    stateChangesLog.add(newItems);\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendCommandToServer(Object? newValue) async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    // Server returns a confirmed response\n    return ['initial', newItem, 'server_confirmed'];\n  }\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    serverResponseApplied = true;\n    return state.copy(items: serverResponse as List<String>);\n  }\n\n  @override\n  Future<Object?> reloadFromServer() async {\n    reloadWasCalled = true;\n    return ['reloaded'];\n  }\n\n// NOTE: We do NOT override shouldReload - it uses the default (error != null)\n// So reload will NOT happen on success, but server response will still be applied.\n}\n"
  },
  {
    "path": "test/optimistic_sync_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart' hide Retry;\n\nvoid main() {\n  var feature = BddFeature('OptimisticSync mixin');\n\n  // ==========================================================================\n  // Case 1: Single dispatch applies optimistic update and sends request\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Single dispatch applies optimistic update and sends request.')\n      .given('An action with the OptimisticSync mixin.')\n      .when('The action is dispatched once.')\n      .then('The optimistic update is applied immediately.')\n      .and('The request is sent to the server.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n    requestLog.clear();\n\n    await store.dispatchAndWait(ToggleLikeAction());\n\n    expect(store.state.liked, true);\n    expect(requestLog, ['saveValue(true)', 'onFinish()']);\n  });\n\n  // ==========================================================================\n  // Case 2: Rapid dispatches apply all optimistic updates but coalesce requests\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Rapid dispatches apply all optimistic updates but coalesce requests.')\n      .given('An action with the OptimisticSync mixin.')\n      .when('The action is dispatched multiple times rapidly.')\n      .then('All optimistic updates are applied immediately.')\n      .and('Only necessary requests are sent (coalesced).')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n    requestLog.clear();\n    saveValueDelay = const Duration(milliseconds: 100);\n\n    // Dispatch rapidly: false -> true -> false -> true\n    store.dispatch(ToggleLikeAction()); // false -> true, sends request\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n\n    store.dispatch(ToggleLikeAction()); // true -> false, locked\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false);\n\n    store.dispatch(ToggleLikeAction()); // false -> true, locked\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n\n    // Wait for all requests to complete\n    await store.waitAllActions([]);\n\n    // Final state should be true (last toggle)\n    expect(store.state.liked, true);\n\n    // Request log: first sends true, no follow-up needed, then onFinish at end\n    expect(requestLog, ['saveValue(true)', 'onFinish()']);\n\n    saveValueDelay = Duration.zero;\n  });\n\n  // ==========================================================================\n  // Case 3: Follow-up request sent when state differs after completion\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Follow-up request sent when state differs after completion.')\n      .given('An action with the OptimisticSync mixin.')\n      .when('The state changes while a request is in flight.')\n      .and('The final state differs from what was sent.')\n      .then('A follow-up request is sent with the new state.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n    requestLog.clear();\n    saveValueDelay = const Duration(milliseconds: 100);\n\n    // Dispatch: false -> true (sends request)\n    store.dispatch(ToggleLikeAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n\n    // Dispatch while locked: true -> false\n    store.dispatch(ToggleLikeAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false);\n\n    // Wait for all to complete\n    await store.waitAllActions([]);\n\n    expect(store.state.liked, false);\n\n    // First request sent true, then follow-up sent false, then onFinish at end\n    expect(requestLog, ['saveValue(true)', 'saveValue(false)', 'onFinish()']);\n\n    saveValueDelay = Duration.zero;\n  });\n\n  // ==========================================================================\n  // Case 4: No follow-up when state returns to sent value\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('No follow-up when state returns to sent value.')\n      .given('An action with the OptimisticSync mixin.')\n      .when('The state changes while a request is in flight.')\n      .and('The state returns to the value that was sent.')\n      .then('No follow-up request is sent.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n    requestLog.clear();\n    saveValueDelay = const Duration(milliseconds: 100);\n\n    // Dispatch: false -> true (sends request)\n    store.dispatch(ToggleLikeAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Dispatch: true -> false\n    store.dispatch(ToggleLikeAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Dispatch: false -> true (back to sent value)\n    store.dispatch(ToggleLikeAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    await store.waitAllActions([]);\n\n    expect(store.state.liked, true);\n    // Only one request needed since final state matches sent value, then onFinish\n    expect(requestLog, ['saveValue(true)', 'onFinish()']);\n\n    saveValueDelay = Duration.zero;\n  });\n\n  // ==========================================================================\n  // Case 5: Error calls onFinish and keeps optimistic state\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Error calls onFinish and keeps optimistic state.')\n      .given('An action with the OptimisticSync mixin.')\n      .when('The request fails.')\n      .then('onFinish is called.')\n      .and('The optimistic state remains.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n    requestLog.clear();\n    shouldFail = true;\n\n    await store.dispatchAndWait(ToggleLikeAction());\n\n    // Optimistic update remains since no reload (onFinish is general-purpose)\n    expect(store.state.liked, true);\n    expect(requestLog, ['saveValue(true)', 'onFinish()']);\n\n    shouldFail = false;\n  });\n\n  // ==========================================================================\n  // Case 6: Different keys can have concurrent requests\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Different keys can have concurrent requests.')\n      .given('Actions with different optimisticSyncKeyParams.')\n      .when('Both are dispatched concurrently.')\n      .then('Both requests are sent in parallel.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, items: {'A': false, 'B': false}));\n    requestLog.clear();\n    saveValueDelay = const Duration(milliseconds: 100);\n\n    // Dispatch for item A and B concurrently\n    store.dispatch(ToggleLikeItemAction('A'));\n    store.dispatch(ToggleLikeItemAction('B'));\n\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Both optimistic updates applied\n    expect(store.state.items['A'], true);\n    expect(store.state.items['B'], true);\n\n    await store.waitAllActions([]);\n\n    // Both requests sent (not blocked by each other)\n    expect(requestLog.contains('saveValue(A, true)'), true);\n    expect(requestLog.contains('saveValue(B, true)'), true);\n\n    saveValueDelay = Duration.zero;\n  });\n\n  // ==========================================================================\n  // Case 7: Same key blocks concurrent requests\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Same key blocks concurrent requests.')\n      .given('Actions with the same optimisticSyncKeyParams.')\n      .when('Both are dispatched while the first is in flight.')\n      .then('The second does not send a request until the first completes.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, items: {'A': false}));\n    requestLog.clear();\n    saveValueDelay = const Duration(milliseconds: 100);\n\n    // Dispatch twice for same item\n    store.dispatch(ToggleLikeItemAction('A')); // false -> true\n    await Future.delayed(const Duration(milliseconds: 10));\n    store.dispatch(ToggleLikeItemAction('A')); // true -> false (locked)\n\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Both optimistic updates applied\n    expect(store.state.items['A'], false);\n\n    // At this point, only one request should have started\n    expect(requestLog, ['saveValue(A, true)']);\n\n    await store.waitAllActions([]);\n\n    // After completion, follow-up request sent, then onFinish at end\n    expect(requestLog,\n        ['saveValue(A, true)', 'saveValue(A, false)', 'onFinish(A)']);\n\n    saveValueDelay = Duration.zero;\n  });\n\n  // ==========================================================================\n  // Case 8: Lock is released after successful request\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Lock is released after successful request.')\n      .given('A OptimisticSync action has completed successfully.')\n      .when('The same action is dispatched again.')\n      .then('A new request is sent (not blocked).')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n    requestLog.clear();\n\n    // First dispatch\n    await store.dispatchAndWait(ToggleLikeAction());\n    expect(store.state.liked, true);\n    expect(requestLog, ['saveValue(true)', 'onFinish()']);\n\n    // Second dispatch after completion\n    await store.dispatchAndWait(ToggleLikeAction());\n    expect(store.state.liked, false);\n    expect(requestLog,\n        ['saveValue(true)', 'onFinish()', 'saveValue(false)', 'onFinish()']);\n  });\n\n  // ==========================================================================\n  // Case 9: Lock is released after failed request\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Lock is released after failed request.')\n      .given('A OptimisticSync action has failed.')\n      .when('The same action is dispatched again.')\n      .then('A new request is sent (not blocked).')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n    requestLog.clear();\n    shouldFail = true;\n\n    // First dispatch (fails)\n    await store.dispatchAndWait(ToggleLikeAction());\n    expect(store.state.liked, true); // Optimistic state remains\n    expect(requestLog, ['saveValue(true)', 'onFinish()']);\n\n    // Second dispatch after failure\n    shouldFail = false;\n    await store.dispatchAndWait(ToggleLikeAction());\n    expect(store.state.liked, false);\n    expect(requestLog,\n        ['saveValue(true)', 'onFinish()', 'saveValue(false)', 'onFinish()']);\n  });\n\n  // ==========================================================================\n  // Case 10: Multiple follow-up requests when state keeps changing\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Multiple follow-up requests when state keeps changing.')\n      .given('An action with the OptimisticSync mixin.')\n      .when('The state changes during each request.')\n      .then('Follow-up requests are sent until state stabilizes.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n    requestLog.clear();\n\n    var requestCount = 0;\n    saveValueCallback = () async {\n      requestCount++;\n      await Future.delayed(const Duration(milliseconds: 50));\n      // Toggle state during first two requests\n      if (requestCount <= 2) {\n        store.dispatch(ToggleLikeAction());\n      }\n    };\n\n    await store.dispatchAndWait(ToggleLikeAction());\n\n    // Should have sent multiple follow-up requests\n    expect(requestLog.length, greaterThan(1));\n\n    saveValueCallback = null;\n  });\n\n  // ==========================================================================\n  // Case 11: OptimisticSync cannot be combined with NonReentrant\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('OptimisticSync cannot be combined with NonReentrant.')\n      .given('An action that combines OptimisticSync and NonReentrant.')\n      .when('The action is dispatched.')\n      .then('An assertion error is thrown.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n\n    expect(\n      () => store.dispatch(OptimisticSyncWithNonReentrantAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The OptimisticSync mixin cannot be combined with the NonReentrant mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 12: OptimisticSync cannot be combined with Throttle\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('OptimisticSync cannot be combined with Throttle.')\n      .given('An action that combines OptimisticSync and Throttle.')\n      .when('The action is dispatched.')\n      .then('An assertion error is thrown.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n\n    expect(\n      () => store.dispatch(OptimisticSyncWithThrottleAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The OptimisticSync mixin cannot be combined with the Throttle mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 13: computeOptimisticSyncKey can be overridden to share keys\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('computeOptimisticSyncKey can be overridden to share keys.')\n      .given(\n          'Two different action types with the same computeOptimisticSyncKey.')\n      .when('Both are dispatched while the first is in flight.')\n      .then('They share the same lock.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false, count: 0));\n    requestLog.clear();\n    saveValueDelay = const Duration(milliseconds: 100);\n\n    // Dispatch first action type\n    store.dispatch(SharedKeyAction1());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.count, 1);\n\n    // Dispatch second action type with same key (should be locked)\n    store.dispatch(SharedKeyAction2());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.count, 2); // Optimistic update applied\n\n    // At this point, only first request sent\n    expect(requestLog, ['saveValue(sharedKey, 1)']);\n\n    await store.waitAllActions([]);\n\n    // Follow-up with current value\n    expect(requestLog, ['saveValue(sharedKey, 1)', 'saveValue(sharedKey, 2)']);\n\n    saveValueDelay = Duration.zero;\n  });\n\n  // ==========================================================================\n  // Case 14: State cleanup after store shutdown\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Coalescing state is cleared on store shutdown.')\n      .given('A OptimisticSync action is in progress.')\n      .when('The store is shut down.')\n      .then('The coalescing state is cleared.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n    requestLog.clear();\n    saveValueDelay = const Duration(milliseconds: 50);\n\n    // Start a request\n    store.dispatch(ToggleLikeAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Shutdown store\n    store.shutdown();\n\n    // Wait for old store's action to complete (it continues running even after shutdown)\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    // Create new store - should have fresh coalescing state\n    var newStore = Store<AppState>(initialState: AppState(liked: false));\n    requestLog.clear();\n\n    // Should be able to dispatch without being blocked by old state\n    await newStore.dispatchAndWait(ToggleLikeAction());\n    expect(newStore.state.liked, true);\n    expect(requestLog, ['saveValue(true)', 'onFinish()']);\n\n    saveValueDelay = Duration.zero;\n  });\n\n  // ==========================================================================\n  // Case 15: OptimisticSync cannot be combined with Fresh\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('OptimisticSync cannot be combined with Fresh.')\n      .given('An action that combines OptimisticSync and Fresh.')\n      .when('The action is dispatched.')\n      .then('An assertion error is thrown.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n\n    expect(\n      () => store.dispatch(OptimisticSyncWithFreshAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The OptimisticSync mixin cannot be combined with the Fresh mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 16: OptimisticSync cannot be combined with UnlimitedRetryCheckInternet\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'OptimisticSync cannot be combined with UnlimitedRetryCheckInternet.')\n      .given(\n          'An action that combines OptimisticSync and UnlimitedRetryCheckInternet.')\n      .when('The action is dispatched.')\n      .then('An assertion error is thrown.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n\n    expect(\n      () =>\n          store.dispatch(OptimisticSyncWithUnlimitedRetryCheckInternetAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The UnlimitedRetryCheckInternet mixin cannot be combined with the OptimisticSync mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 17: OptimisticSync cannot be combined with UnlimitedRetries\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('OptimisticSync cannot be combined with UnlimitedRetries.')\n      .given('An action that combines OptimisticSync and UnlimitedRetries.')\n      .when('The action is dispatched.')\n      .then('An assertion error is thrown.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n\n    expect(\n      () => store.dispatch(OptimisticSyncWithUnlimitedRetriesAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The Retry mixin cannot be combined with the OptimisticSync mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 19: OptimisticSync cannot be combined with OptimisticSyncWithPush\n  // ==========================================================================\n  // NOTE: This combination now causes a COMPILE-TIME error because the two\n  // mixins define sendValueToServer with different signatures:\n  // - OptimisticSync: sendValueToServer(Object? optimisticValue)\n  // - OptimisticSyncWithPush: sendValueToServer(Object? optimisticValue, int localRevision, int deviceId)\n  //\n  // This is actually BETTER than a runtime assertion error because it\n  // catches the incompatibility at compile time. The test below is skipped\n  // since we cannot even create such a class.\n  //\n  // To verify: try uncommenting OptimisticSyncWithOptimisticSyncWithPushAction\n  // in this file and you'll get a compilation error.\n\n  // ==========================================================================\n  // Case 21: OptimisticSync cannot be combined with Debounce\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('OptimisticSync cannot be combined with Debounce.')\n      .given('An action that combines OptimisticSync and Debounce.')\n      .when('The action is dispatched.')\n      .then('An assertion error is thrown.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n\n    expect(\n      () => store.dispatch(OptimisticSyncWithDebounceAction()),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The OptimisticSync mixin cannot be combined with the Debounce mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 22: Server response is applied when non-null and state is stable\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Server response is applied when sendValueToServer returns a non-null response and state is stable.')\n      .given(\n          'An action with the OptimisticSync mixin where sendValueToServer returns a non-null response.')\n      .when(\n          'The action is dispatched once and no other dispatch happens while the request is in flight.')\n      .then('The optimistic update is applied immediately.')\n      .and(\n          'After the request completes, applyServerResponseToState is applied using the server response.')\n      .and('onFinish is called once after synchronization completes.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false, count: 0));\n    requestLog.clear();\n\n    // Dispatch action that returns server response\n    await store.dispatchAndWait(ServerResponseAction(increment: 10));\n\n    // Optimistic update was 10, but server returns 15 (normalized value)\n    expect(store.state.count, 15);\n    expect(requestLog, ['saveValue(10)', 'serverResponse(15)', 'onFinish()']);\n  });\n\n  // ==========================================================================\n  // Case 23: Earlier server response does not overwrite newer local optimistic value\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'An earlier server response does not overwrite a newer local optimistic value.')\n      .given(\n          'An action with the OptimisticSync mixin where sendValueToServer returns a non-null response.')\n      .when('The action is dispatched and a request is in flight.')\n      .and(\n          'The action is dispatched again for the same key while the first request is still in flight.')\n      .then(\n          'The latest optimistic value remains visible after the second dispatch.')\n      .and(\n          'The response from the first request is not applied in a way that overwrites the newer optimistic value.')\n      .and(\n          'If a follow-up request is needed, only the final stabilized result is applied.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false, count: 0));\n    requestLog.clear();\n    saveValueDelay = const Duration(milliseconds: 100);\n\n    // Dispatch first action: optimistic count = 10\n    store.dispatch(ServerResponseAction(increment: 10));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.count, 10, reason: 'First optimistic update');\n\n    // Dispatch second action while first is in flight: optimistic count = 20\n    store.dispatch(ServerResponseAction(increment: 10));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.count, 20, reason: 'Second optimistic update');\n\n    // Wait for all to complete\n    await store.waitAllActions([]);\n\n    // First request would have returned 15 (server normalized 10 to 15),\n    // but that should NOT be applied because state changed while in flight.\n    // A follow-up request was sent with 20, which server normalizes to 25.\n    expect(store.state.count, 25, reason: 'Final state from follow-up');\n\n    // Request log shows: first request, follow-up request, then only final serverResponse applied\n    expect(requestLog,\n        ['saveValue(10)', 'saveValue(20)', 'serverResponse(25)', 'onFinish()']);\n\n    saveValueDelay = Duration.zero;\n  });\n\n  // ==========================================================================\n  // Case 24: Server response is ignored when applyServerResponseToState returns null\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Server response is ignored when applyServerResponseToState returns null.')\n      .given(\n          'An action with the OptimisticSync mixin where sendValueToServer returns a non-null response.')\n      .when('The action is dispatched and completes successfully.')\n      .then('The optimistic update is applied immediately.')\n      .and('The server response is not applied to the state.')\n      .and('onFinish is still called after synchronization completes.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false, count: 0));\n    requestLog.clear();\n\n    // Dispatch action that ignores server response\n    await store.dispatchAndWait(IgnoreServerResponseAction(increment: 10));\n\n    // Optimistic update was 10, server returned 15, but it's ignored\n    expect(store.state.count, 10, reason: 'Server response ignored');\n    expect(requestLog, ['saveValue(10)', 'onFinish()']);\n  });\n\n  // ==========================================================================\n  // Case 25: With multiple follow-ups, only the final server response is applied\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'With multiple follow-up requests, only the final non-null server response is applied.')\n      .given(\n          'An action with the OptimisticSync mixin where sendValueToServer returns a non-null response.')\n      .when(\n          'The action is dispatched and the state changes during each request, causing multiple follow-up requests.')\n      .then('Multiple requests are sent until the state stabilizes.')\n      .and('Only the final server response is applied to the state.')\n      .and('onFinish is called once after synchronization completes.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false, count: 0));\n    requestLog.clear();\n\n    var requestCount = 0;\n    saveValueCallback = () async {\n      requestCount++;\n      await Future.delayed(const Duration(milliseconds: 50));\n      // Dispatch again during the first two requests to force follow-ups\n      if (requestCount <= 2) {\n        store.dispatch(ServerResponseAction(increment: 10));\n      }\n    };\n\n    // Initial dispatch: count goes from 0 to 10\n    await store.dispatchAndWait(ServerResponseAction(increment: 10));\n\n    // Should have sent 3 requests (initial + 2 follow-ups)\n    // Only the final server response should be applied\n    // Request chain: 10 -> server returns 15 (not applied, state changed to 20)\n    //                20 -> server returns 25 (not applied, state changed to 30)\n    //                30 -> server returns 35 (applied, state stable)\n    expect(store.state.count, 35, reason: 'Only final server response applied');\n\n    // Verify 3 saveValue calls, but only 1 serverResponse applied\n    expect(requestLog.where((e) => e.startsWith('saveValue')).length, 3);\n    expect(requestLog.where((e) => e.startsWith('serverResponse')).length, 1);\n    expect(requestLog.last, 'onFinish()');\n\n    saveValueCallback = null;\n  });\n\n  // ===========================================================================\n  // Case 26: Bug demonstration WITHOUT revisions\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('BUG: Push can cause missed follow-up.')\n      .given('An action WITHOUT revision tracking.')\n      .when('User taps twice and push arrives between taps.')\n      .then('OptimisticSync incorrectly thinks state is stable.')\n      .note('With push we must use the `OptimisticSyncWithPush` mixin instead.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n\n    // Reset shared test controls to avoid bleed-over from previous scenarios.\n    requestLog.clear();\n    saveValueDelay = Duration.zero;\n    shouldFail = false;\n    saveValueCallback = null;\n    requestCompleter = null;\n\n    // Use completer to control when request completes\n    final request1Completer = Completer<void>();\n    requestCompleter = request1Completer;\n\n    // Tap #1: liked=false -> liked=true (optimistic)\n    store.dispatch(ToggleLikeAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true, reason: 'Tap #1 optimistic update');\n    expect(requestLog, ['saveValue(true)']);\n\n    // Tap #2 (while request 1 in flight): liked=true -> liked=false (optimistic)\n    store.dispatch(ToggleLikeAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false, reason: 'Tap #2 optimistic update');\n\n    // Push arrives (echo of request 1) - overwrites optimistic state!\n    // This simulates a WebSocket push arriving before request completes\n    store.dispatch(SimulatePushAction(liked: true));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true, reason: 'Push overwrote optimistic state');\n\n    // Request 1 completes\n    request1Completer.complete();\n    await Future.delayed(const Duration(milliseconds: 50));\n\n    // BUG: OptimisticSync sees store=true, sent=true, thinks it's stable!\n    // No follow-up sent, final state is WRONG (user's last tap was false)\n    expect(store.state.liked, true,\n        reason: 'BUG: Final state is wrong (should be false)');\n    expect(requestLog, ['saveValue(true)', 'onFinish()'],\n        reason: 'BUG: No follow-up request sent');\n  });\n}\n\n// =============================================================================\n// Test state and helpers\n// =============================================================================\n\nclass AppState {\n  final bool liked;\n  final Map<String, bool> items;\n  final int count;\n\n  AppState({\n    required this.liked,\n    this.items = const {},\n    this.count = 0,\n  });\n\n  AppState copy({bool? liked, Map<String, bool>? items, int? count}) =>\n      AppState(\n        liked: liked ?? this.liked,\n        items: items ?? this.items,\n        count: count ?? this.count,\n      );\n\n  @override\n  String toString() => 'AppState(liked: $liked, items: $items, count: $count)';\n}\n\n// Test control variables\nList<String> requestLog = [];\nDuration saveValueDelay = Duration.zero;\nbool shouldFail = false;\nFuture<void> Function()? saveValueCallback;\n\n// =============================================================================\n// Test actions\n// =============================================================================\n\n/// Basic toggle like action.\nclass ToggleLikeAction extends ReduxAction<AppState>\n    with OptimisticSync<AppState, bool> {\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(state, bool optimisticValueToApply) =>\n      state.copy(liked: optimisticValueToApply);\n\n  @override\n  AppState applyServerResponseToState(state, Object? serverResponse) =>\n      state.copy(liked: serverResponse as bool);\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async {\n    requestLog.add('saveValue($value)');\n    if (saveValueCallback != null) {\n      await saveValueCallback!();\n    } else if (saveValueDelay != Duration.zero) {\n      await Future.delayed(saveValueDelay);\n    } else if (requestCompleter != null) {\n      // Allow tests to hold the request open until they manually complete it.\n      final completer = requestCompleter!;\n      requestCompleter = null;\n      await completer.future;\n    }\n    if (shouldFail) {\n      throw const UserException('Send failed');\n    }\n    return null;\n  }\n\n  @override\n  Future<AppState?> onFinish(Object? error) async {\n    requestLog.add('onFinish()');\n    return null;\n  }\n}\n\n/// Toggle like for a specific item (uses optimisticSyncKeyParams).\nclass ToggleLikeItemAction extends ReduxAction<AppState>\n    with OptimisticSync<AppState, bool> {\n  final String itemId;\n\n  ToggleLikeItemAction(this.itemId);\n\n  @override\n  Object? optimisticSyncKeyParams() => itemId;\n\n  @override\n  bool valueToApply() => !(state.items[itemId] ?? false);\n\n  @override\n  bool getValueFromState(AppState state) => state.items[itemId] ?? false;\n\n  @override\n  AppState applyOptimisticValueToState(state, bool optimisticValueToApply) {\n    final newItems = Map<String, bool>.from(state.items);\n    newItems[itemId] = optimisticValueToApply;\n    return state.copy(items: newItems);\n  }\n\n  @override\n  AppState applyServerResponseToState(state, Object? serverResponse) {\n    final newItems = Map<String, bool>.from(state.items);\n    newItems[itemId] = serverResponse as bool;\n    return state.copy(items: newItems);\n  }\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async {\n    requestLog.add('saveValue($itemId, $value)');\n    if (saveValueDelay != Duration.zero) {\n      await Future.delayed(saveValueDelay);\n    }\n    if (shouldFail) {\n      throw const UserException('Send failed');\n    }\n    return null;\n  }\n\n  @override\n  Future<AppState?> onFinish(Object? error) async {\n    requestLog.add('onFinish($itemId)');\n    return null;\n  }\n}\n\n/// Action that returns a non-null server response.\n/// Server \"normalizes\" the value by adding 5 (e.g., 10 becomes 15).\nclass ServerResponseAction extends ReduxAction<AppState>\n    with OptimisticSync<AppState, int> {\n  final int increment;\n\n  ServerResponseAction({required this.increment});\n\n  @override\n  int valueToApply() => state.count + increment;\n\n  @override\n  int getValueFromState(AppState state) => state.count;\n\n  @override\n  AppState applyOptimisticValueToState(state, int optimisticValueToApply) =>\n      state.copy(count: optimisticValueToApply);\n\n  @override\n  AppState? applyServerResponseToState(state, Object serverResponse) {\n    requestLog.add('serverResponse($serverResponse)');\n    return state.copy(count: serverResponse as int);\n  }\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async {\n    requestLog.add('saveValue($value)');\n    if (saveValueCallback != null) {\n      await saveValueCallback!();\n    } else if (saveValueDelay != Duration.zero) {\n      await Future.delayed(saveValueDelay);\n    }\n    // Server \"normalizes\" the value by adding 5\n    return (value as int) + 5;\n  }\n\n  @override\n  Future<AppState?> onFinish(Object? error) async {\n    requestLog.add('onFinish()');\n    return null;\n  }\n}\n\n/// Action that returns a non-null server response but ignores it.\nclass IgnoreServerResponseAction extends ReduxAction<AppState>\n    with OptimisticSync<AppState, int> {\n  final int increment;\n\n  IgnoreServerResponseAction({required this.increment});\n\n  @override\n  int valueToApply() => state.count + increment;\n\n  @override\n  int getValueFromState(AppState state) => state.count;\n\n  @override\n  AppState applyOptimisticValueToState(state, int optimisticValueToApply) =>\n      state.copy(count: optimisticValueToApply);\n\n  @override\n  AppState? applyServerResponseToState(state, Object serverResponse) {\n    // Intentionally return null to ignore the server response\n    return null;\n  }\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async {\n    requestLog.add('saveValue($value)');\n    // Server returns a value, but we'll ignore it\n    return (value as int) + 5;\n  }\n\n  @override\n  Future<AppState?> onFinish(Object? error) async {\n    requestLog.add('onFinish()');\n    return null;\n  }\n}\n\n/// Action with shared key (type 1).\nclass SharedKeyAction1 extends ReduxAction<AppState>\n    with OptimisticSync<AppState, int> {\n  @override\n  Object computeOptimisticSyncKey() => 'sharedKey';\n\n  @override\n  int valueToApply() => state.count + 1;\n\n  @override\n  int getValueFromState(AppState state) => state.count;\n\n  @override\n  AppState applyOptimisticValueToState(state, int optimisticValueToApply) =>\n      state.copy(count: optimisticValueToApply);\n\n  @override\n  AppState applyServerResponseToState(state, Object? serverResponse) =>\n      state.copy(count: serverResponse as int);\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async {\n    requestLog.add('saveValue(sharedKey, $value)');\n    if (saveValueDelay != Duration.zero) {\n      await Future.delayed(saveValueDelay);\n    }\n    return null;\n  }\n}\n\n/// Action with shared key (type 2).\nclass SharedKeyAction2 extends ReduxAction<AppState>\n    with OptimisticSync<AppState, int> {\n  @override\n  Object computeOptimisticSyncKey() => 'sharedKey';\n\n  @override\n  int valueToApply() => state.count + 1;\n\n  @override\n  int getValueFromState(AppState state) => state.count;\n\n  @override\n  AppState applyOptimisticValueToState(state, int optimisticValueToApply) =>\n      state.copy(count: optimisticValueToApply);\n\n  @override\n  AppState applyServerResponseToState(state, Object? serverResponse) =>\n      state.copy(count: serverResponse as int);\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async {\n    requestLog.add('saveValue(sharedKey, $value)');\n    if (saveValueDelay != Duration.zero) {\n      await Future.delayed(saveValueDelay);\n    }\n    return null;\n  }\n}\n\n// =============================================================================\n// Incompatible mixin combinations\n// =============================================================================\n\nclass OptimisticSyncWithNonReentrantAction extends ReduxAction<AppState>\n    with\n        OptimisticSync<AppState, bool>,\n        // ignore: private_collision_in_mixin_application\n        NonReentrant {\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(state, bool optimisticValueToApply) =>\n      state.copy(liked: optimisticValueToApply);\n\n  @override\n  AppState applyServerResponseToState(state, Object? serverResponse) =>\n      state.copy(liked: serverResponse as bool);\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async => null;\n}\n\nclass OptimisticSyncWithThrottleAction extends ReduxAction<AppState>\n    with\n        OptimisticSync<AppState, bool>,\n        // ignore: private_collision_in_mixin_application\n        Throttle {\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(state, bool optimisticValueToApply) =>\n      state.copy(liked: optimisticValueToApply);\n\n  @override\n  AppState applyServerResponseToState(state, Object? serverResponse) =>\n      state.copy(liked: serverResponse as bool);\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async => null;\n}\n\nclass OptimisticSyncWithDebounceAction extends ReduxAction<AppState>\n    with\n        OptimisticSync<AppState, bool>,\n        // ignore: private_collision_in_mixin_application\n        Debounce {\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(state, bool optimisticValueToApply) =>\n      state.copy(liked: optimisticValueToApply);\n\n  @override\n  AppState applyServerResponseToState(state, Object? serverResponse) =>\n      state.copy(liked: serverResponse as bool);\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async => null;\n}\n\nclass OptimisticSyncWithFreshAction extends ReduxAction<AppState>\n    with\n        OptimisticSync<AppState, bool>,\n        // ignore: private_collision_in_mixin_application\n        Fresh {\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(state, bool optimisticValueToApply) =>\n      state.copy(liked: optimisticValueToApply);\n\n  @override\n  AppState applyServerResponseToState(state, Object? serverResponse) =>\n      state.copy(liked: serverResponse as bool);\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async => null;\n}\n\nclass OptimisticSyncWithUnlimitedRetryCheckInternetAction\n    extends ReduxAction<AppState>\n    with\n        OptimisticSync<AppState, bool>,\n        // ignore: private_collision_in_mixin_application\n        UnlimitedRetryCheckInternet {\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(state, bool optimisticValueToApply) =>\n      state.copy(liked: optimisticValueToApply);\n\n  @override\n  AppState applyServerResponseToState(state, Object? serverResponse) =>\n      state.copy(liked: serverResponse as bool);\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async => null;\n}\n\nclass OptimisticSyncWithUnlimitedRetriesAction extends ReduxAction<AppState>\n    with\n        OptimisticSync<AppState, bool>,\n        // ignore: private_collision_in_mixin_application\n        Retry<AppState>,\n        // ignore: private_collision_in_mixin_application\n        UnlimitedRetries<AppState> {\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(state, bool optimisticValueToApply) =>\n      state.copy(liked: optimisticValueToApply);\n\n  @override\n  AppState applyServerResponseToState(state, Object? serverResponse) =>\n      state.copy(liked: serverResponse as bool);\n\n  @override\n  Future<Object?> sendValueToServer(Object? value) async => null;\n}\n\n// This class is intentionally commented out because combining OptimisticSync\n// and OptimisticSyncWithPush causes a COMPILE-TIME error due to conflicting\n// sendValueToServer signatures. See Case 19 comment above.\n//\n// class OptimisticSyncWithOptimisticSyncWithPushAction\n//     extends ReduxAction<AppState>\n//     with\n//         OptimisticSync<AppState, bool>,\n//         OptimisticSyncWithPush<AppState, bool> {\n//   @override\n//   bool valueToApply() => !state.liked;\n//\n//   @override\n//   bool getValueFromState(AppState state) => state.liked;\n//\n//   @override\n//   AppState applyOptimisticValueToState(state, bool optimisticValueToApply) =>\n//       state.copy(liked: optimisticValueToApply);\n//\n//   @override\n//   AppState applyServerResponseToState(state, Object? serverResponse) =>\n//       state.copy(liked: serverResponse as bool);\n//\n//   @override\n//   Future<Object?> sendValueToServer(\n//     Object? optimisticValue,\n//     int localRevision,\n//     int deviceId,\n//   ) async =>\n//       null;\n//\n//   @override\n//   int getServerRevisionFromState(Object? key) => -1;\n// }\n\n// =============================================================================\n// Push simulation actions\n// =============================================================================\n\n/// Simulates a push update WITHOUT revision tracking.\n/// Used to demonstrate the bug.\nclass SimulatePushAction extends ReduxAction<AppState> {\n  final bool liked;\n\n  SimulatePushAction({required this.liked});\n\n  @override\n  AppState reduce() => state.copy(liked: liked);\n}\n\nCompleter<void>? requestCompleter;\n"
  },
  {
    "path": "test/optimistic_sync_with_push_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n/// These tests verify that [OptimisticSyncWithPush] correctly handles\n/// server-pushed updates (e.g., via WebSockets) when using the revision-based\n/// synchronization system.\n///\n/// The revision system consists of:\n/// - [localRevision]: Tracks local user intent (increments on each dispatch)\n/// - [informServerRevision]: Reports the server's revision from responses/pushes\n///\n/// This ensures that:\n/// 1. Push updates don't cause incorrect \"stable\" detection\n/// 2. Last-write-wins semantics work across devices\n/// 3. Out-of-order/replay pushes don't regress state\nvoid main() {\n  var feature = BddFeature('OptimisticSyncWithPush mixin');\n\n  setUp(() {\n    resetTestState();\n  });\n\n  // ===========================================================================\n  // Case 2: Fix WITH revisions - follow-up correctly sent\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('FIX: With revisions, push does not prevent follow-up.')\n      .given('An action WITH revision tracking.')\n      .when('User taps twice and push arrives between taps.')\n      .then(\n          'OptimisticSyncWithPush correctly sends follow-up based on localRevision.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    // Set up server to return sequential revisions\n    nextServerRevision = 11;\n\n    // Tap #1: liked=false -> liked=true (optimistic), localRev=1\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true, reason: 'Tap #1 optimistic update');\n\n    // Tap #2 (while request 1 potentially still processing): localRev=2\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false, reason: 'Tap #2 optimistic update');\n\n    // Wait for all actions to complete\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    // With revisions, the follow-up should have been sent because\n    // localRev(2) > sentLocalRev(1) at the time request 1 completed\n    expect(requestLog.where((s) => s.startsWith('sendValue')).length,\n        greaterThanOrEqualTo(2),\n        reason: 'Follow-up should be sent');\n  });\n\n  // ===========================================================================\n  // Case 3: Remote device wins (last write wins)\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('Remote device wins under last-write-wins.')\n      .given('This device taps LIKE and sends request.')\n      .when('Other device sends UNLIKE with newer serverRev via push.')\n      .then('Remote wins, push value is preserved.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n\n    // This device taps LIKE: localRev=1\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n\n    // Other device sets UNLIKE with newer serverRev=12 (via push)\n    store.dispatch(SimulatePushWithRevisionAction(liked: false, serverRev: 12));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false, reason: 'Push from other device applied');\n    expect(store.state.serverRevision, 12);\n\n    // Wait for our request to complete\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    // The push's value should be preserved because it had a newer serverRev\n    // and the server response (serverRev=11) is stale\n    expect(store.state.serverRevision, 12, reason: 'Push serverRev preserved');\n  });\n\n  // ===========================================================================\n  // Case 4: Local wins over older remote push\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('Local wins when remote push is older.')\n      .given('This device taps LIKE and sends request.')\n      .when('Request completes, then older push arrives.')\n      .then('Local wins, stale push is ignored.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 15;\n\n    // This device taps LIKE: localRev=1\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 15);\n\n    // Old push arrives (serverRev=12 < 15) - should be IGNORED\n    store.dispatch(SimulatePushWithRevisionAction(liked: false, serverRev: 12));\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // State should NOT change (push was stale)\n    expect(store.state.liked, true, reason: 'Stale push ignored, local wins');\n    expect(store.state.serverRevision, 15, reason: 'ServerRev unchanged');\n  });\n\n  // ===========================================================================\n  // Case 5: Out-of-order / replay safety\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('Out-of-order pushes are ignored (replay safety).')\n      .given('Client has serverRev=20, liked=true.')\n      .when('Reconnect/replay delivers older pushes.')\n      .then('Older pushes are ignored, only newer applied.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: true, serverRevision: 20));\n\n    // Old pushes arrive (replay from reconnect) - should be IGNORED\n    store.dispatch(SimulatePushWithRevisionAction(liked: false, serverRev: 18));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true, reason: 'serverRev=18 < 20, ignored');\n    expect(store.state.serverRevision, 20);\n\n    store.dispatch(SimulatePushWithRevisionAction(liked: false, serverRev: 19));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true, reason: 'serverRev=19 < 20, ignored');\n    expect(store.state.serverRevision, 20);\n\n    // New push arrives (serverRev=21) - should be APPLIED\n    store.dispatch(SimulatePushWithRevisionAction(liked: false, serverRev: 21));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false, reason: 'serverRev=21 > 20, applied');\n    expect(store.state.serverRevision, 21);\n  });\n\n  // ===========================================================================\n  // Case 6: Stale response is not applied when push is newer\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('Stale server response is not applied to state.')\n      .given('Request is sent.')\n      .when('Push with newer serverRev arrives before response.')\n      .then('Response is ignored (stale), push value preserved.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n\n    // Tap: false -> true, localRev=1\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    // Now the request has completed with serverRev=11\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 11);\n\n    // Push arrives with newer serverRev - should be applied\n    store.dispatch(SimulatePushWithRevisionAction(liked: false, serverRev: 15));\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    expect(store.state.liked, false,\n        reason: 'Push with newer serverRev applied');\n    expect(store.state.serverRevision, 15,\n        reason: 'Push serverRev applied (15 > 11)');\n\n    // Now a stale push arrives (serverRev=12 < 15) - should be IGNORED\n    store.dispatch(SimulatePushWithRevisionAction(liked: true, serverRev: 12));\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    expect(store.state.liked, false, reason: 'Stale push ignored');\n    expect(store.state.serverRevision, 15, reason: 'ServerRev unchanged');\n  });\n\n  // ===========================================================================\n  // Case 7: Throws error if informServerRevision() is not called\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('Throws error if informServerRevision() is not called.')\n      .given('An action that does not call informServerRevision().')\n      .when('sendValueToServer completes successfully.')\n      .then('A StateError is thrown.')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(liked: false));\n\n    expect(\n      () => store.dispatchAndWait(ToggleLikeActionNoRevisions()),\n      throwsA(isA<StateError>().having(\n        (e) => e.message,\n        'message',\n        contains('informServerRevision()'),\n      )),\n    );\n  });\n\n  // ===========================================================================\n  // Case 8: DateTime-based server revision\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('DateTime-based server revision works correctly.')\n      .given('Server uses DateTime for revisions.')\n      .when('Push arrives with DateTime-based serverRev.')\n      .then('Ordering works correctly.')\n      .run((_) async {\n    final oldTime = DateTime(2024, 1, 1, 12, 0, 0);\n    final newTime = DateTime(2024, 1, 1, 12, 0, 1);\n\n    var store = Store<AppState>(\n        initialState: AppState(\n            liked: false, serverRevision: oldTime.millisecondsSinceEpoch));\n\n    // Push with older DateTime - should be ignored\n    store.dispatch(SimulatePushWithRevisionAction(\n      liked: true,\n      serverRev:\n          oldTime.subtract(const Duration(seconds: 1)).millisecondsSinceEpoch,\n    ));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false, reason: 'Older DateTime ignored');\n\n    // Push with newer DateTime - should be applied\n    store.dispatch(SimulatePushWithRevisionAction(\n      liked: true,\n      serverRev: newTime.millisecondsSinceEpoch,\n    ));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true, reason: 'Newer DateTime applied');\n  });\n\n  // ===========================================================================\n  // Case 9: Multiple rapid taps coalesce correctly with revisions\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('Multiple rapid taps coalesce correctly with revisions.')\n      .given('User taps rapidly multiple times.')\n      .when('Requests complete with revisions.')\n      .then('Final state reflects optimistic updates, requests are coalesced.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n    requestDelay = const Duration(milliseconds: 50);\n\n    // Rapid taps: false -> true -> false -> true -> false -> true\n    for (var i = 0; i < 5; i++) {\n      store.dispatch(ToggleLikeActionWithRevisions());\n      await Future.delayed(const Duration(milliseconds: 5));\n    }\n\n    // Optimistic state after 5 toggles from false should be true\n    // (odd number of toggles inverts the initial state)\n    expect(store.state.liked, true,\n        reason: '5 toggles from false ends at true (optimistic)');\n\n    // Wait for all to complete\n    await Future.delayed(const Duration(milliseconds: 500));\n\n    // Should have at least 1 request (coalescing may occur)\n    final sendCount = requestLog.where((s) => s.startsWith('sendValue')).length;\n    expect(sendCount, greaterThanOrEqualTo(1));\n\n    // Verify onFinish was called\n    expect(requestLog.last, 'onFinish()');\n\n    requestDelay = Duration.zero;\n  });\n\n  // ===========================================================================\n  // Case 10: localRevision increments correctly across dispatches\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('localRevision increments correctly across dispatches.')\n      .given('Multiple dispatches occur.')\n      .when('Each dispatch calls localRevision.')\n      .then('First dispatch gets localRev=1, follow-up gets localRev=2.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n    requestDelay = const Duration(milliseconds: 50);\n\n    // Dispatch 1: localRev should be 1\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Dispatch 2 (while 1 is in flight): this will increment localRev to 2\n    // but won't send a request yet (locked)\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Wait for request 1 to complete and follow-up to be sent\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    // Check that first request had localRev=1\n    expect(requestLog[0], contains('localRev=1'),\n        reason: 'First request has localRev=1');\n\n    // If follow-up was sent (because state changed), it should have localRev=2\n    final sendValueLogs =\n        requestLog.where((s) => s.startsWith('sendValue')).toList();\n    if (sendValueLogs.length > 1) {\n      expect(sendValueLogs[1], contains('localRev=2'),\n          reason: 'Follow-up has localRev=2');\n    }\n\n    requestDelay = Duration.zero;\n  });\n\n  // ===========================================================================\n  // Case 11: Push during follow-up is handled correctly\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario('Push during follow-up request is handled correctly.')\n      .given('Request completes and follow-up is being sent.')\n      .when('Push arrives during follow-up.')\n      .then('System remains consistent.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n    requestDelay = const Duration(milliseconds: 50);\n\n    // Tap 1\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Tap 2 (triggers follow-up later)\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Push arrives\n    store.dispatch(SimulatePushWithRevisionAction(liked: true, serverRev: 15));\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    // Wait for everything to settle\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    // System should be in a consistent state\n    expect(store.state.serverRevision, greaterThanOrEqualTo(11));\n    expect(requestLog.last, 'onFinish()');\n\n    requestDelay = Duration.zero;\n  });\n\n  // ===========================================================================\n  // Self-echo push is handled correctly: follow-up still sends latest intent\n  // ===========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Self-echo push is handled correctly: follow-up sends latest intent.')\n      .given('Action with requests in flight and user taps again.')\n      .when('Self-echo push arrives before request completes.')\n      .then('Follow-up sends the latest local intent, not the echoed value.')\n      .note('Self-echo is detected by matching deviceId and stale localRevision.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n\n    // Use completer to precisely control when Request 1 completes\n    final request1Completer = Completer<void>();\n    requestCompleter = request1Completer;\n\n    // Tap #1: liked=false -> liked=true (optimistic), localRev=1\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true, reason: 'Tap #1 optimistic update');\n    expect(requestLog, ['sendValue(true, localRev=1)']);\n\n    // Tap #2 (while request 1 in flight): liked=true -> liked=false (optimistic), localRev=2\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false,\n        reason: 'Tap #2 optimistic update (user wants false)');\n\n    // Self-echo push arrives (echo of Request 1)\n    // Using the same deviceId as the current device and localRevision=1 (stale)\n    // This simulates the server echoing back the first request's value\n    store.dispatch(SimulatePushWithRevisionAction(\n      liked: true,\n      serverRev: 11,\n      pushLocalRevision: 1, // Matches request 1's localRevision\n      pushDeviceId: OptimisticSyncWithPush.deviceId(), // Same device = self-echo\n    ));\n    await Future.delayed(const Duration(milliseconds: 10));\n    // Self-echo with stale localRevision should NOT apply to state\n    expect(store.state.liked, false,\n        reason: 'Self-echo with stale localRev should not apply');\n\n    // Request 1 completes\n    request1Completer.complete();\n    await Future.delayed(const Duration(milliseconds: 50));\n\n    // Follow-up should be sent because localRev advanced and isPush=false\n    expect(requestLog.length, greaterThanOrEqualTo(2),\n        reason: 'Follow-up was sent (revision check passed)');\n\n    // Check what the follow-up sent\n    final followUpLog =\n        requestLog.where((s) => s.startsWith('sendValue')).toList();\n    if (followUpLog.length >= 2) {\n      // Should send user's last intent (false)\n      expect(followUpLog[1], 'sendValue(false, localRev=2)',\n          reason: 'Follow-up should send latest local intent (false)');\n    }\n\n    // Wait for everything to complete\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    // Final state should match user's last tap (false)\n    expect(store.state.liked, false,\n        reason: 'Final state should be false (user\\'s last tap)');\n  });\n\n  // ===========================================================================\n  // Follow-up is based on localRevision, not value comparison\n  // ===========================================================================\n  Bdd(feature)\n      .scenario(\n          'Follow-up is sent based on localRevision even when value matches sent value.')\n      .given('An action with requests in flight.')\n      .when(\n          'User toggles multiple times, ending at the same value that was sent.')\n      .then(\n          'Follow-up IS sent because localRevision advanced (revision-based, not value-based).')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n\n    // Control when Request 1 completes.\n    final request1Completer = Completer<void>();\n    requestCompleter = request1Completer;\n\n    // Tap #1: liked=false -> liked=true, localRev=1\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true, reason: 'Tap #1 optimistic');\n    expect(requestLog, ['sendValue(true, localRev=1)']);\n\n    // Tap #2 (while request 1 in flight): liked=true -> liked=false, localRev=2\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false, reason: 'Tap #2 optimistic');\n\n    // Tap #3 (still while request 1 in flight): liked=false -> liked=true, localRev=3\n    // Final value returns to the SAME VALUE that was sent in Request 1.\n    store.dispatch(ToggleLikeActionWithRevisions());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true, reason: 'Tap #3 back to sent value');\n\n    // Request 1 completes.\n    request1Completer.complete();\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    final sendValueLogs =\n        requestLog.where((s) => s.startsWith('sendValue')).toList();\n\n    // With revision-based tracking, a follow-up IS sent because localRev(3) > sentLocalRev(1),\n    // even though the final value (true) equals the sent value (true).\n    // The follow-up sends true with localRev=3.\n    expect(sendValueLogs.length, greaterThanOrEqualTo(2),\n        reason:\n            'Follow-up is sent because localRevision advanced (revision-based tracking)');\n\n    // The follow-up should send the current value (true) with localRev=3\n    if (sendValueLogs.length >= 2) {\n      expect(sendValueLogs[1], 'sendValue(true, localRev=3)',\n          reason: 'Follow-up sends current value with updated localRevision');\n    }\n  });\n}\n\n// =============================================================================\n// Test state\n// =============================================================================\n\nclass AppState {\n  final bool liked;\n  final Map<String, bool> items;\n  final int serverRevision;\n  final Map<String, int> serverRevisions;\n\n  AppState({\n    required this.liked,\n    this.items = const {},\n    this.serverRevision = 0,\n    this.serverRevisions = const {},\n  });\n\n  AppState copy({\n    bool? liked,\n    Map<String, bool>? items,\n    int? serverRevision,\n    Map<String, int>? serverRevisions,\n  }) =>\n      AppState(\n        liked: liked ?? this.liked,\n        items: items ?? this.items,\n        serverRevision: serverRevision ?? this.serverRevision,\n        serverRevisions: serverRevisions ?? this.serverRevisions,\n      );\n\n  @override\n  String toString() =>\n      'AppState(liked: $liked, serverRev: $serverRevision, items: $items)';\n}\n\n// =============================================================================\n// Test control variables\n// =============================================================================\n\nList<String> requestLog = [];\nCompleter<void>? requestCompleter;\nint nextServerRevision = 1;\nDuration requestDelay = Duration.zero;\n\nvoid resetTestState() {\n  requestLog = [];\n  requestCompleter = null;\n  nextServerRevision = 1;\n  requestDelay = Duration.zero;\n}\n\n// =============================================================================\n// Action that does NOT call informServerRevision() (to test error detection)\n// =============================================================================\n\nclass ToggleLikeActionNoRevisions extends ReduxAction<AppState>\n    with OptimisticSyncWithPush<AppState, bool> {\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(state, bool optimisticValue) =>\n      state.copy(liked: optimisticValue);\n\n  @override\n  AppState? applyServerResponseToState(state, Object serverResponse) =>\n      state.copy(liked: serverResponse as bool);\n\n  @override\n  Future<Object?> sendValueToServer(\n    Object? optimisticValue,\n    int localRevision,\n    int deviceId,\n  ) async {\n    requestLog.add('sendValue($optimisticValue)');\n\n    // Wait for completer if provided\n    if (requestCompleter != null) {\n      await requestCompleter!.future;\n    } else if (requestDelay != Duration.zero) {\n      await Future.delayed(requestDelay);\n    }\n\n    // Intentionally NOT calling informServerRevision() to test error detection\n    return optimisticValue;\n  }\n\n  @override\n  Future<AppState?> onFinish(Object? error) async {\n    requestLog.add('onFinish()');\n    return null;\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) {\n    // Use the simple serverRevision field for consistency\n    return state.serverRevision;\n  }\n}\n\n// =============================================================================\n// Actions WITH revision tracking (the fix)\n// =============================================================================\n\nclass ToggleLikeActionWithRevisions extends ReduxAction<AppState>\n    with OptimisticSyncWithPush<AppState, bool> {\n  int _serverRevFromResponse = 0;\n\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(state, bool optimisticValue) =>\n      state.copy(liked: optimisticValue);\n\n  @override\n  AppState? applyServerResponseToState(state, Object serverResponse) {\n    return state.copy(\n      liked: serverResponse as bool,\n      serverRevision: _serverRevFromResponse,\n    );\n  }\n\n  @override\n  Future<Object?> sendValueToServer(\n    Object? optimisticValue,\n    int localRevision,\n    int deviceId,\n  ) async {\n    // localRevision is now passed as a parameter (the CURRENT revision value).\n    // This may differ from what was captured in valueToApply() when this is a\n    // follow-up request (other dispatches may have incremented the revision).\n    requestLog.add('sendValue($optimisticValue, localRev=$localRevision)');\n\n    // Wait for completer if provided\n    if (requestCompleter != null) {\n      await requestCompleter!.future;\n      requestCompleter = null; // Reset after use\n    } else if (requestDelay != Duration.zero) {\n      await Future.delayed(requestDelay);\n    }\n\n    // Get and increment server revision\n    _serverRevFromResponse = nextServerRevision++;\n    informServerRevision(_serverRevFromResponse);\n\n    return optimisticValue;\n  }\n\n  @override\n  Future<AppState?> onFinish(Object? error) async {\n    requestLog.add('onFinish()');\n    return null;\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) {\n    // Use the simple serverRevision field (same as SimulatePushWithRevisionAction)\n    return state.serverRevision;\n  }\n}\n\n/// Simulates a push update WITH revision tracking using the ServerPush mixin.\n/// Only applies if serverRev > current stored serverRev.\nclass SimulatePushWithRevisionAction extends ReduxAction<AppState>\n    with ServerPush<AppState> {\n  final bool liked;\n  final int serverRev;\n  final int pushLocalRevision;\n  final int pushDeviceId;\n\n  SimulatePushWithRevisionAction({\n    required this.liked,\n    required this.serverRev,\n    this.pushLocalRevision = 0,\n    int? pushDeviceId,\n  }) : pushDeviceId = pushDeviceId ?? -999; // Default to a different deviceId\n\n  @override\n  Type associatedAction() => ToggleLikeActionWithRevisions;\n\n  @override\n  PushMetadata pushMetadata() => (\n        serverRevision: serverRev,\n        localRevision: pushLocalRevision,\n        deviceId: pushDeviceId,\n      );\n\n  @override\n  AppState? applyServerPushToState(\n      AppState state, Object? key, int serverRevision) {\n    return state.copy(liked: liked, serverRevision: serverRevision);\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) {\n    return state.serverRevision;\n  }\n}\n"
  },
  {
    "path": "test/persistence_test.dart",
    "content": "import 'dart:async';\n\nimport \"package:async_redux/async_redux.dart\";\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  //\n  // These tests should probably use a mocked time, but they use the real one.\n  // For this reason it may be necessary to use a multiplier (4 in this case\n  // to account for timing errors.\n  Duration duration(int value) => Duration(milliseconds: value * 4);\n\n  late MyPersistor persistor;\n  late LocalDb localDb;\n\n  Future<void> setupPersistorAndLocalDb({\n    Duration? throttle,\n    Duration? saveDuration,\n  }) async {\n    persistor = MyPersistor(throttle: throttle, saveDuration: saveDuration);\n    await persistor.init();\n    await persistor.deleteState();\n    localDb = persistor.localDb;\n  }\n\n  Future<StoreTester<AppState>> createStoreTester() async {\n    //\n    var initialState = await persistor.readState();\n\n    if (initialState == null) {\n      initialState = AppState.initialState();\n      await persistor.saveInitialState(initialState);\n    }\n\n    var store = Store<AppState>(\n      initialState: initialState,\n      persistor: persistor,\n    );\n\n    return StoreTester.from(store);\n  }\n\n  void printResults(List<Object> results) => print(\"-\\nRESULTS:\\n${results.join(\"\\n\")}\\n-\");\n\n  test('Create some simple state and persist, without throttle.', () async {\n    //\n    await setupPersistorAndLocalDb();\n\n    var storeTester = await createStoreTester();\n    expect(storeTester.state.name, \"John\");\n    expect(await storeTester.store.readStateFromPersistence(), storeTester.state);\n\n    storeTester.dispatch(ChangeNameAction(\"Mary\"));\n    TestInfo<AppState> info1 = await (storeTester.waitAllGetLast([ChangeNameAction]));\n    expect(localDb.get(db: \"main\", id: Id(\"name\")), \"Mary\");\n    expect(await storeTester.store.readStateFromPersistence(), info1.state);\n\n    storeTester.dispatch(ChangeNameAction(\"Steve\"));\n    TestInfo<AppState> info2 = await (storeTester.waitAllGetLast([ChangeNameAction]));\n    expect(localDb.get(db: \"main\", id: Id(\"name\")), \"Steve\");\n    expect(await storeTester.store.readStateFromPersistence(), info2.state);\n  });\n\n  test('Create some simple state and persist, with a 1 second throttle.', () async {\n    //\n    await setupPersistorAndLocalDb(throttle: const Duration(seconds: 1));\n\n    var storeTester = await createStoreTester();\n    expect(storeTester.state.name, \"John\");\n    expect(await storeTester.store.readStateFromPersistence(), storeTester.state);\n\n    // 1) The state is changed, but the persisted AppState is not.\n    storeTester.dispatch(ChangeNameAction(\"Mary\"));\n    TestInfo<AppState?> info1 = await (storeTester.waitAllGetLast([ChangeNameAction]));\n    expect(localDb.get(db: \"main\", id: Id(\"name\")), \"John\");\n    expect(info1.state!.name, \"Mary\");\n    expect(await storeTester.store.readStateFromPersistence(), isNot(info1.state));\n\n    // 2) The state is changed, but the persisted AppState is not.\n    storeTester.dispatch(ChangeNameAction(\"Steve\"));\n    TestInfo<AppState?> info2 = await (storeTester.waitAllGetLast([ChangeNameAction]));\n    expect(localDb.get(db: \"main\", id: Id(\"name\")), \"John\");\n    expect(info2.state!.name, \"Steve\");\n    expect(await storeTester.store.readStateFromPersistence(), isNot(info2.state));\n\n    // 3) The state is changed, but the persisted AppState is not.\n    storeTester.dispatch(ChangeNameAction(\"Eve\"));\n    TestInfo<AppState?> info3 = await (storeTester.waitAllGetLast([ChangeNameAction]));\n    expect(localDb.get(db: \"main\", id: Id(\"name\")), \"John\");\n    expect(info3.state!.name, \"Eve\");\n    expect(await storeTester.store.readStateFromPersistence(), isNot(info3.state));\n\n    // 4) Now lets wait until the save is done.\n    await Future.delayed(duration(1500));\n    expect(localDb.get(db: \"main\", id: Id(\"name\")), \"Eve\");\n    expect(await storeTester.store.readStateFromPersistence(), storeTester.state);\n  });\n\n  test(\n      \"There is no throttle. \"\n      \"The state is changed each 40 milliseconds. \"\n      \"Here we test that the initial state is persisted, \"\n      \"and then that the state and the persistence change together.\", () async {\n    //\n    List<String> results = [];\n\n    await setupPersistorAndLocalDb(throttle: null);\n    var storeTester = await createStoreTester();\n\n    String result = writeStateAndDb(storeTester, localDb);\n    results.add(result);\n\n    int count = 0;\n    Completer completer = Completer();\n\n    Timer.periodic(duration(40), (timer) {\n      storeTester.dispatch(ChangeNameAction(count.toString()));\n      String result = writeStateAndDb(storeTester, localDb);\n      results.add(result);\n      count++;\n      if (count == 8) {\n        timer.cancel();\n        completer.complete();\n      }\n    });\n\n    await completer.future;\n\n    printResults(results);\n\n    expect(\n        results.join(),\n        \"(state:John, db: John)\"\n        \"(state:0, db: 0)\"\n        \"(state:1, db: 1)\"\n        \"(state:2, db: 2)\"\n        \"(state:3, db: 3)\"\n        \"(state:4, db: 4)\"\n        \"(state:5, db: 5)\"\n        \"(state:6, db: 6)\"\n        \"(state:7, db: 7)\");\n  });\n\n  test(\n      \"Pausing then resuming: \"\n      \"There is no throttle. \"\n      \"The state is changed each 40 milliseconds. \"\n      \"We pause the persistor at the 3rd change, and resume it at the 6th. \"\n      \"Here we test that the initial state is persisted, \"\n      \"and then that the state and the persistence change together.\", () async {\n    //\n    List<String> results = [];\n\n    await setupPersistorAndLocalDb(throttle: null);\n    var storeTester = await createStoreTester();\n\n    String result = writeStateAndDb(storeTester, localDb);\n    results.add(result);\n\n    int count = 0;\n    Completer completer = Completer();\n\n    Timer.periodic(duration(40), (timer) {\n      storeTester.dispatch(ChangeNameAction(count.toString()));\n      String result = writeStateAndDb(storeTester, localDb);\n      results.add(result);\n      count++;\n      if (count == 8) {\n        timer.cancel();\n        completer.complete();\n      }\n\n      if (count == 3) storeTester.store.pausePersistor();\n      if (count == 6) storeTester.store.resumePersistor();\n    });\n\n    await completer.future;\n\n    printResults(results);\n\n    expect(\n        results.join(),\n        \"(state:John, db: John)\"\n        \"(state:0, db: 0)\"\n        \"(state:1, db: 1)\"\n        \"(state:2, db: 2)\" // PAUSE here.\n        \"(state:3, db: 2)\"\n        \"(state:4, db: 2)\"\n        \"(state:5, db: 2)\" // RESUME here.\n        \"(state:6, db: 6)\"\n        \"(state:7, db: 7)\");\n  });\n\n  //\n\n  test(\n      \"The throttle period is 215 milliseconds. \"\n      \"The state is changed each 60 milliseconds (at 0, 60, 120, 180, 240 etc). \"\n      \"Here we test that the initial state is persisted, \"\n      \"and then that the state and the persistence occur when they should.\", () async {\n    //\n    List<String> results = [];\n\n    await setupPersistorAndLocalDb(throttle: duration(215));\n    var storeTester = await createStoreTester();\n\n    String result = writeStateAndDb(storeTester, localDb);\n    results.add(result);\n\n    int count = 0;\n    Completer completer = Completer();\n\n    Timer.periodic(duration(60), (timer) {\n      storeTester.dispatch(ChangeNameAction(count.toString()));\n      String result = writeStateAndDb(storeTester, localDb);\n      results.add(result);\n      count++;\n      if (count == 15) {\n        timer.cancel();\n        completer.complete();\n      }\n    });\n\n    await completer.future;\n\n    printResults(results);\n\n    expect(\n        results.join(),\n        \"(state:John, db: John)\" // It starts with state and db in the initial state: John.\n        \"(state:0, db: John)\" // Changed state in 60 millis.\n        \"(state:1, db: John)\" // Changed state in 120 millis.\n        \"(state:2, db: John)\" // Changed state in 180 millis.\n        \"(state:3, db: 2)\" // Changed state in 240 millis. Saved db em 215 millis.\n        \"(state:4, db: 2)\" // Changed state in 300 millis.\n        \"(state:5, db: 2)\" // Changed state in 360 millis.\n        \"(state:6, db: 2)\" // Changed state in 420 millis.\n        \"(state:7, db: 6)\" // Changed state in 480 millis. Saved db em 430 millis.\n        \"(state:8, db: 6)\" // Changed state in 540 millis.\n        \"(state:9, db: 6)\" // Changed state in 600 millis.\n        \"(state:10, db: 9)\" // Changed state in 660 millis. Saved db em 645 millis.\n        \"(state:11, db: 9)\" // Changed state in 720 millis.\n        \"(state:12, db: 9)\" // Changed state in 780 millis.\n        \"(state:13, db: 9)\" // Changed state in 840 millis.\n        \"(state:14, db: 13)\"); // Changed state in 900 millis. Saved db em 860 millis.\n  });\n\n  test(\n    \"Pausing then resuming: \"\n    \"The throttle period is 215 milliseconds. \"\n    \"The state is changed each 60 milliseconds (at 0, 60, 120, 180, 240 etc). \"\n    \"We pause the persistor at the 5th change, and resume it at the 12th. \"\n    \"Here we test that the initial state is persisted, \"\n    \"and then that the state and the persistence occur when they should.\",\n    () async {\n      //\n      List<String> results = [];\n\n      await setupPersistorAndLocalDb(throttle: duration(215));\n      var storeTester = await createStoreTester();\n\n      String result = writeStateAndDb(storeTester, localDb);\n      results.add(result);\n\n      int count = 0;\n      Completer completer = Completer();\n\n      Timer.periodic(duration(60), (timer) {\n        storeTester.dispatch(ChangeNameAction(count.toString()));\n        String result = writeStateAndDb(storeTester, localDb);\n        results.add(result);\n        count++;\n        if (count == 15) {\n          timer.cancel();\n          completer.complete();\n        }\n\n        if (count == 5) storeTester.store.pausePersistor();\n        if (count == 12) storeTester.store.resumePersistor();\n      });\n\n      await completer.future;\n\n      printResults(results);\n\n      expect(\n          results.join('\\n'),\n          \"(state:John, db: John)\\n\" // It starts with state and db in the initial state: John.\n          \"(state:0, db: John)\\n\" // Changed state in 60 millis.\n          \"(state:1, db: John)\\n\" // Changed state in 120 millis.\n          \"(state:2, db: John)\\n\" // Changed state in 180 millis.\n          \"(state:3, db: 2)\\n\" // Changed state in 240 millis. Saved db em 215 millis.\n          \"(state:4, db: 4)\\n\" // Changed state in 300 millis. PERSIST AND PAUSE here.\n          \"(state:5, db: 4)\\n\" // Changed state in 360 millis.\n          \"(state:6, db: 4)\\n\" // Changed state in 420 millis.\n          \"(state:7, db: 4)\\n\" // Changed state in 480 millis. Saved db em 430 millis.\n          \"(state:8, db: 4)\\n\" // Changed state in 540 millis.\n          \"(state:9, db: 4)\\n\" // Changed state in 600 millis.\n          \"(state:10, db: 4)\\n\" // Changed state in 660 millis. Saved db em 645 millis.\n          \"(state:11, db: 4)\\n\" // Changed state in 720 millis. RESUME here.\n          \"(state:12, db: 11)\\n\" // Changed state in 780 millis.\n          \"(state:13, db: 11)\\n\" // Changed state in 840 millis.\n          \"(state:14, db: 11)\\n\"); // Changed state in 900 millis. Saved db em 860 millis.\n    },\n    skip: 'Requires precise timing',\n  );\n\n  test(\n    \"Persisting and pausing, then resuming: \"\n    \"The throttle period is 215 milliseconds. \"\n    \"The state is changed each 60 milliseconds (at 0, 60, 120, 180, 240 etc). \"\n    \"We pause the persistor at the 5th change, and resume it at the 12th. \"\n    \"Here we test that the initial state is persisted, \"\n    \"and then that the state and the persistence occur when they should.\",\n    () async {\n      //\n      List<String> results = [];\n\n      await setupPersistorAndLocalDb(throttle: duration(215));\n      var storeTester = await createStoreTester();\n\n      String result = writeStateAndDb(storeTester, localDb);\n      results.add(result);\n\n      int count = 0;\n      Completer completer = Completer();\n\n      Timer.periodic(duration(60), (timer) {\n        storeTester.dispatch(ChangeNameAction(count.toString()));\n        String result = writeStateAndDb(storeTester, localDb);\n        results.add(result);\n        count++;\n        if (count == 15) {\n          timer.cancel();\n          completer.complete();\n        }\n\n        if (count == 5) storeTester.store.persistAndPausePersistor();\n        if (count == 12) storeTester.store.resumePersistor();\n      });\n\n      await completer.future;\n\n      printResults(results);\n\n      // Expected: ... te:5, db: 2)(state:6 ...\n      // Actual: ... te:5, db: 4)(state:6 ...\n\n      expect(\n          results.join('\\n'),\n          \"(state:John, db: John)\\n\" // It starts with state and db in the initial state: John.\n          \"(state:0, db: John)\\n\" // Changed state in 60 millis.\n          \"(state:1, db: John)\\n\" // Changed state in 120 millis.\n          \"(state:2, db: John)\\n\" // Changed state in 180 millis.\n          \"(state:3, db: 2)\\n\" // Changed state in 240 millis. Saved db em 215 millis.\n          \"(state:4, db: 2)\\n\" // Changed state in 300 millis. PAUSE here.\n          \"(state:5, db: 2)\\n\" // Changed state in 360 millis.\n          \"(state:6, db: 2)\\n\" // Changed state in 420 millis.\n          \"(state:7, db: 2)\\n\" // Changed state in 480 millis. Saved db em 430 millis.\n          \"(state:8, db: 2)\\n\" // Changed state in 540 millis.\n          \"(state:9, db: 2)\\n\" // Changed state in 600 millis.\n          \"(state:10, db: 2)\\n\" // Changed state in 660 millis. Saved db em 645 millis.\n          \"(state:11, db: 2)\\n\" // Changed state in 720 millis. RESUME here.\n          \"(state:12, db: 11)\\n\" // Changed state in 780 millis.\n          \"(state:13, db: 11)\\n\" // Changed state in 840 millis.\n          \"(state:14, db: 11)\\n\"); // Changed state in 900 millis. Saved db em 860 millis.\n    },\n    skip: 'Requires precise timing',\n  );\n\n  test(\n      \"There is no throttle. \"\n      \"Each save takes 430 milliseconds. \"\n      \"The state is changed each 120 milliseconds. \"\n      \"Here we test that the initial state is persisted, \"\n      \"and then that the state and the persistence occur when they should.\", () async {\n    //\n    List<String> results = [];\n\n    await setupPersistorAndLocalDb(\n      throttle: null,\n      saveDuration: duration(430),\n    );\n\n    var storeTester = await createStoreTester();\n\n    String result = writeStateAndDb(storeTester, localDb);\n    results.add(result);\n\n    int count = 0;\n    Completer completer = Completer();\n\n    Timer.periodic(duration(120), (timer) {\n      storeTester.dispatch(ChangeNameAction(count.toString()));\n\n      String result = writeStateAndDb(storeTester, localDb);\n      results.add(result);\n\n      count++;\n      if (count == 16) {\n        timer.cancel();\n        completer.complete();\n      }\n    });\n\n    await completer.future;\n\n    printResults(results);\n\n    expect(\n        results.join(),\n        \"(state:John, db: John)\" // It starts with state and db in the initial state: John.\n        \"(state:0, db: John)\" // Changed the state in 120 millis. Started saving state 0 (will finish: 120+430=550 millis).\n        \"(state:1, db: John)\" // Changed the state in 240 millis.\n        \"(state:2, db: John)\" // Changed state in 360 millis.\n        \"(state:3, db: John)\" // Changed state in 480 millis. Started saving state 3 in 275 millis (will finish: 550+430=980 millis).\n        \"(state:4, db: 0)\" // Changed state in 600 millis.\n        \"(state:5, db: 0)\" // Changed state in 720 millis.\n        \"(state:6, db: 0)\" // Changed state in 840 millis.\n        \"(state:7, db: 0)\" // Changed state in 960 millis. Started saving state 7 in 490 millis (will finish: 980+430=1410 millis).\n        \"(state:8, db: 3)\" // Changed state in 1080 millis.\n        \"(state:9, db: 3)\" // Changed state in 1200 millis.\n        \"(state:10, db: 3)\" // Changed state in 1320 millis. Started saving state 10 in 705 millis (will finish: 1410+430=1840 millis).\n        \"(state:11, db: 7)\" // Changed state in 1440 millis.\n        \"(state:12, db: 7)\" // Changed state in 1560 millis.\n        \"(state:13, db: 7)\" // Changed state in 1680 millis.\n        \"(state:14, db: 7)\" // Changed state in 1800 millis.\n        \"(state:15, db: 10)\"); // Changed state in 1920 millis. Started saving state 15 in 920 millis (will finish: 1840+430 millis).\n  });\n\n  test(\n      \"Pausing then resuming: \"\n      \"There is no throttle. \"\n      \"Each save takes 430 milliseconds. \"\n      \"The state is changed each 120 milliseconds. \"\n      \"We pause the persistor at 600 millis (state: 4), and resume it at 1440 millis (state: 11). \"\n      \"Here we test that the initial state is persisted, \"\n      \"and then that the state and the persistence occur when they should.\", () async {\n    //\n    List<String> results = [];\n\n    await setupPersistorAndLocalDb(\n      throttle: null,\n      saveDuration: duration(430),\n    );\n\n    var storeTester = await createStoreTester();\n\n    String result = writeStateAndDb(storeTester, localDb);\n    results.add(result);\n\n    int count = 0;\n    Completer completer = Completer();\n\n    Timer.periodic(duration(120), (timer) {\n      storeTester.dispatch(ChangeNameAction(count.toString()));\n\n      String result = writeStateAndDb(storeTester, localDb);\n      results.add(result);\n\n      count++;\n      if (count == 16) {\n        timer.cancel();\n        completer.complete();\n      }\n\n      if (count == 5) storeTester.store.pausePersistor();\n      if (count == 12) storeTester.store.resumePersistor();\n    });\n\n    await completer.future;\n\n    printResults(results);\n\n    expect(\n        results.join(),\n        \"(state:John, db: John)\" // It starts with state and db in the initial state: John.\n        \"(state:0, db: John)\" // Changed the state in 120 millis. Started saving state 0 (will finish: 120+430=550 millis).\n        \"(state:1, db: John)\" // Changed the state in 240 millis.\n        \"(state:2, db: John)\" // Changed state in 360 millis.\n        \"(state:3, db: John)\" // Changed state in 480 millis. Started saving state 3 in 275 millis (will finish: 550+430=980 millis).\n        \"(state:4, db: 0)\" // Changed state in 600 millis. PAUSED here.\n        \"(state:5, db: 0)\" // Changed state in 720 millis.\n        \"(state:6, db: 0)\" // Changed state in 840 millis.\n        \"(state:7, db: 0)\" // Changed state in 960 millis. Does NOT save, because it's paused.\n        \"(state:8, db: 3)\" // Changed state in 1080 millis. Changed to 3, because previous save finished.\n        \"(state:9, db: 3)\" // Changed state in 1200 millis.\n        \"(state:10, db: 3)\" // Changed state in 1320 millis. Started saving state 10 in 705 millis (will finish: 1410+430=1840 millis).\n        \"(state:11, db: 3)\" // Changed state in 1440 millis. RESUMED here. Will start saving 11.\n        \"(state:12, db: 3)\" // Changed state in 1560 millis.\n        \"(state:13, db: 3)\" // Changed state in 1680 millis.\n        \"(state:14, db: 3)\" // Changed state in 1800 millis.\n        \"(state:15, db: 11)\"); // Changed state in 1920 millis. Changed to 11, because previous save finished.\n  });\n\n  test(\n      \"There is a 300 millis throttle. \"\n      \"A first state change happens. A save starts immediately.\"\n      \"A second state change happens 100 millis after the first. \"\n      \"No other state changes happen. \"\n      \"A second save will happen at 300 millis. \"\n      \"This second save is necessary to save the second state change.\", () async {\n    //\n    List<String> results = [];\n\n    await setupPersistorAndLocalDb(\n      throttle: duration(300),\n      saveDuration: null,\n    );\n\n    var storeTester = await createStoreTester();\n\n    /// Discard the time waiting for the saving of the initial state.\n    await Future.delayed(duration(300));\n\n    // At 0 millis: (state:John, db: John)\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 0 millis the state is changed and saved: (state:1st, db: 1st)\n    storeTester.dispatch(ChangeNameAction(\"1st\"));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 100 millis the state is initially unchanged (state:1st, db: 1st)\n    await Future.delayed(duration(100));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 100 millis the state is changed and saved: (state:2nd, db: 1st)\n    storeTester.dispatch(ChangeNameAction(\"2nd\"));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 200 millis the state is unchanged: (state:2nd, db: 1st)\n    await Future.delayed(duration(100));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // Right before 300 millis the state is unchanged: (state:2nd, db: 1st)\n    await Future.delayed(duration(80));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // Right after 300 millis the state is saved: (state:2nd, db: 2nd)\n    await Future.delayed(duration(40));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    printResults(results);\n\n    expect(\n        results.join(),\n        \"(state:John, db: John)\"\n        \"(state:1st, db: 1st)\"\n        \"(state:1st, db: 1st)\"\n        \"(state:2nd, db: 1st)\"\n        \"(state:2nd, db: 1st)\"\n        \"(state:2nd, db: 1st)\"\n        \"(state:2nd, db: 2nd)\");\n  });\n\n  test(\n      \"There is a 300 save duration, and no throttle. \"\n      \"A first state change happens. A save starts immediately.\"\n      \"A second state change happens 100 millis after the first. \"\n      \"No other state changes happen. \"\n      \"A second save will happen at 300 millis. \"\n      \"This second save is necessary to save the second state change.\", () async {\n    //\n    List<String> results = [];\n\n    await setupPersistorAndLocalDb(\n      throttle: null,\n      saveDuration: duration(300),\n    );\n\n    var storeTester = await createStoreTester();\n\n    /// Discard the time waiting for the saving of the initial state.\n    await Future.delayed(duration(300));\n\n    // At 0 millis: (state:John, db: John)\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 0 millis the state is and the save starts: (state:1st, db: John)\n    storeTester.dispatch(ChangeNameAction(\"1st\"));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 100 millis the state is initially unchanged (state:1st, db: John)\n    await Future.delayed(duration(100));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 100 millis the state is changed, but the previous save hasn't finished: (state:2nd, db: John)\n    storeTester.dispatch(ChangeNameAction(\"2nd\"));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 200 millis the state is unchanged: (state:2nd, db: John)\n    await Future.delayed(duration(100));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // Right before 300 millis the state is unchanged: (state:2nd, db: John)\n    await Future.delayed(duration(80));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // Right after 300 millis the 1st state is saved: (state:2nd, db: 1st)\n    await Future.delayed(duration(40));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // It will take 300 millis more (until 600) to save the 2nd state.\n    // So, at 580 millis we're still at (state:2nd, db: 1st)\n    await Future.delayed(duration(260));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 620 we're finally finished: (state:2nd, db: 2nd)\n    await Future.delayed(duration(40));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    printResults(results);\n\n    expect(\n        results.join(),\n        \"(state:John, db: John)\"\n        \"(state:1st, db: John)\"\n        \"(state:1st, db: John)\"\n        \"(state:2nd, db: John)\"\n        \"(state:2nd, db: John)\"\n        \"(state:2nd, db: John)\"\n        \"(state:2nd, db: 1st)\"\n        \"(state:2nd, db: 1st)\"\n        \"(state:2nd, db: 2nd)\");\n  });\n\n  test(\n      \"There is throttle period of 300 millis. \"\n      \"A first state change happens. A save starts immediately. \"\n      \"A second state change happens 100 millis after the first. \"\n      \"However at 150 a PersistAction is dispatched. \"\n      \"And this saves the second state change right away.\", () async {\n    //\n    List<String> results = [];\n\n    await setupPersistorAndLocalDb(\n      throttle: duration(300),\n      saveDuration: null,\n    );\n\n    var storeTester = await createStoreTester();\n\n    /// Discard the throttle period for the saving of the initial state.\n    await Future.delayed(duration(300));\n\n    // At 0 millis: (state:John, db: John)\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 0 millis the state is changed and saved: (state:1st, db: 1st)\n    storeTester.dispatch(ChangeNameAction(\"1st\"));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 100 millis the state is initially unchanged (state:1st, db: 1st)\n    await Future.delayed(duration(100));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 100 millis the state is changed and saved: (state:2nd, db: 1st)\n    storeTester.dispatch(ChangeNameAction(\"2nd\"));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 150 millis the state is initially unchanged (state:2nd, db: 1st)\n    await Future.delayed(duration(50));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 150 millis the PersistAction is dispatched. The state is changed: (state:2nd, db: 2nd)\n    storeTester.dispatch(PersistAction());\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    // At 400 millis the state is unchanged (state:2nd, db: 2nd)\n    await Future.delayed(duration(150));\n    results.add(writeStateAndDb(storeTester, localDb));\n\n    printResults(results);\n\n    expect(\n        results.join(),\n        \"(state:John, db: John)\"\n        \"(state:1st, db: 1st)\"\n        \"(state:1st, db: 1st)\"\n        \"(state:2nd, db: 1st)\"\n        \"(state:2nd, db: 1st)\"\n        \"(state:2nd, db: 2nd)\"\n        \"(state:2nd, db: 2nd)\");\n  });\n\n  test('Test the persistor in the store holds the correct state.', () async {\n    //\n    await setupPersistorAndLocalDb();\n\n    var initialState = AppState.initialState();\n\n    var store = Store<AppState>(\n      initialState: initialState,\n      persistor: persistor,\n    );\n\n    // When the store is created with a Persistor, the store considers that the\n    // provided initial-state was already persisted. You have to make sure this is the case.\n    expect(store.getLastPersistedStateFromPersistor(), AppState.initialState());\n\n    // Which means it doesn't save the initial-state automatically.\n    var persistedState = await persistor.readState();\n    expect(persistedState, isNull);\n\n    var storeTester = StoreTester.from(store);\n\n    storeTester.dispatch(ChangeNameAction(\"Mary\"));\n    TestInfo<AppState> info1 = await (storeTester.waitAllGetLast([ChangeNameAction]));\n    expect(await storeTester.store.readStateFromPersistence(), info1.state);\n    expect(store.getLastPersistedStateFromPersistor(), initialState.copy(name: \"Mary\"));\n\n    /// If we delete it, it will be null.\n    storeTester.store.deleteStateFromPersistence();\n    expect(store.getLastPersistedStateFromPersistor(), isNull);\n  });\n}\n\nString writeStateAndDb(StoreTester<AppState> storeTester, LocalDb localDb) => \"(\"\n    \"state:${storeTester.state.name}, \"\n    \"db: ${localDb.get(db: 'main', id: Id('name'))}\"\n    \")\";\n\n@immutable\nclass AppState {\n  final String? name;\n\n  AppState({\n    this.name,\n  });\n\n  static AppState initialState() {\n    return AppState(name: \"John\");\n  }\n\n  AppState copy({\n    String? name,\n  }) =>\n      AppState(name: name ?? this.name);\n\n  @override\n  String toString() => 'AppState{name: $name}';\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState && runtimeType == other.runtimeType && name == other.name;\n\n  @override\n  int get hashCode => name.hashCode;\n}\n\nclass Id {\n  final String uid;\n\n  Id(this.uid);\n\n  @override\n  String toString() => 'Id{uid: $uid}';\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) || other is Id && runtimeType == other.runtimeType && uid == other.uid;\n\n  @override\n  int get hashCode => uid.hashCode;\n}\n\n/// T must have [isEmpty] method.\nabstract class LocalDb<T> {\n  //\n  Map<String, T> dbs = {};\n\n  Set<String>? dbNames;\n\n  bool get isEmpty => dbs.isEmpty || dbs.values.every((dynamic t) => t.isEmpty);\n\n  bool get isNotEmpty => !isEmpty;\n\n  T getDb(String? name) {\n    T? db = dbs[name!];\n    if (db == null) throw PersistException(\"Database '$name' does not exist.\");\n    return db;\n  }\n\n  /// This method Must be called right after instantiating the object.\n  /// If it's overridden, you must call super in the beginning.\n  Future<void> init(Iterable<String> dbNames) async {\n    assert(dbNames.isNotEmpty);\n    this.dbNames = dbNames.toSet();\n  }\n\n  Future<void> createDatabases();\n\n  Future<void> deleteDatabases();\n\n  Future<void> save({\n    String? db,\n    Id? id,\n    required Object? info,\n  });\n\n  Object? get({\n    String? db,\n    Id? id,\n    Object orElse()?,\n    Object deserializer(Object? obj)?,\n  });\n\n  Object? getOrThrow({\n    String? db,\n    Id? id,\n    Object deserializer(Object? obj)?,\n  });\n}\n\nclass NotFound {\n  const NotFound();\n\n  static const instance = NotFound();\n}\n\nclass SavedInfo {\n  //\n  final Id id;\n  final Object? info;\n\n  SavedInfo(this.id, this.info);\n\n  @override\n  String toString() => identical(this, NotFound.instance)\n      ? \"SavedInfo{Not Found}\"\n      : 'SavedInfo{id: $id, info: $info}';\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is SavedInfo &&\n          runtimeType == other.runtimeType &&\n          id == other.id &&\n          info == other.info;\n\n  @override\n  int get hashCode => id.hashCode ^ info.hashCode;\n}\n\nclass LocalDbInMemory extends LocalDb<List<SavedInfo>> {\n  //\n\n  /// Must be called right after instantiating the object.\n  /// The databases will be created as List<SavedInfo>.\n  @override\n  Future<void> init(Iterable<String> dbNames) async {\n    super.init(dbNames);\n\n    if (dbs.isNotEmpty) throw PersistException(\"Databases not empty.\");\n\n    dbNames.forEach((dbName) {\n      dbs[dbName] = [];\n    });\n  }\n\n  @override\n  Future<void> createDatabases() => throw AssertionError();\n\n  @override\n  Future<void> deleteDatabases() async => dbs.values.forEach((db) => db.clear());\n\n  @override\n  Future<void> save({\n    String? db,\n    Id? id,\n    required Object? info,\n  }) async {\n    assert(db != null);\n    assert(id != null);\n    assert(info != null);\n\n    var savedInfo = SavedInfo(id!, info);\n    List<SavedInfo> dbObj = getDb(db);\n    dbObj.add(savedInfo);\n  }\n\n  /// Searches the LAST change.\n  /// If not found, returns NotFound.instance.\n  /// Will return null if the saved value is null.\n  @override\n  Object? get({\n    String? db,\n    Id? id,\n    Object orElse()?,\n    Object deserializer(Object? obj)?,\n  }) {\n    assert(db != null);\n    assert(id != null);\n\n    List<SavedInfo> dbObj = getDb(db);\n\n    for (int i = dbObj.length - 1; i >= 0; i--) {\n      var savedInfo = dbObj[i];\n      if (savedInfo.id == id)\n        return (deserializer == null) ? savedInfo.info : deserializer(savedInfo.info);\n    }\n    if (orElse != null)\n      return orElse();\n    else\n      return NotFound.instance;\n  }\n\n  /// Searches the LAST change.\n  /// If not found, returns NotFound.instance.\n  /// Will return null if the saved value is null.\n  @override\n  Object? getOrThrow({\n    String? db,\n    Id? id,\n    Object deserializer(Object? obj)?,\n  }) {\n    assert(db != null);\n    assert(id != null);\n\n    var value = get(\n      db: db,\n      id: id,\n      deserializer: deserializer,\n    );\n\n    if (value == NotFound.instance)\n      throw PersistException(\"Can't find: $id in db: $db.\");\n    else\n      return value;\n  }\n}\n\nclass MyPersistor implements Persistor<AppState> {\n  //\n  final Duration? _throttle;\n  final Duration? _saveDuration;\n\n  MyPersistor({\n    Duration? throttle,\n    Duration? saveDuration,\n  })  : _throttle = throttle,\n        _saveDuration = saveDuration;\n\n  @override\n  Duration? get throttle => _throttle;\n\n  Duration? get saveDuration => _saveDuration;\n\n  LocalDb? _localDb;\n\n  LocalDb get localDb => _localDb ??= LocalDbInMemory();\n\n  Future<void> init() async {\n    localDb.init([\"main\", \"students\"]);\n  }\n\n  @override\n  Future<void> saveInitialState(AppState? state) async {\n    if (localDb.isNotEmpty)\n      throw PersistException(\"Store is already persisted.\");\n    else\n      return persistDifference(lastPersistedState: null, newState: state);\n  }\n\n  @override\n  Future<void> persistDifference({\n    AppState? lastPersistedState,\n    required AppState? newState,\n  }) async {\n    assert(newState != null);\n\n    if (saveDuration != null) await Future.delayed(saveDuration!);\n\n    if (lastPersistedState == null || lastPersistedState.name != newState!.name) {\n      await localDb.save(db: \"main\", id: Id(\"name\"), info: newState!.name);\n    }\n  }\n\n  @override\n  Future<AppState?> readState() async {\n    if (localDb.isEmpty)\n      return null;\n    else\n      return AppState(name: localDb.getOrThrow(db: \"main\", id: Id(\"name\")) as String?);\n  }\n\n  @override\n  Future<void> deleteState() async {\n    localDb.deleteDatabases();\n  }\n}\n\nclass ChangeNameAction extends ReduxAction<AppState> {\n  String name;\n\n  ChangeNameAction(this.name);\n\n  @override\n  AppState reduce() => state.copy(name: name);\n}\n\nclass X {\n  int value = 0;\n\n  void printValue(int v) {\n    print('v = $v');\n  }\n}\n"
  },
  {
    "path": "test/polling_mixin_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:fake_async/fake_async.dart';\nimport 'package:flutter_test/flutter_test.dart' hide Retry;\n\nvoid main() {\n  var feature = BddFeature('Polling mixin');\n\n  // ==========================================================================\n  // Case 1: Poll.start runs reduce immediately and starts polling\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.start runs reduce immediately and starts polling')\n      .given('A polling action with Poll.start')\n      .when('The action is dispatched')\n      .then('It should run reduce and schedule periodic timer ticks')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 2);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 3);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 4);\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 2: Poll.start is no-op when polling is already active\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.start is no-op when polling is already active')\n      .given('Polling is already active for an action type')\n      .when('Poll.start is dispatched again')\n      .then('It should do nothing — no reduce, no timer restart')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      // Dispatch start again — should be no-op\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1); // No change\n\n      // Original timer still ticks\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 2);\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 3: Poll.stop cancels the timer and skips reduce\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.stop cancels the timer and skips reduce')\n      .given('Polling is active')\n      .when('Poll.stop is dispatched')\n      .then('The timer should be cancelled and reduce should not run')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1); // Stop did not run reduce\n\n      // Wait for when ticks would have fired\n      fake.elapse(const Duration(milliseconds: 500));\n      expect(store.state.count, 1); // No ticks\n    });\n  });\n\n  // ==========================================================================\n  // Case 4: Poll.stop when not active is a safe no-op\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.stop when not active is a safe no-op')\n      .given('No polling is active')\n      .when('Poll.stop is dispatched')\n      .then('Nothing should happen and no error is thrown')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 0); // No error, no state change\n    });\n  });\n\n  // ==========================================================================\n  // Case 5: Poll.runNowAndRestart runs reduce and restarts the timer\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.runNowAndRestart runs reduce immediately and restarts the timer')\n      .given('Polling is active')\n      .when('Poll.runNowAndRestart is dispatched')\n      .then('Reduce should run and the timer should restart from that moment')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      // Wait 60ms (not enough for a tick at 100ms)\n      fake.elapse(const Duration(milliseconds: 60));\n      expect(store.state.count, 1);\n\n      // Poll.runNowAndRestart runs reduce and restarts timer\n      store.dispatch(SimplePollAction(poll: Poll.runNowAndRestart));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 2);\n\n      // Timer restarted from this moment. 60ms later: no tick yet\n      fake.elapse(const Duration(milliseconds: 60));\n      expect(store.state.count, 2);\n\n      // 100ms after now: tick fires\n      fake.elapse(const Duration(milliseconds: 40));\n      expect(store.state.count, 3);\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 6: Poll.runNowAndRestart when not active behaves like Poll.start\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.runNowAndRestart when not active behaves like Poll.start')\n      .given('No polling is active')\n      .when('Poll.runNowAndRestart is dispatched')\n      .then('Reduce should run and polling should start')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.runNowAndRestart));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 2);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 3);\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 7: Poll.once runs reduce without affecting active timer\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.once runs reduce without affecting the active timer')\n      .given('Polling is active')\n      .when('Poll.once is dispatched')\n      .then('Reduce should run but the timer continues unchanged')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      // Wait 50ms, then dispatch Poll.once\n      fake.elapse(const Duration(milliseconds: 50));\n      store.dispatch(SimplePollAction(poll: Poll.once));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 2); // Reduce ran\n\n      // Original timer still fires at 100ms from start\n      fake.elapse(const Duration(milliseconds: 50));\n      expect(store.state.count, 3); // Timer tick\n\n      // Next tick at 200ms from start\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 4);\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 8: Poll.once without active polling just runs reduce once\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.once without active polling just runs reduce once')\n      .given('No polling is active')\n      .when('Poll.once is dispatched')\n      .then('Reduce runs once and no timer is started')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.once));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      // No timer should fire\n      fake.elapse(const Duration(milliseconds: 500));\n      expect(store.state.count, 1);\n    });\n  });\n\n  // ==========================================================================\n  // Case 9: Timer ticks dispatch createPollingAction\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Timer ticks dispatch the action from createPollingAction')\n      .given('A polling action whose createPollingAction returns a different action type')\n      .when('Timer ticks fire')\n      .then('The action from createPollingAction should be dispatched')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      // ControllerAction increments by 1; its createPollingAction\n      // returns WorkerAction which increments by 10.\n      store.dispatch(ControllerAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1); // Controller ran (+1)\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 11); // Worker ran (+10)\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 21); // Worker ran again (+10)\n\n      store.dispatch(ControllerAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 10: Long-running polling accumulates correct tick count\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Long-running polling accumulates correct number of ticks')\n      .given('Polling is active with 100ms interval')\n      .when('1 second passes')\n      .then('There should be 10 timer ticks plus the initial reduce')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      fake.elapse(const Duration(seconds: 1));\n      expect(store.state.count, 11); // 1 initial + 10 ticks\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 11: Poll.start after Poll.stop restarts polling\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.start after Poll.stop restarts polling')\n      .given('Polling was active and then stopped')\n      .when('Poll.start is dispatched again')\n      .then('Polling should restart from scratch')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 2);\n\n      // Stop\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      fake.elapse(const Duration(milliseconds: 300));\n      expect(store.state.count, 2); // No ticks\n\n      // Restart\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 3); // Reduce ran immediately\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 4); // First tick after restart\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 12: Poll.stop in the middle of ticks prevents further ticks\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.stop in the middle of ticks prevents further ticks')\n      .given('Polling is active and some ticks have fired')\n      .when('Poll.stop is dispatched')\n      .then('No more ticks should fire')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n\n      fake.elapse(const Duration(milliseconds: 250)); // ~2 ticks\n      expect(store.state.count, 3); // 1 initial + 2 ticks\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      final countAtStop = store.state.count;\n\n      fake.elapse(const Duration(seconds: 1));\n      expect(store.state.count, countAtStop); // No more ticks\n    });\n  });\n\n  // ==========================================================================\n  // Case 13: Different action types have independent timers\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Different action types have independent timers')\n      .given('Two different action types with Polling')\n      .when('Both are started and one is stopped')\n      .then('The other should continue polling independently')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(PollActionA(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      store.dispatch(PollActionB(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 2);\n\n      // Both tick\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 4); // +1 from A, +1 from B\n\n      // Stop A only\n      store.dispatch(PollActionA(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      // Only B ticks\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 5); // +1 from B only\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 6); // +1 from B only\n\n      store.dispatch(PollActionB(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 14: pollingKeyParams creates independent timers per param\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('pollingKeyParams creates independent timers per param')\n      .given('A polling action that uses pollingKeyParams')\n      .when('Dispatched with different params')\n      .then('Each param should get its own independent timer')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(ParamPollAction('A', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      store.dispatch(ParamPollAction('B', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 2);\n\n      // Both tick independently\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 4);\n\n      // Stop only \"A\"\n      store.dispatch(ParamPollAction('A', poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      // Only \"B\" ticks\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 5);\n\n      // Start \"A\" again\n      store.dispatch(ParamPollAction('A', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 6);\n\n      // Both tick\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 8);\n\n      store.dispatch(ParamPollAction('A', poll: Poll.stop));\n      store.dispatch(ParamPollAction('B', poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 15: Same pollingKeyParams shares timer\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Same pollingKeyParams shares a timer')\n      .given('Polling is active for a specific param')\n      .when('Poll.start is dispatched with the same param')\n      .then('It should be a no-op')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(ParamPollAction('X', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      // Same param, start again — no-op\n      store.dispatch(ParamPollAction('X', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      // Timer still ticks once\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 2);\n\n      store.dispatch(ParamPollAction('X', poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 16: pollingKeyParams with tuple\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('pollingKeyParams with tuple creates independent timers')\n      .given('Actions using tuple pollingKeyParams')\n      .when('Dispatched with different tuple values')\n      .then('Each tuple gets its own timer')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(TupleParamPollAction('u1', 'w1', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      store.dispatch(TupleParamPollAction('u1', 'w2', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 2);\n\n      // Same (u1, w1) — no-op\n      store.dispatch(TupleParamPollAction('u1', 'w1', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 2);\n\n      // Both tick\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 4);\n\n      store.dispatch(TupleParamPollAction('u1', 'w1', poll: Poll.stop));\n      store.dispatch(TupleParamPollAction('u1', 'w2', poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 17: computePollingKey shares timer across action types\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('computePollingKey shares a timer across action types')\n      .given('Two action types that return the same computePollingKey')\n      .when('The first starts polling and the second tries to start')\n      .then('The second should be a no-op')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SharedKeyActionA(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      // SharedKeyActionB with same key — start is no-op\n      store.dispatch(SharedKeyActionB(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      // Tick from A's createPollingAction\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 2);\n\n      // Stop using B (same shared key)\n      store.dispatch(SharedKeyActionB(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      // No more ticks\n      fake.elapse(const Duration(milliseconds: 300));\n      expect(store.state.count, 2);\n    });\n  });\n\n  // ==========================================================================\n  // Case 18: Poll.runNowAndRestart resets the timer interval\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.runNowAndRestart resets the timer interval')\n      .given('Polling is active and 80ms have passed (out of 100ms interval)')\n      .when('Poll.runNowAndRestart is dispatched')\n      .then('The timer restarts — next tick is 100ms from now, not 20ms')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      // Wait 80ms — almost time for the first tick\n      fake.elapse(const Duration(milliseconds: 80));\n      expect(store.state.count, 1);\n\n      // Poll.runNowAndRestart resets the timer\n      store.dispatch(SimplePollAction(poll: Poll.runNowAndRestart));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 2); // Reduce ran\n\n      // 80ms after now — no tick (timer was reset to 100ms from now)\n      fake.elapse(const Duration(milliseconds: 80));\n      expect(store.state.count, 2);\n\n      // 20ms more = 100ms after now — tick fires\n      fake.elapse(const Duration(milliseconds: 20));\n      expect(store.state.count, 3);\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 19: Rapid start/stop/start cycle\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Rapid start/stop/start cycle works correctly')\n      .given('Polling is started, stopped, and started again quickly')\n      .when('Timer ticks fire')\n      .then('Only the last start should produce ticks')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 2); // Second start ran reduce\n\n      // Only one timer active\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 3); // One tick\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 20: Multiple Poll.runNowAndRestart dispatches restart each time\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Multiple Poll.runNowAndRestart dispatches restart the timer each time')\n      .given('Polling is active')\n      .when('Poll.runNowAndRestart is dispatched repeatedly')\n      .then('Each dispatch runs reduce and restarts the timer')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.runNowAndRestart));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      fake.elapse(const Duration(milliseconds: 50));\n      store.dispatch(SimplePollAction(poll: Poll.runNowAndRestart));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 2);\n\n      fake.elapse(const Duration(milliseconds: 50));\n      store.dispatch(SimplePollAction(poll: Poll.runNowAndRestart));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 3);\n\n      // Timer restarts from last now — tick at +100ms\n      fake.elapse(const Duration(milliseconds: 80));\n      expect(store.state.count, 3);\n\n      fake.elapse(const Duration(milliseconds: 20));\n      expect(store.state.count, 4);\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 21: Default key uses (runtimeType, null)\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Default key is based on (runtimeType, null)')\n      .given('Two instances of the same action type with default pollingKeyParams')\n      .when('One starts and the other tries to start')\n      .then('The second should be a no-op')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      store.dispatch(SimplePollAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1); // No-op\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 22: Option 1 pattern — single action for everything\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Option 1: single action controls and performs polling')\n      .given('A single action type that handles all poll values')\n      .when('Various poll values are used')\n      .then('It should correctly control polling and run work')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      // Start\n      store.dispatch(SingleAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 2); // Tick\n\n      // Run once without affecting timer\n      store.dispatch(SingleAction(poll: Poll.once));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 3);\n\n      // Timer still ticks\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 4);\n\n      // Force refresh + restart\n      store.dispatch(SingleAction(poll: Poll.runNowAndRestart));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 5);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 6);\n\n      // Stop\n      store.dispatch(SingleAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      fake.elapse(const Duration(milliseconds: 300));\n      expect(store.state.count, 6); // No more ticks\n    });\n  });\n\n  // ==========================================================================\n  // Case 23: Option 2 pattern — separate controller and worker\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Option 2: separate controller and worker actions')\n      .given('A controller action that dispatches a worker via createPollingAction')\n      .when('Polling starts')\n      .then('Timer ticks should dispatch the worker action')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      // Controller's reduce increments by 1\n      store.dispatch(ControllerAction(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      // Timer dispatches WorkerAction (+10)\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 11);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 21);\n\n      // Stop via controller\n      store.dispatch(ControllerAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      fake.elapse(const Duration(milliseconds: 300));\n      expect(store.state.count, 21);\n    });\n  });\n\n  // ==========================================================================\n  // Case 24: Clearing internal mixin props cancels all polling timers\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Clearing internal mixin props cancels all polling timers')\n      .given('Multiple pollers are active')\n      .when('The store internal mixin props are cleared')\n      .then('All polling timers should be cancelled')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(PollActionA(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      store.dispatch(PollActionB(poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 2);\n\n      // Clear all mixin props\n      store.internalMixinProps.clear();\n\n      // No more ticks from either\n      fake.elapse(const Duration(milliseconds: 500));\n      expect(store.state.count, 2);\n    });\n  });\n\n  // ==========================================================================\n  // Case 25: Poll.once dispatched many times does not start any timer\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Multiple Poll.once dispatches never start a timer')\n      .given('No polling is active')\n      .when('Poll.once is dispatched multiple times')\n      .then('Each dispatch runs reduce but no timer is ever created')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.once));\n      fake.elapse(Duration.zero);\n      store.dispatch(SimplePollAction(poll: Poll.once));\n      fake.elapse(Duration.zero);\n      store.dispatch(SimplePollAction(poll: Poll.once));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 3);\n\n      // No timer\n      fake.elapse(const Duration(seconds: 1));\n      expect(store.state.count, 3);\n    });\n  });\n\n  // ==========================================================================\n  // Case 26: Poll.runNowAndRestart followed by Poll.stop stops immediately\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Poll.runNowAndRestart followed immediately by Poll.stop stops cleanly')\n      .given('Poll.runNowAndRestart is dispatched')\n      .when('Poll.stop is dispatched immediately after')\n      .then('Reduce ran once from now, then no more ticks')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(SimplePollAction(poll: Poll.runNowAndRestart));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 1);\n\n      store.dispatch(SimplePollAction(poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      fake.elapse(const Duration(milliseconds: 500));\n      expect(store.state.count, 1); // No ticks\n    });\n  });\n\n  // ==========================================================================\n  // Case 27: Poll.start with different pollingKeyParams are all independent\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Stopping one param does not affect other params')\n      .given('Three params are polling independently')\n      .when('One is stopped')\n      .then('The other two continue')\n      .run((_) async {\n    fakeAsync((fake) {\n      var store = Store<AppState>(initialState: AppState(0));\n\n      store.dispatch(ParamPollAction('A', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      store.dispatch(ParamPollAction('B', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      store.dispatch(ParamPollAction('C', poll: Poll.start));\n      fake.elapse(Duration.zero);\n      expect(store.state.count, 3);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 6); // 3 ticks\n\n      // Stop B\n      store.dispatch(ParamPollAction('B', poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 8); // 2 ticks (A and C)\n\n      // Stop A\n      store.dispatch(ParamPollAction('A', poll: Poll.stop));\n      fake.elapse(Duration.zero);\n\n      fake.elapse(const Duration(milliseconds: 100));\n      expect(store.state.count, 9); // 1 tick (C only)\n\n      store.dispatch(ParamPollAction('C', poll: Poll.stop));\n      fake.elapse(Duration.zero);\n    });\n  });\n\n  // ==========================================================================\n  // Case 28: Polling mixin cannot be combined with Retry\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Polling mixin cannot be combined with Retry')\n      .given('An action that combines Polling and Retry mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () => store.dispatch(PollingWithRetryAction(poll: Poll.once)),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The Polling mixin cannot be combined with the Retry mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 29: Polling mixin cannot be combined with UnlimitedRetries\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Polling mixin cannot be combined with UnlimitedRetries')\n      .given('An action that combines Polling and UnlimitedRetries mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () => store.dispatch(PollingWithUnlimitedRetriesAction(poll: Poll.once)),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The Polling mixin cannot be combined with the Retry mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 30: Polling mixin cannot be combined with Debounce\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Polling mixin cannot be combined with Debounce')\n      .given('An action that combines Polling and Debounce mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () => store.dispatch(PollingWithDebounceAction(poll: Poll.once)),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The Polling mixin cannot be combined with the Debounce mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 31: Polling mixin cannot be combined with UnlimitedRetryCheckInternet\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Polling mixin cannot be combined with UnlimitedRetryCheckInternet')\n      .given(\n          'An action that combines Polling and UnlimitedRetryCheckInternet mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () => store.dispatch(\n          PollingWithUnlimitedRetryCheckInternetAction(poll: Poll.once)),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The UnlimitedRetryCheckInternet mixin cannot be combined with the Polling mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 32: Polling mixin cannot be combined with OptimisticCommand\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Polling mixin cannot be combined with OptimisticCommand')\n      .given('An action that combines Polling and OptimisticCommand mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () =>\n          store.dispatch(PollingWithOptimisticCommandAction(poll: Poll.once)),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The OptimisticCommand mixin cannot be combined with the Polling mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 33: Polling mixin cannot be combined with OptimisticSync\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Polling mixin cannot be combined with OptimisticSync')\n      .given('An action that combines Polling and OptimisticSync mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () => store.dispatch(PollingWithOptimisticSyncAction(poll: Poll.once)),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The Polling mixin cannot be combined with the OptimisticSync mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 34: Polling mixin cannot be combined with OptimisticSyncWithPush\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario(\n          'Polling mixin cannot be combined with OptimisticSyncWithPush')\n      .given(\n          'An action that combines Polling and OptimisticSyncWithPush mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () => store.dispatch(\n          PollingWithOptimisticSyncWithPushAction(poll: Poll.once)),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The Polling mixin cannot be combined with the OptimisticSyncWithPush mixin.',\n      )),\n    );\n  });\n\n  // ==========================================================================\n  // Case 35: Polling mixin cannot be combined with ServerPush\n  // ==========================================================================\n\n  Bdd(feature)\n      .scenario('Polling mixin cannot be combined with ServerPush')\n      .given('An action that combines Polling and ServerPush mixins')\n      .when('The action is dispatched')\n      .then('It should throw an AssertionError')\n      .run((_) async {\n    var store = Store<AppState>(initialState: AppState(0));\n\n    expect(\n      () => store.dispatch(PollingWithServerPushAction(poll: Poll.once)),\n      throwsA(isA<AssertionError>().having(\n        (e) => e.message,\n        'message',\n        'The Polling mixin cannot be combined with the ServerPush mixin.',\n      )),\n    );\n  });\n\n  // ---------------------------------------------------------------------------\n}\n\n// =============================================================================\n// Test state\n// =============================================================================\n\nclass AppState {\n  final int count;\n\n  AppState(this.count);\n\n  AppState copy({int? count}) => AppState(count ?? this.count);\n\n  @override\n  String toString() => 'AppState($count)';\n}\n\n// =============================================================================\n// Simple polling action — increments count by 1, 100ms interval\n// =============================================================================\n\nclass SimplePollAction extends ReduxAction<AppState> with Polling {\n  @override\n  final Poll poll;\n\n  SimplePollAction({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      SimplePollAction(poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\n// =============================================================================\n// Two independent action types for testing independent timers\n// =============================================================================\n\nclass PollActionA extends ReduxAction<AppState> with Polling {\n  @override\n  final Poll poll;\n\n  PollActionA({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() => PollActionA(poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\nclass PollActionB extends ReduxAction<AppState> with Polling {\n  @override\n  final Poll poll;\n\n  PollActionB({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() => PollActionB(poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\n// =============================================================================\n// Action with pollingKeyParams\n// =============================================================================\n\nclass ParamPollAction extends ReduxAction<AppState> with Polling {\n  final String param;\n  @override\n  final Poll poll;\n\n  ParamPollAction(this.param, {required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  Object? pollingKeyParams() => param;\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      ParamPollAction(param, poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\n// =============================================================================\n// Action with tuple pollingKeyParams\n// =============================================================================\n\nclass TupleParamPollAction extends ReduxAction<AppState> with Polling {\n  final String userId;\n  final String walletId;\n  @override\n  final Poll poll;\n\n  TupleParamPollAction(this.userId, this.walletId, {required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  Object? pollingKeyParams() => (userId, walletId);\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      TupleParamPollAction(userId, walletId, poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\n// =============================================================================\n// Shared key across action types\n// =============================================================================\n\nclass SharedKeyActionA extends ReduxAction<AppState> with Polling {\n  @override\n  final Poll poll;\n\n  SharedKeyActionA({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  Object computePollingKey() => 'shared-timer';\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      SharedKeyActionA(poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\nclass SharedKeyActionB extends ReduxAction<AppState> with Polling {\n  @override\n  final Poll poll;\n\n  SharedKeyActionB({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  Object computePollingKey() => 'shared-timer';\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      SharedKeyActionB(poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\n// =============================================================================\n// Option 1: Single action pattern\n// =============================================================================\n\nclass SingleAction extends ReduxAction<AppState> with Polling {\n  @override\n  final Poll poll;\n\n  SingleAction({this.poll = Poll.once});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() => SingleAction(poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\n// =============================================================================\n// Option 2: Controller + Worker pattern\n// =============================================================================\n\nclass ControllerAction extends ReduxAction<AppState> with Polling {\n  @override\n  final Poll poll;\n\n  ControllerAction({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() => WorkerAction();\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\nclass WorkerAction extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => state.copy(count: state.count + 10);\n}\n\n// =============================================================================\n// Incompatible mixin combinations\n// =============================================================================\n\n// Action that combines Polling with Retry (incompatible)\nclass PollingWithRetryAction extends ReduxAction<AppState>\n    with\n        Retry,\n        // ignore: private_collision_in_mixin_application\n        Polling {\n  @override\n  final Poll poll;\n\n  PollingWithRetryAction({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      PollingWithRetryAction(poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\n// Action that combines Polling with UnlimitedRetries (incompatible)\nclass PollingWithUnlimitedRetriesAction extends ReduxAction<AppState>\n    with\n        Retry<AppState>,\n        UnlimitedRetries,\n        // ignore: private_collision_in_mixin_application\n        Polling {\n  @override\n  final Poll poll;\n\n  PollingWithUnlimitedRetriesAction({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      PollingWithUnlimitedRetriesAction(poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\n// Action that combines Polling with Debounce (incompatible)\nclass PollingWithDebounceAction extends ReduxAction<AppState>\n    with\n        Debounce,\n        // ignore: private_collision_in_mixin_application\n        Polling {\n  @override\n  final Poll poll;\n\n  PollingWithDebounceAction({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      PollingWithDebounceAction(poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\n// Action that combines Polling with UnlimitedRetryCheckInternet (incompatible)\nclass PollingWithUnlimitedRetryCheckInternetAction\n    extends ReduxAction<AppState>\n    with\n        UnlimitedRetryCheckInternet,\n        // ignore: private_collision_in_mixin_application\n        Polling {\n  @override\n  final Poll poll;\n\n  PollingWithUnlimitedRetryCheckInternetAction({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      PollingWithUnlimitedRetryCheckInternetAction(poll: Poll.once);\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n\n// Action that combines Polling with OptimisticCommand (incompatible)\nclass PollingWithOptimisticCommandAction extends ReduxAction<AppState>\n    with\n        OptimisticCommand,\n        // ignore: private_collision_in_mixin_application\n        Polling {\n  @override\n  final Poll poll;\n\n  PollingWithOptimisticCommandAction({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      PollingWithOptimisticCommandAction(poll: Poll.once);\n\n  @override\n  Object? optimisticValue() => null;\n\n  @override\n  AppState applyValueToState(AppState state, Object? value) => state;\n\n  @override\n  Object? getValueFromState(AppState state) => null;\n\n  @override\n  Future<Object?> sendCommandToServer(Object? optimisticValue) async => null;\n\n  @override\n  Future<AppState?> reduce() async => state.copy(count: state.count + 1);\n}\n\n// Action that combines Polling with OptimisticSync (incompatible)\nclass PollingWithOptimisticSyncAction extends ReduxAction<AppState>\n    with\n        OptimisticSync<AppState, int>,\n        // ignore: private_collision_in_mixin_application\n        Polling {\n  @override\n  final Poll poll;\n\n  PollingWithOptimisticSyncAction({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      PollingWithOptimisticSyncAction(poll: Poll.once);\n\n  @override\n  int valueToApply() => 0;\n\n  @override\n  AppState applyOptimisticValueToState(AppState state, int optimisticValue) =>\n      state;\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) =>\n      null;\n\n  @override\n  int getValueFromState(AppState state) => state.count;\n\n  @override\n  Future<Object?> sendValueToServer(Object? optimisticValue) async => null;\n\n  @override\n  Future<AppState?> reduce() async => state.copy(count: state.count + 1);\n}\n\n// Action that combines Polling with OptimisticSyncWithPush (incompatible)\nclass PollingWithOptimisticSyncWithPushAction extends ReduxAction<AppState>\n    with\n        OptimisticSyncWithPush<AppState, int>,\n        // ignore: private_collision_in_mixin_application\n        Polling {\n  @override\n  final Poll poll;\n\n  PollingWithOptimisticSyncWithPushAction({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      PollingWithOptimisticSyncWithPushAction(poll: Poll.once);\n\n  @override\n  int valueToApply() => 0;\n\n  @override\n  AppState applyOptimisticValueToState(AppState state, int optimisticValue) =>\n      state;\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) =>\n      null;\n\n  @override\n  int getValueFromState(AppState state) => state.count;\n\n  @override\n  int getServerRevisionFromState(Object? key) => -1;\n\n  @override\n  Future<Object?> sendValueToServer(\n    Object? optimisticValue,\n    int localRevision,\n    int deviceId,\n  ) async =>\n      null;\n\n  @override\n  Future<AppState?> reduce() async => state.copy(count: state.count + 1);\n}\n\n// Action that combines Polling with ServerPush (incompatible)\nclass PollingWithServerPushAction extends ReduxAction<AppState>\n    with\n        ServerPush,\n        // ignore: private_collision_in_mixin_application\n        Polling {\n  @override\n  final Poll poll;\n\n  PollingWithServerPushAction({required this.poll});\n\n  @override\n  Duration get pollInterval => const Duration(milliseconds: 100);\n\n  @override\n  ReduxAction<AppState> createPollingAction() =>\n      PollingWithServerPushAction(poll: Poll.once);\n\n  @override\n  Type associatedAction() => SimplePollAction;\n\n  @override\n  PushMetadata pushMetadata() =>\n      (serverRevision: 1, localRevision: 1, deviceId: 1);\n\n  @override\n  AppState? applyServerPushToState(\n          AppState state, Object? key, int serverRevision) =>\n      null;\n\n  @override\n  int getServerRevisionFromState(Object? key) => -1;\n\n  @override\n  AppState reduce() => state.copy(count: state.count + 1);\n}\n"
  },
  {
    "path": "test/props_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Add and dispose store properties', () {\n    //\n\n    // If you don't provide a predicate function, all properties which are `Timer`, `Future`, or\n    // `Stream` related will be closed/cancelled/ignored as appropriate, and then removed from the\n    // props. Other properties will not be removed.\n    test('Predicate not provided', () async {\n      final store = Store<int>(initialState: 1);\n\n      // Future prop\n      final future = Future.delayed(const Duration(seconds: 1), () => 'foo');\n      store.setProp('future', future);\n\n      // Timer prop\n      final timer = ManagedTimer(const Duration(milliseconds: 100), () => 'foo');\n      store.setProp('timer', timer);\n\n      // Stream prop\n      final sub = Stream.periodic(const Duration(milliseconds: 10), (i) => i).listen((event) {});\n      store.setProp('subscription', sub);\n\n      // Regular prop\n      store.setProp('value', 'bar');\n\n      expect(timer.isCancelled, isFalse);\n      expect(store.props, hasLength(4));\n\n      // Should dispose/cancel the `future` and `subscription` props\n      // but keep the `value` prop.\n      store.disposeProps();\n\n      expect(timer.isCancelled, isTrue);\n      expect(store.props, hasLength(1));\n      expect(store.props.containsKey('value'), isTrue);\n    });\n\n    test('Predicate provided, does not remove Future/Timer/Stream', () async {\n      final store = Store<int>(initialState: 1);\n\n      // Future prop\n      store.setProp('future', Future.delayed(const Duration(seconds: 1), () => 'foo'));\n\n      // Timer prop\n      final timer = ManagedTimer(const Duration(milliseconds: 100), () => 'foo');\n      store.setProp('timer', timer);\n\n      // Stream prop\n      final sub = Stream.periodic(const Duration(milliseconds: 10), (i) => i).listen((event) {});\n      store.setProp('subscription', sub);\n\n      // Regular prop\n      store.setProp('value', 'bar');\n\n      expect(timer.isCancelled, isFalse);\n      expect(store.props, hasLength(4));\n\n      // Predicate: Only remove the regular prop.\n      store.disposeProps(({key, value}) => key == 'value');\n\n      // Does NOT close the Timer.\n      expect(timer.isCancelled, isFalse);\n\n      // Does NOT close the Future/Timer/Stream.\n      // Removes the regular prop.\n      expect(store.props, hasLength(3));\n      expect(store.props.containsKey('value'), isFalse);\n    });\n  });\n\n  test('Predicate provided, removes Future/Timer/Stream', () async {\n    final store = Store<int>(initialState: 1);\n\n    // Future prop\n    store.setProp('future', Future.delayed(const Duration(seconds: 1), () => 'foo'));\n\n    // Timer prop\n    final timer = ManagedTimer(const Duration(milliseconds: 100), () => 'foo');\n    store.setProp('timer', timer);\n\n    // Stream prop\n    final sub = Stream.periodic(const Duration(milliseconds: 10), (i) => i).listen((event) {});\n    store.setProp('subscription', sub);\n\n    // Regular prop\n    store.setProp('value', 'bar');\n\n    expect(timer.isCancelled, isFalse);\n    expect(store.props, hasLength(4));\n\n    // Predicate: Only remove the regular prop.\n    store.disposeProps(({key, value}) => true);\n\n    // Closes the Timer.\n    expect(timer.isCancelled, isTrue);\n\n    // Removes all.\n    expect(store.props, hasLength(0));\n  });\n}\n\nclass ManagedTimer implements Timer {\n  Timer? _timer;\n  bool _isCancelled = false;\n\n  ManagedTimer(Duration duration, void Function() callback) {\n    _timer = Timer(duration, callback);\n  }\n\n  @override\n  void cancel() {\n    _timer?.cancel();\n    _isCancelled = true;\n  }\n\n  bool get isCancelled => _isCancelled;\n\n  @override\n  bool get isActive => throw UnimplementedError();\n\n  @override\n  int get tick => throw UnimplementedError();\n}\n"
  },
  {
    "path": "test/reducer_future_or_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\n/// These test makes sure that reducers can return:\n///    Null reduce()\n///    AppState reduce()\n///    AppState? reduce()\n///    Future<AppState> reduce()\n///    Future<AppState?> reduce()\n///\n/// But CANNOT return:\n///    Future<AppState>? reduce()\n///    Future<AppState?>? reduce()\n///\nvoid main() {\n  test('Test all accepted and rejected reducer return types', () async {\n    //\n    // The initial state is \"0\".\n    Store<AppState> store = Store<AppState>(initialState: AppState.initialState());\n    await Future.delayed(const Duration(milliseconds: 50));\n    expect(store.state.text, \"0\");\n\n    // Null reduce()\n    // Doesn't change anything and the state is still \"0\".\n    store.dispatch(ActionNull());\n    await Future.delayed(const Duration(milliseconds: 50));\n    expect(store.state.text, \"0\");\n\n    // AppState reduce()\n    // Adds an \"A\" to the state.\n    store.dispatch(ActionA());\n    await Future.delayed(const Duration(milliseconds: 50));\n    expect(store.state.text, \"0A\");\n\n    // AppState? reduce()\n    // Adds a \"B\" to the state.\n    store.dispatch(ActionB());\n    await Future.delayed(const Duration(milliseconds: 50));\n    expect(store.state.text, \"0AB\");\n\n    // Future<AppState> reduce()\n    // Adds a \"C\" to the state.\n    store.dispatch(ActionC());\n    await Future.delayed(const Duration(milliseconds: 50));\n    expect(store.state.text, \"0ABC\");\n\n    // Future<AppState?> reduce()\n    // Adds a \"D\" to the state.\n    store.dispatch(ActionD());\n    await Future.delayed(const Duration(milliseconds: 50));\n    expect(store.state.text, \"0ABCD\");\n\n    // ------------\n\n    dynamic error1;\n\n    try {\n      // Future<AppState>? reduce()\n      await store.dispatch(ActionE());\n    } catch (error) {\n      error1 = error;\n    }\n\n    expect(\n        error1,\n        StoreException(\"Reducer should return `St?` or `Future<St?>`. \"\n            \"Do not return `Future<St>?`.\"));\n\n    // ------------\n\n    dynamic error2;\n\n    try {\n      // Future<AppState?>? reduce()\n      await store.dispatch(ActionF());\n    } catch (error) {\n      error2 = error;\n    }\n\n    expect(\n        error2,\n        StoreException(\"Reducer should return `St?` or `Future<St?>`. \"\n            \"Do not return `Future<St?>?`.\"));\n\n    // ------------\n\n    dynamic error3;\n\n    try {\n      // FutureOr<AppState> reduce()\n      await store.dispatch(ActionG());\n    } catch (error) {\n      error3 = error;\n    }\n\n    expect(\n        error3,\n        StoreException(\"Reducer should return `St?` or `Future<St?>`. \"\n            \"Do not return `FutureOr`.\"));\n\n    // ------------\n\n    dynamic error4;\n\n    try {\n      // FutureOr<AppState?> reduce()\n      await store.dispatch(ActionH());\n    } catch (error) {\n      error4 = error;\n    }\n\n    expect(\n        error4,\n        StoreException(\"Reducer should return `St?` or `Future<St?>`. \"\n            \"Do not return `FutureOr`.\"));\n\n    // ------------\n\n    dynamic error5;\n\n    try {\n      // FutureOr<AppState>? reduce()\n      await store.dispatch(ActionI());\n    } catch (error) {\n      error5 = error;\n    }\n\n    expect(\n        error5,\n        StoreException(\"Reducer should return `St?` or `Future<St?>`. \"\n            \"Do not return `FutureOr`.\"));\n\n    // ------------\n\n    dynamic error6;\n\n    try {\n      // FutureOr<AppState?>? reduce()\n      await store.dispatch(ActionJ());\n    } catch (error) {\n      error6 = error;\n    }\n\n    expect(\n        error6,\n        StoreException(\"Reducer should return `St?` or `Future<St?>`. \"\n            \"Do not return `FutureOr`.\"));\n\n    // ------------\n  });\n}\n\n/// Null reduce()\nclass ActionNull extends ReduxAction<AppState> {\n  @override\n  Null reduce() {\n    return null;\n  }\n}\n\n/// AppState reduce()\nclass ActionA extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copy(state.text + 'A');\n  }\n}\n\n/// AppState? reduce()\nclass ActionB extends ReduxAction<AppState> {\n  @override\n  AppState? reduce() {\n    return state.copy(state.text + 'B');\n  }\n}\n\n/// Future<AppState> reduce()\nclass ActionC extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    return state.copy(state.text + 'C');\n  }\n}\n\n/// Future<AppState?> reduce()\nclass ActionD extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    return state.copy(state.text + 'D');\n  }\n}\n\n/// Future<AppState>? reduce()\nclass ActionE extends ReduxAction<AppState> {\n  @override\n  Future<AppState>? reduce() async {\n    return state.copy(state.text + 'E');\n  }\n}\n\n/// Future<AppState?>? reduce()\nclass ActionF extends ReduxAction<AppState> {\n  @override\n  Future<AppState?>? reduce() async {\n    return state.copy(state.text + 'F');\n  }\n}\n\n/// FutureOr<AppState> reduce()\nclass ActionG extends ReduxAction<AppState> {\n  @override\n  FutureOr<AppState> reduce() async {\n    return state.copy(state.text + 'G');\n  }\n}\n\n/// FutureOr<AppState?> reduce()\nclass ActionH extends ReduxAction<AppState> {\n  @override\n  FutureOr<AppState?> reduce() async {\n    return state.copy(state.text + 'H');\n  }\n}\n\n/// FutureOr<AppState>? reduce()\nclass ActionI extends ReduxAction<AppState> {\n  @override\n  FutureOr<AppState>? reduce() async {\n    return state.copy(state.text + 'I');\n  }\n}\n\n/// FutureOr<AppState?>? reduce()\nclass ActionJ extends ReduxAction<AppState> {\n  @override\n  FutureOr<AppState?>? reduce() async {\n    return state.copy(state.text + 'J');\n  }\n}\n\n@immutable\nclass AppState {\n  final String text;\n\n  AppState(this.text);\n\n  AppState copy(String? text) => AppState(text ?? this.text);\n\n  static AppState initialState() => AppState('0');\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState && runtimeType == other.runtimeType && text == other.text;\n\n  @override\n  int get hashCode => text.hashCode;\n\n  @override\n  String toString() => text.toString();\n}\n"
  },
  {
    "path": "test/retry_mixin_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart' hide Retry;\n\nvoid main() {\n  var feature = BddFeature('Retry actions');\n\n  Bdd(feature)\n      .scenario('Action retries a few times and succeeds.')\n      .given('An action that retries up to 10 times.')\n      .and('The action fails with an user exception the first 4 times.')\n      .when('The action is dispatched.')\n      .then('It does change the state.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n    var action = ActionThatRetriesAndSucceeds();\n    await store.dispatchAndWait(action);\n    expect(action.attempts, 5);\n    expect(action.log, '012345');\n    expect(store.state.count, 2);\n    expect(action.status.isCompletedOk, isTrue);\n  });\n\n  Bdd(feature)\n      .scenario('Action retries unlimited tries until it succeeds.')\n      .given('An action marked with \"UnlimitedRetries\".')\n      .and('The action fails with an user exception the first 6 times.')\n      .when('The action is dispatched.')\n      .then('It does change the state.')\n      .note('Without the \"UnlimitedRetries\" it would fail because the default is 3 retries.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n    var action = ActionThatRetriesUnlimitedAndFails();\n    await store.dispatchAndWait(action);\n    expect(action.attempts, 7);\n    expect(action.log, '01234567');\n    expect(store.state.count, 2);\n    expect(action.status.isCompletedOk, isTrue);\n  });\n\n  Bdd(feature)\n      .scenario('Action retries a few times and fails.')\n      .given('An action that retries up to 3 times.')\n      .and('The action fails with an user exception the first 4 times.')\n      .when('The action is dispatched.')\n      .then('It does NOT change the state.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n    var action = ActionThatRetriesAndFails();\n    await store.dispatchAndWait(action);\n    expect(store.state.count, 1);\n    expect(action.attempts, 4);\n    expect(action.log, '0123');\n    expect(action.status.isCompletedFailed, isTrue);\n  });\n\n  Bdd(feature)\n      .scenario('Sync action becomes ASYNC of it retries, even if it succeeds the first time.')\n      .given('A SYNC action that retries up to 10 times.')\n      .when('The action is dispatched and succeeds the first time.')\n      .then('It cannot be dispatched SYNC anymore.')\n      .run((_) async {\n    var store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n    var action = ActionThatRetriesButSucceedsTheFirstTry();\n    await store.dispatchAndWait(action);\n    expect(action.attempts, 0);\n    expect(action.log, '0');\n    expect(store.state.count, 2);\n    expect(action.status.isCompletedOk, isTrue);\n\n    // The action cannot be dispatched SYNC anymore.\n    expect(() => store.dispatchSync(action), throwsA(isA<StoreException>()));\n  });\n}\n\nclass State {\n  final int count;\n\n  State(this.count);\n\n  @override\n  String toString() => 'State($count)';\n}\n\nclass ActionThatRetriesAndSucceeds extends ReduxAction<State> with Retry {\n  @override\n  Duration get initialDelay => const Duration(milliseconds: 10);\n\n  @override\n  int maxRetries = 10;\n\n  String log = '';\n\n  @override\n  State reduce() {\n    log += attempts.toString();\n    if (attempts <= 4) throw UserException('Failed: $attempts');\n    return State(state.count + 1);\n  }\n}\n\nclass ActionThatRetriesAndFails extends ReduxAction<State> with Retry {\n  @override\n  Duration get initialDelay => const Duration(milliseconds: 10);\n\n  String log = '';\n\n  @override\n  State reduce() {\n    log += attempts.toString();\n    if (attempts <= 4) throw UserException('Failed: $attempts');\n    return State(state.count + 1);\n  }\n}\n\nclass ActionThatRetriesButSucceedsTheFirstTry extends ReduxAction<State> with Retry {\n  @override\n  Duration get initialDelay => const Duration(milliseconds: 10);\n\n  @override\n  int maxRetries = 10;\n\n  String log = '';\n\n  @override\n  State reduce() {\n    log += attempts.toString();\n    return State(state.count + 1);\n  }\n}\n\nclass ActionThatRetriesUnlimitedAndFails extends ReduxAction<State> with Retry, UnlimitedRetries {\n  @override\n  Duration get initialDelay => const Duration(milliseconds: 10);\n\n  String log = '';\n\n  @override\n  State reduce() {\n    log += attempts.toString();\n    if (attempts <= 6) throw UserException('Failed: $attempts');\n    return State(state.count + 1);\n  }\n}\n"
  },
  {
    "path": "test/server_push_init_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  final feature = BddFeature('ServerPush mixin initialization');\n\n  setUp(() {\n    resetTestState();\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Init: Fresh server response applies when backend revision >= persisted.')\n      .given('App launched with state.serverRevision=100 and backend at 100.')\n      .when('User dispatches OptimisticSyncWithPush action.')\n      .then('Response is applied and serverRevision increases to 101.')\n      .run((_) async {\n    final store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 100));\n\n    backend = SimulatedBackend(liked: false, serverRevision: 100);\n\n    await store.dispatchAndWait(ToggleLikeStableAction());\n\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 101);\n    expect(backend.serverRevision, 101);\n  });\n\n  Bdd(feature)\n      .scenario('Init: Fresh push updates both backend and store.')\n      .given('App launched with state.serverRevision=100 and backend at 100.')\n      .when('A fresh push arrives with serverRevision=105.')\n      .then('Both store and backend end at 105 with same liked value.')\n      .run((_) async {\n    final store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 100));\n\n    backend = SimulatedBackend(liked: false, serverRevision: 100);\n\n    // Simulate a fresh push from another device (backend already updated).\n    backend.applyPush(true, 105);\n    await store.dispatchAndWait(PushLikeUpdate(liked: true, serverRev: 105));\n\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 105);\n    expect(backend.liked, true);\n    expect(backend.serverRevision, 105);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Init: Stale push is ignored using persisted serverRevision in state.')\n      .given(\n          'App launched with state.serverRevision=100 and revisionMap empty.')\n      .when('A ServerPush arrives with serverRevision=99.')\n      .then('Push is ignored and state does not regress.')\n      .run((_) async {\n    final store = Store<AppState>(\n        initialState: AppState(liked: true, serverRevision: 100));\n\n    // Backend reflects the persisted state (already at rev 100).\n    backend = SimulatedBackend(liked: true, serverRevision: 100);\n\n    // Simulate a delayed/stale push arriving. The backend has already moved on,\n    // so we only deliver the stale message to the client (no applyPush call).\n    await store.dispatchAndWait(PushLikeUpdate(liked: false, serverRev: 99));\n\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 100);\n  });\n\n  Bdd(feature)\n      .scenario('Init: Push with equal serverRevision is ignored at startup.')\n      .given(\n          'App launched with state.serverRevision=100 and revisionMap empty.')\n      .when(\n          'A ServerPush arrives with serverRevision=100 and a different liked value.')\n      .then('The push is ignored and state remains unchanged.')\n      .run((_) async {\n    final store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 100));\n\n    // Backend reflects the persisted state (already at rev 100).\n    backend = SimulatedBackend(liked: false, serverRevision: 100);\n\n    // Push arrives with equal revision but different value.\n    // Should be ignored because it's not newer.\n    await store.dispatchAndWait(PushLikeUpdate(liked: true, serverRev: 100));\n\n    expect(store.state.liked, false);\n    expect(store.state.serverRevision, 100);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Init: Stale server response is ignored using persisted serverRevision in state.')\n      .given(\n          'App launched with state.serverRevision=100 and a request returns serverRev=1.')\n      .when('A OptimisticSyncWithPush action completes.')\n      .then(\n          'The stale response is not applied and serverRevision does not regress.')\n      .run((_) async {\n    final store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 100));\n\n    // Simulate a stale backend that starts at revision 0, so first response returns 1.\n    backend = SimulatedBackend(liked: false, serverRevision: 0);\n\n    await store.dispatchAndWait(ToggleLikeStableAction());\n\n    // Optimistic UI happened.\n    expect(store.state.liked, true);\n\n    // But serverRevision must not go backwards.\n    expect(store.state.serverRevision, 100);\n\n    expect(backend.requestLog.length, 1);\n    expect(backend.requestLog.first, contains('localRev=1'));\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Init: OptimisticSyncWithPush seeds revisionMap from persisted state so ServerPush ordering works even if push cannot read state.')\n      .given('App launched with state.serverRevision=100.')\n      .when(\n          'A OptimisticSyncWithPush request starts (in flight) and a stale push arrives, but the push action returns null from getServerRevisionFromState.')\n      .then('The stale push is still ignored because revisionMap was seeded.')\n      .note(\n          'This verifies the seeding path: OptimisticSyncWithPush must copy the persisted serverRevision into revisionMap.')\n      .run((_) async {\n    final store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 100));\n\n    // Backend starts at revision 100, will go to 101 when request completes.\n    backend = SimulatedBackend(liked: false, serverRevision: 100);\n\n    // Hold request so we can inject push while the request is in flight.\n    requestCompleter = Completer<void>();\n    requestStarted = Completer<void>();\n    requestFinished = Completer<void>();\n\n    // Start request: this must seed revisionMap from state.serverRevision=100.\n    store.dispatch(ToggleLikeStableAction());\n    await requestStarted!.future; // Wait until request is in flight.\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 100);\n\n    // Stale push arrives (delayed message). Backend has already moved on,\n    // so we only deliver the stale message to the client.\n    await store\n        .dispatchAndWait(PushLikeUpdateNoStateRev(liked: false, serverRev: 99));\n\n    // If seeding didn't happen, this would incorrectly apply and set serverRevision=99.\n    expect(store.state.serverRevision, 100);\n\n    // Now complete the request (backend will increment to 101 and apply).\n    requestCompleter!.complete();\n    await requestFinished!.future; // Wait until action finishes.\n\n    expect(store.state.serverRevision, 101);\n    expect(store.state.liked, true);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Init: Per-key persisted revisions are honored when revisionMap is empty.')\n      .given('App launched with serverRevById[A]=100 and serverRevById[B]=0.')\n      .when('A stale push arrives for A and a fresh push arrives for B.')\n      .then('A push is ignored and B push is applied.')\n      .run((_) async {\n    final store = Store<AppStateItems>(\n      initialState: AppStateItems.initialWithRevs(\n        likedById: {'A': false, 'B': false},\n        serverRevById: {'A': 100, 'B': 0},\n      ),\n    );\n\n    // Initialize backends to match persisted state.\n    backendByItem['A'] = SimulatedBackend(liked: false, serverRevision: 100);\n    backendByItem['B'] = SimulatedBackend(liked: false, serverRevision: 0);\n\n    // Stale push for A (older than persisted 100) must be ignored.\n    // Backend has already moved on, so no applyPush call.\n    await store.dispatchAndWait(\n        PushItemLikeUpdate(itemId: 'A', liked: true, serverRev: 99));\n    expect(store.state.likedById['A'], false);\n    expect(store.state.serverRevById['A'], 100);\n\n    // Fresh push for B should apply (backend updated first).\n    backendByItem['B']!.applyPush(true, 1);\n    await store.dispatchAndWait(\n        PushItemLikeUpdate(itemId: 'B', liked: true, serverRev: 1));\n    expect(store.state.likedById['B'], true);\n    expect(store.state.serverRevById['B'], 1);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Init: OptimisticSyncWithPush seeds per-key revisionMap from persisted state for item keys.')\n      .given('App launched with serverRevById[A]=100.')\n      .when(\n          'A OptimisticSyncWithPush request for A starts, and a stale push for A arrives that cannot read serverRev from state.')\n      .then(\n          'The stale push is ignored because revisionMap was seeded for key A.')\n      .run((_) async {\n    final store = Store<AppStateItems>(\n      initialState: AppStateItems.initialWithRevs(\n        likedById: {'A': false, 'B': false},\n        serverRevById: {'A': 100, 'B': 0},\n      ),\n    );\n\n    // Backend for item A starts at revision 100.\n    backendByItem['A'] = SimulatedBackend(liked: false, serverRevision: 100);\n\n    requestCompleterByItem['A'] = Completer<void>();\n    requestStartedByItem['A'] = Completer<void>();\n    requestFinishedByItem['A'] = Completer<void>();\n\n    // Start request for A: must seed revisionMap for key A from state (100).\n    store.dispatch(ToggleLikeItemStableAction('A'));\n    await requestStartedByItem['A']!.future; // Wait until request is in flight.\n    expect(store.state.likedById['A'], true);\n    expect(store.state.serverRevById['A'], 100);\n\n    // Stale push for A (delayed message). Backend has already moved on.\n    await store.dispatchAndWait(\n        PushItemLikeUpdateNoStateRev(itemId: 'A', liked: false, serverRev: 99));\n\n    // If seeding didn't happen, this would regress serverRevById[A] to 99.\n    expect(store.state.serverRevById['A'], 100);\n\n    // Finish request so the test doesn't leave in-flight work behind.\n    requestCompleterByItem['A']!.complete();\n    await requestFinishedByItem['A']!.future; // Wait until action finishes.\n\n    expect(store.state.serverRevById['A'], 101);\n  });\n}\n\n// =============================================================================\n// Simulated Backend\n// =============================================================================\n\n/// Simulates a backend server that maintains its own state.\n/// When it receives a value, it stores it and increments the serverRevision.\nclass SimulatedBackend {\n  bool liked;\n  int serverRevision;\n  final List<String> requestLog = [];\n\n  SimulatedBackend({required this.liked, required this.serverRevision});\n\n  /// Applies a push that originated from the server (e.g., another device).\n  /// Only updates if the revision is newer than current.\n  void applyPush(bool value, int rev) {\n    requestLog.add('applyPush($value, serverRev=$rev)');\n    if (rev > serverRevision) {\n      serverRevision = rev;\n      liked = value;\n    }\n  }\n\n  /// Simulates sending a value to the server.\n  /// The server stores the value and returns the new state with incremented revision.\n  ({bool value, int serverRevision}) receiveValue(bool value, int localRev) {\n    requestLog.add('receiveValue($value, localRev=$localRev)');\n    liked = value;\n    serverRevision++;\n    return (value: liked, serverRevision: serverRevision);\n  }\n}\n\n// =============================================================================\n// Shared test state\n// =============================================================================\n\nclass AppState {\n  final bool liked;\n  final int serverRevision;\n\n  AppState({required this.liked, this.serverRevision = 0});\n\n  AppState copy({bool? liked, int? serverRevision}) => AppState(\n        liked: liked ?? this.liked,\n        serverRevision: serverRevision ?? this.serverRevision,\n      );\n}\n\nclass AppStateItems {\n  final Map<String, bool> likedById;\n  final Map<String, int> serverRevById;\n\n  AppStateItems({required this.likedById, required this.serverRevById});\n\n  factory AppStateItems.initialWithRevs({\n    required Map<String, bool> likedById,\n    required Map<String, int> serverRevById,\n  }) =>\n      AppStateItems(likedById: likedById, serverRevById: serverRevById);\n\n  AppStateItems copy({\n    Map<String, bool>? likedById,\n    Map<String, int>? serverRevById,\n  }) =>\n      AppStateItems(\n        likedById: likedById ?? this.likedById,\n        serverRevById: serverRevById ?? this.serverRevById,\n      );\n\n  AppStateItems setLiked(String id, bool liked) =>\n      copy(likedById: {...likedById, id: liked});\n\n  AppStateItems setServerRev(String id, int rev) =>\n      copy(serverRevById: {...serverRevById, id: rev});\n}\n\n// =============================================================================\n// Test control variables\n// =============================================================================\n\nlate SimulatedBackend backend;\nMap<String, SimulatedBackend> backendByItem = {};\n\nCompleter<void>? requestCompleter;\nCompleter<void>? requestStarted;\nCompleter<void>? requestFinished;\n\nMap<String, Completer<void>?> requestCompleterByItem = {};\nMap<String, Completer<void>?> requestStartedByItem = {};\nMap<String, Completer<void>?> requestFinishedByItem = {};\n\nvoid resetTestState() {\n  backend = SimulatedBackend(liked: false, serverRevision: 0);\n  backendByItem = {};\n  requestCompleter = null;\n  requestStarted = null;\n  requestFinished = null;\n  requestCompleterByItem = {};\n  requestStartedByItem = {};\n  requestFinishedByItem = {};\n}\n\n// =============================================================================\n// Actions\n// =============================================================================\n\nclass ToggleLikeStableAction extends ReduxAction<AppState>\n    with OptimisticSyncWithPush<AppState, bool> {\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(AppState state, bool optimisticValue) =>\n      state.copy(liked: optimisticValue);\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    final response = serverResponse as ({bool value, int serverRevision});\n    return state.copy(\n      liked: response.value,\n      serverRevision: response.serverRevision,\n    );\n  }\n\n  @override\n  Future<Object?> sendValueToServer(\n    Object? optimisticValue,\n    int localRevision,\n    int deviceId,\n  ) async {\n    // Signal: request is now in-flight (it will block on requestCompleter).\n    // Use isCompleted guard to handle follow-up requests safely.\n    if (requestStarted != null && !requestStarted!.isCompleted) {\n      requestStarted!.complete();\n    }\n\n    if (requestCompleter != null) {\n      await requestCompleter!.future;\n      requestCompleter = null;\n    }\n\n    final response =\n        backend.receiveValue(optimisticValue as bool, localRevision);\n    informServerRevision(response.serverRevision);\n\n    return response;\n  }\n\n  @override\n  Future<AppState?> onFinish(Object? error) async {\n    // Use isCompleted guard to handle follow-up requests safely.\n    if (requestFinished != null && !requestFinished!.isCompleted) {\n      requestFinished!.complete();\n    }\n    return null;\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) => state.serverRevision;\n}\n\nclass PushLikeUpdate extends ReduxAction<AppState> with ServerPush<AppState> {\n  final bool liked;\n  final int serverRev;\n  final int pushLocalRevision;\n  final int pushDeviceId;\n\n  PushLikeUpdate({\n    required this.liked,\n    required this.serverRev,\n    this.pushLocalRevision = 0,\n    int? pushDeviceId,\n  }) : pushDeviceId = pushDeviceId ?? -999;\n\n  @override\n  Type associatedAction() => ToggleLikeStableAction;\n\n  @override\n  PushMetadata pushMetadata() => (\n        serverRevision: serverRev,\n        localRevision: pushLocalRevision,\n        deviceId: pushDeviceId,\n      );\n\n  @override\n  AppState? applyServerPushToState(\n      AppState state, Object? key, int serverRevision) {\n    return state.copy(liked: liked, serverRevision: serverRevision);\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) => state.serverRevision;\n}\n\n/// Same as PushLikeUpdate but pretends it cannot read persisted revision from state.\n/// Used to prove OptimisticSyncWithPush seeded revisionMap.\nclass PushLikeUpdateNoStateRev extends ReduxAction<AppState>\n    with ServerPush<AppState> {\n  final bool liked;\n  final int serverRev;\n  final int pushLocalRevision;\n  final int pushDeviceId;\n\n  PushLikeUpdateNoStateRev({\n    required this.liked,\n    required this.serverRev,\n    this.pushLocalRevision = 0,\n    int? pushDeviceId,\n  }) : pushDeviceId = pushDeviceId ?? -999;\n\n  @override\n  Type associatedAction() => ToggleLikeStableAction;\n\n  @override\n  PushMetadata pushMetadata() => (\n        serverRevision: serverRev,\n        localRevision: pushLocalRevision,\n        deviceId: pushDeviceId,\n      );\n\n  @override\n  AppState? applyServerPushToState(\n      AppState state, Object? key, int serverRevision) {\n    return state.copy(liked: liked, serverRevision: serverRevision);\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) => -1;\n}\n\nclass ToggleLikeItemStableAction extends ReduxAction<AppStateItems>\n    with OptimisticSyncWithPush<AppStateItems, bool> {\n  final String itemId;\n\n  ToggleLikeItemStableAction(this.itemId);\n\n  @override\n  Object? optimisticSyncKeyParams() => itemId;\n\n  @override\n  bool valueToApply() => !(state.likedById[itemId] ?? false);\n\n  @override\n  bool getValueFromState(AppStateItems state) =>\n      state.likedById[itemId] ?? false;\n\n  @override\n  AppStateItems applyOptimisticValueToState(\n      AppStateItems state, bool optimisticValue) {\n    return state.setLiked(itemId, optimisticValue);\n  }\n\n  @override\n  AppStateItems? applyServerResponseToState(\n      AppStateItems state, Object serverResponse) {\n    final response = serverResponse as ({bool value, int serverRevision});\n    return state\n        .setLiked(itemId, response.value)\n        .setServerRev(itemId, response.serverRevision);\n  }\n\n  @override\n  Future<Object?> sendValueToServer(\n    Object? optimisticValue,\n    int localRevision,\n    int deviceId,\n  ) async {\n    // Signal: request is now in-flight (it will block on requestCompleterByItem).\n    // Use isCompleted guard to handle follow-up requests safely.\n    final started = requestStartedByItem[itemId];\n    if (started != null && !started.isCompleted) {\n      started.complete();\n    }\n\n    final c = requestCompleterByItem[itemId];\n    if (c != null) {\n      await c.future;\n      requestCompleterByItem[itemId] = null;\n    }\n\n    // Get or create backend, seeding from persisted state.\n    final itemBackend = backendByItem[itemId] ??\n        SimulatedBackend(\n          liked: state.likedById[itemId] ?? false,\n          serverRevision: state.serverRevById[itemId] ?? 0,\n        );\n    backendByItem[itemId] = itemBackend;\n\n    final response =\n        itemBackend.receiveValue(optimisticValue as bool, localRevision);\n    informServerRevision(response.serverRevision);\n\n    return response;\n  }\n\n  @override\n  Future<AppStateItems?> onFinish(Object? error) async {\n    // Use isCompleted guard to handle follow-up requests safely.\n    final finished = requestFinishedByItem[itemId];\n    if (finished != null && !finished.isCompleted) {\n      finished.complete();\n    }\n    return null;\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) {\n    final k = key is String ? key : itemId;\n    return state.serverRevById[k] ?? -1;\n  }\n}\n\nclass PushItemLikeUpdate extends ReduxAction<AppStateItems>\n    with ServerPush<AppStateItems> {\n  final String itemId;\n  final bool liked;\n  final int serverRev;\n  final int pushLocalRevision;\n  final int pushDeviceId;\n\n  PushItemLikeUpdate({\n    required this.itemId,\n    required this.liked,\n    required this.serverRev,\n    this.pushLocalRevision = 0,\n    int? pushDeviceId,\n  }) : pushDeviceId = pushDeviceId ?? -999;\n\n  @override\n  Type associatedAction() => ToggleLikeItemStableAction;\n\n  @override\n  Object? optimisticSyncKeyParams() => itemId;\n\n  @override\n  PushMetadata pushMetadata() => (\n        serverRevision: serverRev,\n        localRevision: pushLocalRevision,\n        deviceId: pushDeviceId,\n      );\n\n  @override\n  AppStateItems? applyServerPushToState(\n      AppStateItems state, Object? key, int serverRevision) {\n    return state.setLiked(itemId, liked).setServerRev(itemId, serverRevision);\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) {\n    final k = key is String ? key : itemId;\n    return state.serverRevById[k] ?? -1;\n  }\n}\n\nclass PushItemLikeUpdateNoStateRev extends ReduxAction<AppStateItems>\n    with ServerPush<AppStateItems> {\n  final String itemId;\n  final bool liked;\n  final int serverRev;\n  final int pushLocalRevision;\n  final int pushDeviceId;\n\n  PushItemLikeUpdateNoStateRev({\n    required this.itemId,\n    required this.liked,\n    required this.serverRev,\n    this.pushLocalRevision = 0,\n    int? pushDeviceId,\n  }) : pushDeviceId = pushDeviceId ?? -999;\n\n  @override\n  Type associatedAction() => ToggleLikeItemStableAction;\n\n  @override\n  Object? optimisticSyncKeyParams() => itemId;\n\n  @override\n  PushMetadata pushMetadata() => (\n        serverRevision: serverRev,\n        localRevision: pushLocalRevision,\n        deviceId: pushDeviceId,\n      );\n\n  @override\n  AppStateItems? applyServerPushToState(\n      AppStateItems state, Object? key, int serverRevision) {\n    return state.setLiked(itemId, liked).setServerRev(itemId, serverRevision);\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) => -1;\n}\n"
  },
  {
    "path": "test/server_push_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var feature =\n      BddFeature('ServerPush mixin');\n\n  setUp(() {\n    resetTestState();\n  });\n\n  Bdd(feature)\n      .scenario('BUG: Remote newer push can be overwritten by local follow-up.')\n      .given('A OptimisticSyncWithPush action has a request in flight '\n          'and localRevision advanced.')\n      .when('A ServerPush arrives with a newer serverRevision from another '\n          'device before the request completes.')\n      .then('The mixin must not send a follow-up that fights the '\n          'newer serverRevision.')\n      .note('Last write wins: newer serverRevision supersedes pending local '\n          'intent for that key.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n\n    final req1 = Completer<void>();\n    requestCompleter = req1;\n\n    // Tap #1: false -> true, localRev=1, request in flight.\n    store.dispatch(ToggleLikeStableAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n\n    // Tap #2 while in flight: true -> false, localRev=2 (pending local intent differs from sent).\n    store.dispatch(ToggleLikeStableAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false);\n\n    // Remote device push arrives with much newer serverRev, and a different value.\n    store.dispatch(PushLikeUpdate(liked: true, serverRev: 50));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 50);\n\n    // Complete request #1 (serverRev=11, should be stale vs 50).\n    req1.complete();\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    final sendValueLogs =\n        requestLog.where((s) => s.startsWith('sendValue(')).toList();\n\n    // Correct: no follow-up should be sent to override a newer serverRevision.\n    expect(sendValueLogs.length, 1,\n        reason:\n            'Should not fight newer serverRevision with a follow-up request.');\n\n    // Correct: remote push remains the final truth.\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 50);\n  });\n\n  Bdd(feature)\n      .scenario('ServerPush does not increment localRevision.')\n      .given(\n          'A OptimisticSyncWithPush action where requests log localRev values.')\n      .when('A ServerPush action is dispatched between local taps.')\n      .then('The next local request uses the next localRevision '\n          'as if the push never happened.')\n      .note('Pushes must not be treated as local intent.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n\n    await store.dispatchAndWait(ToggleLikeStableAction());\n    expect(requestLog.where((s) => s.startsWith('sendValue(')).length, 1);\n\n    store.dispatch(PushLikeUpdate(liked: false, serverRev: 99));\n    await Future.delayed(const Duration(milliseconds: 10));\n\n    await store.dispatchAndWait(ToggleLikeStableAction());\n\n    final sendValueLogs =\n        requestLog.where((s) => s.startsWith('sendValue(')).toList();\n    expect(sendValueLogs.length, 2);\n\n    expect(sendValueLogs[0], contains('localRev=1'));\n    expect(sendValueLogs[1], contains('localRev=2'),\n        reason: 'Push must not consume a local revision number.');\n  });\n\n  Bdd(feature)\n      .scenario(\n          'ServerPush applies immediately even while the stable-sync key is locked.')\n      .given(\n          'A OptimisticSyncWithPush action has a request in flight for a key.')\n      .when(\n          'A ServerPush arrives for the same key with a newer serverRevision.')\n      .then(\n          'The pushed value is applied immediately and the stale response is ignored.')\n      .note(\n          'Immediate apply is required even when locked; staleness must be deterministic.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n\n    final req1 = Completer<void>();\n    requestCompleter = req1;\n\n    // Tap #1: optimistic true, request in flight.\n    store.dispatch(ToggleLikeStableAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n\n    // Push arrives while locked: apply immediately.\n    store.dispatch(PushLikeUpdate(liked: false, serverRev: 12));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false);\n    expect(store.state.serverRevision, 12);\n\n    // Complete request (serverRev=11) -> must be ignored as stale.\n    req1.complete();\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    expect(store.state.liked, false);\n    expect(store.state.serverRevision, 12);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'ServerPush keying: push for item B does not interfere with item A in flight.')\n      .given(\n          'Two OptimisticSync keys A and B, and a request is in flight for A.')\n      .when('A ServerPush arrives for B and then for A.')\n      .then(\n          'Both pushes apply immediately to their own keys and do not affect the other key lock or revisions.')\n      .note(\n          'Verifies optimisticSyncKeyParams and computeOptimisticSyncKey alignment between OptimisticSyncWithPush and ServerPush.')\n      .run((_) async {\n    var store = Store<AppStateItems>(initialState: AppStateItems.initial());\n\n    nextServerRevision = 1;\n\n    final reqA = Completer<void>();\n    requestCompleterByItem['A'] = reqA;\n\n    // Start request for A (locked for key A).\n    store.dispatch(ToggleLikeItemStableAction('A'));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.likedById['A'], true);\n\n    // Push for B applies immediately (independent key).\n    store.dispatch(PushItemLikeUpdate(itemId: 'B', liked: true, serverRev: 5));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.likedById['B'], true);\n    expect(store.state.serverRevById['B'], 5);\n\n    // Push for A applies immediately even though A is locked.\n    store.dispatch(PushItemLikeUpdate(itemId: 'A', liked: false, serverRev: 6));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.likedById['A'], false);\n    expect(store.state.serverRevById['A'], 6);\n\n    // Complete request for A (will return serverRev=1, stale vs 6 -> should be ignored).\n    reqA.complete();\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    expect(store.state.likedById['A'], false);\n    expect(store.state.serverRevById['A'], 6);\n\n    // Ensure B stayed untouched by A completion.\n    expect(store.state.likedById['B'], true);\n    expect(store.state.serverRevById['B'], 5);\n\n    final sendValueLogs =\n        requestLog.where((s) => s.startsWith('sendValue(')).toList();\n    expect(sendValueLogs.length, 1);\n    expect(sendValueLogs.first, contains('item=A'));\n  });\n\n  Bdd(feature)\n      .scenario('ServerPush ignores pushes with equal serverRevision.')\n      .given('Client already applied serverRevision=20 for a key.')\n      .when(\n          'A ServerPush arrives with serverRevision=20 and a different value.')\n      .then('The push is ignored and state does not regress or flap.')\n      .note('Ordering rule should be strictly greater than current.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 0));\n\n    store.dispatch(PushLikeUpdate(liked: true, serverRev: 20));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 20);\n\n    // Same serverRev, different value -> must be ignored.\n    store.dispatch(PushLikeUpdate(liked: false, serverRev: 20));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 20);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Optimization preserved with pushes: no follow-up if final local value equals sent value.')\n      .given(\n          'A OptimisticSyncWithPush action with request 1 in flight and ServerPush updates may arrive.')\n      .when(\n          'User changes intent during the request but ends back at the original sent value.')\n      .then(\n          'No follow-up request is sent, even if a push overwrote the store temporarily.')\n      .note(\n          'Ensures the revision-based path keeps the same coalescing optimization as the value-based path.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 10));\n\n    nextServerRevision = 11;\n\n    final req1 = Completer<void>();\n    requestCompleter = req1;\n\n    // Tap #1: false -> true, localRev=1, request in flight (sent true).\n    store.dispatch(ToggleLikeStableAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n\n    // Tap #2: true -> false, localRev=2.\n    store.dispatch(ToggleLikeStableAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false);\n\n    // Tap #3: false -> true, localRev=3 (back to sent value).\n    store.dispatch(ToggleLikeStableAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n\n    // Push overwrites store temporarily to false (same rev we'll later apply from response).\n    store.dispatch(PushLikeUpdate(liked: false, serverRev: 11));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false);\n\n    // Complete request #1.\n    req1.complete();\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    final sendValueLogs =\n        requestLog.where((s) => s.startsWith('sendValue(')).toList();\n    expect(sendValueLogs.length, 1,\n        reason:\n            'Should not send follow-up when final local value equals sent value.');\n\n    // Response should restore true (no follow-up needed).\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 11);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Stale ServerPush does not overwrite local optimistic UI while request is in flight.')\n      .given(\n          'A OptimisticSyncWithPush action has a request in flight and the store is optimistic.')\n      .when(\n          'A ServerPush arrives with an older serverRevision for the same key.')\n      .then(\n          'The push is ignored immediately and the optimistic UI state remains unchanged.')\n      .note('Covers out-of-order delivery while locked.')\n      .run((_) async {\n    var store = Store<AppState>(\n        initialState: AppState(liked: false, serverRevision: 0));\n\n    // Seed known server revision in the mixin bookkeeping.\n    store.dispatch(PushLikeUpdate(liked: false, serverRev: 20));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.serverRevision, 20);\n\n    nextServerRevision = 21;\n\n    final req1 = Completer<void>();\n    requestCompleter = req1;\n\n    // Tap: optimistic false -> true, request in flight.\n    store.dispatch(ToggleLikeStableAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 20);\n\n    // Stale push arrives (older than 20) -> must be ignored.\n    store.dispatch(PushLikeUpdate(liked: false, serverRev: 19));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 20);\n\n    // Complete request -> serverRev=21 (newer) should apply.\n    req1.complete();\n    await Future.delayed(const Duration(milliseconds: 200));\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 21);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Stale server response is NOT applied when a newer ServerPush arrives before the response.')\n      .given('A OptimisticSyncWithPush action has a request in flight.')\n      .when('A ServerPush arrives with a newer serverRevision before the '\n          'request completes.')\n      .then('The stale response is ignored and the pushed state remains.')\n      .run((_) async {\n    final store = Store<AppState>(\n      initialState: AppState(liked: false, serverRevision: 10),\n    );\n\n    // Make request 1 return serverRev=11.\n    nextServerRevision = 11;\n\n    // Hold request 1 so we can inject push before response.\n    final c = Completer<void>();\n    requestCompleter = c;\n\n    // Tap #1: optimistic false -> true, localRev=1.\n    store.dispatch(ToggleLikeStableAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n\n    // Newer push arrives from another device BEFORE request 1 completes.\n    store.dispatch(PushLikeUpdate(liked: false, serverRev: 12));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false);\n    expect(store.state.serverRevision, 12);\n\n    // Now let request 1 complete (it would try to apply serverRev=11).\n    c.complete();\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    // Because ServerPush updated the mixin's revision map to 12,\n    // the response with serverRev=11 must be treated as stale and ignored.\n    expect(store.state.liked, false);\n    expect(store.state.serverRevision, 12);\n  });\n\n  Bdd(feature)\n      .scenario('Out-of-order pushes are ignored by ServerPush ordering.')\n      .given('A ServerPush has already applied serverRevision=20.')\n      .when('A ServerPush arrives with an older serverRevision=19.')\n      .then('The older push is ignored and state does not regress.')\n      .run((_) async {\n    final store = Store<AppState>(\n      initialState: AppState(liked: false, serverRevision: 0),\n    );\n\n    // First push (new).\n    store.dispatch(PushLikeUpdate(liked: true, serverRev: 20));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 20);\n\n    // Older push should be ignored even if it tries to overwrite.\n    store.dispatch(PushLikeUpdate(liked: false, serverRev: 19));\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n    expect(store.state.serverRevision, 20);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Self-echo push does not break follow-up and preserves optimistic state.')\n      .given('A OptimisticSyncWithPush action has a request in flight.')\n      .when('User taps again and a self-echo push arrives.')\n      .then('The self-echo is ignored (not applied) and follow-up still sends latest local intent.')\n      .note(\n          'Self-echo is detected by matching deviceId and stale localRevision.')\n      .run((_) async {\n    final store = Store<AppState>(\n      initialState: AppState(liked: false, serverRevision: 10),\n    );\n\n    // Make request 1 wait; it will return serverRev=11, and follow-up will be 12.\n    nextServerRevision = 11;\n    final c = Completer<void>();\n    requestCompleter = c;\n\n    // Tap #1: false -> true (optimistic), localRev=1; request 1 starts.\n    store.dispatch(ToggleLikeStableAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, true);\n\n    // Tap #2 while request 1 in flight: true -> false (optimistic), localRev=2.\n    store.dispatch(ToggleLikeStableAction());\n    await Future.delayed(const Duration(milliseconds: 10));\n    expect(store.state.liked, false);\n\n    // Self-echo push arrives (same deviceId, stale localRev=1).\n    // With the new ServerPush logic, self-echoes are NOT applied to state,\n    // preserving the optimistic value (false).\n    store.dispatch(PushLikeUpdate(\n      liked: true,\n      serverRev: 11,\n      pushLocalRevision: 1, // Stale: less than currentLocalRev=2\n      pushDeviceId: OptimisticSyncWithPush.deviceId(), // Same device = self-echo\n    ));\n    await Future.delayed(const Duration(milliseconds: 10));\n    // Self-echo is NOT applied, so state remains false (optimistic from tap 2).\n    expect(store.state.liked, false,\n        reason: 'Self-echo should not be applied to state');\n\n    // Finish request 1, causing OptimisticSyncWithPush to detect localRev advanced\n    // and send follow-up with the latest local intent (false).\n    c.complete();\n    await Future.delayed(const Duration(milliseconds: 200));\n\n    final sendLogs =\n        requestLog.where((s) => s.startsWith('sendValue(')).toList();\n\n    // First request should be true, localRev=1.\n    expect(sendLogs.first, 'sendValue(true, localRev=1)');\n\n    // Follow-up must send false, localRev=2.\n    expect(sendLogs.length, greaterThanOrEqualTo(2));\n    expect(sendLogs[1], 'sendValue(false, localRev=2)');\n\n    // Final should reflect the follow-up (newer serverRev=12).\n    expect(store.state.liked, false);\n    expect(store.state.serverRevision, 12);\n  });\n}\n\n// =============================================================================\n// Shared test state\n// =============================================================================\n\nclass AppState {\n  final bool liked;\n  final int serverRevision;\n\n  AppState({required this.liked, this.serverRevision = 0});\n\n  AppState copy({bool? liked, int? serverRevision}) => AppState(\n        liked: liked ?? this.liked,\n        serverRevision: serverRevision ?? this.serverRevision,\n      );\n\n  @override\n  String toString() => 'AppState(liked: $liked, serverRev: $serverRevision)';\n}\n\nclass AppStateItems {\n  final Map<String, bool> likedById;\n  final Map<String, int> serverRevById;\n\n  AppStateItems({required this.likedById, required this.serverRevById});\n\n  factory AppStateItems.initial() => AppStateItems(\n        likedById: {'A': false, 'B': false},\n        serverRevById: {'A': 0, 'B': 0},\n      );\n\n  AppStateItems copy({\n    Map<String, bool>? likedById,\n    Map<String, int>? serverRevById,\n  }) =>\n      AppStateItems(\n        likedById: likedById ?? this.likedById,\n        serverRevById: serverRevById ?? this.serverRevById,\n      );\n\n  AppStateItems setLiked(String id, bool liked) =>\n      copy(likedById: {...likedById, id: liked});\n\n  AppStateItems setServerRev(String id, int rev) =>\n      copy(serverRevById: {...serverRevById, id: rev});\n\n  @override\n  String toString() =>\n      'AppStateItems(likedById: $likedById, serverRevById: $serverRevById)';\n}\n\n// =============================================================================\n// Test control variables\n// =============================================================================\n\nList<String> requestLog = [];\nCompleter<void>? requestCompleter;\nMap<String, Completer<void>?> requestCompleterByItem = {};\nint nextServerRevision = 1;\n\nvoid resetTestState() {\n  requestLog = [];\n  requestCompleter = null;\n  requestCompleterByItem = {};\n  nextServerRevision = 1;\n}\n\n// =============================================================================\n// Actions\n// =============================================================================\n\nclass ToggleLikeStableAction extends ReduxAction<AppState>\n    with OptimisticSyncWithPush<AppState, bool> {\n  int _serverRevFromResponse = 0;\n\n  @override\n  bool valueToApply() => !state.liked;\n\n  @override\n  bool getValueFromState(AppState state) => state.liked;\n\n  @override\n  AppState applyOptimisticValueToState(AppState state, bool optimisticValue) =>\n      state.copy(liked: optimisticValue);\n\n  @override\n  AppState? applyServerResponseToState(AppState state, Object serverResponse) {\n    return state.copy(\n      liked: serverResponse as bool,\n      serverRevision: _serverRevFromResponse,\n    );\n  }\n\n  @override\n  Future<Object?> sendValueToServer(\n    Object? optimisticValue,\n    int localRevision,\n    int deviceId,\n  ) async {\n    requestLog.add('sendValue($optimisticValue, localRev=$localRevision)');\n\n    if (requestCompleter != null) {\n      await requestCompleter!.future;\n      requestCompleter = null;\n    }\n\n    _serverRevFromResponse = nextServerRevision++;\n    informServerRevision(_serverRevFromResponse);\n\n    return optimisticValue;\n  }\n\n  @override\n  Future<AppState?> onFinish(Object? error) async {\n    requestLog.add('onFinish()');\n    return null;\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) {\n    return state.serverRevision;\n  }\n}\n\nclass PushLikeUpdate extends ReduxAction<AppState> with ServerPush<AppState> {\n  final bool liked;\n  final int serverRev;\n  final int pushLocalRevision;\n  final int pushDeviceId;\n\n  PushLikeUpdate({\n    required this.liked,\n    required this.serverRev,\n    this.pushLocalRevision = 0,\n    int? pushDeviceId,\n  }) : pushDeviceId = pushDeviceId ?? -999; // Default to a different deviceId\n\n  @override\n  Type associatedAction() => ToggleLikeStableAction;\n\n  @override\n  PushMetadata pushMetadata() => (\n        serverRevision: serverRev,\n        localRevision: pushLocalRevision,\n        deviceId: pushDeviceId,\n      );\n\n  @override\n  AppState? applyServerPushToState(\n      AppState state, Object? key, int serverRevision) {\n    return state.copy(liked: liked, serverRevision: serverRevision);\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) {\n    return state.serverRevision;\n  }\n}\n\nclass ToggleLikeItemStableAction extends ReduxAction<AppStateItems>\n    with OptimisticSyncWithPush<AppStateItems, bool> {\n  final String itemId;\n  int _serverRevFromResponse = 0;\n\n  ToggleLikeItemStableAction(this.itemId);\n\n  @override\n  Object? optimisticSyncKeyParams() => itemId;\n\n  @override\n  bool valueToApply() => !(state.likedById[itemId] ?? false);\n\n  @override\n  bool getValueFromState(AppStateItems state) =>\n      state.likedById[itemId] ?? false;\n\n  @override\n  AppStateItems applyOptimisticValueToState(\n      AppStateItems state, bool optimisticValue) {\n    return state.setLiked(itemId, optimisticValue);\n  }\n\n  @override\n  AppStateItems? applyServerResponseToState(\n      AppStateItems state, Object serverResponse) {\n    return state\n        .setLiked(itemId, serverResponse as bool)\n        .setServerRev(itemId, _serverRevFromResponse);\n  }\n\n  @override\n  Future<Object?> sendValueToServer(\n    Object? optimisticValue,\n    int localRevision,\n    int deviceId,\n  ) async {\n    requestLog\n        .add('sendValue(item=$itemId, value=$optimisticValue, localRev=$localRevision)');\n\n    final c = requestCompleterByItem[itemId];\n    if (c != null) {\n      await c.future;\n      requestCompleterByItem[itemId] = null;\n    }\n\n    _serverRevFromResponse = nextServerRevision++;\n    informServerRevision(_serverRevFromResponse);\n\n    return optimisticValue;\n  }\n\n  @override\n  Future<AppStateItems?> onFinish(Object? error) async {\n    requestLog.add('onFinish(item=$itemId)');\n    return null;\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) {\n    return state.serverRevById[key] ?? -1;\n  }\n}\n\nclass PushItemLikeUpdate extends ReduxAction<AppStateItems>\n    with ServerPush<AppStateItems> {\n  final String itemId;\n  final bool liked;\n  final int serverRev;\n  final int pushLocalRevision;\n  final int pushDeviceId;\n\n  PushItemLikeUpdate({\n    required this.itemId,\n    required this.liked,\n    required this.serverRev,\n    this.pushLocalRevision = 0,\n    int? pushDeviceId,\n  }) : pushDeviceId = pushDeviceId ?? -999; // Default to a different deviceId\n\n  @override\n  Type associatedAction() => ToggleLikeItemStableAction;\n\n  @override\n  Object? optimisticSyncKeyParams() => itemId;\n\n  @override\n  PushMetadata pushMetadata() => (\n        serverRevision: serverRev,\n        localRevision: pushLocalRevision,\n        deviceId: pushDeviceId,\n      );\n\n  @override\n  AppStateItems? applyServerPushToState(\n      AppStateItems state, Object? key, int serverRevision) {\n    return state.setLiked(itemId, liked).setServerRev(itemId, serverRevision);\n  }\n\n  @override\n  int getServerRevisionFromState(Object? key) {\n    return state.serverRevById[key] ?? -1;\n  }\n}\n"
  },
  {
    "path": "test/store_connector_test.dart",
    "content": "import \"package:async_redux/async_redux.dart\";\nimport \"package:flutter/material.dart\";\nimport \"package:flutter_test/flutter_test.dart\";\n\nvoid main() {\n  group(StoreConnector, () {\n    // TODO shouldUpdateModel does not currently work with converter\n    testWidgets(\n      \"shouldUpdateModel.converter\",\n      (tester) async {\n        final storeTester = StoreTester<int>(initialState: 0);\n\n        await tester.pumpWidget(StoreProvider<int>(\n          store: storeTester.store,\n          child: MaterialApp(\n            home: StoreConnector<int, int>(\n              converter: (store) => store.state,\n              shouldUpdateModel: (state) => state % 2 == 0,\n              builder: (context, value) {\n                return Text(value.toString());\n              },\n            ),\n          ),\n        ));\n\n        expect(find.text(\"0\"), findsOneWidget);\n\n        await storeTester.dispatchState(1);\n        await tester.pumpAndSettle();\n        expect(find.text(\"0\"), findsOneWidget);\n\n        await storeTester.dispatchState(2);\n        await tester.pumpAndSettle();\n        expect(find.text(\"2\"), findsOneWidget);\n      },\n      skip: true,\n    );\n\n    testWidgets(\n      \"shouldUpdateModel.vm\",\n      (tester) async {\n        final store = Store<int>(initialState: 0);\n\n        await tester.pumpWidget(StoreProvider<int>(\n          store: store,\n          child: _TestWidget(),\n        ));\n\n        expect(find.text(\"0\"), findsOneWidget);\n\n        store.dispatch(UpdateStateAction(1));\n        await tester.pumpAndSettle();\n        expect(find.text(\"0\"), findsOneWidget);\n\n        store.dispatch(UpdateStateAction(2));\n        await tester.pumpAndSettle();\n        expect(find.text(\"2\"), findsOneWidget);\n      },\n    );\n\n    testWidgets(\n      \"shouldUpdateModel.vm with external rebuild\",\n      (tester) async {\n        final store = Store<int>(initialState: 0);\n\n        await tester.pumpWidget(StoreProvider<int>(\n          store: store,\n          child: _TestWidget(),\n        ));\n\n        expect(find.text(\"0\"), findsOneWidget);\n\n        store.dispatch(UpdateStateAction(1));\n        await tester.pump();\n        await tester.pump();\n        expect(find.text(\"0\"), findsOneWidget);\n\n        tester.firstState<_TestWidgetState>(find.byType(_TestWidget)).forceRebuild();\n        await tester.pump();\n        await tester.pump();\n        expect(find.text(\"0\"), findsOneWidget);\n      },\n    );\n\n    testWidgets(\n      \"When the observed state changes, the widget rebuilds\",\n      (tester) async {\n        final store = Store<AppState>(\n          initialState: AppState(\n            text: 'x',\n            boolean: false,\n          ),\n        );\n\n        await tester.pumpWidget(StoreProvider<AppState>(\n          store: store,\n          child: _AnotherConnector(),\n        ));\n\n        // Initially, that's what we have.\n        expect(find.text(\"text: x / boolean: false\"), findsOneWidget);\n\n        store.dispatch(UpdateStateAction(\n          AppState(text: 'y', boolean: false),\n        ));\n\n        await tester.pump();\n        await tester.pump();\n        expect(find.text(\"text: y / boolean: false\"), findsOneWidget);\n\n        store.dispatch(UpdateStateAction(\n          AppState(text: 'y', boolean: true),\n        ));\n\n        await tester.pump();\n        await tester.pump();\n        expect(find.text(\"text: y / boolean: true\"), findsOneWidget);\n      },\n    );\n\n    testWidgets(\n      \"When 'isWaiting' changes, the widget rebuilds (notify true)\",\n      (tester) async {\n        final store = Store<int>(initialState: 1);\n\n        await tester.pumpWidget(StoreProvider<int>(\n          store: store,\n          child: _IsWaitingConnector(),\n        ));\n\n        // 1) Initially, we're NOT waiting.\n        expect(find.text(\"isWaiting: false\"), findsOneWidget);\n\n        // 2) When we dispatch an ASYNC action, we ARE waiting.\n        store.dispatch(_AsyncChangeStateAction(), notify: true);\n        await tester.pump();\n        expect(find.text(\"isWaiting: true\"), findsOneWidget);\n\n        // 3) After 99 milliseconds we are STILL waiting, because the action takes 100ms to finish.\n        await tester.pump(const Duration(milliseconds: 99));\n        print('store.state.text = ${'isWaiting: ${store.isWaiting(_AsyncChangeStateAction)}'}');\n        expect(find.text(\"isWaiting: true\"), findsOneWidget);\n\n        // 4) After 1 more millisecond we are FINISHED WAITING, as the action finished.\n        await tester.pump(const Duration(milliseconds: 1));\n        print('store.state.text = ${'isWaiting: ${store.isWaiting(_AsyncChangeStateAction)}'}');\n        expect(find.text(\"isWaiting: false\"), findsOneWidget);\n      },\n    );\n\n    testWidgets(\n      \"When 'isWaiting' changes, the widget rebuilds (notify false)\",\n      (tester) async {\n        final store = Store<int>(initialState: 1);\n\n        await tester.pumpWidget(StoreProvider<int>(\n          store: store,\n          child: _IsWaitingConnector(),\n        ));\n\n        // 1) Initially, we're NOT waiting.\n        expect(find.text(\"isWaiting: false\"), findsOneWidget);\n\n        // 2) When we dispatch an ASYNC action, we ARE waiting.\n        store.dispatch(_AsyncChangeStateAction(), notify: false);\n        await tester.pump();\n        expect(find.text(\"isWaiting: true\"), findsOneWidget);\n\n        // 3) After 99 milliseconds we are STILL waiting, because the action takes 100ms to finish.\n        await tester.pump(const Duration(milliseconds: 99));\n        print('store.state.text = ${'isWaiting: ${store.isWaiting(_AsyncChangeStateAction)}'}');\n        expect(find.text(\"isWaiting: true\"), findsOneWidget);\n\n        // 4) After 1 more millisecond we are FINISHED WAITING, as the action finished.\n        await tester.pump(const Duration(milliseconds: 1));\n        print('store.state.text = ${'isWaiting: ${store.isWaiting(_AsyncChangeStateAction)}'}');\n        expect(find.text(\"isWaiting: false\"), findsOneWidget);\n      },\n    );\n\n    testWidgets(\n      \"When 'isWaiting' changes, the widget rebuilds. \"\n      \"The action fails with a dialog\",\n      (tester) async {\n        final store = Store<int>(initialState: 1);\n\n        await tester.pumpWidget(StoreProvider<int>(\n          store: store,\n          child: _IsWaitingConnector(),\n        ));\n\n        // 1) Initially, we're NOT waiting.\n        expect(find.text(\"isWaiting: false\"), findsOneWidget);\n\n        // 2) When we dispatch an ASYNC action, we ARE waiting.\n        store.dispatch(_AsyncChangeStateAction(failWithDialog: true));\n        await tester.pump();\n        expect(find.text(\"isWaiting: true\"), findsOneWidget);\n\n        // 3) After 99 milliseconds we are STILL waiting, because the action takes 100ms to finish.\n        await tester.pump(const Duration(milliseconds: 99));\n        print('store.state.text = ${'isWaiting: ${store.isWaiting(_AsyncChangeStateAction)}'}');\n        expect(find.text(\"isWaiting: true\"), findsOneWidget);\n\n        // 4) After 1 more millisecond we are FINISHED WAITING, as the action finished.\n        await tester.pump(const Duration(milliseconds: 1));\n        print('store.state.text = ${'isWaiting: ${store.isWaiting(_AsyncChangeStateAction)}'}');\n        expect(find.text(\"isWaiting: false\"), findsOneWidget);\n      },\n    );\n\n    testWidgets(\n      \"When 'isWaiting' changes, the widget rebuilds. \"\n      \"The action fails with no dialog\",\n      (tester) async {\n        final store = Store<int>(initialState: 1);\n\n        await tester.pumpWidget(StoreProvider<int>(\n          store: store,\n          child: _IsWaitingConnector(),\n        ));\n\n        // 1) Initially, we're NOT waiting.\n        expect(find.text(\"isWaiting: false\"), findsOneWidget);\n\n        // 2) When we dispatch an ASYNC action, we ARE waiting.\n        store.dispatch(_AsyncChangeStateAction(failNoDialog: true));\n        await tester.pump();\n        expect(find.text(\"isWaiting: true\"), findsOneWidget);\n\n        // 3) After 99 milliseconds we are STILL waiting, because the action takes 100ms to finish.\n        await tester.pump(const Duration(milliseconds: 99));\n        print('store.state.text = ${'isWaiting: ${store.isWaiting(_AsyncChangeStateAction)}'}');\n        expect(find.text(\"isWaiting: true\"), findsOneWidget);\n\n        // 4) After 1 more millisecond we are FINISHED WAITING, as the action finished.\n        await tester.pump(const Duration(milliseconds: 1));\n        print('store.state.text = ${'isWaiting: ${store.isWaiting(_AsyncChangeStateAction)}'}');\n        expect(find.text(\"isWaiting: false\"), findsOneWidget);\n      },\n    );\n  });\n}\n\nclass _TestWidget extends StatefulWidget {\n  @override\n  State<_TestWidget> createState() => _TestWidgetState();\n}\n\nclass _TestWidgetState extends State<_TestWidget> {\n  void forceRebuild() => setState(() {});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: _TestContent(key: const ValueKey(\"tester\")),\n    );\n  }\n}\n\nclass _TestContent extends StatelessWidget {\n  _TestContent({\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<int, ViewModel>(\n      vm: () => Factory(this),\n      shouldUpdateModel: (state) => state % 2 == 0,\n      builder: (context, vm) {\n        return Text(vm.counter.toString());\n      },\n    );\n  }\n}\n\nclass Factory extends VmFactory<int, _TestContent, ViewModel> {\n  Factory(connector) : super(connector);\n\n  @override\n  ViewModel fromStore() {\n    return ViewModel(\n      counter: state,\n    );\n  }\n}\n\nclass ViewModel extends Vm {\n  final int counter;\n\n  ViewModel({\n    required this.counter,\n  }) : super(equals: [counter]);\n}\n\n////////////////////////////////////////////////////////////////////////////////////////////////////\n\nclass _AnotherWidget extends StatelessWidget {\n  final String text;\n  final bool boolean;\n\n  const _AnotherWidget({\n    required this.text,\n    required this.boolean,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Text('text: $text / boolean: $boolean'),\n    );\n  }\n}\n\nclass _AnotherConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<AppState, AnotherViewModel>(\n      vm: () => AnotherFactory(this),\n      builder: (context, vm) {\n        return _AnotherWidget(\n          text: vm.text,\n          boolean: vm.boolean,\n        );\n      },\n    );\n  }\n}\n\nclass AnotherFactory extends VmFactory<AppState, _AnotherConnector, AnotherViewModel> {\n  AnotherFactory(connector) : super(connector);\n\n  @override\n  AnotherViewModel fromStore() {\n    return AnotherViewModel(\n      text: state.text,\n      boolean: state.boolean,\n    );\n  }\n}\n\nclass AnotherViewModel extends Vm {\n  final String text;\n  final bool boolean;\n\n  AnotherViewModel({\n    required this.text,\n    required this.boolean,\n  }) : super(equals: [text, boolean]);\n}\n\nclass AppState {\n  final String text;\n  final bool boolean;\n\n  AppState({\n    required this.text,\n    required this.boolean,\n  });\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState &&\n          runtimeType == other.runtimeType &&\n          text == other.text &&\n          boolean == other.boolean;\n\n  @override\n  int get hashCode => text.hashCode ^ boolean.hashCode;\n\n  @override\n  String toString() {\n    return 'AppState{text: $text, boolean: $boolean}';\n  }\n}\n\n////////////////////////////////////////////////////////////////////////////////////////////////////\n\nclass _IsWaitingWidget extends StatelessWidget {\n  final bool isWaiting;\n\n  const _IsWaitingWidget({required this.isWaiting});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(home: Text('isWaiting: $isWaiting'));\n  }\n}\n\nclass _IsWaitingConnector extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return StoreConnector<int, IsWaitingViewModel>(\n      vm: () => IsWaitingFactory(this),\n      builder: (context, vm) {\n        return _IsWaitingWidget(isWaiting: vm.isWaiting);\n      },\n    );\n  }\n}\n\nclass IsWaitingFactory extends VmFactory<int, _IsWaitingConnector, IsWaitingViewModel> {\n  IsWaitingFactory(connector) : super(connector);\n\n  @override\n  IsWaitingViewModel fromStore() {\n    return IsWaitingViewModel(\n      isWaiting: isWaiting(_AsyncChangeStateAction),\n    );\n  }\n}\n\nclass IsWaitingViewModel extends Vm {\n  final bool isWaiting;\n\n  IsWaitingViewModel({\n    required this.isWaiting,\n  }) : super(equals: [isWaiting]);\n}\n\nclass _AsyncChangeStateAction extends ReduxAction<int> {\n  //\n  final bool failWithDialog;\n  final bool failNoDialog;\n\n  _AsyncChangeStateAction({\n    this.failWithDialog = false,\n    this.failNoDialog = false,\n  });\n\n  @override\n  Future<int?> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 100));\n    if (failWithDialog) throw const UserException('Fail');\n    if (failNoDialog) throw const UserException('Fail').noDialog;\n    return null;\n  }\n}\n"
  },
  {
    "path": "test/store_observer_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _MyAction extends ReduxAction<num> {\n  final num number;\n\n  _MyAction(this.number);\n\n  @override\n  num reduce() => number;\n}\n\nclass _MyAsyncAction extends ReduxAction<num> {\n  final num number;\n\n  _MyAsyncAction(this.number);\n\n  @override\n  Future<num> reduce() async {\n    await Future.sync(() {});\n    return number;\n  }\n}\n\nclass _MyStateObserver extends StateObserver<num> {\n  num? iniValue;\n  num? endValue;\n\n  @override\n  void observe(\n    ReduxAction<num?> action,\n    num? stateIni,\n    num? stateEnd,\n    Object? error,\n    int dispatchCount,\n  ) {\n    iniValue = stateIni;\n    endValue = stateEnd;\n  }\n}\n\nvoid main() {\n  var observer = _MyStateObserver();\n  StoreTester<num> createStoreTester() {\n    var store = Store<num>(initialState: 0, stateObservers: [observer]);\n    return StoreTester.from(store);\n  }\n\n  test('Dispatch a sync action, see what the StateObserver picks up. ', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state, 0);\n\n    storeTester.dispatch(_MyAction(1));\n    var condition = (TestInfo<num?>? info) => info!.state == 1;\n    await storeTester.waitConditionGetLast(condition);\n    expect(observer.iniValue, 0);\n    expect(observer.endValue, 1);\n  });\n\n  test('Dispatch an async action, see what the StateObserver picks up.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state, 0);\n\n    storeTester.dispatch(_MyAsyncAction(1));\n    var condition = (TestInfo<num?>? info) => info!.state == 1;\n    await storeTester.waitConditionGetLast(condition);\n    expect(observer.iniValue, 0);\n    expect(observer.endValue, 1);\n  });\n}\n"
  },
  {
    "path": "test/store_provider_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test('backdoorStaticGlobal', () async {\n    Store<String> store = Store<String>(initialState: \"abc\");\n    StoreProvider(store: store, child: Container());\n\n    expect(StoreProvider.backdoorStaticGlobal(), store);\n    expect(StoreProvider.backdoorStaticGlobal<dynamic>(), store);\n    expect(StoreProvider.backdoorStaticGlobal<String>(), store);\n\n    expect(() => StoreProvider.backdoorStaticGlobal<int>(), throwsA(isA<StoreException>()));\n\n    var backdoorStore = StoreProvider.backdoorStaticGlobal();\n    expect(backdoorStore, store);\n\n    var backdoorState = StoreProvider.backdoorStaticGlobal().state;\n    expect(backdoorState, \"abc\");\n  });\n}\n"
  },
  {
    "path": "test/store_tester_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n@immutable\nclass AppState {\n  final String text;\n\n  AppState(this.text);\n\n  AppState.add(AppState state, String text) : text = state.text + \",\" + text;\n}\n\nclass Action1 extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => AppState.add(state, \"1\");\n}\n\nclass Action2 extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => AppState.add(state, \"2\");\n}\n\nclass Action3 extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => AppState.add(state, \"3\");\n}\n\nclass Action3b extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    dispatch(Action4());\n    return AppState.add(state, \"3b\");\n  }\n}\n\nclass Action4 extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => AppState.add(state, \"4\");\n}\n\nclass Action5 extends ReduxAction<AppState> {\n  @override\n  AppState reduce() => AppState.add(state, \"5\");\n}\n\nclass Action6 extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    dispatch(Action1());\n    dispatch(Action2());\n    dispatch(Action3());\n    return AppState.add(state, \"6\");\n  }\n}\n\nclass Action6b extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    dispatch(Action1());\n    await Future.delayed(const Duration(milliseconds: 10));\n    dispatch(Action2());\n    dispatch(Action3());\n    return AppState.add(state, \"6b\");\n  }\n}\n\nclass Action6c extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    dispatch(Action1());\n    dispatch(Action2());\n    dispatch(Action3b());\n    return AppState.add(state, \"6c\");\n  }\n}\n\nclass Action7 extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    dispatch(Action4());\n    dispatch(Action6());\n    dispatch(Action2());\n    dispatch(Action5());\n    return AppState.add(state, \"7\");\n  }\n}\n\nclass Action7b extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    dispatch(Action4());\n    dispatch(Action6b());\n    dispatch(Action2());\n    dispatch(Action5());\n    return AppState.add(state, \"7b\");\n  }\n}\n\nclass Action8 extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    dispatch(Action2());\n    return AppState.add(state, \"8\");\n  }\n}\n\nclass Action9 extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 100));\n    return AppState.add(state, \"9\");\n  }\n}\n\nclass Action10a extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    dispatch(Action1());\n    dispatch(Action2());\n    dispatch(Action11a());\n    dispatch(Action3());\n    return AppState.add(state, \"10\");\n  }\n}\n\nclass Action10b extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    dispatch(Action1());\n    dispatch(Action2());\n    await dispatch(Action11b());\n    dispatch(Action3());\n    return AppState.add(state, \"10\");\n  }\n}\n\nclass Action10c extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    dispatch(Action1());\n    dispatch(Action2());\n    dispatch(Action11b());\n    dispatch(Action3());\n    return AppState.add(state, \"10\");\n  }\n}\n\nclass Action11a extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    throw const UserException(\"Hello!\");\n  }\n}\n\nclass Action11b extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    throw const UserException(\"Hello!\");\n  }\n}\n\nclass Action12 extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    dispatch(Action13());\n    return AppState.add(state, \"12\");\n  }\n}\n\nclass Action13 extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 1));\n    return AppState.add(state, \"13\");\n  }\n}\n\nvoid main() {\n  StoreTester<AppState> createStoreTester() {\n    var store = Store<AppState>(initialState: AppState(\"0\"));\n    return StoreTester.from(store);\n  }\n\n  test('Dispatch multiple actions but only issue a single change event.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    int invocations = 0;\n\n    storeTester.store.onChange.listen((event) {\n      invocations += 1;\n    });\n\n    await storeTester.dispatch(Action1());\n    await storeTester.dispatch(Action2());\n    await storeTester.dispatch(Action3());\n    await storeTester.dispatch(Action4());\n\n    expect(invocations, 4);\n    expect(storeTester.state.text, \"0,1,2,3,4\");\n\n    storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    invocations = 0;\n\n    storeTester.store.onChange.listen((event) {\n      invocations += 1;\n    });\n\n    await storeTester.dispatch(Action1(), notify: false);\n    await storeTester.dispatch(Action2(), notify: false);\n    await storeTester.dispatch(Action3(), notify: false);\n    await storeTester.dispatch(Action4(), notify: true);\n\n    expect(invocations, 1);\n    expect(storeTester.state.text, \"0,1,2,3,4\");\n  });\n\n  test(\n      'Dispatch some actions and wait until some condition is met. '\n      'Get the end state.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n\n    var condition = (TestInfo<AppState?>? info) => info!.state!.text == \"0,1,2\";\n    TestInfo<AppState?> info1 = await (storeTester.waitConditionGetLast(condition));\n    expect(info1.state!.text, \"0,1,2\");\n    expect(info1.ini, false);\n\n    TestInfo<AppState?> info2 =\n        await (storeTester.waitConditionGetLast((info) => info.state.text == \"0,1,2,3,4\"));\n    expect(info2.state!.text, \"0,1,2,3,4\");\n    expect(info2.ini, false);\n  });\n\n  test(\n      'Dispatch some actions and wait until some condition is met. '\n      'Get the end state.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n\n    var condition = (TestInfo<AppState?>? info) => info!.state!.text == \"0,1,2\" && info.ini;\n    TestInfo<AppState?> info1 =\n        await (storeTester.waitConditionGetLast(condition, ignoreIni: false));\n    expect(info1.state!.text, \"0,1,2\");\n    expect(info1.ini, true);\n\n    TestInfo<AppState?> info2 = await (storeTester.waitConditionGetLast(\n        (info) => info.state.text == \"0,1,2,3,4\" && !info.ini,\n        ignoreIni: false));\n    expect(info2.state!.text, \"0,1,2,3,4\");\n    expect(info2.ini, false);\n  });\n\n  test(\n      'Dispatch some actions and wait until some condition is met. '\n      'Get all of the intermediary states (END only).', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n\n    TestInfoList<AppState?> infos =\n        await storeTester.waitCondition((info) => info.state.text == \"0,1,2\");\n\n    expect(infos.length, 2);\n    expect(infos.getIndex(0).state!.text, \"0,1\");\n    expect(infos.getIndex(0).ini, false);\n    expect(infos.getIndex(1).state!.text, \"0,1,2\");\n    expect(infos.getIndex(1).ini, false);\n\n    infos = await storeTester.waitCondition((info) => info.state.text == \"0,1,2,3,4\");\n    expect(infos.length, 2);\n    expect(infos.getIndex(0).state!.text, \"0,1,2,3\");\n    expect(infos.getIndex(0).ini, false);\n    expect(infos.getIndex(1).state!.text, \"0,1,2,3,4\");\n    expect(infos.getIndex(1).ini, false);\n  });\n\n  test(\n      'Dispatch some actions and wait until some condition is met. '\n      'Get all of the intermediary states, '\n      'including INI and END.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n\n    TestInfoList<AppState?> infos =\n        await storeTester.waitCondition((info) => info.state.text == \"0,1,2\", ignoreIni: false);\n    expect(infos.length, 4);\n    expect(infos.getIndex(0).state!.text, \"0\");\n    expect(infos.getIndex(0).ini, true);\n    expect(infos.getIndex(1).state!.text, \"0,1\");\n    expect(infos.getIndex(1).ini, false);\n    expect(infos.getIndex(2).state!.text, \"0,1\");\n    expect(infos.getIndex(2).ini, true);\n    expect(infos.getIndex(3).state!.text, \"0,1,2\");\n    expect(infos.getIndex(3).ini, false);\n\n    infos =\n        await storeTester.waitCondition((info) => info.state.text == \"0,1,2,3,4\", ignoreIni: false);\n    expect(infos.length, 4);\n    expect(infos.getIndex(0).state!.text, \"0,1,2\");\n    expect(infos.getIndex(0).ini, true);\n    expect(infos.getIndex(1).state!.text, \"0,1,2,3\");\n    expect(infos.getIndex(1).ini, false);\n    expect(infos.getIndex(2).state!.text, \"0,1,2,3\");\n    expect(infos.getIndex(2).ini, true);\n    expect(infos.getIndex(3).state!.text, \"0,1,2,3,4\");\n    expect(infos.getIndex(3).ini, false);\n  });\n\n  test(\n      'Dispatch some action and wait for it. '\n      'Get the end state.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    TestInfo<AppState?> info = await (storeTester.wait(Action1));\n    expect(info.state!.text, \"0,1\");\n    expect(info.errors, isEmpty);\n  });\n\n  test(\n      'Dispatch some action and wait for a different one. '\n      'Gets an error.', () async {\n    var storeTester = createStoreTester();\n\n    storeTester.dispatch(Action1());\n\n    // await storeTester.wait(Action2);\n\n    await storeTester.wait(Action2).then(\n      (_) {\n        throw AssertionError();\n        return null; // ignore: dead_code\n      },\n      onError: expectAsync1(\n        (Object error) {\n          expect(error, const TypeMatcher<StoreException>());\n          expect(\n              error.toString(),\n              'Got this unexpected action: Action1 INI.\\n'\n              'Was expecting: Action2 INI.\\n'\n              'obtainedIni: [Action1]\\n'\n              'ignoredIni: []');\n        },\n      ),\n    );\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in order. '\n      'Get the end state.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    TestInfo<AppState?> info = await (storeTester.waitAllGetLast([Action1, Action2, Action3]));\n    expect(info.state!.text, \"0,1,2,3\");\n    expect(info.errors, isEmpty);\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in order. '\n      'Get the end state.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    // Action6 will dispatch actions 1, 2 and 3, and only then it will finish.\n    storeTester.dispatch(Action6());\n\n    TestInfo<AppState?> info =\n        await (storeTester.waitAllGetLast([Action6, Action1, Action2, Action3]));\n    expect(info.state!.text, \"0,1,2,3,6\");\n    expect(info.errors, isEmpty);\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in order. '\n      'Gets an error because they are not in order.', () async {\n    var storeTester = createStoreTester();\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action2());\n\n    await storeTester.waitAllGetLast([Action1, Action2, Action3]).then((_) {\n      throw AssertionError();\n      return null; // ignore: dead_code\n    }, onError: expectAsync1((Object error) {\n      expect(error, const TypeMatcher<StoreException>());\n      expect(\n          error.toString(),\n          'Got this unexpected action: Action3 INI.\\n'\n          'Was expecting: Action2 INI.\\n'\n          'obtainedIni: [Action1, Action3]\\n'\n          'ignoredIni: []');\n    }));\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in order. '\n      'Gets an error because a different one was dispatched in the middle.', () async {\n    var storeTester = createStoreTester();\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action4());\n    storeTester.dispatch(Action3());\n\n    await storeTester.waitAllGetLast([Action1, Action2, Action3]).then((_) {\n      throw AssertionError();\n      return null; // ignore: dead_code\n    }, onError: expectAsync1((Object error) {\n      expect(error, const TypeMatcher<StoreException>());\n      expect(\n          error.toString(),\n          'Got this unexpected action: Action4 INI.\\n'\n          'Was expecting: Action3 INI.\\n'\n          'obtainedIni: [Action1, Action2, Action4]\\n'\n          'ignoredIni: []');\n    }));\n  });\n\n  test(\n      'Dispatch a few actions and wait until one of them is dispatched, '\n      'ignoring the others.'\n      'Get the end state after this action.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n\n    TestInfo<AppState?> info = await storeTester.waitUntil(Action3);\n    expect(info.state!.text, \"0,1,2,3\");\n    expect(info.errors, isEmpty);\n  });\n\n  test(\n      'Dispatch a few actions and wait until all of them finish, '\n      'ignoring the others.'\n      'Get the end state after all actions finish.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n\n    TestInfo<AppState?> info = await storeTester.waitUntilAllGetLast([Action3, Action2]);\n    expect(info.state!.text, \"0,1,2,1,3\");\n    expect(info.errors, isEmpty);\n  });\n\n  test(\n      'Dispatch a few actions and wait until all of them finish, '\n      'ignoring the others.'\n      'Get all states until all actions finish.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n\n    TestInfoList<AppState?> infos = await storeTester.waitUntilAll([Action3, Action2]);\n\n    expect(infos.length, 4);\n    expect(infos.getIndex(0).state!.text, \"0,1\");\n    expect(infos.getIndex(1).state!.text, \"0,1,2\");\n    expect(infos.getIndex(2).state!.text, \"0,1,2,1\");\n    expect(infos.getIndex(3).state!.text, \"0,1,2,1,3\");\n  });\n\n  test(\n      'Wait until some action that is never dispatched.'\n      'Should timeout.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action4());\n\n    await storeTester.waitUntil(Action3, timeoutInSeconds: 1).then((_) {\n      throw AssertionError();\n      return null; // ignore: dead_code\n    }, onError: expectAsync1((Object error) {\n      expect(error, const TypeMatcher<StoreException>());\n      expect(error.toString(), \"Timeout.\");\n    }));\n  });\n\n  test(\n      'Dispatch a few actions and wait until one specific action instance is dispatched, '\n      'ignoring the others.'\n      'Get the end state after this action.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    var action3 = Action3();\n    storeTester.dispatch(action3);\n    storeTester.dispatch(Action4());\n\n    TestInfo<AppState?> info = await storeTester.waitUntilAction(action3);\n    expect(info.state!.text, \"0,1,2,3\");\n    expect(info.errors, isEmpty);\n  });\n\n  test(\n      'Wait until some action that is never dispatched.'\n      'Should timeout.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action4());\n\n    await storeTester.waitUntilAction(Action3(), timeoutInSeconds: 1).then((_) {\n      throw AssertionError();\n      return null; // ignore: dead_code\n    }, onError: expectAsync1((Object error) {\n      expect(error, const TypeMatcher<StoreException>());\n      expect(error.toString(), \"Timeout.\");\n    }));\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in ANY order. '\n      'Get the end state.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    TestInfo<AppState?> info =\n        await (storeTester.waitAllUnorderedGetLast([Action3, Action1, Action2]));\n    expect(info.state!.text, \"0,1,2,3\");\n    expect(info.errors, isEmpty);\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in ANY order. '\n      'Gets an error because there is a different one in the middle.', () async {\n    var storeTester = createStoreTester();\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action4());\n    storeTester.dispatch(Action3());\n\n    await storeTester.waitAllUnorderedGetLast([Action1, Action2, Action3]).then((_) {\n      throw AssertionError();\n      return null; // ignore: dead_code\n    }, onError: expectAsync1((Object error) {\n      expect(error, const TypeMatcher<StoreException>());\n      expect(error.toString(), \"Unexpected action was dispatched: Action4 INI.\");\n    }));\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in order. '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    TestInfoList<AppState?> infos = await storeTester.waitAll([Action1, Action2, Action3]);\n    expect(infos.getIndex(0).state!.text, \"0,1\");\n    expect(infos.getIndex(1).state!.text, \"0,1,2\");\n    expect(infos.getIndex(2).state!.text, \"0,1,2,3\");\n    expect(infos.getIndex(0).errors, isEmpty);\n    expect(infos.getIndex(1).errors, isEmpty);\n    expect(infos.getIndex(2).errors, isEmpty);\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in ANY order. '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    TestInfoList<AppState?> infos = await storeTester\n        .waitAllUnordered([Action1, Action2, Action3, Action2], timeoutInSeconds: 1);\n\n    // The states are indexed by order of dispatching\n    // (doesn't matter the order we were expecting them).\n    expect(infos.length, 4);\n    expect(infos.getIndex(0).state!.text, \"0,1\");\n    expect(infos.getIndex(1).state!.text, \"0,1,2\");\n    expect(infos.getIndex(2).state!.text, \"0,1,2,2\");\n    expect(infos.getIndex(3).state!.text, \"0,1,2,2,3\");\n    expect(infos.getIndex(0).errors, isEmpty);\n    expect(infos.getIndex(1).errors, isEmpty);\n    expect(infos.getIndex(2).errors, isEmpty);\n    expect(infos.getIndex(3).errors, isEmpty);\n\n    // Can get first and last.\n    expect(infos.first.state!.text, \"0,1\");\n    expect(infos.last.state!.text, \"0,1,2,2,3\");\n\n    // Number of infos.\n    expect(infos.length, 4);\n    expect(infos.isEmpty, false);\n    expect(infos.isNotEmpty, true);\n\n    // It's usually better to get them by type, not order.\n    expect(infos[Action1]!.state!.text, \"0,1\");\n    expect(infos[Action2]!.state!.text, \"0,1,2\");\n    expect(infos[Action3]!.state!.text, \"0,1,2,2,3\");\n\n    // Operator [] is the same as get().\n    expect(infos.get(Action1)!.state!.text, \"0,1\");\n    expect(infos.get(Action2)!.state!.text, \"0,1,2\");\n    expect(infos.get(Action3)!.state!.text, \"0,1,2,2,3\");\n\n    // But get is useful if some action is repeated, then you can get by type and repeating order.\n    expect(infos.get(Action1, 1)!.state!.text, \"0,1\");\n    expect(infos.get(Action2, 1)!.state!.text, \"0,1,2\");\n    expect(infos.get(Action2, 2)!.state!.text, \"0,1,2,2\");\n    expect(infos.get(Action3, 1)!.state!.text, \"0,1,2,2,3\");\n\n    // If the action is not repeated to that order, return null;\n    expect(infos.get(Action3, 2), isNull);\n    expect(infos.get(Action3, 500), isNull);\n\n    // Get repeated actions as list.\n    List<TestInfo<AppState?>?> action2s = infos.getAll(Action2);\n    expect(action2s.length, 2);\n    expect(action2s[0]!.state!.text, \"0,1,2\");\n    expect(action2s[1]!.state!.text, \"0,1,2,2\");\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in ANY order. '\n      'Ignore some actions. '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    TestInfoList<AppState?> infos = await storeTester.waitAllUnordered(\n      [Action1, Action3],\n      timeoutInSeconds: 1,\n      ignore: [Action2],\n    );\n\n    // The states are indexed by order of dispatching\n    // (doesn't matter the order we were expecting them).\n    expect(infos.length, 2);\n    expect(infos.getIndex(0).state!.text, \"0,1\");\n    expect(infos.getIndex(1).state!.text, \"0,1,2,2,3\");\n    expect(infos.getIndex(0).errors, isEmpty);\n    expect(infos.getIndex(1).errors, isEmpty);\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in order. '\n      'Ignore some actions. '\n      'Get the end state.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action4());\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action4());\n    storeTester.dispatch(Action5());\n    storeTester.dispatch(Action4());\n\n    TestInfo<AppState?> info = await (storeTester.waitAllGetLast(\n      [Action1, Action3, Action5],\n      ignore: [Action2, Action4],\n    ));\n\n    // All actions affect the state, even the ones ignored by the store-tester.\n    // However, ignored action can run any number of times.\n    expect(info.state!.text, \"0,4,1,2,2,3,4,2,4,5\");\n    expect(info.errors, isEmpty);\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in order. '\n      'Ignore some actions. '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action4());\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action4());\n    storeTester.dispatch(Action5());\n    storeTester.dispatch(Action4());\n\n    TestInfoList<AppState?> infos = await storeTester.waitAll(\n      [Action1, Action3, Action5],\n      ignore: [Action2, Action4],\n    );\n\n    // All actions affect the state, even the ones ignored by the store-tester.\n    // However, ignored action can run any number of times.\n    expect(infos.last.state!.text, \"0,4,1,2,2,3,4,2,4,5\");\n    expect(infos.last.errors, isEmpty);\n\n    // Only 3 states were collected. The ignored action doesn't generate info.\n    expect(infos.length, 3);\n    expect(infos.getIndex(0).state!.text, \"0,4,1\");\n    expect(infos.getIndex(1).state!.text, \"0,4,1,2,2,3\");\n    expect(infos.getIndex(2).state!.text, \"0,4,1,2,2,3,4,2,4,5\");\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in order. '\n      'Ignore some actions, including one which we are also waiting for it. '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n\n    // We are waiting for this Action2 (after Action1) which would otherwise be ignored.\n    storeTester.dispatch(Action2());\n\n    storeTester.dispatch(Action3());\n\n    TestInfoList<AppState?> infos = await storeTester.waitAll(\n      [Action1, Action2, Action3],\n      ignore: [Action2],\n    );\n\n    // All actions affect the state, even the ones ignored by the store-tester.\n    // However, ignored action can run any number of times.\n    expect(infos.last.state!.text, \"0,1,2,3\");\n    expect(infos.last.errors, isEmpty);\n\n    // Only 3 states were collected. The ignored action doesn't generate info.\n    expect(infos.length, 3);\n    expect(infos.getIndex(0).state!.text, \"0,1\");\n    expect(infos.getIndex(1).state!.text, \"0,1,2\");\n    expect(infos.getIndex(2).state!.text, \"0,1,2,3\");\n  });\n\n  test(\n      'Dispatch a few actions and wait for all of them, in order. '\n      'Ignore some actions, including one which we are also waiting for it. '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action4());\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n\n    // We are waiting for this Action4 (after Action3) which is otherwise ignored.\n    storeTester.dispatch(Action4());\n\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action4());\n    storeTester.dispatch(Action5());\n    storeTester.dispatch(Action4());\n\n    TestInfoList<AppState?> infos = await storeTester.waitAll(\n      [Action1, Action3, Action4, Action5],\n      ignore: [Action2, Action4],\n    );\n\n    // All actions affect the state, even the ones ignored by the store-tester.\n    // However, ignored action can run any number of times.\n    expect(infos.last.state!.text, \"0,4,1,2,2,3,4,2,4,5\");\n    expect(infos.last.errors, isEmpty);\n\n    // Only 4 states were collected. The ignored action doesn't generate info.\n    expect(infos.length, 4);\n    expect(infos.getIndex(0).state!.text, \"0,4,1\");\n    expect(infos.getIndex(1).state!.text, \"0,4,1,2,2,3\");\n    expect(infos.getIndex(2).state!.text, \"0,4,1,2,2,3,4\");\n    expect(infos.getIndex(3).state!.text, \"0,4,1,2,2,3,4,2,4,5\");\n  });\n\n  test(\n      'Dispatch a few actions, some async that dispatch others, '\n      'and wait for all of them, in order. '\n      'Ignore some actions, including one which we are also waiting for it. '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    // Action6 will dispatch actions 1, 2 and 3, and only then it will finish.\n    storeTester.dispatch(Action6());\n\n    TestInfoList<AppState?> infos = await storeTester.waitAll(\n      [Action6, Action1, Action2, Action3],\n      ignore: [Action6],\n    );\n\n    // All actions affect the state, even the ones ignored by the store-tester.\n    // However, ignored action can run any number of times.\n    expect(infos.last.state!.text, \"0,1,2,3,6\");\n    expect(infos.last.errors, isEmpty);\n\n    // Only 4 states were collected. The ignored action doesn't generate info.\n    expect(infos.length, 4);\n    expect(infos.getIndex(0).state!.text, \"0,1\");\n    expect(infos.getIndex(1).state!.text, \"0,1,2\");\n    expect(infos.getIndex(2).state!.text, \"0,1,2,3\");\n    expect(infos.getIndex(3).state!.text, \"0,1,2,3,6\");\n  });\n\n  test(\n      'Dispatch a more complex action sequence. '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    // Action6 will dispatch actions 1, 2 and 3, and only then it will finish.\n    storeTester.dispatch(Action7());\n\n    TestInfoList<AppState?> infos = await storeTester.waitAll(\n      [Action7, Action4, Action6, Action1, Action2, Action3, Action5],\n      ignore: [Action2],\n    );\n\n    // All actions affect the state, even the ones ignored by the store-tester.\n    // However, ignored action can run any number of times.\n    expect(infos.last.state!.text, \"0,4,1,2,3,6,2,5,7\");\n    expect(infos.last.errors, isEmpty);\n\n    // Only 7 states were collected. The ignored action doesn't generate info.\n    expect(infos.length, 7);\n    expect(infos.getIndex(0).state!.text, \"0,4\");\n    expect(infos.getIndex(1).state!.text, \"0,4,1\");\n    expect(infos.getIndex(2).state!.text, \"0,4,1,2\");\n    expect(infos.getIndex(3).state!.text, \"0,4,1,2,3\");\n    expect(infos.getIndex(4).state!.text, \"0,4,1,2,3,6\");\n    expect(infos.getIndex(5).state!.text, \"0,4,1,2,3,6,2,5\");\n    expect(infos.getIndex(6).state!.text, \"0,4,1,2,3,6,2,5,7\");\n  });\n\n  test(\n      'Dispatch a more complex action sequence. '\n      'One of the actions contains \"await\". '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    // Action6b will dispatch actions 1, 2 and 3, and only then it will finish.\n    storeTester.dispatch(Action7b());\n\n    TestInfoList<AppState?> infos = await storeTester.waitAll(\n      [Action7b, Action4, Action6b, Action1, Action2, Action5, Action2, Action3],\n    );\n\n    // All actions affect the state, even the ones ignored by the store-tester.\n    // However, ignored action can run any number of times.\n    expect(infos.last.state!.text, \"0,4,1,2,5,7b,2,3,6b\");\n    expect(infos.last.errors, isEmpty);\n\n    // All 8 states were collected.\n    expect(infos.length, 8);\n  });\n\n  test(\n      'Dispatch a more complex actions sequence. '\n      'One of the actions contains \"await\". '\n      'Ignore an action. '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    // Action6b will dispatch actions 1, 2 and 3, and only then it will finish.\n    storeTester.dispatch(Action7b());\n\n    TestInfoList<AppState?> infos = await storeTester.waitAll(\n      [Action7b, Action4, Action6b, Action1, Action2, Action5, Action3],\n      ignore: [Action2],\n    );\n\n    // All actions affect the state, even the ones ignored by the store-tester.\n    // However, ignored action can run any number of times.\n    expect(infos.last.state!.text, \"0,4,1,2,5,7b,2,3,6b\");\n    expect(infos.last.errors, isEmpty);\n\n    // Only 7 states were collected. The ignored action doesn't generate info.\n    expect(infos.length, 7);\n  });\n\n  test(\n      'Dispatch a more complex actions sequence. '\n      'An ignored action will finish after all others have started. '\n      'Get all of the intermediary states.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    // Action9 will dispatch Action2 after some delay.\n    storeTester.dispatch(Action8());\n\n    storeTester.dispatch(Action9());\n\n    TestInfoList<AppState?> infos = await storeTester.waitAll(\n      [\n        Action9,\n        Action2,\n      ],\n      ignore: [Action8],\n    );\n\n    // All actions affect the state, even the ones ignored by the store-tester.\n    // However, ignored action can run any number of times.\n    expect(infos.last.state!.text, \"0,2,8,9\");\n    expect(infos.last.errors, isEmpty);\n\n    // Only 2 states were collected. The ignored action doesn't generate info.\n    expect(infos.length, 2);\n    expect(infos.getIndex(0).state!.text, \"0,2\");\n    expect(infos.getIndex(1).state!.text, \"0,2,8,9\");\n  });\n\n  test(\n      'An ignored action starts after the last expected actions starts, '\n      'but before this last expected action finishes.', () async {\n    //\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n    storeTester.dispatch(Action9());\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action9());\n    storeTester.dispatch(Action1());\n\n    TestInfoList<AppState?> infos = await storeTester.waitAll(\n      [\n        Action9,\n        Action9,\n      ],\n      ignore: [Action1],\n    );\n\n    expect(infos.last.state!.text, \"0,1,1,9,9\");\n    expect(infos.last.errors, isEmpty);\n    expect(infos.length, 2);\n    expect(infos.getIndex(0).state!.text, \"0,1,1,9\");\n    expect(infos.getIndex(1).state!.text, \"0,1,1,9,9\");\n  });\n\n  // TODO: THIS ONE IS FAILING. FIX!!!\n  test(\"Wait for a sync action that dispatches an async action which is ignored.\", () async {\n    var storeTester = createStoreTester();\n\n    storeTester.dispatch(Action12());\n    storeTester.dispatch(Action12());\n\n    var infos = await storeTester.waitAll(\n      [\n        Action12,\n        Action12,\n      ],\n      ignore: [Action13],\n    );\n\n    expect(infos.getIndex(0).state.text, \"0,12\");\n    expect(infos.getIndex(1).state.text, \"0,12,12\");\n  });\n\n  test('Makes sure we wait until the END of all ignored actions.', () async {\n    //\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n    storeTester.dispatch(Action6());\n\n    expect(() async => await storeTester.waitAllGetLast([Action1, Action2], ignore: [Action6]),\n        throwsA(anything));\n  });\n\n  test('Makes sure we wait until the END of all ignored actions.', () async {\n    //\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n    storeTester.dispatch(Action6());\n    TestInfo<AppState?> info = await (storeTester.waitAllGetLast(\n      [\n        Action1,\n        Action2,\n        Action3,\n      ],\n      ignore: [Action6],\n    ));\n    expect(info.state!.text, \"0,1,2,3\");\n    expect(info.errors, isEmpty);\n\n    storeTester.dispatch(Action6());\n    info = await (storeTester.waitAllGetLast([\n      Action6,\n      Action1,\n      Action2,\n      Action3,\n    ]));\n\n    expect(info.state!.text, \"0,1,2,3,6,1,2,3,6\");\n  });\n\n  test('Makes sure we wait until the END of all ignored actions.', () async {\n    //\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n    storeTester.dispatch(Action6());\n    TestInfo<AppState?> info = await (storeTester.waitAllUnorderedGetLast(\n      [\n        Action1,\n        Action2,\n        Action3,\n      ],\n      ignore: [Action6],\n    ));\n    expect(info.state!.text, \"0,1,2,3\");\n    expect(info.errors, isEmpty);\n\n    storeTester.dispatch(Action6());\n    info = await (storeTester.waitAllGetLast([\n      Action6,\n      Action1,\n      Action2,\n      Action3,\n    ]));\n    expect(info.state!.text, \"0,1,2,3,6,1,2,3,6\");\n  });\n\n  test('Makes sure we wait until the END of all ignored actions.', () async {\n    //\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n    storeTester.dispatch(Action6());\n    TestInfo<AppState?> info = await (storeTester.waitAllUnorderedGetLast(\n      [\n        Action1,\n        Action2,\n      ],\n      ignore: [Action3, Action6],\n    ));\n    expect(info.state!.text, \"0,1,2\");\n    expect(info.errors, isEmpty);\n\n    // Now waits Action4 just to make sure Action3 hasn't leaked.\n    storeTester.dispatch(Action4());\n    info = await (storeTester.waitAllGetLast(\n      [Action4],\n    ));\n    expect(info.state!.text, \"0,1,2,3,6,4\");\n    expect(info.errors, isEmpty);\n  });\n\n  test('Makes sure we wait until the END of all ignored actions.', () async {\n    //\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n    storeTester.dispatch(Action6c());\n\n    expect(\n        () async => await storeTester\n            .waitAllUnorderedGetLast([Action1, Action2], ignore: [Action3b, Action6c]),\n        throwsA(StoreException(\"Got this unexpected action: Action4 INI.\")));\n  });\n\n  test('Error message when time is out.', () async {\n    //\n    var storeTester = createStoreTester();\n    await storeTester.waitAllUnordered([Action1], timeoutInSeconds: 1).then((_) {\n      fail('There was no timeout.');\n      return null; // ignore: dead_code\n    }, onError: expectAsync1((dynamic error) {\n      expect(error, StoreExceptionTimeout());\n    }));\n  });\n\n  //\n\n  test(\n      'An action dispatches other actions, and one of them throws an error. '\n      'Wait until that action finishes, '\n      'and check the error.', () async {\n    var storeTester = createStoreTester();\n\n    runZonedGuarded(() {\n      storeTester.dispatch(Action10a());\n    }, (error, stackTrace) {\n      expect(error, const UserException(\"Hello!\"));\n    });\n\n    TestInfo<AppState?> info = await storeTester.waitUntil(Action11a);\n    expect(info.error, const UserException(\"Hello!\"));\n    expect(info.processedError, null);\n    expect(info.state!.text, \"0,1,2\");\n    expect(info.ini, false);\n  });\n\n  test(\n      'An action dispatches other actions, and one of them throws an error. '\n      'Wait until that action finishes, '\n      'and check the error.', () async {\n    var storeTester = createStoreTester();\n\n    runZonedGuarded(() {\n      storeTester.dispatch(Action10b());\n    }, (error, stackTrace) {\n      expect(error, const UserException(\"Hello!\"));\n    });\n\n    TestInfo<AppState?> info = await storeTester.waitUntil(Action11b);\n    expect(info.error, const UserException(\"Hello!\"));\n    expect(info.processedError, null);\n    expect(info.state!.text, \"0,1,2\");\n    expect(info.ini, false);\n  });\n\n  test(\n      'An action dispatches other actions, and one of them throws an error. '\n      'Wait until that action finishes, '\n      'and check the error.', () async {\n    var storeTester = createStoreTester();\n\n    runZonedGuarded(() {\n      storeTester.dispatch(Action10c());\n    }, (error, stackTrace) {\n      expect(error, const UserException(\"Hello!\"));\n    });\n\n    TestInfo<AppState?> info = await storeTester.waitUntil(Action11b);\n    expect(info.error, const UserException(\"Hello!\"));\n    expect(info.processedError, null);\n    expect(info.state!.text, \"0,1,2,3\");\n    expect(info.ini, false);\n  });\n\n  test(\n      'An action dispatches other actions, and one of them '\n      '(a sync one) throws an error. '\n      'Wait until the error TYPE is thrown, '\n      'and check the error.', () async {\n    var storeTester = createStoreTester();\n\n    runZonedGuarded(() {\n      storeTester.dispatch(Action10a());\n    }, (error, stackTrace) {});\n\n    TestInfo<AppState?> info = await (storeTester.waitUntilErrorGetLast(\n      error: UserException,\n      timeoutInSeconds: 1,\n    ));\n\n    expect(info.error, const UserException(\"Hello!\"));\n    expect(info.processedError, null);\n    expect(info.state!.text, \"0,1,2\");\n    expect(info.ini, false);\n  });\n\n  test(\n      'An action dispatches other actions, and one of them '\n      '(an async one) throws an error. '\n      'Wait until the error (compare using equals) is thrown, '\n      'and check the error.', () async {\n    var storeTester = createStoreTester();\n\n    runZonedGuarded(() {\n      storeTester.dispatch(Action10a());\n    }, (error, stackTrace) {});\n\n    TestInfo<AppState?> info = await (storeTester.waitUntilErrorGetLast(\n      error: const UserException(\"Hello!\"),\n      timeoutInSeconds: 1,\n    ));\n\n    expect(info.error, const UserException(\"Hello!\"));\n    expect(info.processedError, null);\n    expect(info.state!.text, \"0,1,2\");\n    expect(info.ini, false);\n  });\n\n  test('The lastInfo can be accessed through StoreTester.lastInfo.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n\n    var condition = (TestInfo<AppState?>? info) => info!.state!.text == \"0,1,2\";\n    await storeTester.waitConditionGetLast(condition);\n\n    // Same as expect(info1.state.text, \"0,1,2\");\n    expect(storeTester.lastInfo.state.text, \"0,1,2\");\n\n    // Same as expect(info1.ini, false);\n    expect(storeTester.lastInfo.ini, false);\n\n    await storeTester.waitConditionGetLast((info) => info.state.text == \"0,1,2,3,4\");\n\n    // Same as expect(info2.state.text, \"0,1,2,3,4\");\n    expect(storeTester.lastInfo.state.text, \"0,1,2,3,4\");\n\n    // Same as expect(info1.ini, false);\n    expect(storeTester.lastInfo.ini, false);\n  });\n\n  test('Wait condition with testImmediately true/false.', () async {\n    // ---\n\n    // 1) If testImmediately=false, it should timeout, because it will wait until an Action\n    // is dispatched, and after that it's not \"0\" anymore.\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n\n    await storeTester\n        .waitConditionGetLast((info) => info.state.text == \"0\",\n            testImmediately: false, timeoutInSeconds: 1)\n        .then((_) {\n      throw AssertionError();\n      return null; // ignore: dead_code\n    }, onError: expectAsync1((Object error) {\n      expect(error, const TypeMatcher<StoreException>());\n      expect(error.toString(), \"Timeout.\");\n    }));\n\n    expect(storeTester.state.text, \"0,1,2,3,4\");\n\n    // ---\n\n    // 2) If testImmediately=true, it should work, because it will test before any Action\n    // is dispatched, and that's already \"0\".\n    storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action3());\n    storeTester.dispatch(Action4());\n\n    TestInfo<AppState?> info = await (storeTester\n        .waitConditionGetLast((info) => info.state.text == \"0\", timeoutInSeconds: 1));\n\n    expect(info.state!.text, \"0\");\n    expect(storeTester.state.text, \"0,1,2,3,4\");\n\n    // ---\n\n    // 3) Let's see if the current testInfo is kept.\n    info = await (storeTester.waitConditionGetLast((info) => info.state.text == \"0,1,2,3,4\",\n        timeoutInSeconds: 1));\n\n    expect(info.state!.text, \"0,1,2,3,4\");\n    expect(storeTester.state.text, \"0,1,2,3,4\");\n\n    // ---\n\n    // 4) Let's see if the current testInfo is kept.\n    storeTester.dispatch(Action5());\n\n    info = await (storeTester.waitConditionGetLast((info) => info.state.text == \"0,1,2,3,4\",\n        timeoutInSeconds: 1));\n\n    expect(info.state!.text, \"0,1,2,3,4\");\n    expect(storeTester.state.text, \"0,1,2,3,4,5\");\n  });\n\n  test(\n      \"Wait condition with testImmediately true \"\n      \"should not see the action of previous test-infos.\", () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n\n    TestInfoList<AppState?> infos = await storeTester.waitCondition(\n      (info) => info.state.text == \"0,1\",\n      testImmediately: true,\n    );\n\n    expect(infos[Action1]!.action, isA<Action1>());\n    expect(storeTester.currentTestInfo.action, isA<Action1>());\n\n    infos = await storeTester.waitCondition(\n      (info) {\n        if (info.action is Action1) throw AssertionError();\n        return true;\n      },\n      testImmediately: true,\n    );\n  });\n\n  test(\n      \"Wait condition with testImmediately true \"\n      \"should not see the action of previous test-infos (a more realistic test).\", () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n\n    TestInfoList<AppState?> infos = await storeTester.waitCondition(\n      (info) => info.state.text == \"0,1\",\n      testImmediately: true,\n    );\n\n    expect(infos, hasLength(1));\n    expect(infos[Action1]!.state!.text, \"0,1\");\n    expect(storeTester.currentTestInfo.action, isA<Action1>());\n    expect(storeTester.currentTestInfo.state.text, \"0,1\");\n\n    storeTester.dispatch(Action2());\n    storeTester.dispatch(Action1());\n\n    bool hasDispatchedAction1 = false;\n\n    infos = await storeTester.waitCondition(\n      (info) {\n        if (info.action is Action1) hasDispatchedAction1 = true;\n        return hasDispatchedAction1 && info.state.text.contains(\",2\");\n      },\n      testImmediately: true,\n    );\n\n    expect(infos, hasLength(2));\n    expect(infos[Action2]!.state!.text, \"0,1,2\");\n    expect(infos[Action1]!.state!.text, \"0,1,2,1\");\n  });\n\n  test('Two simultaneous store testers will receive the same state changes.', () async {\n    var storeTester1 = createStoreTester();\n    var storeTester2 = StoreTester.from(storeTester1.store);\n\n    expect(storeTester1.state.text, \"0\");\n    expect(storeTester2.state.text, \"0\");\n\n    storeTester1.dispatch(Action1());\n    storeTester1.dispatch(Action2());\n    storeTester1.dispatch(Action3());\n    storeTester1.dispatch(Action4());\n\n    TestInfo<AppState?> info1 = await (storeTester1\n        .waitConditionGetLast((info) => info.state.text == \"0,1,2,3\", timeoutInSeconds: 1));\n\n    TestInfo<AppState?> info2 = await (storeTester2\n        .waitConditionGetLast((info) => info.state.text == \"0,1\", timeoutInSeconds: 1));\n\n    expect(info1.state!.text, \"0,1,2,3\");\n    expect(info2.state!.text, \"0,1\");\n    expect(storeTester1.state.text, \"0,1,2,3,4\");\n    expect(storeTester2.state.text, \"0,1,2,3,4\");\n  });\n\n  test('StoreTester.dispatchState.', () async {\n    var storeTester = createStoreTester();\n    expect(storeTester.state.text, \"0\");\n\n    storeTester.dispatch(Action1());\n    storeTester.dispatch(Action2());\n\n    // Remove state \"1\" from the stream, but not state \"2\".\n    await storeTester.waitUntil(Action1);\n    expect(storeTester.lastInfo.state.text, \"0,1\");\n    expect(storeTester.state.text, \"0,1,2\");\n\n    // When we dispatchState, it empties the stream.\n    // This means state \"2\" will be removed.\n    await storeTester.dispatchState(AppState(\"my state\"));\n    expect(storeTester.lastInfo.state.text, \"my state\");\n    expect(storeTester.state.text, \"my state\");\n    storeTester.dispatch(Action3());\n    expect(storeTester.state.text, \"my state,3\");\n    expect(storeTester.lastInfo.state.text, \"my state\");\n\n    await storeTester.waitUntil(Action3);\n    expect(storeTester.lastInfo.state.text, \"my state,3\");\n    expect(storeTester.state.text, \"my state,3\");\n  });\n}\n"
  },
  {
    "path": "test/store_wait_action_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var feature = BddFeature('Store wait action');\n\n  test('waitCondition', () async {\n    // Returns a future that completes when the state is in the given condition.\n    // Since the state is already in the condition, the future completes immediately.\n    var store = Store<State>(initialState: State(1));\n    await store.waitCondition((state) => state.count == 1);\n\n    // If we disallow the future to complete immediately, it will throw a TimeoutException.\n    store = Store<State>(initialState: State(1));\n    expect(() => store.waitCondition((state) => state.count == 1, completeImmediately: false),\n        throwsA(isA<StoreException>()));\n\n    // The state is NEVER in the condition, but the timeout will end it.\n    await expectLater(\n      () {\n        print('state = ${store.state}');\n        return store.waitCondition((state) => state.count == 2, timeoutMillis: 10);\n      },\n      throwsA(isA<TimeoutException>()),\n    );\n\n    // An ASYNC action will put the state in the condition, after a while.\n    store = Store<State>(initialState: State(1));\n    store.dispatch(IncrementActionAsync());\n    await store.waitCondition((state) => state.count == 2);\n\n    // A SYNC action will put the state in the condition, before the condition is created.\n    store = Store<State>(initialState: State(1));\n    store.dispatch(IncrementAction());\n    expect(store.state.count, 2);\n    await store.waitCondition((state) => state.count == 2);\n\n    // A Future will dispatch a SYNC action that puts the state in the condition.\n    store = Store<State>(initialState: State(1));\n    Future(() => store.dispatch(IncrementAction()));\n    expect(store.state.count, 1);\n    await store.waitCondition((state) => state.count == 2);\n    expect(store.state.count, 2);\n\n    // A Future will dispatch a SYNC action that puts the state in the condition, after a while.\n    store = Store<State>(initialState: State(1));\n    Future.delayed(const Duration(milliseconds: 50), () => store.dispatch(IncrementAction()));\n    expect(store.state.count, 1);\n    await store.waitCondition((state) => state.count == 2);\n    expect(store.state.count, 2);\n  });\n\n  test('waitAllActions', () async {\n    // Returns a future that completes when no actions are in progress.\n    // Since no actions are currently in progress, the future completes immediately.\n    // We are ACCEPTING futures completed immediately.\n    var store = Store<State>(initialState: State(1));\n    await store.waitAllActions([], completeImmediately: true);\n\n    // Returns a future that completes when no actions are in progress.\n    // Since no actions are currently in progress, the future completes immediately.\n    // We are NOT accepting futures completed immediately: should throw a StoreException.\n    store = Store<State>(initialState: State(1));\n    await expectLater(\n      () => store.waitAllActions([]),\n      throwsA(isA<StoreException>()),\n    );\n\n    // Returns a future that completes when no actions are in progress.\n    // There is an actions is progress.\n    store = Store<State>(initialState: State(1));\n    store.dispatchAndWait(DelayedAction(1, delayMillis: 1));\n    expect(store.state.count, 1);\n    await store.waitAllActions([]);\n    expect(store.state.count, 2);\n  });\n\n  test('waitActionType', () async {\n    // Returns a future that completes when the actions of the given type is NOT in progress.\n    // Since no actions are currently in progress, the future completes immediately.\n    var store = Store<State>(initialState: State(1));\n    await store.waitActionType(DelayedAction, completeImmediately: true);\n\n    // Again, since no actions are currently in progress, the future completes immediately.\n    // The timeout is irrelevant.\n    store = Store<State>(initialState: State(1));\n    await store.waitActionType(DelayedAction, timeoutMillis: 1, completeImmediately: true);\n\n    // An actions of the given type is in progress.\n    // But then the action ends.\n    store = Store<State>(initialState: State(1));\n    store.dispatch(DelayedAction(1, delayMillis: 10));\n    await store.waitActionType(DelayedAction);\n\n    // An actions of the given type is in progress.\n    // But the wait will timeout.\n    store = Store<State>(initialState: State(1));\n    store.dispatch(DelayedAction(1, delayMillis: 1000));\n    await expectLater(\n      () => store.waitActionType(DelayedAction, timeoutMillis: 1),\n      throwsA(isA<TimeoutException>()),\n    );\n  });\n\n  test('waitAllActionTypes', () async {\n    // Returns a future that completes when ALL actions of the given type are NOT in progress.\n    // Since no actions are currently in progress, the future completes immediately.\n    var store = Store<State>(initialState: State(1));\n    store.dispatch(DelayedAction(1, delayMillis: 10));\n    store.waitAllActionTypes([DelayedAction, AnotherDelayedAction]);\n\n    // An actions of the given type is in progress.\n    // But then the action ends.\n    store = Store<State>(initialState: State(1));\n    store.dispatch(DelayedAction(1, delayMillis: 10));\n    store.waitAllActionTypes([DelayedAction, AnotherDelayedAction]);\n\n    // ---\n\n    // An actions of the given type is in progress.\n    // But the wait will timeout.\n    store = Store<State>(initialState: State(1));\n    store.dispatch(DelayedAction(1, delayMillis: 1000));\n\n    dynamic error;\n    try {\n      await store.waitAllActionTypes([DelayedAction, AnotherDelayedAction], timeoutMillis: 10);\n    } catch (_error) {\n      error = _error;\n    }\n    expect(error, isA<TimeoutException>());\n  });\n\n  test('waitActionCondition', () async {\n    // Returns a future that completes when the actions of the given type that are in progress\n    // meet the given condition. Since no actions are currently in progress, and we're checking\n    // to see if there are no actions in progress, the future completes immediately.\n    var store = Store<State>(initialState: State(1));\n    await store.waitActionCondition((actions, triggerAction) => actions.isEmpty,\n        completeImmediately: true);\n  });\n\n  test('waitAnyActionTypeFinishes', () async {\n    // Returns a future that completes when ANY action of the given types finish after the\n    // method is called. We start an action before calling the method, then call the method.\n    // As soon as the action finishes, the future completes.\n    var store = Store<State>(initialState: State(1));\n    store.dispatch(DelayedAction(1, delayMillis: 10));\n    ReduxAction<State> action =\n        await store.waitAnyActionTypeFinishes([DelayedAction], timeoutMillis: 2000);\n    expect(action, isA<DelayedAction>());\n    expect(action.status.isCompletedOk, true);\n\n    // ---\n\n    // Returns a future that completes when an action of ANY of the given types finish after\n    // the method is called. We start an action before calling the method, then call the method.\n    // As soon as the action finishes, the future completes.\n    store = Store<State>(initialState: State(1));\n\n    dynamic error;\n    try {\n      await store.waitAnyActionTypeFinishes([DelayedAction], timeoutMillis: 10);\n    } catch (_error) {\n      error = _error;\n    }\n    expect(error, isA<TimeoutException>());\n  });\n\n  Bdd(feature)\n      .scenario('We dispatch no actions and wait for all to finish.')\n      .given('No actions are dispatched.')\n      .when('We wait until no actions are dispatched.')\n      .then('The code continues immediately.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    await store.waitAllActions([], completeImmediately: true);\n    expect(store.state.count, 1);\n  });\n\n  Bdd(feature)\n      .scenario('We dispatch async actions and wait for all to finish.')\n      .given('Three ASYNC actions.')\n      .when('The actions are dispatched in PARALLEL.')\n      .and('We wait until NO ACTIONS are being dispatched.')\n      .then('After we wait, all actions finished.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n\n    store.dispatch(DelayedAction(10, delayMillis: 50));\n    store.dispatch(AnotherDelayedAction(100, delayMillis: 100));\n    store.dispatch(DelayedAction(1000, delayMillis: 20));\n\n    expect(store.state.count, 1);\n    await store.waitAllActions([]);\n    expect(store.state.count, 1 + 10 + 100 + 1000);\n  });\n\n  Bdd(feature)\n      .scenario('We dispatch an async action and wait for its action TYPE to finish.')\n      .given('An ASYNC actions.')\n      .when('The action is dispatched.')\n      .then('We wait until its type finished dispatching.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n\n    store.dispatch(AnotherDelayedAction(123, delayMillis: 100));\n    store.dispatch(DelayedAction(1000, delayMillis: 10));\n\n    expect(store.state.count, 1);\n    await store.waitActionType(DelayedAction, timeoutMillis: 2000);\n    expect(store.state.count, 1001);\n    await store.waitActionType(AnotherDelayedAction, timeoutMillis: 2000);\n    expect(store.state.count, 1124);\n  });\n\n  Bdd(feature)\n      .scenario('We dispatch async actions and wait for some action TYPES to finish.')\n      .given('Four ASYNC actions.')\n      .and('The fourth takes longer than an others to finish.')\n      .when('The actions are dispatched in PARALLEL.')\n      .and('We wait until there the types of the faster 3 finished dispatching.')\n      .then('After we wait, the 3 actions finished, and the fourth did not.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n\n    store.dispatch(DelayedAction(10, delayMillis: 50));\n    store.dispatch(AnotherDelayedAction(100, delayMillis: 100));\n    store.dispatch(YetAnotherDelayedAction(100000, delayMillis: 200));\n    store.dispatch(DelayedAction(1000, delayMillis: 10));\n\n    expect(store.state.count, 1);\n    await store.waitAllActionTypes([DelayedAction, AnotherDelayedAction], timeoutMillis: 2000);\n    expect(store.state.count, 1 + 10 + 100 + 1000);\n  });\n\n  Bdd(feature)\n      .scenario('We dispatch async actions and wait for some of them to finish.')\n      .given('Four ASYNC actions.')\n      .and('The fourth takes longer than an others to finish.')\n      .when('The actions are dispatched in PARALLEL.')\n      .and('We wait until there the faster 3 finished dispatching.')\n      .then('After we wait, the 3 actions finished, and the fourth did not.')\n      .run((_) async {\n    final store = Store<State>(initialState: State(1));\n\n    expect(store.state.count, 1);\n\n    var action50 = DelayedAction(10, delayMillis: 50);\n    var action100 = AnotherDelayedAction(100, delayMillis: 100);\n    var action200 = YetAnotherDelayedAction(100000, delayMillis: 200);\n    var action10 = DelayedAction(1000, delayMillis: 10);\n\n    store.dispatch(action50);\n    store.dispatch(action100);\n    store.dispatch(action200); // We don't wait for this one.\n    store.dispatch(action10);\n\n    expect(store.state.count, 1);\n    await store.waitAllActions([action50, action100, action10]);\n    expect(store.state.count, 1 + 10 + 100 + 1000);\n  });\n}\n\nclass State {\n  final int count;\n\n  State(this.count);\n\n  @override\n  String toString() {\n    return 'State($count)';\n  }\n}\n\nclass IncrementAction extends ReduxAction<State> {\n  @override\n  State reduce() => State(state.count + 1);\n}\n\nclass IncrementActionAsync extends ReduxAction<State> {\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 10));\n    return State(state.count + 1);\n  }\n}\n\nclass DelayedAction extends ReduxAction<State> {\n  final int increment;\n  final int delayMillis;\n\n  DelayedAction(this.increment, {required this.delayMillis});\n\n  @override\n  Future<State> reduce() async {\n    await Future.delayed(Duration(milliseconds: delayMillis));\n    return State(state.count + increment);\n  }\n}\n\nclass AnotherDelayedAction extends DelayedAction {\n  AnotherDelayedAction(int increment, {required int delayMillis})\n      : super(increment, delayMillis: delayMillis);\n}\n\nclass YetAnotherDelayedAction extends DelayedAction {\n  YetAnotherDelayedAction(int increment, {required int delayMillis})\n      : super(increment, delayMillis: delayMillis);\n}\n"
  },
  {
    "path": "test/store_wrap_reduce_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n@immutable\nclass AppState {\n  final int count;\n\n  AppState({this.count = 0});\n\n  AppState copy({int? count}) => AppState(count: count ?? this.count);\n}\n\nclass _SyncAction extends ReduxAction<AppState> {\n  static int count = 0;\n\n  @override\n  AppState reduce() {\n    count++;\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass _AsyncAction extends ReduxAction<AppState> {\n  static int count = 0;\n\n  @override\n  Future<AppState> reduce() async {\n    await microtask;\n    count++;\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass _TestWrapReduce extends WrapReduce<AppState> {\n  @override\n  AppState process({required oldState, required newState}) => newState;\n}\n\nvoid main() {\n  late Store<AppState> store;\n\n  setUp(() async {\n    store = Store<AppState>(\n      initialState: AppState(),\n      wrapReduce: _TestWrapReduce(),\n    );\n  });\n\n  group(WrapReduce, () {\n    test(\"Only reduces sync reducer once.\", () async {\n      expect(store.state.count, 0);\n      await store.dispatch(_SyncAction());\n      expect(_SyncAction.count, 1);\n      expect(store.state.count, 1);\n    });\n    test(\"Only reduces async reducer once.\", () async {\n      expect(store.state.count, 0);\n      await store.dispatch(_AsyncAction());\n      expect(_AsyncAction.count, 1);\n      expect(store.state.count, 1);\n    });\n  });\n}\n"
  },
  {
    "path": "test/sync_async_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n// Developed by Marcelo Glasberg (2019) https://glasberg.dev and https://github.com/marcglasberg\n// For more info: https://asyncredux.com AND https://pub.dev/packages/async_redux\n\n/// This tests show that both sync and async reducers work as they should.\n/// Async reducers work as long as we return uncompleted futures.\n/// https://www.woolha.com/articles/dart-event-loop-microtask-event-queue\n/// https://steemit.com/utopian-io/@tensor/the-fundamentals-of-zones-microtasks-and-event-loops-in-the-dart-programming-language-dart-tutorial-part-3\n\nvoid main() {\n  test(\n      'This tests the mechanism of a SYNC Reducer: '\n      'The reducer changes the state to A, and it will later be changed to B. '\n      'It works if the reducer returns `AppState`.', () async {\n    //\n    var state = \"\";\n\n    String reduce() {\n      Future.microtask(() => state += \"A\");\n      return \"B\";\n    }\n\n    /// There is no 'A' after calling the reducer, because it ran SYNC.\n    state = reduce();\n    expect(state, \"B\");\n\n    /// After a microtask, 'A' appears.\n    await Future.microtask(() {});\n    expect(state, \"BA\");\n  });\n\n  test(\n      'This tests the mechanism of a ASYNC Reducer: '\n      'The reducer changes the state to A, and it will later be changed to B. '\n      'It works if the reducer returns a `Future<AppState>` and contains the `await` keyword. '\n      'This works because the `then` is called synchronously after the `return`.', () async {\n    //\n    var state = \"\";\n\n    Future<String> reduce() async {\n      state += \"A\";\n      Future.microtask(() => state += \"B\");\n      state += \"C\";\n      await Future.microtask(() {});\n      state += \"D\";\n      Future.microtask(() => state += \"E\");\n      return \"F\";\n    }\n\n    /// There is no 'E' yet, because even if the reducer is async,\n    /// the then() method ran SYNC after the value was returned.\n    await reduce().then((newState) => state += newState);\n    expect(state, \"ACBDF\");\n\n    /// After a microtask, 'E' appears.\n    await Future.microtask(() {});\n    expect(state, \"ACBDFE\");\n  });\n\n  test(\n      \"Tests what happens when we do it wrong, and return COMPLETED Futures.\"\n      \"The reducer changes the state to A, and it should later be changed to B.\"\n      \"It fails if the reducer returns a Future<AppState> and it does NOT contain the await keyword.\"\n      \"If the reducer does NOT contain the `await` keyword, it means it was created as a completed Future.\"\n      \"In this case, Dart schedules the `then` for the next microtask\"\n      \"(see why here: https://github.com/dart-lang/sdk/issues/14323).\"\n      \"In other words, in this case `then` is called asynchronously, one microtask after the `return`.\"\n      \"If some other process changes the state in that exact microtask the state change may be lost.\"\n      \"We don't allow this to happen because we check the reducer signature, and if it returns\"\n      \"a Future we force it to wait for the next microtask. \"\n      \"In other words, we make sure the future is uncompleted.\", () async {\n    //\n    var state = \"\";\n\n    Future<String> reduce() async {\n      Future.microtask(() => state += \"B\");\n      return \"A\";\n    }\n\n    // The reducer returned 'A', but the microtask that adds 'B'\n    // ran before the then() method had the chance to run.\n    await reduce().then((newState) => state += newState);\n    expect(state, \"BA\");\n\n    // It's all finished by now, nothing yet to run.\n    await Future.microtask(() {});\n    expect(state, \"BA\");\n  });\n\n  test(\n      '1) A sync reducer is called, '\n      'and no actions are dispatched inside of the reducer. '\n      'It acts as a pure function, just like a regular reducer of \"vanilla\" Redux.', () async {\n    states = [];\n    var storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n    expect(storeTester.state.text, 'A');\n    storeTester.dispatch(Action1B());\n    TestInfo<AppState?> info = await (storeTester.waitAllUnorderedGetLast([Action1B]));\n    expect(states, [AppState('A')]);\n    expect(info.state!.text, 'AB');\n  });\n\n  test(\n      '2) A sync reducer is called, '\n      'which dispatches another sync action. '\n      'They are both executed synchronously.', () async {\n    states = [];\n    var storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n    expect(storeTester.state.text, 'A');\n    storeTester.dispatch(Action2B());\n    TestInfo<AppState?> info = await (storeTester.waitAllUnorderedGetLast([Action2B, Action2C]));\n    expect(states, [AppState('A'), AppState('AC')]);\n    expect(info.state!.text, 'ACB');\n  });\n\n  test(\n      '3) A sync reducer is called, '\n      'which dispatches an ASYNC action.', () async {\n    states = [];\n    var storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n    expect(storeTester.state.text, 'A');\n    storeTester.dispatch(Action3B());\n    TestInfo<AppState?> info = await (storeTester.waitAllUnorderedGetLast([Action3B, Action3C]));\n    expect(states, [AppState('A'), AppState('A')]);\n    expect(info.state!.text, 'ABC');\n  });\n\n  test(\n      '4) An ASYNC reducer is called, '\n      'which dispatches another ASYNC action. '\n      'The second reducer finishes BEFORE the first.', () async {\n    states = [];\n    var storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n    expect(storeTester.state.text, 'A');\n    storeTester.dispatch(Action4B());\n    TestInfo<AppState?> info = await (storeTester.waitAllUnorderedGetLast([Action4B, Action4C]));\n    expect(states, [AppState('A'), AppState('A'), AppState('A'), AppState('AC')]);\n    expect(info.state!.text, 'ACB');\n  });\n\n  test(\n      '5) An ASYNC reducer is called, '\n      'which dispatches another ASYNC action. '\n      'The second reducer finishes AFTER the first.', () async {\n    states = [];\n    var storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n    expect(storeTester.state.text, 'A');\n    storeTester.dispatch(Action5B());\n    TestInfo<AppState?> info = await (storeTester.waitAllUnorderedGetLast([Action5B, Action5C]));\n    expect(states, [AppState('A'), AppState('A'), AppState('A'), AppState('A')]);\n    expect(info.state!.text, 'ABC');\n  });\n\n  test(\n      \"This tests the mechanism of ASYNC Reducers: \"\n      \"1) Completed then Completed = state gets swallowed. \"\n      \"2) Completed then Uncompleted = wrong, but works because of order. \"\n      \"3) Uncompleted then Completed = state gets swallowed. \"\n      \"4) Uncompleted then Uncompleted = correct and works. \"\n      \"Note: \"\n      \"* An async reducer that returns a COMPLETED future's will:\"\n      \"   - Apply the state in the very next microtask after it is dispatched.\"\n      \"   - Apply the state in the very next microtask after the reducer returned (which is bad).\"\n      \"* An async reducer that returns an UNCOMPLETED future's will:\"\n      \"   - Apply the state in the very next microtask after it is dispatched, or after that.\"\n      \"   - Apply the state in the SAME microtask when the reducer returned (which is good).\",\n      () async {\n    //\n    // 1) Completed then Completed = state gets swallowed.\n    var storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n    expect(storeTester.state.text, 'A');\n    storeTester.dispatch(Action6ACompleted());\n    storeTester.dispatch(Action6BCompleted());\n    var info = await (storeTester.waitAllUnordered([Action6ACompleted, Action6BCompleted]));\n    expect(info.first.action.runtimeType, Action6ACompleted);\n    expect(info.last.action.runtimeType, Action6BCompleted);\n    expect(info.first.state.text, 'AX');\n    expect(info.last.state.text, 'A'); // The X was swallowed.\n\n    // 2) Completed then Uncompleted = wrong, but works because of order.\n    storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n    expect(storeTester.state.text, 'A');\n    storeTester.dispatch(Action6ACompleted());\n    storeTester.dispatch(Action6BUncompleted());\n    info = await storeTester.waitAllUnordered([Action6ACompleted, Action6BUncompleted]);\n    expect(info.first.action.runtimeType, Action6ACompleted);\n    expect(info.last.action.runtimeType, Action6BUncompleted);\n    expect(info.first.state.text, 'AX');\n    expect(info.last.state.text, 'AX');\n\n    // 3) Uncompleted then Completed = state gets swallowed.\n    storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n    expect(storeTester.state.text, 'A');\n    storeTester.dispatch(Action6AUncompleted());\n    storeTester.dispatch(Action6BCompleted());\n    info = await storeTester.waitAllUnordered([Action6AUncompleted, Action6BCompleted]);\n    expect(info.first.action.runtimeType, Action6AUncompleted);\n    expect(info.last.action.runtimeType, Action6BCompleted);\n    expect(info.first.state.text, 'AX');\n    expect(info.last.state.text, 'A'); // The X was swallowed.\n\n    // 4) Uncompleted then Uncompleted = correct and works.\n    storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n    expect(storeTester.state.text, 'A');\n    storeTester.dispatch(Action6AUncompleted());\n    storeTester.dispatch(Action6BUncompleted());\n    info = await storeTester.waitAllUnordered([Action6AUncompleted, Action6BUncompleted]);\n    expect(info.first.action.runtimeType, Action6AUncompleted);\n    expect(info.last.action.runtimeType, Action6BUncompleted);\n    expect(info.first.state.text, 'AX');\n    expect(info.last.state.text, 'AX');\n  });\n\n  test(\n      \"Test that if you add method assertUncompletedFuture() to the end of reducers, \"\n      \"it's capable of detecting completed futures.\", () async {\n    //\n    var storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n\n    // ---\n\n    dynamic error1 = \"\";\n\n    runZonedGuarded(() async {\n      storeTester.dispatch(Action7Completed());\n    }, (_error, stackTrace) {\n      error1 = _error;\n    });\n\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    expect(error1.toString(), contains(\"This may result in state changes being lost\"));\n\n    // ---\n\n    dynamic error2 = \"\";\n\n    runZonedGuarded(() async {\n      storeTester.dispatch(Action7Uncompleted());\n    }, (_error, stackTrace) {\n      error2 = _error;\n    });\n\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    expect(error2, \"\");\n  });\n\n  test(\n      \"Test that dispatching a sync action works just the same as calling a sync function, \"\n      \"and dispatching an async action works just the same as calling an async function.\",\n      () async {\n    //\n    var storeTester = StoreTester<AppState>(initialState: AppState.initialState());\n\n    // ---\n\n    states = [];\n\n    Future<void> asyncFunction() async {\n      states.add(AppState('f1'));\n      await Future.microtask(() {});\n      states.add(AppState('f2'));\n    }\n\n    /// The below code will print: 1 3 5 2 4 6\n\n    states.add(AppState('BEFORE'));\n    storeTester.dispatch(MyAsyncAction());\n    asyncFunction();\n    states.add(AppState('AFTER'));\n\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    expect(states, [\n      AppState('BEFORE'),\n      AppState('a1'),\n      AppState('f1'),\n      AppState('AFTER'),\n      AppState('a2'),\n      AppState('f2'),\n    ]);\n  });\n}\n\n// ----------------------------------------------\n\n/// The app state, which in this case is just a text.\n@immutable\nclass AppState {\n  final String text;\n\n  AppState(this.text);\n\n  AppState copy(String? text) => AppState(text ?? this.text);\n\n  static AppState initialState() => AppState('A');\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is AppState && runtimeType == other.runtimeType && text == other.text;\n\n  @override\n  int get hashCode => text.hashCode;\n\n  @override\n  String toString() => text.toString();\n}\n\nlate List<AppState?> states;\n\n// ----------------------------------------------\n\nclass Action1B extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    states.add(state);\n    return state.copy(state.text + 'B');\n  }\n}\n\n// ----------------------------------------------\n\nclass Action2B extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    states.add(state);\n    dispatch(Action2C());\n    states.add(state);\n    return state.copy(state.text + 'B');\n  }\n}\n\nclass Action2C extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    return state.copy(state.text + 'C');\n  }\n}\n\n// ----------------------------------------------\n\nclass Action3B extends ReduxAction<AppState> {\n  @override\n  AppState reduce() {\n    states.add(state);\n    dispatch(Action3C());\n    states.add(state);\n    return state.copy(state.text + 'B');\n  }\n}\n\nclass Action3C extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    await Future.microtask(() {});\n    return state.copy(state.text + 'C');\n  }\n}\n\n// ----------------------------------------------\n\nclass Action4B extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    states.add(state);\n    await Future.delayed(const Duration(milliseconds: 100));\n    states.add(state);\n    dispatch(Action4C());\n    states.add(state);\n    await Future.delayed(const Duration(milliseconds: 200));\n    states.add(state);\n    return state.copy(state.text + 'B');\n  }\n}\n\nclass Action4C extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    return state.copy(state.text + 'C');\n  }\n}\n\n// ----------------------------------------------\n\nclass Action5B extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    states.add(state);\n    await Future.delayed(const Duration(milliseconds: 100));\n    states.add(state);\n    dispatch(Action5C());\n    states.add(state);\n    await Future.delayed(const Duration(milliseconds: 50));\n    states.add(state);\n    return state.copy(state.text + 'B');\n  }\n}\n\nclass Action5C extends ReduxAction<AppState> {\n  @override\n  Future<AppState> reduce() async {\n    await Future.delayed(const Duration(milliseconds: 200));\n    return state.copy(state.text + 'C');\n  }\n}\n\n// ----------------------------------------------\n\nclass Action6ACompleted extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async => state.copy(state.text + 'X');\n}\n\nclass Action6BCompleted extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async => state;\n}\n\nclass Action6AUncompleted extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await microtask;\n    return state.copy(state.text + 'X');\n  }\n}\n\nclass Action6BUncompleted extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await microtask;\n    return state;\n  }\n}\n\n// ----------------------------------------------\n\nclass Action7Completed extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    assertUncompletedFuture();\n    return state;\n  }\n}\n\nclass Action7Uncompleted extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    await microtask;\n    assertUncompletedFuture();\n    return state;\n  }\n}\n\n// ----------------------------------------------\n\nclass MyAsyncAction extends ReduxAction<AppState> {\n  @override\n  Future<AppState?> reduce() async {\n    states.add(AppState('a1'));\n    await microtask;\n    states.add(AppState('a2'));\n    return state;\n  }\n}\n\n// ----------------------------------------------\n"
  },
  {
    "path": "test/test_utils.dart",
    "content": "import 'dart:io';\n\n/// Do not run on CI environments, like GitHub Actions.\nbool get isCI => Platform.environment.containsKey('CI');\n"
  },
  {
    "path": "test/throttle_mixin_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:bdd_framework/bdd_framework.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var feature = BddFeature('Throttle actions');\n\n  Bdd(feature)\n      .scenario('Action is throttled when dispatched twice quickly')\n      .given('An action with the Throttle mixin')\n      .when('The action is dispatched twice within the throttle period')\n      .then('It should only execute once')\n      .run((_) async {\n    //\n    var store = Store<AppState>(initialState: AppState(0));\n    await store.dispatch(ThrottleAction());\n    expect(store.state.count, 1);\n\n    // Dispatch again immediately. This dispatch should be aborted.\n    await store.dispatch(ThrottleAction());\n    expect(store.state.count, 1);\n  });\n\n  Bdd(feature)\n      .scenario('Action executes again after throttle period expires')\n      .given('An action with the Throttle mixin')\n      .when('The action is dispatched, '\n          'then after waiting for the throttle period, dispatched again')\n      .then('It should execute both times')\n      .run((_) async {\n    //\n    var store = Store<AppState>(initialState: AppState(0));\n    await store.dispatch(ThrottleAction());\n    expect(store.state.count, 1);\n\n    // Wait for a bit more than the default throttle (400 ms).\n    await Future.delayed(const Duration(milliseconds: 400));\n    await store.dispatch(ThrottleAction());\n    expect(store.state.count, 2);\n  });\n\n  Bdd(feature)\n      .scenario(\n          'Two different actions with the same lock are throttled together')\n      .given('Two actions that override lockBuilder to return the same lock')\n      .when('Both actions are dispatched in quick succession')\n      .then('Only the first action should execute')\n      .run((_) async {\n    //\n    var store = Store<AppState>(initialState: AppState(0));\n    await store.dispatch(ThrottleAction1());\n    expect(store.state.count, 1);\n\n    // ThrottleAction2 uses the same lock as ThrottleAction1.\n    await store.dispatch(ThrottleAction2());\n    expect(store.state.count, 1);\n  });\n\n  Bdd(feature)\n      .scenario('Two different actions with the same lock execute '\n          'if throttle period expires')\n      .given('Two actions that override lockBuilder to return the same lock')\n      .when('The first action is dispatched, '\n          'throttle period passes, then the second is dispatched')\n      .then('Both actions should execute')\n      .run((_) async {\n    //\n    var store = Store<AppState>(initialState: AppState(0));\n    await store.dispatch(ThrottleAction1());\n    expect(store.state.count, 1);\n\n    await Future.delayed(const Duration(milliseconds: 400));\n    await store.dispatch(ThrottleAction2());\n    expect(store.state.count, 2);\n  });\n\n  Bdd(feature)\n      .scenario('Actions with different runtime types '\n          'are not throttled together')\n      .given('Two actions with the Throttle mixin but different runtime types')\n      .when('Both actions are dispatched in quick succession')\n      .then('Both should execute independently')\n      .run((_) async {\n    //\n    var store = Store<AppState>(initialState: AppState(0));\n    await store.dispatch(ThrottleActionA());\n    expect(store.state.count, 1);\n\n    await store.dispatch(ThrottleActionB());\n    expect(store.state.count, 2);\n  });\n}\n\n// A simple state that holds a count.\nclass AppState {\n  final int count;\n\n  AppState(this.count);\n\n  AppState copy({int? count}) => AppState(count ?? this.count);\n\n  @override\n  String toString() => 'TestState($count)';\n}\n\n// An action that uses the Throttle mixin to increment the state.\nclass ThrottleAction extends ReduxAction<AppState> with Throttle {\n  @override\n  int throttle = 300;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Two actions that override lockBuilder to return the same lock.\nclass ThrottleAction1 extends ReduxAction<AppState> with Throttle {\n  @override\n  int throttle = 300;\n\n  @override\n  Object? lockBuilder() => 'sharedLock';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass ThrottleAction2 extends ReduxAction<AppState> with Throttle {\n  @override\n  int throttle = 300;\n\n  @override\n  Object? lockBuilder() => 'sharedLock';\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\n// Two actions with default lock (their runtime types differ).\nclass ThrottleActionA extends ReduxAction<AppState> with Throttle {\n  @override\n  int throttle = 300;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n\nclass ThrottleActionB extends ReduxAction<AppState> with Throttle {\n  @override\n  int throttle = 300;\n\n  @override\n  AppState reduce() {\n    return state.copy(count: state.count + 1);\n  }\n}\n"
  },
  {
    "path": "test/user_exception_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:fast_immutable_collections/fast_immutable_collections.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test(\n      'Get title and content from UserException. '\n      'Note: This is already tested in async_redux_core, so no need to do much here.', () {\n    //\n    // UserException with no given cause.\n    var exception = const UserException('Some msg');\n    var (title, content) = exception.titleAndContent();\n    expect(title, '');\n    expect(content, 'Some msg');\n    expect(exception.toString(), 'UserException{Some msg}');\n\n    // UserException with cause, and the cause is also an UserException.\n    exception = const UserException('Some msg', reason: 'Other msg');\n    (title, content) = exception.titleAndContent();\n    expect(title, 'Some msg');\n    expect(content, 'Other msg');\n    expect(exception.toString(), 'UserException{Some msg|Reason: Other msg}');\n\n    // UserException with cause, and the cause is NOT an UserException.\n    exception = const UserException('Some msg', reason: 'Other msg');\n    (title, content) = exception.titleAndContent();\n    expect(title, 'Some msg');\n    expect(content, 'Other msg');\n    expect(exception.toString(), 'UserException{Some msg|Reason: Other msg}');\n  });\n\n  test('Adding callbacks', () {\n    //\n    String result = '';\n    var exception = const UserException('Some msg') //\n        .addCallbacks(onOk: () => result += 'a', onCancel: () => result += 'b');\n\n    expect(exception.onOk, isNotNull);\n    expect(exception.onCancel, isNotNull);\n\n    expect(result, '');\n    exception.onOk?.call();\n    expect(result, 'a');\n    exception.onCancel?.call();\n    expect(result, 'ab');\n  });\n\n  test('Adding properties', () {\n    //\n    var exception = const UserException('Some msg').addProps({'1': 'a', '2': 'b'});\n\n    expect(exception.reason, isNull);\n    expect(exception.hardCause, isNull);\n    expect(exception.onOk, isNull);\n    expect(exception.onCancel, isNull);\n    expect(exception.props, {'1': 'a', '2': 'b'}.lock);\n\n    exception = exception.addProps({'2': 'c', '3': 'd'});\n    expect(exception.props, {'1': 'a', '2': 'c', '3': 'd'}.lock);\n\n    exception = exception.addProps(null);\n    exception = exception.addProps({});\n    expect(exception.props, {'1': 'a', '2': 'c', '3': 'd'}.lock);\n  });\n\n  test('Adding hard cause', () {\n    //\n    // 1) The cause is not null, String, or UserException.\n    var exception = const UserException('Some msg') //\n        .addCause(const FormatException('Some other msg'));\n\n    expect(exception.reason, isNull);\n    expect(exception.hardCause, isA<FormatException>());\n    expect(exception.onOk, isNull);\n    expect(exception.onCancel, isNull);\n    expect(exception.props, isEmpty);\n\n    // 2) Another hard cause will replace a previous one.\n    exception = exception.addCause(UnsupportedError('Yet another'));\n\n    expect(exception.reason, isNull);\n    expect(exception.hardCause, isA<UnsupportedError>());\n    expect(exception.onOk, isNull);\n    expect(exception.onCancel, isNull);\n    expect(exception.props, isEmpty);\n\n    // 3)  string will add to the reason.\n    exception = exception.addCause('Some text');\n\n    expect(exception.reason, 'Some text');\n    expect(exception.hardCause, isA<UnsupportedError>());\n    expect(exception.onOk, isNull);\n    expect(exception.onCancel, isNull);\n    expect(exception.props, isEmpty);\n\n    // 4) A string will add to (not replace) the reason.\n    exception = exception.addCause('Yet another text');\n\n    expect(exception.reason, 'Some text\\n\\nReason: Yet another text');\n    expect(exception.hardCause, isA<UnsupportedError>());\n    expect(exception.onOk, isNull);\n    expect(exception.onCancel, isNull);\n    expect(exception.props, isEmpty);\n\n    // 5) A UserException will add to (not replace) the reason.\n    exception = exception.addCause(const UserException('My exception'));\n\n    expect(exception.reason, 'Some text\\n\\nReason: Yet another text\\n\\nReason: My exception');\n    expect(exception.hardCause, isA<UnsupportedError>());\n    expect(exception.onOk, isNull);\n    expect(exception.onCancel, isNull);\n    expect(exception.props, isEmpty);\n\n    // 6) A UserException with a reason will add to (not replace) the reason.\n    exception = exception.addCause(const UserException('Another exception', reason: 'My reason'));\n\n    expect(\n        exception.reason,\n        'Some text\\n\\nReason: Yet another text\\n\\nReason: My exception\\n\\n'\n        'Reason: Another exception\\n\\nReason: My reason');\n    expect(exception.hardCause, isA<UnsupportedError>());\n    expect(exception.onOk, isNull);\n    expect(exception.onCancel, isNull);\n    expect(exception.props, isEmpty);\n\n    // 6) Adding null as a cause doesn't change anything.\n    exception = exception.addCause(null);\n\n    expect(\n        exception.reason,\n        'Some text\\n\\nReason: Yet another text\\n\\nReason: My exception\\n\\n'\n        'Reason: Another exception\\n\\nReason: My reason');\n    expect(exception.hardCause, isA<UnsupportedError>());\n    expect(exception.onOk, isNull);\n    expect(exception.onCancel, isNull);\n    expect(exception.props, isEmpty);\n  });\n}\n"
  },
  {
    "path": "test/view_model_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass MyObjPlain {\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) || //\n      other is MyObjPlain && runtimeType == other.runtimeType;\n\n  @override\n  int get hashCode => 0;\n}\n\nclass MyObjVmEquals extends VmEquals<MyObjVmEquals> {\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) || //\n      other is MyObjVmEquals && runtimeType == other.runtimeType;\n\n  @override\n  int get hashCode => 0;\n}\n\nclass ViewModel extends Vm {\n  final String name;\n  final int age;\n  final dynamic myObj;\n\n  ViewModel(this.name, this.age, this.myObj)\n      : super(equals: [\n          name,\n          age,\n          myObj,\n        ]);\n}\n\nvoid main() {\n  test('Vm equality.', () {\n    //\n    // Comparison by equality. Same object.\n    dynamic myObj = MyObjPlain();\n    var vm1 = ViewModel(\"John\", 35, myObj);\n    var vm2 = ViewModel(\"John\", 35, myObj);\n    expect(vm1 == vm2, isTrue);\n\n    // Comparison by equality. Different objects.\n    vm1 = ViewModel(\"John\", 35, MyObjPlain());\n    vm2 = ViewModel(\"John\", 35, MyObjPlain());\n    expect(vm1 == vm2, isTrue);\n\n    // ---\n\n    // Now we're going to use a VmEquals object:\n    // Same by equality, but Different by vmEquals().\n    expect(MyObjVmEquals() == MyObjVmEquals(), isTrue);\n    expect(MyObjVmEquals().vmEquals(MyObjVmEquals()), isFalse);\n\n    // Comparison by identity. Same object.\n    myObj = MyObjVmEquals();\n    vm1 = ViewModel(\"John\", 35, myObj);\n    vm2 = ViewModel(\"John\", 35, myObj);\n    expect(vm1 == vm2, isTrue);\n\n    // Comparison by identity. Different objects.\n    vm1 = ViewModel(\"John\", 35, MyObjVmEquals());\n    vm2 = ViewModel(\"John\", 35, MyObjVmEquals());\n    expect(vm1 != vm2, isTrue);\n  });\n}\n"
  },
  {
    "path": "test/wait_action_test.dart",
    "content": "import 'package:async_redux/async_redux.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nlate Store<AppState> store;\n\n@immutable\nclass AppState {\n  final Wait wait;\n\n  AppState({this.wait = Wait.empty});\n\n  AppState copy({Wait? wait}) => AppState(wait: wait ?? this.wait);\n}\n\n// This simulates using the Freezed package.\nclass AppStateFreezed {\n  final Wait wait;\n\n  AppStateFreezed({this.wait = Wait.empty});\n\n  AppStateFreezed copyWith({Wait? wait}) => AppStateFreezed(wait: wait ?? this.wait);\n}\n\n// This simulates using the BuiltValue package.\nclass AppStateBuiltValue {\n  Wait wait;\n\n  AppStateBuiltValue({this.wait = Wait.empty});\n\n  AppStateBuiltValue rebuild(dynamic func(dynamic state)) => func(AppStateBuiltValue(wait: Wait()));\n}\n\nclass MyAction {}\n\nclass MyAction1 {}\n\nclass MyAction2 {}\n\nvoid main() {\n  setUp(() async {\n    store = Store<AppState>(initialState: AppState(wait: Wait()));\n  });\n\n  test(\"Wait class is immutable. Empty object is always the same instance.\", () {\n    var wait1 = Wait();\n\n    var wait2 = wait1.add(flag: \"x\");\n    expect(wait1, isNot(wait2));\n\n    var wait3 = wait2.remove(flag: \"x\");\n    expect(wait3, wait1);\n    expect(wait3, isNot(wait2));\n\n    var wait4 = wait2.clear();\n    expect(wait4, wait1);\n    expect(wait4, isNot(wait2));\n    expect(wait4, wait3);\n  });\n\n  test(\"Waiting for some action to finish.\", () {\n    var action = MyAction();\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action), false);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n\n    store.dispatch(WaitAction.add(action));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action), true);\n    expect(store.state.wait.isWaitingForType<MyAction>(), true);\n\n    store.dispatch(WaitAction.remove(action));\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action), false);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n  });\n\n  test(\"Waiting for some action type to finish.\", () {\n    var action1a = MyAction1();\n    var action1b = MyAction1();\n    var action2 = MyAction2();\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action1a), false);\n    expect(store.state.wait.isWaiting(action2), false);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n    expect(store.state.wait.isWaitingForType<MyAction1>(), false);\n    expect(store.state.wait.isWaitingForType<MyAction2>(), false);\n\n    store.dispatch(WaitAction.add(action1a));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action1a), true);\n    expect(store.state.wait.isWaiting(action1b), false);\n    expect(store.state.wait.isWaiting(action2), false);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n    expect(store.state.wait.isWaitingForType<MyAction1>(), true);\n    expect(store.state.wait.isWaitingForType<MyAction2>(), false);\n\n    store.dispatch(WaitAction.add(action1b));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action1a), true);\n    expect(store.state.wait.isWaiting(action1b), true);\n    expect(store.state.wait.isWaiting(action2), false);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n    expect(store.state.wait.isWaitingForType<MyAction1>(), true);\n    expect(store.state.wait.isWaitingForType<MyAction2>(), false);\n\n    store.dispatch(WaitAction.add(action2));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action1a), true);\n    expect(store.state.wait.isWaiting(action1b), true);\n    expect(store.state.wait.isWaiting(action2), true);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n    expect(store.state.wait.isWaitingForType<MyAction1>(), true);\n    expect(store.state.wait.isWaitingForType<MyAction2>(), true);\n\n    store.dispatch(WaitAction.remove(action1a));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action1a), false);\n    expect(store.state.wait.isWaiting(action1b), true);\n    expect(store.state.wait.isWaiting(action2), true);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n    expect(store.state.wait.isWaitingForType<MyAction1>(), true);\n    expect(store.state.wait.isWaitingForType<MyAction2>(), true);\n\n    store.dispatch(WaitAction.remove(action1b));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action1a), false);\n    expect(store.state.wait.isWaiting(action1b), false);\n    expect(store.state.wait.isWaiting(action2), true);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n    expect(store.state.wait.isWaitingForType<MyAction1>(), false);\n    expect(store.state.wait.isWaitingForType<MyAction2>(), true);\n  });\n\n  test(\"Waiting for 2 actions to finish.\", () {\n    var action1 = MyAction();\n    var action2 = MyAction();\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action1), false);\n    expect(store.state.wait.isWaiting(action2), false);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n\n    store.dispatch(WaitAction.add(action1));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action1), true);\n    expect(store.state.wait.isWaiting(action2), false);\n    expect(store.state.wait.isWaitingForType<MyAction>(), true);\n\n    store.dispatch(WaitAction.add(action2));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action1), true);\n    expect(store.state.wait.isWaiting(action2), true);\n    expect(store.state.wait.isWaitingForType<MyAction>(), true);\n\n    store.dispatch(WaitAction.remove(action1));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action1), false);\n    expect(store.state.wait.isWaiting(action2), true);\n    expect(store.state.wait.isWaitingForType<MyAction>(), true);\n\n    store.dispatch(WaitAction.remove(action2));\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action1), false);\n    expect(store.state.wait.isWaiting(action2), false);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n  });\n\n  test(\"Clear the waiting (everything).\", () {\n    var action = MyAction();\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action), false);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n\n    store.dispatch(WaitAction.add(action));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaitingForType<MyAction>(), true);\n\n    store.dispatch(WaitAction.clear());\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action), false);\n    expect(store.state.wait.isWaitingForType<MyAction>(), false);\n  });\n\n  test(\"Clear the waiting (a specific flag).\", () {\n    var action1 = MyAction();\n    store.dispatch(WaitAction.add(action1, ref: \"X\"));\n    store.dispatch(WaitAction.add(action1, ref: \"Y\"));\n\n    var action2 = MyAction();\n    store.dispatch(WaitAction.add(action2, ref: \"X\"));\n    store.dispatch(WaitAction.add(action2, ref: \"A\"));\n\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action1), true);\n    expect(store.state.wait.isWaiting(action2), true);\n    expect(store.state.wait.isWaitingForType<MyAction>(), true);\n\n    store.dispatch(WaitAction.clear(action1));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action1), false);\n    expect(store.state.wait.isWaiting(action2), true);\n    expect(store.state.wait.isWaitingForType<MyAction>(), true);\n  });\n\n  test(\"Waiting for some action with ref and ref.\", () {\n    var action = MyAction();\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action), false);\n\n    store.dispatch(WaitAction.add(action, ref: 123));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action), true);\n\n    store.dispatch(WaitAction.add(action, ref: 456));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action, ref: 123), true);\n    expect(store.state.wait.isWaiting(action, ref: 456), true);\n    expect(store.state.wait.isWaiting(action, ref: 789), false);\n    expect(store.state.wait.isWaiting(action), true);\n\n    /// Removing ref without ref removes ref (ignores subRefs).\n    store.dispatch(WaitAction.remove(action));\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action, ref: 123), false);\n    expect(store.state.wait.isWaiting(action, ref: 456), false);\n    expect(store.state.wait.isWaiting(action, ref: 789), false);\n    expect(store.state.wait.isWaiting(action), false);\n\n    // ---\n\n    // Now try again, removing ref by ref.\n\n    action = MyAction();\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action), false);\n\n    store.dispatch(WaitAction.add(action, ref: 123));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action), true);\n\n    store.dispatch(WaitAction.add(action, ref: 456));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action), true);\n\n    /// Removing ref with ref removes just the ref (until all are removed).\n    store.dispatch(WaitAction.remove(action, ref: 123));\n    expect(store.state.wait.isWaitingAny, true);\n    expect(store.state.wait.isWaiting(action, ref: 123), false);\n    expect(store.state.wait.isWaiting(action, ref: 456), true);\n    expect(store.state.wait.isWaiting(action, ref: 789), false);\n    expect(store.state.wait.isWaiting(action), true);\n\n    store.dispatch(WaitAction.remove(action, ref: 456));\n    expect(store.state.wait.isWaitingAny, false);\n    expect(store.state.wait.isWaiting(action, ref: 123), false);\n    expect(store.state.wait.isWaiting(action, ref: 456), false);\n    expect(store.state.wait.isWaiting(action, ref: 789), false);\n    expect(store.state.wait.isWaiting(action), false);\n  });\n\n  test(\"Test compatibility with the Freezed package.\", () {\n    Store<AppStateFreezed> freezedStore;\n    freezedStore = Store<AppStateFreezed>(initialState: AppStateFreezed(wait: Wait()));\n\n    var action = MyAction();\n    expect(freezedStore.state.wait.isWaitingAny, false);\n    expect(freezedStore.state.wait.isWaiting(action), false);\n\n    freezedStore.dispatch(WaitAction.add(action));\n    expect(freezedStore.state.wait.isWaitingAny, true);\n    expect(freezedStore.state.wait.isWaiting(action), true);\n\n    freezedStore.dispatch(WaitAction.remove(action));\n    expect(freezedStore.state.wait.isWaitingAny, false);\n    expect(freezedStore.state.wait.isWaiting(action), false);\n  });\n\n  test(\"Test compatibility with the BuiltValue package.\", () {\n    Store<AppStateBuiltValue> builtValueStore;\n    builtValueStore = Store<AppStateBuiltValue>(initialState: AppStateBuiltValue(wait: Wait()));\n\n    var action = MyAction();\n    expect(builtValueStore.state.wait.isWaitingAny, false);\n    expect(builtValueStore.state.wait.isWaiting(action), false);\n\n    builtValueStore.dispatch(WaitAction.add(action));\n    expect(builtValueStore.state.wait.isWaitingAny, true);\n    expect(builtValueStore.state.wait.isWaiting(action), true);\n\n    builtValueStore.dispatch(WaitAction.remove(action));\n    expect(builtValueStore.state.wait.isWaitingAny, false);\n    expect(builtValueStore.state.wait.isWaiting(action), false);\n  });\n\n  test(\"Test compatibility with the BuiltValue package.\", () {\n    Store<AppStateFreezed> freezedStore;\n    freezedStore = Store<AppStateFreezed>(initialState: AppStateFreezed(wait: Wait()));\n\n    var action = MyAction();\n    expect(freezedStore.state.wait.isWaitingAny, false);\n    expect(freezedStore.state.wait.isWaiting(action), false);\n\n    freezedStore.dispatch(WaitAction.add(action));\n    expect(freezedStore.state.wait.isWaitingAny, true);\n    expect(freezedStore.state.wait.isWaiting(action), true);\n\n    freezedStore.dispatch(WaitAction.remove(action));\n    expect(freezedStore.state.wait.isWaitingAny, false);\n    expect(freezedStore.state.wait.isWaiting(action), false);\n  });\n}\n"
  }
]