Describe the bug 🐞
A Connect() that gets deferred never receives OnCompleted when the source is later disposed.
ObservableCache.Connect() defers when notifications are suspended, rather than subscribing immediately:
: _suspensionTracker.Value.NotificationsSuspendedObservable.Do(static _ => { }, observer.OnCompleted)
.Where(static b => !b).Take(1).Select(_ => CreateConnectObservable(predicate, suppressEmptyChangeSets)).Switch();
Once the suspension lifts, the subscriber activates and works normally: it gets its initial changeset and every subsequent one. But it never terminates. Disposing the cache completes an ordinary subscriber and does nothing for a deferred one.
That leaks. Anything relying on Finally for cleanup, or awaiting completion, waits forever. Watch() has the same shape.
Step to reproduce
var log = "";
var observer = Observer.Create<IChangeSet<int, int>>(
_ => log += "OnNext;",
_ => log += "OnError;",
() => log += "OnCompleted;");
var cache = new SourceCache<int, int>(static x => x);
using (cache.Connect().Subscribe()) { }
var suspend = cache.SuspendNotifications();
cache.AddOrUpdate(1);
cache.Connect().SubscribeSafe(observer); // deferred by the suspension
suspend.Dispose();
cache.AddOrUpdate(2);
cache.Dispose();
// log is "OnNext;OnNext;" - the OnCompleted never arrives.
// Connecting outside a suspension gives "OnNext;OnCompleted;"
Expected behavior
A deferred subscriber should be indistinguishable from an ordinary one once it activates, including receiving OnCompleted (or OnError) when the source terminates.
DynamicData Version
main
Additional information ℹ️
Found while reviewing #1133, which reuses this deferral shape to defer subscriptions made during an Edit(). That PR is not the cause, but it does turn this from a corner case into a routine one: connecting during a suspension is unusual, connecting during an edit is not. SourceList picks up the same behavior there.
Errors are unaffected, OnError propagates correctly on both paths. It is specifically completion that is lost.
Probably worth fixing on the suspension path here so #1133 inherits the fix rather than duplicating the gap.
Describe the bug 🐞
A
Connect()that gets deferred never receivesOnCompletedwhen the source is later disposed.ObservableCache.Connect()defers when notifications are suspended, rather than subscribing immediately:Once the suspension lifts, the subscriber activates and works normally: it gets its initial changeset and every subsequent one. But it never terminates. Disposing the cache completes an ordinary subscriber and does nothing for a deferred one.
That leaks. Anything relying on
Finallyfor cleanup, or awaiting completion, waits forever.Watch()has the same shape.Step to reproduce
Expected behavior
A deferred subscriber should be indistinguishable from an ordinary one once it activates, including receiving
OnCompleted(orOnError) when the source terminates.DynamicData Version
main
Additional information ℹ️
Found while reviewing #1133, which reuses this deferral shape to defer subscriptions made during an
Edit(). That PR is not the cause, but it does turn this from a corner case into a routine one: connecting during a suspension is unusual, connecting during an edit is not.SourceListpicks up the same behavior there.Errors are unaffected,
OnErrorpropagates correctly on both paths. It is specifically completion that is lost.Probably worth fixing on the suspension path here so #1133 inherits the fix rather than duplicating the gap.