All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zio.Schedule.scala Maven / Gradle / Ivy

There is a newer version: 2.1.16
Show newest version
/*
 * Copyright 2018-2024 John A. De Goes and the ZIO Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package zio

import zio.stacktracer.TracingImplicits.disableAutoTrace

import java.time.{Instant, OffsetDateTime}
import java.time.temporal.ChronoField._
import java.time.temporal.TemporalAdjusters
import java.time.{Instant, OffsetDateTime}
import scala.annotation.tailrec

/**
 * A `Schedule[Env, In, Out]` defines a recurring schedule, which consumes
 * values of type `In`, and which returns values of type `Out`.
 *
 * Schedules are defined as a possibly infinite set of intervals spread out over
 * time. Each interval defines a window in which recurrence is possible.
 *
 * When schedules are used to repeat or retry effects, the starting boundary of
 * each interval produced by a schedule is used as the moment when the effect
 * will be executed again.
 *
 * Schedules compose in the following primary ways:
 *
 * * Union. This performs the union of the intervals of two schedules. *
 * Intersection. This performs the intersection of the intervals of two
 * schedules. * Sequence. This concatenates the intervals of one schedule onto
 * another.
 *
 * In addition, schedule inputs and outputs can be transformed, filtered (to
 * terminate a schedule early in response to some input or output), and so
 * forth.
 *
 * A variety of other operators exist for transforming and combining schedules,
 * and the companion object for `Schedule` contains all common types of
 * schedules, both for performing retrying, as well as performing repetition.
 */
trait Schedule[-Env, -In, +Out] extends Serializable { self =>
  import Schedule.Decision._
  import Schedule._

  type State

  def initial: State

  def step(now: OffsetDateTime, in: In, state: State)(implicit
    trace: Trace
  ): ZIO[Env, Nothing, (State, Out, Decision)]

  /**
   * Returns a new schedule that performs a geometric intersection on the
   * intervals defined by both schedules.
   */
  final def &&[Env1 <: Env, In1 <: In, Out2](that: Schedule[Env1, In1, Out2])(implicit
    zippable: Zippable[Out, Out2]
  ): Schedule.WithState[(self.State, that.State), Env1, In1, zippable.Out] =
    (self intersectWith that)(_ intersect _)

  /**
   * Returns a new schedule that has both the inputs and outputs of this and the
   * specified schedule.
   */
  final def ***[Env1 <: Env, In2, Out2](
    that: Schedule[Env1, In2, Out2]
  ): Schedule.WithState[(self.State, that.State), Env1, (In, In2), (Out, Out2)] =
    new Schedule[Env1, (In, In2), (Out, Out2)] {
      override type State = (self.State, that.State)
      override final val initial: State = (self.initial, that.initial)
      override final def step(now: OffsetDateTime, in: (In, In2), state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, (Out, Out2), Decision)] = {
        val (in1, in2) = in

        self.step(now, in1, state._1).zipWith(that.step(now, in2, state._2)) {
          case ((lState, out, Continue(lInterval)), (rState, out2, Continue(rInterval))) =>
            val interval = lInterval.union(rInterval)
            ((lState, rState), out -> out2, Continue(interval))
          case ((lState, out, _), (rState, out2, _)) =>
            ((lState, rState), out -> out2, Done)
        }
      }
    }

  /**
   * The same as `&&`, but ignores the left output.
   */
  final def *>[Env1 <: Env, In1 <: In, Out2](
    that: Schedule[Env1, In1, Out2]
  )(implicit trace: Trace): Schedule.WithState[(self.State, that.State), Env1, In1, Out2] =
    (self && that).map(_._2)

  /**
   * A symbolic alias for `andThen`.
   */
  final def ++[Env1 <: Env, In1 <: In, Out2 >: Out](
    that: Schedule[Env1, In1, Out2]
  )(implicit trace: Trace): Schedule.WithState[(self.State, that.State, Boolean), Env1, In1, Out2] =
    self andThen that

  /**
   * Returns a new schedule that allows choosing between feeding inputs to this
   * schedule, or feeding inputs to the specified schedule.
   */
  final def +++[Env1 <: Env, In2, Out2](
    that: Schedule[Env1, In2, Out2]
  ): Schedule.WithState[(self.State, that.State), Env1, Either[In, In2], Either[Out, Out2]] =
    new Schedule[Env1, Either[In, In2], Either[Out, Out2]] {
      override type State = (self.State, that.State)
      override final val initial: State = (self.initial, that.initial)
      override final def step(
        now: OffsetDateTime,
        either: Either[In, In2],
        state: State
      )(implicit trace: Trace): ZIO[Env1, Nothing, (State, Either[Out, Out2], Decision)] =
        either match {
          case Left(in) =>
            self.step(now, in, state._1).map { case (lState, out, decision) =>
              ((lState, state._2), Left(out), decision)
            }
          case Right(in2) =>
            that.step(now, in2, state._2).map { case (rState, out2, decision) =>
              ((state._1, rState), Right(out2), decision)
            }
        }
    }

  /**
   * Operator alias for `andThenEither`.
   */
  final def <||>[Env1 <: Env, In1 <: In, Out2](
    that: Schedule[Env1, In1, Out2]
  ): Schedule.WithState[(self.State, that.State, Boolean), Env1, In1, Either[Out, Out2]] =
    self.andThenEither(that)

  /**
   * The same as `&&`, but ignores the right output.
   */
  final def <*[Env1 <: Env, In1 <: In, Out2](
    that: Schedule[Env1, In1, Out2]
  )(implicit trace: Trace): Schedule.WithState[(self.State, that.State), Env1, In1, Out] =
    (self && that).map(_._1)

  /**
   * An operator alias for `zip`.
   */
  final def <*>[Env1 <: Env, In1 <: In, Out2](that: Schedule[Env1, In1, Out2])(implicit
    zippable: Zippable[Out, Out2]
  ): Schedule.WithState[(self.State, that.State), Env1, In1, zippable.Out] =
    self zip that

  /**
   * A backwards version of `>>>`.
   */
  final def <<<[Env1 <: Env, In2](
    that: Schedule[Env1, In2, In]
  ): Schedule.WithState[(that.State, self.State), Env1, In2, Out] =
    that >>> self

  /**
   * Returns the composition of this schedule and the specified schedule, by
   * piping the output of this one into the input of the other. Effects
   * described by this schedule will always be executed before the effects
   * described by the second schedule.
   */
  final def >>>[Env1 <: Env, Out2](
    that: Schedule[Env1, Out, Out2]
  ): Schedule.WithState[(self.State, that.State), Env1, In, Out2] =
    new Schedule[Env1, In, Out2] {
      override type State = (self.State, that.State)
      override final val initial: State = (self.initial, that.initial)
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Out2, Decision)] =
        self.step(now, in, state._1).flatMap {
          case (lState, out, Done) =>
            that.step(now, out, state._2).map { case (rState, out2, _) =>
              ((lState, rState), out2, Done)
            }
          case (lState, out, Continue(interval)) =>
            that.step(now, out, state._2).map {
              case (rState, out2, Done) => ((lState, rState), out2, Done)
              case (rState, out2, Continue(interval2)) =>
                val combined = interval max interval2

                ((lState, rState), out2, Continue(combined))
            }
        }
    }

  /**
   * Returns a new schedule that performs a geometric union on the intervals
   * defined by both schedules.
   */
  final def ||[Env1 <: Env, In1 <: In, Out2](that: Schedule[Env1, In1, Out2])(implicit
    zippable: Zippable[Out, Out2]
  ): Schedule.WithState[(self.State, that.State), Env1, In1, zippable.Out] =
    (self unionWith that)(_ union _)

  /**
   * Returns a new schedule that chooses between two schedules with a common
   * output.
   */
  final def |||[Env1 <: Env, Out1 >: Out, In2](
    that: Schedule[Env1, In2, Out1]
  )(implicit trace: Trace): Schedule.WithState[(self.State, that.State), Env1, Either[In, In2], Out1] =
    (self +++ that).map(_.merge)

  /**
   * Returns a new schedule with the given delay added to every interval defined
   * by this schedule.
   */
  final def addDelay(f: Out => Duration)(implicit trace: Trace): Schedule.WithState[self.State, Env, In, Out] =
    addDelayZIO(out => ZIO.succeed(f(out)))

  /**
   * Returns a new schedule with the given effectfully computed delay added to
   * every interval defined by this schedule.
   */
  final def addDelayZIO[Env1 <: Env](f: Out => URIO[Env1, Duration])(implicit
    trace: Trace
  ): Schedule.WithState[self.State, Env1, In, Out] =
    modifyDelayZIO((out, duration) => f(out).map(duration + _))

  /**
   * The same as `andThenEither`, but merges the output.
   */
  final def andThen[Env1 <: Env, In1 <: In, Out2 >: Out](
    that: Schedule[Env1, In1, Out2]
  )(implicit trace: Trace): Schedule.WithState[(self.State, that.State, Boolean), Env1, In1, Out2] =
    (self andThenEither that).map(_.merge)

  /**
   * Returns a new schedule that first executes this schedule to completion, and
   * then executes the specified schedule to completion.
   */
  final def andThenEither[Env1 <: Env, In1 <: In, Out2](
    that: Schedule[Env1, In1, Out2]
  ): Schedule.WithState[(self.State, that.State, Boolean), Env1, In1, Either[Out, Out2]] =
    new Schedule[Env1, In1, Either[Out, Out2]] {
      override type State = (self.State, that.State, Boolean)
      override final val initial: State = (self.initial, that.initial, true)
      override final def step(now: OffsetDateTime, in: In1, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Either[Out, Out2], Decision)] = {
        val onLeft = state._3
        if (onLeft) self.step(now, in, state._1).flatMap {
          case (lState, out, Continue(interval)) =>
            ZIO.succeed(((lState, state._2, true), Left(out), Continue(interval)))
          case (lState, _, Done) =>
            that.step(now, in, state._2).map { case (rState, out, decision) =>
              ((lState, rState, false), Right(out), decision)
            }
        }
        else
          that.step(now, in, state._2).map { case (rState, out, decision) =>
            ((state._1, rState, false), Right(out), decision)
          }
      }
    }

  /**
   * Returns a new schedule that maps this schedule to a constant output.
   */
  final def as[Out2](out2: => Out2)(implicit trace: Trace): Schedule.WithState[self.State, Env, In, Out2] =
    self.map(_ => out2)

  /**
   * Returns a new schedule that passes each input and output of this schedule
   * to the specified function, and then determines whether or not to continue
   * based on the return value of the function.
   */
  final def check[In1 <: In](test: (In1, Out) => Boolean)(implicit
    trace: Trace
  ): Schedule.WithState[self.State, Env, In1, Out] =
    checkZIO((in1, out) => ZIO.succeed(test(in1, out)))

  /**
   * Returns a new schedule that passes each input and output of this schedule
   * to the specified function, and then determines whether or not to continue
   * based on the return value of the function.
   */
  final def checkZIO[Env1 <: Env, In1 <: In](
    test: (In1, Out) => URIO[Env1, Boolean]
  ): Schedule.WithState[self.State, Env1, In1, Out] =
    new Schedule[Env1, In1, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In1, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Out, Decision)] =
        self.step(now, in, state).flatMap {
          case (state, out, Done) => ZIO.succeed((state, out, Done))
          case (state, out, Continue(interval)) =>
            test(in, out).map { b =>
              if (b) (state, out, Continue(interval)) else (state, out, Done)
            }
        }
    }

  /**
   * Returns a new schedule that collects the outputs of this one into a chunk.
   */
  final def collectAll[Out1 >: Out](implicit
    trace: Trace
  ): Schedule.WithState[(self.State, Chunk[Out1]), Env, In, Chunk[Out1]] =
    collectWhile(_ => true)

  /**
   * Returns a new schedule that collects the outputs of this one into a list as
   * long as the condition f holds.
   */
  final def collectWhile[Out1 >: Out](f: Out => Boolean)(implicit
    trace: Trace
  ): Schedule.WithState[(self.State, Chunk[Out1]), Env, In, Chunk[Out1]] =
    collectWhileZIO(out => ZIO.succeed(f(out)))

  /**
   * Returns a new schedule that collects the outputs of this one into a list as
   * long as the effectual condition f holds.
   */
  final def collectWhileZIO[Env1 <: Env, Out1 >: Out](f: Out => URIO[Env1, Boolean])(implicit
    trace: Trace
  ): Schedule.WithState[(self.State, Chunk[Out1]), Env1, In, Chunk[Out1]] =
    new Schedule[Env1, In, Chunk[Out1]] {
      override type State = (self.State, Chunk[Out1])
      override final val initial: State = (self.initial, Chunk.empty)
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Chunk[Out1], Decision)] = {
        val s = state._1
        val z = state._2
        self.step(now, in, s).flatMap {
          case (s, out, Done) =>
            f(out).map { b =>
              if (!b) ((s, z), z, Done)
              else {
                val z2 = z :+ out
                ((s, z2), z2, Done)
              }
            }
          case (s, out, Continue(interval)) =>
            f(out).map { b =>
              if (!b) ((s, z), z, Done)
              else {
                val z2 = z :+ out
                ((s, z2), z2, Continue(interval))
              }

            }
        }
      }
    }

  /**
   * Returns a new schedule that collects the outputs of this one into a list
   * until the condition f fails.
   */
  final def collectUntil[Out1 >: Out](f: Out => Boolean)(implicit
    trace: Trace
  ): Schedule.WithState[(self.State, Chunk[Out1]), Env, In, Chunk[Out1]] =
    collectUntilZIO(out => ZIO.succeed(f(out)))

  /**
   * Returns a new schedule that collects the outputs of this one into a list
   * until the effectual condition f fails.
   */
  final def collectUntilZIO[Env1 <: Env, Out1 >: Out](f: Out => URIO[Env1, Boolean])(implicit
    trace: Trace
  ): Schedule.WithState[(self.State, Chunk[Out1]), Env1, In, Chunk[Out1]] =
    collectWhileZIO(!f(_))

  /**
   * A named alias for `<<<`.
   */
  final def compose[Env1 <: Env, In2](
    that: Schedule[Env1, In2, In]
  ): Schedule.WithState[(that.State, self.State), Env1, In2, Out] =
    that >>> self

  /**
   * Returns a new schedule that deals with a narrower class of inputs than this
   * schedule.
   */
  final def contramap[Env1 <: Env, In2](f: In2 => In)(implicit
    trace: Trace
  ): Schedule.WithState[self.State, Env, In2, Out] =
    self.contramapZIO(in => ZIO.succeed(f(in)))

  /**
   * Returns a new schedule that deals with a narrower class of inputs than this
   * schedule.
   */
  final def contramapZIO[Env1 <: Env, In2](f: In2 => URIO[Env1, In]): Schedule.WithState[self.State, Env1, In2, Out] =
    new Schedule[Env1, In2, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in2: In2, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Out, Decision)] =
        f(in2).flatMap(in => self.step(now, in, state))
    }

  /**
   * A schedule that recurs during the given duration
   */
  final def upTo(duration: Duration)(implicit
    trace: Trace
  ): Schedule.WithState[(self.State, Option[OffsetDateTime]), Env, In, Out] =
    self <* Schedule.upTo(duration)

  /**
   * Returns a new schedule with the specified effectfully computed delay added
   * before the start of each interval produced by this schedule.
   */
  final def delayed(f: Duration => Duration)(implicit trace: Trace): Schedule.WithState[self.State, Env, In, Out] =
    self.delayedZIO(d => ZIO.succeed(f(d)))

  /**
   * Returns a new schedule that outputs the delay between each occurence.
   */
  final def delays: Schedule.WithState[self.State, Env, In, Duration] =
    new Schedule[Env, In, Duration] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env, Nothing, (State, Duration, Decision)] =
        self.step(now, in, state).flatMap {
          case (state, _, Done) =>
            ZIO.succeed((state, Duration.Zero, Done))
          case (state, _, Continue(interval)) =>
            val delay = Duration.fromInterval(now, interval.start)
            ZIO.succeed((state, delay, Continue(interval)))
        }
    }

  /**
   * Returns a new schedule with the specified effectfully computed delay added
   * before the start of each interval produced by this schedule.
   */
  final def delayedZIO[Env1 <: Env](
    f: Duration => URIO[Env1, Duration]
  ): Schedule.WithState[self.State, Env1, In, Out] =
    modifyDelayZIO((_, delay) => f(delay))

  /**
   * Returns a new schedule that contramaps the input and maps the output.
   */
  final def dimap[In2, Out2](f: In2 => In, g: Out => Out2)(implicit
    trace: Trace
  ): Schedule.WithState[self.State, Env, In2, Out2] =
    contramap(f).map(g)

  /**
   * Returns a new schedule that contramaps the input and maps the output.
   */
  final def dimapZIO[Env1 <: Env, In2, Out2](
    f: In2 => URIO[Env1, In],
    g: Out => URIO[Env1, Out2]
  ): Schedule.WithState[self.State, Env1, In2, Out2] =
    contramapZIO(f).mapZIO(g)

  /**
   * Returns a driver that can be used to step the schedule, appropriately
   * handling sleeping.
   */
  final def driver(implicit trace: Trace): UIO[Schedule.Driver[self.State, Env, In, Out]] =
    Ref.make[(Option[Out], self.State)]((None, self.initial)).map { ref =>
      val next = (in: In) =>
        for {
          state <- ref.get.map(_._2)
          now   <- Clock.currentDateTime
          dec   <- self.step(now, in, state)
          v <- dec match {
                 case (state, out, Done) =>
                   ref.set((Some(out), state)) *> Exit.failNone.asInstanceOf[Exit[None.type, Out]]
                 case (state, out, Continue(interval)) =>
                   ref.set((Some(out), state)) *> ZIO.sleep(Duration.fromInterval(now, interval.start)) as out
               }
        } yield v

      val last = ref.get.flatMap {
        case (None, _)    => ZIO.fail(new NoSuchElementException("There is no value left"))
        case (Some(b), _) => Exit.succeed(b)
      }

      val reset = ref.set((None, self.initial))

      val state = ref.get.map(_._2)

      Schedule.Driver(next, last, reset, state)
    }

  /**
   * A named alias for `||`.
   */
  final def either[Env1 <: Env, In1 <: In, Out2](
    that: Schedule[Env1, In1, Out2]
  ): Schedule.WithState[(self.State, that.State), Env1, In1, (Out, Out2)] =
    self || that

  /**
   * The same as `either` followed by `map`.
   */
  final def eitherWith[Env1 <: Env, In1 <: In, Out2, Out3](
    that: Schedule[Env1, In1, Out2]
  )(f: (Out, Out2) => Out3)(implicit
    trace: Trace
  ): Schedule.WithState[(self.State, that.State), Env1, In1, Out3] =
    (self || that).map(f.tupled)

  /**
   * Returns a new schedule that will run the specified finalizer as soon as the
   * schedule is complete. Note that unlike `ZIO#ensuring`, this method does not
   * guarantee the finalizer will be run. The `Schedule` may not initialize or
   * the driver of the schedule may not run to completion. However, if the
   * `Schedule` ever decides not to continue, then the finalizer will be run.
   */
  final def ensuring(finalizer: UIO[Any]): Schedule.WithState[self.State, Env, In, Out] =
    new Schedule[Env, In, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env, Nothing, (State, Out, Decision)] =
        self.step(now, in, state).flatMap {
          case (state, out, Done)               => finalizer.as((state, out, Done))
          case (state, out, Continue(interval)) => ZIO.succeed((state, out, Continue(interval)))
        }
    }

  /**
   * Returns a new schedule that packs the input and output of this schedule
   * into the first element of a tuple. This allows carrying information through
   * this schedule.
   */
  final def first[X]: Schedule.WithState[(self.State, Unit), Env, (In, X), (Out, X)] =
    self *** Schedule.identity[X]

  /**
   * Returns a new schedule that folds over the outputs of this one.
   */
  final def fold[Z](z: Z)(f: (Z, Out) => Z)(implicit trace: Trace): Schedule.WithState[(self.State, Z), Env, In, Z] =
    foldZIO(z)((z, out) => ZIO.succeed(f(z, out)))

  /**
   * Returns a new schedule that effectfully folds over the outputs of this one.
   */
  final def foldZIO[Env1 <: Env, Z](
    z: Z
  )(f: (Z, Out) => URIO[Env1, Z]): Schedule.WithState[(self.State, Z), Env1, In, Z] =
    new Schedule[Env1, In, Z] {
      override type State = (self.State, Z)
      override final val initial: State = (self.initial, z)
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Z, Decision)] = {
        val s = state._1
        val z = state._2
        self.step(now, in, s).flatMap {
          case (s, _, Done)                 => ZIO.succeed(((s, z), z, Done))
          case (s, out, Continue(interval)) => f(z, out).map(z2 => ((s, z2), z, Continue(interval)))
        }
      }
    }

  /**
   * Returns a new schedule that loops this one continuously, resetting the
   * state when this schedule is done.
   */
  final def forever: Schedule.WithState[self.State, Env, In, Out] =
    new Schedule[Env, In, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env, Nothing, (State, Out, Decision)] =
        self.step(now, in, state).flatMap {
          case (_, _, Done)                     => step(now, in, initial)
          case (state, out, Continue(interval)) => ZIO.succeed((state, out, Continue(interval)))
        }
    }

  /**
   * Returns a new schedule that combines this schedule with the specified
   * schedule, continuing as long as both schedules want to continue and merging
   * the next intervals according to the specified merge function.
   */
  final def intersectWith[Env1 <: Env, In1 <: In, Out2](
    that: Schedule[Env1, In1, Out2]
  )(f: (Intervals, Intervals) => Intervals)(implicit
    zippable: Zippable[Out, Out2]
  ): Schedule.WithState[(self.State, that.State), Env1, In1, zippable.Out] =
    new Schedule[Env1, In1, zippable.Out] {
      override type State = (self.State, that.State)
      override final val initial: State = (self.initial, that.initial)

      private def loop(
        in: In1,
        lState: self.State,
        out: Out,
        lInterval: Intervals,
        rState: that.State,
        out2: Out2,
        rInterval: Intervals
      )(implicit trace: Trace): ZIO[Env1, Nothing, (State, zippable.Out, Decision)] = {
        val combined = f(lInterval, rInterval)
        if (combined.nonEmpty)
          ZIO.succeed(((lState, rState), zippable.zip(out, out2), Continue(combined)))
        else if (lInterval < rInterval)
          self.step(lInterval.end, in, lState).flatMap {
            case ((lState, out, Continue(lInterval))) =>
              loop(in, lState, out, lInterval, rState, out2, rInterval)
            case ((lState, out, _)) => ZIO.succeed(((lState, rState), zippable.zip(out, out2), Done))
          }
        else
          that.step(rInterval.end, in, rState).flatMap {
            case ((rState, out2, Continue(rInterval))) =>
              loop(in, lState, out, lInterval, rState, out2, rInterval)
            case ((rState, out2, _)) => ZIO.succeed(((lState, rState), zippable.zip(out, out2), Done))
          }
      }

      override final def step(now: OffsetDateTime, in: In1, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, zippable.Out, Decision)] = {
        val left  = self.step(now, in, state._1)
        val right = that.step(now, in, state._2)

        left.zipWith(right)((_, _)).flatMap {
          case ((lState, out, Continue(lInterval)), (rState, out2, Continue(rInterval))) =>
            loop(in, lState, out, lInterval, rState, out2, rInterval)
          case ((lState, out, _), (rState, out2, _)) =>
            ZIO.succeed(((lState, rState), zippable.zip(out, out2), Done))
        }
      }
    }

  /**
   * Returns a new schedule that randomly modifies the size of the intervals of
   * this schedule.
   */
  final def jittered(implicit trace: Trace): Schedule.WithState[self.State, Env, In, Out] =
    jittered(0.8, 1.2)

  /**
   * Returns a new schedule that randomly modifies the size of the intervals of
   * this schedule.
   *
   * The new interval size is between `min * old interval size` and `max * old
   * interval size`.
   *
   * [Research](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)
   * shows that `jittered(0.0, 1.0)` is a suitable range for a retrying
   * schedule.
   */
  final def jittered(min: Double, max: Double)(implicit
    trace: Trace
  ): Schedule.WithState[self.State, Env, In, Out] =
    delayedZIO[Env] { duration =>
      Random.nextDouble.map { random =>
        val d        = duration.toNanos
        val jittered = d * min * (1 - random) + d * max * random

        Duration.fromNanos(jittered.toLong)
      }
    }

  /**
   * Returns a new schedule that makes this schedule available on the `Left`
   * side of an `Either` input, allowing propagating some type `X` through this
   * channel on demand.
   */
  final def left[X]: Schedule.WithState[(self.State, Unit), Env, Either[In, X], Either[Out, X]] =
    self +++ Schedule.identity[X]

  /**
   * Returns a new schedule that maps the output of this schedule through the
   * specified function.
   */
  final def map[Out2](f: Out => Out2)(implicit trace: Trace): Schedule.WithState[self.State, Env, In, Out2] =
    self.mapZIO(out => ZIO.succeed(f(out)))

  /**
   * Returns a new schedule that maps the output of this schedule through the
   * specified effectful function.
   */
  final def mapZIO[Env1 <: Env, Out2](f: Out => URIO[Env1, Out2]): Schedule.WithState[self.State, Env1, In, Out2] =
    new Schedule[Env1, In, Out2] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Out2, Decision)] =
        self.step(now, in, state).flatMap { case (state, out, decision) =>
          f(out).map(out2 => (state, out2, decision))
        }
    }

  /**
   * Returns a new schedule that modifies the delay using the specified
   * function.
   */
  final def modifyDelay(f: (Out, Duration) => Duration): Schedule.WithState[self.State, Env, In, Out] =
    modifyDelayZIO((out, duration) => Exit.succeed(f(out, duration)))

  /**
   * Returns a new schedule that modifies the delay using the specified
   * effectual function.
   */
  final def modifyDelayZIO[Env1 <: Env](
    f: (Out, Duration) => URIO[Env1, Duration]
  ): Schedule.WithState[self.State, Env1, In, Out] =
    new Schedule[Env1, In, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Out, Decision)] =
        self.step(now, in, state).flatMap {
          case (state, out, Done) => ZIO.succeed((state, out, Done))
          case (state, out, Continue(interval)) =>
            val delay = Interval(now, interval.start).size

            f(out, delay).map { duration =>
              val oldStart = interval.start
              val newStart = now.plusNanos(duration.toNanos)
              val delta    = java.time.Duration.between(oldStart, newStart)
              val newEnd =
                if (interval.end == OffsetDateTime.MAX) OffsetDateTime.MAX
                else
                  try { interval.end.plus(delta) }
                  catch { case _: java.time.DateTimeException => OffsetDateTime.MAX }

              val newInterval = Interval(newStart, newEnd)

              (state, out, Continue(newInterval))
            }
        }
    }

  /**
   * Returns a new schedule that applies the current one but runs the specified
   * effect for every decision of this schedule. This can be used to create
   * schedules that log failures, decisions, or computed values.
   */
  final def onDecision[Env1 <: Env](
    f: (State, Out, Decision) => URIO[Env1, Any]
  ): Schedule.WithState[self.State, Env1, In, Out] =
    new Schedule[Env1, In, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Out, Decision)] =
        self.step(now, in, state).flatMap { case (state, out, decision) =>
          f(state, out, decision).as((state, out, decision))
        }
    }

  /**
   * Returns a new schedule that passes through the inputs of this schedule.
   */
  final def passthrough[In1 <: In](implicit trace: Trace): Schedule.WithState[self.State, Env, In1, In1] =
    new Schedule[Env, In1, In1] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In1, state: State)(implicit
        trace: Trace
      ): ZIO[Env, Nothing, (State, In1, Decision)] =
        self.step(now, in, state).map { case (state, _, decision) =>
          (state, in, decision)
        }
    }

  /**
   * Returns a new schedule with its environment provided to it, so the
   * resulting schedule does not require any environment.
   */
  final def provideEnvironment(env: ZEnvironment[Env]): Schedule.WithState[self.State, Any, In, Out] =
    new Schedule[Any, In, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Any, Nothing, (State, Out, Decision)] =
        self.step(now, in, state).provideEnvironment(env)
    }

  /**
   * Transforms the environment being provided to this schedule with the
   * specified function.
   */
  final def provideSomeEnvironment[Env2](
    f: ZEnvironment[Env2] => ZEnvironment[Env]
  ): Schedule.WithState[self.State, Env2, In, Out] =
    new Schedule[Env2, In, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env2, Nothing, (State, Out, Decision)] =
        self.step(now, in, state).provideSomeEnvironment(f)
    }

  /**
   * Returns a new schedule that reconsiders every decision made by this
   * schedule, possibly modifying the next interval and the output type in the
   * process.
   */
  final def reconsider[Out2](
    f: (State, Out, Decision) => Either[Out2, (Out2, Interval)]
  )(implicit trace: Trace): Schedule.WithState[self.State, Env, In, Out2] =
    reconsiderZIO { case (state, out, decision) => ZIO.succeed(f(state, out, decision)) }

  /**
   * Returns a new schedule that effectfully reconsiders every decision made by
   * this schedule, possibly modifying the next interval and the output type in
   * the process.
   */
  final def reconsiderZIO[Env1 <: Env, In1 <: In, Out2](
    f: (State, Out, Decision) => URIO[Env1, Either[Out2, (Out2, Interval)]]
  ): Schedule.WithState[self.State, Env1, In1, Out2] =
    new Schedule[Env1, In1, Out2] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In1, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Out2, Decision)] =
        self.step(now, in, state).flatMap {
          case (state, out, Done) =>
            f(state, out, Done).map {
              case Left(out2)       => (state, out2, Done)
              case Right((out2, _)) => (state, out2, Done)
            }
          case (state, out, Continue(interval)) =>
            f(state, out, Continue(interval)).map {
              case Left(out2)              => (state, out2, Done)
              case Right((out2, interval)) => (state, out2, Continue(interval))
            }
        }
    }

  /**
   * Returns a new schedule that outputs the number of repetitions of this one.
   */
  final def repetitions(implicit trace: Trace): Schedule.WithState[(self.State, Long), Env, In, Long] =
    fold(0L)((n: Long, _: Out) => n + 1L)

  /**
   * Return a new schedule that automatically resets the schedule to its initial
   * state after some time of inactivity defined by `duration`.
   */
  final def resetAfter(duration: Duration)(implicit
    trace: Trace
  ): Schedule.WithState[(self.State, Option[OffsetDateTime]), Env, In, Out] =
    (self zip Schedule.elapsed).resetWhen(_._2 >= duration).map(_._1)

  /**
   * Resets the schedule when the specified predicate on the schedule output
   * evaluates to true.
   */
  final def resetWhen(f: Out => Boolean): Schedule.WithState[self.State, Env, In, Out] =
    new Schedule[Env, In, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env, Nothing, (State, Out, Decision)] =
        self.step(now, in, state).flatMap { case (state, out, decision) =>
          if (f(out)) self.step(now, in, self.initial) else ZIO.succeed((state, out, decision))
        }
    }

  /**
   * Returns a new schedule that makes this schedule available on the `Right`
   * side of an `Either` input, allowing propagating some type `X` through this
   * channel on demand.
   */
  final def right[X]: Schedule.WithState[(Unit, self.State), Env, Either[X, In], Either[X, Out]] =
    Schedule.identity[X] +++ self

  /**
   * Runs a schedule using the provided inputs, and collects all outputs.
   */
  final def run(now: OffsetDateTime, input: Iterable[In])(implicit trace: Trace): URIO[Env, Chunk[Out]] = {
    def loop(
      now: OffsetDateTime,
      xs: List[In],
      state: State,
      acc: Chunk[Out]
    ): URIO[Env, Chunk[Out]] =
      xs match {
        case Nil => ZIO.succeed(acc)
        case in :: xs =>
          self.step(now, in, state).flatMap {
            case (_, out, Done)                   => ZIO.succeed(acc :+ out)
            case (state, out, Continue(interval)) => loop(interval.start, xs, state, acc :+ out)
          }
      }

    loop(now, input.toList, self.initial, Chunk.empty)
  }

  /**
   * Returns a new schedule that packs the input and output of this schedule
   * into the second element of a tuple. This allows carrying information
   * through this schedule.
   */
  final def second[X]: Schedule.WithState[(Unit, self.State), Env, (X, In), (X, Out)] =
    Schedule.identity[X] *** self

  /**
   * Returns a new schedule that effectfully processes every input to this
   * schedule.
   */
  final def tapInput[Env1 <: Env, In1 <: In](
    f: In1 => URIO[Env1, Any]
  ): Schedule.WithState[self.State, Env1, In1, Out] =
    new Schedule[Env1, In1, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In1, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Out, Decision)] =
        f(in) *> self.step(now, in, state)
    }

  /**
   * Returns a new schedule that effectfully processes every output from this
   * schedule.
   */
  final def tapOutput[Env1 <: Env](f: Out => URIO[Env1, Any]): Schedule.WithState[self.State, Env1, In, Out] =
    new Schedule[Env1, In, Out] {
      override type State = self.State
      override final val initial: State = self.initial
      override final def step(now: OffsetDateTime, in: In, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, Out, Decision)] =
        self.step(now, in, state).tap { case (_, out, _) => f(out) }
    }

  /**
   * Returns a new schedule that combines this schedule with the specified
   * schedule, continuing as long as either schedule wants to continue and
   * merging the next intervals according to the specified merge function.
   */
  final def unionWith[Env1 <: Env, In1 <: In, Out2](
    that: Schedule[Env1, In1, Out2]
  )(
    f: (Intervals, Intervals) => Intervals
  )(implicit zippable: Zippable[Out, Out2]): Schedule.WithState[(self.State, that.State), Env1, In1, zippable.Out] =
    new Schedule[Env1, In1, zippable.Out] {
      override type State = (self.State, that.State)
      override final val initial: State = (self.initial, that.initial)
      override final def step(now: OffsetDateTime, in: In1, state: State)(implicit
        trace: Trace
      ): ZIO[Env1, Nothing, (State, zippable.Out, Decision)] = {
        val left  = self.step(now, in, state._1)
        val right = that.step(now, in, state._2)

        left.zipWith(right) {
          case ((lstate, l, Done), (rstate, r, Done)) =>
            ((lstate, rstate), zippable.zip(l, r), Done)
          case ((lstate, l, Done), (rstate, r, Continue(interval))) =>
            ((lstate, rstate), zippable.zip(l, r), Continue(interval))
          case ((lstate, l, Continue(interval)), (rstate, r, Done)) =>
            ((lstate, rstate), zippable.zip(l, r), Continue(interval))
          case ((lstate, l, Continue(linterval)), (rstate, r, Continue(rinterval))) =>
            val combined = f(linterval, rinterval)
            ((lstate, rstate), zippable.zip(l, r), Continue(combined))
        }
      }
    }

  /**
   * Returns a new schedule that maps the output of this schedule to unit.
   */
  final def unit(implicit trace: Trace): Schedule.WithState[self.State, Env, In, Unit] =
    self.as(())

  /**
   * Returns a new schedule that continues until the specified predicate on the
   * input evaluates to true.
   */
  final def untilInput[In1 <: In](f: In1 => Boolean)(implicit
    trace: Trace
  ): Schedule.WithState[self.State, Env, In1, Out] =
    check((in, _) => !f(in))

  /**
   * Returns a new schedule that continues until the specified effectful
   * predicate on the input evaluates to true.
   */
  final def untilInputZIO[Env1 <: Env, In1 <: In](
    f: In1 => URIO[Env1, Boolean]
  )(implicit trace: Trace): Schedule.WithState[self.State, Env1, In1, Out] =
    checkZIO((in, _) => f(in).map(b => !b))

  /**
   * Returns a new schedule that continues until the specified predicate on the
   * output evaluates to true.
   */
  final def untilOutput(f: Out => Boolean)(implicit trace: Trace): Schedule.WithState[self.State, Env, In, Out] =
    check((_, out) => !f(out))

  /**
   * Returns a new schedule that continues until the specified effectful
   * predicate on the output evaluates to true.
   */
  final def untilOutputZIO[Env1 <: Env](f: Out => URIO[Env1, Boolean])(implicit
    trace: Trace
  ): Schedule.WithState[self.State, Env1, In, Out] =
    checkZIO((_, out) => f(out).map(b => !b))

  /**
   * Returns a new schedule that continues for as long the specified predicate
   * on the input evaluates to true.
   */
  final def whileInput[In1 <: In](f: In1 => Boolean)(implicit
    trace: Trace
  ): Schedule.WithState[self.State, Env, In1, Out] =
    check((in, _) => f(in))

  /**
   * Returns a new schedule that continues for as long the specified effectful
   * predicate on the input evaluates to true.
   */
  final def whileInputZIO[Env1 <: Env, In1 <: In](
    f: In1 => URIO[Env1, Boolean]
  ): Schedule.WithState[self.State, Env1, In1, Out] =
    checkZIO((in, _) => f(in))

  /**
   * Returns a new schedule that continues for as long the specified predicate
   * on the output evaluates to true.
   */
  final def whileOutput(f: Out => Boolean)(implicit trace: Trace): Schedule.WithState[self.State, Env, In, Out] =
    check((_, out) => f(out))

  /**
   * Returns a new schedule that continues for as long the specified effectful
   * predicate on the output evaluates to true.
   */
  final def whileOutputZIO[Env1 <: Env](f: Out => URIO[Env1, Boolean]): Schedule.WithState[self.State, Env1, In, Out] =
    checkZIO((_, out) => f(out))

  /**
   * A named method for `&&`.
   */
  final def zip[Env1 <: Env, In1 <: In, Out2](that: Schedule[Env1, In1, Out2])(implicit
    zippable: Zippable[Out, Out2]
  ): Schedule.WithState[(self.State, that.State), Env1, In1, zippable.Out] =
    self && that

  /**
   * The same as `&&`, but ignores the right output.
   */
  final def zipLeft[Env1 <: Env, In1 <: In, Out2](
    that: Schedule[Env1, In1, Out2]
  )(implicit trace: Trace): Schedule.WithState[(self.State, that.State), Env1, In1, Out] =
    self <* that

  /**
   * The same as `&&`, but ignores the left output.
   */
  final def zipRight[Env1 <: Env, In1 <: In, Out2](
    that: Schedule[Env1, In1, Out2]
  )(implicit trace: Trace): Schedule.WithState[(self.State, that.State), Env1, In1, Out2] =
    self *> that

  /**
   * Equivalent to `zip` followed by `map`.
   */
  final def zipWith[Env1 <: Env, In1 <: In, Out2, Out3](
    that: Schedule[Env1, In1, Out2]
  )(f: (Out, Out2) => Out3)(implicit
    trace: Trace
  ): Schedule.WithState[(self.State, that.State), Env1, In1, Out3] =
    (self zip that).map(f.tupled)
}

object Schedule {

  /**
   * A schedule that recurs anywhere, collecting all inputs into a list.
   */
  def collectAll[A](implicit trace: Trace): Schedule.WithState[(Unit, Chunk[A]), Any, A, Chunk[A]] =
    identity[A].collectAll

  /**
   * A schedule that recurs as long as the condition f holds, collecting all
   * inputs into a list.
   */
  def collectWhile[A](f: A => Boolean)(implicit
    trace: Trace
  ): Schedule.WithState[(Unit, Chunk[A]), Any, A, Chunk[A]] =
    identity[A].collectWhile(f)

  /**
   * A schedule that recurs as long as the effectful condition holds, collecting
   * all inputs into a list.
   */
  def collectWhileZIO[Env, A](f: A => URIO[Env, Boolean])(implicit
    trace: Trace
  ): Schedule.WithState[(Unit, Chunk[A]), Env, A, Chunk[A]] =
    identity[A].collectWhileZIO(f)

  /**
   * A schedule that recurs until the condition f fails, collecting all inputs
   * into a list.
   */
  def collectUntil[A](f: A => Boolean)(implicit
    trace: Trace
  ): Schedule.WithState[(Unit, Chunk[A]), Any, A, Chunk[A]] =
    identity[A].collectUntil(f)

  /**
   * A schedule that recurs until the effectful condition f fails, collecting
   * all inputs into a list.
   */
  def collectUntilZIO[Env, A](f: A => URIO[Env, Boolean])(implicit
    trace: Trace
  ): Schedule.WithState[(Unit, Chunk[A]), Env, A, Chunk[A]] =
    identity[A].collectUntilZIO(f)

