0

I'm facing a problem with the HttpClient of Angular. I will like to know how to make a request with its replay shared to be repeated. Consider the following example code:

import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';

/** Node */
export class Node {

  /** Attributes */
  attributes$: Observable<any>;

  constructor(private http: HttpClient, private href: string) {
    this.attributes$ = http.get(href).pipe(shareReplay());
  }

  update(patches: any): Observable<any> {
    const req = this.http.patch(this.href, patches);

    // TODO: subscribe to request and update attributes

    return req;
  }
}

What I'm trying to do is to make the attributes$ observable notify a new value after the PATCH request has been sent to the resource.

Notice that the attributes$ observable only perform the first http request if someone subscribe to it. Accessing the attribute doesn't have any effect as it should be. This is an important feature I will want to keep.

Is there any way to do this?

Thanks in advance

2
  • Separate the request and the result observable, like I show in stackoverflow.com/a/41554338/3001761 Commented Jul 4, 2018 at 16:55
  • @jonrsharpe I review the answer you referenced and I think is correct. With the difference that in this case there is no method to call to perform the first retrieval. The first retrieval is performed the first time the attributes$ is acceded. May be you can write an answer here with this details in mind? I could answer it myself with the help you give me with the link but I prefer you to take the credit for it. Tank you Commented Jul 4, 2018 at 17:04

1 Answer 1

0

I end up solving the problem so I will share with you my solution. First start saying that I approach the problem using TDD. So here I will post first the test suite I use

import { HttpClient } from '@angular/common/http';
import {
  HttpClientTestingModule,
  HttpTestingController
} from '@angular/common/http/testing';
import { TestBed, inject } from '@angular/core/testing';
import { bufferCount } from 'rxjs/operators';
import { zip } from 'rxjs';

import { Node } from './node';

const rootHref = '/api/nodes/root';
const rootChildrenHref = '/api/nodes/root/children';

const rootResource = {
  _links: {
    'node-has-children': { href: rootChildrenHref }
  }
};

function expectOneRequest(controller: HttpTestingController, href: string, method: string, body: any) {
  // The following `expectOne()` will match the request's URL and METHOD.
  // If no requests or multiple requests matched that URL
  // `expectOne()` would throw.
  const req = controller.expectOne({
    method,
    url: href
  });

  // Respond with mock data, causing Observable to resolve.
  // Subscribe callback asserts that correct data was returned.
  req.flush(body);
}

describe('CoreModule Node', () => {
  let httpTestingController: HttpTestingController;
  let subject: Node;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ]
    });
  });

  beforeEach(inject([ HttpClient, HttpTestingController ],
    (http: HttpClient, testingController: HttpTestingController) => {
      subject = new Node(http, rootHref);
      httpTestingController = testingController;
    }));

  afterEach(() => {
    // After every test, assert that there are no more pending requests.
    httpTestingController.verify();
  });

  it('should be created', () => {
    expect(subject).toBeTruthy();
  });

  it('#attributes$ is provided', () => {
    expect(subject.attributes$).toBeTruthy();
  });

  it('#attributes$ is observable element', (done: DoneFn) => {
    subject.attributes$.subscribe(root => {
      expect(root).toBeTruthy();
      done();
    });

    expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
  });

  it('#attributes$ observable is cached', (done: DoneFn) => {
    zip(subject.attributes$, subject.attributes$).subscribe(([root1, root2]) => {
      expect(root1).toBe(root2);
      done();
    });

    expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
  });

  it('#update() affect attributes$', (done: DoneFn) => {
    // the subscribe at the end of this pipe will trigger a children request
    // but the update() call will trigger a resource request that in turn
    // will trigger a children request. Therefore bufferCount will produce
    // an array of size two.

    subject.attributes$.pipe(bufferCount(2)).subscribe(collection => {
      expect<number>(collection.length).toEqual(2);
    });

    subject.update([]).subscribe(root => {
      expect(root).toBeTruthy();
      done();
    });

    expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
    expectOneRequest(httpTestingController, rootHref, 'PATCH', {});
    expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
  });

  it('#update() return observable', (done: DoneFn) => {
    subject.update([]).subscribe(root => {
      expect(root).toBeTruthy();
      done();
    });

    expectOneRequest(httpTestingController, rootHref, 'PATCH', {});
    expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
  });
});

As you can see the test suite verify that HTTP request calls are made just if someone subscribe to the observable as intended. With this test suite in place I come up with the following implementation of the Node class:

import { HttpClient } from '@angular/common/http';
import { Observable, Subject } from 'rxjs';
import { mergeMap, shareReplay } from 'rxjs/operators';

/** Node
 *
 * A node is the representation of an artifact on the Sorbotics Platform
 * Registry.
 */
export class Node {

  /** Attributes */
  public attributes$: Observable<any>;

  private attributesSubject$: Subject<any> = new Subject();

  private initialFetch = false;

  constructor(private http: HttpClient, private href: string) {

    // the attributes$ observable is a custom implementation that allow us to
    // perform the http request on the first subscription
    this.attributes$ = new Observable(subscriber => {
      /* the Node resource is fetched if this is the first subscription to the
         observable */
      if (!this.initialFetch) {
        this.initialFetch = true;

        this.fetchResource()
          .subscribe(resource => this.attributesSubject$.next(resource));
      }

      // connect this subscriber to the subject
      this.attributesSubject$
        .subscribe(resource => subscriber.next(resource));
    });
  }

  /* Fetch Node resource on the Platform Registry */
  private fetchResource: () => Observable<any> =
    () => this.http.get(this.href)

  /** Update node
   *
   * This method implement the update of the node attributes. Once the update
   * is performed successfully the attributes$ observable will push new values 
   * to subscribed parties.
   *
   * @param patches Set of patches that describe the update.
   */
  public update(patches: any): Observable<any> {
    const req = this.http.patch(this.href, patches)
      .pipe(mergeMap(() => this.fetchResource()), shareReplay(1));

    req.subscribe(resource => this.attributesSubject$.next(resource));

    return req;
  }
}

As you can se I learn from the answer referenced in the comment made by @jonrsharpe but introduce the use of a custom subscription handling at the observable. This way I can delay HTTP request until the first subscription is made.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.