Skip to content

[Bug]: Deferred Connect() never receives OnCompleted when the source is disposed #1136

Description

@dwcullop

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions