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:
- Task type string is manually specified — typo = runtime error, not compile error.
- 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.
- 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.
Problem
TaskContext::spawn_child()takes an untypedTaskSubmission, requiring manual type-name strings andpayload_json()serialization. This loses the compile-time safety that the typed API (DomainHandle::submit_with()) provides.Problems:
ScanL2Taskwith the"scan-l1-dir"type name and it would compile fine."scanner::") must be manually omitted or included depending on whetherspawn_childauto-prefixes — easy to get wrong.Same-domain workaround via
ctx.domain()For cross-domain children, the docs suggest:
This is type-safe but requires knowing the parent ID and manually threading it. For same-domain children (the common case),
spawn_childis the natural API but lacks type safety.Proposal
Add a typed
spawn_childvariant onTaskContext, or a helper onDomainHandle:Option A: Typed method on
TaskContextOption B: Builder pattern on
TaskContextThis mirrors
DomainHandle::submit_with()but automatically sets the parent ID fromctx.record().id.Option C: Convenience on
DomainHandlePreference
Option B is the most ergonomic — mirrors the existing
submit_withpattern while keeping the auto-parenting benefit ofspawn_child. Option C is a lighter change (just sugar on the existing builder) butchild_of(ctx)is less discoverable than a dedicated method.