  /**
   * Takes a schedule that produces a delay, and returns a new schedule that
   * uses this delay to further delay intervals in the resulting schedule.
   */
  def delayed[Env, In](schedule: Schedule[Env, In, Duration])(implicit
    trace: Trace
  ): Schedule.WithState[schedule.State, Env, In, Duration] =
    schedule.addDelay(x => x)

  /**
   * A schedule that recurs for as long as the predicate evaluates to true.
   */
  def recurWhile[A](f: A => Boolean)(implicit trace: Trace): Schedule.WithState[Unit, Any, A, A] =
    identity[A].whileInput(f)

  /**
   * A schedule that recurs for as long as the effectful predicate evaluates to
   * true.
   */
  def recurWhileZIO[Env, A](f: A => URIO[Env, Boolean]): Schedule.WithState[Unit, Env, A, A] =
    identity[A].whileInputZIO(f)

  /**
   * A schedule that recurs for as long as the predicate is equal.
   */
  def recurWhileEquals[A](a: => A)(implicit trace: Trace): Schedule.WithState[Unit, Any, A, A] =
    identity[A].whileInput(_ == a)

  /**
   * A schedule that recurs for until the predicate evaluates to true.
   */
  def recurUntil[A](f: A => Boolean)(implicit trace: Trace): Schedule.WithState[Unit, Any, A, A] =
    identity[A].untilInput(f)

