Create onboarding and its guard

So If I read the docs to Ionic Storage, I think this is a little ‘overkill’ for my simple Application:

Created for teams building complex, data-driven apps, apps managing sensitive data, …

And in this discussion we are at my main problem again ^^ I always have the problem that some people say I need to that and others say I should do that. For example, some say, I should use Promises whereever I can, others say, I should use Oservebales whenever possible (and then avoid Promises/Observables).
And here’s the same, I saw some tutorails using the system I have and now you tell me that it is crab… And I as a beginner sit here and can flip a coin who is right…

I think you’re referring to something called “Ionic Offline Storage”, with which I have no experience. I’m talking about this. It can use SQLite on device if you wish, or it can just be a frontend for IndexedDB, which works even in ordinary browser environments, making it super-convenient for development.

You have to decide a lot of things for yourself, and one of them is who to pay attention to. Personally, I treat official docs like those from Ionic, Angular, and MDN as the gold standard. There are bloggers (like Ben Lesh, for example) that I trust.

Basically, anything on the internet that tries to sell me “courses” I immediately blacklist. After a while, you develop your own eye for code quality.

I say that Promises are OK for things that are one-shot events, but generally I don’t deploy them in new code unless I’m dealing with an API that provides them. Converting a Promise to an Observable is really easy with the RxJS from operator. Anybody saying you should avoid Observables in a web app (especially an Angular one, where they’re woven deeply into the framework itself) is somebody I would ignore.

The only “tutorials” I would ever pay any attention to when I’m first starting out with a library are the official ones, like the Tour of Heroes. The variation in quality of J. Internet Rando “tutorials” is so great, and my ability to judge what is idiomatic and what isn’t for a framework or library is underdeveloped at that point, so I stick to getting a grounding with official docs.

2 Likes

I’m talking about this

Okay this seems to be quit easy in usage and does the same as LocalStorage. But still I have a question: Why should a custom StorageEngine be netter than the one provided by Capacitor, which is used to convert web apps into mobile apps?

To me the question would rather be “why did the Capacitor folks make yet another storage system that is harder to use than Ionic Storage, which predates the entire Capacitor project by years?”, and I don’t have a good answer for that.

I just implemented the IonicStorage. could you just have a quick look on my updated storage.service if everything in there is okay?

import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage'

@Injectable({
  providedIn: 'root'
})
export class StorageService {

  constructor(private storage: Storage) { }

  store(key: string, value: any) {
    this.storage.ready().then( () => {
      this.storage.set(key, value);
    });
  }

  get(key: string) {
    this.storage.ready().then(() => {
      this.storage.get(key).then(res => {
        if(!res) {
          return false;
        } else {
          return res;
        }
      })
    });
  }

  remove(key: string) {
    this.storage.ready().then(() => {
      this.storage.remove(key);
    });
  }
  
  clear() {
    this.storage.ready().then(() => {
      this.storage.clear();
    });
  }
}

EDIT:
Well, der must be a mistake, because When I use your version of a guard

canActivate(): Promise<boolean> {
    return this.storageService.get(Constants.ONBOARDED)
    .then(onboarded => !onboarded);   
  }

I get the error-message, that property then does not exist on type void

  • Declare types for function return values
  • Do absolutely everything in your power to avoid any
  • Read this post. In here lies your proximate problem - get is a “class C” function, and so it must be declared to return a Promise (or Observable), and its first word should be return. You have some design leeway for what store and remove are, but need to be aware of the consequences of the choices

So I would write get like this, which should give you an idea of how to fix the rest of StorageService:

get<T>(key: string): Promise<T> {
  return this.storage.ready().then(() => this.storage.get(key));
}

How do I avoid any if all items stored have a differnet structure?
What does <T> stand for?

Same answer to both questions.

get<T>(key: string): Promise<T> { return this.storage.ready().then(this.storage.get(key)); }
throws this error

 src/app/services/storage.service.ts:21:38
[ng]         21     return this.storage.ready().then(this.storage.get(key));
[ng]                                                 ~~~~~~~~~~~~~~~~~~~~~
[ng]         Did you forget to use 'await'?

Sorry, typo.

return this.storage.ready().then(() => this.storage.get(key));

I only need to add the <T> to store because there is a value, right? And should I return stuff from all methods and not only from get to catch errors?

store<T>(key: string, value: T) {
    this.storage.ready().then( () => {
      this.storage.set(key, value);
    });
  }

  get<T>(key: string): Promise<T> {
    return this.storage.ready().then(() => this.storage.get(key));
  }

  remove(key: string) {
    this.storage.ready().then(() => {
      this.storage.remove(key);
    });
  }
  
  clear() {
    this.storage.ready().then(() => {
      this.storage.clear();
    });
  }

I guess so. I would phrase it as improving type safety by avoiding any.

That depends on how you characterize them. In my three-type taxonomy, only the return values from type C functions are relevant. Type A and B functions should return void (as described in that post), because they are designed either so that (A) nobody cares when they finish or (B) they do care internally, but nobody outside needs to know.

This leads to another philosophical discussion about Storage. My position, as laid out there, is to write optimistically (so in my versions of these services you are writing, for example, set returns void).

I choose never to rely on Storage for in-app communication. You can do so, but if you do, then you must make set return a Promise, and you must religiously wait on every write you make before any code path may potentially read back, or you introduce a race condition whereby a read halfway across your app hits before your last write completed, thanks to one particular router change if the page has been cached on a SamBuzz K-350 phone that has just been rebooted, but only on Thursdays.

Obviously, that example is exaggerated, but race conditions do have a way of not showing up until your app is in production. I have been burned by so many I have lost count.

At first, I wanna thank you for your patience with me! For me this is all really new and it takes me some time to understand everything…

(A) nobody cares when they finish or (B) they do care internally, but nobody outside needs to know.

Since I try to build a little Game it is important to know, if the data sent to the Storage is really stored, which then means I need to declare all Methods to return a Promise and check it then.

And another thing. This <T> stands for declaring a type that is returned, right? I now understood how to use it in my storage-system but I don’t get how to use it in a resolver for example, that should return an array of json-objects:

export class NamenseingabeResolver implements Resolve<any> {

  constructor(private storageService: StorageService) { }

  resolve(): Promise<any> {
    return this.storageService.get(Constants.SPIELER)
    .then(res => {
      if(res)
        return res;
      else
        return [];
    });   
  }

}

I would push back on this.

Think of it this way. Let’s say you’re playing Hangman, and the game can be paused and resumed. What you would need to write to and read back from Storage is really just:

  • the target word
  • what guesses have been made so far

Everything else can be deduced from that. Now imagine that you have a service that keeps track of guesses. Every time it hears of a new one it adds it to its list, and writes that out to Storage.

The key part: even before that write finishes, the service still has up-to-date data. So as long as everybody else in the app is just asking the service what’s been guessed, you have no problems.

The trouble starts to happen when other parts of the app bypass the service and read directly from Storage. I think this is a categorically bad choice for a myriad of reasons: it makes changing storage methods harder, it makes testing and maintenance harder, the code is harder to follow, etc. So as long as you are disciplined about not either reading or writing to Storage outside of your services, you can avoid bugs like this, even without waiting on Storage writes.

I’m not familiar with the term “resolver” in this context. I’m guessing it’s from some object mapping library. I can’t really improve on the TypeScript documentation I linked earlier, but you can use arrays as generic parameters:

interface Spieler { ... }
class NamenseingabeResolver implements Resolve<Spieler[]> {
  resolve(): Promise<Spieler[]> {...}
}

…or (I’m having a tough time thinking of a real-world situation in which I would do something this abstract, but…):

class NamenseingabeResolver<T> implements Resolve<T[]> {
  resolve(): Promise<T[]> {...}
}

You made a good point with not accessing storage outside a service, I dindn’t think about this yet. This means that services are kind of static so everything else can access it and changes are saved globally.

Resolvers are used to load data for a page/component before it is actually loaded. They are added in the routing module:

{
  path: 'index-namenseingabe',
  loadChildren: () => import('../pages/namenseingabe/namenseingabe.module').then( m => m.NamenseingabePageModule),
  resolve: {
   spieler: NamenseingabeResolver
  }
},

And if I use this:

export class NamenseingabeResolver<T> implements Resolve<T[]> {

  constructor(private storageService: StorageService) { }

  resolve>(): Promise<T[]> {
    return this.storageService.get(Constants.SPIELER)
    .then(res => {
      if(res)
        return res;
      else
        return [];
    }).catch(() => {
      return [];
    });   
  }

I get this error:

ERROR in src/app/resolvers/namenseingabe.resolver.ts:14:5 - error TS2322: Type 'Promise<unknown>' is not assignable to type 'Promise<T[]>'.
[ng]       Type '{}' is missing the following properties from type 'T[]': length, pop, push, concat, and 26 more.```

I found the solution for the error: I had to change

if(res)

to

if(res && Array.isArray(res))

Exactly.

Ah, OK. I have not found lazy loading anywhere near worth the mountain of hassle, so I don’t bother with it.

I think the first example (using a concrete Spieler[T] parameter) is much more readable, anyway. The example here of a Resolve<Hero> could just as easily be a Resolve<Hero[]> to return an array of Heros.

I found the solution for the error: I had to change

if(res)

to

if(res && Array.isArray(res))

Is This also a good solution or should I create my own data-type Spieler for that?

I think what you have is fine.

So I now updatet my system so that storage service is only accessed by the services. I created Behaviour Subjects for my data and here comes the next problem: My guards need access to the data in these subjects but they don’t wait until this data is there. Before, I got the data directly from the storage so it was no problem.

spieler$ = new BehaviorSubject<any>('');

  constructor(private storageService: StorageService) { }

  init() {
    this.storageService.get(Constants.SPIELER).then( async res => {
      if(Array.isArray(res)) {
        this.spieler$.next(res);
        this.spieler = res;
      } else {
        this.spieler$.next([]);
      }
    });
  }

init is called in a resolver, right at the begiining, but still my guard does not wait for the data:

canActivate(): boolean {
    this.spielerService.spieler$.subscribe(async res => {
      if(res && Array.isArray(res) && res.length > 1) {
        this.router.navigate(['/home/spieleauswahl']);
        return false;
      } else {
        return true;
      }
    }).unsubscribe();
  }

Moreover, there is an error, because there is no return-path outside the subscription