Added support to dispatch all UI actions based on mercure bindings on a specific schedule instead of real time

This commit is contained in:
Alejandro Celaya 2020-09-12 08:52:03 +02:00
parent 9b45513684
commit ad437f655e
10 changed files with 31 additions and 18 deletions

View file

@ -13,13 +13,22 @@ export function boundToMercureHub<T = {}>(
WrappedComponent: FC<MercureBoundProps & T>, WrappedComponent: FC<MercureBoundProps & T>,
getTopicForProps: (props: T) => string, getTopicForProps: (props: T) => string,
) { ) {
const pendingUpdates = new Set<CreateVisit>();
return (props: MercureBoundProps & T) => { return (props: MercureBoundProps & T) => {
const { createNewVisit, loadMercureInfo, mercureInfo } = props; const { createNewVisit, loadMercureInfo, mercureInfo } = props;
const { interval } = mercureInfo;
useEffect( useEffect(() => {
bindToMercureTopic(mercureInfo, getTopicForProps(props), createNewVisit, loadMercureInfo), const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisit(visit);
[ mercureInfo ],
); interval && setInterval(() => {
pendingUpdates.forEach(createNewVisit);
pendingUpdates.clear();
}, interval * 1000 * 60);
bindToMercureTopic(mercureInfo, getTopicForProps(props), onMessage, loadMercureInfo);
}, [ mercureInfo ]);
return <WrappedComponent {...props} />; return <WrappedComponent {...props} />;
}; };

View file

@ -1,7 +1,7 @@
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill'; import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
import { MercureInfo } from '../reducers/mercureInfo'; import { MercureInfo } from '../reducers/mercureInfo';
export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topic: string, onMessage: (message: T) => void, onTokenExpired: Function) => () => { // eslint-disable-line max-len export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topic: string, onMessage: (message: T) => void, onTokenExpired: Function) => { // eslint-disable-line max-len
const { mercureHubUrl, token, loading, error } = mercureInfo; const { mercureHubUrl, token, loading, error } = mercureInfo;
if (loading || error || !mercureHubUrl) { if (loading || error || !mercureHubUrl) {

View file

@ -13,11 +13,12 @@ export const GET_MERCURE_INFO = 'shlink/mercure/GET_MERCURE_INFO';
export interface MercureInfo { export interface MercureInfo {
token?: string; token?: string;
mercureHubUrl?: string; mercureHubUrl?: string;
interval?: number;
loading: boolean; loading: boolean;
error: boolean; error: boolean;
} }
export type GetMercureInfoAction = Action<string> & ShlinkMercureInfo; export type GetMercureInfoAction = Action<string> & ShlinkMercureInfo & { interval?: number };
const initialState: MercureInfo = { const initialState: MercureInfo = {
loading: true, loading: true,
@ -27,7 +28,7 @@ const initialState: MercureInfo = {
export default buildReducer<MercureInfo, GetMercureInfoAction>({ export default buildReducer<MercureInfo, GetMercureInfoAction>({
[GET_MERCURE_INFO_START]: (state) => ({ ...state, loading: true, error: false }), [GET_MERCURE_INFO_START]: (state) => ({ ...state, loading: true, error: false }),
[GET_MERCURE_INFO_ERROR]: (state) => ({ ...state, loading: false, error: true }), [GET_MERCURE_INFO_ERROR]: (state) => ({ ...state, loading: false, error: true }),
[GET_MERCURE_INFO]: (_, { token, mercureHubUrl }) => ({ token, mercureHubUrl, loading: false, error: false }), [GET_MERCURE_INFO]: (_, action) => ({ ...action, loading: false, error: false }),
}, initialState); }, initialState);
export const loadMercureInfo = (buildShlinkApiClient: ShlinkApiClientBuilder) => export const loadMercureInfo = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
@ -44,9 +45,9 @@ export const loadMercureInfo = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
} }
try { try {
const result = await mercureInfo(); const info = await mercureInfo();
dispatch<GetMercureInfoAction>({ type: GET_MERCURE_INFO, ...result }); dispatch<GetMercureInfoAction>({ type: GET_MERCURE_INFO, interval: settings.realTimeUpdates.interval, ...info });
} catch (e) { } catch (e) {
dispatch({ type: GET_MERCURE_INFO_ERROR }); dispatch({ type: GET_MERCURE_INFO_ERROR });
} }

View file

@ -30,7 +30,7 @@ const RealTimeUpdates = (
<Input <Input
type="number" type="number"
min={0} min={0}
placeholder={realTimeUpdates.enabled ? 'Immediate' : ''} placeholder="Immediate"
disabled={!realTimeUpdates.enabled} disabled={!realTimeUpdates.enabled}
value={intervalValue(realTimeUpdates.interval)} value={intervalValue(realTimeUpdates.interval)}
onChange={(e) => setRealTimeUpdatesInterval(Number(e.target.value))} onChange={(e) => setRealTimeUpdatesInterval(Number(e.target.value))}

View file

@ -13,7 +13,10 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Services // Services
bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates); bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates);
bottle.decorator('RealTimeUpdates', connect([ 'settings' ], [ 'setRealTimeUpdatesInterval' ])); bottle.decorator(
'RealTimeUpdates',
connect([ 'settings' ], [ 'toggleRealTimeUpdates', 'setRealTimeUpdatesInterval' ]),
);
// Actions // Actions
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates); bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);

View file

@ -20,7 +20,7 @@ describe('helpers', () => {
[ Mock.of<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined }) ], [ Mock.of<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined }) ],
[ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: undefined }) ], [ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: undefined }) ],
])('does not bind an EventSource when loading, error or no hub URL', (mercureInfo) => { ])('does not bind an EventSource when loading, error or no hub URL', (mercureInfo) => {
bindToMercureTopic(mercureInfo, '', identity, identity)(); bindToMercureTopic(mercureInfo, '', identity, identity);
expect(EventSource).not.toHaveBeenCalled(); expect(EventSource).not.toHaveBeenCalled();
expect(onMessage).not.toHaveBeenCalled(); expect(onMessage).not.toHaveBeenCalled();
@ -40,7 +40,7 @@ describe('helpers', () => {
error: false, error: false,
mercureHubUrl, mercureHubUrl,
token, token,
}, topic, onMessage, onTokenExpired)(); }, topic, onMessage, onTokenExpired);
expect(EventSource).toHaveBeenCalledWith(hubUrl, { expect(EventSource).toHaveBeenCalledWith(hubUrl, {
headers: { headers: {

View file

@ -36,11 +36,11 @@ describe('mercureInfoReducer', () => {
}); });
it('returns mercure info on GET_MERCURE_INFO', () => { it('returns mercure info on GET_MERCURE_INFO', () => {
expect(reducer(undefined, { type: GET_MERCURE_INFO, ...mercureInfo })).toEqual({ expect(reducer(undefined, { type: GET_MERCURE_INFO, ...mercureInfo })).toEqual(expect.objectContaining({
...mercureInfo, ...mercureInfo,
loading: false, loading: false,
error: false, error: false,
}); }));
}); });
}); });

