feat(clients): CLI-15 surface ReplayGap reconnect sentinel as typed signal (4/5 clients)
The gateway emits a ReplayGap sentinel MxEvent at the head of a StreamEvents stream resumed via after_worker_sequence when the requested cursor predates the oldest retained event. Clients previously ignored it, silently mis-treating a lossy resume as continuous. Each client now surfaces the sentinel as a distinct, typed, non-terminal signal (never synthesized, never swallowed) so a consumer can detect the gap and re-snapshot; resume contract is after_worker_sequence = oldest_available_sequence - 1. - .NET: MxEventStreamItem (IsReplayGap/ReplayGap/Event) via new StreamEventItemsAsync + AsStreamItemsAsync extension. Build clean, 87 passed. - Go: EventResult.ReplayGap field + IsReplayGap(); ReplayGap type alias. build/vet/test clean. - Rust: EventItem enum (Event/ReplayGap); EventStream now yields Result<EventItem, Error>; CLI renders REPLAY_GAP line / replayGap JSON. fmt/check/test/clippy clean. - Python: ReplayGap dataclass; stream_events yields pb.MxEvent | ReplayGap. 131 passed. - Shared docs: ClientLibrariesDesign non-goals reframed (reconnect-replay protocol is consumable; auto-reconnect stays a non-goal); CrossLanguageSmokeMatrix resume-gap note. Java client is deferred to the windev batch (no local JRE); CLI-15 stays open until it lands. Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
This commit is contained in:
@@ -28,8 +28,8 @@ use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{
|
||||
WriteSecuredBulkEntry,
|
||||
};
|
||||
use zb_mom_ww_mxgateway_client::{
|
||||
next_correlation_id, ApiKey, ClientOptions, Error, GalaxyClient, GatewayClient, MxValue,
|
||||
MxValueProjection, CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION,
|
||||
next_correlation_id, ApiKey, ClientOptions, Error, EventItem, GalaxyClient, GatewayClient,
|
||||
MxValue, MxValueProjection, CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION,
|
||||
};
|
||||
|
||||
const MAX_AGGREGATE_EVENTS: usize = 10_000;
|
||||
@@ -886,17 +886,43 @@ async fn dispatch(command: Command) -> Result<(), Error> {
|
||||
let mut events: Vec<Value> = Vec::new();
|
||||
let mut event_count = 0usize;
|
||||
while event_count < max_events {
|
||||
let Some(event) = stream.next().await else {
|
||||
let Some(item) = stream.next().await else {
|
||||
break;
|
||||
};
|
||||
let event = event?;
|
||||
let item = item?;
|
||||
event_count += 1;
|
||||
if jsonl {
|
||||
println!("{}", event_to_json(&event));
|
||||
} else if json {
|
||||
events.push(event_to_json(&event));
|
||||
} else {
|
||||
println!("{} {}", event.worker_sequence, event.family);
|
||||
match item {
|
||||
EventItem::Event(event) => {
|
||||
if jsonl {
|
||||
println!("{}", event_to_json(&event));
|
||||
} else if json {
|
||||
events.push(event_to_json(&event));
|
||||
} else {
|
||||
println!("{} {}", event.worker_sequence, event.family);
|
||||
}
|
||||
}
|
||||
// Reconnect-replay gap sentinel: recent history was evicted
|
||||
// before this resumed stream could replay it. Render it as a
|
||||
// distinct row so the caller can re-snapshot and resume with
|
||||
// `oldest_available_sequence - 1`.
|
||||
EventItem::ReplayGap(gap) => {
|
||||
let value = json!({
|
||||
"replayGap": {
|
||||
"requestedAfterSequence": gap.requested_after_sequence,
|
||||
"oldestAvailableSequence": gap.oldest_available_sequence,
|
||||
}
|
||||
});
|
||||
if jsonl {
|
||||
println!("{value}");
|
||||
} else if json {
|
||||
events.push(value);
|
||||
} else {
|
||||
println!(
|
||||
"REPLAY_GAP requested_after={} oldest_available={}",
|
||||
gap.requested_after_sequence, gap.oldest_available_sequence
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if json {
|
||||
|
||||
Reference in New Issue
Block a user