Meetily NotificationManagerState Pattern for User Preferences: Implementation Deep Dive

Meetily implements a centralized state-management pattern using Arc<RwLock<T>> to provide thread-safe, persistent notification preferences with deterministic decision gates.

Meetily, an open-source meeting assistant from Zackriya-Solutions, centralizes user notification preferences through a robust NotificationManagerState pattern. This Rust-based implementation ensures consistent behavior across async threads while maintaining persistent storage. The pattern combines immutable state containers with a decision-gate architecture to guarantee that every notification respects user consent and system permissions.

Architectural Foundations of the NotificationManagerState Pattern

The NotificationManagerState pattern rests on three pillars: thread-safe state containers, persistent consent management, and a mandatory decision gate. These components work together in frontend/src-tauri/src/notifications/manager.rs to create a single source of truth for notification preferences.

Immutable Thread-Safe State Containers

All mutable data within the notification system is wrapped in Arc<RwLock<T>> to enable concurrent read access with exclusive write capabilities. The NotificationManager struct holds settings: Arc<RwLock<NotificationSettings>> and initialized: Arc<RwLock<bool>>, allowing async tasks to read current preferences without blocking while writers obtain exclusive locks only during persistence operations.

This approach eliminates race conditions when the frontend queries state while the backend updates preferences, ensuring the React UI receives consistent snapshots of the NotificationSettings struct.

The ConsentManager abstraction handles disk I/O for JSON storage at $XDG_CONFIG/meetily/notifications.json. During initialization, the manager invokes get_settings_with_migration to load existing preferences, while update_settings triggers ConsentManager::save_settings to persist changes immediately.

This guarantees that user preferences survive application restarts and remain synchronized between the in-memory state and the filesystem. The persistence layer validates settings through validate_settings before committing data, preventing corruption of the notification configuration.

Multi-Layer Decision Gate

Before displaying any notification, the should_show_notification method applies three sequential checks:

  1. Consent and system permission validation – Verifies OS-level notification permissions and user consent
  2. Do-Not-Disturb evaluation – Checks both manual DND state and system DND status
  3. Per-type preference verification – Validates specific flags like show_recording_started

Only when all three checks pass does the manager forward the request to SystemNotificationHandler in frontend/src-tauri/src/notifications/system.rs. This gate pattern ensures zero notifications bypass user preferences, even when triggered by background recording events.

NotificationManager Lifecycle and State Operations

The NotificationManagerState pattern follows a strict lifecycle that maintains consistency between the Tauri backend and React frontend.

Construction and Lazy Initialization

Construction occurs through NotificationManager::new(app_handle).await, which instantiates SystemNotificationHandler and ConsentManager, then loads stored settings into the Arc<RwLock<T>> container. The initialize() method operates lazily, deferring OS permission requests until the first notification requirement, optimizing startup performance.

Reading State Snapshots

The get_settings() method returns a cloned NotificationSettings snapshot using Arc<RwLock<NotificationSettings>>.read().await.clone(). This immutable snapshot pattern prevents the frontend from holding references to internal state while allowing the UI to render current preferences without blocking the manager's async operations.

Writing and Validating Updates

When the UI modifies preferences, update_settings(new) validates the payload, writes to disk via ConsentManager, and atomically swaps the in-memory value through Arc<RwLock<NotificationSettings>>.write().await. This transactional approach ensures that disk state and memory state remain synchronized even if the operation encounters errors mid-write.

Implementation Examples

The following code demonstrates practical usage of the NotificationManagerState pattern within Meetily's Tauri architecture.

Initializing the Notification Manager

// frontend/src-tauri/src/notifications/commands.rs
#[tauri::command]
pub async fn init_notifications<R: Runtime>(app: AppHandle<R>) -> Result<()> {
    let manager = NotificationManager::new(app.clone()).await?;
    manager.initialize().await?;
    // Store the manager in AppState for subsequent commands
    Ok(())
}

Displaying Notifications with Preference Checks

async fn on_recording_start<R: Runtime>(mgr: &NotificationManager<R>, meeting: Option<String>) {
    // Internally checks consent, DND, and show_recording_started flag
    if let Err(e) = mgr.show_recording_started(meeting).await {
        log::error!("Failed to show start notification: {}", e);
    }
}

Updating Preferences from the Frontend

TypeScript invocation from React:

// Frontend React component
await invoke('update_notification_settings', {
  newSettings: {
    recording_notifications: true,
    time_based_reminders: false,
    notification_preferences: {
      show_recording_started: true,
      show_recording_stopped: false,
      meeting_reminder_minutes: [10, 2],
    },
  },
});

Corresponding Rust command handler:

// frontend/src-tauri/src/notifications/commands.rs
#[tauri::command]
pub async fn update_notification_settings<R: Runtime>(
    app: AppHandle<R>,
    new_settings: NotificationSettings,
) -> Result<()> {
    let mgr = /* retrieve from AppState */;
    mgr.update_settings(new_settings).await
}

Toggling Do-Not-Disturb Mode

#[tauri::command]
pub async fn set_manual_dnd<R: Runtime>(app: AppHandle<R>, enabled: bool) -> Result<()> {
    let mgr = /* retrieve from AppState */;
    mgr.set_manual_dnd(enabled).await
}

Key Source Files and Module Organization

The NotificationManagerState pattern spans five core files within the Tauri application:

Summary

  • Meetily's NotificationManagerState pattern centralizes notification preferences in frontend/src-tauri/src/notifications/manager.rs using Arc<RwLock<T>> for thread-safe access
  • Immutable snapshots ensure the React frontend receives consistent state without blocking async operations, while persistent storage via ConsentManager maintains settings across sessions
  • Decision gate architecture guarantees every notification passes consent, DND, and per-type preference checks before reaching SystemNotificationHandler
  • Transactional updates through update_settings validate and persist changes atomically, preventing state drift between memory and disk

Frequently Asked Questions

How does Meetily ensure thread safety when accessing notification preferences?

Meetily wraps all mutable notification state in Arc<RwLock<T>> structures within the NotificationManager struct. This allows multiple async tasks to read NotificationSettings simultaneously without blocking, while write operations acquire exclusive locks only during persistence. The pattern prevents race conditions when the UI queries state while background recording events trigger notification checks.

Where does Meetily store notification user preferences persistently?

The ConsentManager abstraction in frontend/src-tauri/src/notifications/settings.rs handles JSON serialization to $XDG_CONFIG/meetily/notifications.json. The manager loads these settings during initialization via get_settings_with_migration and immediately persists any changes through save_settings when update_settings is called, ensuring preferences survive application restarts.

What prevents unauthorized notifications when Do-Not-Disturb is enabled?

The should_show_notification method in frontend/src-tauri/src/notifications/manager.rs implements a mandatory three-layer check: first validating consent and OS permissions, then checking both manual DND state and system DND status, and finally verifying per-type preferences like show_recording_started. Only after all checks pass does the manager invoke SystemNotificationHandler, effectively gatekeeping all notification displays.

How does the frontend update notification settings without corrupting state?

The frontend invokes the update_notification_settings Tauri command, which routes to NotificationManager::update_settings. This method validates the payload, writes to disk via ConsentManager, and atomically updates the Arc<RwLock<T>> container. By cloning settings for reads and executing serialized writes, the pattern maintains a single source of truth while allowing concurrent access from multiple UI threads.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →