Skip to main content

Core API

4 min read

Module & System

Modules encapsulate state and logic. Systems run modules and provide the runtime.


Creating a Module

A module is created with createModule:

import { createModule, t } from '@directive-run/core';

// Define a module with its schema and initial values
const counterModule = createModule("counter", {
  schema: {
    facts: {
      count: t.number(),
    },
    derivations: {},
    events: {},
    requirements: {},
  },

  init: (facts) => {
    facts.count = 0;
  },
});

Module Options

OptionDescription
schemaType definitions for facts, derivations, events, requirements
initInitialize facts with default values
deriveComputed values
constraintsRules that generate requirements
resolversFunctions that fulfill requirements
effectsSide effects that run on changes
snapshotEventsWhich events create time-travel snapshots (omit to snapshot all)

Creating a System

A system runs one or more modules:

import { createSystem } from '@directive-run/core';

// Single module – facts and derivations are accessed directly
// const system = createSystem({ module: counterModule });

// With plugins and debug options
const system = createSystem({
  module: counterModule,
  plugins: [loggingPlugin(), devtoolsPlugin()],
  debug: { timeTravel: true },
});

System Options

OptionDescription
moduleSingle module to run (direct access)
modulesMultiple modules as { name: module } (namespaced access)
pluginsArray of plugins
debugDebug options ({ timeTravel, maxSnapshots })
errorBoundaryError handling strategies per subsystem
initialFactsOverride initial fact values
zeroConfigEnable sensible defaults for dev mode

System API

Facts

Read and write facts directly:

// Read a fact value
const count = system.facts.count;

// Write a fact (triggers reconciliation)
system.facts.count = 10;

// Batch multiple updates into a single reconciliation
system.batch(() => {
  system.facts.count = 10;
  system.facts.loading = true;
});

Derivations

Access computed values:

// Read a computed derivation (auto-tracked, lazy, cached)
const doubled = system.derive.doubled;

Settle

Wait for all resolvers to complete:

// Trigger async work by setting a fact
system.facts.userId = 123;

// Wait for all resolvers to finish before continuing
await system.settle();

Subscribe

React to changes in facts or derivations. Both subscribe() and watch() auto-detect whether each key is a fact or derivation – you can freely mix them:

// Subscribe to specific keys (facts or derivations)
const unsubscribe = system.subscribe(["displayName", "isLoggedIn"], () => {
  console.log('Value changed:', system.derive.displayName);
});

// Mix facts and derivations in one call
const unsub2 = system.subscribe(["userId", "displayName"], () => {
  console.log("userId fact or displayName derivation changed");
});

// Watch a single key with old and new values
const unsub3 = system.watch("displayName", (newValue, prevValue) => {
  console.log(`Changed from "${prevValue}" to "${newValue}"`);
});

// Watch with a custom equality function
const unsub4 = system.watch("items", (newVal, oldVal) => {
  console.log(`Items updated: ${newVal.length} items`);
}, { equalityFn: (a, b) => a.length === b.length });

// Clean up subscriptions when no longer needed
unsubscribe();
unsub3();

When

Wait for a condition to become true. system.when() returns a promise that resolves once the predicate passes:

// Wait until the system reaches a specific state
await system.when(() => system.facts.status === "ready");

// With a timeout (rejects if condition isn't met in time)
await system.when(() => system.derive.isLoggedIn, { timeout: 5000 });

Events

Dispatch events to update facts:

// Dispatch via typed accessor (preferred – autocomplete + type checking)
system.events.increment();
system.events.setUser({ user: newUser });

// Dispatch via object syntax
system.dispatch({ type: "increment" });
system.dispatch({ type: "setUser", user: newUser });

Events are defined in the module and handler functions update facts:

events: {
  // Simple mutation – increment the count
  increment: (facts) => { facts.count += 1; },

  // Mutation with payload
  setUser: (facts, { user }) => { facts.user = user; },
},

Snapshot / Restore

Capture and restore system state:

// Capture the current state
const snapshot = system.getSnapshot();
// { facts: { userId: 123, user: {...} }, version: 1 }

// Restore to a previous snapshot
system.restore(snapshot);

Multi-Module Systems

For larger apps, compose multiple modules:

// Compose multiple modules into one system
const system = createSystem({
  modules: {
    user: userModule,
    cart: cartModule,
    checkout: checkoutModule,
  },
});

// Facts are namespaced by module name
system.facts.user.userId = 123;
system.facts.cart.items = [...system.facts.cart.items, item];

See Multi-Module for more details.


Module Factory

Use createModuleFactory() when you need multiple instances of the same module definition:

import { createModuleFactory, t } from '@directive-run/core';

const chatRoom = createModuleFactory({
  schema: {
    facts: { messages: t.array<string>(), users: t.array<string>() },
    derivations: { count: t.number() },
  },
  init: (facts) => { facts.messages = []; facts.users = []; },
  derive: { count: (facts) => facts.messages.length },
});

const system = createSystem({
  modules: {
    lobby: chatRoom("lobby"),
    support: chatRoom("support"),
  },
});

See Multi-Module for dynamic registration and factory patterns.


Module Lifecycle

  1. createModule() creates the module definition
  2. createSystem() creates the runtime with the module (plugins initialized)
  3. system.start() runs the init function, applies initialFacts/hydrate, then triggers the first reconciliation
  4. Constraints evaluate, requirements are generated, resolvers execute
  5. System settles when all requirements are fulfilled

When facts change, the reconciliation loop runs until all constraints are satisfied.


Next Steps

Previous
Overview
Next
Facts

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

Directive - Constraint-Driven State Management for TypeScript