tiny-counter 0.1.0

Track event counts across time windows with fixed memory and fast queries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
# Cookbook

Advanced recipes combining multiple features. Use these patterns to solve real problems in your app.

## API Rate Limiting

### Burst Protection with Sustained Limits

Allow short bursts but enforce sustained usage limits:

```rust
use tiny_counter::{EventStore, TimeUnit};

let store = EventStore::new();

// Allow 10/min bursts, but only 100/hour sustained
let result = store
    .limit()
    .at_most("api_call", 10, TimeUnit::Minutes)
    .at_most("api_call", 100, TimeUnit::Hours)
    .check_and_record("api_call");

match result {
    Ok(()) => make_api_call(),
    Err(e) => {
        let wait = e.retry_after.unwrap();
        return_error!("Rate limited. Retry in {}s", wait.num_seconds());
    }
}
```

This prevents both burst abuse (10 calls in one second) and sustained overuse (1000 calls over an hour by spacing them 6 seconds apart).

### Tiered Access by User Activity

Increase limits for active users:

```rust
// Free tier: 10/hour
// Pro tier requires 100 logins this month
let user_tier = if store.query("user:login").last_months(1).sum().unwrap_or(0) >= 100 {
    100  // Pro user gets 100/hour
} else {
    10   // Free tier gets 10/hour
};

store
    .limit()
    .at_most("user:api_call", user_tier, TimeUnit::Hours)
    .check_and_record("user:api_call")?;
```

### Progressive Backoff

Increase cooldown based on recent failures:

```rust
let recent_failures = store.query("login:failed").last_minutes(15).sum().unwrap_or(0);

let cooldown = match recent_failures {
    0..=2 => Duration::seconds(0),      // No cooldown
    3..=5 => Duration::seconds(30),     // 30s after 3 failures
    6..=10 => Duration::minutes(5),     // 5m after 6 failures
    _ => Duration::minutes(30),         // 30m after 10+ failures
};

if cooldown > Duration::zero() {
    store
        .limit()
        .cooldown("login:attempt", cooldown)
        .check_and_record("login:attempt")?;
}
```

### Transactional API Calls

Reserve slots before making expensive calls:

```rust
// Reserve slot before work
let reservation = store
    .limit()
    .at_most("api_call", 10, TimeUnit::Minutes)
    .reserve("api_call")?;

// Make API call
match expensive_api_call().await {
    Ok(result) => {
        reservation.commit();  // Count this call
        Ok(result)
    }
    Err(e) => {
        // Auto-cancels on drop if not committed
        Err(e)
    }
}
```

Prevents counting failed or cancelled requests.

## User Analytics

### Daily Active Users (DAU)

Count unique active days:

```rust
let dau = store.query("user:login").last_days(1).count_nonzero().unwrap_or(0);
let wau = store.query("user:login").last_days(7).count_nonzero().unwrap_or(0);
let mau = store.query("user:login").last_days(30).count_nonzero().unwrap_or(0);

println!("DAU: {}, WAU: {}, MAU: {}", dau, wau, mau);
```

Note: This counts "days with activity" not "unique users". For per-user tracking, use separate event IDs:

```rust
// Per-user tracking
store.record(&format!("user:{}:login", user_id));

// Query per user
let user_dau = store
    .query(&format!("user:{}:login", user_id))
    .last_days(1)
    .count_nonzero()
    .unwrap_or(0);
```

### Engagement Scoring

Score users by engagement patterns:

```rust
let app_launches = store.query(&format!("user:{}:launch", user_id))
    .last_days(30).sum().unwrap_or(0);

let feature_uses = store.query(&format!("user:{}:feature_use", user_id))
    .last_days(30).sum().unwrap_or(0);

let active_days = store.query(&format!("user:{}:launch", user_id))
    .last_days(30).count_nonzero().unwrap_or(0);

let score = (app_launches * 1) + (feature_uses * 5) + (active_days * 10);

let segment = match score {
    0..=50 => "dormant",
    51..=200 => "casual",
    201..=500 => "regular",
    _ => "power_user",
};
```

### Conversion Funnels

Track multi-step conversions:

```rust
// Record funnel steps
store.record(&format!("user:{}:landing", user_id));
store.record(&format!("user:{}:signup_started", user_id));
store.record(&format!("user:{}:signup_completed", user_id));

// Calculate conversion rates
let landings = store.query(&format!("user:{}:landing", user_id))
    .last_days(7).sum().unwrap_or(1);

let signups = store.query(&format!("user:{}:signup_completed", user_id))
    .last_days(7).sum().unwrap_or(0);

let conversion_rate = signups as f64 / landings as f64;
println!("Conversion: {:.2}%", conversion_rate * 100.0);
```

