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.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.