1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! The slot that holds the one inner subscription an operator keeps at a time.
//!
//! An operator such as [`Switch`](crate::operators::combining::switch::Switch) or
//! [`ConcatAll`](crate::operators::combining::concat_all::ConcatAll) is subscribed to at most one
//! inner observable at a time, and swaps that subscription as the source emits. Subscribing is an
//! external API call, so it must happen with no lock held, which splits every swap into two
//! locked steps around an unlocked one: reserve the slot, subscribe, fill the slot. Between the
//! two steps the inner observable can terminate the operator synchronously and release the slot,
//! so the fill has to be able to give the new subscription back.
//!
//! [`SubscriptionSlot`] is that three-step state machine and nothing else. It carries **no lock of
//! its own**: it lives inside a model already guarded by the delivery lock — see
//! [`SubscriptionContext::update`](crate::utils::subscribe_with_context::SubscriptionContext::update)
//! — and every method takes `&mut self`. It also disposes nothing: each method hands the
//! subscription it evicts back to the caller, which passes it to
//! [`UpdateOutcome::with_drop_outside`](crate::utils::serialized_delivery::UpdateOutcome::with_drop_outside)
//! so it is dropped outside the lock.
//!
//! # When a slot is the right type
//!
//! [`Reserved`](SubscriptionSlot::Reserved) is the whole of what this type adds. `Idle` and
//! `Active` are what an `Option<D>` already says, so a host that needs only those two keeps its
//! `Option`. Reach for a slot only when the host must tell "a value is on its way" apart from
//! "nothing is held", which takes all three of:
//!
//! - the value is built by an external call that has to run with the lock released, so the state
//! is observable by someone else while the build is in flight;
//! - the slot can be released inside that window, and filling a released slot would install a
//! value that is already dead — in a host that keeps one task alive while there is work to do,
//! that means storing the handle of a task that already stopped, after which no new task is
//! ever started and the queued events stall;
//! - there is exactly one such value. Several of them are keyed, and an absent key already means
//! `Idle`, so each entry collapses back to `Option` — see the maps in
//! [`MergeAll`](crate::operators::combining::merge_all::MergeAll) and
//! [`Amb`](crate::operators::conditional_boolean::amb::Amb), which run this same reserve/fill
//! protocol without this type.
//!
//! When the third state is unreachable, a slot only widens the state space with a variant the
//! host's invariants forbid, which is the opposite of what it is for.
//!
//! # Why this is not [`SharedDisposal`](crate::disposable::shared_disposal::SharedDisposal)
//!
//! The two are the same shape — idle, building, active — but not the same contract, so neither is
//! built on the other:
//!
//! - `SharedDisposal` is itself a disposal, so it needs a terminal `Disposed` state that absorbs
//! later replacements. A slot needs none: its host stops through the delivery, which stops
//! running updates at all ([`DeliveryStopped`](crate::utils::serialized_delivery::DeliveryStopped)),
//! and whatever is left in the slot is disposed when the model is dropped.
//! - `SharedDisposal` releases its own lock while the builder runs, so a second `replace` can race
//! the first and it needs a generation id to tell whether a finished build is still current. A
//! slot cannot be raced: the reserve/fill pair is serialized by the delivery lock, and
//! [`Reserved`](SubscriptionSlot::Reserved) makes a second reserve unreachable.
//!
//! Merging them would give every slot a state it never enters and an id it never reads.
use Educe;
/// The one inner subscription an operator holds at a time.
///
/// `D` is the value being held — a [`Subscription`](crate::observable::Subscription) in every
/// current use, which disposes when dropped.