EventStore

Struct EventStore 

Source
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 calling compact() (&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 consumed

Option 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

Source

pub fn builder() -> EventStoreBuilder

Creates a builder for configuring an EventStore.

Source§

impl EventStore

Source

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
Source

pub fn clock_now(&self) -> DateTime<Utc>

Gets current time from clock.

Source

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");
Source

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);
Source

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.

Source

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.

Source

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 dropped
Source

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 dropped
Source

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));
Source

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));
Source

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));
Source

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);
Source

pub fn is_dirty(&self) -> bool

Returns whether any events have been modified since the last persist.

Source

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());
Source

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);
Source

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());
Source

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 dropped
Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn merge(&self, other: Self) -> Result<()>

Trait Implementations§

Source§

impl Default for EventStore

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

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:

  1. The background auto-persist task is aborted (if configured)
  2. 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
Source§

fn drop(&mut self)

Executes the destructor for this type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

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

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

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

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

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

Source§

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

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

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

Source§

fn into(self) -> U

Calls U::from(self).

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

Source§

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

Source§

type Error = Infallible

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

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

Performs the conversion.
Source§

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

Source§

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

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

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

Performs the conversion.