1 unstable release
| 0.1.0 | Jan 2, 2026 |
|---|
#393 in Algorithms
465KB
9K
SLoC
tiny-counter
Record app usage to drive on-device decisions in mobile and desktop apps.
Count events as they happen. Query aggregates later when you know what questions matter. Details fade automatically—only frequency and recency remain.
Answer questions about events per time:
- How many events in the previous N time units?
- How many time units had one or more events?
- When did an event last occur, or first occur?
For example, recording just app launches answers:
- Has the user used the app in 15 of the last 28 days?
- Has the app launched today? Was it yesterday?
- How often does the user visit settings?
- When did the user last launch the app?
- Has usage increased this week vs last week?
Drive app behavior:
- Show tooltip? (user saved 3 passwords this week → suggest Sync setup)
- Run recovery migration? (user saw error recently)
- Rate limit API? (spread usage across sessions for good citizenship)
Event data:
- Stays on device for privacy
- No exact timestamps stored—only counts per time period
- Details automatically fade as buckets rotate
- Uses 1KB per event (100 events = 100KB)
- Thread safe
Product decisions care about frequency and recency, not individual actions. Aggregate patterns matter; specific details fade over time.
Core Example
use tiny_counter::EventStore;
let store = EventStore::new();
store.record("app_launch");
store.record("app_launch");
let launches = store.query("app_launch").last_days(7).sum().unwrap_or(0);
assert_eq!(launches, 2);
When To Use It
Good for:
- On-device usage analytics
- Feature adoption metrics
- Rate limiting APIs and user actions
- System monitoring (error rates, throughput)
Not for:
- Audit logs (events fall off after tracking window)
- Exact timestamps (only counts per time bucket)
- Unbounded history (fixed-size storage)
Installation
[dependencies]
tiny-counter = "0.1"
# With optional features
tiny-counter = { version = "0.1", features = ["storage-sqlite", "serde-json", "tokio"] }
# For uniform 30-day months instead of calendar months
tiny-counter = { version = "0.1", default-features = false, features = ["storage-fs", "serde-bincode"] }
How It Works
Rotating buckets: Events drop into time buckets. Bucket 0 is "today" (since midnight) or "this hour" (since :00). Bucket 1 is "yesterday" or "last hour." Time advances → buckets rotate → old data falls off.
Multiple time units: One record() updates all time units (minutes, hours, days, months). Query any scale without reprocessing.
Fixed memory: Each event type uses ~1KB (default: 256 buckets × 4 bytes). 200 events = 200KB.
Tradeoff: Trade precision for memory. You get "10 events in last hour" but not exact timestamps. Events older than the tracking window drop silently.
Configuration
Default config tracks 256 total buckets:
- 60 Minutes (last hour)
- 72 Hours (last 3 days)
- 56 Days (last ~8 weeks)
- 52 Weeks (last year)
- 12 Months (last year)
- 4 Years
Customize with builder:
use tiny_counter::EventStore;
let store = EventStore::builder()
.track_hours(24)
.track_days(28)
.track_weeks(26)
.track_years(2)
.build()
.unwrap();
Configuration changes are handled automatically—change bucket counts anytime and existing data adapts on load.
Quick Examples
Recording and Querying
// Record events
store.record("app_launch");
store.record_count("api_call", 5);
// Query time windows
let last_hour = store.query("api_call").last_hours(1).sum().unwrap_or(0);
let last_week = store.query("app_launch").last_days(7).sum().unwrap_or(0);
// Count active periods
let active_days = store.query("app_launch").last_days(28).count_nonzero().unwrap_or(0);
// When did event last occur?
if let Some(duration) = store.query("error:sync").last_seen() {
println!("Last error {} minutes ago", duration.num_minutes());
}
Persistence
use tiny_counter::{EventStore, storage::Sqlite};
let store = EventStore::builder()
.with_storage(Sqlite::open("events.db")?)
.build()?;
store.record("page_view");
store.persist()?; // Save to disk
Rate Limiting
use tiny_counter::TimeUnit;
// Enforce multiple limits
match store.limit()
.at_most("api_call", 10, TimeUnit::Minutes)
.at_most("api_call", 100, TimeUnit::Hours)
.check_and_record("api_call")
{
Ok(_) => println!("Request allowed"),
Err(e) => println!("Rate limited: {}", e),
}
Transactional Reservations
Prevent race conditions with atomic check-and-reserve:
let reservation = store.limit()
.at_most("payment", 1, TimeUnit::Minutes)
.reserve("payment")?;
match process_payment() {
Ok(_) => reservation.commit(), // Count it
Err(_) => reservation.cancel(), // Release slot
}
Comparing Events
// Conversion rate
let conversion = store.query_ratio("purchases", "visits").last_days(7);
// Net change (inventory, balance, connections)
let balance = store.query_delta("deposits", "withdrawals").ever().sum();
Multi-Device Sync
// Export dirty counters from device 1
let device1_data = device1_store.export_dirty()?;
// Merge into device 2
device2_store.merge_all(device1_data)?;
Features
Storage backends:
storage-fs- File-per-event (default)storage-sqlite- SQLite database (all events in one DB)MemoryStorage- No persistence (testing)
Serialization formats:
serde-bincode- Compact binary (default)serde-json- Human-readable JSON
Time bucket behavior:
calendar(default) - Days/weeks/months rotate at local midnight- Disable with
default-features = falsefor uniform 30-day months and 24-hour days
Optional features:
tokio- Auto-persist with background task
Mix and match any storage with any format.
Rate Limiting
Constraint types:
at_most- Maximum N per window (typical rate limit)at_least- Minimum N required (prerequisite check)cooldown- Minimum time between eventswithin- Event must have occurred recentlyduring/outside_of- Schedule-based (business hours, weekdays)
Combine constraints:
use std::time::Duration;
use tiny_counter::Schedule;
store.limit()
.at_most("api", 100, TimeUnit::Hours)
.at_least("login", 1, TimeUnit::Days) // Must be logged in
.cooldown("reset", Duration::minutes(5)) // Wait between resets
.during(Schedule::hours(9, 17)) // Business hours only
.check_and_record("api")?;
Documentation
- REFERENCE.md - Complete API guide with progressive disclosure
- COOKBOOK.md - Advanced patterns combining features
- DESIGN.md - Architecture for contributors
- Examples - Runnable code:
- Core:
basic,type_safety,persistence - Use cases:
rate_limiting,analytics,multi_device,resource_tracking,security,concurrent - Features:
async_autopersist(tokio)
- Core:
License
MIT OR Apache-2.0
Dependencies
~2.2–7.5MB
~130K SLoC