A while ago, our team was asked to write a small application for nurses to register some of the tasks they perform when entering a patient’s room. The application was very straightforward, the nurses had to select the room and tap a few buttons describing their activities and submit. We were very proud that we had provided an application that could record the activities in just a few clicks. The application’s main goal was to gather data on the nurse’s work and see where training or optimizations were required.
A first test run immediately showed us a shortcoming in the application, something we hadn’t considered. In large buildings, such as hospitals and nursing homes, you don’t always have good internet coverage, so we risk losing valuable data. We needed a solution, or our app would be useless.
Solution
keep a local copy of the submissions and sync them when there is a good internet connection.
The solution consists of 2 steps, handle the error when wrong connected and process the requests when back online.
Schematic representaion
step 1
step 2
The fun part lets code
first we need a fuction to handle our requests, and our errors I’ve put thos functions in a seperate service
import {Injectable} from "@angular/core";
import {HttpClient, HttpErrorResponse, HttpParams} from "@angular/common/http";
import {concat, EMPTY, Observable, throwError, TimeoutError} from "rxjs";
import {catchError, retry, timeout} from "rxjs/operators";
import appPackage from '../../package.json'
export class SyncTask<T> {
constructor(
public url: string,
public body: T,
public params?: string
) {}
}
@Injectable({providedIn: "root"})
export class SyncService {
private _HTTP_TIMEOUT = 5000;
private _RETRY_TIMES = 2;
private _STORAGE_KEY = `${appPackage.name}-syncTasks`;
constructor(private http: HttpClient) {
}
tryPostRequest<T>(url: string, payload: T, params: HttpParams): Observable<T> {
return this.http.post<T>(url, payload, {params}).pipe(
timeout(this._HTTP_TIMEOUT),
retry(this._RETRY_TIMES),
catchError((err: HttpErrorResponse) => this.handleError<T>(err, url, payload, params)),
);
}
private handleError<T>(
err: HttpErrorResponse,
url: string,
payload: T,
params: HttpParams
): Observable<any> {
if (SyncService.isBadOrNotConnected(err)) {
this.addOrUpdateSyncTask<T>(url, payload, params);
return EMPTY
} else {
return throwError(err);
}
}
}
allot already happens in the code above,
When a call is made, we give this function the post URL, the body, and, if needed, the queryParams.
The function will perform the call, and we give it a timeout; this means it will wait, in our case, 5000ms and retry the call two times, so only after 15 seconds do we go into the catch error function.
In the catchError we first check if the error is one we want to catch, if not we just throw the error.
private static isBadOrNotConnected(err: HttpErrorResponse): boolean {
return ( err instanceof TimeoutError || err.error instanceof ErrorEvent || !window.navigator.onLine ) }
In this function we check if it is an error we want to handle also if the user is still online.
private addOrUpdateSyncTask<T>(url: string, payload: T, params: HttpParams): void {
const syncTasks = this.getExistingSyncTasks();
syncTasks.push(new SyncTask(url, payload, params?.toString()));
localStorage.setItem(this._STORAGE_KEY, JSON.stringify(syncTasks));
}
in the local storage we might have already have tasks waiting so we add our task to the list.
private getExistingSyncTasks(): SyncTask<any>[] {
const tasksFormStorage = localStorage.getItem(this._STORAGE_KEY);
return tasksFormStorage ? JSON.parse(tasksFormStorage) : [];
}
Save when back online
when we come back online we need to process the entries we stored our localStorage
sync(): Observable<any> {
const syncTasks = this.getExistingSyncTasks();
localStorage.removeItem(this._STORAGE_KEY);
return concat(...syncTasks.map((task: SyncTask<any>) => this.tryPostRequest(task.url, task.body, task.params ? JSON.parse(task.params) : null)))
}
this function takes all the entries from the local storage and processes them one by one if one fails it is send back.
our complete service looks like this:
import {Injectable} from "@angular/core";
import {HttpClient, HttpErrorResponse, HttpParams} from "@angular/common/http";
import {concat, EMPTY, Observable, throwError, TimeoutError} from "rxjs";
import {catchError, retry, timeout} from "rxjs/operators";
import appPackage from '../../package.json'
export class SyncTask<T> {
constructor(
public url: string,
public body: T,
public params?: string
) {
}
}
@Injectable({providedIn: "root"})
export class SyncService {
private _HTTP_TIMEOUT = 5000;
private _RETRY_TIMES = 2;
private _STORAGE_KEY = `${appPackage.name}-syncTasks`;
constructor(private http: HttpClient) {
}
tryPostRequest<T>(url: string, payload: T, params: HttpParams): Observable<T> {
return this.http.post<T>(url, payload, {params}).pipe(
timeout(this._HTTP_TIMEOUT),
retry(this._RETRY_TIMES),
catchError((err: HttpErrorResponse) => this.handleError<T>(err, url, payload, params)),
);
}
sync(): Observable<any> {
const syncTasks = this.getExistingSyncTasks();
localStorage.removeItem(this._STORAGE_KEY);
return concat(...syncTasks.map((task: SyncTask<any>) => this.tryPostRequest(task.url, task.body, task.params ? JSON.parse(task.params) : null)))
}
private handleError<T>(
err: HttpErrorResponse,
url: string,
payload: T,
params: HttpParams
): Observable<any> {
if (SyncService.isBadOrNotConnected(err)) {
this.addOrUpdateSyncTask<T>(url, payload, params);
return EMPTY
} else {
return throwError(err);
}
}
private static isBadOrNotConnected(err: HttpErrorResponse): boolean {
return (
err instanceof TimeoutError || err.error instanceof ErrorEvent || !window.navigator.onLine
)
}
private addOrUpdateSyncTask<T>(url: string, payload: T, params: HttpParams): void {
const syncTasks = this.getExistingSyncTasks();
syncTasks.push(new SyncTask(url, payload, params?.toString()));
localStorage.setItem(this._STORAGE_KEY, JSON.stringify(syncTasks));
}
private getExistingSyncTasks(): SyncTask<any>[] {
const tasksFormStorage = localStorage.getItem(this._STORAGE_KEY);
return tasksFormStorage ? JSON.parse(tasksFormStorage) : [];
}
}
Ok we have all elements in place now, so in every service we can call the tyryPostRequest function
sync when online
RxJs has a neat function that is able to listen to browser events, we make use of this and when the browser tells us we are back online we strat our sync tast
fromEvent(window, 'online').pipe(
startWith(<string>null),
switchMap(_ => this.syncService.sync())
).subscribe()