# Change Log
## [0.5.0] — 2026-04-20
First release of the async-first rewrite. Breaking change vs.
`0.4.0` — the entire public surface was rebuilt around an actor
pattern with two frontends (async + blocking). The pre-0.5 sync
prototype (`Request` / `Subscribe` directly at the crate root) is
gone; migrate via the `motorcortex_rust::core::*` (async) or
`motorcortex_rust::blocking::*` (sync façade) modules.
### Headline changes
- **Actor-style async core** — `core::Request` + `core::Subscribe`
handles (`Clone + Send + Sync`) backed by dedicated driver
threads. Multiple cloned handles multiplex onto one driver; no
user-visible `Mutex` needed to serialise NNG req/rep ordering.
- **`core::Subscription` with three opt-in sinks** — sync
`notify(cb)`, async `latest().await` (watch-backed, lossy),
async `stream(capacity)` (broadcast-backed, lossless within the
ring, explicit `Err(Missed(n))` on lag).
- **Blocking façade** at `motorcortex_rust::blocking::*` — same
method names and semantics as the async core, just without
`.await`. Each handle owns a hidden current-thread tokio runtime.
- **`ConnectionState`** (`Disconnected` / `Connected` /
`ConnectionLost` / `SessionExpired`) published through
`tokio::sync::watch` on both `Request` and `Subscribe`.
- **Session tokens + automatic reconnect** — driver periodically
refreshes the server-issued session token on a live pipe, pauses
while `ConnectionLost` / `SessionExpired`, and on reconnect
calls `RestoreSession(token)` to resume without a re-login.
`ConnectionOptions` gained `reconnect: bool` (default `true`),
`reconnect_min` / `reconnect_max` (100 ms / 30 s defaults on the
NNG dialer), `token_refresh_interval` (30 s default, matches
python), and `max_reconnect_attempts: Option<u32>` safety net.
- **`Subscribe::resubscribe(&req)`** — re-registers every active
subscription after a reconnect that lost server-side group
state. Outstanding `Subscription` handles stay valid; the
driver rebinds each one in place with the fresh server-assigned
id.
- **`parse_url`** free function — splits Motorcortex's
`scheme://host:req_port:sub_port` convention into two dial URLs.
Handles hostnames, IPv4, and bracketed IPv6. `Request::connect_to`
/ `Subscribe::connect_to` convenience constructors on top.
### Error handling rules
- RPCs returning `Result<StatusCode>` never convert a non-OK server
reply into `Err` — callers branch on the returned code. `Err`
means transport / decode trouble. Applies uniformly to
`login` / `logout` / `set_parameter` / `set_parameters` /
`request_parameter_tree` / `remove_group` /
`restore_session`. `remove_group` used to error on non-OK —
that's the one behavioural change from `0.4.0` if you were
catching `MotorcortexError::Status` on it.
- Fixed several panic paths on user-controllable input:
`CString::new(cert_path)` and `CString::new(url)` with an
embedded NUL byte now return `MotorcortexError::Connection`;
`DataType::try_from` in the parameter-value dispatchers returns
`V::default()` (decoder) / empty `Vec` (encoder) for unknown
wire codes instead of `.unwrap()`-panicking.
### Dependencies
- Added `tokio` (features: `rt`, `rt-multi-thread`, `sync`,
`time`, `macros`) and `futures` (for the `Stream` trait on
`Subscription::stream`).
### Tests and tooling
- `cargo test --lib` — 84 tests including the pure state-machine
helper for the reconnect handler.
- `cargo test --test unit` — 53 offline tests.
- `cargo test --test integration -- --test-threads=1` — 39 tests
plus one `#[ignore]`d stress test
(`stress_16_clones_hot_get_parameter`).
- Merged coverage (lib + unit + integration) on the release tip:
**90.69 %** regions / **90.07 %** lines / **93.10 %** functions.
Every `core/*` module and every `blocking/*` module clears the
80-90 % phase-5 gate.
- Four runnable examples under `examples/`: `async_request`,
`blocking_request`, `subscribe_latest`, `subscribe_stream`.
- `tests/server/main.cpp` gained the `MCX_TEST_SERVER_LIFETIME_SEC`
watchdog so reconnect tests can simulate a crash without harness
support.
### Removed
- Legacy sync `Request`, `Subscribe`, `Subscription`, and
`ReadOnlySubscription` at the crate root — replaced by
`core::*`.
- `Connection` trait — existed only to unify the deleted sync
types.
- `API.md` — the hand-maintained reference doc for the old sync
surface. Superseded by `README.md` (getting started + examples)
+ `ARCHITECTURE.md` (design + internals) + `cargo doc`.
### Housekeeping
- `CHANGE.md` renamed to `CHANGELOG.md`.
- Added a `LICENSE` file (MIT) matching the existing
`license = "MIT"` in `Cargo.toml`.
- `Cargo.toml` gained `readme`, `documentation`, `keywords`,
`categories`, and `rust-version = "1.85"` for crates.io /
docs.rs discoverability.
- README picked up a "Running behind nginx" section covering the
token-refresh-vs-proxy-timeout story.
---
## [0.4.0] and earlier — pre-rewrite synchronous surface
Re-architects the crate around an actor pattern with two usable
frontends (async and blocking). Async is the primary surface; the
legacy synchronous types are gone.
### Added
**`motorcortex_rust::core::*` — the async core.**
- `Request` handle (`Clone + Send + Sync`), backed by a dedicated
`std::thread` driver that owns a `ConnectionManager`. Commands
cross into the driver via `tokio::sync::mpsc::UnboundedSender`;
replies come back on `tokio::sync::oneshot::Receiver`.
Multiple cloned handles multiplex onto one driver, which
enforces NNG's Req/Rep ordering in the type system without a
user-visible `Mutex<…>`.
- Methods: `new` / `default` / `clone` / `state()` /
`parameter_tree()` / `connect` / `disconnect` / `connect_to` /
`login` / `logout` / `request_parameter_tree` /
`get_parameter` / `set_parameter` / `get_parameters` /
`set_parameters` (tuple types) / `create_group` /
`remove_group` / `get_parameter_tree_hash`.
- `Subscribe` handle (`Clone + Send + Sync`) with a second
dedicated thread on connect that loops on `nng_recv` and
dispatches payloads to the matching `Subscription::update`.
Methods: `new` / `connect` / `disconnect` / `connect_to` /
`subscribe(&Request, paths, alias, fdiv)` /
`unsubscribe(&Request, Subscription)` /
`resubscribe(&Request)` / `state()`.
- `Subscription` handle (`Clone + Send + Sync`) with three
independent opt-in sinks:
- `notify<F>(cb)` — sync fire-and-forget callback, runs on the
receive thread, zero allocation.
- `latest<V>().await` — `tokio::sync::watch` backed; resolves to
the most recent payload, lossy by design.
- `stream<V>(capacity)` — lazy `tokio::sync::broadcast`,
returns `impl Stream<Item = StreamResult<V>>`. Bounded ring
with explicit `Err(Missed(n))` back-pressure when the consumer
lags. Not allocated unless someone actually calls it.
- Plus sync `read<V>` / `read_all<V>` decoding the latest
payload, `id` / `name` / `paths` / `fdiv` for descriptive
metadata. `id` is atomic and `paths` reads through an internal
`RwLock` so `Subscribe::resubscribe` can swap the server-side
group descriptor in place without invalidating outstanding
clones.
- `ConnectionState` enum (`Disconnected` / `Connected` /
`ConnectionLost` / `SessionExpired`) published via
`tokio::sync::watch`. Only Disconnected / Connected emitted in
Phase 1–3; pipe-event-driven transitions wire up in Phase 4
alongside session-token reconnect.
- Shared parameter-tree cache: `Request::parameter_tree()` returns
an `Arc<RwLock<ParameterTree>>` shared with the driver. Cloned
handles observe the same cache. Populated by
`request_parameter_tree`, used client-side by
`get_parameter` / `set_parameter` so RPCs stay a single channel
hop.
**`motorcortex_rust::blocking::*` — sync façade over the async core.**
- `blocking::Request` / `Subscribe` / `Subscription` with the
same method names as `core::*`, all sync (no `.await`). Each
handle owns a single-threaded `tokio::runtime::Runtime` and
forwards every call through `rt.block_on(self.inner.method())`.
- `Subscription::iter::<V>(capacity)` returns a blocking
`Iterator` over the broadcast ring — same `Missed(n)` semantics
as the async `stream()`.
- `as_async()` on each blocking handle exposes the inner async
type for the hybrid case where one part of an app is blocking
and another is async.
**Free helpers.**
- `motorcortex_rust::parse_url(&str) -> Result<(String, String)>`
splits the Motorcortex dual-port convention
(`scheme://host:req_port:sub_port`) into two dial URLs. Handles
hostnames, IPv4, and bracketed IPv6. Mirrors the role of
`motorcortex-python`'s `parseUrl` but with just one output
flavour (two URLs, no path-based fallback).
**Tests and docs.**
- `tests/integration/blocking.rs` — end-to-end smoke tests for
the sync façade (lifecycle, callback fires, blocking iter
surfaces the value we wrote).
- `ARCHITECTURE.md` now documents the three-sink Subscription
model and the full `stream()` data flow (fan-out, unfold, back-
pressure ring semantics). Phase 4 reconnect design is baked in.
- `README.md` rewritten around the new public surface with four
example snippets (async, subscribe, blocking, state observer).
- `TODO.md` tracks phase progress and the remaining Phase 4
(reconnect + session tokens) + Phase 5 (polish) work.
### Changed
- **Async core replaces the legacy sync types** entirely — see
*Removed* below.
- CI `unit-tests` job runs `cargo llvm-cov --lib` so
`#[cfg(test)]` modules inside `src/` land in the merged
coverage report.
- README pipeline / coverage badges temporarily point at
`async-rewrite`; swap back once the rewrite merges.
- `TimeSpec` moved from `src/client/subscription.rs` to a
dedicated `src/time_spec.rs`. Same semantics, same public path
at the crate root (`motorcortex_rust::TimeSpec`).
### Removed
- Legacy sync `Request`, `Subscribe`, `Subscription`, and
`ReadOnlySubscription` — replaced by `core::*`.
~1280 lines deleted from `src/client/`.
- `Connection` trait — it existed only to unify the two deleted
sync types; nobody consumes it.
- `tests/integration/{connect,session,parameters,subscribe,tree}.rs`
— every scenario already covered by the new
`tests/integration/async_*.rs` suite against `core::*`.
- `tests/common/connection.rs` — `setup_connection` /
`safe_disconnect` only existed to glue the legacy sync handle
to the ctor-managed test_server; the async tests use
`Request::connect_to(&url, opts).await` directly.
- `tests/integration/subscribe.rs::test_subscribe_read_all` —
had been `#[ignore]`d indefinitely because its
`elapsed_time <= 1000 ms` assertion bracketed a deliberate
`sleep(1000 ms)` and could never pass. Fresh coverage of the
flatten-everything API lands against `core::Subscription`
against the new stack.
### Fixed (kept from `development`, carried into this branch)
- Same `Request::get_parameter_tree_hash` reply-type fix from
`development` is baked into the new `core::Request`
implementation — `GetParameterTreeHashMsg` is decoded as
`ParameterTreeHashMsg` from day one.
### Tests — running tally on branch tip
- `cargo test --lib` — **60** (up from 2 on `development`). Of
those, 48 are async / core::* tests; 12 cover `parse_url`.
- `cargo test --test unit` — **53** (down from 80, since the
redundant legacy `request_error_paths` and `subscription`
modules were replaced by lib tests inside `src/core/{proto,
request,subscription}.rs`).
- `cargo test --test integration -- --test-threads=1` — **23**,
**zero ignored** (up from 10 tests + 1 ignored on
`development`).
- `cargo test --doc` — 3 (new).
- Merged coverage on branch tip: **94.95 %** regions, **94.83 %**
lines, **96.57 %** functions. Ahead of `development`'s 93.99 %.
---
## Unreleased
### Added
- Split test suite into `tests/unit.rs` (offline) and `tests/integration/` (server-driven), mirroring the motorcortex-python layout.
- GitLab CI pipeline (`.gitlab-ci.yml`) with `build` / `test` / `coverage` / `release` stages; coverage collected via `cargo-llvm-cov` and published as Cobertura.
- `tests/README.md` with the testing walkthrough, layout diagram, and a *Known gaps* section.
- README badges: pipeline status, coverage report, crates.io version, Rust edition, MIT license.
- README *CI runner setup* section documenting the prerequisites needed to provision a GitLab runner for the pipeline.
- Committed TLS test certs in the repo (`tests/server/config/motorcortex.pem` + `tests/mcx.cert.crt`, Root CA). Integration tests resolve the CA via `CARGO_MANIFEST_DIR` instead of relying on `/etc/ssl/certs/mcx.cert.pem` on the host — mirrors motorcortex-python and lets CI run without runner-side cert setup.
- Expanded `tests/unit.rs` (2 → 80 offline tests) mirroring motorcortex-python's layout. New modules: `version`, `parameter_tree` (empty/populate/non-OK/unknown-path/iterator filter), `primitive_types` (wire-level `SetParameterValue`/`GetParameterValue` round-trips for every scalar dtype + arrays/vecs), `message_types` (proto round-trips for `LoginMsg`, `StatusMsg`, `ParameterTreeMsg`), `status_code` (`Ok = 0` invariant + `as_str_name`/`from_str_name` round-trip over every variant), `init_threads` (smoke tests for the NNG init wrapper), `logger` (`LogLevel` variants + init entry points), `timespec` (field access, `from_buffer` layout + short-input guard, chrono `DateTime` round-trips), `error_display` (every `MotorcortexError` variant + `From<prost::DecodeError>`), `set_parameter_value_coverage` / `get_parameter_value_coverage` (exhaustive per-impl method sweeps over scalars / `[T; 2]` / `Vec<T>`), `parameters_coverage` (all four `Parameters::into_vec` impls), `subscription` (protocol-1 buffer decode via hand-crafted fixtures — single scalar, two-param tuple, array parameter; protocol-0 / unknown-version bail-outs; callback invocation via `update`; full `ReadOnlySubscription` delegation), `request_error_paths` (`decode_message` hash/size/body errors, every `Request` method without `connect()` hits `Socket is not available`, empty-tree `ParameterNotFound`). Also added `#[cfg(test)] mod tests` inside `parameter_value/processing.rs` for crate-private `encode_parameter_value`/`decode_parameter_value` fallthrough coverage (`Bytes`/`Char`/`UserType`/`Undefined`); CI's `unit-tests` job now runs `cargo llvm-cov --lib` alongside `--test unit` so those lib tests land in the merged report. Two integration tests added in `tree.rs` (`test_get_parameter_tree_hash`, `test_remove_nonexistent_group_returns_error_status`). Region coverage climbs from ~0.5% to ~94%.
### Changed
- README test snippet now documents the two-binary workflow (`--test unit` + `--test integration -- --test-threads=1`).
- CI now installs rustup + the stable toolchain into `$CI_PROJECT_DIR/.rustup` via a shared `.rust-setup` `before_script`, removing the need for a runner-side Rust install. Toolchain and `cargo-llvm-cov` binary are cached alongside the cargo registry.
- Re-exported `TimeSpec` from the crate root (previously reachable only as `client::subscription::TimeSpec`, which the crate never made `pub`). It already leaked through `Subscription::read` return types, so surfacing it is a no-op for downstream code and unblocks writing unit tests against it.
### Fixed
- `Request::get_parameter_tree_hash` now decodes the reply as `ParameterTreeHashMsg` (it was decoding `ParameterTreeMsg`, whose hash doesn't match the server's reply — every call errored with `Invalid message hash`). Surfaced by the new `test_get_parameter_tree_hash` integration test.
### Removed
- `tests/integration/subscribe.rs::test_subscribe_read_all` — the
only integration test covering `Subscription::read_all::<V>`. Its
timing assertion was broken by construction (elapsed measured
across an explicit 1 s sleep + asserted `≤ 1000 ms`), so the test
had been `#[ignore]`d indefinitely. Fresh coverage of the
flatten-everything API will land against `core::Subscription::stream`
in Phase 2 instead of reviving the legacy test.
### Known issues
- Integration tests share `root/Control/dummy*` parameters on a single server instance, so `--test-threads=1` is mandatory.
## 4.0
### Added
- `Send` implementation for `request` and `subscribe`.
- Errors infrastructure (#2417309).
- API documentation (#e925fb9).
- Interface returning an iterator for the parameter tree (#ad68262).
- Number of threads initialization (#fc6c2e5).
### Fixed
- Improved possible resource leak (#c30d250).
- Fixed IP issues (#f1ccf89).
- Decoding bug fix (#922d409).
- Fixed mbedtls resolution to use the local version (#c8e30a3).
### Changed
- Moved socket creation from `new` to `connect` (#8efa9bb).
- Updated documentation for `request` and `subscribe` relationship (#93444c5).
- Versions updated multiple times (#fef6b30, #fcffaf0, #b18fd0c).
- Refactored large sections of codebase (#fd59a31, #d0caf45).
### Removed
- Vendor and lock files (#d84583c).