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 ?
Related
I'm getting this only when I subscribe to the function that makes api call from inside the Angular service, so the object that subscribes to the function is empty. Here's my code snippet from my service:
getSchedules(): Observable<Schedule[]> {
this.http.get<TempSchedules[]>(this.apiUrl).subscribe(x => this.temp = x);
this.temp.forEach((e, i) => {
// Do something, this loop is never executed because this.temp is empty
});
// Some processing here
return something; }
Here is my http.get function somewhere inside the service:
getTempSchedules(): Observable<TempSchedules[]> {
return this.http.get<TempSchedules[]>(this.apiUrl);
}
From the above, this.temp is empty. Why is that?
I called the above function in the service constructor as
constructor(private http:HttpClient) {
this.getTempSchedules().subscribe(x => this.temp = x);
}
Here is a code snippet from a component that calls that function in the service:
ngOnInit(): void {
this.scheduleService.getTempSchedules().subscribe(x => this.tempSchedules = x);
}
The component works fine, so when I use the value this.tempSchedules in the html it is displayed correctly. What am I missing here?
It is not working because you are not getting the way observable works. It is async process and you need to be in subscribe block to get it. In case you want to do some funky stuff with the response before returning it to the component, then you should use map
getTempSchedules(): Observable<Schedule[]> {
return this.http.get<TempSchedules[]>(this.apiUrl)
.pipe(map(res => {
return res.forEach(() => {
// Do something, this loop will be executed
})
})) }
use it in component as :
ngOnInit(): void {
this.scheduleService.getTempSchedules().subscribe(x => this.tempSchedules = x);
}
globleVariable: any;
ngOnInit() {
// This doesn't work. methodTwo throws error saying "cannot read someField from null. "
this.methodOne();
this.methodTwo();
}
methodOne() {
this.firstService.subscribe((res) => { this.globleVariable = res });
}
methodTwo() {
this.secondService.subscribe((res) => { console.log(this.globleVariable.someField) });
}
As shown above, methodOne set the value of globleVariable and methodTwo uses it, therefore the former must finish running before the latter.
I am wondering how to achieve that.
Instead of subscribing in the methods, combine them into one stream and subscribe to that in ngInit(). You can use tap to perform the side effect of updating globaleVariable that you were previously performing in subscribe().
In the example below the "methods" are converted into fields since there is no reason for them to be methods anymore (you can keep them as methods if you want). Then the concat operator is used to create a single stream, where methodOne$ will execute and then when it's complete, methodTwo$ will execute.
Because concat executes in order, you are guaranteed that globaleVariable will be set by methodOne$ before methodTwo$ begins.
globleVariable: any;
methodOne$ = this.someService.pipe(tap((res) => this.globleVariable = res));
methodTwo$ = this.someService.pipe(tap((res) => console.log(this.globleVariable.someField));
ngOnInit() {
concat(this.methodOne$, this.methodTwo$).subscribe();
}
You can create a subject for which observable 2 will wait to subscribe like below :-
globalVariable: any;
subject: Subject = new Subject();
methodOne() {
this.someService.subscribe((res) => { this.globleVariable = res; this.subject.next(); });
}
methodTwo() {
this.subject.pipe(take(1), mergeMap(() => this.someService)).subscribe((res) => {
console.log(this.globleVariable.someField) });
}
The only way to guarantee a method call after a subscription yields is to use the subscription callbacks.
Subscriptions have two main callbacks a success and a failure.
So the way to implement a method call after the subscription yeilds is to chain it like this:
globleVariable: any;
ngOnInit() {
this.methodOne();
}
methodOne() {
this.someService.subscribe((res) => {
this.globleVariable = res
this.methodTwo(); // <-- here, in the callback
});
}
methodTwo() {
this.someService.subscribe((res) => { console.log(this.globleVariable.someField) });
}
You might want to chain the calls with some other rxjs operators for a more standard usage.
ngOnInit() {
this.someService.method1.pipe(
take(1),
tap(res1 => this.globleVariable = res1)
switchmap(res1 => this.someService.method2), // <-- when first service call yelds success
catchError(err => { // <-- failure callback
console.log(err);
return throwError(err)
}),
).subscribe(res2 => { // <-- when second service call yelds success
console.log(this.globleVariable.someField) });
});
}
Please remember to complete any subscriptions when the component is destroyed to avoid the common memory leak.
my take,
so it's a bit confusing when you use same service that throws different results, so instead of someService I used firstService and secondService here.
this.firstService.pipe(
switchMap(globalVariable) =>
this.secondService.pipe(
map(fields => Object.assign({}, globalVariable, { someField: fields }))
)
)
).subscribe(result => {
this.globalVariable = result;
})
What I like about this approach is that you have the flexibility on how you want to use the final result as it is decoupled with any of the property in your class.
Imagine we have the following factory:
#Injectable()
export class DataClassFactory {
constructor(
private dataService: DataService,
) { }
public createThing(initialData?: InitialData): AsyncSubject<DataClass> {
let dataClass: AsyncSubject<DataClass> = new AsyncSubject<DataClass>();
if (!!initialData) {
dataClass.next(new DataClass(initialData));
dataClass.complete();
} else {
this.dataService.getData().subscribe((dataResponse) => {
dataClass.next(new ReportRequest(dataResponse));
dataClass.complete();
});
}
}
return dataClass;
}
}
We inject this factory, invoke the createThing method, and subscribe to the response in some component. I originally tried to use a plain Subject, but then I realized that in the case where we already have initial data, next() is called before the response is returned, so the subscriber in the component never gets that value.
My question is: is this correct situation in which to use an AsyncSubject, or is there a different/better way to handle this sort of method that has potential synchronous and asynchronous timelines?
I would do something along these lines
public createThing(initialData?: InitialData): Observable<DataClass | ReportRequest> {
if (!!initialData) {
const data = new DataClass(initialData);
return of(data);
} else {
return this.dataService.getData()
.pipe(map(dataResponse => new ReportRequest(dataResponse));
}
}
Whoever calls createThing would get an Observable to which it would have to subscribe.
This Observable would emit an instance of DataClass if initialData is not null, otherwise it would return and instance of ReportRequest as soon as dataService responds.
I've been working on a personal app. My goal is to have a search and delete users using Observables. For search functionality I have used this idea, this is how I implemented:
export class UserComponent implements OnInit {
users: Observable<User[]>;
private searchTerms = new Subject<string>();
ngOnInit(): void {
this.setUsers();
}
private setUsers(): void {
this.users = this.searchTerms
.debounceTime(300) // wait 300ms after each keystroke before considering the term
.distinctUntilChanged() // ignore if next search term is same as previous
.switchMap(term => term // switch to new observable each time the term changes
// return the http search observable
? this.userService.search(term)
// or the observable of all users if there was no search term
: this.userService.search(''))
.catch(() => {
this.response = -1;
return Observable.of<User[]>([]);
});
}
}
For delete functionality I use this:
private deleteSubject = new Subject();
delete(user: User): void {
this.userService
.delete(user)
.then(() => {
this.deleteSubject.next('');
})
.catch(() => {
console.log('error');
});
}
private setUsers(): void {
... // here comes the search functionality shown above
this.subscription = this.deleteSubject.subscribe(
(term: string) => this.users = this.userService.search(''),
(err) => console.log(err)
);
}
The problem that I faced with this implementation is that after I delete an user my search functionality is broken because I lost the subscriber for searchTerms because I reassigned this.users. So I've been searching and found that I can use merge operator to append another subject, follow this idea. Sadly I'm facing a problem with the implementation. I tried this:
delete(user: User): void {
...
// change
this.deleteSubject.next('');
// with
this.deleteSubject.next({op: 'delete', id: user.id});
}
private setUsers(): void {
...
this.users.merge(this.deleteSubject)
.startWith([])
.scan((acc: any, val: any) => {
if (val.op && val.op === 'delete') {
let index = acc.findIndex((elt: any) => elt.id === val.id);
acc.splice(index, 1);
return acc;
} else {
return acc.concat(val);
}
});
But my scan operator is never execute because this.deleteSubject doesn't have a subscriber for it. I know how to create and associate a subscriber to my this.deleteSubject but don't know how to mix it with my merge operator. Also see that it's getting a bit complex, I guess it should be another way to get this done. Comments are appreciated.
Please take a look at my plunker, hope gets a bit clear. I've simulated the calls to my services using a memory array userList.
I fixed my problem, I had to do two changes:
Assign this.users.merge(this.deleteSubject) to this.users
In the else block for scan operator change return acc.concat(val); to return val;
I updated my plunker if you want to take a look.
Cheers.
I am on Angular 2.3.1 and I am fairly new to both Angular and event based programming. I have two subscriptions, route.params.subscribe and engineService.getEngines(). In my onInit I want to call getEngineName after this.route.params.subscribe and this.engineService.getEngines().subscribe complete.
Reason for this: getEngineName functions depends on the engineId from the queryParams and the engines array which is populated after the completion of getEngines() call.
I did look at flatMap and switchMap but I did not completely understand them.
This is the code in the component:
export class ItemListComponent implements OnInit {
items: Item[];
engines: Engine[];
private engineId: number;
constructor(
private router: Router,
private route: ActivatedRoute,
private itemService: ItemService,
private engineService: EngineService
) {}
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.engineId = +params['engineId'];
// itemService is irrelevant to this question
this.itemService.getItems({engineId: this.engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
});
this.engineService.getEngines()
.subscribe(engines => this.engines = engines);
// This should only run after engineId and engines array have been populated.
this.getEngineName(this.engineId);
}
getEngineName(engineId: number) {
this.engines.find((engine) => {
return engine.id === engineId;
})
}
}
Why don't you just move the logic inside the route.params callback?
this.route.params.subscribe((params: Params) => {
this.engineId = +params['engineId'];
// itemService is irrelevant to this question
this.itemService.getItems({engineId: this.engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
//this.engineId is defined here (1)
this.engineService.getEngines()
.subscribe((engines) => {
this.engines = engines;
//this.engines is defined here (2)
this.getEngineName(this.engineId);
});
});
with flatMap and forkJoin:
this.route.params.flatMap((params: Params) => {
this.engineId = +params['engineId'];
return Observable.forkJoin(
this.itemService.getItems({engineId: this.engineId}),
this.engineService.getEngines()
)
}).subscribe((data)=>{
let items = data[0];
let engines = data[1];
this.items = items;
this.engines = engines;
this.getEngineName(this.engineId);
});
switchMap is recommended in this scenario.
this.route.params.pluck('engineId') //pluck will select engineId key from params
.switchMap(engineId => {
this.getItems(engineId);
return this.engineService.getEngines().map(engines => {
/*this.engineService.getEngines() emits the list of engines.
Then use map operator to convert the list of engines to engine we are looking for
*/
return engines.find((engine) => {
return engine.id === engineId;
})
})
}).subscribe(engine => {
//engine
})
getItems(engineId) {
this.itemService.getItems({engineId: engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
}
Suppose the engineId in the params changes, the first observable this.route.params.pluck('engineId') will emit data, which will cause the next observable this.engineService.getEngines() to get fired. Now suppose the route changes before this observable emits the data. Here you need to cancel getEngines observable to prevent error. This is done by switchMap.
switchMap cancels inner observable if outer observable is fired.
PS: I have avoided keeping any states like this.engineId etc.