El servidor empuja MatchEvent (Available | Lost) a los consumers cuando
sus inputs cambian de match — sea porque un productor llegó, porque
otro mejor lo desplazó, o porque desapareció.
Mecánica:
- Frame::MatchEvent con MatchEventKind { Available, Lost } y los datos
del match (consumer_flow, producer_session/label/flow, ty, via, pinned).
- Server: SessionTxTable (Arc<Mutex<HashMap<SessionId, mpsc::Sender>>>)
+ LastMatches (último match conocido por consumer/input). En cada
register/unregister, broadcast_match_diffs recomputa con el broker
y emite SOLO los diffs respecto al estado anterior.
- Session::run_post_handshake usa tokio::select! para multiplexar
read_frame del cliente y rx.recv() de su tx push.
- Cleanup ahora también limpia push_table y last_matches y dispara un
broadcast (para notificar a quienes pierden el match).
- Client: VecDeque<MatchEvent> bufferea eventos que llegan mezclados
con respuestas a Ping. API:
- take_event() — non-blocking, drena buffer
- await_event(timeout) — bloquea hasta evento o timeout
- ping() ahora drena MatchEvents intermedios hasta encontrar el Pong.
Capacity del canal push por sesión: 32 frames (try_send no-blocking;
si se llena, los eventos extra se descartan — se documenta como
ephemeral, el cliente puede re-consultar via brahman-status).
Test nuevo en brahman-handshake/tests/handshake.rs:
- match_event_pushed_on_producer_arrival: consumer espera, no recibe
evento → llega productor → recibe Available → productor se va →
recibe Lost.
Example nuevo: brahman-handshake/examples/subscriber.rs — cliente que
loguea cada MatchEvent en tiempo real. Útil para ver la dinámica del
broker. Pings cada 25s para keepalive.
Demo end-to-end verificada (4 eventos, 3 ya cubren el ciclo completo):
T+0.3 alpha llega → Available ← demo.alpha.out
T+0.8 beta llega → (sin evento: alpha gana por orden alfabético)
T+1.3 alpha killed → Available ← demo.beta.out (re-evaluación)
T+1.8 beta killed → Lost ← <none>
El broker emite diff: ningún evento cuando un nuevo productor llega
sin desplazar al ganador actual.
Tests: 28/28 (handshake integ 6→7). cargo check --workspace: 0 errores.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Crate nuevo en crates/core/brahman-handshake que implementa el handshake
real del shared_wit/protocol.wit como wire format Rust↔Rust sobre Unix
socket con frames length-prefixed + cuerpo postcard.
Componentes:
- src/messages.rs: Hello, HelloAck, Ping, Pong, Farewell, HandshakeError
y Frame (enum-suma). HandshakeError ya implementa thiserror::Error y
cruza el wire.
- src/codec.rs: write_frame / read_frame asíncronos con MAX_FRAME_BYTES
de 4 MiB. Test interno de roundtrip.
- src/server.rs: Server::bind crea el listener en Unix socket; emite
ResolvedCard tras validar la Card y devuelve ULID como SessionId.
ServerConfig.init_attached se reporta en HelloAck.
- src/client.rs: Client::connect hace pre-validación local de la Card
(fail fast), envía Hello, parsea HelloAck. ping() y farewell() expuestos.
- tests/handshake.rs: 4 tests de integración:
* full_handshake_roundtrip — happy path con 3 pings + farewell
* rejects_invalid_card_client_side — label vacío rechazado pre-envío
* server_rejects_protocol_mismatch — protocol_version 999.0.0 → Error
* ping_before_hello_rejected — Ping sin Hello previo → Rejected
Limitación conocida: postcard no serializa serde_json::Value (variantes
Array/Object con length dinámico). Se removieron por eso los campos
`extensions` (Card) y `extra` (Permissions). Forward-compat queda
cubierta por schema_version + protocol_version negotiation; si más
adelante necesitamos preservar campos JSON desconocidos, irá en un
WireCard separado o un envelope.
13/13 tests verdes (brahman-card 8 + brahman-handshake codec 1 + integ 4).
cargo check --workspace: 0 errores.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>