Describe the bug 馃悶
The list Switch operator does not deliver terminal events. Completion is dropped, and errors escape as thrown exceptions rather than reaching the observer's OnError.
Switch relays changes to its observer from a private SourceList<T>:
var populator = Observable.Switch(
_sources.Do(_ => { lock (locker) { destination.Clear(); } }))
.Synchronize(locker).PopulateInto(destination);
var publisher = destination.Connect().SubscribeSafe(observer);
That list only ends when it is disposed, so it cannot terminate the output on its own, and nothing carries the terminal event of the source across to the observer. There is no error channel at all: PopulateInto has no error handler, so an OnError from either the sources sequence or the current inner sequence propagates back up the call stack instead.
The error behaviour is the more serious half. Throwing out of OnError breaks the Rx contract, and the exception surfaces at whoever called OnError, which may be an unrelated operator or user code with no way to associate it with this subscription.
Step to reproduce
var log = "";
var observer = Observer.Create<IChangeSet<int>>(
_ => log += "N", _ => log += "E", () => log += "C");
// Completion is dropped
var inner = new SourceList<int>();
inner.Add(1);
var outer = new BehaviorSubject<IObservable<IChangeSet<int>>>(inner.Connect());
using var sub = outer.Switch().SubscribeSafe(observer);
outer.OnCompleted();
inner.Dispose();
// log is "N". Observable.Switch would give "NC".
// Errors are thrown rather than delivered
var innerSubject = new Subject<IChangeSet<int>>();
var outer2 = new BehaviorSubject<IObservable<IChangeSet<int>>>(innerSubject);
using var sub2 = outer2.Switch().SubscribeSafe(observer);
innerSubject.OnError(new InvalidOperationException("boom"));
// throws InvalidOperationException out of the OnError call.
// Observable.Switch would deliver it to observer.OnError.
Both the inner and outer error cases behave this way.
Expected behavior
Identical semantics to Observable.Switch:
- Completes once the sources sequence and the inner sequence it last produced have both completed.
- Delivers errors from either the sources sequence or the current inner sequence to
OnError.
- A superseded inner sequence completing does not terminate the result.
DynamicData Version
main
Additional information 鈩癸笍
Found while fixing the same class of bug in the cache Switch (#1136, #1137). The cache version at least had an error channel, so it only lost completion. The list version has neither.
Raised separately from #1137 to keep that change scoped to the cache.
Describe the bug 馃悶
The list
Switchoperator does not deliver terminal events. Completion is dropped, and errors escape as thrown exceptions rather than reaching the observer'sOnError.Switchrelays changes to its observer from a privateSourceList<T>:That list only ends when it is disposed, so it cannot terminate the output on its own, and nothing carries the terminal event of the source across to the observer. There is no error channel at all:
PopulateIntohas no error handler, so anOnErrorfrom either the sources sequence or the current inner sequence propagates back up the call stack instead.The error behaviour is the more serious half. Throwing out of
OnErrorbreaks the Rx contract, and the exception surfaces at whoever calledOnError, which may be an unrelated operator or user code with no way to associate it with this subscription.Step to reproduce
Both the inner and outer error cases behave this way.
Expected behavior
Identical semantics to
Observable.Switch:OnError.DynamicData Version
main
Additional information 鈩癸笍
Found while fixing the same class of bug in the cache
Switch(#1136, #1137). The cache version at least had an error channel, so it only lost completion. The list version has neither.Raised separately from #1137 to keep that change scoped to the cache.