### Cohort Retention

Track user retention by signup cohort:

```rust
// Day 1: User signs up
store.record(&format!("cohort:2025-01:signup", user_id));

// Day 7: User returns
if store.query(&format!("cohort:2025-01:signup", user_id)).ever().sum().unwrap_or(0) > 0 {
    store.record(&format!("cohort:2025-01:day7", user_id));
}

// Calculate retention
let signups = store.query("cohort:2025-01:signup").ever().count_nonzero().unwrap_or(1);
let day7_returns = store.query("cohort:2025-01:day7").ever().count_nonzero().unwrap_or(0);

let retention = day7_returns as f64 / signups as f64;
println!("Day 7 retention: {:.2}%", retention * 100.0);
```

## Multi-Device Sync

### Offline-First Mobile App

Record events offline, sync when connected:

```rust
// Offline recording
store.record("user:purchase");
store.record("user:page_view");

// Check for pending changes
if store.is_dirty() && is_online() {
    // Upload local changes
    let local_data = store.export_dirty()?;
    api.upload_events(user_id, local_data).await?;

    // Download remote changes
    let remote_data = api.fetch_events(user_id).await?;
    store.merge_all(remote_data)?;

    // Clear dirty flag after successful sync
    store.reset_dirty();
}
```

### Conflict-Free Sync

Merge works with concurrent updates across devices:

```rust
// Device 1 records 5 events
device1.record_count("page_view", 5);
let dev1_data = device1.export_all()?;

// Device 2 records 3 events
device2.record_count("page_view", 3);
let dev2_data = device2.export_all()?;

// Server merges both (5 + 3 = 8)
server.merge_all(dev1_data)?;
server.merge_all(dev2_data)?;

assert_eq!(server.query("page_view").ever().sum(), Some(8));

// Both devices sync and see total
device1.merge_all(server.export_all()?)?;
device2.merge_all(server.export_all()?)?;

assert_eq!(device1.query("page_view").ever().sum(), Some(8));
assert_eq!(device2.query("page_view").ever().sum(), Some(8));
```

### Partial Sync

Sync only specific events:

```rust
// Export only analytics events
let analytics_events = store.export_all()?
    .into_iter()
    .filter(|(id, _)| id.starts_with("analytics:"))
    .collect();

api.upload_analytics(analytics_events).await?;
```

## Resource Management

### Connection Pool Tracking

Track open/closed connections:

```rust
// Connection opened
store.record("db:connection:open");

// Connection closed
store.record("db:connection:closed");

// Current open connections
let open = store.query("db:connection:open").ever().sum().unwrap_or(0);
let closed = store.query("db:connection:closed").ever().sum().unwrap_or(0);
let current = (open as i64 - closed as i64).max(0);

println!("Open connections: {}", current);

// Or use delta query
let current = store
    .query_delta("db:connection:open", "db:connection:closed")
    .ever()
    .sum();

println!("Open connections: {}", current.max(0));
```

### Capacity Limits

Enforce capacity based on current state:

```rust
let open = store.query_delta("db:open", "db:closed").ever().sum();

if open >= 100 {
    return Err("Connection pool exhausted");
}

store.record("db:open");
// ... use connection ...
store.record("db:closed");
```

### Auto-Reset at Midnight

Balance connections at day boundary:

```rust
use chrono::Utc;

// Check if last reset was yesterday
let last_reset = load_last_reset_time();
let now = Utc::now();

if now.date() != last_reset.date() {
    // New day - reset connection counts
    store.balance_delta("db:open", "db:closed")?;
    save_last_reset_time(now);
}
```

## Feature Flags

### Progressive Rollout

Enable features for active users first:

```rust
let launches = store
    .query(&format!("user:{}:launch", user_id))
    .last_days(30)
    .sum()
    .unwrap_or(0);

let new_feature_enabled = launches >= 10;  // Active users only

if new_feature_enabled {
    store.record(&format!("user:{}:new_feature_used", user_id));
}
```

### A/B Testing

Track feature variants and measure outcomes:

