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:
@@ -0,0 +1,40 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods that project a raw <see cref="MxEvent"/> stream into the
|
||||
/// typed <see cref="MxEventStreamItem"/> surface, making the gateway's
|
||||
/// reconnect-replay gap sentinel observable.
|
||||
/// </summary>
|
||||
public static class MxEventStreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Projects a raw <see cref="MxEvent"/> stream (e.g.
|
||||
/// <see cref="MxGatewaySession.StreamEventsAsync"/> or
|
||||
/// <see cref="MxGatewayClient.StreamEventsAsync"/>) into typed
|
||||
/// <see cref="MxEventStreamItem"/> values. Normal events pass through with
|
||||
/// <see cref="MxEventStreamItem.IsReplayGap"/> false; the gateway's
|
||||
/// reconnect-replay gap sentinel is surfaced with
|
||||
/// <see cref="MxEventStreamItem.IsReplayGap"/> true and
|
||||
/// <see cref="MxEventStreamItem.ReplayGap"/> populated. The stream is
|
||||
/// forwarded faithfully — no event is synthesized or dropped.
|
||||
/// </summary>
|
||||
/// <param name="source">The raw event stream to wrap.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the enumeration.</param>
|
||||
/// <returns>The same events, each wrapped as an <see cref="MxEventStreamItem"/>.</returns>
|
||||
public static async IAsyncEnumerable<MxEventStreamItem> AsStreamItemsAsync(
|
||||
this IAsyncEnumerable<MxEvent> source,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
await foreach (MxEvent gatewayEvent in source
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
yield return MxEventStreamItem.From(gatewayEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user