tokio-interval-set 0.1.1

Run a closure repeatedly on a fixed time interval using tokio
Documentation
# tokio-interval-set

[![Crates.io](https://img.shields.io/crates/v/tokio-interval-set.svg)](https://crates.io/crates/tokio-interval-set)
[![Docs.rs](https://docs.rs/tokio-interval-set/badge.svg)](https://docs.rs/tokio-interval-set)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

A lightweight Rust utility for running a closure repeatedly on a fixed time
interval, backed by [`tokio`]'s time and task system.

`tokio-interval-set` provides a simple, safe, and ergonomic API for launching
periodic background tasks — think JavaScript's `setInterval`, but for async
Rust with cancellation handled via a dedicated handle.

---

## Features

- **⏱ïļ Fixed-interval execution** — Run any async closure on a repeating
  [`tokio::time::Interval`].
- **🛑 Graceful cancellation** — Call [`clear()`] on the returned handle to
  synchronously stop the loop after the current tick completes.
- **🔄 Drop-safe** — Dropping the handle (without calling `clear()`) also
  stops the task on the next tick boundary; nothing leaks.
- **ðŸšĻ Panic detection** — If the spawned task panics, calling `clear()` will
  panic with a descriptive message so you know something went wrong.
- **ðŸŠķ Minimal** — One function, one struct, zero dependencies beyond
  [`tokio`].

---

## Quick Start

Add the crate to your `Cargo.toml`:

```toml
[dependencies]
tokio-interval-set = "0.1.0"
tokio = { version = "1", features = ["time", "sync", "rt", "macros"] }
```

Then use it in your code:

```rust
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::interval;
use tokio_interval_set::set_interval;

let rt = tokio::runtime::Builder::new_current_thread()
    .enable_time()
    .build()
    .unwrap();

rt.block_on(async {
    let count = Arc::new(Mutex::new(0u64));
    let count_cloned = Arc::clone(&count);

    let handle = set_interval(interval(Duration::from_millis(100)), move || {
        let count = Arc::clone(&count_cloned);
        async move {
            let mut c = count.lock().await;
            *c += 1;
        }
    });

    tokio::time::sleep(Duration::from_millis(350)).await;
    handle.clear(); // stop the interval

    let final_count = *count.lock().await;
    assert!(final_count >= 3, "expected at least 3 ticks, got {}", final_count);
});
```

---

## API Overview

### `set_interval(interval, func) -> IntervalHandle`

Spawns a background task that calls `func()` every time the given
[`tokio::time::Interval`] ticks.

| Parameter   | Type                                                | Description                     |
|-------------|-----------------------------------------------------|---------------------------------|
| `interval`  | [`tokio::time::Interval`]                           | Controls the tick timing.       |
| `func`      | `Fn() -> Fut + Send + Sync + 'static`               | Closure returning a future.     |

### `IntervalHandle`

The handle returned by `set_interval`. It exposes one method:

| Method                                    | Description                                                             |
|-------------------------------------------|-------------------------------------------------------------------------|
| [`clear(self)`][`clear()`]                | Synchronously stops the interval loop. Panics if the task has panicked. |

If the handle is **dropped** without calling `clear()`, the interval task is
cancelled on the next tick boundary.

---

## Examples

### Stopping via `clear()`

```rust
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::interval;
use tokio_interval_set::set_interval;

let rt = tokio::runtime::Builder::new_current_thread()
    .enable_time()
    .build()
    .unwrap();

rt.block_on(async {
    let count = Arc::new(Mutex::new(0u64));
    let count_clone = Arc::clone(&count);
    let handle = set_interval(
        interval(Duration::from_secs(1)),
        move || {
            let count = Arc::clone(&count_clone);
            async move {
                let mut c = count.lock().await;
                *c += 1;
            }
        },
    );

    tokio::time::sleep(Duration::from_secs(3)).await;
    handle.clear(); // ✅ clean shutdown
    assert_eq!(*count.lock().await, 3);
});
```

### Dropping the handle (automatic cancellation)

```rust
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::interval;
use tokio_interval_set::set_interval;

let rt = tokio::runtime::Builder::new_current_thread()
    .enable_time()
    .build()
    .unwrap();

rt.block_on(async {
    let values = Arc::new(Mutex::new(Vec::new()));
    let values_clone = Arc::clone(&values);

    let handle = set_interval(
        interval(Duration::from_millis(100)),
        move || {
            let values = Arc::clone(&values_clone);
            async move {
                let mut v = values.lock().await;
                v.push(v.len());
            }
        },
    );

    tokio::time::sleep(Duration::from_millis(250)).await;
    drop(handle); // cancels on the next tick
    // ... or just let `handle` go out of scope
});
```

---

## How It Works

1. [`set_interval`] creates a [`tokio::sync::oneshot`] channel and spawns a
   background task via [`tokio::spawn`].
2. The task loops, using [`tokio::select!`] to wait on both the [`Interval`]
   tick and a shutdown signal from the oneshot receiver.
3. On each tick, the closure is called and awaited.
4. When [`clear()`] is invoked (or the handle is dropped), the oneshot sender
   is consumed / dropped, the receiver wakes up, and the loop exits.

This approach ensures that:
- The interval task runs **concurrently** with your other async work.
- The current tick's closure completes before the loop exits.
- No busy-waiting or manual thread management.

---

## Safety & Correctness

- **No `unsafe` code** — the crate is written entirely in safe Rust.
- **Cancellation is clean** — the shutdown signal is sent via a oneshot
  channel; the loop exits on the next iteration.
- **Panic propagation** — if the closure panics, the spawned task panics and
  the oneshot receiver is dropped. Calling `clear()` on a panicked task
  produces a clear panic message (`"Interval was panic"`), so the error is
  never silently swallowed.

---

## License

This project is licensed under the [MIT License](LICENSE).

[`tokio`]: https://tokio.rs
[`tokio::time::Interval`]: https://docs.rs/tokio/latest/tokio/time/struct.Interval.html
[`tokio::spawn`]: https://docs.rs/tokio/latest/tokio/fn.spawn.html
[`tokio::select!`]: https://docs.rs/tokio/latest/tokio/macro.select.html
[`tokio::sync::oneshot`]: https://docs.rs/tokio/latest/tokio/sync/oneshot/index.html
[`set_interval`]: https://docs.rs/tokio-interval-set/latest/tokio_interval_set/fn.set_interval.html
[`IntervalHandle`]: https://docs.rs/tokio-interval-set/latest/tokio_interval_set/struct.IntervalHandle.html
[`clear()`]: https://docs.rs/tokio-interval-set/latest/tokio_interval_set/struct.IntervalHandle.html#method.clear