0c6e5b3ace
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
41 lines
1.7 KiB
C#
41 lines
1.7 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|