StatifierUI.Trace.Replay (StatifierUI v0.6.0)

Copy Markdown View Source

The offline producer of the v1 trace wire format (ADR-0017): a pure function that turns a persisted session event log into the same StatifierUI.Trace.Message stream StatifierUI.Trace.Subscriber produces from a live session - with no Statifier.Session, no process, and no clock.

A host that persists its own session event log - because it restarts, or because it stores runs for audit and renders them later - has the inputs a run was driven by, the compiled chart, and the options the session ran under, and nothing left to subscribe to. from_events/4 is what it calls instead.

The four steps, and why they are the subscriber's four steps

from_events/4 builds a Statifier.Session.Recording through that module's public constructors, hands it to Statifier.Replay.run/1, emits the session.start manifest, and folds the returned stream through the normalizer - which is exactly what StatifierUI.Trace.Subscriber's catch-up attach path does, with the same calls in the same order:

No message shape is constructed in this module. Parity with the subscriber (ADR-0017 decision 3) is therefore structural rather than promised: identical types, identical order, identical seq values, and identical payload bytes under StatifierUI.Trace.Json.encode_lines/1.

What it deliberately does not do

  • No session.terminated. There is no process offline and no exit to observe; an offline stream's end is the end of the entry list. session.halted is unaffected - it comes from a {:halted, reason} stream element and is produced normally.
  • No capacity, no fan-out, no diagnostics counter. A function returning a list drops nothing and sends to nobody, so the subscriber's bounded buffer, its listener set, and its stats/1 have no offline counterpart. The whole list is held in memory, which the subscriber's bound does not have to.
  • No timers and no clock. Statifier.Replay converts a schedule into a pending-timer credit rather than a real Process.send_after/3, and a recorded firing is delivered at its recorded position and nowhere else. Wall-clock time never enters, which is what makes this function deterministic.

The name

This module and Statifier.Replay collide under alias. That is intentional: the engine module is aliased here as EngineReplay at its single call site rather than naming this one something that hides what it is.

Summary

Types

This producer's own options - the subscriber's emission options and no others.

Functions

Produces the wire-format message stream for a recorded run, offline.

Types

opts()

@type opts() :: [
  source: String.t(),
  fixtures: map(),
  parent_session: String.t(),
  invokeid: String.t(),
  projection: StatifierUI.Trace.Projection.Profile.t(),
  otel_context: StatifierUI.Trace.Otel.resolver()
]

This producer's own options - the subscriber's emission options and no others.

:source, :fixtures, :parent_session and :invokeid are forwarded verbatim to StatifierUI.Trace.Manifest.build/3; :source additionally widens the normalizer's ctx exactly as the subscriber's does. :projection and :otel_context are the chokepoint's two options.

Deliberately absent are :capacity, :listeners and :name: a buffer, a fan-out and a process name are process concerns, and this is a function.

Functions

from_events(machine, initialize_opts, events, opts \\ [])

@spec from_events(
  machine :: Statifier.Machine.t(),
  initialize_opts :: keyword(),
  events :: [Statifier.Session.Recording.entry()],
  opts :: opts()
) :: {:ok, [StatifierUI.Trace.Message.t()]} | {:error, term()}

Produces the wire-format message stream for a recorded run, offline.

Errors are values, and the call fails closed

The first failure returns {:error, reason} and no partial list. Where the subscriber continues past a bad effect and records a diagnostic - it would otherwise lose a live stream it can never recover - an offline caller still has the log in hand and can retry, report, or investigate, and a partial list returned as {:ok, messages} would be indistinguishable from a whole one (ADR-0017 decision 5).

The reasons:

Examples

iex> {:ok, machine} = Statifier.compile(~s(<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="a"><state id="a"/></scxml>))
iex> {:ok, [manifest | _]} =
...>   StatifierUI.Trace.Replay.from_events(machine, [session_id: "s", trace: true], [])
iex> {manifest.type, manifest.session, manifest.seq}
{"session.start", "s", 0}

iex> {:ok, machine} = Statifier.compile(~s(<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="a"><state id="a"/></scxml>))
iex> StatifierUI.Trace.Replay.from_events(machine, [session_id: "s"], [])
{:error, {:initialize_opts, :trace_disabled}}