Skip to main content

Examples

Async Chains

Three-module async chain: auth → permissions → dashboard, with configurable failure rates and retry.

Try it

Loading example…

Click “Start Chain” to begin. Adjust failure rate sliders to see error propagation and retry behavior. Each step only runs after its predecessor succeeds.

How it works

Three modules form an async chain using after ordering: auth validates the session, permissions loads after auth succeeds, and dashboard loads after permissions.

  1. after ordering loadPermissions uses after: ['auth::validateSession'] to block until auth’s resolver settles; loadDashboard waits for permissions the same way
  2. crossModuleDeps – each step reads facts from its predecessor to check success (auth.isValid, permissions.role)
  3. Error propagation – if auth fails, permissions never evaluates (its after dependency is in rejected state), and dashboard is doubly blocked
  4. Retry – auth uses retry: { attempts: 2, backoff: 'exponential' }. Restarting auth automatically resumes the chain from where it left off

Summary

What: A three-step async chain (auth → permissions → dashboard) with configurable failure rates, retry with exponential backoff, and visual chain status.

How: Each module’s constraint uses after to depend on the previous step’s constraint, plus crossModuleDeps to read success state. The logging and devtools plugins trace the full chain execution.

Why it works: after provides hard ordering guarantees without manual promise chaining. Error propagation is automatic – downstream steps simply never evaluate when upstream fails. Retrying a single step resumes the entire chain.

Source code

/**
 * Async Chains — Three Directive Modules
 *
 * Demonstrates cross-module constraint chaining with `after` ordering:
 *   Auth → Permissions → Dashboard
 *
 * Each step only fires after the previous step's resolver completes.
 * Cross-module derivations drive the `when()` conditions.
 */

import { createModule, t, type ModuleSchema } from "@directive-run/core";
import {
  validateSession,
  fetchPermissions,
  fetchDashboard,
  type DashboardWidget,
} from "./mock-api.js";

// ============================================================================
// Auth Module
// ============================================================================

export const authSchema = {
  facts: {
    token: t.string(),
    status: t.string<"idle" | "validating" | "valid" | "expired">(),
    userId: t.string(),
    failRate: t.number(),
  },
  derivations: {
    isValid: t.boolean(),
  },
  events: {
    setToken: { value: t.string() },
    setFailRate: { value: t.number() },
    reset: {},
  },
  requirements: {
    VALIDATE_SESSION: { token: t.string() },
  },
} satisfies ModuleSchema;

export const authModule = createModule("auth", {
  schema: authSchema,

  init: (facts) => {
    facts.token = "";
    facts.status = "idle";
    facts.userId = "";
    facts.failRate = 0;
  },

  derive: {
    isValid: (facts) => facts.status === "valid",
  },

  events: {
    setToken: (facts, { value }) => {
      facts.token = value;
      facts.status = "idle";
      facts.userId = "";
    },
    setFailRate: (facts, { value }) => {
      facts.failRate = value;
    },
    reset: (facts) => {
      facts.token = "";
      facts.status = "idle";
      facts.userId = "";
    },
  },

  constraints: {
    validateSession: {
      when: (facts) => facts.token !== "" && facts.status === "idle",
      require: (facts) => ({
        type: "VALIDATE_SESSION",
        token: facts.token,
      }),
    },
  },

  resolvers: {
    validateSession: {
      requirement: "VALIDATE_SESSION",
      key: (req) => `validate-${req.token}`,
      retry: {
        attempts: 2,
        backoff: "exponential",
        initialDelay: 300,
      },
      resolve: async (req, context) => {
        context.facts.status = "validating";

        try {
          const result = await validateSession(req.token, context.facts.failRate);
          if (result.valid) {
            context.facts.status = "valid";
            context.facts.userId = result.userId;
          } else {
            context.facts.status = "expired";
          }
        } catch {
          context.facts.status = "expired";
        }
      },
    },
  },
});

// ============================================================================
// Permissions Module
// ============================================================================

