# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.0.2] — 2026-07-17
Performance-focused release: adopts datalogic-rs 5.1 and lands a hot-path
optimization pass. On the 10-core reference machine the `benchmark` example
runs at ~640K msg/s (P50 6 μs, P99 51 μs); the realistic ISO 20022 →
SwiftMT-103 workload improved ~242K → ~259K msg/s (+7%) with materially
tighter tails (P99.9 ~215–270 μs → ~135–155 μs).
### Added
- `examples/micro_aggregate_bench.rs` — end-to-end benchmark for
aggregate-heavy (`reduce`/`map`) mappings; quantifies the datalogic 5.1
CSE + fusion wins (1.68× on a checkout-style workload).
- `examples/micro_subtree_write_bench.rs` — scaling benchmark for many map
mappings targeting the same subtree (k = 5/25/100 writes).
### Changed
- **deps:** datalogic-rs `5.0 → 5.1` (common-subexpression elimination for
repeated pure aggregates, `reduce(map(...))` fusion) and datavalue-rs
`0.2.2 → 0.2.3`. API-compatible — no dataflow code changes required;
aggregate-heavy mappings improve substantially, scalar pipelines are flat.
- **deps:** `uuid` enables `fast-rng` on non-wasm targets — default v7
message ids draw from a thread-local PRNG seeded once from the OS instead
of paying one `getrandom` syscall per message (+3% throughput, tighter
P99.9). Appropriate for v7 ids, which need uniqueness, not secrecy. The
wasm build keeps the lean `v7`+`std` feature set.
- **Source compatibility (semver-minor):** `ParseConfig` and `PublishConfig`
gained engine-internal precomputed fields (`#[doc(hidden)]`,
`#[serde(skip)]`, not part of the stable API). Struct-literal construction
must now spread a default — e.g.
`ParseConfig { source, target, ..Default::default() }`. `ParseConfig`
derives `Default`; `PublishConfig` has a manual `Default` with
`root_element = "root"` (matching the serde default). JSON wire shape and
`from_json` construction are unchanged.
### Performance
- Map hot loop: arena-side write-through — consecutive mappings writing into
the same subtree no longer re-clone it per mapping; k same-subtree writes
now do O(k) total work instead of O(k²) (k=100 microbench: −27% ns/msg,
and the win grows with written-subtree size).
- Workflow-condition evaluation folded into the first sync task stretch for
mixed sync+async workflows — one context walk instead of two
(`async_handler_benchmark`: +5.7% throughput; marginal cost of one custom
handler dispatch 0.56 → 0.09 μs/msg).
- `publish_json` / `publish_xml` serialize from a borrowed source instead of
deep-cloning the source subtree first — hundreds of avoided allocations
per publish on ISO 20022-shaped payloads.
- Per-task `metadata.progress` write updates the existing slot in place and
refreshes only that arena child instead of re-arenaing the whole
`metadata` tree.
- `log` tasks short-circuit before evaluating any JSONLogic when their level
is filtered out for the `dataflow::log` target — filtered log tasks are
effectively free in production.
- `parse_*` / `publish_*` target paths precomputed at engine build time (no
per-execution `format!` / path split / `Arc<str>` alloc); default v7
message ids stored in an inline buffer instead of a heap `String`.
### Fixed
- `benchmark` and `micro_cond_bench` referenced `{"var": "payload.input.*"}`,
which is outside the evaluation context (`data` / `metadata` /
`temp_data`) — every mapping silently evaluated to null and the published
numbers measured no-op work. Both now use the canonical `parse_json` →
`data.input.*` idiom, and every bench asserts at startup that its workload
actually computes. README performance numbers restated from the fixed
workload.
## [3.0.1] — 2026-06-13
### Performance
- One bump arena shared across consecutive fully-sync workflows instead of
one arena rebuild per workflow.
- Trivially-true workflow conditions fold to `None` at compile time,
skipping per-message condition evaluation entirely.
### Changed
- deps: datavalue-rs `0.2.2` and bumpalo `3.20` minimums aligned with the
lockfile.
### Added
- `examples/micro_cond_bench.rs` — single-threaded `process_message`
microbenchmark.
- `examples/micro_multiworkflow_bench.rs` — quantifies per-workflow arena
rebuild cost.
## [3.0.0] — 2026-05-15
Major redesign of the custom-function API surface on the datalogic v5 core.
Performance is neutral on the realistic ISO 20022 → SwiftMT-103 workload
(230K msg/s, P50 23 μs). The new dyn-Any dispatch path for custom handlers
adds ~1.2 μs/call of framework overhead — well below typical handler I/O
latency.
### Added
- **`AsyncFunctionHandler::Input`** — typed associated input. Handlers declare
`type Input: DeserializeOwned` instead of matching on `FunctionConfig::Custom
{ input, .. }`. The engine pre-parses each task's input JSON into the typed
shape at `Engine::new()` — config-shape errors now fail at startup, not on
first message.
- **`TaskContext<'a>`** — per-call context handed to every handler. Typed
accessors (`data()`, `metadata()`, `temp_data()`, `get(path)`),
audit-trail-aware setters (`set(path, value)` records a `Change`
automatically when `capture_changes` is on), and `add_error(...)`.
Replaces the raw `&mut Message + &FunctionConfig + Arc<DatalogicEngine>`
argument trio.
- **`TaskOutcome` enum** — `Success` / `Status(u16)` / `Skip` / `Halt`.
Replaces the `(usize, Vec<Change>)` tuple, removes the magic-number contract
for filter skip / halt signals.
- **`BoxedFunctionHandler`** type alias (= `Box<dyn DynAsyncFunctionHandler +
Send + Sync>`). Hides the dyn-trait name from user code.
- **`Engine::builder()`** returning `EngineBuilder`. `.register("name", h)`,
`.register_boxed(...)`, `.with_workflow(w)`, `.with_workflows(iter)`,
`.build() -> Result<Engine>`.
- **`Message::builder()`** returning `MessageBuilder`. Collapses the historical
`new` / `with_id` / `from_value` / `without_change_capture` four-way
constructor split into one fluent shape.
- Read accessors on `Message`: `id()`, `payload()`, `payload_arc()`,
`audit_trail()`, `errors()`, `capture_changes()`.
- **`dataflow_rs::prelude`** — re-exports the 14 types you need for the 90%
case (Engine, EngineBuilder, Workflow, Task, Message, MessageBuilder,
AuditTrail, Change, AsyncFunctionHandler, TaskContext, TaskOutcome, Result,
DataflowError, ErrorInfo, WorkflowStatus).
- **`#[must_use]`** on `EngineBuilder`, `MessageBuilder`, `ErrorInfoBuilder`
so drop-on-floor mistakes during the migration are loud.
- **`examples/async_handler_benchmark.rs`** — measures the marginal cost of
one custom-handler dispatch (`+1.2 μs/msg`, `−9% throughput` on a tight
6-op pipeline; `+6%` total ops/sec because the extra task does useful work).
### Changed
- **`AsyncFunctionHandler::execute` signature**:
- **Was**: `async fn execute(&self, &mut Message, &FunctionConfig,
Arc<DatalogicEngine>) -> Result<(usize, Vec<Change>)>`
- **Now**: `async fn execute(&self, &mut TaskContext<'_>, &Self::Input)
-> Result<TaskOutcome>`
- Removes the `match FunctionConfig::Custom { input, .. } | _ =>
Err(...)` boilerplate, the manual `Change` construction, and the
magic-number return tuple.
- **`Engine::new` signature**:
- **Was**: `pub fn new(Vec<Workflow>, Option<HashMap<String, Box<dyn
AsyncFunctionHandler + Send + Sync>>>) -> Result<Self>`
- **Now**: `pub fn new(Vec<Workflow>, HashMap<String,
BoxedFunctionHandler>) -> Result<Self>`
- Use `HashMap::new()` for the no-handler case, or — preferred —
`Engine::builder()`.
- **`Engine::process_message` error contract**: `message.errors()` is now the
always-on view; `Result::Err` only signals "the engine stopped before
processing further workflows". The `WORKFLOW_ERROR` wrapper is now pushed
for **every** workflow failure (not only `continue_on_error: true`); a new
`TASK_STATUS_ERROR` entry is pushed when a handler returns
`TaskOutcome::Status(s)` with `s >= 500`. Wire format and audit-trail
semantics are unchanged.
- **`Message` field encapsulation**: `id`, `payload`, `audit_trail`, `errors`,
`capture_changes` are now `pub(crate)` with read accessors. `context`
remains `pub` — it's the legitimate read surface (tests do
`message.context["data"]["x"]` lookups). Mutate `errors` via
`message.add_error(e)`; mutate `context` via `TaskContext::set(...)`.
- **`FunctionConfig::Custom`** gained a `compiled_input:
Option<CompiledCustomInput>` field (skipped by serde; populated by the
engine at construction time with the typed handler input).
### Removed
- **`Message::with_id`** — use `Message::builder().id(...).build()`.
- **`Message::without_change_capture`** — use
`Message::builder().capture_changes(false).build()`.
- **`FILTER_STATUS_PASS`, `FILTER_STATUS_SKIP`, `FILTER_STATUS_HALT`**
constants — `FilterConfig` returns `TaskOutcome::Success` /
`TaskOutcome::Skip` / `TaskOutcome::Halt` directly. The on-the-wire halt
status code (299) is preserved as `dataflow_rs::engine::task_outcome::HALT_STATUS_CODE`.
### Performance
- Realistic benchmark (500K msgs × 38 ops, M-series 10 cores, release):
227.7K → 230.1K msg/s (within run-to-run noise). P50 23 μs unchanged.
- New async-handler benchmark: ~1.2 μs/call framework overhead for the
dyn-Any dispatch path (typed-input downcast + TaskContext alloc +
change-buffer drain + audit-entry write).
### Wire compatibility
- `Message`, `AuditTrail`, `Change`, `ErrorInfo`, `Workflow`, `Task`,
`FunctionConfig` JSON shapes are **unchanged** within the v3.0.0 dev
line. The `FunctionConfig::Custom.compiled_input` field is
`#[serde(skip)]`; it round-trips through JSON as `None` and is
re-populated when the workflow is loaded into the engine.
### Earlier v3.0.0 work (commit `c375ec6`)
Datalogic v5 integration, sync-stretch arena reuse, hot-path perf work,
and fail-loud `Engine::new` (compile every JSONLogic at startup, return
`Err` on any failure). See commits `c8775fd..c375ec6` for the full set.