Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/coverage-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ jobs:
CARGO_TERM_COLOR: always
BUILD_PROFILE: debug
steps:
- uses: actions/checkout@v7
# CodeScene derives github.com/leynos/wireframe from this checkout's
# origin.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
repository: leynos/wireframe
persist-credentials: false
- name: Setup Rust
uses: leynos/shared-actions/.github/actions/setup-rust@f4764bea8d813b1a8f7ebc37a44907d3c3b1e0e4
- name: Test and Measure Coverage
Expand All @@ -38,5 +43,8 @@ jobs:
uses: leynos/shared-actions/.github/actions/upload-codescene-coverage@f4764bea8d813b1a8f7ebc37a44907d3c3b1e0e4
with:
format: lcov
mode: upload
# Keep this project identity aligned with ci.yml's PR gate.
project-url: https://api.codescene.io/v2/projects/68308
access-token: ${{ env.CS_ACCESS_TOKEN }}
installer-checksum: ${{ vars.CODESCENE_CLI_SHA256 }}
5 changes: 5 additions & 0 deletions docs/contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ the-road-to-wireframe-1-0-feature-set-philosophy-and-capability-maturity.md
- [Testing helpers](wireframe-testing-crate.md) In-process server and client
pair helpers provided by the `wireframe_testing` companion crate.

## Migration guides

- [v0.3.0 to v0.4.0 migration guide](v0-3-0-to-v0-4-0-migration-guide.md)
Moving from builder connection handling to reusable prepared applications.

## Operations and resilience

- [Resilience guide](hardening-wireframe-a-guide-to-production-resilience.md)
Expand Down
35 changes: 33 additions & 2 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,22 @@ boundaries for Epic 635:
client-pool scheduler a single persistent owner task and index-based slot
leases beneath one `PoolCore` root.

These records are proposed, not yet accepted; the review checklist derived from
ADR 011's rules lands with their implementation epic.
The first implementation slice is now in place: consuming
`WireframeApp::prepare().await` returns an immutable `PreparedApp` or a typed
`PrepareError`. Preparation consumes route and middleware registrations and
builds each route chain once. Connection tasks borrow the prepared route table,
so a single prepared application can serve multiple connections without
repeating middleware transforms. `WireframeApp` remains the registration
builder, and its direct connection methods are compatibility APIs.

Server factory evaluation and readiness semantics remain unchanged in this
slice. The server-runtime work tracked by issue
[#642](https://github.com/leynos/wireframe/issues/642) will prepare the factory
result before server readiness; connection-local state and the
`ConnectionRuntime` follow in issue
[#643](https://github.com/leynos/wireframe/issues/643). The records remain
proposed, and the review checklist derived from ADR 011's rules lands with
their implementation epic.

### Server supervisor lifecycle

Expand Down Expand Up @@ -234,6 +248,23 @@ Install Whitaker through the standalone installer described in the
[Whitaker user's guide](whitaker-users-guide.md) so local linting matches
continuous integration (CI).

### CodeScene coverage baseline

The `Coverage (main)` workflow in `.github/workflows/coverage-main.yml` runs on
pushes to `main`. After the test suite succeeds, it generates a ratcheted LCOV
report and uploads that report to CodeScene. The workflow checks out
`leynos/wireframe`, so CodeScene records the coverage under the repository
identity `github.com/leynos/wireframe`, and targets project `68308` explicitly.

The upload reads `CS_ACCESS_TOKEN` from the repository secret into the job
environment, then passes that value through the upload action's required
`access-token` input. This workflow input is permitted because the value still
comes from the repository secret; never hard-code the token in workflow or
source files, and never log it. The pull-request workflow's CodeScene coverage
check uses the same project and repository identity. It consumes the report
published for `main` as the baseline for its changed-line gate, so the main
workflow must publish successfully before that gate can evaluate a pull request.

## Mutation testing

Scheduled mutation testing runs in CI via
Expand Down
18 changes: 18 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -877,3 +877,21 @@ and usability.
- [ ] 19.4.1. Ensure all public items have clear, useful documentation
examples.
- [ ] 19.4.2. Publish documentation to `docs.rs`.

## 20. Prepared application and runtime ownership (in progress)

This phase makes the builder-to-runtime ownership boundary explicit while
sequencing the remaining server and connection-runtime work separately.

### 20.1. Prepared application transition

- [x] 20.1.1. Add the consuming `WireframeApp::prepare().await` transition and
immutable `PreparedApp`, building route middleware chains once and providing
prepared connection drivers. See issue
[#641](https://github.com/leynos/wireframe/issues/641) and
[ADR 012](adr-012-prepared-application-and-connection-runtime.md).
- [ ] 20.1.2. Prepare the application factory before server readiness. See
issue [#642](https://github.com/leynos/wireframe/issues/642).
- [ ] 20.1.3. Extract connection-local runtime ownership and lifecycle
finalization. See issue
[#643](https://github.com/leynos/wireframe/issues/643).
70 changes: 56 additions & 14 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ For invariants and naming rules used across internal modules, see the

A `WireframeApp` collects route handlers and middleware. Each handler is stored
as an `Arc` pointing to an async function that receives a packet reference and
returns `()`. The builder caches these registrations until `handle_connection`
constructs the middleware chain for an accepted stream.[^2]
returns `()`. For manually accepted streams, consume the builder with
`prepare().await` after registration to construct immutable middleware chains
once and obtain a `PreparedApp`. The transition returns
`Result<PreparedApp, PrepareError>`; a preparation failure therefore produces
no partially usable runtime.[^2]

```no_run
use std::sync::Arc;
Expand Down Expand Up @@ -175,11 +178,31 @@ fn inspect_transport_source(error: &WireframeError) -> Option<&dyn Error> {
}
```

Once a stream is accepted—either from a manual accept loop or via
`WireframeServer`—`handle_connection(stream)` builds (or reuses) the middleware
chain, wraps the transport in the configured frame codec (length-delimited by
default), enforces per-frame read timeouts, and writes responses. Serialization
helpers `send_response` and `send_response_framed` (or
For a manually accepted stream, prepare the application once and then call
`PreparedApp::handle_connection_result(stream)` (or the logging
`PreparedApp::handle_connection(stream)` wrapper) for every connection. The
prepared application reuses its middleware chains, wraps each transport in the
configured frame codec (length-delimited by default), enforces per-frame read
timeouts, and writes responses. The deprecated
`WireframeApp::handle_connection` methods rebuild route chains for
compatibility and should not be used in new code.

```rust,no_run
use tokio::io::duplex;
use wireframe::app::{PreparedApp, WireframeApp};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let prepared: PreparedApp = WireframeApp::new()?.prepare().await?;
let (client, server) = duplex(64);
drop(client);
prepared.handle_connection_result(server).await?;
# Ok(())
# }
```

`WireframeServer` still accepts a builder factory and retains its existing
per-connection factory evaluation semantics until the server-runtime migration
lands. Serialization helpers `send_response` and `send_response_framed` (or
`send_response_framed_with_codec` for custom codecs) return typed `SendError`
variants when encoding or I/O fails, and the connection closes after ten
consecutive deserialization errors.[^6][^7]
Expand Down Expand Up @@ -1359,11 +1382,13 @@ additional ergonomics on top of the core primitives.[^13]

`WireframeApp` supports optional setup and teardown callbacks that run once per
connection. Setup can return arbitrary state retained until teardown executes
after the stream finishes processing.[^2] During `handle_connection` the
framework caches middleware chains, enforces read timeouts, and records metrics
for inbound frames, serialization failures, and handler errors before logging
warnings.[^6][^7] `PacketParts::inherit_correlation` ensures response packets
carry the correct correlation identifier even when middleware omits it.[^8]
after the stream finishes processing.[^2] Preparation moves those callback
definitions into the immutable `PreparedApp`; `PreparedApp::handle_connection`
reuses the prepared middleware chains, enforces read timeouts, and records
metrics for inbound frames, serialization failures, and handler errors before
logging warnings.[^6][^7] `PacketParts::inherit_correlation` ensures response
packets carry the correct correlation identifier even when middleware omits
it.[^8]

Immediate responses are available through `send_response` and
`send_response_framed`, both of which report serialization or I/O problems via
Expand Down Expand Up @@ -2543,8 +2568,25 @@ When the optional `metrics` feature is enabled, Wireframe updates the
`wireframe_connections_active` gauge, frame counters tagged by direction, error
counters tagged by kind, and a counter for panicking connection tasks. All
helpers become no-ops when the feature is disabled so instrumentation can stay
in place.[^33] `handle_connection`, the connection actor, and the panic wrapper
call these helpers to maintain consistent telemetry.[^6][^7][^31][^20]
in place.[^33] `PreparedApp::handle_connection`, the connection actor, and the
panic wrapper call these helpers to maintain consistent telemetry.[^6][^7][^31][^20]

Prepared application lifecycle metrics are also emitted when the `metrics`
feature is enabled:

- `wireframe_application_preparations_total` counts each preparation attempt.
Its bounded `outcome` label is `"success"` when an immutable `PreparedApp` is
produced and `"failure"` when preparation returns an error.
- `wireframe_application_preparation_duration_seconds` records the duration of
each preparation attempt, using the same `outcome` label values.
- `wireframe_prepared_connection_uses_total` counts each connection handled by
`PreparedApp::handle_connection_result` or its logging wrapper. It has no
labels.

These metrics are emitted around preparation and prepared-application
connection handling, so repeated connections show reuse of the already-built
route services without repeating middleware transforms. All three helpers are
no-ops when the `metrics` feature is disabled.

## Mutation testing

Expand Down
191 changes: 191 additions & 0 deletions docs/v0-3-0-to-v0-4-0-migration-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# v0.3.0 to v0.4.0 migration guide

This guide covers the prepared-application transition for applications that
drive accepted streams directly. It explains how to move route and middleware
setup out of connection handling while retaining the existing server factory
workflow.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Prepared application transition

`WireframeApp` remains the mutable builder. Register routes, middleware,
protocol hooks, and connection configuration on the builder, then consume it
with `prepare().await`:

```rust,no_run
use std::sync::Arc;

use wireframe::app::{Envelope, Handler, WireframeApp};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let handler: Handler<Envelope> = Arc::new(|_envelope| Box::pin(async {}));
let app = WireframeApp::new()?.route(1, handler)?;
let prepared = app.prepare().await?;
# let _ = prepared;
# Ok(())
# }
```

Preparation consumes the builder. It transforms every registered route's
middleware chain once and returns an immutable `PreparedApp` containing those
services and the runtime configuration. The builder's route-registration
methods are therefore unavailable after the transition; register all routes and
middleware before calling `prepare`.

`prepare` returns `Result<PreparedApp, PrepareError>`. Preparation is currently
infallible, but the typed error provides a stable place for callers to handle
future fallible middleware or runtime preparation steps. A failed preparation
does not expose a partially prepared application.

## Reuse the prepared application

Use the prepared connection methods for every accepted stream. Borrowing the
same `PreparedApp` lets multiple connections share the already-built route
services:

```rust,no_run
use tokio::io::duplex;
use wireframe::app::{PreparedApp, WireframeApp};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let prepared: PreparedApp = WireframeApp::new()?.prepare().await?;

let (client_one, server_one) = duplex(64);
drop(client_one);
prepared.handle_connection_result(server_one).await?;

let (client_two, server_two) = duplex(64);
drop(client_two);
prepared.handle_connection_result(server_two).await?;
# Ok(())
# }
```

`PreparedApp::handle_connection_result` returns stream-processing and handler
I/O errors. `PreparedApp::handle_connection` is the logging convenience wrapper
when the caller does not need to inspect that result. The prepared application
is immutable and has no route-registration surface.

## Update test drivers

Tests that use the `wireframe_testing` companion crate can prepare a builder
and drive one connection with `prepare_and_drive_with_frames`, or prepare once
and reuse the result with `drive_prepared_with_frames`:

```rust,no_run
use wireframe::app::WireframeApp;
use wireframe_testing::{drive_prepared_with_frames, prepare_and_drive_with_frames};

# async fn example() -> std::io::Result<()> {
let app = WireframeApp::new().map_err(std::io::Error::other)?;
let _response = prepare_and_drive_with_frames(app, Vec::new()).await?;

let app = WireframeApp::new().map_err(std::io::Error::other)?;
let prepared = app
.prepare()
.await
.map_err(|error| std::io::Error::other(error.to_string()))?;
let _response = drive_prepared_with_frames(&prepared, Vec::new()).await?;
# Ok(())
# }
```

Both prepared helpers preserve custom `FrameCodec` types. Existing builder or
mutable drivers remain available as deprecated compatibility paths; migrate
tests to the prepared helpers when they need to prove one-time middleware
transformation or reuse prepared route services.

## Migrate byte-handling APIs

The prepared-application transition is independent of the zero-copy byte
migration. The v0.4 byte-facing APIs use `bytes::Bytes` (or the `PayloadBytes`
wrapper) for read-only hand-offs and an explicit edit-on-demand operation for
mutation. Middleware and hook editor APIs are not yet finalized; their
migration is deferred to roadmap items 12.1.2 and 12.2.1. The compatibility
helper names described below are defined by
[ADR 009](adr-009-vec-u8-migration-rollout.md).

### Middleware

The public edit-on-demand API for middleware requests and responses is not yet
finalized. Continue using the current `frame_mut()` and `into_inner()`
compatibility methods while this migration is tracked by roadmap item 12.1.2.
Do not assume a response-editor method or introduce an editor method until that
API is implemented and documented. Read-only middleware should avoid editing
the frame altogether.

### Protocol and client hooks

The hook editor API is also deferred to roadmap item 12.2.1. Keep existing
`Vec<u8>` hook implementations until that API is finalized; client preamble
leftovers intentionally remain `Vec<u8>` in this release. The compatibility
policy is defined in [ADR 009](adr-009-vec-u8-migration-rollout.md).

### Serializers

Serializer output moves from an owned vector to the stable byte wrapper. Use
`PayloadBytes::from_vec` only at an existing compatibility boundary, and keep
the zero-copy value through the codec hand-off:

```text
# Before: serialization materializes a Vec<u8> for every outbound message.
let bytes: Vec<u8> = serializer.serialize(&message)?;
let frame = codec.wrap_payload(bytes::Bytes::from(bytes));

# After: the serializer returns the stable shared byte representation.
let bytes: PayloadBytes = serializer.serialize(&message)?;
let frame = codec.wrap_payload(bytes.into_bytes());

# Compatibility only: an older caller that still requires Vec<u8>.
let bytes: Vec<u8> = serializer.serialize_to_vec(&message)?;
```

`serialize_to_vec` is a temporary compatibility shim where provided; new code
should consume `PayloadBytes` directly. `PayloadBytes::into_vec` is likewise an
escape hatch, not the normal transport path.

### Custom codecs

Codecs should store payloads as `Bytes` when possible and override
`frame_payload_bytes` to return a cheap clone. The `wrap_payload` argument is
already `Bytes`, so only the frame type and extraction methods need changing:

```rust
// Before: a custom frame owns a Vec<u8> payload.
struct MyEnvelope {
payload: Vec<u8>,
}

// After: the frame shares its payload buffer with the codec driver.
use bytes::Bytes;

struct MyEnvelope {
payload: Bytes,
}

impl FrameCodec for MyCodec {
type Frame = MyEnvelope;

fn frame_payload(frame: &MyEnvelope) -> &[u8] { &frame.payload }

fn frame_payload_bytes(frame: &MyEnvelope) -> Bytes { frame.payload.clone() }

fn wrap_payload(&self, payload: Bytes) -> MyEnvelope { MyEnvelope { payload } }
}
```

Keep `Vec<u8>` conversion at the edge of legacy callers with
`PayloadBytes::from_vec` or `PayloadBytes::into_vec`; do not add per-codec
conversion constructors. See
[ADR 008](adr-008-zero-copy-public-byte-container.md) for the read-only and
edit-on-demand design, and the
[zero-copy migration roadmap](zero-copy-frame-and-payload-migration-roadmap.md)
for the staged rollout.

## Server factory compatibility

`WireframeServer` continues to accept an `AppFactory` and retains its existing
factory-evaluation semantics in this release. Applications that construct a
fresh builder per connection therefore do not automatically share a prepared
application. Preparing the application factory before server readiness, and
moving server connection tasks onto a prepared root, are tracked separately in
[issue #642](https://github.com/leynos/wireframe/issues/642).
Loading