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}