tangle-network / gadget

A framework for building modular AVS and Tangle Blueprints: https://docs.tangle.tools/developers/blueprints/introduction
https://tangle.tools
Apache License 2.0
14 stars 4 forks source link

[TASK] Abstract the Runner into the SDK and add injectable config #331

Closed drewstone closed 2 weeks ago

drewstone commented 1 month ago

Overview

Every blueprint main.rs needs to re-implement repetitive run and registration code which oftentimes has only a few minor differences across different blueprints. This should be abstracted and put into the SDK. The custom configuration should be injected through the exposed structs or environment so that there is less code needed to get a blueprint deployed and running.

https://github.com/webb-tools/gadget/tree/drew/gadget-runner

shekohex commented 1 month ago

Just provide the DefaultRunner with a builder pattern where it takes a list of Event Handlers.

shekohex commented 1 month ago

A better approach could be a macro similar to #[tokio::main] that we could build to improve the DX:

#[sdk::main]
async fn main(config: StdGadgetConfiguration) -> sdk::Result<()> {
   // ...
}
shekohex commented 1 month ago

Here is another proposal that I though about last night

Proposal: Integrating Dependency Injection (DI) in Gadget Framework Similar to Axum

Introduction

This proposal aims to introduce a Dependency Injection (DI) system in the Gadget framework, inspired by the DI implementation in the Axum web framework. The goal is to enhance the modularity and extensibility of the Gadget framework by allowing jobs to automatically register and resolve dependencies.

Design

Job Definition

Jobs in the Gadget framework will be defined the same way we are currently have, with the job attribute macro. Since jobs are very similar to Axum's handlers, we could take that fact and have trait Job similar to the Handler trait in axum.

Example:

#[job(id = 1, params(x, y), result(_))]
async fn my_job(Context(ctx): Context<MyContext>, x: u64, y: u64) -> Result<u64, Inflatable>  { 
    // Job implementation ..
}

Job trait

The Job trait would be defined as following:

pub trait Job<T, C>: Clone + Send + Sized + 'static {
    type Out: Into<Field>;
    /// The type of future calling this job returns.
    type Future: Future<Output = Self::Out> + Send + 'static;

    /// Call the handler with the given request.
    fn call(self, params: &[Field], ctx: C) -> Self::Future;

    /// Convert the job fn into a [`JobHandler`] by providing the context
    fn with_context(self, ctx: C) -> JobHandler<Self, T, C> {
        JobHandler::new(self, ctx)
    }
}

The we would implement this trait for anything that adheres to the following:

  1. it is an async fn
  2. Take no more than 16 arguments that all implement Send.
  3. Returns something that implements IntoField

Environment Setup

An environment setup function will be introduced to register jobs and their contexts. This function will manage the lifecycle of the jobs and ensure that dependencies are resolved correctly.

Example:

let my_ctx = MyContext {
   // ...
};

env.with_context(my_ctx)
    .job(my_job)
    .job(my_other_job)
    .run().await?;

That's it, the job method in the env accepts anything that implements the Job trait, which is all #[job] implements that by default, then it will pass all of them when we call run() to their events watchers to do job.call(...).

Conclusion

Integrating a DI system similar to Axum's in the Gadget framework will significantly enhance its modularity and ease of use. By automatically registering and resolving job dependencies, we can streamline the development process and improve the overall robustness of the framework.

Feel free to ask me any questions.