Skip to content

Latest commit

 

History

History
939 lines (726 loc) · 26.1 KB

File metadata and controls

939 lines (726 loc) · 26.1 KB

Central State Management: Deep Architecture Analysis

Analysis Date: 2026-01-04 Analyzer: Claude Code (Architecture Study) Focus: Feasibility of central state store in Makepad framework Context: P2 State Management task & App Contributor System design


Executive Summary

Finding: Traditional centralized state management (Redux, Zustand pattern) is NOT feasible in Makepad's architecture due to fundamental differences in widget ownership and borrow checking models.

Alternative: Shell-coordinated state pattern IS feasible and aligns with Makepad's design philosophy.

App Contributor System:Already well-designed with current MofaApp trait. Minor enhancements recommended.

Recommendation: Skip P2 "Store" implementation. Use shell as state coordinator instead.


1. Makepad's Architecture Model

1.1 Philosophy: Component-Based Ownership

Makepad follows traditional desktop UI patterns (Win32, WxWidgets, Flutter) rather than modern web frameworks:

┌─────────────────────────────────────────────────────────────────────┐
│                    WEB FRAMEWORKS (React/Redux)                    │
├─────────────────────────────────────────────────────────────────────┤
│  • UI = f(state) - Pure function transformation                   │
│  • Centralized store (Redux, Zustand, Jotai)                      │
│  • Props flow down, actions flow up                              │
│  • Components are functions, hooks for effects                    │
│  • Shared state via context/selectors                            │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                      MAKEPAD FRAMEWORK                             │
├─────────────────────────────────────────────────────────────────────┤
│  • Widgets own their state locally                                │
│  • No global store by design                                     │
│  • Parent controls child via WidgetRef methods                   │
│  • Components are structs with owned data                        │
│  • Borrow checking enforces single mutable reference            │
│  • State lifetime = Widget lifetime                             │
└─────────────────────────────────────────────────────────────────────┘

Key Difference:

  • Web frameworks: State exists outside components, flows in
  • Makepad: State exists inside components, accessed via methods

1.2 Widget State Ownership Pattern

Each widget owns its state as struct fields:

#[derive(Live, LiveHook, Widget)]
pub struct MoFaFMScreen {
    #[deref]
    view: View,  // Composed child widgets

    #[rust]
    log_panel_collapsed: bool,        // ← OWNED by this widget
    #[rust]
    audio_manager: Option<AudioManager>, // ← OWNED by this widget
    #[rust]
    log_entries: Vec<String>,          // ← OWNED by this widget
}

State Invariants:

  1. State lives inside the widget struct as #[rust] fields
  2. State lifetime is tied to widget lifetime
  3. No Arc<Mutex<T>> or RwLock<T> - no shared ownership
  4. Parent widgets own children via WidgetRef fields
  5. Makepad's runtime borrow checker enforces single mutable reference

1.3 WidgetRef: Safe Cross-Boundary Access

Makepad generates WidgetRef types for safe access across widget boundaries:

// Auto-generated by Makepad's derive macro
impl MoFaFMScreenRef {
    pub fn stop_timers(&self, cx: &mut Cx) {
        if let Some(inner) = self.borrow_mut() {
            cx.stop_timer(inner.audio_timer);
        }
    }

    pub fn add_log_entry(&self, cx: &mut Cx, entry: String) {
        if let Some(mut inner) = self.borrow_mut() {
            inner.log_entries.push(entry);
            inner.view.redraw(cx);
        }
    }

    pub fn update_dark_mode(&self, cx: &mut Cx, dark_mode: f64) {
        if let Some(mut inner) = self.borrow_mut() {
            inner.view.apply_over(cx, live!{
                draw_bg: { dark_mode: (dark_mode) }
            });
            inner.view.redraw(cx);
        }
    }
}

WidgetRef Characteristics:

  • Smart pointer to the actual widget
  • borrow_mut() provides runtime borrow-checked mutable access
  • Methods on WidgetRef form the public API for external control
  • Shell calls these methods to coordinate child widgets

2. Current App System Analysis

2.1 MofaApp Trait (Current Design)

// mofa-widgets/src/app_trait.rs
pub trait MofaApp {
    fn info() -> AppInfo where Self: Sized;
    fn live_design(cx: &mut Cx);
}

