All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Entries for unreleased work are not written here directly. Each issue drops a
fragment in changelog.d/; the fragments are assembled
into a version section at release. See that README for the format and for when a
change warrants an entry at all.
[0.9.2] 2026-09-30
Patch release: StatifierRouter.Addresses.reap/3 runs on SQLite. Its stamp and delete name their rows in an IN list both adapters take, in place of Postgres's = ANY(...), and a stamp or delete of more than 500 rows is written in batches of 500, one statement each. A Postgres host sees the same counts, and no public function, option or migration version is added or changed.
Fixed
StatifierRouter.Addresses.reap/3runs on SQLite: its stamp and delete no longer use Postgres's= ANY(...), which failed any reap of a SQLite host on 0.8.0 and later that found a row to stamp or delete, under an integer or a text key; a reap with nothing to write sent no statement.
[0.9.1] 2026-09-30
Patch release: the package's migrations run on SQLite again. Migration V03, which renames the subscription index on Postgres, now does nothing on SQLite, where the index already holds the name V02 gave it; before this release its ALTER INDEX failed the migration of a SQLite host on 0.8.0 and later. A Postgres host changes nothing, and no public function, option or migration version is added or changed.
Fixed
- Migration V03 runs on SQLite, where it now does nothing: SQLite never
shortened the subscription index name V03 renames on Postgres. V03's
ALTER INDEX, which SQLite does not have, failed the migration of a SQLite host on 0.8.0 and later.
[0.9.0] 2026-09-30
Feature release: a durable execution can take events at an HTTP location of its own, through the W3C Basic HTTP Event I/O Processor. A configuration that sets the new :basichttp key gives each execution created under a new address row a location the router mints, StatifierRouter.BasicHTTP reads and rotates it, and StatifierRouter.BasicHTTP.Front decodes a POST at it and delivers the event through the existing delivery path. A location is a bearer capability. A configuration without the key behaves exactly as before.
Upgrading: the statifier floor moves from ~> 2.9 to ~> 2.10, the release that ships the processor and its decoder, so a host still on statifier 2.9 upgrades statifier to take this release, whether or not it sets :basichttp. A host that does not set :basichttp changes nothing else: no migration version is added, and StatifierRouter.Migrations.up/1 and down/1 behave exactly as before. A host that wants the front sets :basichttp and adds one migration of its own calling StatifierRouter.Migrations.up_locations/1, with down_locations/1 on rollback, which creates the location table StatifierRouter.Migrations.V04 describes; the README's "Upgrading the tables" and "A BasicHTTP front" show both. The statifier_persistence floor does not move.
Added
StatifierRouter.BasicHTTP, the W3C Basic HTTP Event I/O Processor for durable executions: a configuration that sets the new:basichttpkey (base_url:required,transport:optional) registers it under the processor's URI andbasichttp, and each execution created under a new address row gets a location, the base URL and a 43-character token the router mints, never the execution id, which the chart reads in_ioprocessors.StatifierRouter.BasicHTTP.location/2reads an execution's current location andStatifierRouter.BasicHTTP.rotate_location/2replaces its token, after which the old location answers 404; the chart's own_ioprocessorskeeps the location it started with.StatifierRouter.BasicHTTP.Front, a Plug-shaped front:handle/3decodes a POST at a location with statifier's decoder and delivers it through the existing delivery path, deduplicating per execution on thescxml-send-keyheader, andresponse/1answers 204, 404, 405 withAllow: POST, 400 or 500. A location is a bearer capability: the front authenticates nothing beyond possession of it.StatifierRouter.Migrations.up_locations/1anddown_locations/1create and drop the location table (StatifierRouter.Migrations.V04,<prefix>locations), which only a host that sets:basichttpneeds; it is opt-in and outside the version walk, soup/1anddown/1behave exactly as before. On a configuration with:basichttp, a binding whoseidisbasichttpis refused as reserved.- statifier is required at
~> 2.10, the release that ships the Basic HTTP processor and its decoder.
[0.8.0] 2026-09-27
Feature release: a host can build the router's tables with a primary key of its own, and the subscription table's unique index gets a name that fits in a Postgres identifier under the default table prefix. StatifierRouter.Migrations.up/1 takes :primary_key, which builds the id of every table a version creates with the host's key type and database default; the schemas read the id back through the new StatifierRouter.Schema.Id, and StatifierRouter.Addresses.reap/2 sweeps a text-keyed table. The README gains a guide, "A transactional outbox, end to end".
Upgrading: there is one more migration version, V03, which renames the subscription table's unique index to <table>_invocation_index. A host whose migration calls StatifierRouter.Migrations.up/1 with no version: now runs V03 from it on a fresh database; a host that has already run V02 adds one migration calling StatifierRouter.Migrations.up(from: 3). A host that matches the old truncated index name as a unique violation's constraint matches <table>_invocation_index once V03 has run. Left out, :primary_key changes nothing, and no dependency floor moves.
Added
StatifierRouter.Migrations.up/1takes a:primary_keyoption,[type: ..., default: ...], that builds theidof every table a version creates with the host's own key type and database default, a text id for instance; the schemas read the id back as an integer or a string through the newStatifierRouter.Schema.Id, andStatifierRouter.Addresses.reap/2sweeps a text-keyed table, answering a stringnextcursor it takes back asafter:. Left out, every version builds exactly the tables it built before.
Fixed
- The subscription table's unique index, which V02 named past the 63 bytes Postgres keeps of an identifier so that Postgres created it under a truncated name, is renamed to
<table>_invocation_indexby a new migration version, V03; a host that has already run V02 adds one migration callingStatifierRouter.Migrations.up(from: 3), andStatifierRouter.Migrationsnow says what a long:table_prefixdoes to index names; a host that matches the old truncated name as the constraint of a unique violation (anEcto.ConstraintError, or aunique_constraint/3naming it) matches<table>_invocation_indexinstead once V03 has run.
[0.7.0] 2026-09-26
Feature release: a host that serves send types of its own can declare them, and the contract check says when it did not check a resolver's bindings. StatifierRouter.Config.new/1 takes :send_handlers, merged with :send_type into the one send_types: snapshot every delivery carries, so StatifierRouter.Contracts.check/3 stops reporting the host's own types as unsupported. Two fixes: StatifierRouter.Migrations.up/1 refuses, before any DDL runs, a :leading_columns name that a table the call creates already declares, and StatifierRouter.Broadway's partitioner keeps the producer up on a malformed :bindings_resolver answer.
Upgrading: breaking for a host that sets :bindings_resolver and reads StatifierRouter.Contracts.check/3's :unchecked entries - the list now opens with %{reason: :bindings_resolver, location: nil}, the one entry with no location; a configuration without a resolver gets the report it got before. Config.new/1's refusal union grows by one member, {:exclusive_keys, :send_handlers, :send_types}, answered only to a configuration that gives a non-empty :send_handlers beside a :persistence_options carrying its own :send_types and no :send_type. No migration, no new table, and no dependency floor moves.
Added
StatifierRouter.Config.new/1takes:send_handlers, a map from each send type the host serves itself to its processor module, merged with:send_typeinto the onesend_types:snapshot every delivery carries, soStatifierRouter.Contracts.check/3no longer reports the host's own types under:unsupported_types; left out, the snapshot is built from:send_typealone as before.
Changed
- Breaking for a host that sets
:bindings_resolverand readsStatifierRouter.Contracts.check/3's:uncheckedentries: the list now opens with%{reason: :bindings_resolver, location: nil}, saying the bindings were not checked, where the report before was identical to a clean pass. It is the one entry with no location, so skip it or match its reason before readinglocation, and keep checking each scope's bindings withStatifierRouter.Contracts.undeclared_binding_events/2. This is the one change for a host with a resolver; a configuration without one gets the report it got before.
Fixed
StatifierRouter.Migrations.up/1raisesArgumentErrornaming the column and the tables before any DDL runs when a:leading_columnsname is one a table the call creates already declares (scope,inserted_atand the like; the primary key the repo configures is not checked), where the migration before failed inside Postgres with a duplicate column error. A name only a table outside the call declares, such asexpires_atunderup(from: 2), is accepted as before.StatifierRouter.Broadway's partitioner no longer takes the producer down when a:bindings_resolveranswers something that is not a list of bindings: it partitions that message by its message id, androute/3raises theArgumentErrorinhandle_message/3, where Broadway fails the message.
[0.6.0] 2026-09-25
Feature release: a host that wraps the engine can plug into the router without forking it. StatifierRouter.Config.new/1 takes :on_create and :on_step, which the default delivery calls in place of statifier_persistence's create and step; :bindings_resolver, which answers the bindings per scope through the new StatifierRouter.BindingsResolver behaviour; and :execution_id, which mints each new execution's id. StatifierRouter.Migrations.up/1 takes the host column layout options :leading_columns, :timestamps_position and :column_collations. Every new key and option is optional, and a host that sets none of them sees no change.
Upgrading: Config.new/1's refusal union grows by one member, {:exclusive_keys, :bindings, :bindings_resolver}, answered only to a configuration that gives both keys. No migration, no new table, and no dependency floor moves.
Added
StatifierRouter.Configtakes:on_createand:on_step, a module or a fun the default delivery calls in place ofStatifierPersistence.Executions.create/4andstep/5, with the same arguments and return contract, inside the delivery's transaction; left out, the delivery calls statifier_persistence itself as before.StatifierRouter.Configtakes:bindings_resolver, a module implementing the newStatifierRouter.BindingsResolverbehaviour or an arity-1 fun, answering the bindings of one scope;route/3, the Broadway partitioner andsubscribe/3read its answer for the scope in hand, checked for the reserved and duplicated binding ids as:bindingsis. It is exclusive with:bindings, andConfig.new/1refuses both with{:error, {:exclusive_keys, :bindings, :bindings_resolver}}; left out,:bindingsis read as before.StatifierRouter.Configtakes:execution_id, a module exportingexecution_id/3or an arity-3 fun of(scope, document, key)answering a non-empty string, which the default delivery mints each new execution's id with; that id is the one on the address row, the created execution and the ledger. An answer that is not a non-empty string raisesArgumentError. Left out, the id is a UXID with the prefixex, as before.StatifierRouter.Migrations.up/1takes:leading_columns,:timestamps_positionand:column_collations, statifier_persistence's layout options under the same spellings and rules: host-owned columns immediately afterid,inserted_atmoved to follow them, and a collation per package text column, applied only as a version creates a table, on all four tables.down/1accepts and ignores them. Left out, the tables are built exactly as before.
[0.5.0] 2026-09-25
Feature release: a live session's sends get a delivery scope and the execution target, and a delivery whose step selected no transition says so. StatifierRouter.Config.new/1 takes :processor_scope, so a Statifier.Session's sends resolve their routes under a scope's :route_overrides, and a fun there that answers neither a non-empty scope string nor nil is answered {:error, {:invalid_value, :processor_scope, value}}, a new member of the open StatifierRouter.SendHandler.reason/0. On the send-processor shape an immediate send to the reserved execution target is delivered or refused as at the executor seam, where it was answered as an unregistered route.
Upgrading: two closed sets a host matches grow. Every :unregistered_routes entry of StatifierRouter.Contracts.check/3 now carries reason, :unregistered or :no_timer_queue, typed as the closed StatifierRouter.Contracts.route_reason/0; and StatifierRouter.route/3 gains the outcome {:dropped, binding_id, :unmatched_event}, recorded on the routing ledger as dropped: unmatched_event, for a delivery whose step selected no transition. The dependency floors move to statifier ~> 2.9 and statifier_persistence ~> 0.18; this package adds no migration and no new table.
Added
StatifierRouter.Config.new/1takes:processor_scope, a scope string or a zero-arity funStatifierRouter.SendHandlercalls per send, so a liveStatifier.Session's sends resolve their routes under that scope's:route_overrides.- Breaking for a host that matches
StatifierRouter.route/3's outcomes exhaustively, or a custom:deliverymodule's answers: a binding's delivery whose step selected no transition for the event now answers{:dropped, binding_id, :unmatched_event}in place of{:delivered, binding_id, execution_id}or{:created_and_delivered, binding_id, execution_id}, and its routing-ledger row readsdropped: unmatched_eventwith the execution's id. The execution still took the event, so its input log holds it, and a created execution stays. The outcome cannot tell an event the current state has no transition for from one whose every guard was false. An execution-to-execution send keeps its outcomes. Add a clause for the new tuple wherever you match outcomes; no migration.
Changed
- Breaking for a host that matches
StatifierRouter.Contracts.check/3's:unregistered_routesentries exactly or builds them itself: every entry now carriesreason,:unregisteredfor a<send>whose literaltargetnames no registered route (the entries it reported before), or:no_timer_queuefor a<send>that writes a literaldelayto a registered route on a configuration with no:timer_queue, a sendStatifierRouter.SendHandlernever queues and refuses at run time, as{:no_timer_queue, send_id}once the route resolves. A%{route: _, location: _}pattern still matches every entry. Add areasonkey wherever you compare or build a whole entry, and treat a:no_timer_queueentry as you treat an unregistered route, or configure a:timer_queue.StatifierRouter.Routes.unregistered/2is unchanged. StatifierRouter.Contracts.reason/0and the newStatifierRouter.Contracts.route_reason/0are documented as closed sets: a new reason arrives only in a minor release that names it as breaking.- On the send-processor shape,
StatifierRouter.SendHandler.perform/2delivers an immediate<send>whosetargetis the reservedexecutionname to the execution itsdocumentandkeyparams address, or refuses it as{:error, {:send_refused, reason}}, exactly ashandle_effect/3does at the executor seam; it no longer answers{:error, {:unregistered_route, "execution"}}. The sender's scope is read from the address row its session id names, and a session id that names none is refused as:unaddressed_sender. - Requires
statifier ~> 2.9(thelast_selectionthe outcome is read from) andstatifier_persistence ~> 0.18. A host still on statifier_persistence below 0.17 runs that package's V08 migration before deploying, as its 0.17.0 changelog says.
[0.4.1] 2026-09-23
Patch release: the publish-time contract check now flags a delayed send to the execution target. StatifierRouter.Contracts.check/3 and StatifierRouter.Contracts.undeclared_events/3 report such a <send> as a finding with reason :delay, the send StatifierRouter.SendHandler refuses at run time, so a host's publish step can catch it before a document goes live; StatifierRouter.Contracts.reason/0 gains :delay. No migration, no new configuration option, and no dependency floor moves.
Changed
StatifierRouter.Contracts.check/3andundeclared_events/3report a delayed<send>to the execution target (one that writesdelayordelayexpr) with a literal event and a literaldocumentas a finding with reason:delay, without calling the lookup, because such a send is refused at run time; before, it passed whenever its receiver declared the event.
[0.4.0] 2026-09-23
Feature release: a live session's delayed send, and the refusals and savepoints a host reads. StatifierRouter.SendHandler.perform/2 now records a live session's delayed send on the host's StatifierRouter.TimerQueue, the same row the executor seam writes, and performs its cancel through that queue; a delayed send to the execution target is refused by name; and a delivery or a refusal row that fails settles at a savepoint of its own, so neither a host's own transaction around StatifierRouter.route/3 nor a sender's step is lost to it.
Upgrading: answers a host reads change. A host that runs live sessions through perform/2 and sends with a delay needs a :timer_queue in its configuration (without one the send is answered {:error, {:no_timer_queue, send_id}}), and that queue's schedule/2 must add no second row for a key it already holds, because perform/2 may be handed one send more than once. {:delayed_send_unsupported, send_id} is gone from StatifierRouter.SendHandler.reason/0. A delayed send to the execution target is answered {:send_refused, :delay} with a delay ledger row, where the executor seam answered {:unregistered_route, "execution"} with a route row. perform/2 answers a cancel in a process holding no configuration with {:no_config, StatifierRouter.SendHandler}, where it answered :ok. At the executor seam a send to a route some scope overrides, with no delivery scope in reach, is refused as {:no_delivery_scope, name}. On the send-processor shape a cancel now reaches the configured queue's cancel/3, and that queue's {:error, reason} comes back from perform/2, where it answered :ok without calling the queue. Host queue code run at the executor seam is marked by StatifierRouter.SendHandler.sending_execution/0, so a StatifierRouter.route/3 called from inside its schedule/2 or cancel/3 is refused with {:reentrant_route, execution_id}. An :on_complete route some scope overrides, reached with no delivery scope in the process (a delivery through StatifierRouter.Delivery.deliver_event/4 that no StatifierRouter.route/3 call encloses), answers {:error, {:on_complete, name, {:no_delivery_scope, name}}} and rolls that delivery back, where 0.3.0 used the route's registered configuration. No migration, no new configuration option, and no dependency floor moves.
Changed
StatifierRouter.SendHandler.perform/2records a delayed send on the configuredStatifierRouter.TimerQueueunder the same composed key and with the same row the executor seam writes, and performs a planned cancel through that queue'scancel/3; it no longer answers a delayed send with{:error, {:delayed_send_unsupported, send_id}}, and that reason is dropped fromStatifierRouter.SendHandler.reason/0.StatifierRouter.TimerQueue.schedule/2now states that a queue holds at most one row per entrykey(a repeat is answered:okand adds no row), and the behaviour's moduledoc says how a host fires a queued row throughStatifierRouter.Config.route/3and the route'sdeliver/3.StatifierRouter.SendHandler.perform/2answers a cancel with{:error, {:no_config, StatifierRouter.SendHandler}}when the calling process holds no configuration, where it answered:ok, and a delayed send the same way, where it answered{:error, {:delayed_send_unsupported, send_id}}. Install the configuration withStatifierRouter.SendHandler.put_config/1in the processperform/2runs in; the moduledoc says why this answer is not reported to the chart.- A delayed send to the reserved
executiontarget is answered{:error, {:send_refused, :delay}}byStatifierRouter.SendHandleron both host shapes, and recorded as asend_refusedledger row with the reasondelaywhen the sender has an address row; at the executor seam it was answered{:error, {:unregistered_route, "execution"}}with arouterow, which named a route no host could register.StatifierRouter.SendHandler.refusal/0gains:delay. StatifierRouter.SendHandler.handle_effect/3refuses a send or a delayed send to a route that some scope in:route_overridesoverrides, when no delivery scope is in reach, with{:error, {:no_delivery_scope, name}}instead of sending it to the registered configuration. The send-processor shape (perform/2) is unchanged and still resolves such a send to the registered configuration. A route no scope overrides resolves as before on both shapes.StatifierRouter.SendHandler.sending_execution/0also names the sending execution while the timer queue'sschedule/2andcancel/3run at the executor seam, so aStatifierRouter.route/3called from host queue code there is refused with{:error, {:reentrant_route, execution_id}}.StatifierRouter.SendHandler.reason/0gains{:no_delivery_scope, name}, the answerhandle_effect/3gives a send to a route some scope overrides when no delivery scope is in reach.StatifierRouter.SendHandler.perform/2answers a delayed send with{:error, {:no_timer_queue, send_id}}when the configuration names no:timer_queue, and a delayed send to an unregistered route with{:error, {:unregistered_route, name}}and the samesend_refusedledger row an undelayed send to it writes.StatifierRouter.SendHandler.perform/2answers a cancel with the timer queue's own{:error, reason}when itscancel/3fails, where it answered:okwithout calling the queue.
Fixed
StatifierRouter.route/3called inside a host's own transaction no longer loses that transaction when a delivery answers{:error, reason}: the delivery rolls back to a savepoint of its own and the host's writes stand.- An execution-to-execution send refused with a recorded reason no longer takes the sending step down when its
send_refusedledger row cannot be written: the row rolls back to a savepoint of its own and the refusal is still reported. - A sender's address read that fails while
StatifierRouter.SendHandlerrecords an unregistered-route or delayed-execution-send refusal no longer takes the sending step down: the read now sits inside the refusal row's savepoint, rolls back to it, and the refusal is still reported, with no ledger row.
[0.3.0] 2026-09-22
Feature release: the receiver contract at publish. A host gets StatifierRouter.Contracts, pure functions its own publish step calls to find every execution-target <send> and every binding whose event the receiving document does not accept, judged through a lookup the host supplies, and StatifierRouter.Contracts.check/3, which runs every publish-time check this package ships and answers one report under five named keys. Which finding blocks a publish stays the host's decision.
Upgrading: one dependency floor moves, and no other: statifier to ~> 2.7, the release carrying Statifier.Chart.check_accepts/2, which StatifierRouter.Contracts calls to judge a receiver that declares no events. No migration and no new configuration option.
Added
StatifierRouter.Contracts.undeclared_events/3lists every<send>to the reserved execution target whose literal event its receiving document does not accept, judged through a host-supplied lookup, with the sends an expression or a missingeventordocumentleft unchecked.StatifierRouter.Contracts.undeclared_binding_events/2lists every binding whose event its document does not accept, with the binding's id.StatifierRouter.Contracts.check/3runs every publish-time check the package ships - bothStatifierRouter.Routeschecks and the two above - and answers one report under five named keys.
Changed
- The
statifierrequirement moves to~> 2.7, the release carryingStatifier.Chart.check_accepts/2, which judges a receiver that declares no events.
[0.2.0] 2026-09-22
Feature release: the outbound half, the execution target and the source invoke. A chart now reaches the world through named routes a host registers and overrides per scope, addresses another durable execution by document and key through the same one transaction a binding's delivery uses, and holds an <invoke> open as a subscription to a binding. A host gets the StatifierRouter.Route and StatifierRouter.TimerQueue behaviours, StatifierRouter.SendHandler for both shapes a registered send type arrives in, StatifierRouter.Webhook as a Plug-shaped front, StatifierRouter.Routes for a publish-time check of the routes a machine sends to, and StatifierRouter.PinSource so an addressed execution holds its chart back from retirement.
Upgrading: run V02 against an existing database. A host already running V01 writes StatifierRouter.Migrations.up(from: 2) - from: is inclusive, so that call runs V02 and nothing before it - and V02 adds the subscription table without re-running V01's CREATE TABLE. StatifierRouter.Config.new/1 takes six new options, each optional and each defaulting to the 0.1.0 behaviour: :route_adapters (the route registry - it is spelled :route_adapters, and :routes is a statifier_persistence snapshot option carried inside :persistence_options), :route_overrides, :send_type, :timer_queue, :on_complete and :persistence_options. Two dependency floors move, and no other: statifier to ~> 2.6 and statifier_persistence to ~> 0.13, the releases carrying host-registered Event I/O Processor send types.
Added
StatifierRouter.Route, the behaviour a host implements for one named, one-way outbound destination a chart reaches with<send target="...">.StatifierRouter.SendHandler, which serves both shapes a registered send type reaches a host in:Statifier.Send.Processorfor a live session, andhandle_effect/3for a process-less host to call from itsStatifierPersistence.Executor.StatifierRouter.TimerQueue, the behaviour a host implements for the durable queue a delayed route send is recorded on, keyed by{scope, send_id}.StatifierRouter.Configtakes:route_adapters,:route_overrides,:send_typeand:timer_queue, andStatifierRouter.Config.route/3resolves a route name in a scope.- Giving
:send_typeputs theStatifier.Send.Typessnapshot for that type into:persistence_options, so every create and every step of every delivery declares the host's processor to the engine. - A
<send>whosetargetis the reserved nameexecutiondelivers to the durable execution at the sender's scope, itsdocumentparam and itskeyparam, through the same transaction, dedupe and ledger a binding's delivery uses. StatifierRouter.Delivery.deliver_event/4delivers one prebuilt event under a delivery plan, the door an execution-to-execution send comes in by.StatifierRouter.Addresses.by_execution/2answers the address row naming one execution, ornil.StatifierRouter.SendHandler.execution_target/0answers the reserved target name.- A
<send>whosetargetnames no registered route writes onesend_refusedrouting-ledger row, under the reserved binding idexecutionand the reasonroute, beside the{:error, {:unregistered_route, name}}the sender already heard; a send whose key's scope half names no address row has no scope to record and is reported without a row, and a ledger insert that fails rolls back to its own savepoint rather than to the sending step's. StatifierRouter.subscribe/3andStatifierRouter.cancel/2subscribe one execution's<invoke>to a binding for the lifetime of the invoking state, and undo it (ADR-0007).StatifierRouter.SourceInvokemaps an invoke's start and the engine's cancellation onto those two calls, for a host's invoke handler to delegate to.StatifierRouter.Migrations.V02adds the subscription table those calls write. A host already running V01 migrates to it withStatifierRouter.Migrations.up(from: 2), sincefrom:names the first version the host has not run and the walk includes it.StatifierRouter.Schema.Subscription: the Ecto schema over the subscription table.StatifierRouter.Routes.unregistered/2lists every<send>of the configuration's send type whose literaltargetnames no registered route, with the sends an expression left unchecked, for a host's own publish step.StatifierRouter.Routes.unsupported_types/2lists every<send>whose literaltypeis outside the set the configuration registers.StatifierRouter.Config's:on_completenames a registered route an execution's donedata is handed to on the delivery that finishes it, as adone.executionevent under an idempotency key with no ordinal.StatifierRouter.PinSource, aStatifierPersistence.PinSourceover the address table: a chart is not retired while an address row names one of its active executions.StatifierRouter.Webhook.handle/3routes one verified webhook request, taking the message id from the provider's event id or, absent one, the lowercase hex SHA-256 of the raw body.StatifierRouter.Webhook.status/1answers the HTTP status a provider should see for onehandle/3answer:200for a recorded outcome,500for an error.StatifierRouter.Config's:persistence_optionscarries the statifier_persistence snapshot options -:routes,:invoke_typesand:send_types- onto every create and every step of every delivery.
Changed
StatifierRouter.Delivery.deliver/4answers{:error, {:reentrant_route, execution_id}}when a route called at the executor seam calls back into the sending execution, instead of opening a nested step.StatifierRouter.Config.new/1refuses a route registered under the reserved name with{:reserved_route, name}, and a binding whoseidis that name with{:reserved_binding_id, name}.StatifierRouter.Config.new/1refuses a:storewhose adapter options name a repo other than the configuration's own.StatifierRouter.Addresses.reap/2deletes an address row whose execution the store no longer holds instead of refusing with{:error, :execution_not_found}, so one such row no longer ends every sweep that reaches it.StatifierRouter.Broadway.start_link/1raisesArgumentErrorwhen:nameis missing or is not an atom, as its options table has always said it would; it previously started an unnamed pipeline instead, and passed a{:via, module, term}name through to Broadway, which accepts one. Give the pipeline an atom name, and register it under a registry, if it needs one, by that atom rather than by passing the{:via, module, term}tuple as:name.- The
statifierfloor is~> 2.6and thestatifier_persistencefloor is~> 0.13, the releases carrying host-registered Event I/O Processor send types.
[0.1.0] 2026-09-19
Feature release, and the first: the Broadway front and the binding, addressing and delivery layer that routes external events to durable statifier executions, creating them when absent. A host gets bindings over predicator programs, the (scope, document, key) address table, get-or-create-and-deliver in one transaction under three create modes, dedupe with a horizon, the recorded outcome vocabulary, a resolver behaviour for the chart a new execution starts on, and two reapers it schedules itself.
Added
StatifierRouter.Binding:new/1validates a binding and compiles itsmatchandkeyprograms once,match/2andkey/2evaluate them over a normalized event, andproject/2builds the delivered event's data from the binding's field paths.StatifierRouter.Migrations:up/1anddown/1create and drop the address table, the dedupe table and the routing ledger from a host's one-line delegating migration, withfrom:,version:,table_prefix:andprefix:options.StatifierRouter.Config:new/1resolves the host's repo, table prefix and Postgres schema, andtable/2,put_meta/2andqueryable/2point theStatifierRouter.Schemamodules at the configured tables.StatifierRouter.Schema.Address,StatifierRouter.Schema.DedupeandStatifierRouter.Schema.Ledger: Ecto schemas over the three tables.StatifierRouter.route/3routes one event through the configured bindings and returns one outcome per enabled binding for the event's source, in configuration order, writing a routing ledger row for each key_refused and reporting each no_match as the telemetry event[:statifier_router, :route, :no_match].StatifierRouter.Config.new/1takes:bindings, built throughStatifierRouter.Binding.new/1with a duplicate binding id refused, and an optional:deliverymodule thatroute/3hands each delivery to, defaulting toStatifierRouter.Delivery.StatifierRouter.Delivery, the default delivery module: for a binding whosecreateis:if_absent, it gets or creates the execution an address names and steps the event into it in one transaction on the host's repo, and returns{:created_and_delivered, binding_id, execution_id},{:delivered, binding_id, execution_id}or{:dropped, binding_id, :finished}.StatifierRouter.Config.new/1takes:store,:executor,:resolverand:chart_resolver, the four optionsStatifierRouter.Deliveryrequires.StatifierRouter.Broadway, the Broadway front: a pipeline the host starts in its own supervision tree with any producer, which hands each message toStatifierRouter.route/3, partitions each message by the address of the first enabledorder: :by_keybinding for its source whosematchholds and whosekeyresolves, or by its message id when no such binding addresses it, and fails rather than acknowledges a message whose routing returns an error or raises.StatifierRouter.Dedupe.claim/4, called first in everyStatifierRouter.Deliverytransaction: a message a binding already handled within its dedupe horizon is{:duplicate, binding_id}, recorded on the ledger and delivered nowhere; an expired dedupe row counts as absent.StatifierRouter.Dedupe.reap/2, a plain function the host schedules, deletes expired dedupe rows and returns{:ok, count}.StatifierRouter.route/3returns{:error, :no_message_id}for an event whosemessage_idisnilor empty, before any binding is evaluated.StatifierRouter.Addresses.reap/2, a plain function the host schedules, stamps address rows whose execution it first sees finished and deletes those whose longest enabled binding horizon has elapsed; one call examines at most:limitrows and returns anextcursor.StatifierRouter.Deliverydelivers for:neverbindings, returning{:dropped, binding_id, :no_execution}when the address has no row, and for:always_newbindings, creating one execution per delivery with no address row.StatifierRouter.Resolver, the behaviour a host implements to name the chart a new execution of a document starts on;StatifierRouter.Config.new/1accepts as:resolvera module implementing it or an arity-2 fun.StatifierRouter.Resolver.Static.new/1, a resolver over a map from{scope, document}to a compiled machine, for tests and for charts compiled at boot.StatifierRouter.route/3returns{:error, {:unresolved_document, document, reason}}when the resolver answers{:error, reason}for a document: the delivery's transaction rolls back, so nothing is created and no row is written.