Skip to content

Alternative to both ref-counted and current proposal - force strategy selection at creation time #198

@Jamesernator

Description

@Jamesernator

So there is now a pr for ref-counted, however I'm not convinced this is the best route to take.

For one it limits the ability of observables to even have cold-like behaviour such as replays (or custom implementations of ref-counting).

Secondly even with ref-counting some behaviours in the other motivating issue remain confusing for notably:

Making accidental repeated API requests (ex. calling subscribe twice and forgetting a replay)

continues to be confusing with the ref-counted behaviour as ref-counting only prevents double requests if subscriptions happen concurrently, but if a previous request has already finished then a new subscription will cause a new request regardless even if the author intended replaying the request.

And even more disastrously, if a request subscribes in the immediate between the .next() and .complete() the request will be ignored all-together, i.e. this:

const req = new Observable(subscriber => {
    fetch("foo.txt").then((response) => {
        subscriber.next(response);
        subscriber.complete();
    });
});

reqObservable.subscribe({
    next: (res) => {
        console.log("Got response outer");
        // in some nested call, synchronously:
        req.subscribe({
            next: (res) => {
                console.log("Got response inner");
            },
            complete: () => {
                console.log("Complete inner");
            },
        });
    },
    complete: () => {
        console.log("Complete outer");
    },
});

prints:

Got respone outer
Complete outer
Complete inner

in other words, the inner next is never actually called because it subscribed late, so misses the value, pretty useless for network requests or other promise adoption.


As a solution to both the limitations/confusions/disasters of both current approaches I suggest forcing the decision of whether observables are hot or cold, and whether they are eager or replay, to construction time. This means producers of observables actually have to consider what behaviour they want the observable to have rather than being surprised later.

To do this I propose making the actual constructor private and instead expose the different strategies as static methods:

type SubscribeCallback<T> = (subscriber: Subscriber<T>) => void;

class Observable<T> {
    /* Behaves like the current proposal */
    static lazyUnshared<T>(subscribeCallback: SubscribeCallback<T>): Observable<T>;

    /* Behaves like the ref-counted PR */
    static lazyShared<T>(subscribeCallback: SubscribeCallback<T>): Observable<T>;

    /* Not currently proposed, but allows for re-using values, for example from requests */
    static lazyReplay<T>(subscribeCallback: SubscribeCallback<T>): Observable<T>;

    /* Not currently proposed, but helps address the feedback from issue #170 of
       > API requests at unpredictable and undesirable times (too late when the UI is rendering or too early during critical loading)
    */
    static eagerReplay<T>(subscribeCallback: SubscribeCallback<T>): Observable<T>;
}

these could be used like:

// Each subscription gets it's own timer
const unsharedInterval = Observable.lazyUnshared(subscriber => {
    const interval = setInterval(() => subscriber.next(), 1000);
    subscriber.signal.onabort = () => clearInterval(interval);
});

// Each subscription shares the same timer so will be aligned
const sharedInterval = Observable.lazyShared(subscriber => {
    const interval = setInterval(() => subscriber.next(), 1000);
    subscriber.signal.onabort = () => clearInterval(interval);
});

// The call isn't started until subscription, however once it is called the same values will be replayed
// on all future subscriptions
const networkRequest = Observable.lazyReplay(subscriber => {
     fetch("foo.txt").then(res => { subscriber.next(res); subscriber.complete(res); });
});

// Same the previous example, except the callback is invoked immediately so subscription
// might be able to receive the values earlier than needed
const eagerNetworkRequest = Observable.eagerReplay(subscriber => {
     fetch("foo.txt").then(res => { subscriber.next(res); subscriber.complete(res); });
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions