# `StatifierOban.Timer.JobArgs`
[🔗](https://github.com/riddler/statifier_oban/blob/v0.17.3/lib/statifier_oban/timer/job_args.ex#L1)

The wire shape between a `%Statifier.Effect.SendDelayed{}` and the args
map an Oban job stores.

Oban args live as JSON, so this module owns the (de)serialization of the
one effect a timer job carries, plus the scope it was scheduled under.
Two rules shape it:

- Every deterministic field rides as an explicit JSON value: the dedup
  pair (`scope`, `ordinal`) at the top level - Oban's uniqueness `keys`
  read args at the top level - and the row data ADR-0059 (statifier-ex)
  keeps beside the key (`send_id`, `macrostep`, `microstep`, `round`,
  `c_index`, `owner`), self-describing in the store during an incident.
- The two host-opaque fields, `data` and `caller_context`, 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. `caller_context` stays row data, never a key component
  (st-ADR-0063 decision 6). Decoding uses `:safe`, so a payload naming
  an atom the reading node has never seen decodes to a typed error
  rather than minting atoms. `from_effect/3`'s optional codec runs over
  both fields' bytes and tags the payload with its module name
  (`StatifierOban.OpaqueTerm.Codec`); `to_effect/1` reads whatever tag
  the stored row carries, regardless of what the reading caller passed.

`to_effect/1` is the exact inverse of `from_effect/3` for every
`%SendDelayed{}` the scheduler accepts: what the fired job carries is
enough to rebuild the event and its position, per ADR-0054's
correlation rule - position is read off the stored effect, never
recomputed at delivery.

## What a host may durably put in `caller_context`

Byte-identical is not the same as meaningful. The row outlives the node
that wrote it - that is the whole point of the package - so two rules
bind what a host stamps into the slot, and this module enforces neither
because it never reads the term:

- **Nothing node-local.** A pid, a port, a reference, an ETS table id,
  or a monitor ref comes back as a term that decodes fine and refers to
  nothing. There is no error to raise; the value is simply a lie by the
  time it is read.
- **Nothing whose atoms the reading node may not have seen.** Decoding
  is `:safe`, so a payload naming an unknown atom is a typed decode
  error rather than a minted atom, and an undecodable row is cancelled
  (`{:undecodable, _}`) rather than retried. A term carrying a library's
  internal atoms therefore couples delivery to that library being
  loaded on whichever node happens to run the job.

For the tracing case both rules point the same way, and this is the
shape the family expects: serialize at schedule time to the **W3C Trace
Context text form** - `%{"traceparent" => "00-<trace-id>-<span-id>-01"}`
plus `"tracestate"` where the host propagates one - rather than storing
a live span context or an OTel context map. Strings and string keys
carry no atoms and nothing node-local, the encoding is fixed by a
published spec instead of a library version, and the value stays
readable in the row during an incident. Restoring it is
`opentelemetry_statifier`'s: this package hands the term back exactly as
given (`StatifierOban.Timer.Delivery.fired_event/2`) and reads nothing
out of it (ADR-0006 decision 7).

# `args`

```elixir
@type args() :: %{optional(String.t()) =&gt; term()}
```

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

# `decode_error`

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

# `encode_error`

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

Why `from_effect/3` could not build an args map.

# `from_effect`

```elixir
@spec from_effect(String.t(), Statifier.Effect.SendDelayed.t(), module() | nil) ::
  {:ok, args()} | {:error, encode_error()}
```

Builds the args map for a timer job from the scope and the effect.

The caller has already validated the scope and ordinal via
`StatifierOban.Timer.Key.dedup_key/2`; this function only lays fields
out on the wire. `data` and `caller_context` are encoded through a
`with`, so the first codec failure short-circuits and no
partially-encoded args map is ever returned.

# `to_effect`

```elixir
@spec to_effect(args()) ::
  {:ok, String.t(), Statifier.Effect.SendDelayed.t()} | {:error, decode_error()}
```

Rebuilds the scope and the `%SendDelayed{}` from a job's args.

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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
