0

I have a menu in angular app and this menu controls the selected client in my web application.

As it is :

  • My menu is a component which shares the selected client through a service.
  • The selected client is shared as an Observable.
  • The observable never complete. It just emit the new selected client when the menu is clicked.
  • This is a replay subject. Each new subscriber retrieve the last emitted client.

it seemed like a good design but I encountered some problem when making new observables based on the client one (never completing observables). AFAIK this is due that the first observable never completesand this property will propagate.

//Command
export interface Command {
    productName: string;
    qty: number;
}

//The service
@Injectable()
export class ClientService {
private _client: ReplaySubject<Client> = new ReplaySubject(1);

    setClient(client: Client) { // The menu component calls setClient on a user click
        this._client.next(client);
    }
    getClient(): Observable<Client> { // getClient is heavilly called in child component to observe client selection events.
        return this._client.asObservable();
    }
}


getCommands(): Observable<Command> { //Used in a template with async pipe

    //In a child component
    const commandsObs = this.clientService.getClient()
    //.take(1) //I am forced to use take(1) to get getCommands() observer to complete
    .flatMap(c => {
        return Observable.merge(getCommandsPromise1(c), getCommandsPromise2(c));
    })
    .reduce((acc, next) => Object.assign({}, acc, next))
    .map(next => {
        return finishMakingCommand(next));
    })
    .catch(err => /* Some error management code*/)
}

getCommandsPromise1(client: Client): Promise<any> {
    //REST call returning a promise
    return Promise.resolve({ 'command1': { 'productName': 'toy', qty: 1 } });
}
getCommandsPromise2(client: Client): Promise<any> {
    //REST call returning a promise
    return Promise.resolve({ 'command2': { 'productName': 'another toy', qty: 1 } });
}

finishMakingCommand(commands: any): Command[] {
    // Flattens 'command1' and 'command2' to a list of commands
    return [{'productName': 'toy', qty: 1}, {'productName': 'another toy', qty: 2}];
}

I'd like to know if more experienced developers think a never ending observable is a good design and what are the alternatives to avoid never ending observables.

R. G
  • 257
  • 2
  • 14
  • You should think of observables as a pipline. It's only a never-ending stream if the source is something like `Observable.interval()` that generates continuously, so you'd have to go mad with the menu clicks! But seriously, you should always unsubscribe your subscriptions - see here [Angular/RxJs When should I unsubscribe from `Subscription`](https://stackoverflow.com/questions/38008334/angular-rxjs-when-should-i-unsubscribe-from-subscription) – Richard Matsen Oct 30 '17 at 10:30
  • Actually I'm currently using the async pipe for all the child of the 'client' observable. If I'm right the async pipe unsubscribe automatically. The client observable is a ReplaySubject instantiated once in the service and exposed with 'asObservable' with a getter. – R. G Oct 30 '17 at 10:35
  • You are totally right, my friend. – Richard Matsen Oct 30 '17 at 10:37
  • I don't see any problem with never-ending observables. There only is a problem if the Observable is kept alive while it should not emit any new value. – n00dl3 Oct 30 '17 at 10:41
  • There may be something in the combination of operators and/or the replaySubject. Are you saying that **one** click keeps repeating? – Richard Matsen Oct 30 '17 at 10:45
  • I updated the post to add an example. I'm saying that one click triggers the retrieval of command. That's totally right. However, the observable retrieved through getCommands() never complete. This is a tricky behavior because another developer would expect getCommands to complete at some time. – R. G Oct 30 '17 at 10:49

2 Answers2

0

you can use unsubscribe function to stop this: for example: mySubscribtion: Subscription;

 this.mySubscription = somthing.subscribe((...) => {...})

and then you can unsubscibe on event or onDestroy that way:

this.mySubscribtion.unsubscribe();
Fateh Mohamed
  • 20,445
  • 5
  • 43
  • 52
0

As I mentioned above, think about observables as laying a pipeline. The only time there's a problem is when the water keeps coming (as with Observable.interval(1000) source - it's going to keep ticking). In that situation, if you manually subscribe to the observable, you also need to unsubscribe.

But, as you said, you are using the async pipe and Angular takes care of the unsubscribing there.

With you menu clicks, one click will send one value. It's quite common for an observable never to complete (i.e never receive the completed event), it's not a requirement.

The completed event is generally useful for aggregating operators such as toArray() so that they know the whole set of values.

I would suggest just using

const commandsObs = this.clientService.getClient();

and in the template (example)

<div>{{ commandObs | async }}</div>

Forget about the flatMap, unless it's intending to do something fancy - let me know.

Edit - suggested changes to new sample code

You can try moving the reduce and map inside the flatmap, as they are intended to process the results of the inner observable (Observable.merge).

const commandsObs = this.clientService.getClient()
  .flatMap(c => {
    return Observable.merge(getCommandsPromise1(c), getCommandsPromise2(c))
      .reduce((acc, next) => Object.assign({}, acc, next))
      .map(next => finishMakingCommand(next) )
  });

An alternative version to try,

const commandsObs = this.clientService.getClient()
  .map(c => Observable.forkJoin(getCommandsPromise1(c), getCommandsPromise2(c) );
Richard Matsen
  • 20,671
  • 3
  • 43
  • 77
  • The flatMap is used for illustration purposes. In reality I need to get several REST endpoint called to get commands associated to a client, I also use reduce to construct the real Command structure. I updated the original post accordingly – R. G Oct 30 '17 at 12:26
  • Please also provide info on the other pieces - Command class, getCommandsPromise1, getCommandsPromise2 and finishMakingCommand. I can see `c` parameter is an emitted value from the service, but can't see how it's used. – Richard Matsen Oct 30 '17 at 12:41
  • Updated with example on what would getCommandsPromise1|2 and finishMakingCommand would do – R. G Oct 30 '17 at 13:12
  • Ok thanks. That code looks functional. Is there any further problem? – Richard Matsen Oct 30 '17 at 13:39
  • Actually, since the observable (just after the flatMap) never complete reduce never get to emit any value. I spent quite alot of time to understand what was going on. I just wanted insight on what were the best practice to avoid tricky situation like this one. – R. G Oct 30 '17 at 13:55