```rust
// Assign user to variant
let variant = match user_id % 2 {
    0 => "control",
    _ => "treatment",
};

store.record(&format!("ab_test:variant:{}", variant));

// Track conversions per variant
store.record(&format!("ab_test:{}:conversion", variant));

// Calculate results
let control_conversions = store
    .query("ab_test:control:conversion")
    .last_days(7)
    .sum()
    .unwrap_or(0);

let treatment_conversions = store
    .query("ab_test:treatment:conversion")
    .last_days(7)
    .sum()
    .unwrap_or(0);

let control_users = store
    .query("ab_test:variant:control")
    .last_days(7)
    .sum()
    .unwrap_or(1);

let treatment_users = store
    .query("ab_test:variant:treatment")
    .last_days(7)
    .sum()
    .unwrap_or(1);

let control_rate = control_conversions as f64 / control_users as f64;
let treatment_rate = treatment_conversions as f64 / treatment_users as f64;
let lift = ((treatment_rate - control_rate) / control_rate) * 100.0;

println!("Control: {:.2}%, Treatment: {:.2}%, Lift: {:.2}%",
    control_rate * 100.0, treatment_rate * 100.0, lift);
```

## Security Patterns

### Brute Force Protection

Exponential backoff with time windows:

```rust
let attempts = store.query("login:failed").last_hours(1).sum().unwrap_or(0);

store
    .limit()
    .at_most("login:attempt", 5, TimeUnit::Minutes)
    .at_most("login:attempt", 20, TimeUnit::Hours)
    .cooldown("login:failed", Duration::seconds(2u64.pow(attempts.min(10) as u32)))
    .check_and_record("login:attempt")?;

match authenticate(username, password) {
    Ok(user) => {
        store.record("login:success");
        Ok(user)
    }
    Err(e) => {
        store.record("login:failed");
        Err(e)
    }
}
```

### Anomaly Detection

Detect unusual activity patterns:

```rust
// Baseline: average activity over 30 days
let baseline = store
    .query("user:api_calls")
    .last_days(30)
    .average_nonzero()
    .unwrap_or(10.0);

// Current: activity in last hour
let current = store
    .query("user:api_calls")
    .last_hours(1)
    .sum()
    .unwrap_or(0) as f64;

// Alert if current > 5x baseline
if current > baseline * 5.0 {
    store.record("security:anomaly_detected");
    send_alert("Unusual activity detected");
}
```

### Session Management

Track concurrent sessions with auto-cleanup:

```rust
// Session start
store.record(&format!("user:{}:session:start", user_id));

// Active sessions (started in last 24 hours)
let active_sessions = store
    .query(&format!("user:{}:session:start", user_id))
    .last_hours(24)
    .sum()
    .unwrap_or(0);

// Limit concurrent sessions
if active_sessions >= 3 {
    return Err("Too many active sessions. Close other sessions.");
}
```

## Testing Patterns

### Time-Based Test Scenarios

Test time-dependent behavior deterministically:

```rust
use tiny_counter::TestClock;

#[test]
fn test_weekly_limit() {
    let clock = TestClock::build_for_testing();
    let store = EventStore::builder()
        .with_clock(Arc::new(clock.clone()))
        .build()
        .unwrap();

    // Day 1: Use 5 calls
    store.record_count("api", 5);
    assert!(store.limit().at_most("api", 10, TimeUnit::Weeks).allowed("api"));

    // Day 7: Use 4 more calls (total 9)
    clock.advance(Duration::days(6));
    store.record_count("api", 4);
    assert!(store.limit().at_most("api", 10, TimeUnit::Weeks).allowed("api"));

    // Try 11th call - should fail
    assert!(!store.limit().at_most("api", 10, TimeUnit::Weeks).allowed("api"));

    // Day 8: First call drops off, have room again
    clock.advance(Duration::days(1));
    assert!(store.limit().at_most("api", 10, TimeUnit::Weeks).allowed("api"));
}
```

### Concurrent Access Testing

Test thread safety:

```rust
use std::sync::Arc;
use std::thread;

#[test]
fn test_concurrent_rate_limiting() {
    let store = Arc::new(EventStore::new());

    // Spawn 100 threads trying to reserve 10 slots
    let handles: Vec<_> = (0..100)
        .map(|_| {
            let store = store.clone();
            thread::spawn(move || {
                store
                    .limit()
                    .at_most("api", 10, TimeUnit::Hours)
                    .reserve("api")
            })
        })
        .collect();

    let results: Vec<_> = handles
        .into_iter()
        .map(|h| h.join().unwrap())
        .collect();

    // Exactly 10 should succeed
    let successes = results.iter().filter(|r| r.is_ok()).count();
    assert_eq!(successes, 10);
}
```

## Performance Optimization

### Batch Recording

Reduce lock contention by batching:

```rust
// Instead of N separate records
for event in events {
    store.record(&event.name);  // N locks
}

// Batch count the events
let mut counts: HashMap<String, u64> = HashMap::new();
for event in events {
    *counts.entry(event.name.clone()).or_insert(0) += 1;
}

for (event_name, count) in counts {
    store.record_count(&event_name, count);  // M locks (M < N)
}
```

