devexperts / dx-platform

Mozilla Public License 2.0
33 stars 24 forks source link

Deprecate rx-utils #180

Open raveclassic opened 5 years ago

raveclassic commented 5 years ago

Historically Sink and Context were a wrong abstraction and we should consider removing them in future versions of platform.

Sink exposes unsafe access to inner value when you can forget about subscribing to effect. On the other hand we could replace type Sink<A> = { value: A, effect: Observable<unknown>> } with just a type Sink<A> = Observable<A> where effect is bundled into the stream and you don't have direct unsafe access to sink's value, only in subscriber.

Keeping that in mind, type Context<E, A> becomes just a Reader<E, Observable<A>> and can be dropped as well.

Still we have to deliver LiveData and operators and that's why we should think about #171

igoralekseev commented 5 years ago

Can you please give an example of

type Sink<A> = Observable<A> where effect is bundled into the stream

raveclassic commented 5 years ago

@igoralekseev Assuming we're using rxjs:

const withEffect = <A>(source: Observable<A>, f: (a: A) => Observable<unknown>): Observable<A> =>
    merge(
        source,
        pipe(
            source,
            switchMap(f),
            ignoreElements(),
        ),
    );

declare function save(data: string): Observable<void>;
declare const source: Observable<string>;

const data = withEffect(source, save);
raveclassic commented 5 years ago

@igoralekseev Or if we're creating an instance of effectful viewmodel (typical usage):

interface ViewModel {
    a: Observable<string>;
    b: Observable<string>;
    remove(): void;
}
declare function remove(id: string): Observable<void>;
const createViewModel = (id: string): Observable<ViewModel> => {
    const removeHandler = createHandler<void>();
    const vm = {
        a: of('123'),
        b: of('456'),
        remove: removeHandler.handle,
    };
    const removeEffect = pipe(
        removeHandler.value$,
        switchMap(() => remove(id)),
        share(),
    );
    return withEffect(of(vm), () => removeEffect);
};
raveclassic commented 5 years ago

We could go even shorter with using Proxy for creating handlers:

const withEffect = <A>(source: Observable<A>, effect: Observable<unknown>): Observable<A> =>
    merge(
        source,
        pipe(
            source,
            switchMapTo(effect),
            ignoreElements(),
            share(),
        ),
    );
interface Handler<A> extends Observable<A> {
    (a: A): void;
}
interface VoidHandler<A> extends Observable<A> {
    (): void;
}
const functionKeys: PropertyKey[] = Object.getOwnPropertyNames(Object.getPrototypeOf(constVoid));
const createHandler = <A = never>(): A extends void ? VoidHandler<A> : Handler<A> => {
    const s = new Subject<A>();
    const shared = pipe(
        s,
        share(),
    );
    const next = (a: A) => s.next(a);
    return new Proxy(next, {
        get(target, key) {
            return (functionKeys.includes(key) ? next : shared)[key];
        },
    }) as any;
};

interface ViewModel {
    a: Observable<string>;
    b: Observable<string>;
    remove(): void;
}
declare function remoteRemove(id: string): Observable<void>;
const createViewModel = (id: string): Observable<ViewModel> => {
    const remove = createHandler<void>();
    const vm: ViewModel = {
        a: of('123'),
        b: of('456'),
        remove,
    };
    const removeEffect = pipe(
        remove,
        switchMap(() => remoteRemove(id)),
    );
    return withEffect(of(vm), removeEffect);
};
sutarmin commented 5 years ago

Wow, createHandler looks expressive and impressive. But I would say that having both Observable and Function at the same object (can I still call this thing an object?) is not quite FP way of making things though 😅

raveclassic commented 5 years ago

Why not, it's just an object that implements both Observable and Function interfaces. Anyway it's not related to Sink/Context