puff/async

Puff async fiber runtime with tasks, futures, timers, channels, wait groups, Fiber-local context, deferred callbacks, and non-blocking stream watchers.

Maintainers

Package info

github.com/php-puff/async

pkg:composer/puff/async

Transparency log

Statistics

Installs: 0

Dependents: 1

Suggesters: 1

Stars: 0

Open Issues: 0

dev-main 2026-08-10 06:22 UTC

This package is auto-updated.

Last update: 2026-08-10 06:23:30 UTC


README

Pure PHP 8.2+ Fiber runtime with tasks, futures, timers, channels, wait groups, Fiber-local context, deferred callbacks, and non-blocking stream watchers.

composer require puff/async

Run concurrent tasks

puff_run() starts a root Fiber. puff_async() schedules child tasks, puff_delay() suspends only the current Fiber, and puff_await() returns a task result or rethrows its exception.

$result = puff_run(function (): array {
    $first = puff_async(static function (): string {
        puff_delay(0.05);
        return 'first';
    });
    $second = puff_async(static function (): string {
        puff_delay(0.03);
        return 'second';
    });

    return [puff_await($first), puff_await($second)];
});

Tasks expose their Future for completion callbacks:

$task = puff_async(static fn (): int => 42);
$task->future()->onComplete(
    static function (?Throwable $error, mixed $value): void {
        // Inspect the result without blocking.
    },
);

Deferred callbacks

puff_defer() runs cleanup callbacks in last-in, first-out order when the current Fiber finishes, including when it throws.

puff_run(function (): void {
    puff_defer(static fn () => fclose($first));
    puff_defer(static fn () => fclose($second));

    // $second closes before $first.
});

Channels

An unbuffered channel synchronizes a producer and consumer. Pass a positive capacity to create a buffered channel.

$message = puff_run(function (): mixed {
    $channel = puff_channel();

    puff_async(static function () use ($channel): void {
        puff_delay(0.01);
        $channel->push('hello');
    });

    return $channel->pop(1.0);
});

push() and pop() return false after closure or timeout where applicable. Use close(), isClosed(), length(), and capacity() to inspect channel state.

Wait groups

A wait group blocks the current Fiber until every registered worker calls done() or the optional timeout expires.

$completed = puff_run(function (): array {
    $group = puff_wait_group(3);
    $completed = [];

    foreach ([30, 10, 20] as $delayMs) {
        puff_async(static function () use ($group, &$completed, $delayMs): void {
            puff_defer(static fn () => $group->done());
            puff_delay($delayMs / 1_000);
            $completed[] = $delayMs;
        });
    }

    $group->wait(1.0);
    return $completed;
});

Fiber context and identity

Context values are isolated per Fiber and cleared automatically. Runtime helpers expose the current task ID, parent ID, and pending task count.

  • puff_id() returns the current runtime-managed Task ID, or -1 outside a managed Task.
  • puff_parent_id() returns the parent Task ID, -1 for the root Task, or false outside a Fiber.

The parent ID links a child Task to the Task that created it:

$identity = puff_run(function (): array {
    $rootId = puff_id();
    $child = puff_async(static fn (): array => [
        'id' => puff_id(),
        'parent_id' => puff_parent_id(),
    ]);

    return [
        'root_id' => $rootId,
        'child' => puff_await($child),
    ];
});

Fiber-local context can be combined with the identity helpers:

use Puff\Async\Context;

$metadata = puff_run(function (): array {
    Context::set('request-id', 'req-123');

    return [
        'request_id' => Context::get('request-id'),
        'fiber_id' => puff_id(),
        'parent_id' => puff_parent_id(),
        'task_count' => puff_task_count(),
    ];
});

Use Runtime::onCleanup() to register additional cleanup listeners.

Event loop

The shared event loop supports queued callbacks, one-shot timers, and readable or writable stream watchers.

use Puff\Async\EventLoop;

$loop = EventLoop::get();
$timer = $loop->delay(0.1, static fn () => print "timer fired\n");
$watcher = $loop->onReadable($stream, static function ($stream) use ($loop): void {
    echo fread($stream, 8192);
    $loop->stop();
});
$loop->run();
$loop->cancel($timer);
$loop->cancel($watcher);

runUntil() also works on the main PHP stack. Once its condition becomes true, the loop returns before waiting on unrelated later timers.

Short aliases

The package provides equivalent concise helpers:

Full helper Alias
puff_run() run()
puff_async() co(), go()
puff_await() await()
puff_delay() delay()
puff_defer() defer()
puff_channel() channel(), chan()
puff_wait_group() WaitGroup(), wg()