Skip to main content

rx_rust/utils/
subscribe_with_context.rs

1use crate::utils::serialized_delivery::{DeliveryStopped, UpdateOutcome};
2use crate::{
3    delegate_disposal,
4    disposable::{
5        Disposable, DisposableExt, boxed_disposal::BoxedDisposal, chain_disposal::ChainDisposal,
6    },
7    observable::Subscription,
8    observer::{Flow, Observer, Termination},
9    utils::{
10        pending_events::EventBatch,
11        serialized_delivery::{SerializedDelivery, WeakSerializedDelivery},
12        subscribe_with_auto_dispose_on_termination::is_auto_dispose_on_termination_observer,
13        types::MaybeSend,
14    },
15};
16use educe::Educe;
17
18// The disposal of a context that does not own its source subscription: stopping the context,
19// followed by the caller's own subscription. Returned by `subscribe_with_context`.
20delegate_disposal!(
21    Disposal<'or_sub, D>,
22    ChainDisposal<BoxedDisposal<'or_sub>, D>,
23    where D: Disposable
24);
25
26/// The type-erased disposal of a context that owns its source subscription. Returned by
27/// [`subscribe_with_context_owning_source`].
28pub type OwningDisposal<'or_sub> = BoxedDisposal<'or_sub>;
29
30/// Creates a subscription backed by a shared, serialized context containing the downstream
31/// observer and a mutable model.
32///
33/// The context does not own the source subscription: the caller's subscription is chained after
34/// the context's disposal, so the source is disposed only once downstream drops the returned
35/// subscription. Use this when the context can only terminate from inside the source's own
36/// `on_termination` — directly, or in a continuation of it, such as a scheduler task that delivers
37/// a termination the source had already parked in the model. The source is then finished by the
38/// time the context terminates, so owning it would buy nothing.
39///
40/// When the context can instead terminate while the source is still active — from a notifier, from
41/// a scheduler task, or from another source of a multi-source operator — use
42/// [`subscribe_with_context_owning_source`] so that the source is disposed on termination.
43pub fn subscribe_with_context<'or_sub, T, E, OR, D, M, F>(
44    observer: OR,
45    model: M,
46    builder: F,
47) -> Subscription<Disposal<'or_sub, D>>
48where
49    T: MaybeSend + 'or_sub,
50    E: MaybeSend + 'or_sub,
51    OR: MaybeSend + 'or_sub,
52    D: Disposable,
53    M: MaybeSend + 'or_sub,
54    F: FnOnce(SubscriptionContext<T, E, OR, M>) -> Subscription<D>,
55{
56    debug_assert_observer_compatibility::<OR>();
57    // This context does not own its source subscription, so its own `D` is `()`: the caller's
58    // subscription, of the unrelated type `D`, is chained below instead.
59    let context = SubscriptionContext::<T, E, OR, M, ()>::new(observer, model);
60    let disposal = context.disposal();
61    let subscription = builder(context);
62    subscription.preceded_by(disposal.into_boxed()).map_into()
63}
64
65/// Creates a context subscription whose source subscription is owned by the context.
66///
67/// Owning the source subscription lets the context dispose it automatically when the observer
68/// terminates, including when termination occurs synchronously while `builder` is running.
69///
70/// Use this whenever the context can terminate while the source is still active — from a notifier,
71/// from a scheduler task, or from another source of a multi-source operator. Owning the source
72/// costs `D: MaybeSend + 'or_sub` and erases the disposal into [`OwningDisposal`], so when
73/// the context can only terminate from inside the source's own `on_termination` prefer
74/// [`subscribe_with_context`], which keeps `D` concrete.
75pub fn subscribe_with_context_owning_source<'or_sub, T, E, OR, D, M, F>(
76    observer: OR,
77    model: M,
78    builder: F,
79) -> Subscription<OwningDisposal<'or_sub>>
80where
81    T: MaybeSend + 'or_sub,
82    E: MaybeSend + 'or_sub,
83    OR: Observer<T, E> + MaybeSend + 'or_sub,
84    D: Disposable + MaybeSend + 'or_sub,
85    M: MaybeSend + 'or_sub,
86    F: FnOnce(SubscriptionContext<T, E, OR, M, D>) -> Subscription<D>,
87{
88    debug_assert_observer_compatibility::<OR>();
89    let context = SubscriptionContext::<T, E, OR, M, D>::new(observer, model);
90    let disposal = context.disposal();
91    let subscription = builder(context.clone());
92    let previous_subscription = context.install_source_subscription(subscription);
93    debug_assert!(
94        !matches!(previous_subscription, Ok(Some(_))),
95        "the source subscription is installed only once"
96    );
97    disposal.into_boxed().into_subscription()
98}
99
100/// What a context owns besides its observer and its queued events.
101///
102/// These are dropped together, outside the lock, once the context stops. When the context stops by
103/// terminating, that happens after the observer was notified, so the source is disposed only after
104/// downstream was told the stream ended.
105#[derive(Educe)]
106#[educe(Debug)]
107struct ContextResources<M, D: Disposable> {
108    model: M,
109    /// `None` when the context does not own its source subscription — `D` is then `()` — or
110    /// while the builder of an owned source subscription is still running.
111    source_subscription: Option<Subscription<D>>,
112}
113
114type ContextDelivery<T, E, OR, M, D> = SerializedDelivery<T, E, OR, ContextResources<M, D>>;
115type WeakContextDelivery<T, E, OR, M, D> = WeakSerializedDelivery<T, E, OR, ContextResources<M, D>>;
116
117/// Context used by operator observers to serialize model updates and downstream events.
118///
119/// `D` is the disposal of the source subscription the context owns, and `()` when it owns none.
120#[derive(Educe)]
121#[educe(Debug, Clone)]
122pub struct SubscriptionContext<T, E, OR, M, D: Disposable = ()> {
123    delivery: ContextDelivery<T, E, OR, M, D>,
124}
125
126impl<T, E, OR, M, D: Disposable> SubscriptionContext<T, E, OR, M, D> {
127    /// Creates a context holding `observer` and `model`, owning no source subscription yet.
128    fn new(observer: OR, model: M) -> Self {
129        Self {
130            delivery: SerializedDelivery::idle(
131                observer,
132                ContextResources {
133                    model,
134                    source_subscription: None,
135                },
136            ),
137        }
138    }
139
140    /// Creates the disposal that stops this context.
141    fn disposal(&self) -> SubscriptionContextDisposal<T, E, OR, M, D> {
142        SubscriptionContextDisposal {
143            delivery: self.delivery.clone(),
144        }
145    }
146
147    /// Creates a non-owning reference to this context.
148    pub fn downgrade(&self) -> WeakSubscriptionContext<T, E, OR, M, D> {
149        WeakSubscriptionContext {
150            delivery: self.delivery.downgrade(),
151        }
152    }
153}
154
155impl<T, E, OR, M, D> SubscriptionContext<T, E, OR, M, D>
156where
157    OR: Observer<T, E>,
158    D: Disposable,
159{
160    /// Updates the model and sends the events that update produced, while the context is locked.
161    ///
162    /// An update that emits nothing simply decides no events, and then nothing is sent here.
163    ///
164    /// The callback must not call external APIs or drop values that can re-enter this context.
165    /// Return such values through [`UpdateOutcome::with_drop_outside`] instead.
166    /// If the context's delivery has stopped, the callback is not invoked and [`DeliveryStopped`]
167    /// is returned.
168    pub fn update<R, DO, const EVENTS_DECIDED: bool>(
169        &self,
170        callback: impl FnOnce(&mut M) -> UpdateOutcome<T, E, R, DO, EVENTS_DECIDED>,
171    ) -> Result<R, DeliveryStopped> {
172        self.delivery
173            .update(|resources| callback(&mut resources.model))
174    }
175
176    /// [`Self::update`] for an operator's `on_next`, reporting the flow instead of a result.
177    ///
178    /// The flow is what delivering the events the update produced answered, and [`Flow::Stop`]
179    /// when the context has stopped, so an operator observer can return it directly.
180    pub fn update_flow<DO, const EVENTS_DECIDED: bool>(
181        &self,
182        callback: impl FnOnce(&mut M) -> UpdateOutcome<T, E, (), DO, EVENTS_DECIDED>,
183    ) -> Flow {
184        match self
185            .delivery
186            .update_with_flow(|resources| callback(&mut resources.model))
187        {
188            Ok(((), flow)) => flow,
189            Err(DeliveryStopped) => Flow::Stop,
190        }
191    }
192
193    /// Gives the context the source subscription it owns, to be disposed once the context stops.
194    ///
195    /// The subscription it replaces — none, unless it is installed twice — is handed back so that
196    /// it is dropped outside the lock. Once the context has stopped, `subscription` is not
197    /// installed but disposed, outside the lock, and [`DeliveryStopped`] is returned.
198    fn install_source_subscription(
199        &self,
200        subscription: Subscription<D>,
201    ) -> Result<Option<Subscription<D>>, DeliveryStopped> {
202        self.delivery.update(|resources| {
203            UpdateOutcome::new(resources.source_subscription.replace(subscription))
204        })
205    }
206
207    /// Sends `value` downstream. Returns the flow of the delivery, as [`Self::send`] does.
208    pub fn send_next(&self, value: T) -> Flow {
209        self.send(EventBatch::Next(value))
210    }
211
212    /// Sends `termination` downstream.
213    ///
214    /// Nothing is answered: a termination is the last event, so the caller is done whether it
215    /// was delivered, queued behind a running delivery, or rejected by a context that had already
216    /// stopped — [`Self::send`] would say [`Flow::Stop`] in every case.
217    pub fn send_termination(&self, termination: Termination<E>) {
218        let _ = self.send(EventBatch::Termination(termination));
219    }
220
221    /// Sends `events` downstream, delivering them now or queueing them behind a running delivery.
222    ///
223    /// Returns whether downstream still accepts events. [`Flow::Stop`] means the events were
224    /// rejected and dropped, because the context has stopped or a termination is already queued,
225    /// or that the stream is over: `events` carried a termination, or delivering them ended it.
226    pub fn send(&self, events: EventBatch<T, E>) -> Flow {
227        self.delivery.send(events)
228    }
229}
230
231struct SubscriptionContextDisposal<T, E, OR, M, D: Disposable> {
232    delivery: ContextDelivery<T, E, OR, M, D>,
233}
234
235impl<T, E, OR, M, D: Disposable> Disposable for SubscriptionContextDisposal<T, E, OR, M, D> {
236    /// Stops the context, so that every later event is dropped.
237    fn dispose(self) {
238        self.delivery.stop();
239    }
240}
241
242/// A non-owning reference to a [`SubscriptionContext`].
243#[derive(Educe)]
244#[educe(Debug, Clone)]
245pub struct WeakSubscriptionContext<T, E, OR, M, D: Disposable = ()> {
246    delivery: WeakContextDelivery<T, E, OR, M, D>,
247}
248
249impl<T, E, OR, M, D: Disposable> WeakSubscriptionContext<T, E, OR, M, D> {
250    /// Returns the context, or `None` once every strong reference to it is gone.
251    pub fn upgrade(&self) -> Option<SubscriptionContext<T, E, OR, M, D>> {
252        self.delivery
253            .upgrade()
254            .map(|delivery| SubscriptionContext { delivery })
255    }
256}
257
258fn debug_assert_observer_compatibility<OR>() {
259    debug_assert!(
260        !is_auto_dispose_on_termination_observer::<OR>(),
261        "Do not combine subscribe_with_auto_dispose_on_termination with a context subscription. \
262         Using subscribe_with_context_owning_source handles \"auto dispose on termination\"."
263    );
264}