August 12, 2021

Mixing synchronous code with asynchronous operations

One way to introduce asynchronous operations into your application is the Observer pattern.

The Observer we’re using is a synchronous implementation and available from sendanor/typescript repository. It’s pure TypeScript and once compiled, works in any JavaScript environment.

First, we define FooEvent which contains our event names:

export enum FooEvent {
  UPDATED = 'foo:updated',
  ERROR   = 'foo:error'
}

Then we define an interface FooDTO which expresses how our data transfer object from the backend should look like:

export interface FooDTO {
  readonly name    : string;
  readonly summary : string;
}

We’ll also need a runtime test to make sure TypeScript’s compile time analysis will match the runtime once we get the DTO from the backend.

Let’s create a runtime test:

function isFooDTO (value : any) : value is FooDTO {
  return (
    isObject(value) 
    && isString(value?.name) 
    && isString(value?.summary)
  );
}

You can get the isObject() and isString() functions from the Lodash library or use our TypeScript optimized test functions written on top of the Lodash and available from sendanor/typescript/modules/lodash.

We’ll start by creating a simple FooService class:

export class FooService {

  private _data     : FooDTO | undefined;
  private _observer : Observer<FooEvent>;

  public constructor () {
    this._data     = undefined;
    this._observer = new Observer<FooEvent>("FooService");
  }

  public getData (): FooDTO | undefined {}

  public refreshData () : void {}

  public on (
      name: FooEvent, 
      callback: ObserverCallback<FooEvent>
  ): ObserverDestructor {}
  
}

The foo.getData() can be called from everywhere to get the latest DTO from the backend.

It will return undefined if no data has been received.

public getData (): FooDTO | undefined {
  return this._data;
}

Our foo.refreshData() will encapsulate the asynchronous HTTP request and convert it to our Observer Event pattern:

public refreshData () : void {

  HttpService.get('/path/to').then((response) => {
    
    if (!isFooDTO(response)) {
      throw new TypeError('Not FooDTO: ' + JSON.stringify(response));
    }

    this._data = response.data;

    this._observer.triggerEvent(FooEvent.CHANGED);
    
  }).catch(err => {
    this._observer.triggerEvent(FooEvent.ERROR, err);
  });

}

The foo.on(FooEvent, callback) can be used to listen our events from outside our foo instances:

public on (name: FooEvent, callback: ObserverCallback<FooEvent>): ObserverDestructor {
  return this._observer.listenEvent(name, callback);
}

The return value will be a destructor function that can be called to unbind the listener.

Here’s the full class:

export class FooService {

  private _data     : FooDTO | undefined;
  private _observer : Observer<FooEvent>;
  
  public constructor () {
    this._data     = undefined;
    this._observer = new Observer<FooEvent>("FooService");
  }

  public getData (): FooDTO | undefined {
    return this._data;
  }

  public refreshData () : void {

    HttpService.get('/path/to').then((response) => {
      
      if (!isFooDTO(response)) {
        throw new TypeError('Not FooDTO: ' + JSON.stringify(response));
      }

      this._data = response.data;

      this._observer.triggerEvent(FooEvent.CHANGED);
      
    }).catch(err => {
      this._observer.triggerEvent(FooEvent.ERROR, err);
    });

  }
  
  public on (
      name: FooEvent, 
      callback: ObserverCallback<FooEvent>
  ): ObserverDestructor {
    return this._observer.listenEvent(name, callback);
  }

}

Now, we can create foo instances:

const fooService = new Foo();

Once created, we can start listening events we’re interested of:

const changeListener = fooService.on(FooEvent.CHANGED, () => {

  const currentData = fooService.getData();

  // ...update view

});

For example, here you could test if the data is undefined and create a loader spinner in the UI, and otherwise update the view.

To catch errors, we can listen FooEvent.ERROR event:

const errorListener = fooService.on(FooEvent.ERROR, (ev : FooEvent, error : any) => {
  console.error(`Error: `, err);
});

In order to start the asynchronously HTTP request to fetch our data model, you may call foo.refreshData():

fooService.refreshData();

Once there no longer has a need to listen events, just call destructors:

changeListener();
errorListener();

In a real application, you probably would create a UI component or an intermediate service class to merge these operations as a reusable single .start() and .stop() interface.

That’s all. I hope this was useful!


Comments

Jack hello world!

Jaakko Just testing

Jaakko testing again

Jaakko Testing #3

Jaakko Hello world!