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

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:

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:

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:

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:

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 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 ensure predictable UI re-renders as steps transition through started, finished, or failed states
  • Visual feedback in 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →