Ionic 2 - best way to use observables and local storage? Couchdb with custom server/db?


#1

Hey everyone, I’ve been doing some research for the past couple weeks on the best ways to handle an “offline” mode for an ionic 2 app. So far I’ve read that Pouchdb is the ideal thing to use…but it only seems to work with cloudant?

My app talks to a non cloudant server API and I’m just not really sure if I can use Pouchdb for all my local storage needs that then can be configured to sync with a custom server?

In my ionic 1 apps, I just used local storage and wrote a custom diff checker on objects and updated objects in local storage or on the server, based on what was fresher…except the data models weren’t that complex. In this new app, I have many different types of data that can be fetched and updated and I’m looking for something that is a relatively straightforward way to manage local storage items with observables…AND have the functionality to sync with a remote server that isn’t cloudant.

Any thoughts?


#2

if you have an own server with root access you can install couchdb there. it is “only” a database system.

Localstorage is not the right place for persistent data, because it can be cleared very easy.

So i am using the sqlite plugin because i did not had the opportunity to choose the databasesystem.
If you do not need features like search through db fields/keys --> i have 3 columns: url (identifier because i store results of an endpoint-request), value (string representation of the request payload), date or hash or whatever.

So if you need an offline handling you can simple implement a switch in your http-services. instead of sending a request you simply search for the url in the database and get the latest values.

So you can build up a simple sync system --> For each Get-endpoint you can provide a put or patch endpoint for syncing.

If you need a search in the db --> create complex sql tables like your “sql”-tables are build on backend side.
The easy part --> if you need to patch or change tables afterwards you can reuse the patch-code from the serverside.

So you can create an endpoint “Look for database-structural changes” (after a restart or app update) then execute simply the result from and your tables are up to date. After that you can start syncing and everything is fine.

It depends how many data you need to store.


#3

Pouchdb’s sync is great, when you can connect to a backend couchdb (you don’t have to use cloudant). I didn’t have that option either. I was inspired from this:

but ended up creating a couple of observers to track changes to the database, and then (based on business logic), sync stuff to the server. I might have taken observables a bit too far, but everything can be a stream. (i.e. in the example below, user login and network are observables on top of enums that control when to sync).

RepositoryWrapper.ts:

constructor() {
  //observable that is notified of new records when connected and authenticated
  this.pendingSync$ = new Observable(observer =>
    this._pendingSyncObserver = observer).share();

  //observable of all pending records for user
  this.recordStoreObserver = new BehaviorSubject(undefined);

  this.db = new pouch(RECORD_DB_NAME);
  }
  ...
  private notifyObservers = () => {
     this.recordStoreObserver.next(this.pending);
  }

  //Helper to update array when a record is changed/inserted
  private onUpdatedOrInserted = (newDoc) => {
  const index = this.search(this.pending, newDoc._id);
  const doc = this.pending[index];

  if (doc && doc._id === newDoc._id) { // update
    this.pending[index] = newDoc;
  } else { // insert
    this.pending.unshift(newDoc);
  }
  if ((RecordHelper.readyToSubmit(newDoc) && this._pendingSyncObserver)) {
    this._pendingSyncObserver.next(newDoc);
  }
  if (RecordHelper.isFinalState(newDoc)) {
    setTimeout(() => {
      this.delete(newDoc);
    }, RecordHelper.getPeriodForDoc(newDoc));
  }
  this.notifyObservers();
}

SyncService:

 constructor(private networkMonitor: NetworkMonitor, private userData: UserData,
  private repo: RecordLocalRepository) {

 // monitors network connectivity
 let networkSubject = this.networkMonitor.subject
   .distinctUntilChanged()
   .map((state) => { return state == NetworkStates.CONNECTED; });

 // monitors authentication
 let loginSubject = this.userData.subject
   .distinctUntilChanged()
   .map((state) => { return state == LoginStates.LOGGED_IN; });

 // combines login && network into new observable.  controls when we send
 // records
 this.readyForWork$ = Observable.combineLatest(
   loginSubject,
   networkSubject,
   function(s1, s2) { return s1 && s2; }
 )
   .distinctUntilChanged()
   .do((res) => console.log("readyforwork:::" + res))
   .subscribe((x) => { this.startWork(x); }, (error) => {
     console.log("observable error " + error);
   });

 }

  // if ready, send changes when they happen
  // else unsubscribe from the observer
 private startWork = (ready: boolean) => {
   if (ready) {
   this.workWatcher = this.repo.pendingSync$
     //!TODO test this -- should buffer any records into 10 second chunks
     .bufferTime(10000)
     .filter(list => list !== undefined)
     .filter(list => list.length > 0)
     .subscribe((res) => this.doSync(res), (error) => {
       console.log("observable error " + error);
     });
   this.doSync();
 } else if (this.workWatcher) {
   this.workWatcher.unsubscribe();
 }
 }
 //Sends all ready docs to repo API
  private findWork = (items?) => {
    if (items) {
      if (items.length) {
        return Promise.resolve(items);
      } else {
        return Promise.resolve(new Array(items));
     }
   } else {
   return this.repo.fetchDocs((doc, emit) => {
     if (RecordHelper.readyToSubmit(doc)) {
       emit(doc);
     }
   });
   }
  }
  private doSync = (items?) => {
 this.findWork(items)
   .then((records) => {

     if (records.length > 0) {
     /* 
      removed but something like this
    */
     const requestoptions = new RequestOptions({
      method: RequestMethod.Post,
      url: url,
      headers: headers,
      body: JSON.stringify(records)
     });
    return this.http.request(new Request(requestoptions))
      .timeout(10000, new Error("Unable to Connect"))
      .map((res) => res.json());
     }
   });
 }

#4

Hi @kstile, do you have an example app on git?


#5

Unfortunately I don’t have a fuller example of this approach to share.

I think observables + a central store (i.e. in the code above Pouchdb) make for a compelling architecture. You get a single source of truth that can be subscribed to in view components.

If you squint hard enough you might see the Redux pattern at work. And if you can make that leap, there are a lot of patterns (in both the react, and angular 2 communities) and more importantly dev tools that make life easier.

A variation of the snippet above is in prod and works great, however I ended up with a lot of plumbing code to subscribe and manage the side effects of syncing to the server. If I were to start over I’d look at ng2-redux or ngrx

It doesn’t implement offline syncing, but https://github.com/lathonez/clicker demonstrates ngrx with Ionic.