2

Background

I am currently making a find function for a messaging app, where each collection of messages is referenced by an index. When find() is invoked, it has to extract each message collection reference from the messageRef variable, then use getMessageCollectionFromRef(id) to get the Observable for the message collection. Then, it must grab the user id from this Observable's subscribed JSON data of the person who is not the user, and then invoke getUserById(id) to get the Observable for the user info. Lastly, it must compare the name of the retrieved user to the value of the ngModel search. If search is a substring of userJSONData, then the messageRef of the message collection must be put into the messages Observable, from which async data is loaded on the Angular front-end.

Note: Function cancelFind() is called at the beginning of the find() function only to revert the message Observable list to its original length from AngularFire2 data. This works well.

Attempt

find():void {
        this.cancelFind();
        console.log("- find Started -");
        var newMessages = new Array();
        this.messages = this.messages.switchMap((messages) => {
              console.log("Got messages: " + JSON.stringify(messages));
              messages.forEach((msg) => {
                    console.log("Iterating through message #" + msg.$value);
                    this.msgClass.getMessageCollectionFromRef(msg.$value).take(1).subscribe((message) => {
                          var id = (message.messager1.id != this.user.id ? message.messager1.id : message.messager2.id)
                          this.userClass. getUserById(id).take(1).subscribe((messager) => {
                                if (messager.name.includes(this.search)) {
                                      console.log("Pushing " + JSON.stringify(msg) + " to list");
                                      newMessages.push(msg);
                                }
                          });
                    });
              });
              console.log("Found list: " + JSON.stringify(newMessages));
              return Observable.forkJoin(newMessages);
        });
        console.log("- find Resolved -");
  }

Output

[08:41:59]  console.log: - find Started - 
[08:41:59]  console.log: Got messages: [{"$value":0},{"$value":1}] 
[08:41:59]  console.log: - find Resolved - 
[08:41:59]  console.log: Iterating through message #0
[08:41:59]  console.log: Iterating through message #1 
[08:41:59]  console.log: Found list: []
[08:41:59]  console.log: Pushing {"$value":0} to list 

Side note: Yes, I know my - find Started - and - find Resolved - logs follow synchronous conventions. This is standard in the app I'm developing and would not like to stray away from this.

Error

I am not getting an error, but my issue is that the messages Observable remains empty and, consequently, no message collections appear on screen.

This problem is very unique in that it concerns retrieving nested Observable values in a loop, and I have not found adequate solutions to this issue online. The only similar solutions, such as that to a question I asked months ago found here, concern creating an Observable array off of synchronously-retrieved data.

I apologize for the adequately large description in relation to the length of code, but I believe there is an easy fix for this that I was not able to pinpoint for days. I am still new to Observables, so I will appreciate any help you can provide. Thank you so much!

CozyAzure's Solution

Following CozyAzure's solution, I have been able to produce a newly searched Observable array with properly added values. However, instead of omitting non-matches (null values), the filter command does not do anything to the result Observable array. Here is my resulting error: Error

Workaround

Check for null in your front-end as the async pipe loads messages.

<div *ngFor="let msg of messages | async">
      <message-card *ngIf="msg" [ref]="msg" ></message-card>
</div>
Anthony Krivonos
  • 4,596
  • 4
  • 16
  • 31
  • 1
    `getMessageCollectionFromRef()` can be called in parallel right? – CozyAzure Jul 12 '17 at 12:33
  • Nope, you need the Message Collection to get the ID of the user whom you are messaging. – Anthony Krivonos Jul 12 '17 at 12:34
  • but, each subsequent calls has no dependency to each other right? You can fire them in parallel, and then compare the results later, no? – CozyAzure Jul 12 '17 at 12:35
  • I agree with @CozyAzure, put a `console.log` before your return statement, you'll probably see that it is shown before **Pushing ... to list** – Serge K. Jul 12 '17 at 12:36
  • @CozyAzure Which calls do you mean? Calls to nested functions or each call to get a message collection? – Anthony Krivonos Jul 12 '17 at 12:38
  • @Nathan P. Good idea, I'll try this and update my question. – Anthony Krivonos Jul 12 '17 at 12:38
  • Done. Thanks for the advice! The result was pretty obvious, however, so my question stands as how it would be possible to return this array filled with values as an Observable array. – Anthony Krivonos Jul 12 '17 at 12:44
  • Possible duplicate of [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Serge K. Jul 12 '17 at 13:05
  • I checked that question yesterday and it doesn't really help fix my issue. My problem concerns retrieving nested Observable values in a loop, and I have not found adequate solutions to this issue online. – Anthony Krivonos Jul 12 '17 at 13:21
  • The problem is that you're calling `forkJoin` _before_ `newMessages` is populated, and you'll find all the answers in the question I linked. Using promises will probably help you. – Serge K. Jul 12 '17 at 13:28
  • True, but my issue will then be determining when the newMessages scalar array is completely populated with message collection references so I can then use a callback to join the array into the messages Observable. – Anthony Krivonos Jul 12 '17 at 13:30

1 Answers1

1

I really think you got it mostly correct, other than changing some of the subscribe to .switchMap and .map.

I maybe understanding your requirement wrongly though. I am assuming you want an Observable that contains all the results after calling this.msgClass.getMessageCollectionFromRef(), and then just subscribed to it so that you get all the values.

Here's how you can do it:

find(): Observable<any> {
    return this.message
        .switchMap(messages => {
            let arrayOfObservables = messages.map(msg => {
                return this.msgClass.getMessageCollectionFromRef(msg.$value)
                    .take(1)
                    .switchMap(message => {
                        var id = (message.messager1.id != this.user.id ? message.messager1.id : message.messager2.id)
                        return this.userClass.getUserById(id)
                            .take(1)
                            .map(messager => {
                                return messager.name.includes(this.search) ? messager : null;
                            })
                    })
            });
            return Observable
                .forkJoin(...arrayOfObservables) 
                 //filter out the null values
                .filter(x => x !== null);
        })
}

Walkthrough:

  1. Get all the messages from this.messages.

  2. For each of these messages:

    2.1. call msgClass.getMessageCollectionFromRef()

    2.2. take the first value returned, and then

    • call getUserById with the id
    • map the Observable. If the response is the same as input, return the response, else return Observable.empty
  3. Combine all the Observables in point 2 using Observable.forkJoin()

  4. Filter out the values which are null

Some notes:

  1. In your question, your method .find() explicitly stating return of void, but you are actually returning an Observable. You might want to fix that.
  2. At step 3 and 4, Use rest parameters ... to flatten the arrays to be passed into .forkJoin as arguments
CozyAzure
  • 8,280
  • 7
  • 34
  • 52
  • Great idea, but switchMap would need a default value to return if a `messager` is not found. This is one of the issues I came across before. Is there a way to wait for the map to complete before returning a value in the switchMap method? Thank you! https://www.learnrxjs.io/operators/transformation/switchmap.html – Anthony Krivonos Jul 12 '17 at 14:24
  • 1
    @AnthonyKrivonos you are right. Just need to add the return statement. see my edit – CozyAzure Jul 12 '17 at 14:35
  • Awesome! I'm almost there. Observable.empty() returns a `false` value for the `_isScalar` key, which is a tad hard to parse out of the Observable array. Do you know of a way to filter out null values in the returned Observable? I went on to change one of the last lines to `return twinee.name.includes(this.search) ? tw : null;` because it's easier to check for null. – Anthony Krivonos Jul 12 '17 at 15:54
  • @AnthonyKrivonos yes, simply use `Observable.filter()` . See my edit – CozyAzure Jul 12 '17 at 16:22
  • For some reason, the Observable array still returns with null values. Check my edit! – Anthony Krivonos Jul 12 '17 at 16:55
  • Solved it in the front-end! Put my workaround in an edit. – Anthony Krivonos Jul 12 '17 at 17:03