feat: add new logic for stream api (#76)

This commit is contained in:
Kilin Mikhail
2021-01-17 20:33:44 +03:00
committed by GitHub
parent 5078a2cf4b
commit 4f389c47f5
6 changed files with 110 additions and 54 deletions

View File

@ -1,20 +1,33 @@
import {fromPromise} from '@most/core'; import {failure, pending, RemoteData, success} from '@devexperts/remote-data-ts';
import {fromPromise, startWith, recoverWith, map, now} from '@most/core';
import {Stream} from '@most/types'; import {Stream} from '@most/types';
import {pipe} from 'fp-ts/pipeable';
import {objectEntries} from './objectEntries'; import {objectEntries} from './objectEntries';
type PromiseApi = Record<string, (...args: any[]) => Promise<unknown>>; type PromiseApi = Record<string, (...args: any[]) => Promise<unknown>>;
type StreamApi<T extends PromiseApi> = { type StreamApi<T extends PromiseApi, E> = {
[K in keyof T]: (...params: Parameters<T[K]>) => ( [K in keyof T]: (
T[K] extends (...args: any[]) => Promise<infer R> ? Stream<R> : never ...params: Parameters<T[K]>
); ) => T[K] extends (...args: any[]) => Promise<infer R> ? Stream<RemoteData<E, R>> : never;
}; };
const pipeApiStream = <T>(stream$: Stream<T>) =>
pipe(
stream$,
map(val => success(val)),
startWith(pending),
recoverWith(err => now(failure(err)))
);
export const makeApi = <T extends PromiseApi>(apiObj: T) => { export const makeApi = <T extends PromiseApi, E = Error>(apiObj: T) => {
return objectEntries(apiObj).reduce((streamObj, [apiKey, apiMethod]) => { return objectEntries(apiObj).reduce((streamObj, [apiKey, apiMethod]) => {
return { return {
...streamObj, ...streamObj,
[apiKey]: (...args: Parameters<typeof apiMethod>) => fromPromise(apiMethod(...args)), [apiKey]: (...args: Parameters<typeof apiMethod>) => {
const res = fromPromise(apiMethod(...args));
return pipeApiStream(res);
}
}; };
}, {} as StreamApi<T>); }, {} as StreamApi<T, E>);
}; };

View File

@ -2,6 +2,7 @@ import {Sink, Stream} from '@most/types';
import {noop} from 'lodash'; import {noop} from 'lodash';
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {newDefaultScheduler} from '@most/scheduler'; import {newDefaultScheduler} from '@most/scheduler';
import {pending, RemoteData} from '@devexperts/remote-data-ts';
export function useStream<T extends Array<unknown>, R>( export function useStream<T extends Array<unknown>, R>(
piping: () => Stream<R>, piping: () => Stream<R>,
@ -31,3 +32,32 @@ export function useStream<T extends Array<unknown>, R>(
return state; return state;
} }
export function useStreamRD<T extends Array<unknown>, R, E = Error>(
piping: () => Stream<RemoteData<E, R>>,
props: T,
) {
const [state, setState] = useState<RemoteData<E, R>>(pending);
useEffect(() => {
setState(pending);
// eslint-disable-next-line
}, props);
useEffect(() => {
const effect$ = piping();
const sink: Sink<RemoteData<E, R>> = {
event: (_, val) => {
setState(val);
},
end: noop,
error: noop
};
const unsub = effect$.run(sink, newDefaultScheduler());
return () => {
unsub.dispose();
};
// eslint-disable-next-line
}, props);
return state;
}

View File

@ -1,22 +1,35 @@
import {combine} from '@most/core'; import {combine} from '@most/core';
import {combine as combineRD, map} from '@devexperts/remote-data-ts';
import {pipe} from 'fp-ts/lib/function';
import React, {Fragment, memo} from 'react'; import React, {Fragment, memo} from 'react';
import {isNotEmpty} from '_referers/common';
import {makeTreeList} from '_utils/makeTreeList';
import {commonApi} from '_api/commonApi'; import {commonApi} from '_api/commonApi';
import {useStream} from '_utils/useStream'; import {renderAsyncData} from '_utils/asyncDataUtils';
import {makeTreeList} from '_utils/makeTreeList';
import {useStreamRD} from '_utils/useStream';
import InfoList from '../info-list'; import InfoList from '../info-list';
const stream$ = combine((taskList, folderList) => { const stream$ = combine(
return makeTreeList(folderList, taskList); (taskListRD, folderListRD) => {
}, commonApi.taskList.getAll(), commonApi.folderList.getAll()); return pipe(
combineRD(taskListRD, folderListRD),
map(([taskList, folderList]) => {
return makeTreeList(folderList, taskList);
})
);
},
commonApi.taskList.getAll(),
commonApi.folderList.getAll()
);
const Page: React.FC = () => { const Page: React.FC = () => {
const tree = useStream(() => stream$, []); const treeRD = useStreamRD(() => stream$, []);
return ( return (
<Fragment> <Fragment>
{isNotEmpty(tree) && ( {renderAsyncData(treeRD, tree => (
<InfoList list={tree} space={1} /> <InfoList list={tree} space={1} />
)} ))}
</Fragment> </Fragment>
); );
}; };

View File

@ -1,25 +1,27 @@
import React, {Fragment, memo} from 'react'; import React, {Fragment, memo} from 'react';
import {renderAsyncData} from '_utils/asyncDataUtils';
import {usersApi} from '_api/usersTestApi'; import {usersApi} from '_api/usersTestApi';
import {useStream} from '_utils/useStream'; import {useStreamRD} from '_utils/useStream';
import UserComponent from './User'; import UserComponent from './User';
const Page: React.FC = () => { const Page: React.FC = () => {
const users = useStream(() => usersApi.request(), []); const users = useStreamRD(() => usersApi.request(), []);
return ( return (
<Fragment> <Fragment>
<div> <div>
tags tags
{users?.map(user => ( {renderAsyncData(users, successData =>
<UserComponent userId={user.id} key={user.id}/> successData?.map(user => <UserComponent userId={user.id} key={user.id} />)
))} )}
</div> </div>
<div> <div>
tags tags
{users?.map(user => ( {renderAsyncData(users, successData =>
<UserComponent userId={user.id} key={user.id}/> successData?.map(user => <UserComponent userId={user.id} key={user.id} />)
))} )}
</div> </div>
</Fragment> </Fragment>
); );

View File

@ -1,12 +1,9 @@
import {pending, success} from '@devexperts/remote-data-ts';
import {map} from '@most/core';
import {pipe} from 'fp-ts/lib/function';
import React, {FC, Fragment, memo} from 'react'; import React, {FC, Fragment, memo} from 'react';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import {usersApi} from '_api/usersTestApi'; import {usersApi} from '_api/usersTestApi';
import {renderAsyncData} from '_utils/asyncDataUtils'; import {renderAsyncData} from '_utils/asyncDataUtils';
import {useStream} from '_utils/useStream'; import {useStreamRD} from '_utils/useStream';
import {userEntityStore} from './utils'; import {userEntityStore} from './utils';
type Props = { type Props = {
@ -15,16 +12,13 @@ type Props = {
const User: FC<Props> = ({userId}) => { const User: FC<Props> = ({userId}) => {
const data = const data =
useStream(() => { useStreamRD(() => {
const userStringId = userId.toString(); const userStringId = userId.toString();
return userEntityStore.get(userStringId, () => return userEntityStore.get(userStringId, () =>
pipe( usersApi.findById(userStringId)
usersApi.findById(userStringId),
map(val => success(val))
)
); );
}, [userId]) ?? pending; }, [userId]);
return ( return (
<Fragment> <Fragment>

View File

@ -1,38 +1,44 @@
import React, {FC, memo} from 'react'; import {success} from '@devexperts/remote-data-ts';
import {pipe} from 'fp-ts/es6/pipeable';
import {at, chain, periodic, map} from '@most/core'; import {at, chain, periodic, map} from '@most/core';
import {pipe} from 'fp-ts/es6/pipeable';
import {useStream} from '_utils/useStream'; import React, {FC, memo} from 'react';
import {usersApi} from '_api/usersTestApi';
import {useParams} from 'react-router-dom'; import {useParams} from 'react-router-dom';
import {usersApi} from '_api/usersTestApi';
import {chainRD, renderAsyncData} from '_utils/asyncDataUtils';
import {useStreamRD} from '_utils/useStream';
type Props = { type Props = {
id: string; id: string;
} };
const User: FC<Props> = () => { const User: FC<Props> = () => {
const {id} = useParams<Props>(); const {id} = useParams<Props>();
const user = useStream(() => { const userRD = useStreamRD(() => {
let i = 0; let i = 0;
return pipe( return pipe(
at(3000, undefined), at(3000, undefined),
chain(() => usersApi.findById(id)), chain(() => usersApi.findById(id)),
chain(data => { chainRD(data => {
return pipe(periodic(1000), map(() => { const res = pipe(
i = i + 1; periodic(1000),
return { map(() => {
...data, i = i + 1;
chainableNumber: i return success({
}; ...data,
})); chainableNumber: i
});
})
);
return res;
}) })
); );
}, [id]); }, [id]);
return ( return (
<div> <div>
{user ? ( {renderAsyncData(userRD, user => (
<div> <div>
<div>{user.avatar}</div> <div>{user.avatar}</div>
<div>{user.email}</div> <div>{user.email}</div>
@ -41,9 +47,7 @@ const User: FC<Props> = () => {
<div>{user.last_name}</div> <div>{user.last_name}</div>
<div>{user.chainableNumber}</div> <div>{user.chainableNumber}</div>
</div> </div>
) : ( ))}
'Loading...'
)}
</div> </div>
); );
}; };