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:
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 typedActionobject. - Controllers subscribe via
actionService.on(), run business logic, and write to State — aStateSignal. 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 abind_*attribute. No diffing, no re-render of anything unrelated.
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):
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:
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 errorConvention: 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.
// 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
ActionRecordgives 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.