  /**
   * A schedule that recurs for until the predicate evaluates to true.
   */
  def recurUntilZIO[Env, A](f: A => URIO[Env, Boolean])(implicit
    trace: Trace
  ): Schedule.WithState[Unit, Env, A, A] =
    identity[A].untilInputZIO(f)

  /**
   * A schedule that recurs for until the predicate is equal.
   */
  def recurUntilEquals[A](a: => A)(implicit trace: Trace): Schedule.WithState[Unit, Any, A, A] =
    identity[A].untilInput(_ == a)

  /**
   * A schedule that recurs for until the input value becomes applicable to
   * partial function and then map that value with given function.
   */
  def recurUntil[A, B](pf: PartialFunction[A, B])(implicit
    trace: Trace
  ): Schedule.WithState[Unit, Any, A, Option[B]] =
    identity[A].map(pf.lift(_)).untilOutput(_.isDefined)

  /**
   * A schedule that can recur one time, the specified amount of time into the
   * future.
   */
  def duration(duration: Duration): Schedule.WithState[Boolean, Any, Any, Duration] =
    new Schedule[Any, Any, Duration] {
      override type State = Boolean
      override final val initial: State = true
      def step(now: OffsetDateTime, in: Any, state: State)(implicit
        trace: Trace
      ): ZIO[Any, Nothing, (State, Duration, Decision)] =
        ZIO.succeed {
          if (state) {
            val interval = Interval.after(now.plusNanos(duration.toNanos))
            (false, duration, Decision.Continue(interval))
          } else {
            (false, Duration.Zero, Decision.Done)
          }
        }
    }

