Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ticking Promise Laravel Package

wyrihaximus/ticking-promise

A ReactPHP promise that fires a callback on a recurring interval, making it simple to schedule periodic tasks inside asynchronous PHP applications without manual event-loop timer wiring.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require wyrihaximus/ticking-promise

The package does one thing: it turns "run on the next event loop tick" into a react/promise PromiseInterface you can then(), await(), all() or return from a callback. Instead of Loop::futureTick() (which returns void and gives you no handle on completion), you get a value that participates in promise chains.

Where to look first: read src/ in the repository. The whole surface is a single small class, so confirm the factory signature (notably whether it takes an explicit LoopInterface or uses the global loop from react/event-loop) before writing code against it. There is no config, no service provider, no facades.

First use case: cooperative yielding inside a long synchronous loop so timers, streams and other coroutines get a chance to run.

use WyriHaximus\TickingPromise\TickingPromise;
use function React\Async\async;
use function React\Async\await;

$import = async(function (array $rows): void {
    foreach ($rows as $index => $row) {
        $this->handle($row);              // CPU work, blocks the loop

        if ($index % 100 === 0) {
            await(TickingPromise::create()); // hand the loop back for one tick
        }
    }
});

$import($rows);
Loop::run();                              // nothing happens without a running loop

If you prefer no coroutine layer, the same thing in plain promise style:

TickingPromise::create()->then(function (): void {
    $this->flushBuffers();
});

Implementation Patterns

Chunked / batched processing with backpressure. Yield a tick every N items, not every item. Every tick is a full loop iteration: cheap, but not free. Tune N by measuring.

Ordering work after queued callbacks. Because the promise resolves through the tick queue, anything already queued with futureTick() (and I/O plus timers dispatched in the same iteration) is processed before your then() handler runs. That makes the package a clean "after the current batch, before the next" primitive.

Sequencing steps without nesting. Chain ticks instead of nesting futureTick() closures:

TickingPromise::create()
    ->then(fn () => $this->stepOne())
    ->then(fn () => TickingPromise::create())
    ->then(fn () => $this->stepTwo())
    ->then(null, fn (Throwable $e) => $this->logger->error('pipeline failed', ['e' => $e]));

Fan-out with periodic breathing room. When spawning a large set of promises, interleave ticks so stream reads and timers are not starved by a single burst of then() callbacks:

$pending = [];
foreach ($urls as $url) {
    $pending[] = $this->http->get($url);
    if (count($pending) % 50 === 0) {
        $pending[] = TickingPromise::create();
    }
}
await(all($pending));

Deterministic tests. In wyrihaximus/async-test-utilities based tests, awaiting a tick is a reliable way to flush pending loop callbacks before asserting, instead of usleep() or ad hoc loop manipulation. Extend WyriHaximus\AsyncTestUtilities\AsyncTestCase and let the base class own the loop lifecycle.

Composition with real delays. A tick is zero elapsed time. For backoff or polling intervals combine it with react/promise-timer: tick to yield, sleep() to actually wait.

Deferring recursion. Recursive promise chains that re-enter immediately can grow the stack and monopolise an iteration. Insert a tick at each recursion step to flatten it into separate loop iterations.

Gotchas and Tips

  • No running loop, no resolution. A tick promise is only settled while Loop::run() (or an await() that drives the loop) is active. In a CLI command or queue worker that never starts the loop, the promise stays pending forever and looks like a hang.
  • Loop identity matters. If the package schedules on the global loop but your application runs its own LoopInterface instance (or vice versa), the callback is queued on a loop that is not spinning. Verify which loop is used, and if the factory accepts one, always pass the same instance you run().
  • Ticks are not delays. Do not use them to wait for wall-clock time or for an external system to become ready. Use timers or an actual promise from the operation.
  • An endless tick chain keeps the process alive. ReactPHP loops exit when there is nothing left: no streams, no timers, no queued ticks. A self-perpetuating tick chain means the loop never becomes idle and your script never terminates. Bound the chain.
  • Never call Loop::stop() to break out of tick-driven work. It tears down everything else using the loop mid-flight and makes tests flaky. Let callbacks return and the loop drain naturally.
  • Silent rejections. An exception thrown inside a then() handler rejects the derived promise with nobody listening. Always attach a rejection handler at the end of a tick-started chain, or route it through wyrihaximus/psr-3-callable-throwable-logger.
  • Busy-spin smell. A tick used to poll a condition in a tight loop burns a full CPU core while producing nothing. If a profile shows a hot loop iteration, replace the poll with a real event source.
  • Tiny surface, no extension points. With a handful of stars and zero dependents, do not expect helpers for "N ticks" or cancellation. Compose what you need (a small recursive helper, or a wrapper class) rather than forking; and pin the version in composer.json, since a minor release is the entire API for a package this size.
  • Cancellation does not exist here. Once scheduled, the tick fires. If you need cancellable deferred work, keep your own Deferred and ignore the result after cancelling.
  • Check the PHP and react/promise major in composer.json. On promise v3 you get catch() and finally() directly on the chain; on v2 you write then(null, $onRejected). Mixing majors across a dependency graph is the usual source of "method not found" in async Laravel code.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk