Shared async utilities for SugarCraft — cancellation tokens, subscriptions, and AsyncOps helpers built on ReactPHP.
candy-async provides the foundational async vocabulary used across the SugarCraft TUI ecosystem:
- Cancellation tokens —
CancellationSource/CancellationToken/Cancellablefor coordinated cancellation across async operations - Subscriptions —
Subscriptioninterface andSubscriptions::compose()for managing TEA-style subscription lifecycles - AsyncOps — static helpers for
withTimeout,retry,debounce, andthrottleoperations
use SugarCraft\Async\{AsyncOps, CancellationSource, Subscriptions};
$source = CancellationSource::new();
// Attach a cancellation callback
$source->token()->onCancel(fn() => echo "Cancelled!\n");
$source->cancel(); // prints "Cancelled!"
// Timeout wrapper
$loop = \React\EventLoop\Loop::get();
$promise = AsyncOps::withTimeout($loop, $somePromise, 5.0);
// Retry with backoff
$promise = AsyncOps::retry(
fn() => $httpClient->request('GET', 'https://example.com'),
attempts: 3,
baseBackoffSeconds: 0.5,
);
// Debounce rapid calls
$debounced = AsyncOps::debounce(fn($input) => process($input), 0.15);
$debounced('a');
$debounced('b');
$debounced('c'); // only this fires, 150ms after last call- PHP 8.3+
react/event-loop: ^1.6react/promise: ^3.3
composer require sugarcraft/candy-asyncCancellationSource owns the mutable cancellation flag. It exposes a read-only CancellationToken to consumers. When cancel() is called:
- The flag is flipped (idempotent)
- All callbacks registered via
onCancel()fire in registration order, exactly once
This pattern allows cancellation to propagate without the consumer being able to trigger it themselves.
Subscription is the disposal handle returned by subscribe-style APIs. Subscriptions::compose() lets multiple subscriptions be disposed atomically:
$composite = Subscriptions::compose($sub1, $sub2, $sub3);
$composite->unsubscribe(); // disposes all threeAll helpers are pure functions that do not retain state. They work via Promise plumbing and LoopInterface timers:
withTimeout— wraps a promise; rejects withTimeoutExceptionafter N secondsretry— retries a failed operation up to N times with exponential backoffdebounce— only the last call within the window fires, after silencethrottle— fires at most once per interval, ignoring excess calls
MIT