estus estus - 1 year ago 147
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 Source

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