Documentation

Architecture & UDF

The View → Action → Controller → State cycle and the Actor Model.

Flux is not a component framework — it is an enforced data-flow discipline. Every rule in this page exists to answer one question consistently: who is allowed to touch what. Get that right and the rest of your application becomes independently testable, independently replaceable, and predictable under change.

The cycle

Data moves in exactly one direction:

the cycle
View  →  Action  →  Controller  →  State  →  View
  • Views (plain HTML + directives) never mutate state directly. They read from signals and dispatch actions via declarative on-<event> attributes.
  • Actions are captured by one global delegated listener per event type on document.body, resolved against [action_context], passed through registered modifiers, and dispatched as a typed Action object.
  • Controllers subscribe via actionService.on(), run business logic, and write to State — a StateSignal. Controllers never touch the DOM.
  • State notifies its subscribers, which update only the DOM nodes that actually depend on it — via a directive's subscribe_(), or a bind_* attribute. No diffing, no re-render of anything unrelated.
Note

This is why controllers are "dumb message routers": a controller is forbidden from calling methods on a state signal it does not own, and a view is forbidden from importing a controller directly. The action bus is the only channel between them.

The AFSA action object

Every dispatched action is a full object — not a bare payload — called an Alwatr Flux Standard Action (AFSA):

ts
interface Action<K extends keyof ActionRecord> {
  type: K;
  payload: ActionRecord[K];
  context?: string;               // nearest [action_context] ancestor value
  meta?: Record<string, unknown>; // modifiers may enrich this
}

ActionRecord is an empty interface your application augments via TypeScript declaration merging — every action type and its payload shape is checked at compile time, both when dispatching and when handling:

ts
declare module '@alwatr/flux' {
  interface ActionRecord {
    ui_add_to_cart: {productId: number; qty: number};
    ui_logout: void;
  }
}

actionService.on('ui_add_to_cart', (action) => {
  // action.payload is {productId: number; qty: number} — fully typed
  cartService.add(action.payload.productId, action.payload.qty);
});

actionService.dispatch({type: 'ui_add_to_cart', payload: {productId: 42, qty: 1}}); // ✅
actionService.dispatch({type: 'ui_add_to_cart', payload: 'wrong'});                 // ❌ compile error
Tip

Convention: prefix UI-originated action types with ui_ (dispatched from HTML via on-click, etc.) and leave programmatic, code-originated actions unprefixed — it makes the origin of every action greppable at a glance.

Why not a Virtual DOM?

A Virtual DOM trades a class of correctness bugs for a re-render cost you pay on every state change, whether or not the affected node actually changed. Flux instead tracks dependencies at the signal level: a ComputedSignal only recomputes when one of its declared deps changes, and a directive's DOM write only fires when the exact signal it subscribed to emits. There is nothing to diff, because nothing renders that didn't change.

The Actor Model variant

For complex, decoupled feature modules, Flux combines @alwatr/fsm (see Finite State Machines) with the global action bus to form an Actor: private state (a stateSignal), a private mailbox (dispatch), and outbound communication exclusively through actions — no direct imports between feature modules.

ts
// The actor's only inbound door: translate global actions into local FSM events.
actionService.on('ui_start_upload', (action) => {
  uploadActor.dispatch({type: 'START_UPLOAD', fileId: action.payload.fileId});
});

// The actor's only outbound door: broadcast back out via the action bus.
uploadActor.stateSignal.subscribe((state) => {
  if (state.name === 'success') {
    actionService.dispatch({type: 'upload_completed', payload: state.context});
  }
});

Design pillars

  • Fine-grained reactivity. Signals track exact dependencies — no component tree, no re-render cascades.
  • O(1) global event delegation. One listener per event type handles every element on the page, including ones added after boot — inspired by Qwik's resumability model.
  • Compile-time type safety. Declaration merging on ActionRecord gives the entire action bus full TypeScript coverage with zero runtime overhead.
  • Simplicity over cleverness. No magic, no hidden re-renders, no performance cliffs — every update is one you can point to in the source.