### Selective Persistence

Persist only high-value events:

```rust
// Record everything in memory
store.record("low_value_event");
store.record("critical_event");

// Persist only critical events
if event_is_critical {
    store.persist()?;
}

// Or persist on schedule
if last_persist.elapsed() > Duration::minutes(5) {
    store.persist()?;
}
```

### Memory-Constrained Environments

Configure minimal tracking:

```rust
let store = EventStore::builder()
    .track_minutes(60)   // Only track 1 hour
    .track_hours(24)     // Only track 1 day
    .build()?;

// Each event uses ~1KB instead of ~2KB
```

## Custom Patterns

### Sliding Window Average

Calculate rolling averages:

```rust
// Last 7 days activity
let last_week = store.query("api_call").last_days(7).sum().unwrap_or(0);
let avg_per_day = last_week as f64 / 7.0;

// Compare to previous 7 days
let prev_week = store.query("api_call").days(7..14).sum().unwrap_or(0);
let prev_avg = prev_week as f64 / 7.0;

let growth = ((avg_per_day - prev_avg) / prev_avg) * 100.0;
println!("Week-over-week growth: {:.1}%", growth);
```

### Threshold Alerts

Trigger actions when thresholds crossed:

```rust
let current_hour = store.query("errors").last_hours(1).sum().unwrap_or(0);

if current_hour > 100 {
    store.record("alert:error_threshold_exceeded");
    send_alert("Error rate exceeded 100/hour");
}

// Don't spam alerts
let recent_alerts = store.query("alert:error_threshold_exceeded")
    .last_hours(1)
    .sum()
    .unwrap_or(0);

if recent_alerts == 0 {
    send_alert("Error spike detected");
}
```

### Event Correlation

Find patterns across events:

```rust
// Track event sequences
let checkouts = store.query("checkout").last_days(7).sum().unwrap_or(0);
let cart_updates = store.query("cart_update").last_days(7).sum().unwrap_or(0);
let abandoned = cart_updates.saturating_sub(checkouts);

println!("Cart abandonment: {} of {} carts", abandoned, cart_updates);

// Conversion window analysis
let recent_carts = store.query("cart_update").last_hours(1).sum().unwrap_or(0);
let recent_checkouts = store.query("checkout").last_hours(1).sum().unwrap_or(0);

if recent_carts > 0 && recent_checkouts == 0 {
    store.record("pattern:high_cart_low_checkout");
}
```

## Uniform Buckets for Backend Analytics

Disable calendar alignment for consistent bucket sizes in statistical analysis. **Do not use for user-facing queries.**

### When to Use Uniform Buckets

Use uniform buckets when you need:
- Consistent 30-day periods for comparing months
- Statistical analysis across uniform time windows
- Backend metrics where human time alignment doesn't matter

```toml
[dependencies]
tiny-counter = { version = "0.1", default-features = false, features = ["storage-fs", "serde-bincode"] }
```

### Why Not for User-Facing Queries

Uniform buckets have sharp edges:

**Weeks start on Jan 1st**, not Monday:
- If Jan 1st 2025 is Wednesday, Week 1 = Wed-Tue, Week 2 = Wed-Tue
- Users expect Monday-Sunday weeks

**Months are 30-day periods**, not calendar months:
- Month 1 = Jan 1-30, Month 2 = Jan 31 - Mar 1
- "Last month" means nothing to users

**Days use UTC midnight**, not local time:
```rust
// User in PST (UTC-8) queries "today" at 3pm local time
let today = store.query("app_launch").last_days(1).sum();

// Gets: midnight UTC yesterday to midnight UTC today
// Which is: 4pm yesterday (local) to 4pm today (local)
// Half yesterday, half today - wrong!
```

### Example: Statistical Analysis

Uniform buckets work well for backend metrics:

```rust
// Compare consistent 30-day windows
let this_period = store.query("api_calls").last_months(1).sum().unwrap_or(0);
let last_period = store.query("api_calls").months(1..2).sum().unwrap_or(0);

let growth = ((this_period as f64 - last_period as f64) / last_period as f64) * 100.0;
println!("30-day growth: {:.1}%", growth);

// Each "month" is exactly 30 days - consistent for comparison
```

Use calendar alignment (default) for user-facing queries like "today", "this week", "this month".

## Configuration Migration

Change tracking configuration after deployment. The store converts existing data automatically when loaded from storage.

### Adding More Buckets

Add more buckets to see finer detail in recent history:

