Skip to main content

SyncManager

Struct SyncManager 

Source
pub struct SyncManager { /* private fields */ }
Expand description

Orchestrates synchronization between the Todoist API and local cache.

SyncManager provides methods for syncing data, checking cache staleness, and forcing full syncs when needed.

§Thread Safety

SyncManager is Send but not Sync. Most methods require &mut self because they modify the internal cache and persist changes to disk.

For multi-threaded usage, wrap in Arc<Mutex<SyncManager>> or Arc<tokio::sync::Mutex<SyncManager>>:

use std::sync::Arc;
use tokio::sync::Mutex;
use todoist_api_rs::client::TodoistClient;
use todoist_cache_rs::{CacheStore, SyncManager};

let client = TodoistClient::new("token")?;
let store = CacheStore::new()?;
let manager = Arc::new(Mutex::new(SyncManager::new(client, store)?));

// Lock before calling mutable methods
let mut guard = manager.lock().await;
guard.sync().await?;

In typical CLI usage, the manager is owned by a single async task and no synchronization is needed.

Implementations§

Source§

impl SyncManager

Source

pub async fn resolve_project( &mut self, name_or_id: &str, ) -> SyncResult<&Project>

Resolves a project by name or ID, with auto-sync fallback.

This method first attempts to find the project in the cache. If not found, it performs a sync and retries the lookup. This provides a seamless experience where users can reference recently-created projects without manual syncing.

§Arguments
  • name_or_id - The project name (case-insensitive) or ID to search for
§Returns

A reference to the matching Project from the cache.

§Errors

Returns SyncError::NotFound if the project cannot be found even after syncing. Returns SyncError::Api if the sync operation fails.

§Example
use todoist_api_rs::client::TodoistClient;
use todoist_cache_rs::{CacheStore, SyncManager};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = TodoistClient::new("your-api-token")?;
    let store = CacheStore::new()?;
    let mut manager = SyncManager::new(client, store)?;

    // Find by name (case-insensitive)
    let project = manager.resolve_project("work").await?;
    println!("Found project: {} ({})", project.name, project.id);

    // Find by ID
    let project = manager.resolve_project("12345678").await?;
    println!("Found project: {}", project.name);

    Ok(())
}
Source

pub fn is_shared_project(&self, project_id: &str) -> bool

Returns whether a project is shared with active collaborators other than the owner.

Source

pub fn resolve_collaborator( &self, query: &str, project_id: &str, ) -> SyncResult<&Collaborator>

Resolves a collaborator in a project by name or email.

Matching order:

  1. Exact full name (case-insensitive)
  2. Exact email (case-insensitive)
  3. Prefix/substring match on full name or email (case-insensitive)
Source

pub async fn resolve_section( &mut self, name_or_id: &str, project_id: Option<&str>, ) -> SyncResult<&Section>

Resolves a section by name or ID, with auto-sync fallback.

This method first attempts to find the section in the cache. If not found, it performs a sync and retries the lookup.

§Arguments
  • name_or_id - The section name (case-insensitive) or ID to search for
  • project_id - Optional project ID to scope the search. If provided, only sections in that project are considered for name matching.
§Returns

A reference to the matching Section from the cache.

§Errors

Returns SyncError::NotFound if the section cannot be found even after syncing. Returns SyncError::Api if the sync operation fails.

§Example
use todoist_api_rs::client::TodoistClient;
use todoist_cache_rs::{CacheStore, SyncManager};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = TodoistClient::new("your-api-token")?;
    let store = CacheStore::new()?;
    let mut manager = SyncManager::new(client, store)?;

    // Find by name within a specific project
    let section = manager.resolve_section("To Do", Some("12345678")).await?;
    println!("Found section: {} ({})", section.name, section.id);

    // Find by ID (project_id is ignored for ID lookups)
    let section = manager.resolve_section("87654321", None).await?;
    println!("Found section: {}", section.name);

    Ok(())
}
Source

pub async fn resolve_label(&mut self, name_or_id: &str) -> SyncResult<&Label>

Resolves a label by name or ID, with auto-sync fallback.

This method first attempts to find the label in the cache. If not found, it performs a sync and retries the lookup.

§Arguments
  • name_or_id - The label name (case-insensitive) or ID to search for
§Returns

A reference to the matching Label from the cache.

§Errors

Returns SyncError::NotFound if the label cannot be found even after syncing. Returns SyncError::Api if the sync operation fails.

§Example
use todoist_api_rs::client::TodoistClient;
use todoist_cache_rs::{CacheStore, SyncManager};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = TodoistClient::new("your-api-token")?;
    let store = CacheStore::new()?;
    let mut manager = SyncManager::new(client, store)?;

    // Find by name (case-insensitive)
    let label = manager.resolve_label("urgent").await?;
    println!("Found label: {} ({})", label.name, label.id);

    // Find by ID
    let label = manager.resolve_label("12345678").await?;
    println!("Found label: {}", label.name);

    Ok(())
}
Source

pub async fn resolve_item(&mut self, id: &str) -> SyncResult<&Item>

Resolves an item (task) by ID, with auto-sync fallback.

This method first attempts to find the item in the cache. If not found, it performs a sync and retries the lookup.

Note: Unlike projects, sections, and labels, items can only be looked up by ID since task content is not guaranteed to be unique.

§Arguments
  • id - The item ID to search for
§Returns

A reference to the matching Item from the cache.

§Errors

Returns SyncError::NotFound if the item cannot be found even after syncing. Returns SyncError::Api if the sync operation fails.

§Example
use todoist_api_rs::client::TodoistClient;
use todoist_cache_rs::{CacheStore, SyncManager};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = TodoistClient::new("your-api-token")?;
    let store = CacheStore::new()?;
    let mut manager = SyncManager::new(client, store)?;

    // Find by ID
    let item = manager.resolve_item("12345678").await?;
    println!("Found item: {} ({})", item.content, item.id);

    Ok(())
}
Source

pub async fn resolve_item_by_prefix( &mut self, id_or_prefix: &str, require_checked: Option<bool>, ) -> SyncResult<&Item>

Resolves an item (task) by ID or unique prefix, with auto-sync fallback.

This method first attempts to find the item in the cache by exact ID match or unique prefix. If not found, it performs a sync and retries the lookup.

§Arguments
  • id_or_prefix - The item ID or unique prefix to search for
  • require_checked - If Some(true), only match completed items. If Some(false), only match uncompleted items. If None, match any item regardless of completion status.
§Returns

A reference to the matching Item from the cache.

§Errors

Returns SyncError::NotFound if the item cannot be found even after syncing. Returns SyncError::Api if the sync operation fails.

§Example
use todoist_api_rs::client::TodoistClient;
use todoist_cache_rs::{CacheStore, SyncManager};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = TodoistClient::new("your-api-token")?;
    let store = CacheStore::new()?;
    let mut manager = SyncManager::new(client, store)?;

    // Find uncompleted task by ID prefix
    let item = manager.resolve_item_by_prefix("abc123", Some(false)).await?;
    println!("Found item: {} ({})", item.content, item.id);

    // Find any task by prefix (completed or not)
    let item = manager.resolve_item_by_prefix("def456", None).await?;
    println!("Found item: {}", item.content);

    Ok(())
}
Source§

impl SyncManager

Source

pub fn new(client: TodoistClient, store: CacheStore) -> Result<Self>

Creates a new SyncManager with the given client and store.

The cache is loaded from disk if it exists, otherwise a new empty cache is created.

§Arguments
  • client - The Todoist API client
  • store - The cache store for persistence
§Errors

Returns an error if loading the cache from disk fails (excluding file not found).

Source

pub fn with_stale_threshold( client: TodoistClient, store: CacheStore, stale_minutes: i64, ) -> Result<Self>

Creates a new SyncManager with a custom staleness threshold.

§Arguments
  • client - The Todoist API client
  • store - The cache store for persistence
  • stale_minutes - Number of minutes after which the cache is considered stale
Source

pub fn cache(&self) -> &Cache

Returns a reference to the current cache.

Source

pub fn store(&self) -> &CacheStore

Returns a reference to the cache store.

Source

pub fn client(&self) -> &TodoistClient

Returns a reference to the Todoist client.

Source

pub fn is_stale(&self, now: DateTime<Utc>) -> bool

Returns true if the cache is stale (older than the configured threshold).

A cache is considered stale if:

  • It has never been synced (last_sync is None)
  • It was last synced more than stale_minutes ago
§Arguments
  • now - The current time to compare against
Source

pub fn needs_sync(&self, now: DateTime<Utc>) -> bool

Returns true if a sync is needed.

A sync is needed if:

  • The cache requires a full sync (no sync token)
  • The cache is stale
§Arguments
  • now - The current time to compare against
Source

pub async fn sync(&mut self) -> Result<&Cache>

Performs a sync operation.

This method automatically determines whether to perform a full or incremental sync:

  • Full sync if the cache has never been synced (sync_token is “*”)
  • Incremental sync otherwise

If an incremental sync fails due to an invalid sync token, this method automatically falls back to a full sync with sync_token='*'.

The cache is saved to disk asynchronously after a successful sync.

§Returns

A reference to the updated cache.

§Errors

Returns an error if the API request fails or if saving the cache fails.

Source

pub async fn full_sync(&mut self) -> Result<&Cache>

Forces a full sync, ignoring the stored sync token.

This replaces all cached data with fresh data from the server. The cache is saved to disk asynchronously after a successful sync.

§Returns

A reference to the updated cache.

§Errors

Returns an error if the API request fails or if saving the cache fails.

Source

pub fn reload(&mut self) -> Result<&Cache>

Reloads the cache from disk.

This discards any in-memory changes and loads the cache from disk. Useful if the cache file was modified externally.

§Errors

Returns an error if reading the cache from disk fails.

Source

pub async fn execute_commands( &mut self, commands: Vec<SyncCommand>, ) -> Result<SyncResponse>

Executes one or more commands via the Sync API.

This method sends the commands to the Todoist API, applies the response to the cache, and saves the cache to disk. It returns the full response so callers can access temp_id_mapping to resolve temporary IDs to real IDs, and sync_status to check per-command results.

§Arguments
  • commands - A vector of SyncCommand objects to execute
§Returns

The SyncResponse from the API, containing:

  • sync_status: Success/failure for each command (keyed by command UUID)
  • temp_id_mapping: Maps temporary IDs to real IDs for created resources
  • Updated resources affected by the commands
§Errors

Returns an error if the API request fails or if saving the cache fails.

§Example
use todoist_api_rs::client::TodoistClient;
use todoist_api_rs::sync::{SyncCommand, SyncCommandType};
use todoist_cache_rs::{CacheStore, SyncManager};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = TodoistClient::new("your-api-token")?;
    let store = CacheStore::new()?;
    let mut manager = SyncManager::new(client, store)?;

    // Create a new task
    let temp_id = uuid::Uuid::new_v4().to_string();
    let cmd = SyncCommand::with_temp_id(
        SyncCommandType::ItemAdd,
        &temp_id,
        serde_json::json!({"content": "Buy milk", "project_id": "inbox"}),
    );

    let response = manager.execute_commands(vec![cmd]).await?;

    // Get the real ID from temp_id_mapping
    if let Some(real_id) = response.temp_id_mapping.get(&temp_id) {
        println!("Created task with ID: {}", real_id);
    }

    Ok(())
}

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more