///|
/// Request-scoped context supplied to typed application command handlers.
///
/// Lifecycle-owned task and event capabilities are added by the application
/// runner; the registrar keeps handlers independent from transport details.
pub type CommandContext = @core.AppCommandRequestContext

///|
pub type CommandWindow = @core.CommandWindow

///|
/// Startup-only capability for binding typed command descriptors to handlers.
pub struct CommandRegistrar {
  host : @core.AppCommandHost
  owner : (String, String)?
  routes : Array[@proton_contract.ContractRoute]
  mut registration_closed : Bool
}

///|
/// Creates a registrar over one application command host.
#doc(hidden)
pub fn CommandRegistrar::CommandRegistrar(
  host : @core.AppCommandHost,
) -> CommandRegistrar {
  CommandRegistrar::{
    host,
    owner: None,
    routes: [],
    registration_closed: false,
  }
}

///|
/// Binds one typed descriptor to an asynchronous command handler.
///
/// Request decoding and response encoding are selected from the descriptor's
/// type parameters. The descriptor is the only source of command identity.
pub fn[Request : @json.FromJson, Response : ToJson] CommandRegistrar::bind(
  self : CommandRegistrar,
  command : @proton_contract.Command[Request, Response],
  handler : async (CommandContext, Request) -> Response,
) -> Unit raise {
  guard !self.registration_closed else {
    raise @core.OpRegistrationError::RegistrationSealed
  }
  command.validate()
  let route = command.contract_route()
  if self.owner is Some((id, js_namespace)) {
    guard route.extension_id() == Some(id) &&
      route.extension_namespace() == Some(js_namespace) else {
      raise ExtensionSpecError::ForeignCommandRoute(
        extension_id=id,
        operation_name=route.operation_name(),
      )
    }
  }
  self.host.op_async_with_context(route.operation_name(), handler)
  self.routes.push(route)
}