estus estus - 2 months ago 45
TypeScript Question

Shared RxJS subject for bidirectional data binding in Angular 2

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?

Answer

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)
        );
    }
}