StatifierOban.Invoke.JobArgs (StatifierOban v0.5.0)

Copy Markdown View Source

The wire shape between a %Statifier.Effect.Invoke{} and the args map an invoke-handler Oban job stores.

Oban args live as JSON, so this module owns the (de)serialization of the one effect an invoke job carries, plus the scope it was enqueued under and the handler module whose run/1 the worker calls back into. The rules are StatifierOban.Timer.JobArgs's, applied to the invoke effect:

  • Every deterministic field rides as an explicit JSON value: the dedup triple (scope, invoke_id, macrostep - ADR-0003) at the top level - Oban's uniqueness keys read args at the top level - and the effect's remaining position row data (state_index, invoke_index, microstep, round) beside it, self-describing in the store during an incident. invoke_id is the authored id used verbatim, or the deterministic %MachineState{} counter, st-ADR-0008 (as amended) blesses as the idempotency key; scoping is mandatory because that counter restarts per chart run, and macrostep is what tells a state re-entry's fresh invocation apart from a crash replay of the old one.
  • The two host-opaque fields, params and content, are arbitrary terms with no JSON shape, so they ride as tagged :erlang.term_to_binary/1 payloads (StatifierOban.OpaqueTerm) and come back byte-identical. from_invoke/4's optional codec runs over both fields' bytes and tags the payload with its module name (StatifierOban.OpaqueTerm.Codec); to_invoke/1 reads whatever tag the stored row carries, regardless of what the reading caller passed.
  • handler is the module name of the StatifierOban.Invoke.Handler implementation, written from a validated module at enqueue time and resolved back by the worker - a resolution failure there is deploy-shaped (the module was renamed or removed after the job was stored) and retries, exactly like the timer worker's delivery module.

to_invoke/1 is the exact inverse of from_invoke/4 for every %Statifier.Effect.Invoke{} the base handler enqueues: what the job carries is enough to hand the handler's run/1 the same effect the planning callback saw.

Summary

Types

String-keyed args map as Oban stores and redelivers it.

Why from_invoke/4 could not build an args map.

Functions

Builds the args map for an invoke job from the scope, the handler module, and the effect.

Rebuilds just the identity pair - the scope and the invoke id - from a job's args.

Rebuilds the scope, the handler module name, and the %Statifier.Effect.Invoke{} from a job's args.

Types

args()

@type args() :: %{optional(String.t()) => term()}

String-keyed args map as Oban stores and redelivers it.

decode_error()

@type decode_error() ::
  {:missing_field, String.t()}
  | {:invalid_field, String.t(), term()}
  | StatifierOban.OpaqueTerm.decode_error()

encode_error()

@type encode_error() ::
  {:codec_failed, String.t(), StatifierOban.OpaqueTerm.encode_error()}

Why from_invoke/4 could not build an args map.

Functions

from_invoke(scope, handler, invoke, codec \\ nil)

@spec from_invoke(String.t(), module(), Statifier.Effect.Invoke.t(), module() | nil) ::
  {:ok, args()} | {:error, encode_error()}

Builds the args map for an invoke job from the scope, the handler module, and the effect.

The caller (StatifierOban.Invoke.Handler.perform_start/3) has already validated the scope; this function only lays fields out on the wire. params and content are encoded through a with, so the first codec failure short-circuits and no partially-encoded args map is ever returned.

identity(args)

@spec identity(args()) :: {:ok, String.t(), String.t()} | {:error, decode_error()}

Rebuilds just the identity pair - the scope and the invoke id - from a job's args.

to_invoke/1 fails the whole row when any field is undecodable, including the two host-opaque payloads, which is the common way a row goes bad. This reads only the two plain-string fields that name the invocation, so a caller holding an otherwise undecodable row can still tell the run which invocation it is about (StatifierOban.Invoke.Worker delivers error.communication that way before cancelling). The rules are to_invoke/1's own, because this is the same fetch_binary/2: a missing, empty, or non-string field is a typed error, and then the row names nothing and there is nobody to tell.

to_invoke(args)

@spec to_invoke(args()) ::
  {:ok, String.t(), String.t(), Statifier.Effect.Invoke.t()}
  | {:error, decode_error()}

Rebuilds the scope, the handler module name, and the %Statifier.Effect.Invoke{} from a job's args.

The handler comes back as the stored string, not a resolved module: resolution is the worker's call, because an unresolvable name is a retryable environment fact where every error here is a fact about the row. Returns a typed error rather than raising: an undecodable job is a fact about the row, and the worker decides what to do with it.