| // Copyright (c) 2013, 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'; | |
| /// An [ErrorGroup] entangles the errors of multiple [Future]s and [Stream]s | |
| /// with one another. | |
| /// | |
| /// This allows APIs to expose multiple [Future]s and [Stream]s that have | |
| /// identical error conditions without forcing API consumers to attach error | |
| /// handling to objects they don't care about. | |
| /// | |
| /// To use an [ErrorGroup], register [Future]s and [Stream]s with it using | |
| /// [registerFuture] and [registerStream]. These methods return wrapped versions | |
| /// of the [Future]s and [Stream]s, which should then be used in place of the | |
| /// originals. For example: | |
| /// | |
| /// var errorGroup = new ErrorGroup(); | |
| /// future = errorGroup.registerFuture(future); | |
| /// stream = errorGroup.registerStream(stream); | |
| /// | |
| /// An [ErrorGroup] has two major effects on its wrapped members: | |
| /// | |
| /// * An error in any member of the group will be propagated to every member | |
| /// that hasn't already completed. If those members later complete, their | |
| /// values will be ignored. | |
| /// * If any member of this group has a listener, errors on members without | |
| /// listeners won't get passed to the top-level error handler. | |
| class ErrorGroup { | |
| /// The [Future]s that are members of `this`. | |
| final _futures = <_ErrorGroupFuture>[]; | |
| /// The [Stream]s that are members of `this`. | |
| final _streams = <_ErrorGroupStream>[]; | |
| /// Whether `this` has completed, either successfully or with an error. | |
| var _isDone = false; | |
| /// The [Completer] for [done]. | |
| final _doneCompleter = Completer<void>(); | |
| /// The underlying [Future] for [done]. | |
| /// | |
| /// We need to be able to access it internally as an [_ErrorGroupFuture] so | |
| /// we can check if it has listeners and signal errors on it. | |
| late final _ErrorGroupFuture<void> _done; | |
| /// Returns a [Future] that completes successfully when all members of `this` | |
| /// are complete, or with an error if any member receives an error. | |
| /// | |
| /// This [Future] is effectively in the group in that an error on it won't be | |
| /// passed to the top-level error handler unless no members of the group have | |
| /// listeners attached. | |
| Future<void> get done => _done; | |
| /// Creates a new group with no members. | |
| ErrorGroup() { | |
| _done = _ErrorGroupFuture(this, _doneCompleter.future); | |
| } | |
| /// Registers a [Future] as a member of `this`. | |
| /// | |
| /// Returns a wrapped version of [future] that should be used in its place. | |
| /// | |
| /// If all members of `this` have already completed successfully or with an | |
| /// error, it's a [StateError] to try to register a new [Future]. | |
| Future<T> registerFuture<T>(Future<T> future) { | |
| if (_isDone) { | |
| throw StateError( | |
| "Can't register new members on a complete " | |
| 'ErrorGroup.', | |
| ); | |
| } | |
| final wrapped = _ErrorGroupFuture(this, future); | |
| _futures.add(wrapped); | |
| return wrapped; | |
| } | |
| /// Registers a [Stream] as a member of `this`. | |
| /// | |
| /// Returns a wrapped version of [stream] that should be used in its place. | |
| /// The returned [Stream] will be multi-subscription if and only if [stream] | |
| /// is. | |
| /// | |
| /// Since all errors in a group are passed to all members, the returned | |
| /// [Stream] will automatically unsubscribe all its listeners when it | |
| /// encounters an error. | |
| /// | |
| /// If all members of `this` have already completed successfully or with an | |
| /// error, it's a [StateError] to try to register a new [Stream]. | |
| Stream<T> registerStream<T>(Stream<T> stream) { | |
| if (_isDone) { | |
| throw StateError( | |
| "Can't register new members on a complete " | |
| 'ErrorGroup.', | |
| ); | |
| } | |
| final wrapped = _ErrorGroupStream(this, stream); | |
| _streams.add(wrapped); | |
| return wrapped; | |
| } | |
| /// Sends [error] to all members of `this`. | |
| /// | |
| /// Like errors that come from members, this will only be passed to the | |
| /// top-level error handler if no members have listeners. | |
| /// | |
| /// If all members of `this` have already completed successfully or with an | |
| /// error, it's a [StateError] to try to signal an error. | |
| void signalError(Object error, [StackTrace? stackTrace]) { | |
| if (_isDone) { | |
| throw StateError("Can't signal errors on a complete ErrorGroup."); | |
| } | |
| _signalError(error, stackTrace); | |
| } | |
| /// Signal an error internally. | |
| /// | |
| /// This is just like [signalError], but instead of throwing an error if | |
| /// `this` is complete, it just does nothing. | |
| void _signalError(Object error, [StackTrace? stackTrace]) { | |
| if (_isDone) return; | |
| var caught = false; | |
| for (var future in _futures) { | |
| if (future._isDone || future._hasListeners) caught = true; | |
| future._signalError(error, stackTrace); | |
| } | |
| for (var stream in _streams) { | |
| if (stream._isDone || stream._hasListeners) caught = true; | |
| stream._signalError(error, stackTrace); | |
| } | |
| _isDone = true; | |
| _done._signalError(error, stackTrace); | |
| if (!caught && !_done._hasListeners) { | |
| scheduleMicrotask(() { | |
| // ignore: only_throw_errors | |
| throw error; | |
| }); | |
| } | |
| } | |
| /// Notifies `this` that one of its member [Future]s is complete. | |
| void _signalFutureComplete(_ErrorGroupFuture future) { | |
| if (_isDone) return; | |
| _isDone = | |
| _futures.every((future) => future._isDone) && | |
| _streams.every((stream) => stream._isDone); | |
| if (_isDone) _doneCompleter.complete(); | |
| } | |
| /// Notifies `this` that one of its member [Stream]s is complete. | |
| void _signalStreamComplete(_ErrorGroupStream stream) { | |
| if (_isDone) return; | |
| _isDone = | |
| _futures.every((future) => future._isDone) && | |
| _streams.every((stream) => stream._isDone); | |
| if (_isDone) _doneCompleter.complete(); | |
| } | |
| } | |
| /// A [Future] wrapper that keeps track of whether it's been completed and | |
| /// whether it has any listeners. | |
| /// | |
| /// It also notifies its parent [ErrorGroup] when it completes successfully or | |
| /// receives an error. | |
| class _ErrorGroupFuture<T> implements Future<T> { | |
| /// The parent [ErrorGroup]. | |
| final ErrorGroup _group; | |
| /// Whether `this` has completed, either successfully or with an error. | |
| var _isDone = false; | |
| /// The underlying [Completer] for `this`. | |
| final _completer = Completer<T>(); | |
| /// Whether `this` has any listeners. | |
| bool _hasListeners = false; | |
| /// Creates a new [_ErrorGroupFuture] that's a child of [_group] and wraps | |
| /// [inner]. | |
| _ErrorGroupFuture(this._group, Future<T> inner) { | |
| inner | |
| .then((value) { | |
| if (!_isDone) _completer.complete(value); | |
| _isDone = true; | |
| _group._signalFutureComplete(this); | |
| }) | |
| .catchError((Object e, [StackTrace? s]) async { | |
| _group._signalError(e, s); | |
| }); | |
| // Make sure _completer.future doesn't automatically send errors to the | |
| // top-level. | |
| Future<void> swallowErrors(Future future) async { | |
| try { | |
| await future; | |
| } catch (_) { | |
| // Do nothing. | |
| } | |
| } | |
| swallowErrors(_completer.future); | |
| } | |
| @override | |
| Future<S> then<S>(FutureOr<S> Function(T) onValue, {Function? onError}) { | |
| _hasListeners = true; | |
| return _completer.future.then(onValue, onError: onError); | |
| } | |
| @override | |
| Future<T> catchError(Function onError, {bool Function(Object error)? test}) { | |
| _hasListeners = true; | |
| return _completer.future.catchError(onError, test: test); | |
| } | |
| @override | |
| Future<T> whenComplete(void Function() action) { | |
| _hasListeners = true; | |
| return _completer.future.whenComplete(action); | |
| } | |
| @override | |
| Future<T> timeout(Duration timeLimit, {FutureOr<T> Function()? onTimeout}) { | |
| _hasListeners = true; | |
| return _completer.future.timeout(timeLimit, onTimeout: onTimeout); | |
| } | |
| @override | |
| Stream<T> asStream() { | |
| _hasListeners = true; | |
| return _completer.future.asStream(); | |
| } | |
| /// Signal that an error from [_group] should be propagated through `this`, | |
| /// unless it's already complete. | |
| void _signalError(Object error, [StackTrace? stackTrace]) { | |
| if (!_isDone) _completer.completeError(error, stackTrace); | |
| _isDone = true; | |
| } | |
| } | |
| // TODO(nweiz): currently streams never top-level unhandled errors (issue 7843). | |
| // When this is fixed, this class will need to prevent such errors from being | |
| // top-leveled. | |
| /// A [Stream] wrapper that keeps track of whether it's been completed and | |
| /// whether it has any listeners. | |
| /// | |
| /// It also notifies its parent [ErrorGroup] when it completes successfully or | |
| /// receives an error. | |
| class _ErrorGroupStream<T> extends Stream<T> { | |
| /// The parent [ErrorGroup]. | |
| final ErrorGroup _group; | |
| /// Whether `this` has completed, either successfully or with an error. | |
| var _isDone = false; | |
| /// The underlying [StreamController] for `this`. | |
| final StreamController<T> _controller; | |
| /// The controller's [Stream]. | |
| /// | |
| /// May be different than `_controller.stream` if the wrapped stream is a | |
| /// broadcasting stream. | |
| late final Stream<T> _stream; | |
| /// The [StreamSubscription] that connects the wrapped [Stream] to | |
| /// [_controller]. | |
| late final StreamSubscription<T> _subscription; | |
| /// Whether `this` has any listeners. | |
| bool get _hasListeners => _controller.hasListener; | |
| /// Creates a new [_ErrorGroupFuture] that's a child of [_group] and wraps | |
| /// [inner]. | |
| _ErrorGroupStream(this._group, Stream<T> inner) | |
| : _controller = StreamController(sync: true) { | |
| // Use old-style asBroadcastStream behavior - cancel source _subscription | |
| // the first time the stream has no listeners. | |
| _stream = | |
| inner.isBroadcast | |
| ? _controller.stream.asBroadcastStream( | |
| onCancel: (sub) => sub.cancel(), | |
| ) | |
| : _controller.stream; | |
| _subscription = inner.listen( | |
| _controller.add, | |
| onError: _group._signalError, | |
| onDone: () { | |
| _isDone = true; | |
| _group._signalStreamComplete(this); | |
| _controller.close(); | |
| }, | |
| ); | |
| } | |
| @override | |
| StreamSubscription<T> listen( | |
| void Function(T)? onData, { | |
| Function? onError, | |
| void Function()? onDone, | |
| bool? cancelOnError, | |
| }) { | |
| return _stream.listen( | |
| onData, | |
| onError: onError, | |
| onDone: onDone, | |
| cancelOnError: true, | |
| ); | |
| } | |
| /// Signal that an error from [_group] should be propagated through `this`, | |
| /// unless it's already complete. | |
| void _signalError(Object e, [StackTrace? stackTrace]) { | |
| if (_isDone) return; | |
| _subscription.cancel(); | |
| // Call these asynchronously to work around issue 7913. | |
| Future<void>.value().then((_) { | |
| _controller.addError(e, stackTrace); | |
| _controller.close(); | |
| }); | |
| } | |
| } |