Length is cool, especially when the post is as well-written and well-formatted as this one is. You are under absolutely no compulsion to take my opinion as anything other than the opinion of a really opinionated opinion-haver, and more than likely somebody else will be along shortly to offer a different one.
That being said, I recognize in your code several things that I used to do when I was starting out writing webapps, and no longer do any more for various reasons. I can expand more on why if desired, but:
I spent a lot of time writing C++, and putting as much “intelligence” into data-bearing objects is pretty idiomatic for OOP code. JavaScript is not an OO language. It pretends to be one, but you will be happier in the long run if you refuse to believe it. I now completely swear off “smart” data-bearing objects in JavaScript in favor of TypeScript interface
s (which are just POJOs), and choose to put all the “smarts” in wranglers. One major reason is because we get to avoid all of this error-prone tedium:
.pipe(
map((customers) => {
return customers ? JSON.parse(customers).map(
(customer: Customer) => new Customer(customer)) : []
})
)
We no longer have to worry about sprinkling magic Customer
constructor pixie dust on things, a problem our toolchain unfortunately can’t help us avoid because JavaScript.
So I would convert Customer
to an interface and put whatever intelligence is currently in it into CustomersService
instead.
This looks like a neat feature, but one showstopper problem with it is that you can’t override it elsewhere. This makes mocking it out for testing virtually impossible. So I don’t use it, instead just declaring services in the provider
stanza of my app module.
Now we get to the main event:
Never use
Storage
to communicate within runs of an app. Use it only to talk to the future and listen to the past.
Your situation here is precisely why I adopted this rule. Yes, it’s possible to implement the design you’ve chosen by judiciously waiting off the Promise
returned by storage.set
. However, as you’ve discovered, it’s a PITA to do. It also introduces a needless performance bottleneck. So what I would do instead is this:
import {clone} from "lodash-es";
export class CustomersService {
private customers$ = new BehaviorSubject<Customer[]>([]);
constructor(private storage: Storage) {
// this is the only time we read from storage:
// once at app startup to listen to the past
storage.ready()
.then(() => storage.get(StorageConstants.customers))
.then((customers) => {
if (customers) { this.customers$.next(customers); }
});
}
getCustomers(): Observable<Customer[]> {
return this.customers$;
}
addCustomer(customer: Customer): void {
// this clone may be avoided in some circumstances, but exists because
// it's not easy to detect changes to deep object references in JavaScript
let newCustomers = clone(this.customers$.value);
newCustomers.push(customer);
// the in-app communication is done entirely within CustomersService
this.customers$.next(newCustomers);
// here we speak to the future, and don't care when it's ready to be heard
this.storage.set(StorageConstants.customers, newCustomers);
}
}