StatifierBlocks.Runtime.DurableSubchart (StatifierBlocks v0.26.0)

Copy Markdown View Source

The durable statifier_blocks:subchart handler (sb-2i04, ADR-0008): the child runs as its own persisted statifier_persistence run, and the answer is given at dispatch time rather than from a pure Statifier.Invoke.Handler.start/2.

StatifierBlocks.Runtime.Subchart is untouched and stays the in-memory canonical handler. This is the second module beside it (ADR-0008 decision 1): the two serve the same invoke type string (StatifierBlocks.Core.Subchart.invoke_type/0, the one definition site), compile the same documents to the same bytes, and share the whole of resolution and refusal through StatifierBlocks.Runtime.Subchart.Resolution. Which one answers is the host's session wiring, never the document - so a host that wires the in-memory module into a durable run gets a child that does not survive the restart the parent was made durable to survive.

The seam this plugs into

The durable path has no Statifier.Session. Its invocations are answered by StatifierPersistence.Driver's :dispatch fun, called as dispatch.(type, params, context) inside the durable step that emitted the <invoke>. dispatch_fun/1 builds exactly that fun from a host module:

dispatch = StatifierBlocks.Runtime.DurableSubchart.dispatch_fun(MyApp.Charts)
driver = StatifierPersistence.Driver.new(machine, store, dispatch: dispatch)

A host serving more invoke types than this one writes its own fun and delegates the subchart clause here:

fn
  "statifier_blocks:subchart", params, context ->
    StatifierBlocks.Runtime.DurableSubchart.dispatch(
      "statifier_blocks:subchart", params, context, MyApp.Charts
    )

  "myapp:authorize", params, _context ->
    {:ok, MyApp.authorize(params)}
end

use StatifierBlocks.Runtime.DurableSubchart is the same thing on the host module itself: it declares the resolver behaviour and defines an overridable dispatch/3 delegating here.

No dependency on statifier_persistence

This module names no module from that package and calls nothing in it. Everything it needs arrives in the dispatch context map, and everything it answers is a plain tuple the driver already understands. That keeps statifier_blocks free of a dependency edge on a package that depends, transitively, on the same engine this one compiles for.

src reaches this module through context.invoke

core.subchart carries the document id on the <invoke>'s src and nowhere else (ADR-0004's amendment, R1), and src is not one of the two arguments a dispatch fun is handed. statifier_persistence's t:StatifierPersistence.Driver.dispatch_context/0 therefore carries :invoke, the whole Statifier.Effect.Invoke.t/0 being dispatched; context.invoke.src is the document id this module resolves, and the payload it hands back is the one it was given rather than a synthesised stand-in.

A context with no :invoke key raises ArgumentError rather than refusing the author's chart: it means the host is on a statifier_persistence older than the one that widened the seam, which is a wiring defect and not a chart problem - the same posture the resolver contract's own totality rule takes.

What it answers

{:start_child, %{invoke | content: scxml}, {:invoke, invoke}} on a successful resolve - the same instruction shape the in-memory module plans, unrenamed and unwrapped, because the effect vocabulary is statifier's and only the executor differs (ADR-0008 decision 3). The driver creates the child run inside the parent's own exclusion, records the linkage with its mandatory chart-identity pin, and answers :pending: the parent reaches quiescence and the completion arrives later through the seam's public done.invoke door.

{:error, reason: reason, detail: detail} on a refusal - st-ADR-0068's failure keyword list, which the driver turns into error.communication.invoke.<invoke id> carrying the same reason/detail payload the in-memory {:raise, :platform, ...} instruction carries. :attempts is deliberately absent: a refusal made no attempt.

The refusal set, and which half raises which reason

ADR-0008 decision 5 closes the set at four, and the two packages satisfy it jointly:

ReasonRaised by
unknown_documentthis module
child_compile_findingsthis module
cycle_refusedthis module
child_run_creation_failedStatifierPersistence.Driver, from its own start_child refusals

The fourth is the durable-only reason, and it belongs where it is raised: creating the child run happens after this module has answered, inside the parent's serialized step, so this module has no way to observe it and does not pretend to. Nothing here can emit a fifth.

What is deliberately absent

No cancel, no forward, no completion path. A durable cancel happens because the parent left the invoking state and active_invocations lost the entry; the cascade through the child's own children, the retained records under a distinct terminal status, and the idempotent dropping of late completions are all sp-ADR-0008's (ADR-0008 decision 4, "no bespoke parent-child channel"). This package contributes nothing to them and offers no callback that looks like it does.

Nesting needs no code: the driver drives a child with the same dispatch fun, so a grandchild's <invoke> arrives here exactly as its parent's did, and the protection against a looping document graph is the cycle refusal already in the set (ADR-0008 decision 6). Fan-out - one invocation mapping to N children - is named by that decision and is not built here.

Summary

Types

What dispatch/4 answers: the start instruction StatifierPersistence.Driver executes, or st-ADR-0068's failure keyword list under :error.

Functions

Declares the resolver behaviour and defines an overridable dispatch/3 on the host module, delegating to dispatch/4.

Answers one durable statifier_blocks:subchart invocation.

Builds the three-arity fun StatifierPersistence.Driver's :dispatch option expects, from one host module.

The invoke type this module serves - StatifierBlocks.Core.Subchart.invoke_type/0, the one definition site.

Types

answer()

@type answer() ::
  {:start_child, Statifier.Effect.Invoke.t(),
   {:invoke, Statifier.Effect.Invoke.t()}}
  | {:error,
     reason: StatifierBlocks.Runtime.Subchart.Resolution.reason(), detail: map()}

What dispatch/4 answers: the start instruction StatifierPersistence.Driver executes, or st-ADR-0068's failure keyword list under :error.

Functions

__using__(opts)

(macro)

Declares the resolver behaviour and defines an overridable dispatch/3 on the host module, delegating to dispatch/4.

The behaviour is StatifierBlocks.Runtime.Subchart's own - the same StatifierBlocks.Runtime.Subchart.resolve_chart/2 and StatifierBlocks.Runtime.Subchart.palette/0, in full and without additions (ADR-0008 decision 2).

dispatch(type, params, context, host)

@spec dispatch(String.t(), term(), map(), module()) :: answer()

Answers one durable statifier_blocks:subchart invocation.

context is a StatifierPersistence.Driver dispatch context; its :invoke key carries the payload whose src names the document to resolve, and it is handed to StatifierBlocks.Runtime.Subchart.resolve_chart/2 as the ctx.

The resolved params argument is deliberately unread: the element's own params seed the child run's datamodel on the driver's side, out of the payload handed back, so reading them here would only risk disagreeing with it.

dispatch_fun(host)

@spec dispatch_fun(module()) :: (String.t(), term(), map() -> answer())

Builds the three-arity fun StatifierPersistence.Driver's :dispatch option expects, from one host module.

The fun serves this module's invoke type and nothing else; a host with further invoke types writes its own fun and delegates to dispatch/4, as the moduledoc shows.

invoke_type()

@spec invoke_type() :: String.t()

The invoke type this module serves - StatifierBlocks.Core.Subchart.invoke_type/0, the one definition site.