chore(release): 0.9.0 — the stub sweep
- version 0.8.1 -> 0.9.0 (minor bump: 5 breaking lints vs 0.8.1 — ManagerError::InvalidAdopt variant, RelayError::PubTypedOpen removal, register_imported* Result return, Establishment Debug derive dropped; all pre-1.0 permitted) - CHANGELOG 0.9.0: S-01..S-04 from reviews 010/011 (the marked-pub open contract, channel/control, channel/resources/subscribe, op/register forward-through), the stub guard, and the verification summary - open_pub_channel: a non-object input tears down the freshly allocated id before the early return (the slot is not parked against the ADR-040 bound; regression test added) - register_imported_all: atomic — a refusal leaves the overlay untouched; atomicity documented Verified: cargo test (697 + stub guard), clippy --all-targets --all-features -- -D warnings, fmt --check, cargo doc, wasm32-unknown-unknown check, fuzz corpus replay 5/5, cargo publish --dry-run --allow-dirty clean.
This commit is contained in:
1 parent
4278c13b2d
commit
1a493e9748
6 files changed
+138
-3
No files matched your search
@@ -4,6 +4,103 @@ All notable changes to this crate are documented here. The format is
|
||||
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
this crate adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.9.0] - 2026-10-14
|
||||
|
||||
The stub sweep: four registered-but-always-failing stubs shipped across
|
||||
nine published versions (reviews 010/011) are now real implementations
|
||||
— the marked-pub open contract (S-01), `channel/control` (S-02),
|
||||
`channel/resources/subscribe` (S-03), and `op/register`
|
||||
forward-through (S-04). Every handler that registers on a discoverable
|
||||
surface can now succeed; the four stubs are replaced and the class is
|
||||
release-blocking in both directions (a loud-failure stub is still a
|
||||
stub — the stub guard plus registration-time invariants hold the gate).
|
||||
|
||||
**Breaking (semver-relevant, hence the minor bump).** New variant
|
||||
`ManagerError::InvalidAdopt`; `RelayError::PubTypedOpen` removed (the
|
||||
relay put-translation now allocates and opens on the producer leg);
|
||||
`register_imported`/`register_imported_all` return `Result<(), String>`
|
||||
(beyond the boolean); `Establishment` no longer derives `Debug` (the
|
||||
establisher-contributed `ChannelControl` inner is not `Debug`).
|
||||
|
||||
### Added
|
||||
|
||||
- **The marked-pub open contract — S-01 (ADR-047 amendment 4, review
|
||||
011 §3).** The put path end-to-end:
|
||||
- `ChannelCore::register_pub_openable` + `PubOpenHandler` — the
|
||||
put's handler returns its result for the merged single response;
|
||||
the wrapper awaits it inline, teardown is EOF-driven, no
|
||||
`handler_task` is ever spawned. `register_openable*` refuses
|
||||
`Pub`-typed specs; the wrapper entry points refuse unmarked /
|
||||
non-`Pub` shapes.
|
||||
- The reserved input key `channel_id` (the initiator-allocated id)
|
||||
is stripped pre-validation by `resolve_sink_handler` (P-08, the
|
||||
single source of truth), handed to the wrapper via
|
||||
`OperationContext.metadata`; a missing key rejects
|
||||
`INVALID_INPUT`; adopt parity is enforced
|
||||
(`ManagerError::InvalidAdopt`) and adopt failures answer
|
||||
`channel:open_failed` / reason `adopt_failed`.
|
||||
- Zero `call.published` on marked ops: the first publish chunk on a
|
||||
marked op is rejected loudly (`channel:publish_chunks_rejected`)
|
||||
in both dispatch loops; the handler is never fed.
|
||||
- The relay's put-translation (ADR-042): the hub allocates odd on
|
||||
the producer leg, opens the spoke op in the background
|
||||
(put-acks-in-result), pumps one direction only, and mirrors the
|
||||
response with the producer's reserved key stripped.
|
||||
- `ChannelClient::open_pub_channel` + `PubOpenReply` — the
|
||||
initiator-allocated put open capability (odd ids on the connect
|
||||
side, ADR-047 §5). A failed open tears the local adopt entry down,
|
||||
including the pre-flight INVALID_INPUT path (the allocated id is
|
||||
released, not parked against the ADR-040 bound).
|
||||
- **`channel/control` implemented — S-02 (ADR-052).** The
|
||||
establisher-contributed control handle (`Establishment::with_control`)
|
||||
is registered manager-side at establishment, removed on teardown, and
|
||||
reachable through the `channel/control` op; an establisher without a
|
||||
control handle answers `channel:no_control_handle`.
|
||||
- **`channel/resources/subscribe` implemented — S-03 (ADR-053).** The
|
||||
per-peer-connection `ResourceEnumerators` registry with
|
||||
snapshot-then-changes enumerators, scan-once aggregation over the
|
||||
channel operations, and the empty seed; the always-failing one-shot
|
||||
error stream is gone.
|
||||
- **`op/register` forward-through — S-04 (ADR-022 amendment).**
|
||||
Announced unmarked `Sub`/`Pub` operations register the real
|
||||
streaming/sink forwarding shapes; marked announcements are refused
|
||||
with `MARKED_SPEC_UNSUPPORTED`; marked specs refuse both the generic
|
||||
registration path (`OperationRegistry::register`, `op/register`) and
|
||||
the connection-overlay path (`CallConnection::register_imported`
|
||||
— now returning `Result`; `register_imported_all` is atomic: a
|
||||
refusal leaves the overlay untouched).
|
||||
|
||||
### Changed
|
||||
|
||||
- `HandlerRegistration` gained the private `channel_wrapper` field
|
||||
(the channel wrapper family's plumbing); externally constructed
|
||||
registrations stay impossible (`HandlerRegistration::new` unchanged).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`open_pub_channel` no longer parks a channel slot on the
|
||||
pre-flight rejection.** The early-return on a non-object input now
|
||||
tears down the freshly allocated id (regression:
|
||||
`open_pub_channel_invalid_input_releases_the_allocated_id`).
|
||||
- `Capabilities::zeroize`: an unused map-key iteration tripped
|
||||
`clippy::for_kv_map` under `-D warnings`. No behavior change.
|
||||
|
||||
### Guard
|
||||
|
||||
- `tests/stub_guard.rs` — `cargo test` fails if `not_implemented`
|
||||
appears anywhere under `src/`: the four former pinned-stub tests are
|
||||
deleted and the marked-spec registration refusals are pinned by
|
||||
tests instead.
|
||||
|
||||
### Verified
|
||||
|
||||
- 698 tests pass (including the stub guard); `clippy --all-targets
|
||||
--all-features -- -D warnings`, `fmt --check`, `cargo doc`,
|
||||
`wasm32-unknown-unknown` check, fuzz corpus replay
|
||||
(`cargo test --manifest-path fuzz/shared/Cargo.toml`, 5/5), and
|
||||
`cargo semver-checks` (5 breaking lints vs 0.8.1 — the minor-bump
|
||||
classification above).
|
||||
|
||||
## [0.8.1] - 2026-09-28
|
||||
|
||||
Bug-fix release: a duplicate `adopt_channel`/`open_channel` no longer
|
||||
@@ -737,6 +834,7 @@ Vendored core types (`Connection`, `ProtocolHandler`, `BiStream`,
|
||||
(ADR-046), the channels protocol with openable-ALPNs-as-operations
|
||||
(ADR-047), and the `ChannelClient` transport-agnostic client.
|
||||
|
||||
[0.9.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.9.0
|
||||
[0.8.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.8.1
|
||||
[0.8.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.8.0
|
||||
[0.7.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.7.1
|
||||
|
||||
Generated
+1
-1
@@ -27,7 +27,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alkcall"
|
||||
version = "0.8.1"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "alkcall"
|
||||
version = "0.8.1"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -471,6 +471,7 @@ impl ChannelClient {
|
||||
);
|
||||
}
|
||||
None => {
|
||||
let _ = self.manager.teardown_channel(channel_id);
|
||||
return Err(ChannelOpenError::CallFailed {
|
||||
error: crate::protocol::wire::CallError::invalid_input(
|
||||
"marked pub open op input must be an object (ADR-047 amendment 4 §4.3)",
|
||||
|
||||
@@ -445,3 +445,33 @@ async fn put_handler_error_becomes_the_response() {
|
||||
Some("blobs:read_only".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
/// A non-object input to `open_pub_channel` is rejected INVALID_INPUT
|
||||
/// before any wire traffic — and the locally allocated channel id is
|
||||
/// released (teardown) so the slot is not parked until connection
|
||||
/// close (the 256-per-connection bound, ADR-040).
|
||||
#[tokio::test]
|
||||
async fn open_pub_channel_invalid_input_releases_the_allocated_id() {
|
||||
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
|
||||
let client_conn = Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
|
||||
let _captured = serve_put_side(server_end, token_establisher(), digest_handler()).await;
|
||||
|
||||
let client = ChannelClient::from_connection(client_conn)
|
||||
.await
|
||||
.expect("client");
|
||||
let before = client.manager().open_count();
|
||||
assert!(before >= 1, "channel 0 is installed at client init");
|
||||
let error = match client.open_pub_channel(PUT_OP, json!(7), "alk/blobs").await {
|
||||
Ok(_) => panic!("a non-object input is rejected"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert_eq!(
|
||||
error.call_error().map(|e| e.code.as_str()),
|
||||
Some("INVALID_INPUT")
|
||||
);
|
||||
assert_eq!(
|
||||
client.manager().open_count(),
|
||||
before,
|
||||
"the allocated id is not parked on the early-return path"
|
||||
);
|
||||
}
|
||||
@@ -194,12 +194,16 @@ impl CallConnection {
|
||||
self.imported_operations.read().get(name).cloned()
|
||||
}
|
||||
|
||||
/// Install a bundle of remote announcements on the connection
|
||||
/// overlay. Atomic: if any registration carries the `channel_open`
|
||||
/// marker, the overlay is left untouched (nothing partially
|
||||
/// installed).
|
||||
pub fn register_imported_all(
|
||||
&self,
|
||||
registrations: Vec<HandlerRegistration>,
|
||||
) -> Result<(), String> {
|
||||
let mut overlay = self.imported_operations.write();
|
||||
for reg in registrations {
|
||||
for reg in ®istrations {
|
||||
if reg.spec.channel_open.is_some() {
|
||||
return Err(format!(
|
||||
"register_imported_all: spec `{}` carries the `channel_open` marker — \
|
||||
@@ -208,6 +212,8 @@ impl CallConnection {
|
||||
reg.spec.name
|
||||
));
|
||||
}
|
||||
}
|
||||
for reg in registrations {
|
||||
let name = reg.spec.name.clone();
|
||||
overlay.insert(name, reg);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user