Guides
Host callbacks
A trait the app implements, so the core can call it: a progress report, a note, a question for the user.
Declare it#
A callback is a trait the app implements. Mark it #[undra::callback], take it as Arc<dyn Trait> in a method or function, and call it from the core like any other trait.
| A method on the trait | Means |
|---|---|
fn note(&self, line: String) | fire and forget: the core does not wait, and nothing is lost on the way |
async fn confirm(&self, q: String) -> Result<bool, E> | asks the app and awaits its answer, a typed error included |
#[undra(coalesce)] | a listener that has fallen behind gets only the newest call |
#[undra::callback(background)] | runs off the main thread, in order, without waiting for a frame |
A method that returns a value without async is refused (E0071): the core never blocks waiting for app code.
Show the code for Rust, Swift, Kotlin and TypeScript
/// What the app implements to hear from the core.
#[undra::callback]
pub trait Reporter {
/// Only the newest report of a burst matters.
#[undra(coalesce)]
fn progress(&self, done: u32, total: u32);
fn note(&self, line: String);
/// Asks whether to go on; the app may answer later, or not at all.
async fn confirm(&self, question: String) -> Result<bool, ReportError>;
}
#[undra::api]
impl Workshop {
pub async fn run(&self, steps: u32, reporter: Arc<dyn Reporter>) -> Result<u32, ReportError> { .. }
/// Keeps `reporter` until the returned subscription is closed.
pub fn watch(&self, reporter: Arc<dyn Reporter>) -> Arc<Watch> { .. }
}@MainActor
final class Banner: Reporter {
func progress(done: UInt32, total: UInt32) { bar.value = Double(done) / Double(total) }
func note(line: String) { log.append(line) }
func confirm(question: String) async throws(ReportError) -> Bool { await ask(question) }
}
let steps = try await workshop.run(steps: 3, reporter: Banner())
let watch = try workshop.watch(reporter: banner) // keeps it; watch.close() lets goclass Banner : Reporter {
override fun progress(done: UInt, total: UInt) { bar.progress = done.toFloat() / total.toFloat() }
override fun note(line: String) { log += line }
override suspend fun confirm(question: String): Boolean = ask(question) // throws ReportError
}
val steps = workshop.run(3u, Banner())
val watch = workshop.watch(banner) // keeps it; watch.close() lets goconst banner: Reporter = {
progress(done, total) { bar.value = done / total; },
note(line) { log.push(line); },
confirm(question, signal) { return ask(question, signal); }, // rejects with ReportError
};
const steps = await workshop.run(3, banner);
const watch = await workshop.watch(banner); // keeps it; watch.close() lets goWhere it runs#
By default on the main thread, in the order things happened. A callback joins the same queue as state changes, so a listener sees the stores as they were when the core called it. With background each listener has its own serial executor instead. Either way the core never runs app code inside its own call.
Errors and cancelling#
An implementation that throws the method's own error answers the core with it, typed. Any other error is reported through onError and the core sees the app as unavailable. A throw from a fire-and-forget method is only reported. None of it unwinds into the core.
When the core stops waiting (the call that was awaiting was cancelled), the task running the implementation is cancelled too: a Swift Task, a Kotlin Job, a TypeScript AbortSignal passed as the last argument.
How long it lives#
The core holds the callback until it lets go, so a listener you create inline lives as long as the core uses it. A core that keeps the listener and an app that keeps the core make a cycle across languages. Two ways out:
- A subscription object. Return something the app closes (
watchabove): closing it drops the core's hold. - A weak wrapper. Hand the core a wrapper that forwards while the target lives and does nothing after.
A stream method takes a callback like any other method: the core holds it while the stream runs and lets it go when the stream ends or is dropped. The core never calls a weak wrapper whose target is gone (the question is answered unavailable); if your own code does in Swift, the method throws the error type's Unavailable, and an error type without one stops the app with a message.
Show the code for Swift, Kotlin and TypeScript
let watch = try workshop.watch(reporter: WeakReporter(banner)) // does nothing once banner is goneval watch = workshop.watch(Reporter.weak(banner))const watch = await workshop.watch(weakReporter(banner));Testing#
In a Rust test pass an implementation written in Rust: #[undra::port] impl Reporter for Recorder records the calls, and an async method answers as you set it. See Testing your app.
The playground's Reporter is this page's example, and the contract scenario S28 checks ordering, errors, cancelling and release on Swift, Kotlin and TypeScript.