Skip to main content

nexus-runtime

Runtime abstractions and async primitives.

Composer: nexus-actors/runtime

Namespace: Monadial\Nexus\Runtime\

Async namespace

Monadial\Nexus\Runtime\Async\

Class / InterfaceDescription
Future<T>Async result handle. Methods: await(), map(Closure), flatMap(Closure), isResolved().
FutureSlot<T>Runtime-backed resolver for Future. Methods: resolve(object), fail(FutureException), await(), isResolved().
LazyFutureSlot<T>Internal lazy FutureSlot used by combinators (map, flatMap).

Runtime namespace

Monadial\Nexus\Runtime\Runtime\

Class / InterfaceDescription
RuntimeRuntime contract used by runtimes like Fiber, Swoole, and Step.
CancellableCancellation handle for scheduled tasks (cancel(), isCancelled()).

Mailbox namespace

Monadial\Nexus\Runtime\Mailbox\

Class / InterfaceDescription
Mailbox<T>Generic mailbox contract used by runtime implementations.
MailboxConfigImmutable mailbox configuration (bounded(), unbounded(), withCapacity(), withStrategy()).
OverflowStrategyOverflow behavior enum (DropNewest, DropOldest, Backpressure, ThrowException).
EnqueueResultEnqueue result enum (Accepted, Dropped, Backpressured).

Exception namespace

Monadial\Nexus\Runtime\Exception\

Class / InterfaceDescription
FutureExceptionBase marker interface for future failures.
FutureTimeoutExceptionMarker interface for timeout-style future failures.
MailboxExceptionBase mailbox exception.
MailboxClosedExceptionMailbox operation on closed mailbox.
MailboxOverflowExceptionMailbox overflow with throw strategy.
MailboxTimeoutExceptionMailbox operation timed out.
InvalidMailboxConfigExceptionInvalid mailbox configuration.

Duration

Monadial\Nexus\Runtime\Duration

Immutable nanosecond-precision duration value object used by all runtime APIs.

Runtime interface

The Runtime interface abstracts the concurrency primitive. Implementations are FiberRuntime (nexus-runtime-fiber), SwooleRuntime (nexus-runtime-swoole), and StepRuntime (nexus-runtime-step).

use Monadial\Nexus\Runtime\Async\FutureSlot;
use Monadial\Nexus\Runtime\Duration;
use Monadial\Nexus\Runtime\Mailbox\Mailbox;
use Monadial\Nexus\Runtime\Mailbox\MailboxConfig;
use Monadial\Nexus\Runtime\Runtime\Cancellable;
use Monadial\Nexus\Runtime\Runtime\Runtime;

interface Runtime
{
public function name(): string;

/** @return Mailbox<T> */
public function createMailbox(MailboxConfig $config): Mailbox;

/** Returns the write-side slot for the ask pattern. */
public function createFutureSlot(): FutureSlot;

public function spawn(callable $actorLoop): string;

public function scheduleOnce(Duration $delay, callable $callback): Cancellable;

public function scheduleRepeatedly(
Duration $initialDelay,
Duration $interval,
callable $callback,
): Cancellable;

public function yield(): void;

public function sleep(Duration $duration): void;

public function run(): void;

public function shutdown(Duration $timeout): void;

public function isRunning(): bool;
}

spawn() creates a lightweight fiber or coroutine for an actor's message-processing loop and returns an opaque string identifier. yield() gives up the current timeslice to allow other fibers to run. sleep() suspends for at least the given duration without blocking the scheduler.

Mailbox contracts

use Monadial\Nexus\Runtime\Mailbox\Mailbox;
use Monadial\Nexus\Runtime\Mailbox\MailboxConfig;
use Monadial\Nexus\Runtime\Mailbox\OverflowStrategy;

// Unbounded mailbox (default)
$config = MailboxConfig::unbounded();

// Bounded mailbox — throw when full (default overflow strategy)
$config = MailboxConfig::bounded(capacity: 10_000);

// Bounded mailbox with explicit overflow strategy
$config = MailboxConfig::bounded(
capacity: 10_000,
strategy: OverflowStrategy::DropNewest,
);

// Mutate an existing config (returns new instance)
$config = $config->withCapacity(5_000)->withStrategy(OverflowStrategy::Backpressure);

Overflow strategies:

StrategyBehaviour
DropNewestDiscard the incoming message when full.
DropOldestEvict the oldest queued message to make room.
BackpressureBlock the sender until space is available.
ThrowExceptionThrow MailboxOverflowException immediately.

The Mailbox<T> interface:

