Reputation: 223064
I have a singleton service for app settings
class Setting {
get foo() {
return storage.get('foo');
}
set foo(val)
storage.set('foo', val);
}
}
that is bound in components' views as setting.foo
.
Because storage
calls may be costly and because it may be asynchronous, I would prefer to replace getter/setter with RxJS subject that could update and read storage
whenever needed.
So I'm refactoring it to
class Setting {
constructor() {
this.fooSubject = new ReplaySubject(1);
fooSubject.subscribe((val) => {
storage.set('foo', val);
});
this.foo$ = this.fooSubject
.map(() => storage.get('foo'))
.publishReplay(1).refCount();
}
and using it like setting.foo$ | async
and setting.fooSubject.next(newFoo)
. It looks like costly storage.get
calls are cached now.
There are two problems.
The first one is that both fooSubject
subject and foo$
observable should be publicly available to make this work, while a subject was chosen because it is supposed to be both an observable and an observer.
Can foo$
be made to be a single Subject property of Setting
service, so it could be subscribed with subscribe(...)
and updated with next(...)
?
The second one is that the code is still synchronous.
How can this case be treated for storage.get
and storage.set
that return promises?
Upvotes: 4
Views: 1781
Reputation: 223064
It was really easy to get a subject with side effects by extending AnonymousSubject
(a class for Subject.create(...)
factory). Resulting subject gets destination
and source
properties that hold original subject and observable.
class FooSharedSubject extends AnonymousSubject {
constructor() {
const subject = new BehaviorSubject('');
const observable = subject.asObservable()
.mergeMap((value) => promisedStorage.get('foo'))
.publishReplay(1)
.refCount();
super(subject, observable);
}
next(value): void {
promisedStorage.set('foo', value)).then(
() => this.destination.next(value),
() => this.destination.error(value)
);
}
}
Upvotes: 3
Reputation: 96969
There's not much to suggest in your code when it actually works as you want. I'm not sure I catch your use case exactly but I think you can simplify your code by not using this.foo$
at all.
The functionality of storing the latest value is provided by ReplaySubject
and unless you really need to call storage.get('foo')
every time after storage.set('foo', val);
it's not necessary.
I put your code into a live demo: http://plnkr.co/edit/pxjRQr6Q6Q7LzYb1oode?p=preview
So I think it can be simplified to this.
class Setting {
constructor() {
var storage = new Storage();
this.fooSubject = new ReplaySubject(1);
this.fooSubject.subscribe((val) => {
storage.set('foo', val);
});
}
get observable() {
return this.fooSubject.asObservable();
};
store(val) {
this.fooSubject.next(val);
}
}
I purposely hide the fact that I'm using a Subject
with .asObservable()
and by wrapping .next()
call with store()
method. A typical usage could look like:
let settings = new Setting();
settings.store('Hello');
settings.observable.subscribe(val => console.log(val));
settings.store('Hello 2');
settings.store('Hello 3');
settings.observable.subscribe(val => console.log(val));
settings.store('Hello 4');
Which prints to console:
Hello
Hello 2
Hello 3
Hello 3
Hello 4
Hello 4
Note, that you're not initializing the ReplaySubject
with any value. Even if you called setting.fooSubject.next(newFoo)
right after creating the ReplaySubject
it'll be stored again right after subscribing with storage.set('foo', val);
.
The issue about being synchronous. Well, your code is in fact asynchronous but sequential. Since JavaScript is single threaded then if storage.get('foo')
does some synchronous time consuming operation then it'll block the execution thread and probably the only option is to move it to a Web Worker.
Upvotes: 3