blob: 302b3e8386fb4fcfcaacb62c293efc2061648cbc [file]
part of '../async.dart';
enum StreamStatus { waiting, active, done }
/// Stream that tracks the emitted values of the provided stream and makes
/// them available as a MobX observable value.
///
/// The latest events emitted by the stream are captured an made available as
/// MobX observable values via properties such as [data], [value], [error],
/// [hasError] and [status].
///
/// If the source `stream` is a single-subscription stream, this stream will
/// also be single-subscription. Either calling [listen] or observing [value],
/// etc. in a reaction will start the stream. Both can be done at the same time.
///
/// If the observation ends, and a subscription was never created via [listen],
/// the stream will be paused. If a subscription (created via [listen]) is
/// cancelled, the stream ends, and [value] etc. can no longer be observed
/// inside a reaction.
///
/// If the source `stream` is a broadcast stream, this stream will also be a
/// broadcast stream. This means the observable stream can be listened to
/// multiple times.
class ObservableStream<T> implements Stream<T>, ObservableValue<T?> {
/// Create a stream that tracks the emitted values of the provided stream and
/// makes them available as a MobX observable value.
///
/// If the source `stream` is a single-subscription stream, this stream will
/// also be single-subscription. If the source `stream` is a broadcast stream,
/// this stream will also be a broadcast stream.
///
/// If `initialValue` is provided, [value] will use it as the initial value
/// while waiting for the first item to be emitted from the source stream.
/// If the stream is a single-subscription stream, `initialValue` will also be
/// the first value emitted to the subscription created by [listen].
///
/// If `cancelOnError` is `true`, the stream will be cancelled when an error
/// event is emitted by the source stream. The default value is `false`.
ObservableStream(Stream<T> stream,
{T? initialValue,
bool cancelOnError = false,
ReactiveContext? context,
String? name})
: this._(
context ?? mainContext, stream, initialValue, cancelOnError, name);
ObservableStream._(ReactiveContext context, this._stream, this._initialValue,
this._cancelOnError, String? name)
: _context = context {
_name = name ?? _context.nameFor('ObservableStream<$T>');
}
T? _initialValue;
final bool _cancelOnError;
final ReactiveContext _context;
final Stream<T> _stream;
late String _name;
String get name => _name;
_ObservableStreamController<T>? _controllerField;
_ObservableStreamController<T> get _controller {
if (_controllerField == null) {
_controllerField = _ObservableStreamController<T>(
_context, _stream, _initialValue,
cancelOnError: _cancelOnError, name: '$name.StreamController');
_initialValue = null;
}
return _controllerField!;
}
// Current value or error if failed.
dynamic get data => _controller.data;
/// Current value or null if waiting and no initialValue, or null if data is an error.
@override
T? get value =>
_controller.valueType == _ValueType.value && _controller.data != null
? _controller.data as T
: null;
/// Current error or null if not failed.
dynamic get error =>
_controller.valueType == _ValueType.error ? _controller.data : null;
/// Current data is an error.
bool get hasError => _controller.valueType == _ValueType.error;
/// Current stream status.
StreamStatus get status => _controller.status;
/// Maps the current status and value or error into a value.
///
/// Returns null if a callback is not provided for the active status.
/// If [done] is null, [active] and [error] are used instead.
R? match<R>(
{R Function()? waiting,
R Function(T)? active,
// ignore:avoid_annotating_with_dynamic
R Function(dynamic)? error,
// ignore:avoid_annotating_with_dynamic
R Function(T?, dynamic)? done}) {
final status = _controller.status;
if (status == StreamStatus.waiting) {
return waiting == null ? null : waiting();
}
final data = _controller.data;
final hasValue = _controller.valueType == _ValueType.value;
final overrideDone = status == StreamStatus.done && done == null;
final isActive = status == StreamStatus.active;
if (isActive || overrideDone) {
if (hasValue) {
return active == null ? null : active(data as T);
} else {
return error == null ? null : error(data);
}
}
return hasValue ? done!(data as T, null) : done!(null, data);
}
/// Create a new stream with the provided initialValue and cancelOnError.
ObservableStream<T> configure(
{T? initialValue, bool cancelOnError = false}) =>
ObservableStream._(_context, _stream, initialValue, cancelOnError, name);
ObservableStream<R> _wrap<R>(Stream<R> stream) =>
ObservableStream._(_context, stream, null, _cancelOnError, name);
ObservableFuture<R> _wrapFuture<R>(Future<R> future) =>
ObservableFuture._(_context, future, FutureStatus.pending, null, name);
/// Close the observable stream, and stop any future updates to observable
/// properties or any stream subscribers.
///
/// Most of the time, this method doesn't need to be called. ObservableStream
/// can clean-up automatically. This is always true if the original stream
/// is a broadcast stream.
///
/// However, if the original stream is a single-subscription stream and you
/// previously observed the stream's properties ([data], [value], [error],
/// [hasError], [status], etc.) but then stopped the observation (thereby
/// pausing the stream), then this method can be used to ensure the original
/// paused stream closes correctly.
///
/// Note that if you [listen] to this observable stream, the observable stream
/// will be closed automatically when you cancel the subscription.
Future<void> close() => _controller.close();
// Delegated methods
@override
ObservableFuture<bool> any(bool Function(T element) test) =>
_wrapFuture(_controller.stream.any(test));
@override
ObservableStream<T> asBroadcastStream(
{void Function(StreamSubscription<T> subscription)? onListen,
void Function(StreamSubscription<T> subscription)? onCancel}) =>
_wrap(_controller.stream
.asBroadcastStream(onListen: onListen, onCancel: onCancel));
@override
ObservableStream<E> asyncExpand<E>(Stream<E>? Function(T event) convert) =>
_wrap(_controller.stream.asyncExpand(convert));
@override
ObservableStream<E> asyncMap<E>(FutureOr<E> Function(T event) convert) =>
_wrap(_controller.stream.asyncMap(convert));
@override
ObservableStream<R> cast<R>() => _wrap(_controller.stream.cast());
@override
ObservableFuture<bool> contains(Object? needle) =>
_wrapFuture(_controller.stream.contains(needle));
@override
ObservableStream<T> distinct([bool Function(T previous, T next)? equals]) =>
_wrap(_controller.stream.distinct(equals));
@override
ObservableFuture<E> drain<E>([E? futureValue]) =>
_wrapFuture(_controller.stream.drain(futureValue));
@override
ObservableFuture<T> elementAt(int index) =>
_wrapFuture(_controller.stream.elementAt(index));
@override
ObservableFuture<bool> every(bool Function(T element) test) =>
_wrapFuture(_controller.stream.every(test));
@override
ObservableStream<S> expand<S>(Iterable<S> Function(T element) convert) =>
_wrap(_controller.stream.expand(convert));
@override
ObservableFuture<T> get first => _wrapFuture(_controller.stream.first);
@override
ObservableFuture<T> firstWhere(bool Function(T element) test,
{T Function()? orElse}) =>
_wrapFuture(_controller.stream.firstWhere(test, orElse: orElse));
@override
ObservableFuture<S> fold<S>(
S initialValue, S Function(S previous, T element) combine) =>
_wrapFuture(_controller.stream.fold(initialValue, combine));
@override
ObservableFuture forEach(void Function(T element) action) =>
_wrapFuture(_controller.stream.forEach(action));
@override
ObservableStream<T> handleError(Function onError,
// ignore:avoid_annotating_with_dynamic
{bool Function(dynamic)? test}) =>
_wrap(_controller.stream.handleError(onError, test: test));
@override
bool get isBroadcast => _controller.stream.isBroadcast;
@override
ObservableFuture<bool> get isEmpty => _wrapFuture(_controller.stream.isEmpty);
@override
ObservableFuture<String> join([String separator = '']) =>
_wrapFuture(_controller.stream.join(separator));
@override
ObservableFuture<T> get last => _wrapFuture(_controller.stream.last);
@override
ObservableFuture<T> lastWhere(bool Function(T element) test,
{T Function()? orElse}) =>
_wrapFuture(_controller.stream.lastWhere(test, orElse: orElse));
@override
ObservableFuture<int> get length => _wrapFuture(_controller.stream.length);
@override
StreamSubscription<T> listen(
void Function(T value)? onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) {
if (_controller.isCancelled) {
throw StateError('Tried to observe or listen to an observable stream '
'after the stream has already ended / closed.');
}
return _controller.stream.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
@override
ObservableStream<S> map<S>(S Function(T event) convert) =>
_wrap(_controller.stream.map(convert));
@override
ObservableFuture pipe(StreamConsumer<T> streamConsumer) =>
_wrapFuture(_controller.stream.pipe(streamConsumer));
@override
ObservableFuture<T> reduce(T Function(T previous, T element) combine) =>
_wrapFuture(_controller.stream.reduce(combine));
@override
ObservableFuture<T> get single => _wrapFuture(_controller.stream.single);
@override
ObservableFuture<T> singleWhere(bool Function(T element) test,
{T Function()? orElse}) =>
_wrapFuture(_controller.stream.singleWhere(test, orElse: orElse));
@override
ObservableStream<T> skip(int count) => _wrap(_controller.stream.skip(count));
@override
ObservableStream<T> skipWhile(bool Function(T element) test) =>
_wrap(_controller.stream.skipWhile(test));
@override
ObservableStream<T> take(int count) => _wrap(_controller.stream.take(count));
@override
ObservableStream<T> takeWhile(bool Function(T element) test) =>
_wrap(_controller.stream.takeWhile(test));
@override
ObservableStream<T> timeout(Duration timeLimit,
{void Function(EventSink<T> sink)? onTimeout}) =>
_wrap(_controller.stream.timeout(timeLimit, onTimeout: onTimeout));
@override
ObservableFuture<List<T>> toList() =>
_wrapFuture(_controller.stream.toList());
@override
ObservableFuture<Set<T>> toSet() => _wrapFuture(_controller.stream.toSet());
@override
ObservableStream<S> transform<S>(StreamTransformer<T, S> streamTransformer) =>
_wrap(_controller.stream.transform(streamTransformer));
@override
ObservableStream<T> where(bool Function(T event) test) =>
_wrap(_controller.stream.where(test));
}
enum _ValueType { value, error }
class _ObservableStreamController<T> {
_ObservableStreamController(
ReactiveContext context,
this.origStream,
T? initialValue, {
required this.cancelOnError,
required this.name,
}) : _initialStreamValue = origStream.isBroadcast ? null : initialValue,
_actions =
ActionController(context: context, name: '$name.ActionController'),
_status = Observable(
initialValue == null ? StreamStatus.waiting : StreamStatus.active,
context: context,
name: '$name.status'),
_valueType = Observable(_ValueType.value,
context: context, name: '$name.valueType'),
_data = Observable(initialValue, context: context, name: '$name.data') {
_status
..onBecomeObserved(_listen)
..onBecomeUnobserved(_unsubscribe);
_valueType
..onBecomeObserved(_listen)
..onBecomeUnobserved(_unsubscribe);
_data
..onBecomeObserved(_listen)
..onBecomeUnobserved(_unsubscribe);
_controller = origStream.isBroadcast
? StreamController<T>.broadcast(
onListen: _listen,
onCancel: _unsubscribe,
sync: true,
)
: StreamController<T>(
onListen: _listen,
onPause: _unsubscribe,
onResume: _listen,
onCancel: _onCancel,
sync: true,
);
}
final String name;
final bool cancelOnError;
final Stream<T> origStream;
StreamSubscription<T>? _subscription;
T? _initialStreamValue;
final ActionController _actions;
final Observable<_ValueType> _valueType;
_ValueType get valueType => _valueType.value;
final Observable _data;
dynamic get data => _data.value;
final Observable<StreamStatus> _status;
StreamStatus get status => _status.value;
late final Stream<T> stream = _controller.stream;
late final StreamController<T> _controller;
int _listenCount = 0;
bool _isCancelled = false;
bool get isCancelled => _isCancelled;
Future<void> _onCancel() async {
_unsubscribe();
await close();
}
Future<void> close() async {
_isCancelled = true;
await _subscription?.cancel();
_subscription = null;
// controller.close() never completes if it's a non-broadcast stream
// with no listeners. Avoid this case.
if (origStream.isBroadcast || _controller.hasListener) {
await _controller.close();
}
}
void _listen() {
_listenCount++;
if (_subscription == null) {
if (_isCancelled) {
throw StateError('Tried to observe or listen to an observable stream '
'after the stream has already ended / closed.');
}
_subscription = origStream.listen(_onData,
onError: _onError, onDone: _onDone, cancelOnError: cancelOnError);
scheduleMicrotask(_tryInsertInitialValue);
} else if (_subscription!.isPaused) {
_subscription!.resume();
}
}
void _unsubscribe() {
if (_isCancelled) {
return;
}
_listenCount--;
if (_listenCount == 0 && !(_subscription?.isPaused ?? true)) {
if (origStream.isBroadcast) {
_subscription!.cancel();
_subscription = null;
} else {
_subscription!.pause();
}
}
}
void _onData(T data) {
final actionInfo = _actions.startAction();
try {
_status.value = StreamStatus.active;
_valueType.value = _ValueType.value;
_data.value = data;
} finally {
_actions.endAction(actionInfo);
_tryInsertInitialValue();
_controller.add(data);
}
}
void _onError(Object error) {
final actionInfo = _actions.startAction();
try {
_status.value = StreamStatus.active;
_valueType.value = _ValueType.error;
_data.value = error;
} finally {
_actions.endAction(actionInfo);
_tryInsertInitialValue();
_controller.addError(error);
}
}
void _onDone() {
final actionInfo = _actions.startAction();
try {
_status.value = StreamStatus.done;
} finally {
_actions.endAction(actionInfo);
_tryInsertInitialValue();
_controller.close();
}
}
void _tryInsertInitialValue() {
final initialStreamValue = _initialStreamValue;
if (initialStreamValue != null) {
_initialStreamValue = null;
_controller.add(initialStreamValue);
}
}
}