Skip to content

feat(sdk): Type-safe shared state injection (State<T> extractor) #1286

@drewstone

Description

@drewstone

Problem

Every blueprint re-invents instance state management with OnceCell / Lazy<RwLock<Option<T>>> statics. This pattern is:

  • Fragile: Easy to forget initialization, leading to runtime panics on .unwrap().
  • Flaky in tests: Parallel tests share process-global state, causing non-deterministic failures and requiring careful teardown.
  • Not discoverable: New developers have no way to know what state a handler depends on without reading the full source.

Proposal

An SDK-provided State<T> extractor (similar to axum's State<T>) that works across job handlers and background services.

// Register state at startup
BlueprintRunner::builder(config, env)
    .state(MyDatabasePool::new())
    .state(MyCache::default())
    .router(router())
    .run()
    .await?;

// Extract in job handlers — same signature pattern as Caller, TangleArg, etc.
async fn my_handler(
    State(db): State<MyDatabasePool>,
    State(cache): State<MyCache>,
    caller: Caller,
    args: TangleArg<MyRequest>,
) -> Result<TangleResult<Vec<u8>>, String> {
    // use db and cache directly
}

// Extract in background services
impl BackgroundService for MyBackgroundTask {
    async fn start(&self, state: &ServiceState) -> Result<Receiver<...>> {
        let db = state.get::<MyDatabasePool>()?;
        // ...
    }
}

The runner would hold a type-map (TypeMap or similar) and inject references into handlers via the extractor pattern.

Benefits

  • Eliminates process-global statics: No more OnceCell, Lazy, or static mut.
  • Makes state dependencies explicit: A handler's signature declares exactly what it needs.
  • Fixes parallel test flakiness: Each test spawns its own runner with its own state — no shared globals.
  • Enables proper test isolation: Test harnesses can inject mock/stub state without environment variable tricks.
  • Consistent with ecosystem patterns: Developers familiar with axum, actix-web, or similar frameworks will find this immediately intuitive.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature ➕Tasks that are functional additions or enhancements

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions