pub struct EventStore { /* private fields */ }Expand description
Top-level store for tracking multiple events with automatic counter creation.
EventStore provides a high-level API for recording and querying events. Each event is tracked independently with its own SingleEventCounter. Most methods use &self and internal locking for thread safety.
§Thread Safety and Sharing
EventStore is NOT Clone. To share across threads:
- Use
Arc<EventStore>for read-heavy workloads (record, query) - Use
Arc<Mutex<EventStore>>when callingcompact()(&mut self methods)
Auto-persistence is configured via the builder and managed internally.
§Drop and Synchronous I/O Behavior
IMPORTANT: When an EventStore is dropped, it performs synchronous I/O by calling
persist() if any events are dirty. This ensures data is not lost, but has implications:
§When Drop I/O Matters
- Async contexts: Drop runs synchronously and will block the executor. This can cause performance issues or even deadlocks in async code.
- Performance-sensitive code: Drop may take time proportional to the number of dirty events and the storage backend’s performance.
- Panic unwinding: If a panic occurs, Drop still runs, but persistence errors are silently ignored (they cannot be returned during unwinding).
§Best Practices for Cleanup
Option 1 - Use close() (recommended for clarity):
use tiny_counter::{EventStore, storage::MemoryStorage};
let store = EventStore::builder()
.with_storage(MemoryStorage::new())
.build()
.unwrap();
store.record("user_action");
// Explicitly close with error handling
store.close().expect("Failed to close store");
// Store is now consumedOption 2 - Persist then drop:
use tiny_counter::{EventStore, storage::MemoryStorage};
let store = EventStore::builder()
.with_storage(MemoryStorage::new())
.build()
.unwrap();
store.record("user_action");
// Explicitly persist before drop in async context
store.persist().expect("Failed to persist events");
// Now drop is safe - either no dirty data, or we already handled errors
drop(store);With auto-persist: If you configured auto-persist via the builder, Drop still performs a final persist to catch any events recorded after the last auto-persist cycle:
use tiny_counter::{EventStore, storage::MemoryStorage};
use chrono::Duration;
let store = EventStore::builder()
.with_storage(MemoryStorage::new())
.auto_persist(Duration::seconds(60)) // Background task every 60s
.build()
.unwrap();
store.record("event1");
// Auto-persist will eventually save this
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
store.record("event2");
// This might not be saved yet by auto-persist!
// Option 1: Explicit persist before drop (recommended for critical data)
store.persist().expect("Failed to persist");
drop(store);
// Option 2: Let Drop handle it (blocks executor, errors ignored)
// drop(store); // Will call persist() synchronously§Tradeoffs
- Drop with persist: Convenient, prevents data loss, but blocks and may fail silently
- Explicit persist: More verbose, but allows error handling and avoids blocking Drop
- Auto-persist: Reduces manual persist calls, but Drop still needed for final flush
§Alternative: No-Op Drop
If you don’t configure storage (no with_storage() in builder), Drop is a no-op and has
no I/O implications.
Implementations§
Source§impl EventStore
impl EventStore
Sourcepub fn builder() -> EventStoreBuilder
pub fn builder() -> EventStoreBuilder
Creates a builder for configuring an EventStore.
Source§impl EventStore
impl EventStore
Sourcepub fn new() -> Self
pub fn new() -> Self
Creates a new EventStore with default configuration.
Default configuration includes 6 time units totaling 256 buckets:
- 60 Minutes
- 72 Hours
- 56 Days
- 52 Weeks
- 12 Months
- 4 Years
Sourcepub fn record(&self, event_id: impl EventId)
pub fn record(&self, event_id: impl EventId)
Records a single event at the current time.
§Examples
use tiny_counter::EventStore;
let store = EventStore::new();
store.record("app_launch");
store.record("button_click");Sourcepub fn record_count(&self, event_id: impl EventId, count: u32)
pub fn record_count(&self, event_id: impl EventId, count: u32)
Records multiple events at the current time.
§Examples
use tiny_counter::EventStore;
let store = EventStore::new();
store.record_count("api_call", 5);
store.record_count("page_view", 10);Sourcepub fn record_at(
&self,
event_id: impl EventId,
time: DateTime<Utc>,
) -> Result<()>
pub fn record_at( &self, event_id: impl EventId, time: DateTime<Utc>, ) -> Result<()>
Records a single event at a specific time.
§Examples
use tiny_counter::EventStore;
use chrono::{Duration, Utc};
let store = EventStore::new();
let two_days_ago = Utc::now() - Duration::days(2);
store.record_at("feature_used", two_days_ago).unwrap();§Errors
Returns Error::FutureEvent if the timestamp is in the future.
Note on old events: Events beyond the tracking window are silently dropped with no error.
This is intentional - the library uses fixed-size rotating buckets, and old data falls off
as new data arrives. This represents a loss of granularity (e.g., “25 hours ago” becomes
“1 day ago”) rather than complete data loss. For production use, prefer record() for
real-time events. Use record_at() and record_ago() primarily for testing and backfilling.
Sourcepub fn record_count_at(
&self,
event_id: impl EventId,
count: u32,
time: DateTime<Utc>,
) -> Result<()>
pub fn record_count_at( &self, event_id: impl EventId, count: u32, time: DateTime<Utc>, ) -> Result<()>
Records multiple events at a specific time.
§Examples
use tiny_counter::EventStore;
use chrono::{Duration, Utc};
let store = EventStore::new();
let yesterday = Utc::now() - Duration::days(1);
store.record_count_at("sync_event", 3, yesterday).unwrap();§Errors
Returns Error::FutureEvent if the timestamp is in the future.
Note on old events: Events beyond the tracking window are silently dropped with no error.
This is intentional - the library uses fixed-size rotating buckets, and old data falls off
as new data arrives. This represents a loss of granularity (e.g., “25 hours ago” becomes
“1 day ago”) rather than complete data loss. For production use, prefer record() for
real-time events. Use record_count_at() and record_count_ago() primarily for testing and backfilling.
Sourcepub fn record_ago(&self, event_id: impl EventId, duration: Duration)
pub fn record_ago(&self, event_id: impl EventId, duration: Duration)
Records a single event that occurred a duration ago.
Important: Events outside the tracking window are silently dropped with no error or warning. The tracking window depends on your configuration. With default settings (256 buckets across 6 time units), events older than approximately 4 years are dropped.
This represents a loss of granularity (e.g., “25 hours ago” becomes “1 day ago”), not complete data loss. The library uses fixed-size rotating buckets - old data falls off as new data arrives.
Recommendation: Use record() for production real-time events. Use record_ago() primarily
for testing queries or backfilling recent historical data within the tracking window.
This method never returns an error. Use record_at if you need
to detect when events fall outside the tracking window.
§Examples
use tiny_counter::EventStore;
use chrono::Duration;
let store = EventStore::new();
store.record_ago("sync", Duration::hours(3));
// Events too old are dropped silently
store.record_ago("ancient", Duration::days(365 * 10));
let sum = store.query("ancient").ever().sum();
assert_eq!(sum, Some(0)); // Event was droppedSourcepub fn record_count_ago(
&self,
event_id: impl EventId,
count: u32,
duration: Duration,
)
pub fn record_count_ago( &self, event_id: impl EventId, count: u32, duration: Duration, )
Records multiple events that occurred a duration ago.
Important: Events outside the tracking window are silently dropped with no error or warning. The tracking window depends on your configuration. With default settings (256 buckets across 6 time units), events older than approximately 4 years are dropped.
This represents a loss of granularity (e.g., “25 hours ago” becomes “1 day ago”), not complete data loss. The library uses fixed-size rotating buckets - old data falls off as new data arrives.
Recommendation: Use record_count() for production real-time events. Use record_count_ago()
primarily for testing queries or backfilling recent historical data within the tracking window.
This method never returns an error. Use record_count_at if you need
to detect when events fall outside the tracking window.
§Examples
use tiny_counter::EventStore;
use chrono::Duration;
let store = EventStore::new();
store.record_count_ago("batch_process", 5, Duration::days(1));
// Events too old are dropped silently
store.record_count_ago("ancient_batch", 100, Duration::days(365 * 10));
let sum = store.query("ancient_batch").ever().sum();
assert_eq!(sum, Some(0)); // Events were droppedSourcepub fn query(&self, event_id: impl EventId) -> Query
pub fn query(&self, event_id: impl EventId) -> Query
Creates a query builder for a single event.
§Examples
use tiny_counter::EventStore;
let store = EventStore::new();
store.record("app_launch");
let count = store.query("app_launch").last_days(7).sum();
assert_eq!(count, Some(1));Sourcepub fn query_many(&self, event_ids: &[impl EventId]) -> MultiQuery
pub fn query_many(&self, event_ids: &[impl EventId]) -> MultiQuery
Creates a query builder for multiple events.
Combines counts from multiple events into a single sum.
§Examples
use tiny_counter::EventStore;
let store = EventStore::new();
store.record("app_launch");
store.record("app_resume");
let total_opens = store
.query_many(&["app_launch", "app_resume"])
.last_days(7)
.sum();
assert_eq!(total_opens, Some(2));Sourcepub fn query_ratio(
&self,
numerator: impl EventId,
denominator: impl EventId,
) -> RatioQuery
pub fn query_ratio( &self, numerator: impl EventId, denominator: impl EventId, ) -> RatioQuery
Creates a ratio query builder for two events.
Calculates the ratio of numerator to denominator events.
§Examples
use tiny_counter::EventStore;
let store = EventStore::new();
store.record_count("conversions", 25);
store.record_count("visits", 100);
let conversion_rate = store
.query_ratio("conversions", "visits")
.last_days(7);
assert_eq!(conversion_rate, Some(0.25));Sourcepub fn query_delta(
&self,
positive: impl EventId,
negative: impl EventId,
) -> DeltaQuery
pub fn query_delta( &self, positive: impl EventId, negative: impl EventId, ) -> DeltaQuery
Creates a delta query builder for two events.
Calculates the net change (positive - negative) between two events.
§Examples
use tiny_counter::EventStore;
let store = EventStore::new();
store.record_count("items_added", 10);
store.record_count("items_removed", 3);
let inventory_change = store
.query_delta("items_added", "items_removed")
.last_days(7)
.sum();
assert_eq!(inventory_change, 7);Sourcepub fn is_dirty(&self) -> bool
pub fn is_dirty(&self) -> bool
Returns whether any events have been modified since the last persist.
Sourcepub fn limit(&self) -> Limiter
pub fn limit(&self) -> Limiter
Creates a rate limiter builder for checking constraints.
Use this to create complex rate limiting rules with multiple constraints.
§Examples
use tiny_counter::{EventStore, TimeUnit};
let store = EventStore::new();
let result = store
.limit()
.at_most("api_call", 10, TimeUnit::Minutes)
.at_most("api_call", 100, TimeUnit::Hours)
.check_and_record("api_call");
assert!(result.is_ok());Sourcepub fn balance_delta(
&self,
positive: impl EventId,
negative: impl EventId,
) -> Result<()>
pub fn balance_delta( &self, positive: impl EventId, negative: impl EventId, ) -> Result<()>
Reconciles the delta between two events and records to balance them.
This method calculates the all-time delta (positive - negative) and:
- If delta > 0: records delta to the negative event
- If delta < 0: records |delta| to the positive event
- If delta == 0: does nothing
This is useful for tracking net changes like credits/debits or joins/leaves.
§Examples
use tiny_counter::EventStore;
let store = EventStore::new();
store.record_count("credits", 100);
store.record_count("debits", 30);
// Balance adds 70 to debits to equalize
store.balance_delta("credits", "debits").unwrap();
let delta = store.query_delta("credits", "debits").ever().sum();
assert_eq!(delta, 0);Sourcepub fn persist(&self) -> Result<()>
pub fn persist(&self) -> Result<()>
Persists only dirty (modified) events to storage.
This method performs synchronous I/O to save modified events to the configured storage backend. It’s automatically called by the Drop implementation when the EventStore is dropped, but explicit calls are recommended in async contexts and for error handling.
Returns an error if no storage is configured or if serialization/storage fails. Requires a formatter - either a built-in formatter (serde-bincode, serde-json) or a custom implementation.
§Best Practices
- Async contexts: Call
persist()explicitly before the store goes out of scope to avoid blocking the executor during Drop - Error handling: Explicit calls allow you to handle persistence errors, while Drop silently ignores errors
- With auto-persist: Still call
persist()before drop to ensure the final batch of events is saved and to catch any errors
§Examples
use tiny_counter::{EventStore, storage::MemoryStorage};
let store = EventStore::builder()
.with_storage(MemoryStorage::new())
.build()
.unwrap();
store.record("event");
assert!(store.is_dirty());
// Explicit persist with error handling
store.persist().expect("Failed to persist");
assert!(!store.is_dirty());Sourcepub fn close(self) -> Result<()>
pub fn close(self) -> Result<()>
Explicitly persist and close the event store.
This is a convenience method that calls persist() and then consumes
the EventStore. It’s semantically clearer than calling persist() + drop() and makes
the intent of cleanup explicit.
Preferred over relying on Drop in production code, especially in:
- Async contexts where Drop would block the executor
- Code where error handling is important
- Shutdown sequences where explicit cleanup is desired
§Examples
use tiny_counter::{EventStore, storage::MemoryStorage};
let store = EventStore::builder()
.with_storage(MemoryStorage::new())
.build()
.unwrap();
store.record("event");
// Explicit cleanup with error handling
store.close().expect("Failed to close store");
// Store is now consumed and droppedSourcepub fn persist_all(&self) -> Result<()>
pub fn persist_all(&self) -> Result<()>
Persists all events to storage, regardless of dirty status.
Returns an error if no storage is configured or if serialization/storage fails.
Sourcepub fn reset_dirty(&self)
pub fn reset_dirty(&self)
Clears the dirty flag on all events without persisting.
Use this when you want to mark all events as clean without saving.
Sourcepub fn compact(&mut self) -> Result<()>
pub fn compact(&mut self) -> Result<()>
Compacts storage by loading all events, advancing to current time, persisting back to storage, and clearing memory.
This method:
- Loads all events from storage into memory (triggers convert_if_needed)
- Advances all counters to current time
- Persists all counters (saves non-empty, deletes empty)
- Clears in-memory cache (events will be lazy-loaded as needed)
§Thread Safety
Requires &mut self for exclusive access. When sharing EventStore across
threads, wrap in Arc<Mutex<EventStore>> to safely call this method.
Sourcepub fn memory_usage(&self) -> usize
pub fn memory_usage(&self) -> usize
Returns the approximate memory usage in bytes for all tracked events.
This includes the bucket storage for all intervals across all events.
Sourcepub fn tracked_intervals(&self) -> Vec<(TimeUnit, usize)>
pub fn tracked_intervals(&self) -> Vec<(TimeUnit, usize)>
Returns a list of tracked time units and their bucket counts.
This reflects the default configuration used for new events.
Sourcepub fn export_all(&self) -> Result<HashMap<String, SingleEventCounter>>
pub fn export_all(&self) -> Result<HashMap<String, SingleEventCounter>>
Exports all event counters.
Returns a HashMap mapping event IDs to their SingleEventCounter instances. Useful for serialization, backup, or multi-device sync.
This method queries storage to find all event_ids on disk and loads any counters that aren’t already in memory, ensuring a complete snapshot.
Sourcepub fn export_dirty(&self) -> Result<HashMap<String, SingleEventCounter>>
pub fn export_dirty(&self) -> Result<HashMap<String, SingleEventCounter>>
Exports only dirty (modified) event counters.
Returns a HashMap of event IDs to SingleEventCounter for events that have been modified since the last persist or reset_dirty.
Sourcepub fn import_event(
&self,
event_id: impl EventId,
counter: SingleEventCounter,
) -> Result<()>
pub fn import_event( &self, event_id: impl EventId, counter: SingleEventCounter, ) -> Result<()>
Imports a single event counter, creating or replacing it.
If the event already exists, it is replaced entirely (not merged). Marks the event as dirty after import.
Sourcepub fn import_all(
&self,
events: HashMap<String, SingleEventCounter>,
) -> Result<()>
pub fn import_all( &self, events: HashMap<String, SingleEventCounter>, ) -> Result<()>
Imports multiple event counters, creating or replacing them.
This is a batch version of import_event.
Sourcepub fn merge_event(
&self,
event_id: impl EventId,
counter: SingleEventCounter,
) -> Result<()>
pub fn merge_event( &self, event_id: impl EventId, counter: SingleEventCounter, ) -> Result<()>
Merges a single event counter into the store.
If the event doesn’t exist, creates it with the merged counter. If it exists, merges the counts using SingleEventCounter::merge. Marks the event as dirty after merge.
Sourcepub fn merge_all(
&self,
events: HashMap<String, SingleEventCounter>,
) -> Result<()>
pub fn merge_all( &self, events: HashMap<String, SingleEventCounter>, ) -> Result<()>
Merges multiple event counters into the store.
This is a batch version of merge_event.
pub fn merge(&self, other: Self) -> Result<()>
Trait Implementations§
Source§impl Default for EventStore
impl Default for EventStore
Source§impl Drop for EventStore
Drop implementation for EventStore.
impl Drop for EventStore
Drop implementation for EventStore.
IMPORTANT: This implementation performs synchronous I/O which can block the
current thread. See the struct-level documentation for EventStore for best practices.
When an EventStore is dropped:
- The background auto-persist task is aborted (if configured)
- A final
persist()is attempted if the store has dirty (unsaved) events
§Behavior Details
- Blocking I/O: The persist operation blocks until complete. In async contexts, this blocks the executor thread.
- Silent errors: Any errors during persist are ignored (logged to stderr in debug builds). Errors cannot be returned from Drop.
- Panic safety: Drop runs during panic unwinding, so errors are swallowed to avoid double-panics.
§Recommendations
Call persist() explicitly before dropping to:
- Avoid blocking in async code
- Handle errors properly
- Make cleanup behavior explicit