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()
andisString()
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!