// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Reactive.Disposables;
using System.Threading;
using System.Runtime.CompilerServices;
using System.Reactive.Concurrency;
namespace System.Reactive.Subjects
{
///
/// Represents the result of an asynchronous operation.
/// The last value before the OnCompleted notification, or the error received through OnError, is sent to all subscribed observers.
///
/// The type of the elements processed by the subject.
public sealed class AsyncSubject : ISubject, IDisposable
#if HAS_AWAIT
, INotifyCompletion
#endif
{
private readonly object _gate = new object();
private ImmutableList> _observers;
private bool _isDisposed;
private bool _isStopped;
private T _value;
private bool _hasValue;
private Exception _exception;
///
/// Creates a subject that can only receive one value and that value is cached for all future observations.
///
public AsyncSubject()
{
_observers = new ImmutableList>();
}
///
/// Indicates whether the subject has observers subscribed to it.
///
public bool HasObservers
{
get
{
var observers = _observers;
return observers != null && observers.Data.Length > 0;
}
}
///
/// Notifies all subscribed observers about the end of the sequence, also causing the last received value to be sent out (if any).
///
public void OnCompleted()
{
var os = default(IObserver[]);
var v = default(T);
var hv = false;
lock (_gate)
{
CheckDisposed();
if (!_isStopped)
{
os = _observers.Data;
_observers = new ImmutableList>();
_isStopped = true;
v = _value;
hv = _hasValue;
}
}
if (os != null)
{
if (hv)
{
foreach (var o in os)
{
o.OnNext(v);
o.OnCompleted();
}
}
else
foreach (var o in os)
o.OnCompleted();
}
}
///
/// Notifies all subscribed observers about the exception.
///
/// The exception to send to all observers.
/// is null.
public void OnError(Exception error)
{
if (error == null)
throw new ArgumentNullException("error");
var os = default(IObserver[]);
lock (_gate)
{
CheckDisposed();
if (!_isStopped)
{
os = _observers.Data;
_observers = new ImmutableList>();
_isStopped = true;
_exception = error;
}
}
if (os != null)
foreach (var o in os)
o.OnError(error);
}
///
/// Sends a value to the subject. The last value received before successful termination will be sent to all subscribed and future observers.
///
/// The value to store in the subject.
public void OnNext(T value)
{
lock (_gate)
{
CheckDisposed();
if (!_isStopped)
{
_value = value;
_hasValue = true;
}
}
}
///
/// Subscribes an observer to the subject.
///
/// Observer to subscribe to the subject.
/// Disposable object that can be used to unsubscribe the observer from the subject.
/// is null.
public IDisposable Subscribe(IObserver observer)
{
if (observer == null)
throw new ArgumentNullException("observer");
var ex = default(Exception);
var v = default(T);
var hv = false;
lock (_gate)
{
CheckDisposed();
if (!_isStopped)
{
_observers = _observers.Add(observer);
return new Subscription(this, observer);
}
ex = _exception;
hv = _hasValue;
v = _value;
}
if (ex != null)
observer.OnError(ex);
else if (hv)
{
observer.OnNext(v);
observer.OnCompleted();
}
else
observer.OnCompleted();
return Disposable.Empty;
}
class Subscription : IDisposable
{
private readonly AsyncSubject _subject;
private IObserver _observer;
public Subscription(AsyncSubject subject, IObserver observer)
{
_subject = subject;
_observer = observer;
}
public void Dispose()
{
if (_observer != null)
{
lock (_subject._gate)
{
if (!_subject._isDisposed && _observer != null)
{
_subject._observers = _subject._observers.Remove(_observer);
_observer = null;
}
}
}
}
}
void CheckDisposed()
{
if (_isDisposed)
throw new ObjectDisposedException(string.Empty);
}
///
/// Unsubscribe all observers and release resources.
///
public void Dispose()
{
lock (_gate)
{
_isDisposed = true;
_observers = null;
_exception = null;
_value = default(T);
}
}
#if HAS_AWAIT
///
/// Gets an awaitable object for the current AsyncSubject.
///
/// Object that can be awaited.
public AsyncSubject GetAwaiter()
{
return this;
}
///
/// Specifies a callback action that will be invoked when the subject completes.
///
/// Callback action that will be invoked when the subject completes.
/// is null.
public void OnCompleted(Action continuation)
{
if (continuation == null)
throw new ArgumentNullException("continuation");
OnCompleted(continuation, true);
}
#endif
private void OnCompleted(Action continuation, bool originalContext)
{
//
// [OK] Use of unsafe Subscribe: this type's Subscribe implementation is safe.
//
this.Subscribe/*Unsafe*/(new AwaitObserver(continuation, originalContext));
}
class AwaitObserver : IObserver
{
#if HAS_AWAIT
private readonly SynchronizationContext _context;
#endif
private readonly Action _callback;
public AwaitObserver(Action callback, bool originalContext)
{
#if HAS_AWAIT
if (originalContext)
_context = SynchronizationContext.Current;
#else
System.Diagnostics.Debug.Assert(!originalContext);
#endif
_callback = callback;
}
public void OnCompleted()
{
InvokeOnOriginalContext();
}
public void OnError(Exception error)
{
InvokeOnOriginalContext();
}
public void OnNext(T value)
{
}
private void InvokeOnOriginalContext()
{
#if HAS_AWAIT
if (_context != null)
{
//
// No need for OperationStarted and OperationCompleted calls here;
// this code is invoked through await support and will have a way
// to observe its start/complete behavior, either through returned
// Task objects or the async method builder's interaction with the
// SynchronizationContext object.
//
_context.Post(c => ((Action)c)(), _callback);
}
else
#endif
{
_callback();
}
}
}
///
/// Gets whether the AsyncSubject has completed.
///
public bool IsCompleted
{
get
{
return _isStopped;
}
}
///
/// Gets the last element of the subject, potentially blocking until the subject completes successfully or exceptionally.
///
/// The last element of the subject. Throws an InvalidOperationException if no element was received.
/// The source sequence is empty.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Await pattern for C# and VB compilers.")]
public T GetResult()
{
if (!_isStopped)
{
var e = new ManualResetEvent(false);
OnCompleted(() => e.Set(), false);
e.WaitOne();
}
_exception.ThrowIfNotNull();
if (!_hasValue)
throw new InvalidOperationException(Strings_Linq.NO_ELEMENTS);
return _value;
}
}
}