Back to Lowdefy

@lowdefy/engine

code-docs/packages/engine.md

5.2.09.6 KB
Original Source

@lowdefy/engine

Runtime state management and action execution engine. The brain of Lowdefy's client-side reactivity.

Purpose

This package provides:

  • Page state management (State class)
  • Action execution pipeline (Actions class)
  • Event handling (Events class)
  • Request orchestration (Requests class)
  • Block slot management (Slots class, renamed from Areas)
  • Navigation link creation

Key Exports

javascript
import getContext, {
  Actions,
  Slots,
  createLink,
  Events,
  Requests,
  State,
} from '@lowdefy/engine';

// Create page context
const context = getContext({
  config: pageConfig,
  lowdefy: lowdefyContext,
  ...
});

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Page Context                              │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                         State                                ││
│  │  { formField: 'value', list: [...], nested: { ... } }       ││
│  └─────────────────────────────────────────────────────────────┘│
│                              │                                   │
│           ┌──────────────────┼──────────────────┐               │
│           ▼                  ▼                  ▼               │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐         │
│  │   Events    │    │  Requests   │    │    Slots    │         │
│  │ (handlers)  │    │  (data)     │    │  (blocks)   │         │
│  └──────┬──────┘    └─────────────┘    └─────────────┘         │
│         │                                                       │
│         ▼                                                       │
│  ┌─────────────┐                                                │
│  │   Actions   │                                                │
│  │ (executors) │                                                │
│  └─────────────┘                                                │
└─────────────────────────────────────────────────────────────────┘

Key Classes

State

Manages page state with methods for mutation:

javascript
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 completes
  • resetState() allows "Reset Form" functionality
  • Enables undo/restore patterns

Events

Handles event registration and triggering:

yaml
# 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:

  • Sequential action execution
  • Error handling per action
  • Debounce support (prevents rapid-fire execution)
  • Event-level catch actions for error recovery
  • Keyboard shortcut metadata storage

Shortcut Support

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.

Actions

Executes individual actions within events:

yaml
# 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 state
  • params - Action parameters (operators evaluated)
  • event - Original event object
  • error - Error object (in catch actions only)

Requests

Manages data request lifecycle:

javascript
// Request in config
requests:
  - id: getUsers
    type: MongoDBFind
    connectionId: mongodb
    properties:
      collection: users

Requests class handles:

  • Request execution via API
  • Response caching in state (stores history array, not just latest)
  • Loading state management
  • Error handling
  • Automatic retry on transient failures

Slots

Manages the block tree structure (renamed from Areas in v5):

javascript
// Block slots
slots:
  content:
    blocks:
      - id: header
        type: Title
      - id: form
        type: Box
        slots:
          content:
            blocks: [...]

Slots class:

  • Builds block hierarchy
  • Evaluates block properties (including class and styles)
  • Manages block visibility
  • Handles skeleton loading

The engine also evaluates class (string, array, or cssKey-keyed object of Tailwind classes) and styles (cssKey-keyed inline style objects) alongside properties.

State Container Structure

Each page has these state containers:

ContainerPurposeAccess
stateForm values, user input_state: fieldName
urlQueryURL query parameters_url_query: paramName
inputData passed on navigation_input: fieldName
requestsCached request responses_request: requestId
globalCross-page shared state_global: fieldName

Operator Evaluation

The engine evaluates operators in block properties:

yaml
# 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:

  • When state changes
  • Before rendering blocks
  • For action parameters

Action Execution Flow

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

Design Decisions

Why Class-Based?

Classes provide:

  • Encapsulated state per instance
  • Clear lifecycle methods
  • Bound methods for callbacks
  • Easy to test in isolation

Why Not Redux/MobX?

Lowdefy's state model is simpler:

  • State is page-scoped, not global
  • No complex reducers needed
  • Actions are declarative (from config)
  • Less boilerplate for users

Why Evaluate Operators Client-Side?

Client-side evaluation enables:

  • Reactive UI updates
  • No round-trip for UI changes
  • Fast form interactions
  • Offline capability (for cached data)

State Mutation vs Immutability

State is mutated directly for:

  • Simplicity (no spread operators)
  • Performance (no object recreation)
  • Compatibility with form libraries

React detects changes through explicit re-render triggers.

Integration Points

  • @lowdefy/client: Uses engine for page context
  • @lowdefy/operators: WebParser for operator evaluation
  • @lowdefy/helpers: Utility functions
  • Action plugins: Provide action implementations

Block Property Evaluation

Blocks receive evaluated properties:

javascript
// 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:

  • State changes
  • URL query changes
  • Request completes
  • Input changes