Announcing lifeline-rs, a dependency injection library for asynchronous message-based applications

I'm excited to announce lifeline, a dependency injection library for asynchronous message-based applications.

Lifeline

Lifeline is a dependency injection library for async message-based applications, written in Rust and inspired by Guice. Lifeline uses the DI pattern to reduce direct dependencies between components of an application, allowing individual components to be more easily tested, refactored, or replaced.

Lifeline applies the Dependency Injection pattern to channels: Sender and Receiver pairs which transmit messages. In lifeline, the Message is the abstracting interface, and the concrete types are Services which retrieve channel endpoints, and receive/send messages.

Lifeline provides Sender/Receiver traits which unify common behavior across all channel types - making channel alterations (e.g. mpsc to broadcast for ExampleMessage) a one-liner change. This also allows you to write impl Sender<ExampleMessage> for your functions which run individual tasks.

Cancellation

Lifeline provides trivial cancellation of entire task trees. When services are spawned, they return a Lifeline value. When the lifeline is dropped, their tasks are immediately cancelled. This allows 'message-based shutdown', where tasks can transmit shutdown messages. Listeners drop the lifelines they hold, which results in clean propagation of shutdown events.

Debugging

Inspired by Guice, Lifeline provides deterministic binding errors at compile time, and moves runtime errors to app startup (or as close as possible).

Lifeline also produces extremely greppable applications. When you search for a message type, in the matched lines you find all locations a Sender or Receiver was taken, and the Bus types which distribute channel endpoints.

$ rg "PtyResponse"
tab-pty/src/bus/pty.rs
24:   impl Message<PtyBus> for PtyResponse {

tab-pty/src/message/pty.rs
35:   pub enum PtyResponse {

tab-pty/src/service/client.rs
167:  let rx_response = bus.rx::<PtyResponse>()?;
231:  PtyResponse::Output(out) => {

tab-pty/src/service/pty.rs
45:   let tx_response = bus.tx::<PtyResponse>()?;
74:   tx_exit.send(PtyResponse::Terminated(exit_code)).await?;

Examples/Apps

Here is a quick example that shows how a Lifeline Bus can be defined:

use lifeline::prelude::*;
use tokio::sync::mpsc;

lifeline_bus!(pub struct ExampleBus);

impl Message<ExampleBus> for ExampleMessage {
    type Channel = mpsc::Sender<Self>;
}

fn run() -> anyhow::Result<()> {
    let bus = ExampleBus::default();
    let mut tx = bus.tx::<ExampleMessage>()?;
    tx.send(ExampleMessage::Hello).await?;
    Ok(())
}

The hello.rs example provides an introduction to Messages and Services.

The carrier.rs example shows how to transmit messages between Bus implementations.

For a real-world app, see tab-rs, a config-driven terminal multiplexer based on lifeline.

Roadmap

In the next incremental release:

  • Add Task::spawn_many, which creates a pool of tasks which each take channels from the bus.
  • Allow doc comments in the lifeline_bus!(pub struct MyBus) macro.
  • Add Lifeline::leak, a way to 'opt out' of task cancellation when you don't want it.

In 1.0:

  • Fully stabilize the API.
  • Add support for the smol runtime.
  • Add support for additional third-party async channel libraries.