pub trait TimerControl {
    fn stop_timers(&self, cx: &mut Cx);
    fn start_timers(&self, cx: &mut Cx);
}

pub struct AppRegistry {
    apps: Vec<AppInfo>,  // Metadata only, no state
}

Strengths:

  • ✅ Standardized metadata (name, id, description)
  • ✅ Consistent registration pattern
  • ✅ Timer lifecycle control for resource management
  • ✅ Works within Makepad's constraints
  • ✅ Allows compile-time type safety

Limitations:

  • ⚠️ Apps require compile-time imports in shell (Makepad limitation)
  • ⚠️ No inter-app communication mechanism
  • ⚠️ No shared state between apps (by design)

2.2 Current Shell → App Coupling

Coupling Point 1: Compile-time Imports (REQUIRED by Makepad)

// mofa-studio-shell/src/app.rs
use mofa_fm::screen::MoFaFMScreen;      // ← MUST import concrete type
use mofa_settings::screen::SettingsScreen; // ← MUST import concrete type

live_design! {
    fm_page = <MoFaFMScreen> {}  // ← Concrete type required in DSL
}

Why this is required:

  • live_design! macro is evaluated at compile time
  • Macro expansion needs concrete types for code generation
  • No trait objects or dynamic dispatch in DSL
  • This is a fundamental limitation of Makepad, not a design choice

Coupling Point 2: Direct Method Calls

// Shell controls apps via WidgetRef methods
impl App {
    fn stop_fm_timers(&mut self, cx: &mut Cx) {
        self.ui.mo_fa_fmscreen(ids!(fm_page)).stop_timers(cx);
    }

    fn apply_dark_mode_screens(&mut self, cx: &mut Cx) {
        self.ui.mo_fa_fmscreen(ids!(fm_page))
            .update_dark_mode(cx, self.dark_mode_anim);
        self.ui.settings_screen(ids!(settings_page))
            .update_dark_mode(cx, self.dark_mode_anim);
    }
}

Coupling Point 3: Event Interception

// Shell can intercept child widget events
impl App {
    fn handle_mofa_hero_buttons(&mut self, cx: &mut Cx, event: &Event) {
        let start_btn = self.ui.view(ids!(fm_page.mofa_hero.action_section.start_view));
        match event.hits(cx, start_btn.area()) {
            Hit::FingerUp(_) => {
                // Handle start/stop logic at shell level
                self.start_dataflow(cx);
            }
            _ => {}
        }
    }
}

3. Feasibility of Central State Store

3.1 Architectural Barriers

Barrier 1: Widget Tree Borrow Checking

Makepad's widget tree uses runtime borrow checking that conflicts with shared mutable state:

// ❌ This pattern DOES NOT WORK in Makepad:

struct AppState {
    data: Arc<Mutex<Vec<String>>>,  // Try to share state
}

impl Widget {
    fn update(&mut self, cx: &mut Cx) {
        // Borrow conflict: Multiple widgets try to access shared state
        let widget1 = self.ui.widget1(ids!(w1));
        let widget2 = self.ui.widget2(ids!(w2));

        widget1.borrow_mut().update(cx, &self.app_state);  // Borrow 1
        widget2.borrow_mut().update(cx, &self.app_state);  // Borrow 2 - CONFLICT!
    }
}

Why it fails:

  • WidgetRef::borrow_mut() is runtime borrow checked
  • Makepad tracks active borrows to prevent aliasing violations
  • Overlapping mutable borrows across different widgets cause runtime panics
  • No Arc<Mutex<T>> can work around this - the borrow checker is at the framework level

Barrier 2: No Dependency Injection

Widgets are instantiated by Makepad's runtime, not user code:

// Makepad internally creates widgets:
let widget = MoFaFMScreen::new(cx);  // You don't control this

// ❌ You CANNOT inject state:
// let widget = MoFaFMScreen::with_store(app_store);
// let widget = MoFaFMScreen::new_with_state(cx, shared_state);

No constructor control:

  • Widget creation happens in live_design! macro expansion
  • No custom constructors or builder patterns
  • No way to pass dependencies at creation time
  • State must be set after creation via method calls

Barrier 3: Live DSL Requires Concrete Types

