JSPM

  • Created
  • Published
  • Downloads 3298
  • Score
    100M100P100Q108128F
  • License MIT

Constraint-driven runtime for TypeScript. Declare requirements, let the runtime resolve them.

Package Exports

  • @directive-run/core
  • @directive-run/core/adapter-utils
  • @directive-run/core/migration
  • @directive-run/core/plugins
  • @directive-run/core/testing
  • @directive-run/core/worker

Readme

@directive-run/core

npm downloads bundle size CI license

Constraint-driven runtime for TypeScript. Declare requirements, let the runtime resolve them.

  • Auto-tracking derivations – computed values that track their own dependencies, no manual dep arrays
  • Typed constraint/resolver cycle – constraints declare what must be true, resolvers make it true
  • Events – typed event handlers with payloads for imperative actions
  • Plugin architecture – logging, devtools, persistence, and custom lifecycle hooks
  • Framework adapters – first-class bindings for React, Vue, Svelte, Solid, and Lit

Install

npm install @directive-run/core

Quick Start

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

const counter = createModule("counter", {
  schema: {
    facts: { count: t.number() },
    derivations: { doubled: t.number() },
    events: { increment: {}, reset: {} },
    requirements: {},
  },

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

  derive: {
    doubled: (facts) => facts.count * 2,
  },

  events: {
    increment: (facts) => {
      facts.count += 1;
    },
    reset: (facts) => {
      facts.count = 0;
    },
  },
});

const system = createSystem({ module: counter });
system.start();

system.events.increment();
console.log(system.facts.count);     // 1
console.log(system.read("doubled")); // 2

Derived State

Derivations auto-track which facts they read. No dependency arrays, no manual subscriptions. Derivations can depend on other derivations for composition:

const app = createModule("app", {
  schema: {
    facts: { items: t.array<string>(), filter: t.string() },
    derivations: {
      filtered: t.array<string>(),
      count: t.number(),
      summary: t.string(),
    },
    events: {},
    requirements: {},
  },

  init: (facts) => {
    facts.items = ["apple", "banana", "avocado"];
    facts.filter = "a";
  },

  derive: {
    filtered: (facts) => facts.items.filter((i) => i.startsWith(facts.filter)),
    count: (_facts, derived) => derived.filtered.length,
    summary: (_facts, derived) => `${derived.count} items match`,
  },
});

Constraints and Resolvers

The constraint/resolver cycle is the core of Directive. Constraints declare what must be true. Resolvers declare how to make it true. The runtime connects them automatically.

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

const userModule = createModule("user", {
  schema: {
    facts: {
      userId: t.string().nullable(),
      profile: t.object<{ name: string }>().nullable(),
    },
    derivations: {},
    events: { login: { userId: t.string() } },
    requirements: { FETCH_PROFILE: { userId: t.string() } },
  },

  init: (facts) => {
    facts.userId = null;
    facts.profile = null;
  },

  events: {
    login: (facts, payload) => {
      facts.userId = payload.userId;
    },
  },

  constraints: {
    needsProfile: {
      when: (facts) => facts.userId !== null && facts.profile === null,
      require: (facts) => ({ type: "FETCH_PROFILE", userId: facts.userId! }),
    },
  },

  resolvers: {
    fetchProfile: {
      requirement: "FETCH_PROFILE",
      retry: { attempts: 3, backoff: "exponential" },
      resolve: async (req, context) => {
        const res = await fetch(`/api/users/${req.userId}`);
        context.facts.profile = await res.json();
      },
    },
  },
});

const system = createSystem({ module: userModule });
system.start();

// Dispatching login sets userId, which triggers the constraint,
// which emits the requirement, which the resolver fulfills automatically.
system.events.login({ userId: "u-123" });

Events

Events provide typed imperative actions with payloads. Define them in your schema and handle them with events:

events: {
  addItem: (facts, payload: { name: string; price: number }) => {
    facts.items = [...facts.items, { name: payload.name, price: payload.price }];
  },
  removeItem: (facts, payload: { id: string }) => {
    facts.items = facts.items.filter((i) => i.id !== payload.id);
  },
},

// Typed and autocompleted:
system.events.addItem({ name: "Widget", price: 9.99 });

Framework Adapters

Package Framework Reactivity Model
@directive-run/react React 18+ useSyncExternalStore hooks
@directive-run/vue Vue 3+ Ref / ShallowRef composables
@directive-run/svelte Svelte 4+ Readable stores
@directive-run/solid Solid.js 1+ Accessor signals
@directive-run/lit Lit 3+ ReactiveController classes

Subpath Exports

Import Purpose
@directive-run/core Core runtime – modules, systems, schema types
@directive-run/core/plugins Logging, devtools, persistence, observability, circuit breaker
@directive-run/core/testing Mock resolvers, fake timers, assertion helpers
@directive-run/core/migration Redux/Zustand/XState migration helpers
@directive-run/core/worker Web Worker support
@directive-run/core/adapter-utils Shared utilities for building framework adapters

Why Directive?

  • Declarative over imperative – describe what your system needs, not how to wire it up. Constraints and resolvers replace manual data-fetching orchestration.
  • Auto-tracking over manual subscriptions – derivations detect their own dependencies at runtime. No selector functions, no dependency arrays, no stale closures.
  • Constraint-driven over event-driven – instead of chaining events to coordinate async work, declare constraints that the runtime satisfies automatically with retry, batching, and error boundaries.
  • Framework-agnostic core – one state layer, five framework adapters. Move between React, Vue, Svelte, Solid, and Lit without rewriting your state logic.

Documentation

License

MIT