From aadf9b09f91c907e544aa567f4f67d3905081494 Mon Sep 17 00:00:00 2001 From: Joseph Silber Date: Sun, 14 Apr 2024 14:37:40 -0400 Subject: [PATCH] Add `throttle` method to `LazyCollection` --- src/Illuminate/Collections/LazyCollection.php | 50 +++++++++++++++++ .../SupportLazyCollectionIsLazyTest.php | 20 +++++++ tests/Support/SupportLazyCollectionTest.php | 55 +++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index 2debb010abda..95fd218b36f0 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -1570,6 +1570,28 @@ public function tapEach(callable $callback) }); } + /** + * Throttle the values, releasing them at most once per the given seconds. + * + * @return static + */ + public function throttle(float $seconds) + { + return new static(function () use ($seconds) { + $microseconds = $seconds * 1_000_000; + + foreach ($this as $key => $value) { + $fetchedAt = $this->preciseNow(); + + yield $key => $value; + + $sleep = $microseconds - ($this->preciseNow() - $fetchedAt); + + $this->usleep((int) $sleep); + } + }); + } + /** * Flatten a multi-dimensional associative array with dots. * @@ -1781,4 +1803,32 @@ protected function now() ? Carbon::now()->timestamp : time(); } + + /** + * Get the precise current time. + * + * @return float + */ + protected function preciseNow() + { + return class_exists(Carbon::class) + ? Carbon::now()->getPreciseTimestamp() + : microtime(true) * 1_000_000; + } + + /** + * Sleep for the given amount of microseconds. + * + * @return void + */ + protected function usleep(int $microseconds) + { + if ($microseconds <= 0) { + return; + } + + class_exists(Sleep::class) + ? Sleep::usleep($microseconds) + : usleep($microseconds); + } } diff --git a/tests/Support/SupportLazyCollectionIsLazyTest.php b/tests/Support/SupportLazyCollectionIsLazyTest.php index 3b014f57edea..ead078cf5341 100644 --- a/tests/Support/SupportLazyCollectionIsLazyTest.php +++ b/tests/Support/SupportLazyCollectionIsLazyTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\ItemNotFoundException; use Illuminate\Support\LazyCollection; use Illuminate\Support\MultipleItemsFoundException; +use Illuminate\Support\Sleep; use Mockery as m; use PHPUnit\Framework\TestCase; use stdClass; @@ -1370,6 +1371,25 @@ public function testTapEachIsLazy() }); } + public function testThrottleIsLazy() + { + Sleep::fake(); + + $this->assertDoesNotEnumerate(function ($collection) { + $collection->throttle(10); + }); + + $this->assertEnumerates(5, function ($collection) { + $collection->throttle(10)->take(5)->all(); + }); + + $this->assertEnumeratesOnce(function ($collection) { + $collection->throttle(10)->all(); + }); + + Sleep::fake(false); + } + public function testTimesIsLazy() { $data = LazyCollection::times(INF); diff --git a/tests/Support/SupportLazyCollectionTest.php b/tests/Support/SupportLazyCollectionTest.php index 1db8c2ead02d..5865c09e888b 100644 --- a/tests/Support/SupportLazyCollectionTest.php +++ b/tests/Support/SupportLazyCollectionTest.php @@ -2,9 +2,12 @@ namespace Illuminate\Tests\Support; +use Carbon\CarbonInterval as Duration; +use Illuminate\Foundation\Testing\Wormhole; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\LazyCollection; +use Illuminate\Support\Sleep; use InvalidArgumentException; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -223,6 +226,58 @@ public function testTapEach() $this->assertSame([1, 2, 3, 4, 5], $tapped); } + public function testThrottle() + { + Sleep::fake(); + + $data = LazyCollection::times(3) + ->throttle(2) + ->all(); + + Sleep::assertSlept(function (Duration $duration) { + $this->assertEqualsWithDelta( + 2_000_000, $duration->totalMicroseconds, 1_000 + ); + + return true; + }, times: 3); + + $this->assertSame([1, 2, 3], $data); + + Sleep::fake(false); + } + + public function testThrottleAccountsForTimePassed() + { + Sleep::fake(); + Carbon::setTestNow(now()); + + $data = LazyCollection::times(3) + ->throttle(3) + ->tapEach(function ($value, $index) { + if ($index == 1) { + // Travel in time... + (new Wormhole(1))->second(); + } + }) + ->all(); + + Sleep::assertSlept(function (Duration $duration, int $index) { + $expectation = $index == 1 ? 2_000_000 : 3_000_000; + + $this->assertEqualsWithDelta( + $expectation, $duration->totalMicroseconds, 1_000 + ); + + return true; + }, times: 3); + + $this->assertSame([1, 2, 3], $data); + + Sleep::fake(false); + Carbon::setTestNow(); + } + public function testUniqueDoubleEnumeration() { $data = LazyCollection::times(2)->unique();