View file

@ -19,7 +19,7 @@ describe('<TagsList />', () => {
wrapper = shallow( wrapper = shallow(
<TagsListComp <TagsListComp
{...Mock.all<TagsListProps>()} {...Mock.all<TagsListProps>()}
{...Mock.all<MercureBoundProps>()} {...Mock.of<MercureBoundProps>({ mercureInfo: {} })}
forceListTags={identity} forceListTags={identity}
filterTags={filterTags} filterTags={filterTags}
tagsList={Mock.of<TagsList>(tagsList)} tagsList={Mock.of<TagsList>(tagsList)}

View file

@ -26,7 +26,7 @@ describe('<ShortUrlVisits />', () => {
wrapper = shallow( wrapper = shallow(
<ShortUrlVisits <ShortUrlVisits
{...Mock.all<ShortUrlVisitsProps>()} {...Mock.all<ShortUrlVisitsProps>()}
{...Mock.all<MercureBoundProps>()} {...Mock.of<MercureBoundProps>({ mercureInfo: {} })}
getShortUrlDetail={identity} getShortUrlDetail={identity}
getShortUrlVisits={getShortUrlVisitsMock} getShortUrlVisits={getShortUrlVisitsMock}
match={match} match={match}

View file

@ -26,7 +26,7 @@ describe('<TagVisits />', () => {
wrapper = shallow( wrapper = shallow(
<TagVisits <TagVisits
{...Mock.all<TagVisitsProps>()} {...Mock.all<TagVisitsProps>()}
{...Mock.all<MercureBoundProps>()} {...Mock.of<MercureBoundProps>({ mercureInfo: {} })}
getTagVisits={getTagVisitsMock} getTagVisits={getTagVisitsMock}
match={match} match={match}
history={history} history={history}