# Reference Guide
Start simple, build to complex patterns.
## Basic Operations
### Recording Events
Record an event that just happened:
```rust
use tiny_counter::EventStore;
let store = EventStore::new();
store.record("app_launch");
```
Record multiple occurrences:
```rust
store.record_count("api_call", 5);
```
Record events from the past:
```rust
use chrono::Duration;
store.record_ago("sync", Duration::hours(3));
```
### Querying Events
Query total events in a time range:
```rust
let launches = store.query("app_launch").last_days(7).sum();
match launches {
Some(count) => println!("{} launches this week", count),
None => println!("No data"),
}
```
Check when an event last occurred:
```rust
if let Some(duration) = store.query("settings_visit").last_seen() {
if duration < Duration::days(7) {
println!("Active user");
}
}
```
## Time Scales
Query at different scales. One `record()` updates all scales:
```rust
let last_minute = store.query("event").last_seconds(60).sum();
let last_hour = store.query("event").last_minutes(60).sum();
let last_day = store.query("event").last_hours(24).sum();
let last_week = store.query("event").last_days(7).sum();
let all_time = store.query("event").ever().sum();
```
Available time units:
- `last_seconds(n)` - Recent N seconds
- `last_minutes(n)` - Recent N minutes
- `last_hours(n)` - Recent N hours
- `last_days(n)` - Recent N days
- `last_weeks(n)` - Recent N weeks
- `last_months(n)` - Recent N months (30-day approximation)
- `last_years(n)` - Recent N years (365-day approximation)
- `ever()` - All tracked data
## Aggregations
Calculate averages:
```rust
// Average per period (includes zeros)
let avg = store.query("api_call").last_days(28).average();
// Average per active period (excludes zeros)
let avg_active = store.query("api_call").last_days(28).average_nonzero();
```
Count active periods:
```rust
let active_days = store.query("app_launch").last_days(28).count_nonzero();
println!("Active {} out of 28 days", active_days.unwrap_or(0));
```
Inspect individual buckets:
```rust
for (i, count) in store.query("event").last_days(7).buckets() {
println!("Day {}: {} events", i, count);
}
```
## Multi-Event Queries
Combine multiple events:
```rust
let total = store
.query_many(&["app_launch", "app_resume"])
.last_days(7)
.sum();
```
Calculate ratios:
```rust
let conversion = store
.query_ratio("purchases", "visits")
.last_days(7);
if let Some(rate) = conversion {
println!("Conversion rate: {:.2}%", rate * 100.0);
}
```
Calculate deltas (net changes):
```rust
let inventory = store
.query_delta("items_added", "items_removed")
.ever()
.sum(); // Returns i64 (can be negative)
println!("Current inventory: {}", inventory);
```
Use deltas for tracking state changes: inventory levels, bank balances, connection counts, or any incremental counter.
## Rate Limiting
### Basic Limits
Enforce frequency limits:
```rust
use tiny_counter::TimeUnit;
let result = store
.limit()
.at_most("api_call", 10, TimeUnit::Minutes)
.check_and_record("api_call");
match result {
Ok(()) => {
// Request allowed
}
Err(e) => {
println!("Rate limited. Retry in {:?}", e.retry_after);
}
}
```
Chain multiple constraints (all must pass):
```rust
store
.limit()
.at_most("api_call", 10, TimeUnit::Minutes)
.at_most("api_call", 100, TimeUnit::Hours)
.check_and_record("api_call")?;
```
### Flexible Time Windows
Specify windows three ways:
```rust
// Single unit (backward compatible)
.at_most("api", 10, TimeUnit::Minutes)
// Multiple units with tuple
.at_most("api", 100, (7, TimeUnit::Days))
// Duration syntax
.at_most("api", 100, Duration::days(7))
```
### Advanced Constraints
Require prerequisites:
```rust
// Require login before API access
store
.limit()
.at_least("user_login", 1, TimeUnit::Days)
.check_and_record("protected_api")?;
```
Enforce cooldowns:
```rust
// Password reset once per hour
store
.limit()
.cooldown("password_reset", Duration::hours(1))
.check_and_record("password_reset")?;
```
Require recent events:
```rust
// Checkout within 30 minutes of cart update
store
.limit()
.within("cart_updated", Duration::minutes(30))
.check_and_record("checkout")?;
```
Time-based restrictions:
```rust
use tiny_counter::Schedule;
// Business hours (9am-5pm UTC)
store
.limit()
.during(Schedule::hours(9, 17)?)
.check_and_record("api_call")?;
// Business hours (9am-5pm in system's local timezone)
store
.limit()
.during(Schedule::hours_local_tz(9, 17)?)
.check_and_record("api_call")?;
// Maintenance outside business hours
store
.limit()
.outside_of(Schedule::hours(9, 17)?)
.check_and_record("maintenance")?;
// Weekdays only (UTC)
store
.limit()
.during(Schedule::weekdays())
.check_and_record("work_task")?;
// Weekdays only (local timezone)
store
.limit()
.during(Schedule::weekdays_local_tz())
.check_and_record("work_task")?;
// Weekends only (local timezone)
store
.limit()
.outside_of(Schedule::weekends_local_tz())
.check_and_record("maintenance")?;
```
### Checking Without Recording
Check constraints without side effects:
```rust
// Boolean check
if store.limit().at_most("api", 100, TimeUnit::Hours).allowed("api") {
// Proceed with request
store.record("api");
}
// Full error details
match store.limit().at_most("api", 100, TimeUnit::Hours).check("api") {
Ok(()) => store.record("api"),
Err(e) => println!("Denied: {:?}", e.constraint),
}
```
Query current usage:
```rust
let usage = store
.limit()
.at_most("api_call", 100, TimeUnit::Hours)
.usage("api_call")?;
println!("Used {}/{} ({} remaining)",
usage.count, usage.limit, usage.remaining);
```
### Transactional Reservations
Prevent race conditions with reservations:
```rust
// Reserve slot atomically
let reservation = store
.limit()
.at_most("api_call", 10, TimeUnit::Hours)
.reserve("api_call")?;
// Do work
match make_api_call() {
Ok(_) => reservation.commit(), // Record on success
Err(_) => reservation.cancel(), // Release on failure
}
// Or let it auto-cancel on drop
```
Concurrent safety:
```rust
// Multiple threads trying to reserve
// Exactly N will succeed (N = limit)
let res1 = store.limit().at_most("api", 10, TimeUnit::Hours).reserve("api")?;
let res2 = store.limit().at_most("api", 10, TimeUnit::Hours).reserve("api")?;
// ... exactly 10 total reservations succeed
res1.commit(); // Count this one
// res2 drops without commit - doesn't count
```
## Persistence
### Setup
Enable persistence with a storage backend:
```rust
use tiny_counter::EventStore;
use tiny_counter::storage::Sqlite;
let store = EventStore::builder()
.with_storage(Sqlite::open("events.db")?)
.build()?;
store.record("event");
store.persist()?; // Save to disk
```
Available backends (features):
- `storage-sqlite` - SQLite database
- `storage-fs` - File-per-event filesystem storage
- `MemoryStorage` - In-memory (testing, no feature required)
### Serialization Formats
The library supports pluggable serialization formats that work with any storage backend.
#### Bincode (default)
```rust
use tiny_counter::formatter::BincodeFormat;
let store = EventStore::builder()
.with_storage(storage)
.with_format(BincodeFormat)
.build()?;
```
Compact binary format, fast serialization/deserialization. Used by default when no format is specified.
#### JSON
```rust
use tiny_counter::formatter::JsonFormat;
let store = EventStore::builder()
.with_storage(storage)
.with_format(JsonFormat)
.build()?;
```
Human-readable format, useful for debugging and inspection. Mix with any storage backend:
```rust
use tiny_counter::storage::FilePerEvent;
// JSON files on disk
let store = EventStore::builder()
.with_storage(FilePerEvent::new("events", ".json")?)
.with_format(JsonFormat)
.build()?;
// JSON in SQLite
let store = EventStore::builder()
.with_storage(Sqlite::open("events.db")?)
.with_format(JsonFormat)
.build()?;
```
#### Custom Formats
Implement the `Formatter` trait for custom serialization:
```rust
use tiny_counter::{Formatter, Result, SingleEventCounter};
struct MyFormat;
impl Formatter for MyFormat {
fn serialize(&self, value: &SingleEventCounter) -> Result<Vec<u8>> {
// Your serialization logic
}
fn deserialize(&self, bytes: &[u8]) -> Result<SingleEventCounter> {
// Your deserialization logic
}
}
let store = EventStore::builder()
.with_storage(storage)
.with_format(MyFormat)
.build()?;
```
### Dirty Tracking
Store tracks modified events:
```rust
store.record("event1");
store.record("event2");
store.persist()?; // Writes both
store.record("event3");
store.persist()?; // Writes only event3
```
Check dirty state:
```rust
if store.is_dirty() {
store.persist()?;
}
```
Force save all events:
```rust
store.persist_all()?;
```
### Auto-Persistence (Tokio)
Background persistence with tokio feature:
```rust
let store = EventStore::builder()
.with_storage(storage)
.auto_persist(Duration::seconds(30))
.build()?;
// Record events - auto-saved every 30s
store.record("event");
```
Final persist on drop:
```rust
{
let store = EventStore::builder()
.with_storage(storage)
.build()?;
store.record("event");
} // Automatic persist on drop if dirty
```
## Configuration
### Custom Time Units
Track specific time ranges:
```rust
let store = EventStore::builder()
.track_seconds(120) // Last 2 minutes
.track_minutes(120) // Last 2 hours
.track_hours(48) // Last 2 days
.track_days(90) // Last 3 months
.build()?;
```
Default config (128 buckets total):
- 60 Minutes (last hour)
- 24 Hours (last day)
- 32 Days (last ~4.5 weeks)
- 12 Months (last year)
### Configuration Changes
Configuration changes are handled automatically when rebuilding from storage. No manual migration required.
```rust
// Originally 7 days
let store = EventStore::builder()
.with_storage(storage)
.track_days(7)
.build()?;
// Expand to 28 days - automatically keeps existing 7, adds 21 empty buckets
let store = EventStore::builder()
.with_storage(storage)
.track_days(28)
.build()?;
// Shrink to 3 days - automatically keeps recent 3, drops older data
let store = EventStore::builder()
.with_storage(storage)
.track_days(3)
.build()?;
```
The store detects configuration mismatches on load and adjusts bucket counts automatically:
- **Expanding**: Preserves existing data, fills new buckets with zeros
- **Shrinking**: Keeps most recent buckets, discards older data
- **Adding/removing time units**: Preserves data for existing time units, initializes new ones
## Export, Import, Merge
### Export Events
Export for backup or transfer:
```rust
// Export all events
let events = store.export_all()?; // HashMap<String, SingleEventCounter>
// Export only modified events
let dirty = store.export_dirty()?;
// Serialize with serde
let json = serde_json::to_string(&events)?;
```
### Import Events
Load events from external sources:
```rust
let events: HashMap<String, SingleEventCounter> = serde_json::from_str(&json)?;
// Replace existing events
store.import_all(events)?;
// Or import single event
store.import_event("app_launch", counter)?;
```
### Merge Events
Combine data by addition (commutative and associative):
```rust
// Merge single event
let remote_counter = fetch_from_server("app_launch");
store.merge_event("app_launch", remote_counter)?;
// Merge all events
let remote_events = fetch_all_from_server();
store.merge_all(remote_events)?;
```
### Multi-Device Sync
Synchronize across devices:
```rust
// Device 1: export and send
let device1_data = device1.export_dirty()?;
server.merge_all(device1_data)?;
// Device 2: fetch and merge
let server_data = server.export_all()?;
device2.merge_all(server_data)?;
```
Offline-first pattern:
```rust
// Record offline
store.record("purchase");
// Sync when online
let server_data = api.fetch_events().await?;
store.merge_all(server_data)?;
let local_data = store.export_dirty()?;
api.upload_events(local_data).await?;
store.reset_dirty();
```
### Distributed Systems
Aggregate data from multiple sources:
```rust
// Multiple servers record events
server1.record("request");
server2.record("request");
// Aggregate server combines all
let data1 = server1.export_all()?;
let data2 = server2.export_all()?;
aggregate.merge_all(data1)?;
aggregate.merge_all(data2)?;
// Query sees total across all servers
let total = aggregate.query("request").last_hours(1).sum();
```
## Advanced Features
### Range Queries
Query specific time ranges:
```rust
// This week (days 0-7)
let this_week = store.query("event").days(0..7).sum();
// Last week (days 7-14)
let last_week = store.query("event").days(7..14).sum();
// Skip N days, take M days
let older = store.query("event").days_from(14).take(7).sum();
```
### First Seen Timestamps
Find when events first occurred:
```rust
// Time since first occurrence
let first = store.query("user_signup").first_seen();
// First occurrence in specific time unit
let first_days = store.query("user_signup").first_seen_in(TimeUnit::Days);
```
### Balance Deltas
Zero out delta between opposing events:
```rust
store.record_count("credits", 100);
store.record_count("debits", 30);
// Delta is +70
let delta = store.query_delta("credits", "debits").ever().sum();
assert_eq!(delta, 70);
// Balance by adding 70 debits
store.balance_delta("credits", "debits")?;
// Now zero
let balanced = store.query_delta("credits", "debits").ever().sum();
assert_eq!(balanced, 0);
```
Use for operations like "remove all items" or "reset balance" where you need to reconcile tracked deltas with external state.
### Inspecting Store
Memory usage:
```rust
let bytes = store.memory_usage();
println!("Using {} KB", bytes / 1024);
```
Tracked intervals:
```rust
for (time_unit, bucket_count) in store.tracked_intervals() {
println!("{:?}: {} buckets", time_unit, bucket_count);
}
```
### Test Clock
Control time for deterministic tests:
```rust
use tiny_counter::TestClock;
use std::sync::Arc;
let clock = TestClock::build_for_testing();
let store = EventStore::builder()
.with_clock(Arc::new(clock.clone()))
.build()?;
// Record event
store.record("event");
// Advance time
clock.advance(Duration::days(1));
// Event now in yesterday's bucket
assert_eq!(store.query("event").last_days(1).sum(), Some(0));
assert_eq!(store.query("event").last_days(2).sum(), Some(1));
```
### Calendar-Aligned Buckets (Default)
By default, buckets snap to calendar boundaries in local time:
Days rotate at midnight. Weeks rotate Monday at midnight. Months rotate on the 1st. Years rotate January 1st.
**Default aligns to human time:**
```rust
let today = store.query("sales").last_days(1).sum(); // Today (midnight to now)
let this_month = store.query("sales").last_months(1).sum(); // This month (1st to now)
```
**For uniform bucket sizes (advanced use cases), disable calendar:**
```toml
[dependencies]
tiny-counter = { version = "0.1", default-features = false, features = ["storage-fs", "serde-bincode"] }
```
Uniform buckets align to January 1st of the current year at 00:00 UTC, then rotate in fixed durations. All counters of the same time unit have aligned start times.
```rust
// Without calendar: months are uniform 30-day periods
// Better for statistical analysis with consistent bucket sizes
let stats = store.query("usage").last_months(12).average();
```
**Trade-offs:**
Calendar alignment adds timezone conversion and calendar math to rotation calculations. Benchmarks show negligible overhead. Run benchmarks:
```bash
cargo bench --bench calendar
```
**Example:**
```rust
// Default (calendar-aligned): actual months
// Feb bucket = Feb 1 to Feb 28/29 (28/29 days)
// Mar bucket = Mar 1 to Mar 31 (31 days)
// Without calendar feature: uniform 30-day periods
// Feb bucket = Jan 2 to Feb 1 (30 days)
// Mar bucket = Feb 1 to Mar 3 (30 days)
```
### Custom Storage
Implement `Storage` trait for custom backends:
```rust
use tiny_counter::{Storage, Result};
struct S3Storage { /* ... */ }
impl Storage for S3Storage {
fn save(&mut self, key: &str, data: Vec<u8>) -> Result<()> {
// Upload to S3
}
fn load(&self, key: &str) -> Result<Option<Vec<u8>>> {
// Download from S3
}
fn delete(&mut self, key: &str) -> Result<()> {
// Delete from S3
}
fn list_keys(&self) -> Result<Vec<String>> {
// List all keys
}
}
let store = EventStore::builder()
.with_storage(S3Storage::new())
.build()?;
```
## API Quick Reference
### Recording
```rust
store.record("event")
store.record_count("event", 5)
store.record_at("event", timestamp)?
store.record_count_at("event", 5, timestamp)?
store.record_ago("event", duration)
store.record_count_ago("event", 5, duration)
```
### Querying
```rust
store.query("event").last_minutes(60).sum()
store.query("event").last_hours(24).sum()
store.query("event").last_days(7).sum()
store.query("event").ever().sum()
store.query("event").last_seen()
store.query("event").first_seen()
store.query("event").last_days(7).average()
store.query("event").last_days(7).count_nonzero()
store.query("event").last_days(7).buckets()
```
### Multi-Event
```rust
store.query_many(&["ev1", "ev2"]).last_days(7).sum()
store.query_ratio("num", "denom").last_days(7)
store.query_delta("pos", "neg").last_days(7).sum()
```
### Rate Limiting
```rust
store.limit()
.at_most("event", 10, TimeUnit::Minutes)
.at_least("prereq", 1, TimeUnit::Days)
.cooldown("event", Duration::hours(1))
.within("prereq", Duration::minutes(30))
.during(Schedule::hours(9, 17)?)
.check_and_record("event")?
store.limit().at_most("event", 10, TimeUnit::Minutes).allowed("event")
store.limit().at_most("event", 10, TimeUnit::Minutes).usage("event")?
store.limit().at_most("event", 10, TimeUnit::Minutes).reserve("event")?
```
### Persistence
```rust
store.persist()?
store.persist_all()?
store.is_dirty()
store.reset_dirty()
```
### Export/Import/Merge
```rust
store.export_all()?
store.export_dirty()?
store.import_all(events)?
store.import_event("event", counter)?
store.merge_all(events)?
store.merge_event("event", counter)?
store.merge(other_store)?
```