  /**
   * A schedule that occurs everywhere, which returns the total elapsed duration
   * since the first step.
   */
  val elapsed: Schedule.WithState[Option[OffsetDateTime], Any, Any, Duration] =
    new Schedule[Any, Any, Duration] {
      override type State = Option[OffsetDateTime]
      override final val initial: State = None
      def step(now: OffsetDateTime, in: Any, state: State)(implicit
        trace: Trace
      ): ZIO[Any, Nothing, (State, Duration, Decision)] =
        ZIO.succeed {
          state match {
            case None => (Some(now), Duration.Zero, Decision.Continue(Interval(now, OffsetDateTime.MAX)))
            case Some(start) =>
              val duration = Duration.fromInterval(start, now)

              (Some(start), duration, Decision.Continue(Interval(now, OffsetDateTime.MAX)))
          }
        }
    }

  /**
   * A schedule that always recurs, but will wait a certain amount between
   * repetitions, given by `base * factor.pow(n)`, where `n` is the number of
   * repetitions so far. Returns the current duration between recurrences.
   */
  def exponential(base: Duration, factor: Double = 2.0)(implicit
    trace: Trace
  ): Schedule.WithState[Long, Any, Any, Duration] =
    delayed[Any, Any](forever.map(i => base * math.pow(factor, i.doubleValue)))

  /**
   * A schedule that always recurs, increasing delays by summing the preceding
   * two delays (similar to the fibonacci sequence). Returns the current
   * duration between recurrences.
   */
  def fibonacci(
    one: Duration
  )(implicit trace: Trace): Schedule.WithState[(Duration, Duration), Any, Any, Duration] =
    delayed[Any, Any] {
      unfold[(Duration, Duration)]((one, one)) { case (a1, a2) =>
        (a2, a1 + a2)
      }.map(_._1)
    }