```rust
// Old configuration: 30 minutes
let old_store = EventStore::builder()
    .track_minutes(30)
    .with_storage(storage)
    .build()?;

// Record events...
old_store.record("api_call");
old_store.persist()?;

// New configuration: 60 minutes
let new_store = EventStore::builder()
    .track_minutes(60)
    .with_storage(storage)
    .build()?;

// Data loads and converts automatically
// Old events appear in new buckets at estimated times
let count = new_store.query("api_call").last_minutes(60).sum();
```

All events transfer to the new configuration. Events recorded 25 minutes ago appear in bucket 25 of the new 60-bucket array.

### Adding New Time Units

Add time units for longer-term tracking:

```rust
// Old: minutes only
let old_store = EventStore::builder()
    .track_minutes(60)
    .with_storage(storage)
    .build()?;

// New: minutes + hours
let new_store = EventStore::builder()
    .track_minutes(60)
    .track_hours(24)
    .with_storage(storage)
    .build()?;

// All minute buckets transfer
// Hour buckets populate with same events
```

Events appear in both time units. An event recorded 30 minutes ago appears in minute bucket 30 and hour bucket 0.

### Removing Time Units

Remove time units to reduce memory usage:

```rust
// Old: minutes + hours + days
let old_store = EventStore::builder()
    .track_minutes(60)
    .track_hours(24)
    .track_days(7)
    .with_storage(storage)
    .build()?;

// New: hours + days only
let new_store = EventStore::builder()
    .track_hours(24)
    .track_days(7)
    .with_storage(storage)
    .build()?;

// Events transfer to remaining time units
// Minute-level detail converts to hour buckets
```

Events stay in coarser time units. Precision within the hour is lost—you know events happened this hour but not which minute.

### Reducing Bucket Count

Shrink tracking window to free memory:

```rust
// Old: track 90 days
let old_store = EventStore::builder()
    .track_days(90)
    .with_storage(storage)
    .build()?;

// New: track 30 days
let new_store = EventStore::builder()
    .track_days(30)
    .with_storage(storage)
    .build()?;

// Events from last 30 days transfer
// Events older than 30 days drop
```

**Events older than the new window disappear.** If you had 60 days of data and switch to 30 days, events from days 31-60 vanish permanently.

### What Happens During Conversion

The store converts data when loading from storage. Each event's buckets transfer to the new configuration:

1. Read event from storage
2. Compare stored configuration to current configuration
3. If different, convert bucket arrays
4. Transfer events to new buckets at estimated times

Timestamp estimation uses bucket midpoints. An event in bucket 5 of a 60-minute array happened approximately 55 minutes ago (5 × 60 seconds + 30 seconds).

Events in gaps between time units (like 45-minute tracking + 24-hour tracking) have less precise timestamps. The converter detects these by comparing expected counts in overlapping buckets and estimates their time using the gap midpoint.

### Precision Loss Patterns

**Lose precision when:**

Reducing buckets:
```rust
// 120 minutes → 60 minutes
// Events 61-120 minutes ago drop
```

Creating gaps:
```rust
// 60 minutes + 24 hours → 45 minutes + 24 hours
// Events 46-60 minutes ago lose per-minute precision
```

Removing fine-grained units:
```rust
// Minutes + hours → hours only
// Events lose per-minute precision
```

**Keep precision when:**

Adding buckets:
```rust
// 60 minutes → 120 minutes
// All events transfer, more detail for 61-120 minutes ago
```

Filling gaps:
```rust
// 45 minutes + 24 hours → 60 minutes + 24 hours
// Gap events (46-60 min ago) gain minute precision
```

Adding time units:
```rust
// Days only → days + weeks
// All daily data transfers, weeks populate from same data
```

### Migration Strategy

Test configuration changes before deploying:

```rust
#[test]
fn test_config_migration() {
    let storage = MemoryStorage::new();

    // Old config: 30 days
    let old_store = EventStore::builder()
        .track_days(30)
        .with_storage(storage.clone())
        .build()?;

    old_store.record("event");
    old_store.persist()?;
    drop(old_store);

    // New config: 60 days
    let new_store = EventStore::builder()
        .track_days(60)
        .with_storage(storage)
        .build()?;

    // Verify event transferred
    assert_eq!(new_store.query("event").last_days(60).sum(), Some(1));
}
```

Calculate memory impact:
```rust
// Old: 60 minutes + 24 hours = 84 buckets
// New: 60 minutes + 24 hours + 30 days = 114 buckets
// Increase: 36% more memory per event
```

Avoid shrinking windows in production. If you must shrink, export historical data first:

```rust
// Before shrinking from 90 days to 30 days
let historical = store.export_all()?;
save_to_archive(historical)?;

// Now safe to deploy with smaller window
```