Architecture
flowyd is organized into four strict layers. Imports flow in one direction only — downward. No layer may import from a layer above it.
The four layers
types/
Pure TypeScript interfaces, enums, and discriminated unions. Contains zero logic and imports nothing from the rest of the codebase. Everything else derives its type contracts from this layer.
Key exports: IState, IForkState, IJoinState, IWaitState, IGuard, GuardContext, TransitionDefinition, InstanceSnapshot, DispatchResult, WorkflowDefinition, StateKind, StateStatus, JoinMode.
states/ and guards/
Concrete implementations of IState and IGuard. Import only from types/. Neither layer is aware of the engine, the builder, or each other.
states/—BaseState,StepState,ForkState,JoinState,WaitStateguards/—FnGuard,InjectedGuard,AndGuard,OrGuard,NotGuard,StateCompletedGuard,StateActiveGuard,AlwaysGuard,NeverGuard,Guard(factory namespace)
core/
The engine, builder, instance, and state/guard registries. Imports from types/, states/, and guards/. The main files (plus utils.ts helpers and an index.ts barrel):
| File | Responsibility |
|---|---|
builder.ts | Fluent WorkflowBuilder — accumulates the graph definition |
workflow.ts | Immutable Workflow factory — createInstance, restoreInstance, getDefinition |
instance.ts | Mutable WorkflowInstance — holds snapshot, exposes dispatch, canExecute, etc. |
engine.ts | Pure, stateless WorkflowEngine — computes the next snapshot from a current snapshot + action |
registry.ts | StateRegistry and GuardRegistry — lookup maps with explicit error messages |
visualization/
Stateless exporters. Import only from types/ (they need WorkflowDefinition and InstanceSnapshot shapes — nothing else). The core/ layer has zero knowledge that this layer exists.
This is enforced by keeping visualization as a separate package entry point (flowyd/visualization). Tree-shakers strip it when unused.
Why one-directional imports?
Bidirectional imports create invisible coupling — e.g. if core/engine.ts imported MermaidExporter, a visualization bug could corrupt the engine. Wanting to import "upward" signals a wrong abstraction boundary: extract the shared concept into types/ instead.
No exceptions — every cross-layer dependency goes through a types/ interface.
File map
src/
├── types/
│ ├── state.ts ← StateKind, StateStatus, IState, IForkState, IJoinState, IWaitState, JoinMode
│ ├── guard.ts ← IGuard, GuardFn, GuardContext
│ ├── transition.ts ← TransitionDefinition
│ ├── instance.ts ← ReadonlyInstanceState, InstanceSnapshot, DispatchResult, HistoryEntry, HookContext/HookFn/StateHooks
│ ├── workflow.ts ← WorkflowDefinition, ActionPayloadMap
│ └── index.ts ← barrel re-export
│
├── states/
│ ├── base.ts
│ ├── step-state.ts
│ ├── fork-state.ts
│ ├── join-state.ts
│ ├── wait-state.ts
│ └── index.ts
│
├── guards/
│ ├── constant-guards.ts ← AlwaysGuard, NeverGuard
│ ├── fn-guard.ts ← FnGuard
│ ├── inject-guard.ts ← InjectedGuard
│ ├── and-guard.ts
│ ├── or-guard.ts
│ ├── not-guard.ts
│ ├── state-guard.ts ← StateCompletedGuard, StateActiveGuard
│ ├── factory.ts ← Guard namespace
│ └── index.ts
│
├── core/
│ ├── builder.ts
│ ├── workflow.ts
│ ├── instance.ts
│ ├── engine.ts
│ ├── registry.ts
│ ├── utils.ts
│ └── index.ts
│
└── visualization/
├── exporter.ts
├── mermaid.ts
├── json-graph.ts
└── index.ts