Module: Plutonium::Interaction::Concerns::Dispatchable

Extended by:
ActiveSupport::Concern
Includes:
Scoping
Included in:
Base
Defined in:
lib/plutonium/interaction/concerns/dispatchable.rb

Overview

Turns an interaction into a dispatcher: instead of doing the work inline, it persists a Async::Run, enqueues it, and sends the user to it.

The interaction keeps everything else it already was — it declares inputs, validates them, renders a form, and is gated by the same policy as before. Only #execute changes.

The outcome stays synchronous

call still returns an Outcome immediately, because dispatching is what succeeded. Whether the WORK succeeds is the run's business, reported on the run's own page — which is why Outcome needs no third "pending" state.

Nothing is passed in at the call site

Every value the run needs is already reachable: the targets are the +resource+/+resources+ the controller resolved through the policy scope, and the authorization triple comes from the controller the interaction is rendering in. So no controller and no host app has to hand a dispatching interaction anything an inline one does not already get.

What dispatch records, and why it is exactly this

The job has no controller, so Async::Context rebuilds the authorization context from the row. Three of the columns exist purely so that rebuild cannot silently widen:

  • authorization_namespace — the portal's module NAME. Without it, perform-time lookup falls back to the base policy and every narrowing the portal applied is lost.
  • policy_class_name — the policy dispatch ACTUALLY RESOLVED, never an inferred "#ModelPolicy". A namespaced portal and an STI fallback both make the inferred name wrong, and Context refuses to run when the recorded name and today's lookup disagree.
  • policy_action — the predicate, so perform re-checks PERMISSION and not merely visibility.

Examples:

class Blogging::ArchivePosts < Plutonium::Resource::Interaction
  attribute :resources
  attribute :reason, :string

  async do
    on_failure :continue
    def perform_on(post) = post.archive!(reason: options["reason"])
  end
end

Defined Under Namespace

Modules: ExecuteOverride Classes: NotEnabledError