pub struct ObservableProperty<T>{ /* private fields */ }Expand description
A thread-safe observable property that notifies observers when its value changes
This type wraps a value of type T and allows multiple observers to be notified
whenever the value is modified. All operations are thread-safe and can be called
from multiple threads concurrently.
§Type Requirements
The generic type T must implement:
Clone: Required for returning values and passing them to observersSend: Required for transferring between threadsSync: Required for concurrent access from multiple threads'static: Required for observer callbacks that may outlive the original scope
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new("initial".to_string());
let observer_id = property.subscribe(Arc::new(|old, new| {
println!("Changed from '{}' to '{}'", old, new);
})).map_err(|e| {
eprintln!("Failed to subscribe: {}", e);
e
})?;
property.set("updated".to_string()).map_err(|e| {
eprintln!("Failed to set value: {}", e);
e
})?; // Prints: Changed from 'initial' to 'updated'
property.unsubscribe(observer_id).map_err(|e| {
eprintln!("Failed to unsubscribe: {}", e);
e
})?;Implementations§
Source§impl<T: Clone + Send + Sync + 'static> ObservableProperty<T>
impl<T: Clone + Send + Sync + 'static> ObservableProperty<T>
Sourcepub fn new(initial_value: T) -> Self
pub fn new(initial_value: T) -> Self
Creates a new observable property with the given initial value
§Arguments
initial_value- The starting value for this property
§Examples
use observable_property::ObservableProperty;
let property = ObservableProperty::new(42);
match property.get() {
Ok(value) => assert_eq!(value, 42),
Err(e) => eprintln!("Failed to get property value: {}", e),
}Sourcepub fn with_equality<F>(initial_value: T, eq_fn: F) -> Self
pub fn with_equality<F>(initial_value: T, eq_fn: F) -> Self
Creates a new observable property with custom equality comparison
This constructor allows you to define custom logic for determining when two values
are considered “equal”. Observers are only notified when the equality function
returns false (i.e., when the values are considered different).
This is particularly useful for:
- Float comparisons with epsilon tolerance (avoiding floating-point precision issues)
- Case-insensitive string comparisons
- Semantic equality that differs from structural equality
- Preventing spurious notifications for “equal enough” values
§Arguments
initial_value- The starting value for this propertyeq_fn- A function that returnstrueif two values should be considered equal,falseif they should be considered different (which triggers notifications)
§Examples
§Float comparison with epsilon
use observable_property::ObservableProperty;
use std::sync::Arc;
// Create a property that only notifies if the difference is > 0.001
let temperature = ObservableProperty::with_equality(
20.0_f64,
|a, b| (a - b).abs() < 0.001
);
let _sub = temperature.subscribe_with_subscription(Arc::new(|old, new| {
println!("Significant temperature change: {} -> {}", old, new);
}))?;
temperature.set(20.0005)?; // No notification (within epsilon)
temperature.set(20.5)?; // Notification triggered§Case-insensitive string comparison
use observable_property::ObservableProperty;
use std::sync::Arc;
// Only notify on case-insensitive changes
let username = ObservableProperty::with_equality(
"Alice".to_string(),
|a, b| a.to_lowercase() == b.to_lowercase()
);
let _sub = username.subscribe_with_subscription(Arc::new(|old, new| {
println!("Username changed: {} -> {}", old, new);
}))?;
username.set("alice".to_string())?; // No notification
username.set("ALICE".to_string())?; // No notification
username.set("Bob".to_string())?; // Notification triggered§Semantic equality for complex types
use observable_property::ObservableProperty;
use std::sync::Arc;
#[derive(Clone)]
struct Config {
host: String,
port: u16,
timeout_ms: u64,
}
// Only notify if critical fields change
let config = ObservableProperty::with_equality(
Config { host: "localhost".to_string(), port: 8080, timeout_ms: 1000 },
|a, b| a.host == b.host && a.port == b.port // Ignore timeout changes
);
let _sub = config.subscribe_with_subscription(Arc::new(|old, new| {
println!("Critical config changed: {}:{} -> {}:{}",
old.host, old.port, new.host, new.port);
}))?;
config.modify(|c| c.timeout_ms = 2000)?; // No notification (timeout ignored)
config.modify(|c| c.port = 9090)?; // Notification triggered§Performance Considerations
The equality function is called every time set() or set_async() is called,
so it should be relatively fast. For expensive comparisons, consider using
filtered observers instead.
§Thread Safety
The equality function must be Send + Sync + 'static as it may be called from
any thread that modifies the property.
Sourcepub fn with_validator<F>(
initial_value: T,
validator: F,
) -> Result<Self, PropertyError>
pub fn with_validator<F>( initial_value: T, validator: F, ) -> Result<Self, PropertyError>
Creates a new observable property with value validation
This constructor enables value validation for the property. Any attempt to set
a value that fails validation will be rejected with a ValidationError. This
ensures the property always contains valid data according to your business rules.
The validator function is called:
- When the property is created (to validate the initial value)
- Every time
set()orset_async()is called (before the value is changed) - When
modify()is called (after the modification function runs)
If validation fails, the property value remains unchanged and an error is returned.
§Arguments
initial_value- The starting value for this property (must pass validation)validator- A function that validates values, returningOk(())for valid values orErr(String)with an error message for invalid values
§Returns
Ok(Self)- If the initial value passes validationErr(PropertyError::ValidationError)- If the initial value fails validation
§Use Cases
§Age Validation
use observable_property::ObservableProperty;
use std::sync::Arc;
// Only allow ages between 0 and 150
let age = ObservableProperty::with_validator(
25,
|age| {
if *age <= 150 {
Ok(())
} else {
Err(format!("Age must be between 0 and 150, got {}", age))
}
}
)?;
let _sub = age.subscribe_with_subscription(Arc::new(|old, new| {
println!("Age changed: {} -> {}", old, new);
}))?;
age.set(30)?; // ✓ Valid - prints: "Age changed: 25 -> 30"
// Attempt to set invalid age
match age.set(200) {
Err(e) => println!("Validation failed: {}", e), // Prints validation error
Ok(_) => unreachable!(),
}
assert_eq!(age.get()?, 30); // Value unchanged after failed validation§Email Format Validation
use observable_property::ObservableProperty;
// Validate email format (simplified)
let email = ObservableProperty::with_validator(
"user@example.com".to_string(),
|email| {
if email.contains('@') && email.contains('.') {
Ok(())
} else {
Err(format!("Invalid email format: {}", email))
}
}
)?;
email.set("valid@email.com".to_string())?; // ✓ Valid
match email.set("invalid-email".to_string()) {
Err(e) => println!("{}", e), // Prints: "Validation failed: Invalid email format: invalid-email"
Ok(_) => unreachable!(),
}§Range Validation for Floats
use observable_property::ObservableProperty;
// Temperature must be between absolute zero and practical maximum
let temperature = ObservableProperty::with_validator(
20.0_f64,
|temp| {
if *temp >= -273.15 && *temp <= 1000.0 {
Ok(())
} else {
Err(format!("Temperature {} is out of valid range [-273.15, 1000.0]", temp))
}
}
)?;
temperature.set(100.0)?; // ✓ Valid
temperature.set(-300.0).unwrap_err(); // ✗ Fails validation§Multiple Validation Rules
use observable_property::ObservableProperty;
// Username validation with multiple rules
let username = ObservableProperty::with_validator(
"alice".to_string(),
|name| {
if name.is_empty() {
return Err("Username cannot be empty".to_string());
}
if name.len() < 3 {
return Err(format!("Username must be at least 3 characters, got {}", name.len()));
}
if name.len() > 20 {
return Err(format!("Username must be at most 20 characters, got {}", name.len()));
}
if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err("Username can only contain letters, numbers, and underscores".to_string());
}
Ok(())
}
)?;
username.set("bob".to_string())?; // ✓ Valid
username.set("ab".to_string()).unwrap_err(); // ✗ Too short
username.set("user@123".to_string()).unwrap_err(); // ✗ Invalid characters§Rejecting Invalid Initial Values
use observable_property::ObservableProperty;
// Attempt to create with invalid initial value
let result = ObservableProperty::with_validator(
200,
|age| {
if *age <= 150 {
Ok(())
} else {
Err(format!("Age must be at most 150, got {}", age))
}
}
);
match result {
Err(e) => println!("Failed to create property: {}", e),
Ok(_) => unreachable!(),
}§Performance Considerations
The validator function is called on every set() or set_async() operation,
so it should be relatively fast. For expensive validations, consider:
- Caching validation results if the same value is set multiple times
- Using async validation patterns outside of the property setter
- Implementing early-exit validation logic (check cheapest rules first)
§Thread Safety
The validator function must be Send + Sync + 'static as it may be called from
any thread that modifies the property. Ensure your validation logic is thread-safe.
§Combining with Other Features
Validation works alongside other property features:
- Custom equality: Validation runs before equality checking
- History tracking: Only valid values are stored in history
- Observers: Observers only fire when validation succeeds and values differ
- Batching: Validation occurs when batch is committed, not during batch
Sourcepub fn with_max_threads(initial_value: T, max_threads: usize) -> Self
pub fn with_max_threads(initial_value: T, max_threads: usize) -> Self
Creates a new observable property with a custom maximum thread count for async notifications
This constructor allows you to customize the maximum number of threads used for
asynchronous observer notifications via set_async(). This is useful for tuning
performance based on your specific use case and system constraints.
§Arguments
initial_value- The starting value for this propertymax_threads- Maximum number of threads to use for async notifications. If 0 is provided, defaults to 4.
§Thread Pool Behavior
When set_async() is called, observers are divided into batches and each batch
runs in its own thread, up to the specified maximum. For example:
- With 100 observers and
max_threads = 4: 4 threads with ~25 observers each - With 10 observers and
max_threads = 8: 10 threads with 1 observer each - With 2 observers and
max_threads = 4: 2 threads with 1 observer each
§Use Cases
§High-Throughput Systems
use observable_property::ObservableProperty;
// For systems with many CPU cores and CPU-bound observers
let property = ObservableProperty::with_max_threads(0, 8);§Resource-Constrained Systems
use observable_property::ObservableProperty;
// For embedded systems or memory-constrained environments
let property = ObservableProperty::with_max_threads(42, 1);§I/O-Heavy Observers
use observable_property::ObservableProperty;
// For observers that do network/database operations
let property = ObservableProperty::with_max_threads("data".to_string(), 16);§Performance Considerations
- Higher values: Better parallelism but more thread overhead and memory usage
- Lower values: Less overhead but potentially slower async notifications
- Optimal range: Typically between 1 and 2x the number of CPU cores
- Zero value: Automatically uses the default value (4)
§Thread Safety
This setting only affects async notifications (set_async()). Synchronous
operations (set()) always execute observers sequentially regardless of this setting.
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
// Create property with custom thread pool size
let property = ObservableProperty::with_max_threads(42, 2);
// Subscribe observers as usual
let _subscription = property.subscribe_with_subscription(Arc::new(|old, new| {
println!("Value changed: {} -> {}", old, new);
})).expect("Failed to create subscription");
// Async notifications will use at most 2 threads
property.set_async(100).expect("Failed to set value asynchronously");Sourcepub fn with_persistence<P>(initial_value: T, persistence: P) -> Selfwhere
P: PropertyPersistence<Value = T>,
pub fn with_persistence<P>(initial_value: T, persistence: P) -> Selfwhere
P: PropertyPersistence<Value = T>,
Creates a new observable property with automatic persistence
This constructor creates a property that automatically saves its value to persistent
storage whenever it changes. It attempts to load the initial value from storage, falling
back to the provided initial_value if loading fails.
§Arguments
initial_value- The value to use if loading from persistence failspersistence- An implementation ofPropertyPersistencethat handles save/load operations
§Behavior
- Attempts to load the initial value from
persistence.load() - If loading fails, uses the provided
initial_value - Sets up an internal observer that automatically calls
persistence.save()on every value change - Returns the configured property
§Error Handling
- Load failures are logged and the provided
initial_valueis used instead - Save failures during subsequent updates will be logged but won’t prevent the update
- The property continues to operate normally even if persistence operations fail
§Type Requirements
The persistence handler’s Value type must match the property’s type T.
§Examples
§File-based Persistence
use observable_property::{ObservableProperty, PropertyPersistence};
use std::fs;
struct FilePersistence {
path: String,
}
impl PropertyPersistence for FilePersistence {
type Value = String;
fn load(&self) -> Result<Self::Value, Box<dyn std::error::Error + Send + Sync>> {
fs::read_to_string(&self.path)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
}
fn save(&self, value: &Self::Value) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
fs::write(&self.path, value)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
}
}
// Create a property that auto-saves to a file
let property = ObservableProperty::with_persistence(
"default_value".to_string(),
FilePersistence { path: "./data.txt".to_string() }
);
// Value changes are automatically saved to disk
property.set("new_value".to_string())
.expect("Failed to set value");§JSON Database Persistence
use observable_property::{ObservableProperty, PropertyPersistence};
use serde::{Serialize, Deserialize};
use std::fs;
#[derive(Clone, Serialize, Deserialize)]
struct UserPreferences {
theme: String,
font_size: u32,
}
struct JsonPersistence {
path: String,
}
impl PropertyPersistence for JsonPersistence {
type Value = UserPreferences;
fn load(&self) -> Result<Self::Value, Box<dyn std::error::Error + Send + Sync>> {
let data = fs::read_to_string(&self.path)?;
let prefs = serde_json::from_str(&data)?;
Ok(prefs)
}
fn save(&self, value: &Self::Value) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let data = serde_json::to_string_pretty(value)?;
fs::write(&self.path, data)?;
Ok(())
}
}
let default_prefs = UserPreferences {
theme: "dark".to_string(),
font_size: 14,
};
let prefs = ObservableProperty::with_persistence(
default_prefs,
JsonPersistence { path: "./preferences.json".to_string() }
);
// Changes are auto-saved as JSON
prefs.modify(|p| {
p.theme = "light".to_string();
p.font_size = 16;
}).expect("Failed to update preferences");§Thread Safety
The persistence handler must be Send + Sync + 'static since save operations
may be called from any thread holding a reference to the property.
§Performance Considerations
- Persistence operations are called synchronously on every value change
- Use fast storage backends or consider debouncing frequent updates
- For high-frequency updates, consider implementing a buffered persistence strategy
Sourcepub fn with_history(initial_value: T, history_size: usize) -> Self
pub fn with_history(initial_value: T, history_size: usize) -> Self
Creates a new observable property with history tracking enabled
This constructor creates a property that maintains a history of previous values, allowing you to undo changes and view historical values. The history buffer is automatically managed with a fixed size limit.
§Arguments
initial_value- The starting value for this propertyhistory_size- Maximum number of previous values to retain in history. If 0, history tracking is disabled and behaves like a regular property.
§History Behavior
- The history buffer stores up to
history_sizeprevious values - When the buffer is full, the oldest value is removed when a new value is added
- The current value is not included in the history - only past values
- History is stored in chronological order (oldest to newest)
- Undo operations pop values from the history and restore them as current
§Memory Considerations
Each historical value requires memory equivalent to size_of::<T>(). For large
types or high history sizes, consider:
- Using smaller history_size values
- Wrapping large types in
Arc<T>to share data between history entries - Implementing custom equality checks to avoid storing duplicate values
§Examples
§Basic History Usage
use observable_property::ObservableProperty;
// Create property with space for 5 historical values
let property = ObservableProperty::with_history(0, 5);
// Make some changes
property.set(10)?;
property.set(20)?;
property.set(30)?;
assert_eq!(property.get()?, 30);
// Undo last change
property.undo()?;
assert_eq!(property.get()?, 20);
// Undo again
property.undo()?;
assert_eq!(property.get()?, 10);§View History
use observable_property::ObservableProperty;
let property = ObservableProperty::with_history("start".to_string(), 3);
property.set("second".to_string())?;
property.set("third".to_string())?;
property.set("fourth".to_string())?;
// Get all historical values (oldest to newest)
let history = property.get_history();
assert_eq!(history.len(), 3);
assert_eq!(history[0], "start"); // oldest
assert_eq!(history[1], "second");
assert_eq!(history[2], "third"); // newest (most recent past value)
// Current value is "fourth" and not in history
assert_eq!(property.get()?, "fourth");§History with Observer Pattern
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::with_history(100, 10);
// Observers work normally with history-enabled properties
let _subscription = property.subscribe_with_subscription(Arc::new(|old, new| {
println!("Value changed: {} -> {}", old, new);
}))?;
property.set(200)?; // Triggers observer
property.undo()?; // Also triggers observer when reverting to 100§Bounded History Buffer
use observable_property::ObservableProperty;
// Only keep 2 historical values
let property = ObservableProperty::with_history(1, 2);
property.set(2)?; // history: [1]
property.set(3)?; // history: [1, 2]
property.set(4)?; // history: [2, 3] (oldest '1' was removed)
let history = property.get_history();
assert_eq!(history, vec![2, 3]);
assert_eq!(property.get()?, 4);§Thread Safety
History tracking is fully thread-safe and works correctly even when multiple
threads are calling set(), undo(), and get_history() concurrently.
Sourcepub fn with_event_log(initial_value: T, event_log_size: usize) -> Self
pub fn with_event_log(initial_value: T, event_log_size: usize) -> Self
Creates a new observable property with event sourcing enabled
This method enables full event logging for the property, recording every change as a timestamped event. This provides powerful capabilities for debugging, auditing, and event replay.
§Features
- Complete Audit Trail: Every change is recorded with old value, new value, and timestamp
- Time-Travel Debugging: Examine the complete history of state changes
- Event Replay: Reconstruct property state at any point in time
- Thread Information: Each event captures which thread made the change
- Sequential Numbering: Events are numbered starting from 0
§Arguments
initial_value- The starting value for this propertyevent_log_size- Maximum number of events to keep in memory (0 = unlimited)
§Memory Considerations
Event logs store complete copies of both old and new values for each change. For properties with large values or high update frequency:
- Use a bounded
event_log_sizeto prevent unbounded memory growth - Consider using
with_history()if you only need value snapshots without metadata - Monitor memory usage in production environments
When the log exceeds event_log_size, the oldest events are automatically removed.
§Examples
§Basic Event Logging
use observable_property::ObservableProperty;
use std::sync::Arc;
// Create property with unlimited event log
let counter = ObservableProperty::with_event_log(0, 0);
counter.set(1)?;
counter.set(2)?;
counter.set(3)?;
// Retrieve the complete event log
let events = counter.get_event_log();
assert_eq!(events.len(), 3);
// Examine first event
assert_eq!(events[0].old_value, 0);
assert_eq!(events[0].new_value, 1);
assert_eq!(events[0].event_number, 0);
// Examine last event
assert_eq!(events[2].old_value, 2);
assert_eq!(events[2].new_value, 3);
assert_eq!(events[2].event_number, 2);§Bounded Event Log
use observable_property::ObservableProperty;
// Keep only the last 3 events
let property = ObservableProperty::with_event_log(100, 3);
property.set(101)?;
property.set(102)?;
property.set(103)?;
property.set(104)?; // Oldest event (100->101) is now removed
let events = property.get_event_log();
assert_eq!(events.len(), 3);
assert_eq!(events[0].old_value, 101);
assert_eq!(events[2].new_value, 104);§Time-Travel Debugging
use observable_property::ObservableProperty;
use std::time::Duration;
let config = ObservableProperty::with_event_log("default".to_string(), 0);
let start = std::time::Instant::now();
config.set("config_v1".to_string())?;
std::thread::sleep(Duration::from_millis(10));
config.set("config_v2".to_string())?;
std::thread::sleep(Duration::from_millis(10));
config.set("config_v3".to_string())?;
// Find what the config was 15ms after start
let target_time = start + Duration::from_millis(15);
let events = config.get_event_log();
let mut state = "default".to_string();
for event in events {
if event.timestamp <= target_time {
state = event.new_value.clone();
} else {
break;
}
}
println!("State at +15ms: {}", state);§Audit Trail with Thread Information
use observable_property::ObservableProperty;
use std::sync::Arc;
use std::thread;
let shared_state = Arc::new(ObservableProperty::with_event_log(0, 0));
let handles: Vec<_> = (0..3)
.map(|i| {
let state = shared_state.clone();
thread::spawn(move || {
state.set(i * 10).expect("Failed to set");
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
// Examine which threads made changes
let events = shared_state.get_event_log();
for event in events {
println!(
"Event #{}: {} -> {} (thread: {})",
event.event_number,
event.old_value,
event.new_value,
event.thread_id
);
}§Event Replay
use observable_property::ObservableProperty;
let account_balance = ObservableProperty::with_event_log(1000, 0);
// Simulate transactions
account_balance.modify(|b| *b -= 100)?; // Withdrawal
account_balance.modify(|b| *b += 50)?; // Deposit
account_balance.modify(|b| *b -= 200)?; // Withdrawal
// Replay all transactions
let events = account_balance.get_event_log();
println!("Transaction History:");
for event in events {
let change = event.new_value as i32 - event.old_value as i32;
let transaction_type = if change > 0 { "Deposit" } else { "Withdrawal" };
println!(
"[{}] {}: ${} (balance: {} -> {})",
event.event_number,
transaction_type,
change.abs(),
event.old_value,
event.new_value
);
}§Thread Safety
Event logging is fully thread-safe and works correctly even when multiple threads are modifying the property concurrently. Event numbers are assigned sequentially based on the order changes complete (not the order they start).
§Difference from History
While with_history() only stores previous values, with_event_log() stores
complete event objects with timestamps and metadata. This makes event logs
more suitable for auditing and debugging, but they consume more memory.
| Feature | with_history() | with_event_log() |
|---|---|---|
| Stores values | ✓ | ✓ |
| Stores timestamps | ✗ | ✓ |
| Stores thread info | ✗ | ✓ |
| Sequential numbering | ✗ | ✓ |
| Old + new values | ✗ | ✓ |
| Memory overhead | Low | Higher |
| Undo support | ✓ | ✗ (manual) |
Sourcepub fn undo(&self) -> Result<(), PropertyError>
pub fn undo(&self) -> Result<(), PropertyError>
Reverts the property to its previous value from history
This method pops the most recent value from the history buffer and makes it the current value. The current value is not added to history during undo. All subscribed observers are notified of this change.
§Returns
Ok(())if the undo was successfulErr(PropertyError::NoHistory)if:- History tracking is not enabled (created without
with_history()) - History buffer is empty (no previous values to restore)
- History tracking is not enabled (created without
§Undo Chain Behavior
Consecutive undo operations walk back through history until exhausted:
Initial: value=4, history=[1, 2, 3]
After undo(): value=3, history=[1, 2]
After undo(): value=2, history=[1]
After undo(): value=1, history=[]
After undo(): Error(NoHistory) - no more history§Observer Notification
Observers are notified with the current value as “old” and the restored
historical value as “new”, maintaining the same notification pattern as set().
§Examples
§Basic Undo
use observable_property::ObservableProperty;
let property = ObservableProperty::with_history(1, 5);
property.set(2)?;
property.set(3)?;
assert_eq!(property.get()?, 3);
property.undo()?;
assert_eq!(property.get()?, 2);
property.undo()?;
assert_eq!(property.get()?, 1);
// No more history
assert!(property.undo().is_err());§Undo with Observers
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::with_history(10, 5);
let _subscription = property.subscribe_with_subscription(Arc::new(|old, new| {
println!("Changed from {} to {}", old, new);
}))?;
property.set(20)?; // Prints: "Changed from 10 to 20"
property.undo()?; // Prints: "Changed from 20 to 10"§Error Handling
use observable_property::{ObservableProperty, PropertyError};
// Property without history
let no_history = ObservableProperty::new(42);
match no_history.undo() {
Err(PropertyError::NoHistory { .. }) => {
println!("Expected: history not enabled");
}
_ => panic!("Should fail without history"),
}
// Property with history but empty
let empty_history = ObservableProperty::with_history(100, 5);
match empty_history.undo() {
Err(PropertyError::NoHistory { .. }) => {
println!("Expected: no history to undo");
}
_ => panic!("Should fail with empty history"),
}§Undo After Multiple Changes
use observable_property::ObservableProperty;
let counter = ObservableProperty::with_history(0, 3);
// Make several changes
for i in 1..=5 {
counter.set(i)?;
}
assert_eq!(counter.get()?, 5);
// Undo three times (limited by history_size=3)
counter.undo()?;
assert_eq!(counter.get()?, 4);
counter.undo()?;
assert_eq!(counter.get()?, 3);
counter.undo()?;
assert_eq!(counter.get()?, 2);
// No more history (oldest value in buffer was 2)
assert!(counter.undo().is_err());§Thread Safety
This method is thread-safe and can be called concurrently with set(),
get(), and other operations from multiple threads.
Sourcepub fn get_history(&self) -> Vec<T>
pub fn get_history(&self) -> Vec<T>
Returns a snapshot of all historical values
This method returns a vector containing all previous values currently stored in the history buffer, ordered from oldest to newest. The current value is not included in the returned vector.
§Returns
A Vec<T> containing historical values in chronological order:
vec[0]is the oldest value in historyvec[len-1]is the most recent past value (the one that would be restored byundo())- Empty vector if history is disabled or no history has been recorded
§Memory
This method clones all historical values, so the returned vector owns its data independently of the property. This allows safe sharing across threads without holding locks.
§Examples
§Basic History Retrieval
use observable_property::ObservableProperty;
let property = ObservableProperty::with_history("a".to_string(), 5);
property.set("b".to_string())?;
property.set("c".to_string())?;
property.set("d".to_string())?;
let history = property.get_history();
assert_eq!(history.len(), 3);
assert_eq!(history[0], "a"); // oldest
assert_eq!(history[1], "b");
assert_eq!(history[2], "c"); // newest (what undo() would restore)
// Current value is not in history
assert_eq!(property.get()?, "d");§Empty History
use observable_property::ObservableProperty;
// No history recorded yet
let fresh = ObservableProperty::with_history(42, 10);
assert!(fresh.get_history().is_empty());
// History disabled (size = 0)
let no_tracking = ObservableProperty::with_history(42, 0);
assert!(no_tracking.get_history().is_empty());
// Regular property (no history support)
let regular = ObservableProperty::new(42);
assert!(regular.get_history().is_empty());§History Buffer Limit
use observable_property::ObservableProperty;
// Limited history size
let property = ObservableProperty::with_history(1, 3);
for i in 2..=6 {
property.set(i)?;
}
// Only last 3 historical values are kept
let history = property.get_history();
assert_eq!(history, vec![3, 4, 5]);
assert_eq!(property.get()?, 6); // current§Iterating Through History
use observable_property::ObservableProperty;
let property = ObservableProperty::with_history(0.0f64, 5);
property.set(1.5)?;
property.set(3.0)?;
property.set(4.5)?;
println!("Historical values:");
for (i, value) in property.get_history().iter().enumerate() {
println!(" [{}] {}", i, value);
}§Checking History Before Undo
use observable_property::ObservableProperty;
let property = ObservableProperty::with_history(100, 5);
property.set(200)?;
property.set(300)?;
// Check what undo would restore
let history = property.get_history();
if !history.is_empty() {
let would_restore = history.last().unwrap();
println!("Undo would restore: {}", would_restore);
// Actually perform the undo
property.undo()?;
assert_eq!(property.get()?, *would_restore);
}§Thread Safety
This method acquires a read lock, allowing multiple concurrent readers. The returned vector is independent of the property’s internal state.
Sourcepub fn get_event_log(&self) -> Vec<PropertyEvent<T>>
pub fn get_event_log(&self) -> Vec<PropertyEvent<T>>
Gets the complete event log for this property
Returns a vector of all recorded property change events. Each event contains the old value, new value, timestamp, event number, and thread information. This provides a complete audit trail of all changes to the property.
This method acquires a read lock, allowing multiple concurrent readers. The returned vector is independent of the property’s internal state.
§Returns
A vector of PropertyEvent<T> objects, in chronological order (oldest first).
Returns an empty vector if:
- Event logging is not enabled (property not created with
with_event_log()) - No changes have been made yet
§Examples
§Basic Event Log Retrieval
use observable_property::ObservableProperty;
let counter = ObservableProperty::with_event_log(0, 0);
counter.set(1)?;
counter.set(2)?;
counter.set(3)?;
let events = counter.get_event_log();
assert_eq!(events.len(), 3);
// First event
assert_eq!(events[0].old_value, 0);
assert_eq!(events[0].new_value, 1);
assert_eq!(events[0].event_number, 0);
// Last event
assert_eq!(events[2].old_value, 2);
assert_eq!(events[2].new_value, 3);
assert_eq!(events[2].event_number, 2);§Filtering Events by Time
use observable_property::ObservableProperty;
use std::time::{Duration, Instant};
let property = ObservableProperty::with_event_log(0, 0);
let start = Instant::now();
property.set(1)?;
std::thread::sleep(Duration::from_millis(10));
property.set(2)?;
std::thread::sleep(Duration::from_millis(10));
property.set(3)?;
let cutoff = start + Duration::from_millis(15);
let recent_events: Vec<_> = property.get_event_log()
.into_iter()
.filter(|e| e.timestamp > cutoff)
.collect();
println!("Recent events: {}", recent_events.len());§Analyzing Event Patterns
use observable_property::ObservableProperty;
let score = ObservableProperty::with_event_log(0, 0);
score.modify(|s| *s += 10)?;
score.modify(|s| *s -= 3)?;
score.modify(|s| *s += 5)?;
let events = score.get_event_log();
let total_increases = events.iter()
.filter(|e| e.new_value > e.old_value)
.count();
let total_decreases = events.iter()
.filter(|e| e.new_value < e.old_value)
.count();
println!("Increases: {}, Decreases: {}", total_increases, total_decreases);§Event Log with Thread Information
use observable_property::ObservableProperty;
use std::sync::Arc;
use std::thread;
let property = Arc::new(ObservableProperty::with_event_log(0, 0));
let handles: Vec<_> = (0..3).map(|i| {
let prop = property.clone();
thread::spawn(move || {
prop.set(i * 10).expect("Set failed");
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
// Analyze which threads made changes
for event in property.get_event_log() {
println!("Event {}: Thread {}", event.event_number, event.thread_id);
}§Replaying Property State
use observable_property::ObservableProperty;
let property = ObservableProperty::with_event_log(100, 0);
property.set(150)?;
property.set(200)?;
property.set(175)?;
// Replay state at each point in time
let events = property.get_event_log();
let mut state = 100; // Initial value
println!("Initial state: {}", state);
for event in events {
state = event.new_value;
println!("After event {}: {}", event.event_number, state);
}§Thread Safety
This method is thread-safe and can be called concurrently from multiple threads. The returned event log is a snapshot at the time of the call.
§Performance
This method clones the entire event log. For properties with large event logs,
consider the memory and performance implications. If you only need recent events,
use filtering on the result or create the property with a bounded event_log_size.
Sourcepub fn get(&self) -> Result<T, PropertyError>
pub fn get(&self) -> Result<T, PropertyError>
Gets the current value of the property
This method acquires a read lock, which allows multiple concurrent readers but will block if a writer currently holds the lock.
§Returns
Ok(T) containing a clone of the current value, or Err(PropertyError)
if the lock is poisoned.
§Examples
use observable_property::ObservableProperty;
let property = ObservableProperty::new("hello".to_string());
match property.get() {
Ok(value) => assert_eq!(value, "hello"),
Err(e) => eprintln!("Failed to get property value: {}", e),
}Sourcepub fn get_metrics(&self) -> Result<PropertyMetrics, PropertyError>
pub fn get_metrics(&self) -> Result<PropertyMetrics, PropertyError>
Returns performance metrics for this property
Provides insight into property usage patterns and observer notification performance. This is useful for profiling, debugging, and performance optimization.
§Metrics Provided
- total_changes: Number of times the property value has been changed
- observer_calls: Total number of observer notification calls made
- avg_notification_time: Average time taken to notify all observers
§Note
- For
set_async(), the notification time measures the time to spawn threads, not the actual observer execution time (since threads are fire-and-forget). - Observer calls are counted even if they panic (panic recovery continues).
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(0);
// Subscribe multiple observers
property.subscribe(Arc::new(|old, new| {
println!("Observer 1: {} -> {}", old, new);
}))?;
property.subscribe(Arc::new(|old, new| {
println!("Observer 2: {} -> {}", old, new);
}))?;
// Make some changes
property.set(42)?;
property.set(100)?;
property.set(200)?;
// Get performance metrics
let metrics = property.get_metrics()?;
println!("Total changes: {}", metrics.total_changes); // 3
println!("Observer calls: {}", metrics.observer_calls); // 6 (3 changes × 2 observers)
println!("Avg notification time: {:?}", metrics.avg_notification_time);Sourcepub fn set(&self, new_value: T) -> Result<(), PropertyError>
pub fn set(&self, new_value: T) -> Result<(), PropertyError>
Sets the property to a new value and notifies all observers
This method will:
- Acquire a write lock (blocking other readers/writers)
- Update the value and capture a snapshot of observers
- Release the lock
- Notify all observers sequentially with the old and new values
Observer notifications are wrapped in panic recovery to prevent one misbehaving observer from affecting others.
§Arguments
new_value- The new value to set
§Returns
Ok(()) if successful, or Err(PropertyError) if the lock is poisoned.
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(10);
property.subscribe(Arc::new(|old, new| {
println!("Value changed from {} to {}", old, new);
})).map_err(|e| {
eprintln!("Failed to subscribe: {}", e);
e
})?;
property.set(20).map_err(|e| {
eprintln!("Failed to set property value: {}", e);
e
})?; // Triggers observer notificationSourcepub fn set_async(&self, new_value: T) -> Result<(), PropertyError>
pub fn set_async(&self, new_value: T) -> Result<(), PropertyError>
Sets the property to a new value and notifies observers asynchronously
This method is similar to set() but spawns observers in background threads
for non-blocking operation. This is useful when observers might perform
time-consuming operations.
Observers are batched into groups and each batch runs in its own thread to limit resource usage while still providing parallelism.
§Thread Management (Fire-and-Forget Pattern)
Important: This method uses a fire-and-forget pattern. Spawned threads are not joined and run independently in the background. This design is intentional for non-blocking behavior but has important implications:
§Characteristics:
- ✅ Non-blocking: Returns immediately without waiting for observers
- ✅ High performance: No synchronization overhead
- ⚠️ No completion guarantee: Thread may still be running when method returns
- ⚠️ No error propagation: Observer errors are logged but not returned
- ⚠️ Testing caveat: May need explicit delays to observe side effects
- ⚠️ Ordering caveat: Multiple rapid
set_async()calls may result in observers receiving notifications out of order due to thread scheduling. Useset()if sequential ordering is critical.
§Use Cases:
- UI updates: Fire updates without blocking the main thread
- Logging: Asynchronous logging that doesn’t block operations
- Metrics: Non-critical telemetry that can be lost
- Notifications: Fire-and-forget alerts or messages
§When NOT to Use:
- Critical operations: Use
set()if you need guarantees - Transactional updates: Use
set()for atomic operations - Sequential dependencies: If next operation depends on observer completion
§Testing Considerations:
use observable_property::ObservableProperty;
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
use std::time::Duration;
let property = ObservableProperty::new(0);
let was_called = Arc::new(AtomicBool::new(false));
let flag = was_called.clone();
property.subscribe(Arc::new(move |_, _| {
flag.store(true, Ordering::SeqCst);
}))?;
property.set_async(42)?;
// ⚠️ Immediate check might fail - thread may not have run yet
// assert!(was_called.load(Ordering::SeqCst)); // May fail!
// ✅ Add a small delay to allow background thread to complete
std::thread::sleep(Duration::from_millis(10));
assert!(was_called.load(Ordering::SeqCst)); // Now reliable§Arguments
new_value- The new value to set
§Returns
Ok(()) if successful, or Err(PropertyError) if the lock is poisoned.
Note that this only indicates the property was updated successfully;
observer execution happens asynchronously and errors are not returned.
§Examples
§Basic Usage
use observable_property::ObservableProperty;
use std::sync::Arc;
use std::time::Duration;
let property = ObservableProperty::new(0);
property.subscribe(Arc::new(|old, new| {
// This observer does slow work but won't block the caller
std::thread::sleep(Duration::from_millis(100));
println!("Slow observer: {} -> {}", old, new);
})).map_err(|e| {
eprintln!("Failed to subscribe: {}", e);
e
})?;
// This returns immediately even though observer is slow
property.set_async(42).map_err(|e| {
eprintln!("Failed to set value asynchronously: {}", e);
e
})?;
// Continue working immediately - observer runs in background
println!("Main thread continues without waiting");§Multiple Rapid Updates
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(0);
property.subscribe(Arc::new(|old, new| {
// Expensive operation (e.g., database update, API call)
println!("Processing: {} -> {}", old, new);
}))?;
// All of these return immediately - observers run in parallel
property.set_async(1)?;
property.set_async(2)?;
property.set_async(3)?;
property.set_async(4)?;
property.set_async(5)?;
// All observer calls are now running in background threadsSourcepub fn begin_update(&self) -> Result<(), PropertyError>
pub fn begin_update(&self) -> Result<(), PropertyError>
Begins a batch update, suppressing observer notifications
Call this method to start a batch of updates where you want to change the value multiple times but only notify observers once at the end. This is useful for bulk updates where intermediate values don’t matter.
§Nested Batches
This method supports nesting - you can call begin_update() multiple times
and must call end_update() the same number of times. Observers will only
be notified when the outermost batch is completed.
§Thread Safety
Each batch is scoped to the current execution context. If you begin a batch in one thread, it won’t affect other threads.
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(0);
property.subscribe(Arc::new(|old, new| {
println!("Value changed: {} -> {}", old, new);
}))?;
// Begin batch update
property.begin_update()?;
// These changes won't trigger notifications
property.set(10)?;
property.set(20)?;
property.set(30)?;
// End batch - single notification from 0 to 30
property.end_update()?;§Nested Batches
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(0);
property.subscribe(Arc::new(|old, new| {
println!("Value changed: {} -> {}", old, new);
}))?;
property.begin_update()?; // Outer batch
property.set(5)?;
property.begin_update()?; // Inner batch
property.set(10)?;
property.end_update()?; // End inner batch (no notification yet)
property.set(15)?;
property.end_update()?; // End outer batch - notification sent: 0 -> 15Sourcepub fn end_update(&self) -> Result<(), PropertyError>
pub fn end_update(&self) -> Result<(), PropertyError>
Ends a batch update, sending a single notification with the final value
This method completes a batch update started with begin_update(). When the
outermost batch is completed, observers will be notified once with the value
change from the start of the batch to the final value.
§Behavior
- If the value hasn’t changed during the batch, no notification is sent
- Supports nested batches - only notifies when all batches are complete
- If called without a matching
begin_update(), returns an error
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(0);
property.subscribe(Arc::new(|old, new| {
println!("Value changed: {} -> {}", old, new);
}))?;
property.begin_update()?;
property.set(10)?;
property.set(20)?;
property.end_update()?; // Prints: "Value changed: 0 -> 20"Sourcepub fn subscribe(
&self,
observer: Observer<T>,
) -> Result<ObserverId, PropertyError>
pub fn subscribe( &self, observer: Observer<T>, ) -> Result<ObserverId, PropertyError>
Subscribes an observer function to be called when the property changes
The observer function will be called with the old and new values whenever
the property is modified via set() or set_async().
§Arguments
observer- A function wrapped inArcthat takes(&T, &T)parameters
§Returns
Ok(ObserverId) containing a unique identifier for this observer,
or Err(PropertyError::InvalidConfiguration) if the maximum observer limit is exceeded.
§Observer Limit
To prevent memory exhaustion, there is a maximum limit of observers per property
(currently set to 10,000). If you attempt to add more observers than this limit,
the subscription will fail with an InvalidConfiguration error.
This protection helps prevent:
- Memory leaks from forgotten unsubscriptions
- Unbounded memory growth in long-running applications
- Out-of-memory conditions in resource-constrained environments
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(0);
let observer_id = property.subscribe(Arc::new(|old_value, new_value| {
println!("Property changed from {} to {}", old_value, new_value);
})).map_err(|e| {
eprintln!("Failed to subscribe observer: {}", e);
e
})?;
// Later, unsubscribe using the returned ID
property.unsubscribe(observer_id).map_err(|e| {
eprintln!("Failed to unsubscribe observer: {}", e);
e
})?;Sourcepub fn unsubscribe(&self, id: ObserverId) -> Result<bool, PropertyError>
pub fn unsubscribe(&self, id: ObserverId) -> Result<bool, PropertyError>
Removes an observer identified by its ID
§Arguments
id- The observer ID returned bysubscribe()
§Returns
Ok(bool) where true means the observer was found and removed,
false means no observer with that ID existed.
Returns Err(PropertyError) if the lock is poisoned.
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(0);
let id = property.subscribe(Arc::new(|_, _| {})).map_err(|e| {
eprintln!("Failed to subscribe: {}", e);
e
})?;
let was_removed = property.unsubscribe(id).map_err(|e| {
eprintln!("Failed to unsubscribe: {}", e);
e
})?;
assert!(was_removed); // Observer existed and was removed
let was_removed_again = property.unsubscribe(id).map_err(|e| {
eprintln!("Failed to unsubscribe again: {}", e);
e
})?;
assert!(!was_removed_again); // Observer no longer existsSourcepub fn subscribe_weak(
&self,
observer: Weak<dyn Fn(&T, &T) + Send + Sync>,
) -> Result<ObserverId, PropertyError>
pub fn subscribe_weak( &self, observer: Weak<dyn Fn(&T, &T) + Send + Sync>, ) -> Result<ObserverId, PropertyError>
Subscribes a weak observer that automatically cleans up when dropped
Unlike subscribe() which holds a strong reference to the observer, this method
stores only a weak reference. When the observer’s Arc is dropped elsewhere,
the observer will be automatically removed from the property on the next notification.
This is useful for scenarios where you want observers to have independent lifetimes
without needing explicit unsubscribe calls or Subscription guards.
§Arguments
observer- A weak reference to the observer function
§Returns
Ok(ObserverId) containing a unique identifier for this observer,
or Err(PropertyError::InvalidConfiguration) if the maximum observer limit is exceeded.
§Automatic Cleanup
The observer will be automatically removed when:
- The
Arcthat theWeakwas created from is dropped - The next notification occurs (via
set(),set_async(),modify(), etc.)
§Examples
§Basic Weak Observer
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(0);
{
// Create observer as trait object
let observer: Arc<dyn Fn(&i32, &i32) + Send + Sync> = Arc::new(|old: &i32, new: &i32| {
println!("Value changed: {} -> {}", old, new);
});
// Subscribe with a weak reference
property.subscribe_weak(Arc::downgrade(&observer))?;
property.set(42)?; // Prints: "Value changed: 0 -> 42"
// When observer Arc goes out of scope, weak reference becomes invalid
}
// Next set automatically cleans up the dead observer
property.set(100)?; // No output - observer was automatically cleaned up§Managing Observer Lifetime
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(String::from("initial"));
// Store the observer Arc somewhere accessible (as trait object)
let observer: Arc<dyn Fn(&String, &String) + Send + Sync> = Arc::new(|old: &String, new: &String| {
println!("Text changed: '{}' -> '{}'", old, new);
});
property.subscribe_weak(Arc::downgrade(&observer))?;
property.set(String::from("updated"))?; // Works - observer is alive
// Explicitly drop the observer when done
drop(observer);
property.set(String::from("final"))?; // No output - observer was dropped§Multi-threaded Weak Observers
use observable_property::ObservableProperty;
use std::sync::Arc;
use std::thread;
let property = Arc::new(ObservableProperty::new(0));
let property_clone = property.clone();
// Create observer as trait object
let observer: Arc<dyn Fn(&i32, &i32) + Send + Sync> = Arc::new(|old: &i32, new: &i32| {
println!("Thread observer: {} -> {}", old, new);
});
property.subscribe_weak(Arc::downgrade(&observer))?;
let handle = thread::spawn(move || {
property_clone.set(42)
});
handle.join().unwrap()?; // Prints: "Thread observer: 0 -> 42"
// Observer is still alive
property.set(100)?; // Prints: "Thread observer: 42 -> 100"
// Drop the observer
drop(observer);
property.set(200)?; // No output§Comparison with subscribe() and subscribe_with_subscription()
subscribe(): Holds strong reference, requires manualunsubscribe()subscribe_with_subscription(): Holds strong reference, automatic cleanup via RAII guardsubscribe_weak(): Holds weak reference, cleanup when Arc is dropped elsewhere
Use subscribe_weak() when:
- You want to control observer lifetime independently from subscriptions
- You need multiple code paths to potentially drop the observer
- You want to avoid keeping observers alive longer than necessary
Sourcepub fn subscribe_filtered<F>(
&self,
observer: Observer<T>,
filter: F,
) -> Result<ObserverId, PropertyError>
pub fn subscribe_filtered<F>( &self, observer: Observer<T>, filter: F, ) -> Result<ObserverId, PropertyError>
Subscribes an observer that only gets called when a filter condition is met
This is useful for observing only specific types of changes, such as when a value increases or crosses a threshold.
§Arguments
observer- The observer function to call when the filter passesfilter- A predicate function that receives(old_value, new_value)and returnsbool
§Returns
Ok(ObserverId) for the filtered observer, or Err(PropertyError) if the lock is poisoned.
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(0);
// Only notify when value increases
let id = property.subscribe_filtered(
Arc::new(|old, new| println!("Value increased: {} -> {}", old, new)),
|old, new| new > old
).map_err(|e| {
eprintln!("Failed to subscribe filtered observer: {}", e);
e
})?;
property.set(10).map_err(|e| {
eprintln!("Failed to set value: {}", e);
e
})?; // Triggers observer (0 -> 10)
property.set(5).map_err(|e| {
eprintln!("Failed to set value: {}", e);
e
})?; // Does NOT trigger observer (10 -> 5)
property.set(15).map_err(|e| {
eprintln!("Failed to set value: {}", e);
e
})?; // Triggers observer (5 -> 15)Sourcepub fn subscribe_debounced(
&self,
observer: Observer<T>,
debounce_duration: Duration,
) -> Result<ObserverId, PropertyError>
pub fn subscribe_debounced( &self, observer: Observer<T>, debounce_duration: Duration, ) -> Result<ObserverId, PropertyError>
Subscribes an observer that only gets called after changes stop for a specified duration
Debouncing delays observer notifications until a quiet period has passed. Each new change resets the timer. This is useful for expensive operations that shouldn’t run on every single change, such as auto-save, search-as-you-type, or form validation.
§How It Works
When the property changes:
- A timer starts for the specified
debounce_duration - If another change occurs before the timer expires, the timer resets
- When the timer finally expires with no new changes, the observer is notified
- Only the most recent change is delivered to the observer
§Arguments
observer- The observer function to call after the debounce perioddebounce_duration- How long to wait after the last change before notifying
§Returns
Ok(ObserverId) for the debounced observer, or Err(PropertyError) if subscription fails.
§Examples
§Auto-Save Example
use observable_property::ObservableProperty;
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
use std::time::Duration;
let document = ObservableProperty::new("".to_string());
let save_count = Arc::new(AtomicUsize::new(0));
let count_clone = save_count.clone();
// Auto-save only after user stops typing for 500ms
document.subscribe_debounced(
Arc::new(move |_old, new| {
count_clone.fetch_add(1, Ordering::SeqCst);
println!("Auto-saving: {}", new);
}),
Duration::from_millis(500)
)?;
// Rapid changes (user typing)
document.set("H".to_string())?;
document.set("He".to_string())?;
document.set("Hel".to_string())?;
document.set("Hell".to_string())?;
document.set("Hello".to_string())?;
// At this point, no auto-save has occurred yet
assert_eq!(save_count.load(Ordering::SeqCst), 0);
// Wait for debounce period
std::thread::sleep(Duration::from_millis(600));
// Now auto-save has occurred exactly once with the final value
assert_eq!(save_count.load(Ordering::SeqCst), 1);§Search-as-You-Type Example
use observable_property::ObservableProperty;
use std::sync::Arc;
use std::time::Duration;
let search_query = ObservableProperty::new("".to_string());
// Only search after user stops typing for 300ms
search_query.subscribe_debounced(
Arc::new(|_old, new| {
if !new.is_empty() {
println!("Searching for: {}", new);
// Perform expensive API call here
}
}),
Duration::from_millis(300)
)?;
// User types quickly - no searches triggered yet
search_query.set("r".to_string())?;
search_query.set("ru".to_string())?;
search_query.set("rus".to_string())?;
search_query.set("rust".to_string())?;
// Wait for debounce
std::thread::sleep(Duration::from_millis(400));
// Now search executes once with "rust"§Form Validation Example
use observable_property::ObservableProperty;
use std::sync::Arc;
use std::time::Duration;
let email = ObservableProperty::new("".to_string());
// Validate email only after user stops typing for 500ms
email.subscribe_debounced(
Arc::new(|_old, new| {
if new.contains('@') && new.contains('.') {
println!("✓ Email looks valid");
} else if !new.is_empty() {
println!("✗ Email appears invalid");
}
}),
Duration::from_millis(500)
)?;
email.set("user".to_string())?;
email.set("user@".to_string())?;
email.set("user@ex".to_string())?;
email.set("user@example".to_string())?;
email.set("user@example.com".to_string())?;
// Validation only runs once after typing stops
std::thread::sleep(Duration::from_millis(600));§Performance Considerations
- Each debounced observer spawns a background thread when changes occur
- The thread sleeps for the debounce duration and then checks if it should notify
- Multiple rapid changes don’t create multiple threads - they just update the pending value
- Memory overhead: ~2 Mutex allocations per debounced observer
§Thread Safety
Debounced observers are fully thread-safe. Multiple threads can trigger changes simultaneously, and the debouncing logic will correctly handle the most recent value.
Sourcepub fn subscribe_throttled(
&self,
observer: Observer<T>,
throttle_interval: Duration,
) -> Result<ObserverId, PropertyError>
pub fn subscribe_throttled( &self, observer: Observer<T>, throttle_interval: Duration, ) -> Result<ObserverId, PropertyError>
Subscribes an observer that gets called at most once per specified duration
Throttling ensures that regardless of how frequently the property changes,
the observer is notified at most once per throttle_interval. The first change
triggers an immediate notification, then subsequent changes are rate-limited.
§How It Works
When the property changes:
- If enough time has passed since the last notification, notify immediately
- Otherwise, schedule a notification for after the throttle interval expires
- During the throttle interval, additional changes update the pending value but don’t trigger additional notifications
§Arguments
observer- The observer function to call (rate-limited)throttle_interval- Minimum time between observer notifications
§Returns
Ok(ObserverId) for the throttled observer, or Err(PropertyError) if subscription fails.
§Examples
§Scroll Event Handling
use observable_property::ObservableProperty;
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
use std::time::Duration;
let scroll_position = ObservableProperty::new(0);
let update_count = Arc::new(AtomicUsize::new(0));
let count_clone = update_count.clone();
// Update UI at most every 100ms, even if scrolling continuously
scroll_position.subscribe_throttled(
Arc::new(move |_old, new| {
count_clone.fetch_add(1, Ordering::SeqCst);
println!("Updating UI for scroll position: {}", new);
}),
Duration::from_millis(100)
)?;
// Rapid scroll events (e.g., 60fps = ~16ms per frame)
for i in 1..=20 {
scroll_position.set(i * 10)?;
std::thread::sleep(Duration::from_millis(16));
}
// UI updates happened less frequently than scroll events
let updates = update_count.load(Ordering::SeqCst);
assert!(updates < 20); // Throttled to ~100ms intervals
assert!(updates > 0); // But at least some updates occurred§Mouse Movement Tracking
use observable_property::ObservableProperty;
use std::sync::Arc;
use std::time::Duration;
let mouse_position = ObservableProperty::new((0, 0));
// Track mouse position, but only log every 200ms
mouse_position.subscribe_throttled(
Arc::new(|_old, new| {
println!("Mouse at: ({}, {})", new.0, new.1);
}),
Duration::from_millis(200)
)?;
// Simulate rapid mouse movements
for x in 0..50 {
mouse_position.set((x, x * 2))?;
std::thread::sleep(Duration::from_millis(10));
}§API Rate Limiting
use observable_property::ObservableProperty;
use std::sync::Arc;
use std::time::Duration;
let sensor_reading = ObservableProperty::new(0.0);
// Send sensor data to API at most once per second
sensor_reading.subscribe_throttled(
Arc::new(|_old, new| {
println!("Sending to API: {:.2}", new);
// Actual API call would go here
}),
Duration::from_secs(1)
)?;
// High-frequency sensor updates
for i in 0..100 {
sensor_reading.set(i as f64 * 0.1)?;
std::thread::sleep(Duration::from_millis(50));
}§Difference from Debouncing
use observable_property::ObservableProperty;
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
use std::time::Duration;
let property = ObservableProperty::new(0);
let throttle_count = Arc::new(AtomicUsize::new(0));
let debounce_count = Arc::new(AtomicUsize::new(0));
let throttle_clone = throttle_count.clone();
let debounce_clone = debounce_count.clone();
// Throttling: Notifies periodically during continuous changes
property.subscribe_throttled(
Arc::new(move |_, _| {
throttle_clone.fetch_add(1, Ordering::SeqCst);
}),
Duration::from_millis(100)
)?;
// Debouncing: Notifies only after changes stop
property.subscribe_debounced(
Arc::new(move |_, _| {
debounce_clone.fetch_add(1, Ordering::SeqCst);
}),
Duration::from_millis(100)
)?;
// Continuous changes for 500ms
for i in 1..=50 {
property.set(i)?;
std::thread::sleep(Duration::from_millis(10));
}
// Wait for debounce to complete
std::thread::sleep(Duration::from_millis(150));
// Throttled: Multiple notifications during the period
assert!(throttle_count.load(Ordering::SeqCst) >= 4);
// Debounced: Single notification after changes stopped
assert_eq!(debounce_count.load(Ordering::SeqCst), 1);§Performance Considerations
- Throttled observers spawn background threads to handle delayed notifications
- First notification is immediate (no delay), subsequent ones are rate-limited
- Memory overhead: ~1 Mutex allocation per throttled observer
§Thread Safety
Throttled observers are fully thread-safe. Multiple threads can trigger changes and the throttling logic will correctly enforce the rate limit.
Sourcepub fn notify_observers_batch(
&self,
changes: Vec<(T, T)>,
) -> Result<(), PropertyError>
pub fn notify_observers_batch( &self, changes: Vec<(T, T)>, ) -> Result<(), PropertyError>
Notifies all observers with a batch of changes
This method allows you to trigger observer notifications for multiple
value changes efficiently. Unlike individual set() calls, this method
acquires the observer list once and then notifies all observers with each
change in the batch.
§Performance Characteristics
- Lock optimization: Acquires read lock only to snapshot observers, then releases it
- Non-blocking: Other operations can proceed during observer notifications
- Panic isolation: Individual observer panics don’t affect other observers
§Arguments
changes- A vector of tuples(old_value, new_value)to notify observers about
§Returns
Ok(()) if successful. Observer errors are logged but don’t cause the method to fail.
§Examples
use observable_property::ObservableProperty;
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
let property = ObservableProperty::new(0);
let call_count = Arc::new(AtomicUsize::new(0));
let count_clone = call_count.clone();
property.subscribe(Arc::new(move |old, new| {
count_clone.fetch_add(1, Ordering::SeqCst);
println!("Change: {} -> {}", old, new);
}))?;
// Notify with multiple changes at once
property.notify_observers_batch(vec![
(0, 10),
(10, 20),
(20, 30),
])?;
assert_eq!(call_count.load(Ordering::SeqCst), 3);§Note
This method does NOT update the property’s actual value - it only triggers
observer notifications. Use set() if you want to update the value and
notify observers.
Sourcepub fn subscribe_with_subscription(
&self,
observer: Observer<T>,
) -> Result<Subscription<T>, PropertyError>
pub fn subscribe_with_subscription( &self, observer: Observer<T>, ) -> Result<Subscription<T>, PropertyError>
Subscribes an observer and returns a RAII guard for automatic cleanup
This method is similar to subscribe() but returns a Subscription object
that automatically removes the observer when it goes out of scope. This
provides a more convenient and safer alternative to manual subscription
management.
§Arguments
observer- A function wrapped inArcthat takes(&T, &T)parameters
§Returns
Ok(Subscription<T>) containing a RAII guard for the observer,
or Err(PropertyError) if the lock is poisoned.
§Examples
§Basic RAII Subscription
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(0);
{
let _subscription = property.subscribe_with_subscription(Arc::new(|old, new| {
println!("Value: {} -> {}", old, new);
}))?;
property.set(42)?; // Prints: "Value: 0 -> 42"
property.set(100)?; // Prints: "Value: 42 -> 100"
// Automatic cleanup when _subscription goes out of scope
}
property.set(200)?; // No output - subscription was cleaned up§Comparison with Manual Management
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new("initial".to_string());
// Method 1: Manual subscription management (traditional approach)
let observer_id = property.subscribe(Arc::new(|old, new| {
println!("Manual: {} -> {}", old, new);
}))?;
// Method 2: RAII subscription management (recommended)
let _subscription = property.subscribe_with_subscription(Arc::new(|old, new| {
println!("RAII: {} -> {}", old, new);
}))?;
// Both observers will be called
property.set("changed".to_string())?;
// Prints:
// "Manual: initial -> changed"
// "RAII: initial -> changed"
// Manual cleanup required for first observer
property.unsubscribe(observer_id)?;
// Second observer (_subscription) is automatically cleaned up when
// the variable goes out of scope - no manual intervention needed§Error Handling with Early Returns
use observable_property::ObservableProperty;
use std::sync::Arc;
fn process_with_monitoring(property: &ObservableProperty<i32>) -> Result<(), observable_property::PropertyError> {
let _monitoring = property.subscribe_with_subscription(Arc::new(|old, new| {
println!("Processing: {} -> {}", old, new);
}))?;
property.set(1)?;
if property.get()? > 0 {
return Ok(()); // Subscription automatically cleaned up on early return
}
property.set(2)?;
Ok(()) // Subscription automatically cleaned up on normal return
}
let property = ObservableProperty::new(0);
process_with_monitoring(&property)?; // Monitoring active only during function call
property.set(99)?; // No monitoring output - subscription was cleaned up§Multi-threaded Subscription Management
use observable_property::ObservableProperty;
use std::sync::Arc;
use std::thread;
let property = Arc::new(ObservableProperty::new(0));
let property_clone = property.clone();
let handle = thread::spawn(move || -> Result<(), observable_property::PropertyError> {
let _subscription = property_clone.subscribe_with_subscription(Arc::new(|old, new| {
println!("Thread observer: {} -> {}", old, new);
}))?;
property_clone.set(42)?; // Prints: "Thread observer: 0 -> 42"
// Subscription automatically cleaned up when thread ends
Ok(())
});
handle.join().unwrap()?;
property.set(100)?; // No output - thread subscription was cleaned up§Use Cases
This method is particularly useful in scenarios such as:
- Temporary observers that should be active only during a specific scope
- Error-prone code where manual cleanup might be forgotten
- Complex control flow where multiple exit points make manual cleanup difficult
- Resource-constrained environments where observer leaks are problematic
Sourcepub fn subscribe_filtered_with_subscription<F>(
&self,
observer: Observer<T>,
filter: F,
) -> Result<Subscription<T>, PropertyError>
pub fn subscribe_filtered_with_subscription<F>( &self, observer: Observer<T>, filter: F, ) -> Result<Subscription<T>, PropertyError>
Subscribes a filtered observer and returns a RAII guard for automatic cleanup
This method combines the functionality of subscribe_filtered() with the automatic
cleanup benefits of subscribe_with_subscription(). The observer will only be
called when the filter condition is satisfied, and it will be automatically
unsubscribed when the returned Subscription goes out of scope.
§Arguments
observer- The observer function to call when the filter passesfilter- A predicate function that receives(old_value, new_value)and returnsbool
§Returns
Ok(Subscription<T>) containing a RAII guard for the filtered observer,
or Err(PropertyError) if the lock is poisoned.
§Examples
§Basic Filtered RAII Subscription
use observable_property::ObservableProperty;
use std::sync::Arc;
let counter = ObservableProperty::new(0);
{
// Monitor only increases with automatic cleanup
let _increase_monitor = counter.subscribe_filtered_with_subscription(
Arc::new(|old, new| {
println!("Counter increased: {} -> {}", old, new);
}),
|old, new| new > old
)?;
counter.set(5)?; // Prints: "Counter increased: 0 -> 5"
counter.set(3)?; // No output (decrease)
counter.set(7)?; // Prints: "Counter increased: 3 -> 7"
// Subscription automatically cleaned up when leaving scope
}
counter.set(10)?; // No output - subscription was cleaned up§Multi-Condition Temperature Monitoring
use observable_property::ObservableProperty;
use std::sync::Arc;
let temperature = ObservableProperty::new(20.0_f64);
{
// Create filtered subscription that only triggers for significant temperature increases
let _heat_warning = temperature.subscribe_filtered_with_subscription(
Arc::new(|old_temp, new_temp| {
println!("🔥 Heat warning! Temperature rose from {:.1}°C to {:.1}°C",
old_temp, new_temp);
}),
|old, new| new > old && (new - old) > 5.0 // Only trigger for increases > 5°C
)?;
// Create another filtered subscription for cooling alerts
let _cooling_alert = temperature.subscribe_filtered_with_subscription(
Arc::new(|old_temp, new_temp| {
println!("❄️ Cooling alert! Temperature dropped from {:.1}°C to {:.1}°C",
old_temp, new_temp);
}),
|old, new| new < old && (old - new) > 3.0 // Only trigger for decreases > 3°C
)?;
// Test the filters
temperature.set(22.0)?; // No alerts (increase of only 2°C)
temperature.set(28.0)?; // Heat warning triggered (increase of 6°C from 22°C)
temperature.set(23.0)?; // Cooling alert triggered (decrease of 5°C)
// Both subscriptions are automatically cleaned up when they go out of scope
}
temperature.set(35.0)?; // No alerts - subscriptions were cleaned up§Conditional Monitoring with Complex Filters
use observable_property::ObservableProperty;
use std::sync::Arc;
let stock_price = ObservableProperty::new(100.0_f64);
{
// Monitor significant price movements (> 5% change)
let _volatility_alert = stock_price.subscribe_filtered_with_subscription(
Arc::new(|old_price, new_price| {
let change_percent = ((new_price - old_price) / old_price * 100.0).abs();
println!("📈 Significant price movement: ${:.2} -> ${:.2} ({:.1}%)",
old_price, new_price, change_percent);
}),
|old, new| {
let change_percent = ((new - old) / old * 100.0).abs();
change_percent > 5.0 // Trigger on > 5% change
}
)?;
stock_price.set(103.0)?; // No alert (3% change)
stock_price.set(108.0)?; // Alert triggered (4.85% from 103, but let's say it rounds up)
stock_price.set(95.0)?; // Alert triggered (12% decrease)
// Subscription automatically cleaned up when leaving scope
}
stock_price.set(200.0)?; // No alert - monitoring ended§Cross-Thread Filtered Monitoring
use observable_property::ObservableProperty;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
let network_latency = Arc::new(ObservableProperty::new(50)); // milliseconds
let latency_clone = network_latency.clone();
let monitor_handle = thread::spawn(move || -> Result<(), observable_property::PropertyError> {
// Monitor high latency in background thread with automatic cleanup
let _high_latency_alert = latency_clone.subscribe_filtered_with_subscription(
Arc::new(|old_ms, new_ms| {
println!("⚠️ High latency detected: {}ms -> {}ms", old_ms, new_ms);
}),
|_, new| *new > 100 // Alert when latency exceeds 100ms
)?;
// Simulate monitoring for a short time
thread::sleep(Duration::from_millis(10));
// Subscription automatically cleaned up when thread ends
Ok(())
});
// Simulate network conditions
network_latency.set(80)?; // No alert (under threshold)
network_latency.set(150)?; // Alert triggered in background thread
monitor_handle.join().unwrap()?;
network_latency.set(200)?; // No alert - background monitoring ended§Use Cases
This method is ideal for:
- Threshold-based monitoring with automatic cleanup
- Temporary conditional observers in specific code blocks
- Event-driven systems where observers should be active only during certain phases
- Resource management scenarios where filtered observers have limited lifetimes
§Performance Notes
The filter function is evaluated for every property change, so it should be lightweight. Complex filtering logic should be optimized to avoid performance bottlenecks, especially in high-frequency update scenarios.
Sourcepub fn with_config(
initial_value: T,
max_threads: usize,
max_observers: usize,
) -> Self
pub fn with_config( initial_value: T, max_threads: usize, max_observers: usize, ) -> Self
Creates a new observable property with full configuration control
This constructor provides complete control over the property’s configuration, allowing you to customize both thread pool size and maximum observer count.
§Arguments
initial_value- The starting value for this propertymax_threads- Maximum threads for async notifications (0 = use default)max_observers- Maximum number of allowed observers (0 = use default)
§Examples
use observable_property::ObservableProperty;
// Create a property optimized for high-frequency updates with many observers
let property = ObservableProperty::with_config(0, 8, 50000);
assert_eq!(property.get().unwrap(), 0);Sourcepub fn observer_count(&self) -> usize
pub fn observer_count(&self) -> usize
Returns the current number of active observers
This method is useful for debugging, monitoring, and testing to verify that observers are being properly managed and cleaned up.
§Returns
The number of currently subscribed observers, or 0 if the lock is poisoned.
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let property = ObservableProperty::new(42);
assert_eq!(property.observer_count(), 0);
let id1 = property.subscribe(Arc::new(|_, _| {}))?;
assert_eq!(property.observer_count(), 1);
let id2 = property.subscribe(Arc::new(|_, _| {}))?;
assert_eq!(property.observer_count(), 2);
property.unsubscribe(id1)?;
assert_eq!(property.observer_count(), 1);Sourcepub fn try_get(&self) -> Option<T>
pub fn try_get(&self) -> Option<T>
Gets the current value without Result wrapping
This is a convenience method that returns None if the lock is poisoned
(which shouldn’t happen with graceful degradation) instead of a Result.
§Returns
Some(T) containing the current value, or None if somehow inaccessible.
§Examples
use observable_property::ObservableProperty;
let property = ObservableProperty::new(42);
assert_eq!(property.try_get(), Some(42));Sourcepub fn modify<F>(&self, f: F) -> Result<(), PropertyError>
pub fn modify<F>(&self, f: F) -> Result<(), PropertyError>
Atomically modifies the property value using a closure
This method allows you to update the property based on its current value in a single atomic operation. The closure receives a mutable reference to the value and can modify it in place.
§Arguments
f- A closure that receives&mut Tand modifies it
§Returns
Ok(()) if successful, or Err(PropertyError) if the lock is poisoned.
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let counter = ObservableProperty::new(0);
counter.subscribe(Arc::new(|old, new| {
println!("Counter: {} -> {}", old, new);
}))?;
// Increment counter atomically
counter.modify(|value| *value += 1)?;
assert_eq!(counter.get()?, 1);
// Double the counter atomically
counter.modify(|value| *value *= 2)?;
assert_eq!(counter.get()?, 2);Sourcepub fn map<U, F>(
&self,
transform: F,
) -> Result<ObservableProperty<U>, PropertyError>
pub fn map<U, F>( &self, transform: F, ) -> Result<ObservableProperty<U>, PropertyError>
Creates a derived property that automatically updates when this property changes
This method applies a transformation function to create a new ObservableProperty of a
potentially different type. The derived property automatically updates whenever the source
property changes, maintaining the transformation relationship.
This enables functional reactive programming patterns and property chaining, similar to
map operations in functional programming or reactive frameworks.
§Type Parameters
U- The type of the derived property (must beClone + Send + Sync + 'static)F- The transformation function type
§Arguments
transform- A function that converts values from typeTto typeU
§Returns
Ok(ObservableProperty<U>)- The derived property with the transformed initial valueErr(PropertyError)- If unable to read the source property or create the subscription
§Lifetime and Ownership
- The derived property remains connected to the source property through an observer subscription
- The subscription keeps both properties alive as long as the source has observers
- When the derived property is dropped, updates stop, but the source property continues working
- The transformation function is called immediately to compute the initial value, then on every change
§Examples
§Temperature Conversion (Celsius to Fahrenheit)
use observable_property::ObservableProperty;
use std::sync::Arc;
// Create a Celsius property
let celsius = ObservableProperty::new(20.0);
// Derive a Fahrenheit property that auto-updates
let fahrenheit = celsius.map(|c| c * 9.0 / 5.0 + 32.0)?;
assert_eq!(fahrenheit.get()?, 68.0);
// Observe the derived property
let _sub = fahrenheit.subscribe_with_subscription(Arc::new(|_old, new| {
println!("Fahrenheit: {:.1}°F", new);
}))?;
celsius.set(25.0)?; // Prints: "Fahrenheit: 77.0°F"
assert_eq!(fahrenheit.get()?, 77.0);
celsius.set(0.0)?; // Prints: "Fahrenheit: 32.0°F"
assert_eq!(fahrenheit.get()?, 32.0);§String Formatting
use observable_property::ObservableProperty;
let count = ObservableProperty::new(42);
// Create a formatted string property
let message = count.map(|n| format!("Count: {}", n))?;
assert_eq!(message.get()?, "Count: 42");
count.set(100)?;
assert_eq!(message.get()?, "Count: 100");§Mathematical Transformations
use observable_property::ObservableProperty;
let radius = ObservableProperty::new(5.0);
// Derive area from radius (πr²)
let area = radius.map(|r| std::f64::consts::PI * r * r)?;
assert!((area.get()? - 78.54).abs() < 0.01);
radius.set(10.0)?;
assert!((area.get()? - 314.16).abs() < 0.01);§Chaining Multiple Transformations
use observable_property::ObservableProperty;
let base = ObservableProperty::new(10);
// Chain multiple transformations
let doubled = base.map(|x| x * 2)?;
let squared = doubled.map(|x| x * x)?;
let formatted = squared.map(|x| format!("Result: {}", x))?;
assert_eq!(formatted.get()?, "Result: 400");
base.set(5)?;
assert_eq!(formatted.get()?, "Result: 100"); // (5 * 2)² = 100§Type Conversion
use observable_property::ObservableProperty;
let integer = ObservableProperty::new(42);
// Convert integer to float
let float_value = integer.map(|i| *i as f64)?;
assert_eq!(float_value.get()?, 42.0);
// Convert to boolean (is even?)
let is_even = integer.map(|i| i % 2 == 0)?;
assert_eq!(is_even.get()?, true);
integer.set(43)?;
assert_eq!(is_even.get()?, false);§Complex Object Transformation
use observable_property::ObservableProperty;
#[derive(Clone)]
struct User {
first_name: String,
last_name: String,
age: u32,
}
let user = ObservableProperty::new(User {
first_name: "John".to_string(),
last_name: "Doe".to_string(),
age: 30,
});
// Derive full name from user
let full_name = user.map(|u| format!("{} {}", u.first_name, u.last_name))?;
assert_eq!(full_name.get()?, "John Doe");
// Derive adult status
let is_adult = user.map(|u| u.age >= 18)?;
assert_eq!(is_adult.get()?, true);§Working with Options
use observable_property::ObservableProperty;
let optional = ObservableProperty::new(Some(42));
// Extract value with default
let value_or_zero = optional.map(|opt| opt.unwrap_or(0))?;
assert_eq!(value_or_zero.get()?, 42);
optional.set(None)?;
assert_eq!(value_or_zero.get()?, 0);§Performance Considerations
- The transformation function is called on every change to the source property
- Keep transformation functions lightweight for best performance
- The derived property maintains its own observer list independent of the source
- Cloning the source property is cheap (internal Arc), but each clone shares the same observers
§Thread Safety
The transformation function must be Send + Sync + 'static as it may be called from
any thread that modifies the source property. Ensure your transformation logic is thread-safe.
§Comparison with computed()
map()is simpler and works on a single source propertycomputed()can depend on multiple source propertiesmap()is an instance method;computed()is a standalone function- For single-source transformations,
map()is more ergonomic
Sourcepub fn update_batch<F>(&self, f: F) -> Result<(), PropertyError>
pub fn update_batch<F>(&self, f: F) -> Result<(), PropertyError>
Updates the property through multiple intermediate states and notifies observers for each change
This method is useful for animations, multi-step transformations, or any scenario where you want to record and notify observers about intermediate states during a complex update. The provided function receives a mutable reference to the current value and returns a vector of intermediate states. Observers are notified for each transition between states.
§Behavior
- Captures the initial value
- Calls the provided function with
&mut Tto get intermediate states - Updates the property’s value to the final state (last intermediate state, or unchanged if empty)
- Notifies observers for each state transition:
- initial → intermediate[0]
- intermediate[0] → intermediate[1]
- … → intermediate[n]
§Arguments
f- A closure that receives&mut Tand returns a vector of intermediate states
§Returns
Ok(()) if successful, or Err(PropertyError) if the lock is poisoned.
§Examples
§Animation with Intermediate States
use observable_property::ObservableProperty;
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
let position = ObservableProperty::new(0);
let notification_count = Arc::new(AtomicUsize::new(0));
let count_clone = notification_count.clone();
position.subscribe(Arc::new(move |old, new| {
count_clone.fetch_add(1, Ordering::SeqCst);
println!("Position: {} -> {}", old, new);
}))?;
// Animate from 0 to 100 in steps of 25
position.update_batch(|_current| {
vec![25, 50, 75, 100]
})?;
// Observers were notified 4 times:
// 0 -> 25, 25 -> 50, 50 -> 75, 75 -> 100
assert_eq!(notification_count.load(Ordering::SeqCst), 4);
assert_eq!(position.get()?, 100);§Multi-Step Transformation
use observable_property::ObservableProperty;
use std::sync::Arc;
let data = ObservableProperty::new(String::from("hello"));
data.subscribe(Arc::new(|old, new| {
println!("Transformation: '{}' -> '{}'", old, new);
}))?;
// Transform through multiple steps
data.update_batch(|current| {
let step1 = current.to_uppercase(); // "HELLO"
let step2 = format!("{}!", step1); // "HELLO!"
let step3 = format!("{} WORLD", step2); // "HELLO! WORLD"
vec![step1, step2, step3]
})?;
assert_eq!(data.get()?, "HELLO! WORLD");§Counter with Intermediate Values
use observable_property::ObservableProperty;
use std::sync::Arc;
let counter = ObservableProperty::new(0);
counter.subscribe(Arc::new(|old, new| {
println!("Count: {} -> {}", old, new);
}))?;
// Increment with recording intermediate states
counter.update_batch(|current| {
*current += 10; // Modify in place (optional)
vec![5, 8, 10] // Intermediate states to report
})?;
// Final value is the last intermediate state
assert_eq!(counter.get()?, 10);§Empty Intermediate States
use observable_property::ObservableProperty;
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
let value = ObservableProperty::new(42);
let was_notified = Arc::new(AtomicBool::new(false));
let flag = was_notified.clone();
value.subscribe(Arc::new(move |_, _| {
flag.store(true, Ordering::SeqCst);
}))?;
// No intermediate states - value remains unchanged, no notifications
value.update_batch(|current| {
*current = 100; // This modification is ignored
Vec::new() // No intermediate states
})?;
assert!(!was_notified.load(Ordering::SeqCst));
assert_eq!(value.get()?, 42); // Value unchanged§Performance Considerations
- Lock is held during function execution and state collection
- All intermediate states are stored in memory before notification
- Observers are notified sequentially for each state transition
- Consider using
set()ormodify()if you don’t need intermediate state tracking
§Use Cases
- Animations: Smooth transitions through intermediate visual states
- Progressive calculations: Show progress through multi-step computations
- State machines: Record transitions through multiple states
- Debugging: Track how a value transforms through complex operations
- History tracking: Maintain a record of transformation steps
Sourcepub fn bind_bidirectional(
&self,
other: &ObservableProperty<T>,
) -> Result<(), PropertyError>where
T: PartialEq,
pub fn bind_bidirectional(
&self,
other: &ObservableProperty<T>,
) -> Result<(), PropertyError>where
T: PartialEq,
Creates a bidirectional binding between two properties
This method establishes a two-way synchronization where changes to either property will automatically update the other. This is particularly useful for model-view synchronization patterns where a UI control and a data model need to stay in sync.
§How It Works
- Each property subscribes to changes in the other
- When property A changes, property B is updated to match
- When property B changes, property A is updated to match
- Infinite loops are prevented by comparing values before updating
§Loop Prevention
The method uses value comparison to prevent infinite update loops. If the new
value equals the current value, no update is triggered. This requires T to
implement PartialEq.
§Type Requirements
The value type must implement:
Clone- For copying values between propertiesPartialEq- For comparing values to prevent infinite loopsSend + Sync- For thread-safe operation'static- For storing in observers
§Returns
Ok(())if the binding was successfully establishedErr(PropertyError)if subscription fails (e.g., observer limit exceeded)
§Subscription Management
The subscriptions created by this method are stored as strong references and will
remain active until one of the properties is dropped or the observers are manually
unsubscribed. The returned ObserverIds can be used to unsubscribe if needed.
§Examples
§Basic Two-Way Binding
use observable_property::ObservableProperty;
let model = ObservableProperty::new(0);
let view = ObservableProperty::new(0);
// Establish bidirectional binding
model.bind_bidirectional(&view)?;
// Update model - view automatically updates
model.set(42)?;
assert_eq!(view.get()?, 42);
// Update view - model automatically updates
view.set(100)?;
assert_eq!(model.get()?, 100);§Model-View Synchronization
use observable_property::ObservableProperty;
use std::sync::Arc;
// Model representing application state
let username = ObservableProperty::new("".to_string());
// View representing UI input field
let username_field = ObservableProperty::new("".to_string());
// Bind them together
username.bind_bidirectional(&username_field)?;
// Add validation observer on the model
username.subscribe(Arc::new(|_old, new| {
if new.len() > 3 {
println!("Valid username: {}", new);
}
}))?;
// User types in UI field
username_field.set("john".to_string())?;
// Both properties are now "john", validation observer triggered
assert_eq!(username.get()?, "john");
// Programmatic model update
username.set("alice".to_string())?;
// UI field automatically reflects the change
assert_eq!(username_field.get()?, "alice");§Multiple Property Synchronization
use observable_property::ObservableProperty;
let slider_value = ObservableProperty::new(50);
let text_input = ObservableProperty::new(50);
let display_label = ObservableProperty::new(50);
// Create a synchronized group of controls
slider_value.bind_bidirectional(&text_input)?;
slider_value.bind_bidirectional(&display_label)?;
// Update any one of them
text_input.set(75)?;
// All are synchronized
assert_eq!(slider_value.get()?, 75);
assert_eq!(text_input.get()?, 75);
assert_eq!(display_label.get()?, 75);§With Additional Observers
use observable_property::ObservableProperty;
use std::sync::Arc;
let celsius = ObservableProperty::new(0.0);
let fahrenheit = ObservableProperty::new(32.0);
// Note: For unit conversion, you'd typically use computed properties
// instead of bidirectional binding, but this shows the concept
celsius.bind_bidirectional(&fahrenheit)?;
// Add logging to observe synchronization
celsius.subscribe(Arc::new(|old, new| {
println!("Celsius changed: {:.1}°C -> {:.1}°C", old, new);
}))?;
fahrenheit.subscribe(Arc::new(|old, new| {
println!("Fahrenheit changed: {:.1}°F -> {:.1}°F", old, new);
}))?;
celsius.set(100.0)?;
// Both properties are now 100.0 (not a real unit conversion!)
// Prints:
// "Celsius changed: 0.0°C -> 100.0°C"
// "Fahrenheit changed: 32.0°F -> 100.0°F"§Thread Safety
The binding is fully thread-safe. Both properties can be updated from any thread, and the synchronization will work correctly across thread boundaries.
§Performance Considerations
- Each bound property creates two observer subscriptions (one in each direction)
- Value comparisons are performed on every update to prevent loops
- Consider using computed properties for one-way transformations instead
- Binding many properties in a chain may amplify update overhead
§Limitations
- Both properties must have the same type
T - Not suitable for complex transformations (use computed properties instead)
- Value comparison relies on
PartialEqimplementation quality - Circular update chains with 3+ properties may have propagation delays
Source§impl<T: Clone + Default + Send + Sync + 'static> ObservableProperty<T>
impl<T: Clone + Default + Send + Sync + 'static> ObservableProperty<T>
Sourcepub fn get_or_default(&self) -> T
pub fn get_or_default(&self) -> T
Gets the current value or returns the default if inaccessible
This convenience method is only available when T implements Default.
It provides a fallback to T::default() if the value cannot be read.
§Examples
use observable_property::ObservableProperty;
let property = ObservableProperty::new(42);
assert_eq!(property.get_or_default(), 42);
// Even if somehow inaccessible, returns default
let empty_property: ObservableProperty<i32> = ObservableProperty::new(0);
assert_eq!(empty_property.get_or_default(), 0);Trait Implementations§
Source§impl<T: Clone + Send + Sync + 'static> Clone for ObservableProperty<T>
impl<T: Clone + Send + Sync + 'static> Clone for ObservableProperty<T>
Source§fn clone(&self) -> Self
fn clone(&self) -> Self
Creates a new reference to the same observable property
This creates a new ObservableProperty instance that shares the same
underlying data with the original. Changes made through either instance
will be visible to observers subscribed through both instances.
§Examples
use observable_property::ObservableProperty;
use std::sync::Arc;
let property1 = ObservableProperty::new(42);
let property2 = property1.clone();
property2.subscribe(Arc::new(|old, new| {
println!("Observer on property2 saw change: {} -> {}", old, new);
})).map_err(|e| {
eprintln!("Failed to subscribe: {}", e);
e
})?;
// This change through property1 will trigger the observer on property2
property1.set(100).map_err(|e| {
eprintln!("Failed to set value: {}", e);
e
})?;1.0.0 · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more