# How iloader Tracks the Progress and State of Operations: Event-Driven Architecture Explained

> Discover how iloader tracks operations using an event-driven architecture. Learn how Rust backend events update the React frontend for real-time progress and state tracking.

- Repository: [Nicholas Sharp/iloader](https://github.com/nab138/iloader)
- Tags: internals
- Published: 2026-09-12

---

**iloader tracks operation progress through a Tauri-based event system where the Rust backend emits lifecycle events ("started", "finished", "failed") that a React frontend consumes to update an immutable OperationState, enabling real-time UI feedback.**

iloader is a desktop application for sideloading iOS apps and managing SideStore installations. According to the nab138/iloader source code, the application coordinates long-running tasks through a tightly-coupled frontend-backend event architecture. This system ensures that operations like installing SideStore or sideloading an IPA provide immediate visual feedback as each step transitions from initiation through completion or failure.

## Static Operation Definitions and State Types

The foundation of iloader's progress tracking begins with statically defined operation schemas in **src/components/operations.ts**. These TypeScript interfaces establish the contract between what the UI expects and what the backend executes.

### Operation and Step Structure

Each workflow is defined as an **Operation** containing an ordered list of steps, translation keys for user-facing messages, and a unique identifier:

```typescript
export type Operation = {
  id: string;
  titleKey: string;
  successMessageKey?: string;
  successTitleKey?: string;
  steps: OperationStep[];
};

export type OperationStep = { id: string; titleKey: string };

export const installSideStoreOperation: Operation = { … };
export const sideloadOperation = { … };

```

The `id` field (e.g., `"install_sidestore"`) serves as the namespace for runtime events, while the `steps` array defines the sequential stages the backend must report against.

### Runtime State Tracking

While an operation executes, iloader maintains a mutable snapshot in the **OperationState** type, also declared in [`src/components/operations.ts`](https://github.com/nab138/iloader/blob/main/src/components/operations.ts):

```typescript
export type OperationState = {
  current: Operation;
  completed: string[];
  started: string[];
  failed: { stepId: string; extraDetails: AppError }[];
};

```

This structure captures three distinct lifecycle arrays:
- **started**: Step IDs that have begun execution
- **completed**: Step IDs that finished successfully  
- **failed**: Objects containing the failed step ID and associated **AppError** details

## Cross-Platform Event Communication

iloader bridges its Rust backend and React frontend through Tauri's event system. When a user initiates an operation, the frontend registers a listener specific to that operation's ID, while the backend emits structured updates as work progresses.

### Frontend Listener Registration

In **src/App.tsx**, the `startOperation` function establishes a typed event listener using Tauri's `listen` API. The listener targets the channel `"operation_" + operation.id` and updates the React state immutably:

```typescript
const unlistenFn = await listen<OperationUpdate>(
  "operation_" + operation.id,
  (event) => {
    setOperationState((old) => {
      if (!old) return null;
      switch (event.payload.updateType) {
        case "started":
          return { …old, started: [...old.started, event.payload.stepId] };
        case "finished":
          return { …old, completed: [...old.completed, event.payload.stepId] };
        case "failed":
          return {
            …old,
            failed: [
              ...old.failed,
              {
                stepId: event.payload.stepId,
                extraDetails: event.payload.extraDetails,
              },
            ],
          };
      }
      return old;
    });
  },
);

```

The `OperationUpdate` payload type mirrors between TypeScript and Rust, ensuring type safety across the boundary. Updates are handled via switch cases on `updateType`, creating new state objects rather than mutating existing ones to maintain React's change detection integrity.

### Backend Event Emission

The Rust side implements an **Operation** helper struct in **src-tauri/src/operation.rs** that emits events matching the frontend's listener contract. Three primary methods handle lifecycle transitions:

```rust
pub fn start(&self, id: &str) -> Result<(), AppError> {
    self.window.emit(
        &format!("operation_{}", self.id),
        OperationUpdate {
            update_type: "started",
            step_id: id,
            extra_details: None,
        },
    )
}
pub fn complete(&self, id: &str) -> Result<(), AppError> { … }
pub fn fail<T>(&self, id: &str, error: AppError) -> Result<T, AppError> { … }

```

Each method constructs a JSON payload containing `update_type`, `step_id`, and optional `extra_details`, firing it through the Tauri window to any subscribed frontend listeners. This decouples the business logic execution from presentation concerns.

## Visual Progress Representation

The **OperationView** component in **src/components/OperationView.tsx** consumes the `OperationState` to render a modal interface. It maps each step to a visual indicator based on its current status:

- **Completed steps**: Displayed with a checkmark icon (FaCircleCheck)
- **Active steps**: Show a loading spinner while in the `started` array but not yet `completed`
- **Failed steps**: Render with an exclamation icon (FaCircleExclamation) alongside detailed error information from the `failed` array
- **Skipped steps**: Indicated with a minus icon (FaCircleMinus) for steps remaining after a failure

The view also aggregates error suggestions and provides a "copy error to clipboard" button for troubleshooting, accessing the `extraDetails` field from failed step records.

## Summary

iloader implements a reliable, event-driven pipeline for tracking operation progress:

- **Static definitions** in [`src/components/operations.ts`](https://github.com/nab138/iloader/blob/main/src/components/operations.ts) establish operation schemas and state shapes
- **Tauri events** bridge the Rust backend and React frontend using operation-specific channels
- **Immutable state updates** in [`src/App.tsx`](https://github.com/nab138/iloader/blob/main/src/App.tsx) ensure predictable UI re-renders as steps transition through started, finished, or failed states
- **Visual feedback** in [`OperationView.tsx`](https://github.com/nab138/iloader/blob/main/OperationView.tsx) translates state arrays into intuitive progress indicators and error reporting

## Frequently Asked Questions

### How does iloader communicate progress from Rust to the React frontend?

iloader uses Tauri's built-in event system. The Rust backend emits JSON payloads to channels named `operation_{id}` (e.g., `operation_install_sidestore`), while the frontend registers typed listeners using `listen<OperationUpdate>()`. This creates a loosely-coupled messaging layer where the backend reports step lifecycles without direct knowledge of the UI implementation.

### What happens to the operation state when a step fails?

When the backend calls `operation.fail()`, it emits a payload with `update_type: "failed"` and includes an **AppError** object in `extra_details`. The frontend listener appends this to the `failed` array in `OperationState`, which triggers the UI to display an error icon, show detailed troubleshooting information, and mark subsequent steps as skipped rather than attempted.

### Where is the current operation progress stored in iloader?

Progress lives in React component state managed within **src/App.tsx** via the `OperationState` type. This state tracks three arrays (`started`, `completed`, `failed`) rather than a percentage value, allowing granular reporting of exactly which steps have executed. The state is local to the component lifecycle and resets when a new operation begins.

### Can multiple operations run simultaneously in iloader?

Each operation uses a unique event channel based on its `id` field (e.g., `"operation_sideload"` vs. `"operation_install_sidestore"`). However, the current implementation in [`App.tsx`](https://github.com/nab138/iloader/blob/main/App.tsx) manages a single `operationState` object, suggesting the UI is designed to display one primary operation at a time. The architecture supports concurrent operations technically, but the frontend state management focuses on a single active workflow.