  // format: off
  /**
   * A schedule that recurs on a fixed interval. Returns the number of
   * repetitions of the schedule so far.
   *
   * If the action run between updates takes longer than the interval, then the
   * action will be run immediately, but re-runs will not "pile up".
   *
   * 
   * |-----interval-----|-----interval-----|-----interval-----|
   * |---------action--------||action|-----|action|-----------|
   * 
*/ def fixed(interval: Duration): Schedule.WithState[(Option[(Long, Long)], Long), Any, Any, Long] = new Schedule[Any, Any, Long] { import java.time.Duration override type State = (Option[(Long, Long)], Long) override final val initial: State = (None, 0L) private val intervalNanos = interval.toNanos() def step(now: OffsetDateTime, in: Any, state: State)(implicit trace: Trace ): ZIO[Any, Nothing, (State, Long, Decision)] = ZIO.succeed(state match { case (Some((startNanos, lastRun)), n) => val nowNanos = Duration.between(Instant.EPOCH, now).toNanos() val runningBehind = nowNanos > (lastRun + intervalNanos) val boundary = if (interval.isZero) interval else Duration.ofNanos(intervalNanos - ((nowNanos - startNanos) % intervalNanos)) val sleepTime = if (boundary.isZero) interval else boundary val nextRun = if (runningBehind) now else now.plus(sleepTime) ( (Some((startNanos, Duration.between(Instant.EPOCH, nextRun.toInstant).toNanos())), n + 1L), n, Decision.Continue(Interval.after(nextRun)) ) case (None, n) => val nowNanos = Duration.between(Instant.EPOCH, now.toInstant).toNanos val nextRun = now.plus(interval) ( (Some((nowNanos, Duration.between(Instant.EPOCH, nextRun.toInstant).toNanos())), n + 1L), n, Decision.Continue(Interval.after(nextRun)) ) }) } /** * A schedule that recurs during the given duration */ def upTo(duration: Duration)(implicit trace: Trace ): Schedule.WithState[Option[OffsetDateTime], Any, Any, Duration] = elapsed.whileOutput(_ < duration) /** * A schedule that always recurs, producing a count of repeats: 0, 1, 2. */ val forever: Schedule.WithState[Long, Any, Any, Long] = unfold(0L)(_ + 1L) /** * A schedule that recurs once with the specified delay. */ def fromDuration(duration: Duration): Schedule.WithState[Boolean, Any, Any, Duration] = Schedule.duration(duration) /** * A schedule that recurs once for each of the specified durations, delaying * each time for the length of the specified duration. Returns the length of * the current duration between recurrences. */ def fromDurations( duration: Duration, durations: Duration* ): Schedule.WithState[(::[Duration], Boolean), Any, Any, Duration] = new Schedule[Any, Any, Duration] { override type State = (::[Duration], Boolean) override final val initial: State = (::(duration, durations.toList), true) override final def step(now: OffsetDateTime, in: Any, state: State)(implicit trace: Trace ): ZIO[Any, Nothing, (State, Duration, Decision)] = { val durations = state._1 val continue = state._2 ZIO.succeed(if (continue) { val interval = Interval.after(now.plusNanos(durations.head.toNanos)) durations match { case x :: y :: z => ((::(y, z), true), x, Decision.Continue(interval)) case x :: y => ((::(x, y), false), x, Decision.Continue(interval)) } } else ((durations, false), Duration.Zero, Decision.Done)) } } /** * A schedule that always recurs, mapping input values through the specified * function. */ def fromFunction[A, B](f: A => B)(implicit trace: Trace): Schedule.WithState[Unit, Any, A, B] = identity[A].map(f) /** * A schedule that always recurs, which counts the number of recurrences. */ val count: Schedule.WithState[Long, Any, Any, Long] = unfold(0L)(_ + 1L) /** * A schedule that always recurs, which returns inputs as outputs. */ def identity[A]: Schedule.WithState[Unit, Any, A, A] = new Schedule[Any, A, A] { override type State = Unit override final val initial: State = () override final def step(now: OffsetDateTime, in: A, state: State)(implicit trace: Trace ): ZIO[Any, Nothing, (State, A, Decision)] = ZIO.succeed((state, in, Decision.Continue(Interval.after(now)))) } /** * A schedule that always recurs, but will repeat on a linear time interval, * given by `base * n` where `n` is the number of repetitions so far. Returns * the current duration between recurrences. */ def linear(base: Duration)(implicit trace: Trace): Schedule.WithState[Long, Any, Any, Duration] = delayed[Any, Any](forever.map(i => base * (i + 1).doubleValue())) /** * A schedule that recurs one time. */ def once(implicit trace: Trace): Schedule.WithState[Long, Any, Any, Unit] = recurs(1).unit /** * A schedule spanning all time, which can be stepped only the specified * number of times before it terminates. */ def recurs(n: Long)(implicit trace: Trace): Schedule.WithState[Long, Any, Any, Long] = forever.whileOutput(_ < n) /** * A schedule spanning all time, which can be stepped only the specified * number of times before it terminates. */ def recurs(n: Int)(implicit trace: Trace): Schedule.WithState[Long, Any, Any, Long] = recurs(n.toLong) /** * Returns a schedule that recurs continuously, each repetition spaced the * specified duration from the last run. */ def spaced(duration: Duration)(implicit trace: Trace): Schedule.WithState[Long, Any, Any, Long] = forever.addDelay(_ => duration) /** * A schedule that does not recur, it just stops. */ def stop(implicit trace: Trace): Schedule.WithState[Long, Any, Any, Unit] = recurs(0).unit /** * Returns a schedule that repeats one time, producing the specified constant * value. */ def succeed[A](a: => A)(implicit trace: Trace): Schedule.WithState[Long, Any, Any, A] = forever.as(a) /** * Unfolds a schedule that repeats one time from the specified state and * iterator. */ def unfold[A](a: => A)(f: A => A): Schedule.WithState[A, Any, Any, A] = new Schedule[Any, Any, A] { override type State = A override final lazy val initial: State = a override final def step(now: OffsetDateTime, in: Any, state: State)(implicit trace: Trace ): ZIO[Any, Nothing, (State, A, Decision)] = ZIO.succeed { (f(state), state, Decision.Continue(Interval(now, OffsetDateTime.MAX))) } } //format: off /** * A schedule that divides the timeline to `interval`-long windows, and sleeps * until the nearest window boundary every time it recurs. * * For example, `windowed(10.seconds)` would produce a schedule as follows: *
   *      10s        10s        10s       10s
   * |----------|----------|----------|----------|
   * |action------|sleep---|act|-sleep|action----|
   * 
*/ def windowed(interval: Duration): Schedule.WithState[(Option[Long], Long), Any, Any, Long] = new Schedule[Any, Any, Long] { override type State = (Option[Long], Long) override final val initial: State = (None, 0L) private val nanos = interval.toNanos override final def step(now: OffsetDateTime, in: Any, state: State)(implicit trace: Trace ): ZIO[Any, Nothing, (State, Long, Decision)] = ZIO.succeed(state match { case (Some(startNanos), n) => ( (Some(startNanos), n + 1), n, Decision.Continue( Interval.after( now.plus( nanos - (Duration.fromInterval(Instant.EPOCH, now.toInstant).toNanos - startNanos) % nanos, java.time.temporal.ChronoUnit.NANOS ) ) ) ) case (None, n) => ( (Some(Duration.fromInterval(Instant.EPOCH, now.toInstant).toNanos), n + 1), n, Decision.Continue(Interval.after(now.plus(nanos, java.time.temporal.ChronoUnit.NANOS))) ) }) } /** * Cron-like schedule that recurs every specified `second` of each minute. It * triggers at zero nanosecond of the second. Producing a count of repeats: 0, * 1, 2. * * NOTE: `second` parameter is validated lazily. Must be in range 0...59. */ def secondOfMinute(second0: Int)(implicit trace: Trace): Schedule.WithState[(OffsetDateTime, Long), Any, Any, Long] = new Schedule[Any, Any, Long] { override type State = (OffsetDateTime, Long) override final val initial: State = (OffsetDateTime.MIN, 0L) override final def step(now: OffsetDateTime, in: Any, state: State)(implicit trace: Trace ): ZIO[Any, Nothing, (State, Long, Decision)] = if (second0 < 0 || 59 < second0) { ZIO.die( new IllegalArgumentException(s"Invalid argument in `secondOfMinute($second0)`. Must be in range 0...59") ) } else { val (end0, n) = state val initial = n == 0 val second00 = nextSecond(now, second0, initial) val start = beginningOfSecond(second00) val end = endOfSecond(second00) val interval = Interval(start, end) ZIO.succeed(((end, n + 1), n, Decision.Continue(interval))) } } /** * Cron-like schedule that recurs every specified `minute` of each hour. It * triggers at zero second of the minute. Producing a count of repeats: 0, 1, * 2. * * NOTE: `minute` parameter is validated lazily. Must be in range 0...59. */ def minuteOfHour(minute: Int)(implicit trace: Trace): Schedule.WithState[(OffsetDateTime, Long), Any, Any, Long] = new Schedule[Any, Any, Long] { override type State = (OffsetDateTime, Long) override final val initial: State = (OffsetDateTime.MIN, 0L) override final def step(now: OffsetDateTime, in: Any, state: State)(implicit trace: Trace ): ZIO[Any, Nothing, (State, Long, Decision)] = if (minute < 0 || 59 < minute) { ZIO.die(new IllegalArgumentException(s"Invalid argument in `minuteOfHour($minute)`. Must be in range 0...59")) } else { val (end0, n) = state val initial = n == 0 val minute0 = nextMinute(now, minute, initial) val start = beginningOfMinute(minute0) val end = endOfMinute(minute0) val interval = Interval(start, end) ZIO.succeed(((end, n + 1), n, Decision.Continue(interval))) } } /** * Cron-like schedule that recurs every specified `hour` of each day. It * triggers at zero minute of the hour. Producing a count of repeats: 0, 1, 2. * * NOTE: `hour` parameter is validated lazily. Must be in range 0...23. */ def hourOfDay(hour: Int)(implicit trace: Trace): Schedule.WithState[(OffsetDateTime, Long), Any, Any, Long] = new Schedule[Any, Any, Long] { override type State = (OffsetDateTime, Long) override final val initial: State = (OffsetDateTime.MIN, 0L) override final def step(now: OffsetDateTime, in: Any, state: State)(implicit trace: Trace ): ZIO[Any, Nothing, (State, Long, Decision)] = if (hour < 0 || 23 < hour) { ZIO.die(new IllegalArgumentException(s"Invalid argument in `hourOfDay($hour)`. Must be in range 0...23")) } else { val (end0, n) = state val initial = n == 0 val hour0 = nextHour(now, hour, initial) val start = beginningOfHour(hour0) val end = endOfHour(hour0) val interval = Interval(start, end) ZIO.succeed(((end, n + 1), n, Decision.Continue(interval))) } } /** * Cron-like schedule that recurs every specified `day` of each week. It * triggers at zero hour of the week. Producing a count of repeats: 0, 1, 2. * * NOTE: `day` parameter is validated lazily. Must be in range 1 (Monday)...7 * (Sunday). */ def dayOfWeek(day: Int)(implicit trace: Trace): Schedule.WithState[(OffsetDateTime, Long), Any, Any, Long] = new Schedule[Any, Any, Long] { override type State = (OffsetDateTime, Long) override final val initial: State = (OffsetDateTime.MIN, 0L) override final def step(now: OffsetDateTime, in: Any, state: State)(implicit trace: Trace ): ZIO[Any, Nothing, (State, Long, Decision)] = if (day < 1 || 7 < day) { ZIO.die( new IllegalArgumentException( s"Invalid argument in `dayOfWeek($day)`. Must be in range 1 (Monday)...7 (Sunday)" ) ) } else { val (end0, n) = state val initial = n == 0 val day0 = nextDay(now, day, initial) val start = beginningOfDay(day0) val end = endOfDay(day0) val interval = Interval(start, end) ZIO.succeed(((end, n + 1), n, Decision.Continue(interval))) } } /** * Cron-like schedule that recurs every specified `day` of month. Won't recur * on months containing less days than specified in `day` param. * * It triggers at zero hour of the day. Producing a count of repeats: 0, 1, 2. * * NOTE: `day` parameter is validated lazily. Must be in range 1...31. */ def dayOfMonth(day: Int)(implicit trace: Trace): Schedule.WithState[(OffsetDateTime, Long), Any, Any, Long] = new Schedule[Any, Any, Long] { override type State = (OffsetDateTime, Long) override final val initial: State = (OffsetDateTime.MIN, 0L) override final def step(now: OffsetDateTime, in: Any, state: State)(implicit trace: Trace ): ZIO[Any, Nothing, (State, Long, Decision)] = if (day < 1 || 31 < day) { ZIO.die(new IllegalArgumentException(s"Invalid argument in `dayOfMonth($day)`. Must be in range 1...31")) } else { val (end0, n) = state val initial = n == 0 val day0 = nextDayOfMonth(now, day, initial) val start = beginningOfDay(day0) val end = endOfDay(day0) val interval = Interval(start, end) ZIO.succeed(((end, n + 1), n, Decision.Continue(interval))) } } /** * An `Interval` represents an interval of time. Intervals can encompass all time, or no time * at all. */ sealed abstract class Interval private (val start: OffsetDateTime, val end: OffsetDateTime) { self => final def <(that: Interval): Boolean = (self min that) == self final def isEmpty: Boolean = start.compareTo(end) >= 0 final def intersect(that: Interval): Interval = { val start = Interval.max(self.start, that.start) val end = Interval.min(self.end, that.end) Interval(start, end) } final def max(that: Interval): Interval = { val m = self min that if (m == self) that else self } final def min(that: Interval): Interval = if (self.end.compareTo(that.start) <= 0) self else if (that.end.compareTo(self.start) <= 0) that else if (self.start.compareTo(that.start) < 0) self else if (that.start.compareTo(self.start) < 0) that else if (self.end.compareTo(that.end) <= 0) self else that final def nonEmpty: Boolean = !isEmpty final def size: Duration = Duration.fromNanos(java.time.Duration.between(start, end).toNanos) } object Interval extends Function2[OffsetDateTime, OffsetDateTime, Interval] { /** * Constructs a new interval from the two specified endpoints. If the start endpoint greater * than the end endpoint, then a zero size interval will be returned. */ def apply(start: OffsetDateTime, end: OffsetDateTime): Interval = if (start.isBefore(end) || start == end) new Interval(start, end) {} else empty def after(start: OffsetDateTime): Interval = Interval(start, OffsetDateTime.MAX) def before(end: OffsetDateTime): Interval = Interval(OffsetDateTime.MIN, end) /** * An interval of zero-width. */ val empty: Interval = Interval(OffsetDateTime.MIN, OffsetDateTime.MIN) private def min(l: OffsetDateTime, r: OffsetDateTime): OffsetDateTime = if (l.compareTo(r) <= 0) l else r private def max(l: OffsetDateTime, r: OffsetDateTime): OffsetDateTime = if (l.compareTo(r) >= 0) l else r } /** * Intervals represents a set of intervals. */ sealed abstract case class Intervals private (intervals: List[Interval]) { self => /** * A symbolic alias for `intersect`. */ def &&(that: Intervals): Intervals = self.intersect(that) /** * A symbolic alias for `union`. */ def ||(that: Intervals): Intervals = self.union(that) /** * The union of this set of intervals and the specified set of intervals */ def union(that: Intervals): Intervals = { @tailrec def loop(left: List[Interval], right: List[Interval], interval: Interval, acc: List[Interval]): Intervals = (left, right) match { case (Nil, Nil) => Intervals((interval :: acc).reverse) case (Nil, right :: rights) => if (interval.end.isBefore(right.start)) loop(Nil, rights, right, interval :: acc) else loop(Nil, rights, Interval(interval.start, right.end), acc) case (left :: lefts, Nil) => if (interval.end.isBefore(left.start)) loop(lefts, Nil, left, interval :: acc) else loop(lefts, Nil, Interval(interval.start, left.end), acc) case (left :: lefts, right :: rights) if left.start.isBefore(right.start) => if (interval.end.isBefore(left.start)) loop(lefts, right :: rights, left, interval :: acc) else loop(lefts, right :: rights, Interval(interval.start, left.end), acc) case (left :: lefts, right :: rights) => if (interval.end.isBefore(right.start)) loop(left :: lefts, rights, right, interval :: acc) else loop(left :: lefts, rights, Interval(interval.start, right.end), acc) } (self.intervals, that.intervals) match { case (left, Nil) => Intervals(left) case (Nil, right) => Intervals(right) case (left :: lefts, right :: rights) if left.start.isBefore(right.start) => loop(lefts, right :: rights, left, List.empty) case (left :: lefts, right :: rights) => loop(left :: lefts, rights, right, List.empty) } } /** * The intersection of this set of intervals and the specified set of * intervals. */ def intersect(that: Intervals): Intervals = { @tailrec def loop(left: List[Interval], right: List[Interval], acc: List[Interval]): Intervals = (left, right) match { case (Nil, _) => Intervals(acc.reverse) case (_, Nil) => Intervals(acc.reverse) case (left :: lefts, right :: rights) => val interval = left.intersect(right) val intervals = if (interval.isEmpty) acc else interval :: acc if (left < right) loop(lefts, right :: rights, intervals) else loop(left :: lefts, rights, intervals) } loop(self.intervals, that.intervals, List.empty) } /** * The start of the earliest interval in this set. */ def start: OffsetDateTime = intervals.headOption.getOrElse(Interval.empty).start /** * The end of the latest interval in this set. */ def end: OffsetDateTime = intervals.headOption.getOrElse(Interval.empty).end /** * Whether the start of this set of intervals is before the start of the * specified set of intervals */ def <(that: Intervals): Boolean = self.start.isBefore(that.start) /** * Whether this set of intervals is empty. */ def nonEmpty: Boolean = intervals.nonEmpty /** * The set of intervals that starts last. */ def max(that: Intervals): Intervals = if (self < that) that else self } object Intervals { /** * Constructs a set of intervals from the specified intervals. */ def apply(intervals: Interval*): Intervals = intervals.foldLeft(Intervals.empty) { (intervals, interval) => intervals.union(Intervals(List(interval))) } /** * The empty set of intervals. */ val empty: Intervals = Intervals(List.empty) private def apply(intervals: List[Interval]): Intervals = new Intervals(intervals) {} } def minOffsetDateTime(l: OffsetDateTime, r: OffsetDateTime): OffsetDateTime = if (l.compareTo(r) <= 0) l else r def maxOffsetDateTime(l: OffsetDateTime, r: OffsetDateTime): OffsetDateTime = if (l.compareTo(r) >= 0) l else r type WithState[State0, -Env, -In0, +Out0] = Schedule[Env, In0, Out0] { type State = State0 } final case class Driver[+State, -Env, -In, +Out]( next: In => ZIO[Env, None.type, Out], last: IO[NoSuchElementException, Out], reset: UIO[Unit], state: UIO[State] ) sealed trait Decision object Decision { final case class Continue(interval: Intervals) extends Decision object Continue { def apply(interval: Interval): Decision = Continue(Intervals(interval)) } case object Done extends Decision } private def nextDay(now: OffsetDateTime, day: Int, initial: Boolean): OffsetDateTime = { val temporalAdjuster = if (initial) TemporalAdjusters.nextOrSame(java.time.DayOfWeek.of(day)) else TemporalAdjusters.next(java.time.DayOfWeek.of(day)) now.`with`(temporalAdjuster) } private def nextDayOfMonth(now: OffsetDateTime, day: Int, initial: Boolean): OffsetDateTime = if (now.getDayOfMonth == day && initial) now else if (now.getDayOfMonth < day) now.`with`(DAY_OF_MONTH, day.toLong) else findNextMonth(now, day, 1) private def findNextMonth(now: OffsetDateTime, day: Int, months: Int): OffsetDateTime = if (now.`with`(DAY_OF_MONTH, day.toLong).plusMonths(months.toLong).getDayOfMonth == day) now.`with`(DAY_OF_MONTH, day.toLong).plusMonths(months.toLong) else findNextMonth(now, day, months + 1) private def nextHour(now: OffsetDateTime, hour: Int, initial: Boolean): OffsetDateTime = if (now.getHour == hour && initial) now else if (now.getHour < hour) now.`with`(HOUR_OF_DAY, hour.toLong) else now.`with`(HOUR_OF_DAY, hour.toLong).plusDays(1) private def nextMinute(now: OffsetDateTime, minute: Int, initial: Boolean): OffsetDateTime = if (now.getMinute == minute && initial) now else if (now.getMinute < minute) now.`with`(MINUTE_OF_HOUR, minute.toLong) else now.`with`(MINUTE_OF_HOUR, minute.toLong).plusHours(1L) private def nextSecond(now: OffsetDateTime, second: Int, initial: Boolean): OffsetDateTime = if (now.getSecond == second && initial) now else if (now.getSecond < second) now.`with`(SECOND_OF_MINUTE, second.toLong) else now.`with`(SECOND_OF_MINUTE, second.toLong).plusMinutes(1L) private def beginningOfDay(now: OffsetDateTime): OffsetDateTime = OffsetDateTime.of(now.getYear, now.getMonth.getValue, now.getDayOfMonth, 0, 0, 0, 0, now.getOffset) private def endOfDay(now: OffsetDateTime): OffsetDateTime = beginningOfDay(now).plusDays(1L) private def beginningOfHour(now: OffsetDateTime): OffsetDateTime = OffsetDateTime.of(now.getYear, now.getMonth.getValue, now.getDayOfMonth, now.getHour, 0, 0, 0, now.getOffset) private def endOfHour(now: OffsetDateTime): OffsetDateTime = beginningOfHour(now).plusHours(1L) private def beginningOfMinute(now: OffsetDateTime): OffsetDateTime = OffsetDateTime.of( now.getYear, now.getMonth.getValue, now.getDayOfMonth, now.getHour, now.getMinute, 0, 0, now.getOffset ) private def endOfMinute(now: OffsetDateTime): OffsetDateTime = beginningOfMinute(now).plusMinutes(1L) private def beginningOfSecond(now: OffsetDateTime): OffsetDateTime = OffsetDateTime.of( now.getYear, now.getMonth.getValue, now.getDayOfMonth, now.getHour, now.getMinute, now.getSecond, 0, now.getOffset ) private def endOfSecond(now: OffsetDateTime): OffsetDateTime = beginningOfSecond(now).plusSeconds(1L) }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy