From 1a24f568e5dd72c80bdc7083913ee79c666c9d52 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 17 Jan 2026 17:31:43 +0100 Subject: [PATCH 1/3] copy innmind/time-warp --- README.md | 30 +++++++++++++ proofs/logger.php | 22 ++++++++++ proofs/usleep.php | 32 ++++++++++++++ proofs/via.php | 29 ++++++++++++ src/Async/Resumable.php | 44 +++++++++++++++++++ src/Async/Suspended.php | 88 +++++++++++++++++++++++++++++++++++++ src/Halt.php | 68 ++++++++++++++++++++++++++++ src/Halt/Async.php | 41 +++++++++++++++++ src/Halt/Implementation.php | 24 ++++++++++ src/Halt/Logger.php | 45 +++++++++++++++++++ src/Halt/Usleep.php | 52 ++++++++++++++++++++++ src/Halt/Via.php | 39 ++++++++++++++++ 12 files changed, 514 insertions(+) create mode 100644 proofs/logger.php create mode 100644 proofs/usleep.php create mode 100644 proofs/via.php create mode 100644 src/Async/Resumable.php create mode 100644 src/Async/Suspended.php create mode 100644 src/Halt.php create mode 100644 src/Halt/Async.php create mode 100644 src/Halt/Implementation.php create mode 100644 src/Halt/Logger.php create mode 100644 src/Halt/Usleep.php create mode 100644 src/Halt/Via.php 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/logger.php b/proofs/logger.php new file mode 100644 index 0000000..801aa1a --- /dev/null +++ b/proofs/logger.php @@ -0,0 +1,22 @@ + $assert + ->object( + Halt::logger(Halt::new(), new NullLogger)( + Period::millisecond(100), + )->unwrap(), + ) + ->instance(SideEffect::class), + ); +}; diff --git a/proofs/usleep.php b/proofs/usleep.php new file mode 100644 index 0000000..f0e576f --- /dev/null +++ b/proofs/usleep.php @@ -0,0 +1,32 @@ + $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, + ), + ); +}; diff --git a/proofs/via.php b/proofs/via.php new file mode 100644 index 0000000..e0a4d52 --- /dev/null +++ b/proofs/via.php @@ -0,0 +1,29 @@ +same($period, $in); + + return $expected; + }); + + $assert->same($expected, $halt($period)); + }, + ); +}; diff --git a/src/Async/Resumable.php b/src/Async/Resumable.php new file mode 100644 index 0000000..8e2aabd --- /dev/null +++ b/src/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/Async/Suspended.php b/src/Async/Suspended.php new file mode 100644 index 0000000..a36a725 --- /dev/null +++ b/src/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.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..66b2f5f --- /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/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)); + } +} From 07d70e2878a933801ed5958d606e4e5b9a6dd4ef Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 17 Jan 2026 17:33:11 +0100 Subject: [PATCH 2/3] regroup halt proofs --- proofs/halt.php | 63 +++++++++++++++++++++++++++++++++++++++++++++++ proofs/logger.php | 22 ----------------- proofs/usleep.php | 32 ------------------------ proofs/via.php | 29 ---------------------- 4 files changed, 63 insertions(+), 83 deletions(-) create mode 100644 proofs/halt.php delete mode 100644 proofs/logger.php delete mode 100644 proofs/usleep.php delete mode 100644 proofs/via.php 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/proofs/logger.php b/proofs/logger.php deleted file mode 100644 index 801aa1a..0000000 --- a/proofs/logger.php +++ /dev/null @@ -1,22 +0,0 @@ - $assert - ->object( - Halt::logger(Halt::new(), new NullLogger)( - Period::millisecond(100), - )->unwrap(), - ) - ->instance(SideEffect::class), - ); -}; diff --git a/proofs/usleep.php b/proofs/usleep.php deleted file mode 100644 index f0e576f..0000000 --- a/proofs/usleep.php +++ /dev/null @@ -1,32 +0,0 @@ - $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, - ), - ); -}; diff --git a/proofs/via.php b/proofs/via.php deleted file mode 100644 index e0a4d52..0000000 --- a/proofs/via.php +++ /dev/null @@ -1,29 +0,0 @@ -same($period, $in); - - return $expected; - }); - - $assert->same($expected, $halt($period)); - }, - ); -}; From bc58686986ea8e3e9ebf8f7e68ffe9c31198683f Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 17 Jan 2026 17:36:07 +0100 Subject: [PATCH 3/3] move async classes to the Halt namespace --- src/Halt/Async.php | 4 ++-- src/{ => Halt}/Async/Resumable.php | 2 +- src/{ => Halt}/Async/Suspended.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/{ => Halt}/Async/Resumable.php (94%) rename src/{ => Halt}/Async/Suspended.php (98%) diff --git a/src/Halt/Async.php b/src/Halt/Async.php index 66b2f5f..b6d17f4 100644 --- a/src/Halt/Async.php +++ b/src/Halt/Async.php @@ -4,8 +4,8 @@ namespace Innmind\Time\Halt; use Innmind\Time\{ - Async\Suspended, - Async\Resumable, + Halt\Async\Suspended, + Halt\Async\Resumable, Clock, Period, }; diff --git a/src/Async/Resumable.php b/src/Halt/Async/Resumable.php similarity index 94% rename from src/Async/Resumable.php rename to src/Halt/Async/Resumable.php index 8e2aabd..d687fee 100644 --- a/src/Async/Resumable.php +++ b/src/Halt/Async/Resumable.php @@ -1,7 +1,7 @@