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:
@@ -92,6 +92,44 @@ goroutine cleanup. Raw protobuf messages remain available through the
|
||||
`errors.As` for `GatewayError`, `CommandError`, and `MxAccessError`; command
|
||||
errors preserve the raw reply.
|
||||
|
||||
### Reconnect-replay gap
|
||||
|
||||
Each `EventResult` carries exactly one of `Event`, `ReplayGap`, or `Err`. When
|
||||
you resume a stream with `EventsAfter`/`SubscribeEventsAfter` and a non-zero
|
||||
`afterWorkerSequence`, the gateway replays buffered events from that point. If
|
||||
the requested sequence predates the oldest event still retained in its replay
|
||||
ring, it delivers a single **replay-gap sentinel** at the head of the resumed
|
||||
stream: `res.ReplayGap` is non-nil (`res.Event` is nil, `res.IsReplayGap()` is
|
||||
true) and normal events follow it.
|
||||
|
||||
A gap means events were lost, so any locally cached tag/alarm state is now
|
||||
stale. On seeing it, discard your cached state and re-snapshot. To resume
|
||||
without provoking another gap, reconnect from just before the oldest retained
|
||||
sequence:
|
||||
|
||||
```go
|
||||
for res := range events {
|
||||
switch {
|
||||
case res.Err != nil:
|
||||
// terminal: stream ended (see ErrSlowConsumer for the overflow case)
|
||||
return res.Err
|
||||
case res.IsReplayGap():
|
||||
gap := res.ReplayGap
|
||||
log.Printf("replay gap: requested after %d, oldest available %d; re-snapshotting",
|
||||
gap.GetRequestedAfterSequence(), gap.GetOldestAvailableSequence())
|
||||
resnapshot()
|
||||
// to resume cleanly, reconnect with:
|
||||
// session.EventsAfter(ctx, gap.GetOldestAvailableSequence()-1)
|
||||
default:
|
||||
handle(res.Event)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The gateway sets `ReplayGap` only on `StreamEvents` results (never on a fresh,
|
||||
non-resumed stream and never on `DrainEvents`). The client makes the gateway's
|
||||
sentinel typed and observable; it never synthesizes or swallows it.
|
||||
|
||||
For alarms, the package exposes `Client.QueryActiveAlarms` for one-shot
|
||||
snapshots, `Client.StreamAlarms` for the server-streaming feed, and
|
||||
`Client.AcknowledgeAlarm` to ack an alarm by full reference. The streaming
|
||||
|
||||
Reference in New Issue
Block a user