Skip to main content

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

  1. Keep effects pure data: Don't put functions or closures in effect descriptors
  2. Use composition: Build complex effects from simple ones
  3. Add compensation: For critical operations, always define rollback effects
  4. Log strategically: Use log effects for debugging but remove in production
  5. Batch when possible: Use parallel effects for independent operations
  6. Test effects separately: Effects are data and can be easily tested
  7. Type your ports: Ensure full type safety with properly typed port interfaces