Skip to main content

github_copilot_sdk/
subscription.rs

1//! Subscription handles for observing session and lifecycle events.
2//!
3//! Returned by [`Session::subscribe`](crate::session::Session::subscribe) and
4//! [`Client::subscribe_lifecycle`](crate::Client::subscribe_lifecycle).
5//!
6//! Each subscription is an opt-in **observer** of events that are also
7//! delivered to the per-event handlers installed on the session config
8//! (see [`crate::handler`]). Subscribers receive a clone of every event but
9//! cannot influence permission decisions, tool results, or any other event
10//! whose handler return value affects the runtime.
11//!
12//! # Async iteration
13//!
14//! The subscription types implement [`tokio_stream::Stream`], so consumers
15//! can use adapter combinators from [`tokio_stream::StreamExt`] or
16//! `futures::StreamExt` (filtering, mapping, batching, racing with
17//! `tokio::select!`, etc.) without learning the SDK's internal channel
18//! choice. A simple `while let Ok(event) = sub.recv().await { ... }` loop
19//! also works for callers who don't need the [`Stream`](tokio_stream::Stream)
20//! surface.
21//!
22//! # Resume bootstrap and lag policy
23//!
24//! The first subscription on a resumed session may begin with a lossless,
25//! ordered bootstrap prefix. Once its owner catches up, delivery switches
26//! atomically to the bounded live stream. See
27//! [`Session::subscribe`](crate::session::Session::subscribe) for the
28//! unbounded retention, eager ownership, cleanup, and router limits.
29//!
30//! Each live subscriber maintains its own finite queue. If a consumer cannot
31//! keep up, the oldest live events are dropped and the next call yields
32//! [`Lagged`](crate::subscription::Lagged) reporting how many events were skipped.
33//! Slow subscribers do not block the producer.
34
35use std::collections::VecDeque;
36use std::fmt;
37use std::pin::Pin;
38use std::sync::Arc;
39use std::task::{Context, Poll};
40
41use parking_lot::Mutex;
42use tokio::sync::broadcast::{Receiver, Sender, WeakSender};
43use tokio_stream::wrappers::BroadcastStream;
44use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
45use tokio_stream::{Stream, StreamExt as _};
46
47use crate::types::{SessionEvent, SessionLifecycleEvent};
48use crate::{Custom, Repr};
49
50/// The subscription fell behind the producer.
51///
52/// Reports the number of events that were dropped from this subscriber's
53/// queue because the consumer didn't keep up. The subscription continues
54/// after this error, starting from the next live event — callers who care
55/// about lag should match on it and decide whether to resync, re-fetch, or
56/// log and continue.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct Lagged(pub(crate) u64);
59
60impl Lagged {
61    /// Number of events skipped before this consumer could read them.
62    pub fn skipped(&self) -> u64 {
63        self.0
64    }
65}
66
67impl fmt::Display for Lagged {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "subscription lagged behind by {} events", self.0)
70    }
71}
72
73impl std::error::Error for Lagged {}
74
75/// Error kind for subscription receive operations.
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77#[non_exhaustive]
78pub enum RecvErrorKind {
79    /// The producer is gone — the session has shut down or the client has
80    /// stopped. No further events will be delivered.
81    Closed,
82
83    /// The subscriber fell behind. See [`Lagged`].
84    Lagged(Lagged),
85}
86
87impl fmt::Display for RecvErrorKind {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        match self {
90            RecvErrorKind::Closed => write!(f, "subscription closed"),
91            RecvErrorKind::Lagged(l) => write!(f, "{l}"),
92        }
93    }
94}
95
96/// Error returned by [`crate::subscription::EventSubscription::recv`] and
97/// [`crate::subscription::LifecycleSubscription::recv`].
98#[derive(Debug)]
99pub struct RecvError {
100    repr: Repr<RecvErrorKind>,
101}
102
103impl RecvError {
104    /// The [`RecvErrorKind`] of this error.
105    pub fn kind(&self) -> &RecvErrorKind {
106        match &self.repr {
107            Repr::Simple(k) | Repr::SimpleMessage(k, ..) | Repr::Custom(Custom { kind: k, .. }) => {
108                k
109            }
110        }
111    }
112}
113
114impl fmt::Display for RecvError {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match &self.repr {
117            Repr::Simple(k) => write!(f, "{k}"),
118            Repr::SimpleMessage(_, m) => write!(f, "{m}"),
119            Repr::Custom(Custom { error, .. }) => write!(f, "{error}"),
120        }
121    }
122}
123
124impl std::error::Error for RecvError {
125    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
126        match &self.repr {
127            Repr::Custom(Custom { error, .. }) => Some(&**error),
128            _ => None,
129        }
130    }
131}
132
133impl From<RecvErrorKind> for RecvError {
134    fn from(kind: RecvErrorKind) -> Self {
135        Self {
136            repr: Repr::Simple(kind),
137        }
138    }
139}
140
141impl From<Lagged> for RecvError {
142    fn from(lagged: Lagged) -> Self {
143        Self::from(RecvErrorKind::Lagged(lagged))
144    }
145}
146
147enum ResumeBootstrapState {
148    Unclaimed(VecDeque<SessionEvent>),
149    Claimed(VecDeque<SessionEvent>),
150    Disabled,
151}
152
153/// Publication, ownership, and the empty-queue handoff share one lock so
154/// the bootstrap owner observes an exact prefix without gaps or duplicates.
155pub(crate) struct ResumeBootstrap {
156    state: Mutex<ResumeBootstrapState>,
157    live: WeakSender<SessionEvent>,
158}
159
160/// Releases only unclaimed events when the publishing task exits or unwinds.
161pub(crate) struct ResumeBootstrapCleanup(Arc<ResumeBootstrap>);
162
163impl Drop for ResumeBootstrapCleanup {
164    fn drop(&mut self) {
165        self.0.release_unclaimed();
166    }
167}
168
169impl ResumeBootstrap {
170    pub(crate) fn new(event_tx: &Sender<SessionEvent>) -> Arc<Self> {
171        Arc::new(Self {
172            state: Mutex::new(ResumeBootstrapState::Unclaimed(VecDeque::new())),
173            live: event_tx.downgrade(),
174        })
175    }
176
177    pub(crate) fn cleanup_guard(self: &Arc<Self>) -> ResumeBootstrapCleanup {
178        ResumeBootstrapCleanup(self.clone())
179    }
180
181    pub(crate) fn publish(&self, event_tx: &Sender<SessionEvent>, event: SessionEvent) {
182        let mut state = self.state.lock();
183        match &mut *state {
184            ResumeBootstrapState::Unclaimed(events) | ResumeBootstrapState::Claimed(events) => {
185                events.push_back(event.clone());
186            }
187            ResumeBootstrapState::Disabled => {}
188        }
189        // Other observers stay live while the bootstrap owner catches up.
190        let _ = event_tx.send(event);
191    }
192
193    pub(crate) fn subscribe(
194        self: &Arc<Self>,
195        event_tx: &Sender<SessionEvent>,
196    ) -> EventSubscription {
197        let mut state = self.state.lock();
198        match &mut *state {
199            ResumeBootstrapState::Unclaimed(events) => {
200                let events = std::mem::take(events);
201                *state = ResumeBootstrapState::Claimed(events);
202                EventSubscription {
203                    inner: None,
204                    bootstrap: Some(self.clone()),
205                }
206            }
207            ResumeBootstrapState::Claimed(_) | ResumeBootstrapState::Disabled => {
208                EventSubscription::new(event_tx.subscribe())
209            }
210        }
211    }
212
213    fn pop(&self, live: &mut Option<BroadcastStream<SessionEvent>>) -> Option<SessionEvent> {
214        let mut state = self.state.lock();
215        let ResumeBootstrapState::Claimed(events) = &mut *state else {
216            return None;
217        };
218        if let Some(event) = events.pop_front() {
219            return Some(event);
220        }
221        // Subscribe under the publication lock, never replaying the broadcast
222        // copy of an event already delivered from the bootstrap queue.
223        *live = self
224            .live
225            .upgrade()
226            .map(|sender| BroadcastStream::new(sender.subscribe()));
227        *state = ResumeBootstrapState::Disabled;
228        None
229    }
230
231    pub(crate) fn release_unclaimed(&self) {
232        let mut state = self.state.lock();
233        if matches!(*state, ResumeBootstrapState::Unclaimed(_)) {
234            *state = ResumeBootstrapState::Disabled;
235        }
236    }
237
238    fn abandon(&self) {
239        let mut state = self.state.lock();
240        if matches!(*state, ResumeBootstrapState::Claimed(_)) {
241            *state = ResumeBootstrapState::Disabled;
242        }
243    }
244}
245
246/// Subscription to runtime events for a single
247/// [`Session`](crate::session::Session).
248///
249/// Created by [`Session::subscribe`](crate::session::Session::subscribe).
250/// Implements [`Stream`] yielding `Result<SessionEvent, Lagged>`.
251/// Drop the value to unsubscribe; there is no separate cancel handle.
252/// A resume bootstrap is claimed at construction, not on the first poll.
253/// Dropping its owner discards any unread bootstrap events.
254#[must_use = "dropping the subscription unsubscribes and discards any owned resume bootstrap backlog"]
255pub struct EventSubscription {
256    inner: Option<BroadcastStream<SessionEvent>>,
257    bootstrap: Option<Arc<ResumeBootstrap>>,
258}
259
260impl EventSubscription {
261    pub(crate) fn new(rx: Receiver<SessionEvent>) -> Self {
262        Self {
263            inner: Some(BroadcastStream::new(rx)),
264            bootstrap: None,
265        }
266    }
267
268    fn next_bootstrap_event(&mut self) -> Option<SessionEvent> {
269        let event = self
270            .bootstrap
271            .as_ref()
272            .and_then(|bootstrap| bootstrap.pop(&mut self.inner));
273        if event.is_none() {
274            self.bootstrap = None;
275        }
276        event
277    }
278
279    /// Receive the next event.
280    ///
281    /// Returns:
282    ///
283    /// - `Ok(event)` for the next delivered event.
284    /// - [`RecvErrorKind::Lagged`] if live delivery fell behind; call again
285    ///   to continue from the next available live event.
286    /// - [`RecvErrorKind::Closed`] once the producer is gone and any retained
287    ///   events have been drained.
288    ///
289    /// # Cancel safety
290    ///
291    /// **Cancel-safe.** Bootstrap events are removed before the future's
292    /// first suspension point. Once live delivery begins, this wraps a
293    /// cancel-safe `tokio::sync::broadcast::Receiver` via `BroadcastStream`.
294    pub async fn recv(&mut self) -> Result<SessionEvent, RecvError> {
295        match self.next().await {
296            Some(Ok(event)) => Ok(event),
297            Some(Err(lagged)) => Err(lagged.into()),
298            None => Err(RecvErrorKind::Closed.into()),
299        }
300    }
301}
302
303impl Stream for EventSubscription {
304    type Item = Result<SessionEvent, Lagged>;
305
306    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
307        if let Some(event) = self.next_bootstrap_event() {
308            return Poll::Ready(Some(Ok(event)));
309        }
310        let Some(inner) = self.inner.as_mut() else {
311            return Poll::Ready(None);
312        };
313        match Pin::new(inner).poll_next(cx) {
314            Poll::Ready(Some(Ok(event))) => Poll::Ready(Some(Ok(event))),
315            Poll::Ready(Some(Err(BroadcastStreamRecvError::Lagged(n)))) => {
316                Poll::Ready(Some(Err(Lagged(n))))
317            }
318            Poll::Ready(None) => Poll::Ready(None),
319            Poll::Pending => Poll::Pending,
320        }
321    }
322}
323
324impl Drop for EventSubscription {
325    fn drop(&mut self) {
326        if let Some(bootstrap) = &self.bootstrap {
327            bootstrap.abandon();
328        }
329    }
330}
331
332/// Subscription to lifecycle events on a [`Client`](crate::Client).
333///
334/// Created by [`Client::subscribe_lifecycle`](crate::Client::subscribe_lifecycle).
335/// Implements [`Stream`] yielding `Result<SessionLifecycleEvent, Lagged>`.
336/// Drop the value to unsubscribe; there is no separate cancel handle.
337#[must_use = "dropping the subscription unsubscribes"]
338pub struct LifecycleSubscription {
339    inner: BroadcastStream<SessionLifecycleEvent>,
340}
341
342impl LifecycleSubscription {
343    pub(crate) fn new(rx: Receiver<SessionLifecycleEvent>) -> Self {
344        Self {
345            inner: BroadcastStream::new(rx),
346        }
347    }
348
349    /// Receive the next event.
350    ///
351    /// Returns:
352    ///
353    /// - `Ok(event)` for the next delivered event.
354    /// - [`RecvErrorKind::Lagged`] if the subscriber fell behind; call again
355    ///   to continue from the next available event.
356    /// - [`RecvErrorKind::Closed`] once the producer is gone.
357    ///
358    /// # Cancel safety
359    ///
360    /// **Cancel-safe.** Wraps a `tokio::sync::broadcast::Receiver` via
361    /// `BroadcastStream`; both are cancel-safe by design. Dropping the future
362    /// before completion leaves buffered events available for the next call.
363    pub async fn recv(&mut self) -> Result<SessionLifecycleEvent, RecvError> {
364        match self.next().await {
365            Some(Ok(event)) => Ok(event),
366            Some(Err(lagged)) => Err(lagged.into()),
367            None => Err(RecvErrorKind::Closed.into()),
368        }
369    }
370}
371
372impl Stream for LifecycleSubscription {
373    type Item = Result<SessionLifecycleEvent, Lagged>;
374
375    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
376        match Pin::new(&mut self.inner).poll_next(cx) {
377            Poll::Ready(Some(Ok(event))) => Poll::Ready(Some(Ok(event))),
378            Poll::Ready(Some(Err(BroadcastStreamRecvError::Lagged(n)))) => {
379                Poll::Ready(Some(Err(Lagged(n))))
380            }
381            Poll::Ready(None) => Poll::Ready(None),
382            Poll::Pending => Poll::Pending,
383        }
384    }
385}
386
387#[cfg(test)]
388mod tests;