Line data Source code
1 : // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'dart:async';
6 :
7 : /// A collection of streams whose events are unified and sent through a central
8 : /// stream.
9 : ///
10 : /// Both errors and data events are forwarded through [stream]. The streams in
11 : /// the group won't be listened to until [stream] has a listener. **Note that
12 : /// this means that events emitted by broadcast streams will be dropped until
13 : /// [stream] has a listener.**
14 : ///
15 : /// If the `StreamGroup` is constructed using [new StreamGroup], [stream] will
16 : /// be single-subscription. In this case, if [stream] is paused or canceled, all
17 : /// streams in the group will likewise be paused or canceled, respectively.
18 : ///
19 : /// If the `StreamGroup` is constructed using [new StreamGroup.broadcast],
20 : /// [stream] will be a broadcast stream. In this case, the streams in the group
21 : /// will never be paused and single-subscription streams in the group will never
22 : /// be canceled. **Note that single-subscription streams in a broadcast group
23 : /// may drop events if a listener is added and later removed.** Broadcast
24 : /// streams in the group will be canceled once [stream] has no listeners, and
25 : /// will be listened to again once [stream] has listeners.
26 : ///
27 : /// [stream] won't close until [close] is called on the group *and* every stream
28 : /// in the group closes.
29 : class StreamGroup<T> implements Sink<Stream<T>> {
30 : /// The stream through which all events from streams in the group are emitted.
31 10 : Stream<T> get stream => _controller.stream;
32 : StreamController<T> _controller;
33 :
34 : /// Whether the group is closed, meaning that no more streams may be added.
35 : var _closed = false;
36 :
37 : /// The current state of the group.
38 : ///
39 : /// See [_StreamGroupState] for detailed descriptions of each state.
40 : var _state = _StreamGroupState.dormant;
41 :
42 : /// Streams that have been added to the group, and their subscriptions if they
43 : /// have been subscribed to.
44 : ///
45 : /// The subscriptions will be null until the group has a listener registered.
46 : /// If it's a broadcast group and it goes dormant again, broadcast stream
47 : /// subscriptions will be canceled and set to null again. Single-subscriber
48 : /// stream subscriptions will be left intact, since they can't be
49 : /// re-subscribed.
50 : final _subscriptions = new Map<Stream<T>, StreamSubscription<T>>();
51 :
52 : /// Merges the events from [streams] into a single (single-subscriber) stream.
53 : ///
54 : /// This is equivalent to adding [streams] to a group, closing that group, and
55 : /// returning its stream.
56 : static Stream<T> merge<T>(Iterable<Stream<T>> streams) {
57 0 : var group = new StreamGroup<T>();
58 0 : streams.forEach(group.add);
59 0 : group.close();
60 0 : return group.stream;
61 : }
62 :
63 : /// Creates a new stream group where [stream] is single-subscriber.
64 0 : StreamGroup() {
65 0 : _controller = new StreamController<T>(
66 0 : onListen: _onListen,
67 0 : onPause: _onPause,
68 0 : onResume: _onResume,
69 0 : onCancel: _onCancel,
70 : sync: true);
71 : }
72 :
73 : /// Creates a new stream group where [stream] is a broadcast stream.
74 5 : StreamGroup.broadcast() {
75 10 : _controller = new StreamController<T>.broadcast(
76 10 : onListen: _onListen, onCancel: _onCancelBroadcast, sync: true);
77 : }
78 :
79 : /// Adds [stream] as a member of this group.
80 : ///
81 : /// Any events from [stream] will be emitted through [this.stream]. If this
82 : /// group has a listener, [stream] will be listened to immediately; otherwise
83 : /// it will only be listened to once this group gets a listener.
84 : ///
85 : /// If this is a single-subscription group and its subscription has been
86 : /// canceled, [stream] will be canceled as soon as its added. If this returns
87 : /// a [Future], it will be returned from [add]. Otherwise, [add] returns
88 : /// `null`.
89 : ///
90 : /// Throws a [StateError] if this group is closed.
91 : Future add(Stream<T> stream) {
92 5 : if (_closed) {
93 0 : throw new StateError("Can't add a Stream to a closed StreamGroup.");
94 : }
95 :
96 10 : if (_state == _StreamGroupState.dormant) {
97 0 : _subscriptions.putIfAbsent(stream, () => null);
98 10 : } else if (_state == _StreamGroupState.canceled) {
99 : // Listen to the stream and cancel it immediately so that no one else can
100 : // listen, for consistency. If the stream has an onCancel listener this
101 : // will also fire that, which may help it clean up resources.
102 0 : return stream.listen(null).cancel();
103 : } else {
104 15 : _subscriptions.putIfAbsent(stream, () => _listenToStream(stream));
105 : }
106 :
107 : return null;
108 : }
109 :
110 : /// Removes [stream] as a member of this group.
111 : ///
112 : /// No further events from [stream] will be emitted through this group. If
113 : /// [stream] has been listened to, its subscription will be canceled.
114 : ///
115 : /// If [stream] has been listened to, this *synchronously* cancels its
116 : /// subscription. This means that any events from [stream] that haven't yet
117 : /// been emitted through this group will not be.
118 : ///
119 : /// If [stream]'s subscription is canceled, this returns
120 : /// [StreamSubscription.cancel]'s return value. Otherwise, it returns `null`.
121 : Future remove(Stream<T> stream) {
122 10 : var subscription = _subscriptions.remove(stream);
123 5 : var future = subscription == null ? null : subscription.cancel();
124 5 : if (_closed && _subscriptions.isEmpty) _controller.close();
125 : return future;
126 : }
127 :
128 : /// A callback called when [stream] is listened to.
129 : ///
130 : /// This is called for both single-subscription and broadcast groups.
131 : void _onListen() {
132 5 : _state = _StreamGroupState.listening;
133 10 : _subscriptions.forEach((stream, subscription) {
134 : // If this is a broadcast group and this isn't the first time it's been
135 : // listened to, there may still be some subscriptions to
136 : // single-subscription streams.
137 : if (subscription != null) return;
138 0 : _subscriptions[stream] = _listenToStream(stream);
139 : });
140 : }
141 :
142 : /// A callback called when [stream] is paused.
143 : void _onPause() {
144 0 : _state = _StreamGroupState.paused;
145 0 : for (var subscription in _subscriptions.values) {
146 0 : subscription.pause();
147 : }
148 : }
149 :
150 : /// A callback called when [stream] is resumed.
151 : void _onResume() {
152 0 : _state = _StreamGroupState.listening;
153 0 : for (var subscription in _subscriptions.values) {
154 0 : subscription.resume();
155 : }
156 : }
157 :
158 : /// A callback called when [stream] is canceled.
159 : ///
160 : /// This is only called for single-subscription groups.
161 : Future _onCancel() {
162 0 : _state = _StreamGroupState.canceled;
163 :
164 0 : var futures = _subscriptions.values
165 0 : .map((subscription) => subscription.cancel())
166 0 : .where((future) => future != null)
167 0 : .toList();
168 :
169 0 : _subscriptions.clear();
170 0 : return futures.isEmpty ? null : Future.wait(futures);
171 : }
172 :
173 : /// A callback called when [stream]'s last listener is canceled.
174 : ///
175 : /// This is only called for broadcast groups.
176 : void _onCancelBroadcast() {
177 5 : _state = _StreamGroupState.dormant;
178 :
179 10 : _subscriptions.forEach((stream, subscription) {
180 : // Cancel the broadcast streams, since we can re-listen to those later,
181 : // but allow the single-subscription streams to keep firing. Their events
182 : // will still be added to [_controller], but then they'll be dropped since
183 : // it has no listeners.
184 0 : if (!stream.isBroadcast) return;
185 0 : subscription.cancel();
186 0 : _subscriptions[stream] = null;
187 : });
188 : }
189 :
190 : /// Starts actively forwarding events from [stream] to [_controller].
191 : ///
192 : /// This will pause the resulting subscription if [this] is paused.
193 : StreamSubscription<T> _listenToStream(Stream<T> stream) {
194 15 : var subscription = stream.listen(_controller.add,
195 15 : onError: _controller.addError, onDone: () => remove(stream));
196 10 : if (_state == _StreamGroupState.paused) subscription.pause();
197 : return subscription;
198 : }
199 :
200 : /// Closes the group, indicating that no more streams will be added.
201 : ///
202 : /// If there are no streams in the group, [stream] is closed immediately.
203 : /// Otherwise, [stream] will close once all streams in the group close.
204 : ///
205 : /// Returns a [Future] that completes once [stream] has actually been closed.
206 : Future close() {
207 5 : if (_closed) return _controller.done;
208 :
209 5 : _closed = true;
210 20 : if (_subscriptions.isEmpty) _controller.close();
211 :
212 10 : return _controller.done;
213 : }
214 : }
215 :
216 : /// An enum of possible states of a [StreamGroup].
217 : class _StreamGroupState {
218 : /// The group has no listeners.
219 : ///
220 : /// New streams added to the group will be listened once the group has a
221 : /// listener.
222 : static const dormant = const _StreamGroupState("dormant");
223 :
224 : /// The group has one or more listeners and is actively firing events.
225 : ///
226 : /// New streams added to the group will be immediately listeners.
227 : static const listening = const _StreamGroupState("listening");
228 :
229 : /// The group is paused and no more events will be fired until it resumes.
230 : ///
231 : /// New streams added to the group will be listened to, but then paused. They
232 : /// will be resumed once the group itself is resumed.
233 : ///
234 : /// This state is only used by single-subscriber groups.
235 : static const paused = const _StreamGroupState("paused");
236 :
237 : /// The group is canceled and no more events will be fired ever.
238 : ///
239 : /// New streams added to the group will be listened to, canceled, and
240 : /// discarded.
241 : ///
242 : /// This state is only used by single-subscriber groups.
243 : static const canceled = const _StreamGroupState("canceled");
244 :
245 : /// The name of the state.
246 : ///
247 : /// Used for debugging.
248 : final String name;
249 :
250 5 : const _StreamGroupState(this.name);
251 :
252 0 : String toString() => name;
253 : }
|