Core Engine
The core engine (@asyncflowstate/core) is a framework-agnostic state machine that powers every framework binding.
Architecture
Your Application
@asyncflowstate/core
Framework Agnostic Foundation
The high-level orchestrator for async processes.
Transition logic (idle → loading → success/error).
Keep, restart, or enqueue concurrent executions.
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.
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 anyState Lifecycle
Every flow follows a predictable state machine:
Flow Options
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:
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:
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 outputsSequential Execution
Chain flows where each step depends on the previous:
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:
// 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.
