Updated mercure integration so that the hook accepts a list of topics to subscribe

This commit is contained in:
Alejandro Celaya 2021-02-28 10:12:30 +01:00
parent 71ee886e24
commit 9904ac757b
10 changed files with 41 additions and 21 deletions

View file

@ -0,0 +1,7 @@
export class Topics {
public static visits = () => 'https://shlink.io/new-visit';
public static shortUrlVisits = (shortCode: string) => `https://shlink.io/new-visit/${shortCode}`;
public static orphanVisits = () => 'https://shlink.io/new-orphan-visit';
}

View file

@ -12,7 +12,7 @@ export interface MercureBoundProps {
export function boundToMercureHub<T = {}>( export function boundToMercureHub<T = {}>(
WrappedComponent: FC<MercureBoundProps & T>, WrappedComponent: FC<MercureBoundProps & T>,
getTopicForProps: (props: T) => string, getTopicsForProps: (props: T) => string[],
) { ) {
const pendingUpdates = new Set<CreateVisit>(); const pendingUpdates = new Set<CreateVisit>();
@ -22,7 +22,7 @@ export function boundToMercureHub<T = {}>(
useEffect(() => { useEffect(() => {
const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisits([ visit ]); const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisits([ visit ]);
const closeEventSource = bindToMercureTopic(mercureInfo, getTopicForProps(props), onMessage, loadMercureInfo); const closeEventSource = bindToMercureTopic(mercureInfo, getTopicsForProps(props), onMessage, loadMercureInfo);
if (!interval) { if (!interval) {
return closeEventSource; return closeEventSource;

View file

@ -1,13 +1,17 @@
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, topics: 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) {
return undefined; return undefined;
} }
const onEventSourceMessage = ({ data }: { data: string }) => onMessage(JSON.parse(data) as T);
const onEventSourceError = ({ status }: { status: number }) => status === 401 && onTokenExpired();
const subscriptions: EventSource[] = topics.map((topic) => {
const hubUrl = new URL(mercureHubUrl); const hubUrl = new URL(mercureHubUrl);
hubUrl.searchParams.append('topic', topic); hubUrl.searchParams.append('topic', topic);
@ -17,8 +21,11 @@ export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topic: string, o
}, },
}); });
es.onmessage = ({ data }: { data: string }) => onMessage(JSON.parse(data) as T); es.onmessage = onEventSourceMessage;
es.onerror = ({ status }: { status: number }) => status === 401 && onTokenExpired(); es.onerror = onEventSourceError;
return () => es.close(); return es;
});
return () => subscriptions.forEach((es) => es.close());
}; };

View file

@ -10,6 +10,7 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { CreateShortUrlProps } from '../short-urls/CreateShortUrl'; import { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
import { VisitsOverview } from '../visits/reducers/visitsOverview'; import { VisitsOverview } from '../visits/reducers/visitsOverview';
import { Versions } from '../utils/helpers/version'; import { Versions } from '../utils/helpers/version';
import { Topics } from '../mercure/helpers/Topics';
import { isServerWithId, SelectedServer } from './data'; import { isServerWithId, SelectedServer } from './data';
import './Overview.scss'; import './Overview.scss';
@ -119,4 +120,4 @@ export const Overview = (
</Card> </Card>
</> </>
); );
}, () => 'https://shlink.io/new-visit'); }, () => [ Topics.visits(), Topics.orphanVisits() ]);

View file

@ -9,6 +9,7 @@ import { determineOrderDir, OrderDir } from '../utils/utils';
import { isReachableServer, SelectedServer } from '../servers/data'; import { isReachableServer, SelectedServer } from '../servers/data';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { parseQuery } from '../utils/helpers/query'; import { parseQuery } from '../utils/helpers/query';
import { Topics } from '../mercure/helpers/Topics';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams'; import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
import { ShortUrlsTableProps } from './ShortUrlsTable'; import { ShortUrlsTableProps } from './ShortUrlsTable';
@ -98,6 +99,6 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
</Card> </Card>
</> </>
); );
}, () => 'https://shlink.io/new-visit'); }, () => [ Topics.visits() ]);
export default ShortUrlsList; export default ShortUrlsList;

View file

@ -8,6 +8,7 @@ import { Result } from '../utils/Result';
import { ShlinkApiError } from '../api/ShlinkApiError'; import { ShlinkApiError } from '../api/ShlinkApiError';
import { TagsList as TagsListState } from './reducers/tagsList'; import { TagsList as TagsListState } from './reducers/tagsList';
import { TagCardProps } from './TagCard'; import { TagCardProps } from './TagCard';
import { Topics } from '../mercure/helpers/Topics';
const { ceil } = Math; const { ceil } = Math;
const TAGS_GROUPS_AMOUNT = 4; const TAGS_GROUPS_AMOUNT = 4;
@ -75,6 +76,6 @@ const TagsList = (TagCard: FC<TagCardProps>) => boundToMercureHub((
{renderContent()} {renderContent()}
</> </>
); );
}, () => 'https://shlink.io/new-visit'); }, () => [ Topics.visits() ]);
export default TagsList; export default TagsList;

View file

@ -1,6 +1,7 @@
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { ShlinkVisitsParams } from '../api/types'; import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics';
import { TagVisits as TagVisitsState } from './reducers/tagVisits'; import { TagVisits as TagVisitsState } from './reducers/tagVisits';
import VisitsStats from './VisitsStats'; import VisitsStats from './VisitsStats';
import { OrphanVisitsHeader } from './OrphanVisitsHeader'; import { OrphanVisitsHeader } from './OrphanVisitsHeader';
@ -26,4 +27,4 @@ export const OrphanVisits = boundToMercureHub(({
> >
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} /> <OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
</VisitsStats> </VisitsStats>
), () => 'https://shlink.io/new-orphan-visit'); ), () => [ Topics.orphanVisits() ]);

View file

@ -3,6 +3,7 @@ import { RouteComponentProps } from 'react-router';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { ShlinkVisitsParams } from '../api/types'; import { ShlinkVisitsParams } from '../api/types';
import { parseQuery } from '../utils/helpers/query'; import { parseQuery } from '../utils/helpers/query';
import { Topics } from '../mercure/helpers/Topics';
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits'; import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader'; import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
import { ShortUrlDetail } from './reducers/shortUrlDetail'; import { ShortUrlDetail } from './reducers/shortUrlDetail';
@ -45,6 +46,6 @@ const ShortUrlVisits = boundToMercureHub(({
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} /> <ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
</VisitsStats> </VisitsStats>
); );
}, ({ match }) => `https://shlink.io/new-visit/${match.params.shortCode}`); }, ({ match }) => [ Topics.shortUrlVisits(match.params.shortCode) ]);
export default ShortUrlVisits; export default ShortUrlVisits;

View file

@ -2,6 +2,7 @@ import { RouteComponentProps } from 'react-router';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import ColorGenerator from '../utils/services/ColorGenerator'; import ColorGenerator from '../utils/services/ColorGenerator';
import { ShlinkVisitsParams } from '../api/types'; import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics';
import { TagVisits as TagVisitsState } from './reducers/tagVisits'; import { TagVisits as TagVisitsState } from './reducers/tagVisits';
import TagVisitsHeader from './TagVisitsHeader'; import TagVisitsHeader from './TagVisitsHeader';
import VisitsStats from './VisitsStats'; import VisitsStats from './VisitsStats';
@ -27,6 +28,6 @@ const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} /> <TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
</VisitsStats> </VisitsStats>
); );
}, () => 'https://shlink.io/new-visit'); }, () => [ Topics.visits() ]);
export default TagVisits; export default TagVisits;

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: {