| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'async_expand.dart'; | |
| import 'common_callbacks.dart'; | |
| /// A utility to take events from the most recent sub stream returned by a | |
| /// callback. | |
| extension Switch<T> on Stream<T> { | |
| /// Maps events to a Stream and emits values from the most recently created | |
| /// Stream. | |
| /// | |
| /// When the source emits a value it will be converted to a [Stream] using | |
| /// [convert] and the output will switch to emitting events from that result. | |
| /// Like [asyncExpand] but the [Stream] emitted by a previous element | |
| /// will be ignored as soon as the source stream emits a new event. | |
| /// | |
| /// This means that the source stream is not paused until a sub stream | |
| /// returned from the [convert] callback is done. Instead, the subscription | |
| /// to the sub stream is canceled as soon as the source stream emits a new | |
| /// event. | |
| /// | |
| /// Errors from [convert], the source stream, or any of the sub streams are | |
| /// forwarded to the result stream. | |
| /// | |
| /// The result stream will not close until the source stream closes and | |
| /// the current sub stream have closed. | |
| /// | |
| /// If the source stream is a broadcast stream, the result will be as well, | |
| /// regardless of the types of streams created by [convert]. In this case, | |
| /// some care should be taken: | |
| /// | |
| /// * If [convert] returns a single subscription stream it may be listened to | |
| /// and never canceled. | |
| /// | |
| /// See also: | |
| /// - [concurrentAsyncExpand], which emits events from all sub streams | |
| /// concurrently instead of cancelling subscriptions to previous subs | |
| /// streams. | |
| Stream<S> switchMap<S>(Stream<S> Function(T) convert) { | |
| return map(convert).switchLatest(); | |
| } | |
| } | |
| /// A utility to take events from the most recent sub stream. | |
| extension SwitchLatest<T> on Stream<Stream<T>> { | |
| /// Emits values from the most recently emitted Stream. | |
| /// | |
| /// When the source emits a stream, the output will switch to emitting events | |
| /// from that stream. | |
| /// | |
| /// Whether the source stream is a single-subscription stream or a | |
| /// broadcast stream, the result stream will be the same kind of stream, | |
| /// regardless of the types of streams emitted. | |
| Stream<T> switchLatest() { | |
| var controller = isBroadcast | |
| ? StreamController<T>.broadcast(sync: true) | |
| : StreamController<T>(sync: true); | |
| controller.onListen = () { | |
| StreamSubscription<T>? innerSubscription; | |
| var outerStreamDone = false; | |
| void listenToInnerStream(Stream<T> innerStream) { | |
| assert(innerSubscription == null); | |
| var subscription = innerStream | |
| .listen(controller.add, onError: controller.addError, onDone: () { | |
| innerSubscription = null; | |
| if (outerStreamDone) controller.close(); | |
| }); | |
| // If a pause happens during an innerSubscription.cancel, | |
| // we still listen to the next stream when the cancel is done. | |
| // Then we immediately pause it again here. | |
| if (controller.isPaused) subscription.pause(); | |
| innerSubscription = subscription; | |
| } | |
| var addError = controller.addError; | |
| final outerSubscription = listen(null, onError: addError, onDone: () { | |
| outerStreamDone = true; | |
| if (innerSubscription == null) controller.close(); | |
| }); | |
| outerSubscription.onData((innerStream) async { | |
| var currentSubscription = innerSubscription; | |
| if (currentSubscription == null) { | |
| listenToInnerStream(innerStream); | |
| return; | |
| } | |
| innerSubscription = null; | |
| outerSubscription.pause(); | |
| try { | |
| await currentSubscription.cancel(); | |
| } catch (error, stack) { | |
| controller.addError(error, stack); | |
| } finally { | |
| if (!isBroadcast && !controller.hasListener) { | |
| // Result single-subscription stream subscription was cancelled | |
| // while waiting for previous innerStream cancel. | |
| // | |
| // Ensure that the last received stream is also listened to and | |
| // cancelled, then do nothing further. | |
| innerStream.listen(null).cancel().ignore(); | |
| } else { | |
| outerSubscription.resume(); | |
| listenToInnerStream(innerStream); | |
| } | |
| } | |
| }); | |
| if (!isBroadcast) { | |
| controller | |
| ..onPause = () { | |
| innerSubscription?.pause(); | |
| outerSubscription.pause(); | |
| } | |
| ..onResume = () { | |
| innerSubscription?.resume(); | |
| outerSubscription.resume(); | |
| }; | |
| } | |
| controller.onCancel = () { | |
| var sub = innerSubscription; | |
| var cancels = [ | |
| if (!outerStreamDone) outerSubscription.cancel(), | |
| if (sub != null) sub.cancel(), | |
| ]; | |
| if (cancels.isEmpty) return null; | |
| return cancels.wait.then(ignoreArgument); | |
| }; | |
| }; | |
| return controller.stream; | |
| } | |
| } |