sugarcraft / candy-async
Shared async utilities for SugarCraft — cancellation tokens, subscriptions, and AsyncOps helpers (withTimeout, retry, debounce, throttle) built on ReactPHP.
Requires
- php: ^8.3
- react/event-loop: ^1.6
- react/promise: ^3.3
Requires (Dev)
- phpunit/phpunit: ^10.5
This package is not auto-updated.
Last update: 2026-06-02 12:59:29 UTC
README
Shared async utilities for SugarCraft — cancellation tokens, subscriptions, and AsyncOps helpers built on ReactPHP.
Overview
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
Quickstart
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
Requirements
- PHP 8.3+
react/event-loop: ^1.6react/promise: ^3.3
Installation
composer require sugarcraft/candy-async
Architecture
Cancellation
CancellationSource 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.
Subscriptions
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 three
AsyncOps
All 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
License
MIT