package gs.timing.v0 import cats.effect.Ref import cats.effect.Sync import cats.syntax.all.* /** Provider of monotonic time - a monotonic clock is a clock that will never * adjust or jump forwards or backwards, and represents the amount of time * elapsed since some arbitrary point in time in the past. * * This is useful for calculating _elapsed time_ -- the exact duration elapsed * between two relative events. This is _not useful_ for calculating anything * related to real dates and times. * * ## System Provider * * For most real use cases, the _system_ provider is appropriate. This * delegates to `System.nanoTime()` under the covers, which leverages the * monotonic clock of the system where the application is running. * * {{{ * import cats.effect.IO * import gs.timing.v0.MonotonicProvider * * val provider = MonotonicProvider.system[IO] * * val time: IO[Long] = provider.monotonic() * }}} * * ## Manual Provider * * For testing purposes, a manual provider is supported. This provider allows * the user to manually control a tick count and assume the role of the clock. * This allows for deterministic clock values. * * {{{ * import cats.effect.IO * import gs.timing.v0.MonotonicProvider * * for * provider <- MonotonicProvider.manual[IO] * t1 <- provider.monotonic() // 0 * _ <- provider.tick() * t2 <- provider.monotonic() // 1 * _ <- provider.set(10) * t3 <- provider.monotonic() // 10 * _ <- provider.reset() * t4 <- provider.monotonic() // 0 * yield * () * }}} */ trait MonotonicProvider[F[_]]: /** @return * The current value of the underlying monotonic clock. */ def monotonic(): F[Long] object MonotonicProvider: /** @return * A new provider based on the system's underlying monotonic clock. This * implementation delegates to `System.nanoTime()`. */ def system[F[_]: Sync]: SystemProvider[F] = new SystemProvider[F] /** @return * A new provider, always initialized to 0, that is manually controlled by * the user. */ def manual[F[_]: Sync]: F[ManualTickProvider[F]] = ManualTickProvider.initialize[F] final class SystemProvider[F[_]: Sync] extends MonotonicProvider[F]: override def monotonic(): F[Long] = Sync[F].delay(System.nanoTime()) /** Manual implementation of the [[MonotonicProvider]] that allows the caller * to directly increment/set a tick count, where each tick corresponds to one * nanosecond of elapsed time. * * Use `ManualTickProvider.initialize[F]` to instantiate this class: * * {{{ * val program: IO[ManualTickProvider[IO]] = * ManualTickProvider.initialize[IO] * }}} */ final class ManualTickProvider[F[_]] private ( ticks: Ref[F, Long] ) extends MonotonicProvider[F]: /** @return * The current tick count. */ override def monotonic(): F[Long] = ticks.get /** Increment the tick count by 1. */ def tick(): F[Unit] = ticks.update(_ + 1) /** Set the tick count to a specific value. * * @param newTickCount * The new tick count value. * @return * Side-effect. */ def set(newTickCount: Long): F[Unit] = ticks.set(newTickCount) /** Reset the tick count to 0. */ def reset(): F[Unit] = ticks.set(0) object ManualTickProvider: def initialize[F[_]: Sync]: F[ManualTickProvider[F]] = Ref.of[F, Long](0).map(ticks => new ManualTickProvider[F](ticks)) end ManualTickProvider end MonotonicProvider