Effects
Effects in Flow are data descriptors that represent side effects to be executed. They are pure data structures, not imperative actions, making them testable, serializable, and composable.
Effects as Data
Unlike traditional side effect implementations, Flow effects are data descriptors that describe what should happen, not how it happens:
import { Effect } from "@hex-di/flow";
// Effects are data, not functions
const logEffect = Effect.log("Hello, world!");
// { _tag: 'log', message: 'Hello, world!' }
const delayEffect = Effect.delay(1000);
// { _tag: 'delay', ms: 1000 }
// Effects can be composed
const sequenceEffect = Effect.sequence([
Effect.log("Starting..."),
Effect.delay(1000),
Effect.log("Done!"),
]);
Effect Types
Flow provides 10 built-in effect types, each with a discriminator _tag property:
1. InvokeEffect
Call a port method with arguments:
const effect = Effect.invoke("UserService", "fetchUser", { id: "123" });
// With compensation for rollback
const effectWithCompensation = Effect.invoke(
"PaymentService",
"charge",
{ amount: 100 },
{
compensate: Effect.invoke("PaymentService", "refund", { amount: 100 }),
}
);
2. SpawnEffect
Start a long-running activity:
const effect = Effect.spawn("dataSync", { interval: 5000 });
// With compensation to stop on rollback
const effectWithCompensation = Effect.spawn(
"backgroundJob",
{ jobId: "abc" },
{
compensate: Effect.stop("backgroundJob"),
}
);
3. StopEffect
Stop a running activity:
const effect = Effect.stop("dataSync");
4. EmitEffect
Emit an event back to the machine:
const effect = Effect.emit({ type: "RETRY", payload: { attempt: 1 } });
// With delayed emission
const delayedEmit = Effect.emit({ type: "TIMEOUT" }, { delay: 5000 });
// With compensation
const emitWithCompensation = Effect.emit(
{ type: "START_PROCESS" },
{
compensate: Effect.emit({ type: "CANCEL_PROCESS" }),
}
);
5. DelayEffect
Wait for a specified duration:
const effect = Effect.delay(3000); // Wait 3 seconds
// With compensation to cancel delay
const delayWithCompensation = Effect.delay(10000, {
compensate: Effect.none(), // Cancels the delay
});
6. ParallelEffect
Run multiple effects concurrently:
const effect = Effect.parallel([
Effect.invoke("ServiceA", "method1"),
Effect.invoke("ServiceB", "method2"),
Effect.invoke("ServiceC", "method3"),
]);
// All effects run at the same time
7. SequenceEffect
Run effects sequentially, one after another:
const effect = Effect.sequence([
Effect.log("Step 1"),
Effect.delay(1000),
Effect.log("Step 2"),
Effect.invoke("DataService", "save"),
Effect.log("Step 3"),
]);
// Each effect waits for the previous to complete
8. NoneEffect
A no-op effect (useful for conditional logic):
const effect = Effect.none();
// Useful in conditional branches
const conditionalEffect = shouldLog ? Effect.log("Event occurred") : Effect.none();
9. ChooseEffect
Conditional branching based on predicates:
const effect = Effect.choose([
{
predicate: ctx => ctx.retryCount < 3,
effect: Effect.sequence([Effect.log("Retrying..."), Effect.emit({ type: "RETRY" })]),
},
{
predicate: ctx => ctx.retryCount >= 3,
effect: Effect.sequence([Effect.log("Max retries reached"), Effect.emit({ type: "FAILURE" })]),
},
]);
10. LogEffect
Logging for debugging and monitoring:
const effect = Effect.log("State transition completed");
// Log with context interpolation
const effectWithContext = Effect.log(ctx => `User ${ctx.userId} logged in`);
Effect Composition
Effects can be composed to create complex behaviors:
Parallel Composition
Run multiple operations simultaneously:
import { Effect } from "@hex-di/flow";
const parallelDataFetch = Effect.parallel([
Effect.invoke("UserService", "fetchProfile", { id: userId }),
Effect.invoke("PostService", "fetchPosts", { userId }),
Effect.invoke("CommentService", "fetchComments", { userId }),
]);
Sequential Composition
Chain operations that depend on order:
const saveSequence = Effect.sequence([
Effect.log("Validating data..."),
Effect.invoke("ValidationService", "validate", data),
Effect.log("Saving to database..."),
Effect.invoke("DatabaseService", "save", data),
Effect.log("Sending notification..."),
Effect.invoke("NotificationService", "notify", { event: "data_saved" }),
Effect.log("Save complete!"),
]);
Nested Composition
Combine parallel and sequential patterns:
const complexFlow = Effect.sequence([
Effect.log("Starting complex operation"),
Effect.parallel([
Effect.invoke("CacheService", "warm"),
Effect.sequence([
Effect.invoke("AuthService", "verify"),
Effect.invoke("PermissionService", "check"),
]),
]),
Effect.log("Ready to process"),
Effect.invoke("ProcessingService", "execute"),
]);
Compensation Support
For GxP compliance (F8), effects support compensation for rollback scenarios:
const transactionEffect = Effect.sequence([
Effect.invoke(
"AccountService",
"debit",
{ account: "A", amount: 100 },
{ compensate: Effect.invoke("AccountService", "credit", { account: "A", amount: 100 }) }
),
Effect.invoke(
"AccountService",
"credit",
{ account: "B", amount: 100 },
{ compensate: Effect.invoke("AccountService", "debit", { account: "B", amount: 100 }) }
),
]);
Port Method Utilities
Flow provides utilities for extracting method information from ports:
import { MethodNames, MethodParams, MethodReturn } from "@hex-di/flow";
interface UserService {
fetchUser(id: string): Promise<User>;
updateUser(id: string, data: Partial<User>): Promise<User>;
deleteUser(id: string): Promise<void>;
}
// Extract method names
type Methods = MethodNames<UserService>;
// 'fetchUser' | 'updateUser' | 'deleteUser'
// Extract parameters for a method
type FetchParams = MethodParams<UserService, "fetchUser">;
// [id: string]
// Extract return type for a method
type FetchReturn = MethodReturn<UserService, "fetchUser">;
// Promise<User>
Using Effects in Machines
Effects are typically used in transitions and state entry/exit:
import { defineMachine, Effect } from "@hex-di/flow";
const machine = defineMachine({
id: "order-processing",
initial: "pending",
context: { orderId: null, items: [] },
states: {
pending: {
entry: [Effect.log("Order pending")],
on: {
PROCESS: {
target: "processing",
effects: [
Effect.parallel([
Effect.invoke("InventoryService", "reserve", { items: [] }),
Effect.invoke("PaymentService", "authorize", { amount: 0 }),
]),
],
},
},
},
processing: {
entry: [Effect.spawn("orderMonitor", { orderId: "123" })],
exit: [Effect.stop("orderMonitor")],
on: {
SUCCESS: {
target: "completed",
effects: [
Effect.sequence([
Effect.invoke("PaymentService", "capture"),
Effect.invoke("ShippingService", "schedule"),
Effect.invoke("EmailService", "sendConfirmation"),
]),
],
},
FAILURE: {
target: "failed",
effects: [
Effect.parallel([
Effect.invoke("InventoryService", "release"),
Effect.invoke("PaymentService", "void"),
Effect.log("Order processing failed"),
]),
],
},
},
},
completed: {
entry: [Effect.log("Order completed successfully")],
},
failed: {
entry: [Effect.log("Order failed")],
},
},
});
Best Practices
- Keep effects pure data: Don't put functions or closures in effect descriptors
- Use composition: Build complex effects from simple ones
- Add compensation: For critical operations, always define rollback effects
- Log strategically: Use log effects for debugging but remove in production
- Batch when possible: Use parallel effects for independent operations
- Test effects separately: Effects are data and can be easily tested
- Type your ports: Ensure full type safety with properly typed port interfaces