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
|
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||||
this crate adheres to [Semantic Versioning](https://semver.org/).
|
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
|
## [0.8.1] - 2026-09-28
|
||||||
|
|
||||||
Bug-fix release: a duplicate `adopt_channel`/`open_channel` no longer
|
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-046), the channels protocol with openable-ALPNs-as-operations
|
||||||
(ADR-047), and the `ChannelClient` transport-agnostic client.
|
(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.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.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
|
[0.7.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.7.1
|
||||||
|
|||||||
Generated
+1
-1
@@ -27,7 +27,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alkcall"
|
name = "alkcall"
|
||||||
version = "0.8.1"
|
version = "0.9.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "alkcall"
|
name = "alkcall"
|
||||||
version = "0.8.1"
|
version = "0.9.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -471,6 +471,7 @@ impl ChannelClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
let _ = self.manager.teardown_channel(channel_id);
|
||||||
return Err(ChannelOpenError::CallFailed {
|
return Err(ChannelOpenError::CallFailed {
|
||||||
error: crate::protocol::wire::CallError::invalid_input(
|
error: crate::protocol::wire::CallError::invalid_input(
|
||||||
"marked pub open op input must be an object (ADR-047 amendment 4 §4.3)",
|
"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())
|
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()
|
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(
|
pub fn register_imported_all(
|
||||||
&self,
|
&self,
|
||||||
registrations: Vec<HandlerRegistration>,
|
registrations: Vec<HandlerRegistration>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut overlay = self.imported_operations.write();
|
let mut overlay = self.imported_operations.write();
|
||||||
for reg in registrations {
|
for reg in ®istrations {
|
||||||
if reg.spec.channel_open.is_some() {
|
if reg.spec.channel_open.is_some() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"register_imported_all: spec `{}` carries the `channel_open` marker — \
|
"register_imported_all: spec `{}` carries the `channel_open` marker — \
|
||||||
@@ -208,6 +212,8 @@ impl CallConnection {
|
|||||||
reg.spec.name
|
reg.spec.name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
for reg in registrations {
|
||||||
let name = reg.spec.name.clone();
|
let name = reg.spec.name.clone();
|
||||||
overlay.insert(name, reg);
|
overlay.insert(name, reg);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user