live_design! {
    // ❌ Trait objects DON'T WORK:
    apps: Vec<Box<dyn MofaApp>>,

    // ❌ Dynamic dispatch DON'T WORK:
    app = <dyn MofaAppWidget>,

    // ❌ Generics DON'T WORK:
    app = <AppWidget<dyn MofaApp>>,

    // ✅ Only concrete types work:
    fm_page = <MoFaFMScreen> {},
    settings_page = <SettingsScreen> {},
}

Why:

  • live_design! is a macro that generates Rust code
  • Macro expansion needs to know exact types for monomorphization
  • No type erasure or dynamic dispatch in generated code
  • This is by design for performance and code generation

3.2 What IS Possible

Option A: File-Based Shared State ✅ (Current Pattern)

// All apps read/write to disk
pub struct Preferences {
    pub dark_mode: bool,
    pub providers: Vec<Provider>,
}

impl Preferences {
    pub fn load() -> Result<Self> {
        // Read from ~/.dora/dashboard/preferences.json
    }

    pub fn save(&self) -> Result<()> {
        // Write to ~/.dora/dashboard/preferences.json
    }
}

// Each app independently:
let prefs = Preferences::load()?;
self.dark_mode = prefs.dark_mode;

Pros:

  • ✅ Already works
  • ✅ Simple and reliable
  • ✅ Durable across restarts
  • ✅ No sharing conflicts

Cons:

  • ⚠️ No real-time synchronization
  • ⚠️ Serialization overhead
  • ⚠️ File I/O latency

Verdict: Keep as durability layer, combine with other patterns


Option B: Shell as State Coordinator ✅ RECOMMENDED

impl App {
    #[rust]
    app_state: AppState,  // Shell owns all shared state

    fn notify_dark_mode_change(&mut self, cx: &mut Cx) {
        // Propagate to all children via WidgetRef methods
        self.ui.mo_fa_fmscreen(ids!(fm_page))
            .on_dark_mode_change(cx, self.app_state.dark_mode);
        self.ui.settings_screen(ids!(settings_page))
            .on_dark_mode_change(cx, self.app_state.dark_mode);
        self.ui.sidebar(ids!(sidebar_menu))
            .on_dark_mode_change(cx, self.app_state.dark_mode);
    }
}

// Apps implement state change handlers:
impl MoFaFMScreenRef {
    pub fn on_dark_mode_change(&self, cx: &mut Cx, enabled: bool) {
        if let Some(mut inner) = self.borrow_mut() {
            inner.dark_mode = enabled;
            inner.view.redraw(cx);
        }
    }
}

