Skip to content

Type-safe spawn_child for cross-domain typed tasks #65

@deepjoy

Description

@deepjoy

Problem

TaskContext::spawn_child() takes an untyped TaskSubmission, requiring manual type-name strings and payload_json() serialization. This loses the compile-time safety that the typed API (DomainHandle::submit_with()) provides.

// Current: untyped, error-prone
ctx.spawn_child(
    TaskSubmission::new("scan-l1-dir")        // string must match TypedTask::TASK_TYPE
        .key(&format!("{}:{}", bucket, prefix))
        .priority(ctx.record().priority)
        .payload_json(&ScanL1DirTask {        // no compile-time check that this matches the type name
            bucket, dir_prefix, scan_start_ns, scope,
        })?,
).await?;

Problems:

  1. Task type string is manually specified — typo = runtime error, not compile error.
  2. Payload type isn't checked against the task type — you could serialize ScanL2Task with the "scan-l1-dir" type name and it would compile fine.
  3. Domain prefix ("scanner::") must be manually omitted or included depending on whether spawn_child auto-prefixes — easy to get wrong.

Same-domain workaround via ctx.domain()

For cross-domain children, the docs suggest:

let storage = ctx.domain::<Storage>();
storage.submit_with(Upload { ... })
    .parent(ctx.record().id)
    .await?;

This is type-safe but requires knowing the parent ID and manually threading it. For same-domain children (the common case), spawn_child is the natural API but lacks type safety.

Proposal

Add a typed spawn_child variant on TaskContext, or a helper on DomainHandle:

Option A: Typed method on TaskContext

impl TaskContext {
    pub async fn spawn_child_typed<T: TypedTask>(
        &self,
        task: T,
    ) -> Result<SubmitOutcome, StoreError> {
        let mut sub = TaskSubmission::new(
            &format!("{}::{}", T::Domain::NAME, T::TASK_TYPE)
        )
        .payload_json(&task)?;

        if let Some(key) = task.key() {
            sub = sub.key(key);
        }
        // apply tags, config defaults, etc.

        self.spawn_child(sub).await
    }
}

Option B: Builder pattern on TaskContext

// Same type safety as DomainHandle::submit_with, but auto-sets parent
ctx.spawn_child_with(ScanL1DirTask { bucket, dir_prefix, .. })
    .priority(Priority::BACKGROUND)
    .key(&format!("{}:{}", bucket, prefix))
    .await?;

This mirrors DomainHandle::submit_with() but automatically sets the parent ID from ctx.record().id.

Option C: Convenience on DomainHandle

// On the domain handle returned by ctx.domain()
let scanner = ctx.domain::<Scanner>();
scanner.submit_with(ScanL1DirTask { .. })
    .child_of(ctx)  // sets parent_id from context, inherits TTL, tags
    .priority(Priority::BACKGROUND)
    .await?;

Preference

Option B is the most ergonomic — mirrors the existing submit_with pattern while keeping the auto-parenting benefit of spawn_child. Option C is a lighter change (just sugar on the existing builder) but child_of(ctx) is less discoverable than a dedicated method.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions