| Crates.io | tiny-counter |
| lib.rs | tiny-counter |
| version | 0.1.0 |
| created_at | 2026-01-02 17:47:38.101952+00 |
| updated_at | 2026-01-02 17:47:38.101952+00 |
| description | Track event counts across time windows with fixed memory and fast queries |
| homepage | https://github.com/jhugman/tiny-counter |
| repository | https://github.com/jhugman/tiny-counter |
| max_upload_size | |
| id | 2018942 |
| size | 844,987 |
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:
For example, recording just app launches answers:
Drive app behavior:
Event data:
Product decisions care about frequency and recency, not individual actions. Aggregate patterns matter; specific details fade over time.
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);
Good for:
Not for:
[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"] }
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.
Default config tracks 256 total buckets:
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.
// 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());
}
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
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),
}
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
}
// 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();
// Export dirty counters from device 1
let device1_data = device1_store.export_dirty()?;
// Merge into device 2
device2_store.merge_all(device1_data)?;
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 JSONTime bucket behavior:
calendar (default) - Days/weeks/months rotate at local midnightdefault-features = false for uniform 30-day months and 24-hour daysOptional features:
tokio - Auto-persist with background taskMix and match any storage with any format.
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")?;
basic, type_safety, persistencerate_limiting, analytics, multi_device, resource_tracking, security, concurrentasync_autopersist (tokio)MIT OR Apache-2.0