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 = {}>(
WrappedComponent: FC<MercureBoundProps & T>,
getTopicForProps: (props: T) => string,
getTopicsForProps: (props: T) => string[],
) {
const pendingUpdates = new Set<CreateVisit>();
@ -22,7 +22,7 @@ export function boundToMercureHub<T = {}>(
useEffect(() => {
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) {
return closeEventSource;

View file

@ -1,24 +1,31 @@
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
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;
if (loading || error || !mercureHubUrl) {
return undefined;
}
const hubUrl = new URL(mercureHubUrl);
const onEventSourceMessage = ({ data }: { data: string }) => onMessage(JSON.parse(data) as T);
const onEventSourceError = ({ status }: { status: number }) => status === 401 && onTokenExpired();
hubUrl.searchParams.append('topic', topic);
const es = new EventSource(hubUrl, {
headers: {
Authorization: `Bearer ${token}`,
},
const subscriptions: EventSource[] = topics.map((topic) => {
const hubUrl = new URL(mercureHubUrl);
hubUrl.searchParams.append('topic', topic);
const es = new EventSource(hubUrl, {
headers: {
Authorization: `Bearer ${token}`,
},
});
es.onmessage = onEventSourceMessage;
es.onerror = onEventSourceError;
return es;
});
es.onmessage = ({ data }: { data: string }) => onMessage(JSON.parse(data) as T);
es.onerror = ({ status }: { status: number }) => status === 401 && onTokenExpired();
return () => es.close();
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 { VisitsOverview } from '../visits/reducers/visitsOverview';
import { Versions } from '../utils/helpers/version';
import { Topics } from '../mercure/helpers/Topics';
import { isServerWithId, SelectedServer } from './data';
import './Overview.scss';
@ -119,4 +120,4 @@ export const Overview = (
</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 { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { parseQuery } from '../utils/helpers/query';
import { Topics } from '../mercure/helpers/Topics';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
import { ShortUrlsTableProps } from './ShortUrlsTable';
@ -98,6 +99,6 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
</Card>
</>
);
}, () => 'https://shlink.io/new-visit');
}, () => [ Topics.visits() ]);
export default ShortUrlsList;

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ describe('helpers', () => {
[ Mock.of<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined }) ],
[ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: undefined }) ],
])('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(onMessage).not.toHaveBeenCalled();
@ -40,7 +40,7 @@ describe('helpers', () => {
error: false,
mercureHubUrl,
token,
}, topic, onMessage, onTokenExpired);
}, [ topic ], onMessage, onTokenExpired);
expect(EventSource).toHaveBeenCalledWith(hubUrl, {
headers: {