Skip to content

Core Engine

The core engine (@asyncflowstate/core) is a framework-agnostic state machine that powers every framework binding.

Architecture

Application Layer

Your Application

React
Next.js
Vue
Svelte
Angular
SolidJS

@asyncflowstate/core

Framework Agnostic Foundation

Flow Engine

The high-level orchestrator for async processes.

State Machine

Transition logic (idle → loading → success/error).

Concurrency Ctrl

Keep, restart, or enqueue concurrent executions.

Retry & UX Logic

Auto-retries, backoff, and minDuration polish.

The Flow Class

The Flow class is the foundation of everything. It wraps any async function and manages its lifecycle.

ts
import { Flow } from "@asyncflowstate/core";

const saveFlow = new Flow(async (data: FormData) => {
  const response = await fetch("/api/save", {
    method: "POST",
    body: data,
  });
  return response.json();
});

// Execute
const result = await saveFlow.execute(formData);

// Check state
console.log(saveFlow.status); // "idle" | "loading" | "success" | "error"
console.log(saveFlow.data); // Last successful result
console.log(saveFlow.error); // Last error, if any

State Lifecycle

Every flow follows a predictable state machine:

Idle
execute()
Loading
Success
Error
Success
Error
reset() or retry

Flow Options

ts
interface FlowOptions<TInput, TOutput> {
  // Callbacks
  onSuccess?: (data: TOutput) => void;
  onError?: (error: Error) => void;
  onStart?: (input: TInput) => void;

  // Retry
  retry?: {
    maxAttempts?: number; // default: 1 (no retry)
    delay?: number; // default: 1000ms
    backoff?: "fixed" | "linear" | "exponential";
    shouldRetry?: (error: Error, attempt: number) => boolean;
  };

  // Concurrency
  concurrency?: "keep" | "restart" | "enqueue";

  // UX Polish
  loading?: {
    minDuration?: number; // Minimum loading time (prevents flashes)
    delay?: number; // Delay before showing loading state
  };

  // Auto Reset
  autoReset?: {
    enabled?: boolean;
    delay?: number; // ms after success to reset to idle
  };

  // Optimistic UI
  optimisticResult?: TOutput;

  // Rate limiting
  debounce?: number;
  throttle?: number;
}

Subscribing to Changes

The core engine uses a pub/sub model for framework integration:

ts
const flow = new Flow(fetchData);

// Subscribe to state changes
const unsubscribe = flow.subscribe((state) => {
  console.log("Status:", state.status);
  console.log("Data:", state.data);
  console.log("Error:", state.error);
  console.log("Loading:", state.loading);
});

// Execute
await flow.execute();

// Cleanup
unsubscribe();

Parallel Execution

Run multiple flows simultaneously with aggregated state:

ts
import { FlowParallel } from "@asyncflowstate/core";

const parallel = new FlowParallel([
  new Flow(fetchUsers),
  new Flow(fetchPosts),
  new Flow(fetchComments),
]);

const results = await parallel.execute();
// All three complete — results is an array of outputs

Sequential Execution

Chain flows where each step depends on the previous:

ts
import { FlowSequence } from "@asyncflowstate/core";

const sequence = new FlowSequence([
  { name: "Validate", flow: validateFlow },
  { name: "Upload", flow: uploadFlow, mapInput: (prev) => prev.fileId },
  { name: "Notify", flow: notifyFlow, mapInput: (prev) => prev.url },
]);

const finalResult = await sequence.execute(initialData);

Using Without a Framework

The core engine works in any JavaScript environment:

ts
// Node.js, Deno, Bun, Workers, etc.
import { Flow } from "@asyncflowstate/core";

const migration = new Flow(
  async (batchSize: number) => {
    return await db.migrate({ limit: batchSize });
  },
  {
    retry: { maxAttempts: 3, backoff: "exponential" },
    onSuccess: (result) => console.log(`Migrated ${result.count} records`),
    onError: (err) => console.error("Migration failed:", err),
  },
);
await migration.execute(1000);

Best Practices

Directly using the core engine requires disciplining your application architecture. Follow these standards to extract the maximum value from AsyncFlowState.

Architectural Patterns

Decouple from Frameworks

Define your Flow instances in a shared service or controller layer, not just inside your components. Using @asyncflowstate/core outside of your UI allows you to test your business logic in isolation using standard unit testing tools like Vitest or Jest.

The "Engine" Mindset

Think of a Flow as a "behavior wrapper" around an async operation. Instead of passing handlers down multiple levels of props, pass the Flow instance itself. Your components can then independently subscribe to the same engine state.

Concurrency Control

Choosing the Right Strategy

  • keep: Best for initial data fetches (if already loading, don't start another).
  • restart: Best for search-as-you-type (abandon previous request for the newest one).
  • enqueue: Best for sequential task processing (like "Upload Image" batching).

Avoid "Ghost" Executions

If you are manually subscribing to a Flow, always call the unsubscribe() function when your component unmounts or your process ends. While framework adapters (React, Vue) handle this for you, direct core usage requires manual cleanup to prevent memory leaks.

Error Management

State vs Exceptions

AsyncFlowState captures errors in flow.error. However, flow.execute() still returns a Promise that will reject on error by default. This ensures standard try/catch or .catch() logic still works while the UI automatically updates.

Use shouldRetry for Logic

Don't retry everything. Use the shouldRetry callback to inspect the error. For example, you should retry on 503 Service Unavailable, but never on 401 Unauthorized or 400 Bad Request.

Built with by AsyncFlowState Contributors
Open Source · MIT License