interface Mailbox
{
/** Returns Accepted, Dropped, or Backpressured. */
public function enqueue(object $message): EnqueueResult;

/** Non-blocking poll. Returns null when empty. */
public function dequeue(): mixed;

/**
* Suspends the calling fiber or coroutine until an envelope arrives
* or the timeout expires. Throws MailboxTimeoutException on timeout,
* MailboxClosedException if the mailbox has been closed.
*/
public function dequeueBlocking(Duration $timeout): object;

public function count(): int;
public function isFull(): bool;
public function isEmpty(): bool;
public function close(): void;
}

dequeueBlocking() is the primary receive primitive for actor loops. It suspends the current fiber (or Swoole coroutine) and resumes it as soon as a message is enqueued, without spinning or polling.

Duration

Duration is an immutable nanosecond-precision value object. All arithmetic methods return new instances; the original is never mutated.

use Monadial\Nexus\Runtime\Duration;

// Factory methods
$d = Duration::seconds(5);
$d = Duration::millis(200);
$d = Duration::micros(500);
$d = Duration::nanos(1_000_000);
$d = Duration::zero();

// Arithmetic (returns new instances)
$total = Duration::seconds(1)->plus(Duration::millis(500));
$half = Duration::seconds(2)->dividedBy(2);
$long = Duration::millis(100)->multipliedBy(10);
$diff = Duration::seconds(5)->minus(Duration::millis(500));

// Comparison
$d->isGreaterThan(Duration::millis(100)); // bool
$d->isLessThan(Duration::millis(100)); // bool
$d->equals(Duration::millis(200)); // bool
$d->isZero(); // bool
$d->compareTo(Duration::millis(100)); // -1 | 0 | 1

// Conversion
$d->toNanos(); // int
$d->toMicros(); // int
$d->toMillis(); // int
$d->toSeconds(); // int (truncated)
$d->toSecondsFloat(); // float

// String representation (implements Stringable)
echo Duration::seconds(1)->plus(Duration::millis(500)); // "1s 500ms"

Future and FutureSlot

Future<T> is the read-side handle to a pending async result. FutureSlot<T> is the write-side resolution mechanism. The runtime creates slots via Runtime::createFutureSlot(); the actor system uses them internally for the ask pattern.

use Monadial\Nexus\Runtime\Async\Future;
use Monadial\Nexus\Runtime\Async\FutureSlot;
use Monadial\Nexus\Runtime\Exception\FutureException;

// The runtime creates the slot; the caller receives the Future.
$slot = $runtime->createFutureSlot();
$future = new Future($slot);

// Resolve from a handler when the result is ready.
$slot->resolve(new OrderConfirmed($orderId));

// Fail the slot if an error occurs.
$slot->fail($someException); // $someException implements FutureException

// Cancel the slot (e.g. on timeout).
$slot->cancel();

// Register a cancellation callback.
$slot->onCancel(function (): void {
// clean up resources
});

// await() suspends the current fiber/coroutine until resolved or failed.
$result = $future->await(); // throws FutureException on failure or cancellation

// Check without suspending.
$future->isResolved(); // bool

// Cancel from the read side.
$future->cancel();

map() and flatMap() compose futures without blocking:

// Transform the result lazily when it arrives.
$future
->map(static fn(OrderConfirmed $c): OrderSummary => new OrderSummary($c->orderId))
->flatMap(static fn(OrderSummary $s): Future => $enrichmentFuture)
->await();

Cancellable

scheduleOnce() and scheduleRepeatedly() return a Cancellable that lets callers abort a scheduled callback before it fires:

use Monadial\Nexus\Runtime\Runtime\Cancellable;

$cancellable = $runtime->scheduleOnce(
Duration::millis(500),
function (): void {
// fires once after 500 ms
},
);

$cancellable->cancel(); // abort if not yet fired
$cancellable->isCancelled(); // bool

$heartbeat = $runtime->scheduleRepeatedly(
Duration::zero(), // initial delay
Duration::seconds(1), // interval
function (): void {
// fires every second
},
);

// Stop the repeating schedule when no longer needed.
$heartbeat->cancel();

When To Use nexus-runtime Only

Use nexus-runtime without nexus-core when:

  • async result composition (Future, map, flatMap, await) is needed in non-actor code
  • runtime-neutral scheduling contracts are required for adapters or infrastructure code
  • deterministic orchestration in tests is needed (for example with nexus-runtime-step)
  • shared timeout/cancellation primitives (Duration, Cancellable) are needed in libraries

Why It Is Useful

  • keeps actor-free modules lightweight and decoupled from actor APIs
  • lets infrastructure code depend on stable runtime contracts only
  • improves testability by using runtime implementations directly
  • avoids forcing full actor-system adoption when only async primitives are needed

Bootstrap

See Bootstrap Runtime for actor-system and standalone setup flows.