diff --git a/src/mercure/helpers/Topics.ts b/src/mercure/helpers/Topics.ts new file mode 100644 index 00000000..42e08d4f --- /dev/null +++ b/src/mercure/helpers/Topics.ts @@ -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'; +} diff --git a/src/mercure/helpers/boundToMercureHub.tsx b/src/mercure/helpers/boundToMercureHub.tsx index 38cae023..5b5d5680 100644 --- a/src/mercure/helpers/boundToMercureHub.tsx +++ b/src/mercure/helpers/boundToMercureHub.tsx @@ -12,7 +12,7 @@ export interface MercureBoundProps { export function boundToMercureHub( WrappedComponent: FC, - getTopicForProps: (props: T) => string, + getTopicsForProps: (props: T) => string[], ) { const pendingUpdates = new Set(); @@ -22,7 +22,7 @@ export function boundToMercureHub( 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; diff --git a/src/mercure/helpers/index.ts b/src/mercure/helpers/index.ts index 19c2176c..33073818 100644 --- a/src/mercure/helpers/index.ts +++ b/src/mercure/helpers/index.ts @@ -1,24 +1,31 @@ import { EventSourcePolyfill as EventSource } from 'event-source-polyfill'; import { MercureInfo } from '../reducers/mercureInfo'; -export const bindToMercureTopic = (mercureInfo: MercureInfo, topic: string, onMessage: (message: T) => void, onTokenExpired: Function) => { // eslint-disable-line max-len +export const bindToMercureTopic = (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()); }; diff --git a/src/servers/Overview.tsx b/src/servers/Overview.tsx index 12cd6909..703cf717 100644 --- a/src/servers/Overview.tsx +++ b/src/servers/Overview.tsx @@ -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 = ( ); -}, () => 'https://shlink.io/new-visit'); +}, () => [ Topics.visits(), Topics.orphanVisits() ]); diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index 971cc128..4736bc8c 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -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) => boundToMercur ); -}, () => 'https://shlink.io/new-visit'); +}, () => [ Topics.visits() ]); export default ShortUrlsList; diff --git a/src/tags/TagsList.tsx b/src/tags/TagsList.tsx index a7d21bce..7d30c5c1 100644 --- a/src/tags/TagsList.tsx +++ b/src/tags/TagsList.tsx @@ -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) => boundToMercureHub(( {renderContent()} ); -}, () => 'https://shlink.io/new-visit'); +}, () => [ Topics.visits() ]); export default TagsList; diff --git a/src/visits/OrphanVisits.tsx b/src/visits/OrphanVisits.tsx index a0538fd2..a6ae86a7 100644 --- a/src/visits/OrphanVisits.tsx +++ b/src/visits/OrphanVisits.tsx @@ -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(({ > -), () => 'https://shlink.io/new-orphan-visit'); +), () => [ Topics.orphanVisits() ]); diff --git a/src/visits/ShortUrlVisits.tsx b/src/visits/ShortUrlVisits.tsx index a65df735..3d71b8cf 100644 --- a/src/visits/ShortUrlVisits.tsx +++ b/src/visits/ShortUrlVisits.tsx @@ -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(({ ); -}, ({ match }) => `https://shlink.io/new-visit/${match.params.shortCode}`); +}, ({ match }) => [ Topics.shortUrlVisits(match.params.shortCode) ]); export default ShortUrlVisits; diff --git a/src/visits/TagVisits.tsx b/src/visits/TagVisits.tsx index acf60a9e..d6772682 100644 --- a/src/visits/TagVisits.tsx +++ b/src/visits/TagVisits.tsx @@ -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(({ ); -}, () => 'https://shlink.io/new-visit'); +}, () => [ Topics.visits() ]); export default TagVisits; diff --git a/test/mercure/helpers/index.test.tsx b/test/mercure/helpers/index.test.tsx index 2fb971b4..f4a3d187 100644 --- a/test/mercure/helpers/index.test.tsx +++ b/test/mercure/helpers/index.test.tsx @@ -20,7 +20,7 @@ describe('helpers', () => { [ Mock.of({ loading: false, error: false, mercureHubUrl: undefined }) ], [ Mock.of({ 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: {