diff --git a/README.md b/README.md
index 6e807d0..e78359c 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,8 @@ composer require innmind/time
## Usage
+### Accessing time
+
```php
use Innmind\Time\{
Clock,
@@ -38,6 +40,34 @@ Here we reference 2 points in time, the first is the exact moment we call `now`
The method `at()` accepts any string that is allowed by `\DateTimeImmutable`.
+### Halt process
+
+```php
+use Innmind\Time\{
+ Halt
+ Period,
+};
+
+function yourApp(Halt $halt): void
+{
+ // do something
+ $halt(Period::minute(42))->unwrap();
+ // do some more
+}
+
+yourApp(Halt::new());
+```
+
+This example will halt your program for 42 minutes.
+
+#### Logging
+
+```php
+use Innmind\Time\Halt;
+use Psr\Log\LoggerInterface;
+
+$halt = Halt::logger($halt, /** an instance of LoggerInterface */);
+
## Documentation
Full documentation is available at .
diff --git a/proofs/halt.php b/proofs/halt.php
new file mode 100644
index 0000000..1285eb8
--- /dev/null
+++ b/proofs/halt.php
@@ -0,0 +1,63 @@
+ $assert
+ ->time(static function() use ($assert) {
+ $assert
+ ->object(
+ Halt::new()(Period::millisecond(500))->unwrap(),
+ )
+ ->instance(SideEffect::class);
+ })
+ ->inMoreThan()
+ ->milliseconds(500),
+ );
+
+ yield test(
+ 'Prevent converting months',
+ static fn($assert) => $assert->throws(
+ static fn() => Halt::new()(Period::month(1))->unwrap(),
+ LogicException::class,
+ ),
+ );
+
+ yield test(
+ 'Halt::logger()',
+ static fn($assert) => $assert
+ ->object(
+ Halt::logger(Halt::new(), new NullLogger)(
+ Period::millisecond(100),
+ )->unwrap(),
+ )
+ ->instance(SideEffect::class),
+ );
+
+ yield test(
+ 'Halt::via()',
+ static function($assert) {
+ $period = Period::millisecond(500);
+ $expected = Attempt::result(SideEffect::identity);
+
+ $halt = Halt::via(static function($in) use ($assert, $period, $expected) {
+ $assert->same($period, $in);
+
+ return $expected;
+ });
+
+ $assert->same($expected, $halt($period));
+ },
+ );
+};
diff --git a/src/Halt.php b/src/Halt.php
new file mode 100644
index 0000000..32ede9d
--- /dev/null
+++ b/src/Halt.php
@@ -0,0 +1,68 @@
+
+ */
+ #[\NoDiscard]
+ public function __invoke(Period $period): Attempt
+ {
+ return ($this->implementation)($period);
+ }
+
+ #[\NoDiscard]
+ public static function new(): self
+ {
+ return new self(Usleep::new());
+ }
+
+ #[\NoDiscard]
+ public static function logger(self $self, LoggerInterface $logger): self
+ {
+ return new self(Logger::psr($self->implementation, $logger));
+ }
+
+ /**
+ * @internal
+ */
+ #[\NoDiscard]
+ public static function async(Clock $clock): self
+ {
+ return new self(Async::of($clock));
+ }
+
+ /**
+ * @internal
+ *
+ * @param callable(Period): Attempt $via
+ */
+ #[\NoDiscard]
+ public static function via(callable $via): self
+ {
+ return new self(Via::of($via));
+ }
+}
diff --git a/src/Halt/Async.php b/src/Halt/Async.php
new file mode 100644
index 0000000..b6d17f4
--- /dev/null
+++ b/src/Halt/Async.php
@@ -0,0 +1,41 @@
+clock->now(),
+ $period,
+ ));
+
+ return $return->unwrap();
+ }
+
+ #[\NoDiscard]
+ public static function of(Clock $clock): self
+ {
+ return new self($clock);
+ }
+}
diff --git a/src/Halt/Async/Resumable.php b/src/Halt/Async/Resumable.php
new file mode 100644
index 0000000..d687fee
--- /dev/null
+++ b/src/Halt/Async/Resumable.php
@@ -0,0 +1,44 @@
+ $result
+ */
+ private function __construct(
+ private Attempt $result,
+ ) {
+ }
+
+ /**
+ * @psalm-pure
+ *
+ * @param Attempt $result
+ */
+ #[\NoDiscard]
+ public static function of(Attempt $result): self
+ {
+ return new self($result);
+ }
+
+ /**
+ * @return Attempt
+ */
+ #[\NoDiscard]
+ public function unwrap(): Attempt
+ {
+ return $this->result;
+ }
+}
diff --git a/src/Halt/Async/Suspended.php b/src/Halt/Async/Suspended.php
new file mode 100644
index 0000000..2e1ab52
--- /dev/null
+++ b/src/Halt/Async/Suspended.php
@@ -0,0 +1,88 @@
+ $result
+ */
+ #[\NoDiscard]
+ public function next(
+ Clock $clock,
+ Attempt $result,
+ ): self|Resumable {
+ $error = $result->match(
+ static fn() => false,
+ static fn() => true,
+ );
+
+ if ($error) {
+ // The drawback of resuming with the error is that an error occuring
+ // due to another Fiber will affect all of them as for now there is
+ // no way to distinguish due to which Fiber the halt failed.
+ // This will need real world experience to know if this approach is
+ // ok or not.
+ return Resumable::of($result);
+ }
+
+ $now = $clock->now();
+ $expectedEnd = $this->at->goForward($this->period);
+
+ if ($now->aheadOf($expectedEnd)) {
+ return Resumable::of($result);
+ }
+
+ return new self(
+ $this->at,
+ $this->period,
+ $expectedEnd
+ ->elapsedSince($now)
+ ->asPeriod(),
+ );
+ }
+
+ /**
+ * @psalm-mutation-free
+ */
+ #[\NoDiscard]
+ public function period(): Period
+ {
+ return $this->remaining;
+ }
+}
diff --git a/src/Halt/Implementation.php b/src/Halt/Implementation.php
new file mode 100644
index 0000000..8422fb0
--- /dev/null
+++ b/src/Halt/Implementation.php
@@ -0,0 +1,24 @@
+
+ */
+ #[\NoDiscard]
+ public function __invoke(Period $period): Attempt;
+}
diff --git a/src/Halt/Logger.php b/src/Halt/Logger.php
new file mode 100644
index 0000000..ca9b30d
--- /dev/null
+++ b/src/Halt/Logger.php
@@ -0,0 +1,45 @@
+halt = $halt;
+ $this->logger = $logger;
+ }
+
+ #[\Override]
+ public function __invoke(Period $period): Attempt
+ {
+ $this->logger->debug('Halting current process...', ['period' => [
+ 'years' => $period->years(),
+ 'months' => $period->months(),
+ 'days' => $period->days(),
+ 'hours' => $period->hours(),
+ 'minutes' => $period->minutes(),
+ 'seconds' => $period->seconds(),
+ 'milliseconds' => $period->milliseconds(),
+ ]]);
+
+ return ($this->halt)($period);
+ }
+
+ #[\NoDiscard]
+ public static function psr(Implementation $halt, LoggerInterface $logger): self
+ {
+ return new self($halt, $logger);
+ }
+}
diff --git a/src/Halt/Usleep.php b/src/Halt/Usleep.php
new file mode 100644
index 0000000..654ee1a
--- /dev/null
+++ b/src/Halt/Usleep.php
@@ -0,0 +1,52 @@
+months() !== 0) {
+ // a month is not constant
+ return Attempt::error(new \LogicException('Months can not be converted to milliseconds'));
+ }
+
+ /** @psalm-suppress ArgumentTypeCoercion todo update types to fix this error */
+ \usleep($this->convert($period) * 1000);
+
+ return Attempt::result(SideEffect::identity());
+ }
+
+ #[\NoDiscard]
+ public static function new(): self
+ {
+ return new self;
+ }
+
+ private function convert(Period $period): int
+ {
+ $second = 1000;
+ $minute = 60 * $second;
+ $hour = 60 * $minute;
+ $day = 24 * $hour;
+ $year = 365 * $day;
+
+ return $period->years() * $year +
+ $period->days() * $day +
+ $period->hours() * $hour +
+ $period->minutes() * $minute +
+ $period->seconds() * $second +
+ $period->milliseconds();
+ }
+}
diff --git a/src/Halt/Via.php b/src/Halt/Via.php
new file mode 100644
index 0000000..68a5ed6
--- /dev/null
+++ b/src/Halt/Via.php
@@ -0,0 +1,39 @@
+ $via
+ */
+ private function __construct(
+ private \Closure $via,
+ ) {
+ }
+
+ #[\Override]
+ public function __invoke(Period $period): Attempt
+ {
+ return ($this->via)($period);
+ }
+
+ /**
+ * @param callable(Period): Attempt $via
+ */
+ #[\NoDiscard]
+ public static function of(callable $via): self
+ {
+ return new self(\Closure::fromCallable($via));
+ }
+}