code-docs/packages/engine.md
Runtime state management and action execution engine. The brain of Lowdefy's client-side reactivity.
This package provides:
import getContext, {
Actions,
Slots,
createLink,
Events,
Requests,
State,
} from '@lowdefy/engine';
// Create page context
const context = getContext({
config: pageConfig,
lowdefy: lowdefyContext,
...
});
┌─────────────────────────────────────────────────────────────────┐
│ Page Context │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ State ││
│ │ { formField: 'value', list: [...], nested: { ... } } ││
│ └─────────────────────────────────────────────────────────────┘│
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Events │ │ Requests │ │ Slots │ │
│ │ (handlers) │ │ (data) │ │ (blocks) │ │
│ └──────┬──────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Actions │ │
│ │ (executors) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Manages page state with methods for mutation:
class State {
constructor(context) {
this.context = context;
this.frozenState = null; // Initial state snapshot
}
set(field, value) // Set value at path
del(field) // Delete value at path
swapItems(field, from, to) // Swap array items
removeItem(field, index) // Remove array item
freezeState() // Snapshot initial state
resetState() // Restore to initial state
}
Why freeze/reset?
freezeState() captures state after onInit completesresetState() allows "Reset Form" functionalityHandles event registration and triggering:
# Events defined in config
events:
onClick:
- id: action1
type: SetState
params:
field: count
value:
_sum:
- _state: count
- 1
# With debounce (300ms default)
onSearchChange:
debounce:
ms: 500 # Debounce delay
leading: false # Fire on leading edge
trailing: true # Fire on trailing edge (default)
actions:
- id: search
type: Request
params:
requestId: searchData
Events orchestrate action execution and handle:
initEvent() preserves the shortcut string (or string array) from the event config on the runtime event object. Blocks access it via events.onClick?.shortcut to render shortcut badges.
The shortcut property is read-only metadata — the Events class doesn't handle keyboard listening. The ShortcutManager in @lowdefy/client reads shortcut strings from the block tree and registers the actual keyboard listeners via tinykeys.
Executes individual actions within events:
# Action types from plugins
SetState # Modify state
Request # Execute data request
Link # Navigate to page
CallMethod # Call block method
DisplayMessage # Show notification
Validate # Validate form
...
# Error handling with catchActions
events:
onSave:
try:
- id: saveData
type: Request
params:
requestId: saveUser
catch:
- id: showError
type: DisplayMessage
params:
type: error
content:
_error: message
Actions receive:
context - Page context with stateparams - Action parameters (operators evaluated)event - Original event objecterror - Error object (in catch actions only)Manages data request lifecycle:
// Request in config
requests:
- id: getUsers
type: MongoDBFind
connectionId: mongodb
properties:
collection: users
Requests class handles:
Manages the block tree structure (renamed from Areas in v5):
// Block slots
slots:
content:
blocks:
- id: header
type: Title
- id: form
type: Box
slots:
content:
blocks: [...]
Slots class:
class and styles)The engine also evaluates class (string, array, or cssKey-keyed object of Tailwind classes) and styles (cssKey-keyed inline style objects) alongside properties.
Each page has these state containers:
| Container | Purpose | Access |
|---|---|---|
state | Form values, user input | _state: fieldName |
urlQuery | URL query parameters | _url_query: paramName |
input | Data passed on navigation | _input: fieldName |
requests | Cached request responses | _request: requestId |
global | Cross-page shared state | _global: fieldName |
The engine evaluates operators in block properties:
# Before evaluation
properties:
title:
_if:
test:
_state: isAdmin
then: Admin Panel
else: User Dashboard
# After evaluation (if state.isAdmin = true)
properties:
title: Admin Panel
Operators are evaluated:
Event Triggered (e.g., onClick)
│
▼
Events.triggerEvent()
│
▼
For each action in event:
│
┌────┴────┐
▼ ▼
Evaluate Skip if
operators condition
in params is false
│
▼
Actions.callAction()
│
├──► SetState: Update context.state
│
├──► Request: Call API, store response
│
├──► Link: Navigate to new page
│
└──► etc.
│
▼
Re-evaluate block properties
│
▼
React re-renders
Classes provide:
Lowdefy's state model is simpler:
Client-side evaluation enables:
State is mutated directly for:
React detects changes through explicit re-render triggers.
Blocks receive evaluated properties:
// Config
blocks:
- id: greeting
type: Title
properties:
content:
_string:
- 'Hello, '
- _state: userName
- '!'
// Evaluated (state.userName = 'Alice')
block.eval.properties = {
content: 'Hello, Alice!'
}
Properties re-evaluate when: