0

I found few questions with same title and, as far as I could see, some of them are suggesting that the solution is basically return an Observable instead of an array (others are about FireBase which isn't my case). Well, as far I am concern, the code below does return an Observable (look at "getServerSentEvent(): Observable {return Observable.create ...")

My final goal is get all events from a stream returned from a Rest WebFlux. I didn't past bellow the backend because I am pretty sure the issue is related to some mistake in Angular.

On top of that, I can debug and see the events properlly comming to extratos$ from app.component.ts(see image bellow).

Whole logs

core.js:6185 ERROR Error: InvalidPipeArgument: '[object Object]' for pipe 'AsyncPipe'
    at invalidPipeArgumentError (common.js:5743)
    at AsyncPipe._selectStrategy (common.js:5920)
    at AsyncPipe._subscribe (common.js:5901)
    at AsyncPipe.transform (common.js:5879)
    at Module.ɵɵpipeBind1 (core.js:36653)
    at AppComponent_Template (app.component.html:8)
    at executeTemplate (core.js:11949)
    at refreshView (core.js:11796)
    at refreshComponent (core.js:13229)
    at refreshChildComponents (core.js:11527)

app.component.ts

import { Component, OnInit } from '@angular/core';
import { AppService } from './app.service';
import { SseService } from './sse.service';
import { Extrato } from './extrato';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  providers: [SseService],
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  //extratos: any;
  extratos$ : Observable<any>;

  constructor(private appService: AppService, private sseService: SseService) { }

  ngOnInit() {
    this.getExtratoStream();
  }

  getExtratoStream(): void {
    this.sseService
      .getServerSentEvent("http://localhost:8080/extrato")
      .subscribe(
        data => {
          this.extratos$ = data;
        }
      );
  }
}

sse.service.ts

import { Injectable, NgZone } from '@angular/core';
import { Observable } from 'rxjs';
import { Extrato } from './extrato';

@Injectable({
  providedIn: "root"
})
export class SseService {
  extratos: Extrato[] = [];
  constructor(private _zone: NgZone) { }

  //getServerSentEvent(url: string): Observable<Array<Extrato>> {
  getServerSentEvent(url: string): Observable<any> {
    return Observable.create(observer => {
      const eventSource = this.getEventSource(url);
      eventSource.onmessage = event => {
        this._zone.run(() => {
          let json = JSON.parse(event.data);
          this.extratos.push(new Extrato(json['id'], json['descricao'], json['valor']));
          observer.next(this.extratos);
        });
      };
      eventSource.onerror = (error) => {
        if (eventSource.readyState === 0) {
          console.log('The stream has been closed by the server.');
          eventSource.close();
          observer.complete();
        } else {
          observer.error('EventSource error: ' + error);
        }
      }

    });
  }
  private getEventSource(url: string): EventSource {
    return new EventSource(url);
  }
}

app.component.html

<h1>Extrato Stream</h1>
<div *ngFor="let ext of extratos$ | async">
  <div>{{ext.descricao}}</div>
</div>

Evidence that the observable extratos$ is filled in

enter image description here

Paul R
  • 208,748
  • 37
  • 389
  • 560
Jim C
  • 3,957
  • 25
  • 85
  • 162
  • 1
    `extratos$` is declared as `Array` and used as `Observable`? – Guerric P Mar 23 '20 at 22:32
  • as far as I see, I declare it as Observable in app.component.ts (extratos$ : Observable;). Do you mean "extratos: Extrato[] = [];" from sse.services.ts? I guess it isn't the issue. Am I wrong? – Jim C Mar 23 '20 at 22:38

1 Answers1

1

When you write this observer.next(this.extratos);, that means this.extratos is what you get on component side in the data argument of the callback, so when you do this this.extratos$ = data; you are actually storing the extratos Array. TypeScript doesn't complain about it, probably because it's not smart enough to infer the types when you build an Observable from scratch like you did.

Try this:

this.extratos$ = this.sseService
      .getServerSentEvent("http://localhost:8080/extrato");

and in the template: <div *ngFor="let ext of extratos$ | async">

Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • Now it is working but please let me double check: are sure I am still coding as it is supposed when working with Async approach? I have read numerous blogs and articles all mentioning to use "| async". Do you see some other weird code above that I should do different in order to properly set async on ngfor or, maybe, I missed some point that "async" isn't necessary at all? – Jim C Mar 23 '20 at 22:51
  • 1
    Forget my previous answer I just understood that you're using SSE, which makes your Observable a hot Observable and this is indeed a use case for the `AsyncPipe`. I've edited my answer – Guerric P Mar 23 '20 at 23:06
  • It is working what it is good. But, regard my knowledge it reveals some gap: how can your suggestion missing "subscribe" works? In order to get the result from an observable it is a MUST to subscribe, right? – Jim C Mar 23 '20 at 23:22
  • 1
    Yes but the `AsyncPipe` does it for you, which is good because it also unsubscribes for you, which you can forget as a developer when you subscribe yourself, and this can cause memory leaks – Guerric P Mar 23 '20 at 23:23
  • You certaling answered my question. A last comment if you don't mind: "| async" means "subscribe, only start to listen when there is the first event and listen until it is closed" and "$" at the end of extratos is just a conventional way to say "this is an observable", i.e., it doesn't add nothing else then a friendly conventional name, right? – Jim C Mar 23 '20 at 23:28
  • 1
    Exactly, it also means unsubscribe when the component is destroyed – Guerric P Mar 23 '20 at 23:41
  • 1
    And yes the $ is just a convention – Guerric P Mar 23 '20 at 23:41