-
Notifications
You must be signed in to change notification settings - Fork 0
Promises and Async
SERP provides a family of async primitives for managing deferred results and continuation chains:
| Primitive | Purpose |
|---|---|
serp::Promise<T> |
Single-shot async result — resolved or rejected once. |
serp::Response<T> |
Result carrier passed to method handlers; the handler side of a Promise. |
serp::Async<T> |
Helper wrapper for constructing async operations ergonomically. |
serp::PPromise<T> |
Persistent promise — may be resolved multiple times (stream-like). |
These primitives integrate with serp::Call and serp::Timer to express async workflows without callbacks-in-callbacks.
serp::Promise<T> represents a value of type T that will be available in the future.
serp::Promise<int> promise;
// Success:
promise.resolve(42);
// Failure:
promise.reject(serp::Error("computation failed"));A promise may be resolved or rejected exactly once. Subsequent calls are ignored.
.then() registers a continuation lambda that runs when the promise resolves:
promise.then([](int value) {
use(value);
});The continuation runs on the EventLoop of the service that registered it — not the EventLoop of the resolver. This is safe by design.
.then() returns a new Promise for the return value of the lambda, enabling sequential async steps:
fetchData()
.then([](RawData raw) {
return parse(raw); // returns Promise<ParsedData>
})
.then([this](ParsedData data) {
return store(data); // returns Promise<void>
})
.then([this]() {
logInfo()->info("Done");
});Each step begins only after the previous resolves. Rejection short-circuits the chain — downstream .then() lambdas are skipped.
serp::Response<T> is the handler side of an async method call. It is passed as the last argument to a serp::Call handler when the call has a non-void return type.
The handler captures Response and resolves it when the result is ready — possibly after the handler function has returned:
call.setHandler([this](std::string input, serp::Response<std::string> response) {
// Kick off async work; capture response by value
processAsync(input, [response](std::string result) mutable {
response.resolve(result);
});
// Handler returns immediately; response is resolved later
});| Method | Description |
|---|---|
response.resolve(value) |
Deliver a successful result to the caller. |
response.reject(error) |
Signal failure to the caller. |
Response is move-only and safe to capture by value in lambdas. It may be passed across threads. The framework ensures delivery on the caller's EventLoop.
serp::Async<T> is a convenience wrapper that packages a Promise and its resolution logic together. It is typically used to bridge callback-based APIs into the promise model:
serp::Async<int> async([](serp::Promise<int> promise) {
startBackgroundJob([promise]() mutable {
promise.resolve(computeResult());
});
});
async.then([](int result) {
use(result);
});serp::PPromise<T> (persistent promise) behaves like a Promise but can be resolved multiple times. Each resolution delivers the value to the current set of subscribers. This is useful for streaming results or repeated async events.
serp::PPromise<SensorReading> sensorStream;
// Producer — called repeatedly:
sensorStream.resolve(currentReading);
// Consumer:
sensorStream.then([this](SensorReading r) {
updateDisplay(r);
});For most use cases, prefer serp::Property<T> or serp::Notification<Args...> over PPromise — they are integrated with the generated interface and transport layer. Use PPromise for internal async patterns that don't cross service boundaries.
To reject a promise if a result does not arrive within a deadline, combine a serp::Timer (one-shot) with a shared serp::Promise:
auto promise = std::make_shared<serp::Promise<Result>>();
// Start the operation
beginOperation([promise](Result r) mutable {
promise->resolve(r);
});
// Set a timeout
auto timeout = serp::Timer::create(eventLoop(), std::chrono::seconds(5), [promise]() mutable {
promise->reject(serp::Error("operation timed out"));
});
timeout->setRepeating(false);
timeout->start();
// Consume
promise->then([this](Result r) {
handleResult(r);
});The first to fire (operation or timer) resolves/rejects the promise. The second call is silently ignored by the promise.
This is the canonical pattern for an async method implementation:
// Interface (generated)
serp::Call<MyService, serp::Response<std::string>, std::string> processItem;
// Implementation
processItem.setHandler([this](std::string input, serp::Response<std::string> response) {
// Kick off async work without blocking the EventLoop
m_worker.submit(input, [response](std::string result) mutable {
// Called from worker thread; Response handles cross-thread delivery
response.resolve(result);
});
});
// Caller
myService.processItem("data", [](std::string result) {
// Called on caller's EventLoop when the response resolves
logInfo()->info("Result: " + result);
});serp::Promise<Config> loadConfig() { /* ... */ }
serp::Promise<bool> applyConfig(Config) { /* ... */ }
loadConfig()
.then([this](Config cfg) {
return applyConfig(cfg);
})
.then([this](bool ok) {
if (ok) {
markReady();
} else {
logError()->error("Config apply failed");
}
});| Primitive | Single-shot | Multi-shot | Cross-thread | Integrates with serp::Call |
|---|---|---|---|---|
Promise<T> |
Yes | No | Yes | Via Response<T>
|
Response<T> |
Yes | No | Yes | Yes — directly |
Async<T> |
Yes | No | Yes | Wraps Promise
|
PPromise<T> |
No | Yes | Yes | Manual |
-
Methods —
serp::Callandserp::Responsein practice - Timers and Watchdog — combining timers with promises for timeouts
- Services and Lifecycle — EventLoop and thread delivery guarantees
Getting Started
The Development Model
Architecture Language
Code Generator
- Generator Overview
- Generated Code Layout
- Deployment Configurations
- Lifecycle Backends
- CMake Integration
Framework Internals
- Core Concepts
- Services & Lifecycle
- Methods
- Properties
- Notifications
- Timers & Watchdog
- Promises & Async
- Streams
- Commands
- Logging
- Test Engine
- Transports
- Runtime & Debug Tools
VS Code Plugin
Examples