export const permissionsSchema = {
  facts: {
    role: t.string(),
    permissions: t.array<string>(),
    loaded: t.boolean(),
    failRate: t.number(),
  },
  derivations: {
    canEdit: t.boolean(),
    canPublish: t.boolean(),
    canManageUsers: t.boolean(),
  },
  events: {
    setFailRate: { value: t.number() },
    reset: {},
  },
  requirements: {
    LOAD_PERMISSIONS: {},
  },
} satisfies ModuleSchema;

export const permissionsModule = createModule("permissions", {
  schema: permissionsSchema,

  crossModuleDeps: { auth: authSchema },

  init: (facts) => {
    facts.role = "";
    facts.permissions = [];
    facts.loaded = false;
    facts.failRate = 0;
  },

  derive: {
    canEdit: (facts) => facts.self.permissions.includes("write"),
    canPublish: (facts) => facts.self.permissions.includes("write") && facts.self.role !== "viewer",
    canManageUsers: (facts) => facts.self.permissions.includes("manage-users"),
  },

  events: {
    setFailRate: (facts, { value }) => {
      facts.failRate = value;
    },
    reset: (facts) => {
      facts.role = "";
      facts.permissions = [];
      facts.loaded = false;
    },
  },

  constraints: {
    loadPermissions: {
      after: ["auth::validateSession"],
      when: (facts) => {
        // Use the fact directly — derivation values aren't available in the
        // facts proxy passed to constraints (they live in the derive layer).
        return facts.auth.status === "valid" && !facts.self.loaded;
      },
      require: { type: "LOAD_PERMISSIONS" },
    },
  },

  resolvers: {
    loadPermissions: {
      requirement: "LOAD_PERMISSIONS",
      retry: {
        attempts: 2,
        backoff: "exponential",
        initialDelay: 200,
      },
      resolve: async (_req, context) => {
        try {
          const result = await fetchPermissions(context.facts.failRate);
          context.facts.role = result.role;
          context.facts.permissions = result.permissions;
          context.facts.loaded = true;
        } catch {
          context.facts.loaded = false;
        }
      },
    },
  },
});

// ============================================================================
// Dashboard Module
// ============================================================================

export const dashboardSchema = {
  facts: {
    widgets: t.array<DashboardWidget>(),
    loaded: t.boolean(),
    failRate: t.number(),
  },
  derivations: {
    widgetCount: t.number(),
  },
  events: {
    setFailRate: { value: t.number() },
    reset: {},
  },
  requirements: {
    LOAD_DASHBOARD: { role: t.string() },
  },
} satisfies ModuleSchema;

export const dashboardModule = createModule("dashboard", {
  schema: dashboardSchema,

  crossModuleDeps: { permissions: permissionsSchema },

  init: (facts) => {
    facts.widgets = [];
    facts.loaded = false;
    facts.failRate = 0;
  },

  derive: {
    widgetCount: (facts) => facts.self.widgets.length,
  },

  events: {
    setFailRate: (facts, { value }) => {
      facts.failRate = value;
    },
    reset: (facts) => {
      facts.widgets = [];
      facts.loaded = false;
    },
  },

  constraints: {
    loadDashboard: {
      after: ["permissions::loadPermissions"],
      when: (facts) => {
        return facts.permissions.role !== "" && !facts.self.loaded;
      },
      require: (facts) => ({
        type: "LOAD_DASHBOARD",
        role: facts.permissions.role,
      }),
    },
  },

  resolvers: {
    loadDashboard: {
      requirement: "LOAD_DASHBOARD",
      key: (req) => `dashboard-${req.role}`,
      retry: {
        attempts: 2,
        backoff: "exponential",
        initialDelay: 300,
      },
      resolve: async (req, context) => {
        try {
          const result = await fetchDashboard(req.role, context.facts.failRate);
          context.facts.widgets = result.widgets;
          context.facts.loaded = true;
        } catch {
          context.facts.loaded = false;
        }
      },
    },
  },
});

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works