9

In rxjs5, I have an AsyncSubject and want to subscribe to it multiple times, but only ONE subscriber should ever receive the next() event. All others (if they are not yet unsubscribed) should immediately get the complete() event without next().

Example:

let fired = false;
let as = new AsyncSubject();

const setFired = () => {
    if (fired == true) throw new Error("Multiple subscriptions executed");
    fired = true;
}

let subscription1 = as.subscribe(setFired);
let subscription2 = as.subscribe(setFired);

// note that subscription1/2 could be unsubscribed from in the future
// and still only a single subscriber should be triggered

setTimeout(() => {
    as.next(undefined);
    as.complete();
}, 500);
Dynalon
  • 6,577
  • 10
  • 54
  • 84
  • Subjects are multicast by design, such that all their subscribers will receive events. I'm not sure RxJs is going to support what you're trying to do. This does feel like an X-Y problem -- can you give us more insight on exactly what you're trying to accomplish? – Thorn G Nov 01 '16 at 18:01

2 Answers2

1

You can easily implement this by writing a small class that wraps the initial AsyncSubject

import {AsyncSubject, Subject, Observable, Subscription} from 'rxjs/RX'

class SingleSubscriberObservable<T> {
    private newSubscriberSubscribed = new Subject();

    constructor(private sourceObservable: Observable<T>) {}

    subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Subscription {
        this.newSubscriberSubscribed.next();
        return this.sourceObservable.takeUntil(this.newSubscriberSubscribed).subscribe(next, error, complete);
    }
}

You can then try it out in your example:

const as = new AsyncSubject();
const single = new SingleSubscriberObservable(as)

let fired = false;

function setFired(label:string){
    return ()=>{
        if(fired == true) throw new Error("Multiple subscriptions executed");
        console.log("FIRED", label);
        fired = true;
    }
}

function logDone(label: string){
    return ()=>{
       console.log(`${label} Will stop subscribing to source observable`);
    }
}

const subscription1 = single.subscribe(setFired('First'), ()=>{}, logDone('First'));
const subscription2 = single.subscribe(setFired('Second'), ()=>{}, logDone('Second'));
const subscription3 = single.subscribe(setFired('Third'), ()=>{}, logDone('Third'));

setTimeout(()=>{
    as.next(undefined);
    as.complete();
}, 500)

The key here is this part:

subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Subscription {
    this.newSubscriberSusbscribed.next();
    return this.sourceObservable.takeUntil(this.newSubscriberSubscribed).subscribe(next, error, complete);
}

Every time someone calls subscribe we will signal the newSubscriberSubscribed subject.

When we subscribe to the underlying Observable we use

takeUntil(this.newSubscriberSubscribed)

This means that when the next subscriber calls:

this.newSubscriberSubscribed.next()

The previously returned observable will complete.

So this will result in what you are asking which is that the previous subscription complete whenever a new subscription comes along.

The output of the application would be:

First Will stop subscribing to source observable
Second Will stop subscribing to source observable
FIRED Third
Third Will stop subscribing to source observable

EDIT:

If you want to do it where the first that subscribed stays subscribed and all future subscriptions receive immediate complete (so that while the earliest subscriber is still subscribed nobody else can subscribe). You can do it like this:

class SingleSubscriberObservable<T> {
    private isSubscribed: boolean = false;

    constructor(private sourceObservable: Observable<T>) {}

    subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Subscription {
        if(this.isSubscribed){
            return Observable.empty().subscribe(next, error, complete);    
        }
        this.isSubscribed = true;
        var unsubscribe = this.sourceObservable.subscribe(next, error, complete);
        return new Subscription(()=>{
            unsubscribe.unsubscribe();
            this.isSubscribed = false;
        });
    }
}

We keep a flag this.isSusbscribed to keep track of whether there is someone currently subscribed. We also return a custom subscription that we can use to set this flag back to false when things unsubscribe.

Whenever someone tries to subscribe, if we instead susbscribe them to an empty Observable which would complete immediately. The output would look like:

Second Will stop subscribing to source observable
Third Will stop subscribing to source observable
FIRED First
First Will stop subscribing to source observable
Daniel Tabuenca
  • 13,147
  • 3
  • 35
  • 38
  • `this.subscriptions.next()` is unknown because there is no `this.subscriptions` field in your example? – Dynalon Nov 08 '16 at 07:56
  • I don't think your example handles correctly if a subscriber unsubscribed before the .next() event. See my other comment: "Not the first subscriber should fire, but rather the earliest subscriber that is still subscribed" – Dynalon Nov 08 '16 at 07:58
  • I edited, it should be `this.newSubscriberSubscribed` it was left over from a previous edit when I changed the name to make it clearer. – Daniel Tabuenca Nov 08 '16 at 15:22
  • It wasn't clear from your question that was the behavior you wanted. I edited the Answer to include a different version of the `SingleSubscriberObservable` that will make the first subscriber be the single one. – Daniel Tabuenca Nov 08 '16 at 15:42
0

The simples way is to wrap your AsyncSubject in another object that handles the logic of calling 1 subscriber only. Assuming you want to invoke the 1st subscriber only, the code below should be a good starting point

let as = new AsyncSubject();

const createSingleSubscriberAsyncSubject = as => {
    // define empty array for subscribers
    let subscribers = [];

    const subscribe = callback => {
        if (typeof callback !== 'function') throw new Error('callback provided is not a function');

        subscribers.push(callback);

        // return a function to unsubscribe
        const unsubscribe = () => { subscribers = subscribers.filter(cb => cb !== callback); };
        return unsubscribe;
    };

    // the main subscriber that will listen to the original AS
    const mainSubscriber = (...args) => {
        // you can change this logic to invoke the last subscriber
        if (subscribers[0]) {
            subscribers[0](...args);
        }
    };

    as.subscribe(mainSubscriber);

    return {
        subscribe,
        // expose original AS methods as needed
        next: as.next.bind(as),
        complete: as.complete.bind(as),
    };
};

// use

const singleSub = createSingleSubscriberAsyncSubject(as);

// add 3 subscribers
const unsub1 = singleSub.subscribe(() => console.log('subscriber 1'));
const unsub2 = singleSub.subscribe(() => console.log('subscriber 2'));
const unsub3 = singleSub.subscribe(() => console.log('subscriber 3'));

// remove first subscriber
unsub1();

setTimeout(() => {
    as.next(undefined);
    as.complete();
    // only 'subscriber 2' is printed
}, 500);
gafi
  • 12,113
  • 2
  • 30
  • 32
  • the 1st subscriber is not correct, because subscribers can unsubscribe. It should read "the earliest subscriber that is still subscribed" – Dynalon Nov 08 '16 at 07:54
  • This is what it is doing now. It will add subscribers to the array in order, if any of them unsubscribe it will be removed from the array, so the first element in the array will always be the earliest one that is still subscribed. Let me know if this is not what you mean – gafi Nov 08 '16 at 11:26
  • One tiny comment: Your solution does not send .complete() to the other subscribers, but not everyone needs this and I know how to add it so I am awarding you the bounty. – Dynalon Nov 09 '16 at 06:50
  • My answer was shorter, sent the `.complete()` was more performant (no array searches) and more idiomatic RX. Oh well.... – Daniel Tabuenca Nov 09 '16 at 10:09
  • @Dyna Tell me what you want the other subscribers to do when `.complete()` is called, I'll add it for sake of completeness – gafi Nov 09 '16 at 13:33
  • @dtabuenc Since your edit the answer was well appreciated and I consider your answer - too - to be a good one. But one has to decide where to award the bounty. If it was possible to split, I'd done that :( Gotta say I didn't find the time to test your codes myself, so I judged based on code read only – Dynalon Nov 10 '16 at 07:13