In my Angular 2 app, I have implemented a simple service cache using promises, and it works fine. However, I am not yet satisfied with my implementation:
export class Cache<T extends Cacheable<T, I>, I> {
items: T[];
init(items: T[]): void {
this.items = items;
}
isInitialised(): boolean {
return this.items != null;
}
all(): Promise<T[]> {
return Promise.resolve(this.items);
}
get(id: I): Promise<T> {
return Promise.resolve(this.items.find(item => item.id == id));
}
put(item: T): void {
this.items.push(item);
}
update(item: T): void {
this.get(item.id)
.then(cached => cached.copy(item));
}
remove(id: I): void {
let index = this.items.findIndex(item => item.id == id);
if (index > -1) {
this.items.splice(index, 1);
}
}
}
I would like the cache to initialise no matter which method is called, whether it is .all
, .get
etc. But, the key is that I want the first call to this.items
to force the cache to initialise. Any subsequent call should somehow wait until the cache is initialised before manipulating ´this.items´.
To do so, I thought of using a proxy method .itemsProxy()
which would return a promise, and replace any call to .items
with that method and adapt code consequently. This would require having a cache state which is either NOT_INITIALSED
, INITIALSING
or INITIALSED
.
The proxy method would look like this:
export class Cache<T extends Cacheable<T, I>, I> {
constructor(private service: AbstractService) {}
initPromise: Promise<T[]>;
state: NOT_INITIALISED;
itemsProxy(): Promise<T> {
if (this.state == INITIALISED)
return Promise.resolve(this.items);
if (this.state == NOT_INITIALISED) {
this.initPromise = this.service.all().then(items => {
// Avoid override when multiple subscribers
if (this.state != INITIALISED) {
this.items = items;
this.state = INITIALISED;
}
return this.items;
});
this.state = INITIALISING;
}
return this.initPromise;
}
...
}
Notice how initialisation is done via the this.service.all()
, which looks like this:
all(): Promise<T[]> {
if (this.cache.isInitialised()) {
return this.cache.all();
}
return this.http
.get(...)
.toPromise()
.then(response => {
this.cache.init(
response.json().data.map(item => new T(item))
);
return this.cache.all();
}); // No catch for demo
}
Note that new T(...)
is not valid, but I simplified the code for demo purposes.
The above solution does not seem to work like I'd expect it to, and asynchronous calls are hard to debug.
I thought of making this.items
an Observable
, where each call would subscribe to it. However, my understanding of observables is quite limited: I don't know if a subscriber can modify the data of the observable it is subscribed to (i.e. are observables mutable like a T[] ?). In addition, it seems to me that observables "produce" results, so after the cache is initialised, would any new subscriber be able to access the data produced by the observable ?
How would you implement this kind of "synchronization" mechanism ?