Pros:

  • ✅ Single source of truth
  • ✅ Type-safe (Rust's type system)
  • ✅ Follows Makepad patterns
  • ✅ Explicit data flow (easy to debug)
  • ✅ No runtime locks or borrow conflicts

Cons:

  • ⚠️ Manual wiring required
  • ⚠️ Shell becomes coordinator

Verdict: Best approach for Makepad architecture


Option C: Event Bus Pattern ✅ (Optional Enhancement)

pub enum AppEvent {
    DarkModeChanged { enabled: bool },
    ProviderAdded { provider: Provider },
    ProviderRemoved { id: ProviderId },
    DataflowStarted,
    DataflowStopped,
}

impl App {
    fn broadcast_event(&mut self, cx: &mut Cx, event: AppEvent) {
        // Send to all interested widgets
        self.ui.mo_fa_fmscreen(ids!(fm_page))
            .handle_app_event(cx, &event);
        self.ui.settings_screen(ids!(settings_page))
            .handle_app_event(cx, &event);
    }
}

// Apps implement event handler:
impl MoFaFMScreenRef {
    pub fn handle_app_event(&self, cx: &mut Cx, event: &AppEvent) {
        match event {
            AppEvent::DarkModeChanged { enabled } => {
                self.on_dark_mode_change(cx, *enabled);
            }
            _ => {} // Ignore irrelevant events
        }
    }
}

Pros:

  • ✅ Decoupled (event emitter doesn't know receivers)
  • ✅ Extensible (add new events without changing callers)
  • ✅ Type-safe (enum variants)

Cons:

  • ⚠️ More boilerplate
  • ⚠️ Still need manual wiring

Verdict: Good addition for complex interactions


3.3 Why Traditional Patterns DON'T Work

Redux-Style Store ❌

// ❌ DOES NOT WORK in Makepad:

pub struct Store<T> {
    state: Arc<RwLock<T>>,
    reducers: Vec<Box<dyn Reducer<T>>>,
    subscribers: Vec<Box<dyn Subscriber<T>>>,
}

impl<T> Store<T> {
    pub fn dispatch(&self, action: Action) {
        // Multiple widgets try to borrow simultaneously
        let state = self.state.write().unwrap();  // Lock acquired

        for subscriber in &self.subscribers {
            subscriber.notify(&state);  // Each subscriber borrows widget
            // CONFLICT: WidgetRef::borrow_mut() while RwLock held
        }
    }
}

Why it fails:

  1. Arc<RwLock<T>> conflicts with WidgetRef::borrow_mut()
  2. Lock ordering deadlocks (widget lock then store lock)
  3. Makepad's runtime borrow checker doesn't know about RwLock
  4. Nested borrowing causes runtime panics

Zustand-Style Hooks ❌

// ❌ DOES NOT WORK in Makepad:

let state = use_store(cx);  // No hooks system

impl Widget {
    fn draw(&mut self, cx: &mut Cx) {
        let dark_mode = self.state.dark_mode;  // Can't access external state
        // Widgets only have access to their own #[rust] fields
    }
}

Why it fails:

  1. No hooks system in Makepad
  2. Widgets can only access their own state
  3. No context or dependency injection
  4. State is owned, not referenced

Context/Provider Pattern ❌

// ❌ DOES NOT WORK in Makepad:

live_design! {
    <AppStateProvider value={app_state}>
        <MoFaFMScreen />  // Can't pass props like this
    </AppStateProvider>
}

Why it fails:

  1. No props system in live_design!
  2. No component composition with props
  3. Widgets aren't functions
  4. No HOC (Higher-Order Component) pattern

4. App Contributor System Design

4.1 Current System: Already 90% There

The current MofaApp trait system is well-designed:

// To add a new app, contributor creates:

// 1. App crate (apps/my_app/Cargo.toml)
[package]
name = "my-app"

// 2. App implementation (apps/my-app/src/lib.rs)
pub struct MyAppApp;

impl MofaApp for MyAppApp {
    fn info() -> AppInfo {
        AppInfo {
            name: "My App",
            id: "my-app",
            description: "Does cool things",
        }
    }

    fn live_design(cx: &mut Cx) {
        screen::live_design(cx);
    }
}

// 3. Shell modifications (3 lines of code)

// In mofa-studio-shell/src/app.rs imports:
use my_app::screen::MyAppScreen;

// In LiveRegister:
<MyAppApp as MofaApp>::live_design(cx);

// In after_new_from_doc:
self.app_registry.register(MyAppApp::info());

// In live_design!:
my_app_page = <MyAppScreen> {}

Why this works well:

  • ✅ Minimal shell changes (3-4 lines)
  • ✅ No changes to app fundamentals
  • ✅ Type-safe at compile time
  • ✅ Works within Makepad constraints
  • ✅ Clear contributor workflow

4.2 Recommended Enhancements

Enhancement 1: State Dependencies Declaration

pub trait MofaApp {
    fn info() -> AppInfo where Self: Sized;
    fn live_design(cx: &mut Cx);

    // NEW: Declare what state this app needs
    fn state_dependencies() -> StateDependencies {
        StateDependencies {
            reads: vec!["dark_mode"],
            writes: vec![],
        }
    }
}

pub struct StateDependencies {
    pub reads: Vec<&'static str>,
    pub writes: Vec<&'static str>,
}

Enhancement 2: State Change Listener Trait (Optional)

// Apps implement this if they need state change notifications
pub trait StateChangeListener {
    fn on_dark_mode_change(&mut self, cx: &mut Cx, enabled: bool);
    fn on_provider_change(&mut self, cx: &mut Cx, providers: &[Provider]);
}

// WidgetRef implementation
impl StateChangeListener for MoFaFMScreen {
    fn on_dark_mode_change(&mut self, cx: &mut Cx, enabled: bool) {
        self.dark_mode = enabled;
        self.update_ui(cx);
    }
}

4.3 Enhanced Contributor Workflow

With enhancements, adding an app requires:

  1. ✅ Create struct implementing MofaApp
  2. ✅ Add 3 lines to shell (imports, registration, live_design)
  3. ➕ Optionally declare state dependencies
  4. ➕ Optionally implement state change listeners

NO changes to:

  • ❌ App fundamentals
  • ❌ Widget ownership model
  • ❌ State management architecture
  • ❌ Makepad framework constraints

5. Implementation Recommendations

5.1 For P2 State Management Task

Recommendation: DON'T implement traditional state store

Pattern Feasibility Effort Recommendation
Redux-style store ❌ Not feasible High Don't pursue
Zustand-style hooks ❌ Not supported High Don't pursue
Arc<Mutex<T>> sharing ❌ Conflicts with borrow checker Medium Don't pursue
Context/Provider ❌ No props system High Don't pursue
Shell coordinator ✅ Works Low Do this
File-based ✅ Already done None Keep as-is
Event bus ✅ Works Medium Optional

Recommended Implementation:

// 1. Shell owns shared state
impl App {
    #[rust]
    app_state: AppState,
}

pub struct AppState {
    pub dark_mode: bool,
    pub providers: Vec<Provider>,
    pub active_dataflow: bool,
    // Add more as needed
}

// 2. Apps declare dependencies
impl MofaApp for MoFaFMApp {
    fn state_dependencies() -> StateDependencies {
        StateDependencies {
            reads: vec!["dark_mode", "active_dataflow"],
            writes: vec!["active_dataflow"],
        }
    }
}

// 3. Shell broadcasts changes
impl App {
    fn notify_dark_mode_change(&mut self, cx: &mut Cx) {
        // Use WidgetRef methods to notify children
        self.broadcast_to_apps(cx, |app| {
            app.on_dark_mode_change(cx, self.app_state.dark_mode);
        });
    }
}

// 4. File persistence for durability
impl AppState {
    pub fn load() -> Self {
        let prefs = Preferences::load().unwrap_or_default();
        Self {
            dark_mode: prefs.dark_mode,
            providers: prefs.providers,
            active_dataflow: false,
        }
    }

    pub fn save(&self) {
        let prefs = Preferences {
            dark_mode: self.dark_mode,
            providers: self.providers.clone(),
        };
        prefs.save().ok();
    }
}

5.2 For App Contributor System

Status: ✅ Already well-designed

Current MofaApp trait works well. Enhancements:

  1. Keep MofaApp trait as-is - simple, effective
  2. Keep TimerControl trait - useful for lifecycle
  3. Add StateDependencies - let apps declare needs
  4. Add StateChangeListener - optional for notifications
  5. Keep compile-time imports - Makepad limitation, accept it

5.3 Updated P2 Success Criteria

Criterion Original Updated Status
Single source of truth for state Central store Shell coordinator ✅ Feasible
Automatic UI updates Subscribers WidgetRef methods ✅ Works
State persists across restarts File I/O File I/O ✅ Already done
Contributors can add apps easily Plugin system MofaApp trait ✅ Already works

6. Key Insights

6.1 Makepad ≠ Web Frameworks

Aspect Web (React/Redux) Makepad (Desktop UI)
State location Central store Component-owned
Data flow Unidirectional (props down, actions up) Parent↔Child methods
Mutation Actions/Reducers Direct method calls
Sharing Context/Selectors WidgetRef borrow_mut()
Lifecycle Effect hooks LiveHook traits
Paradigm Functional transformations Component ownership

6.2 Embrace the Model

Makepad's architecture is intentional, not broken:

  • Component ownership prevents bugs (no hidden global mutations)
  • Borrow checking prevents race conditions at compile time
  • WidgetRef provides safe cross-boundary access
  • File persistence provides durability without complexity
  • Explicit data flow makes debugging easier

6.3 For Contributors

Adding an app is already easy:

  • Create struct implementing MofaApp
  • Add 3-4 lines to shell
  • No core refactoring needed

What contributors DON'T need:

  • ❌ Understanding shell internals
  • ❌ Modifying state management
  • ❌ Learning complex patterns
  • ❌ Breaking existing apps

What contributors DO need:

  • ✅ Implement MofaApp trait
  • ✅ Define widgets in live_design!
  • ✅ Handle events in their widgets
  • ✅ Optionally implement TimerControl

7. Conclusion

7.1 Central State Management: Not Recommended

Finding: Traditional centralized state management is architecturally incompatible with Makepad's component ownership model.

Alternatives that work:

  1. ✅ Shell as state coordinator (primary)
  2. ✅ File-based persistence (durability)
  3. ✅ Event broadcasting (optional, for complex cases)

What to avoid:

  • ❌ Redux-style stores
  • ❌ Shared mutable state (Arc<Mutex<T>>)
  • ❌ Context/Provider patterns
  • ❌ Dependency injection

7.2 App Contributor System: Already Works

Finding: Current MofaApp trait system is well-designed for contributors.

Minor enhancements recommended:

  • State dependencies declaration
  • State change listener trait (optional)

No major changes needed.

7.3 Updated Roadmap Impact

P2 State Management task: REVISED

Original plan:

  • Design Store<T> type ❌
  • Implement subscribers ❌
  • Add middleware ❌

Revised plan:

  • ✅ Shell owns AppState struct
  • ✅ Apps declare StateDependencies
  • ✅ WidgetRef methods for state updates
  • ✅ File persistence for durability

Timeline: 1-2 weeks (not 4-6 weeks)


8. Action Items

Immediate (Week 1)

  • Add AppState struct to App
  • Implement shell coordinator pattern
  • Document contributor workflow

Short Term (Week 2-3)

  • Add StateDependencies to MofaApp trait
  • Implement state change notifications
  • Create contributor guide

Cancelled

  • Design Store<T> type - Not feasible
  • Implement action/reducer pattern - Not compatible
  • Add middleware system - Not needed

Appendix A: Code Examples

A.1 Shell Coordinator Pattern

// mofa-studio-shell/src/app.rs
impl App {
    #[rust]
    app_state: AppState,

    fn toggle_dark_mode(&mut self, cx: &mut Cx) {
        self.app_state.dark_mode = !self.app_state.dark_mode;

        // Save to disk
        self.app_state.save();

        // Broadcast to all widgets
        self.apply_dark_mode_panels(cx);
        self.apply_dark_mode_screens(cx);

        // Notify apps that implement StateChangeListener
        self.notify_state_change(cx, StateEvent::DarkModeChanged {
            enabled: self.app_state.dark_mode
        });
    }

    fn notify_state_change(&mut self, cx: &mut Cx, event: StateEvent) {
        // Notify interested apps
        self.ui.mo_fa_fmscreen(ids!(fm_page))
            .on_state_event(cx, &event);
        self.ui.settings_screen(ids!(settings_page))
            .on_state_event(cx, &event);
    }
}

A.2 App with State Dependencies

// apps/my_app/src/lib.rs
pub struct MyAppApp;

impl MofaApp for MyAppApp {
    fn info() -> AppInfo {
        AppInfo {
            name: "My App",
            id: "my-app",
            description: "Demonstrates state management",
        }
    }

    fn live_design(cx: &mut Cx) {
        screen::live_design(cx);
    }

    fn state_dependencies() -> StateDependencies {
        StateDependencies {
            reads: vec!["dark_mode", "providers"],
            writes: vec![],
        }
    }
}

A.3 State Change Handler

// apps/my_app/src/screen.rs
impl MyAppScreenRef {
    pub fn on_state_event(&self, cx: &mut Cx, event: &StateEvent) {
        match event {
            StateEvent::DarkModeChanged { enabled } => {
                if let Some(mut inner) = self.borrow_mut() {
                    inner.dark_mode = *enabled;
                    inner.view.redraw(cx);
                }
            }
            StateEvent::ProvidersChanged { providers } => {
                if let Some(mut inner) = self.borrow_mut() {
                    inner.providers = providers.clone();
                    inner.view.redraw(cx);
                }
            }
            _ => {}
        }
    }
}

Appendix B: Comparison with Other Frameworks

B.1 Flutter (Closest Analog)

Similarities:

  • Widgets own state
  • Parent controls child via methods
  • No global store

Differences:

  • Flutter has InheritedWidget for state sharing
  • Makepad has no equivalent (by design)

B.2 React (Opposite Approach)

React:

  • State outside components
  • Hooks for effects
  • Context for sharing

Makepad:

  • State inside components
  • LiveHook for lifecycle
  • WidgetRef for access

Document Version: 1.0 Last Updated: 2026-01-04 Related Documents: