diff --git a/package-lock.json b/package-lock.json
index 915d59e6..9bf1d07c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -114,6 +114,7 @@
"@emotion/react": "^11.14.0",
"@eslint/js": "^10.0.1",
"@faker-js/faker": "^9.0.1",
+ "@flamefrontend/sse-runtime-react": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.6",
@@ -1843,6 +1844,26 @@
"npm": ">=9.0.0"
}
},
+ "node_modules/@flamefrontend/sse-runtime-core": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@flamefrontend/sse-runtime-core/-/sse-runtime-core-0.10.0.tgz",
+ "integrity": "sha512-TUd7Uv/NkYI0qCv2EzsCRnCLqBRxpMJDc4CZVUcpxIZ6ol8gOW8SAvIm3COyRrqkDCZjOX6hHoQjtYIiOuA6OQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@flamefrontend/sse-runtime-react": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@flamefrontend/sse-runtime-react/-/sse-runtime-react-0.7.0.tgz",
+ "integrity": "sha512-djPvMmeNTLRS+78vDjSh6iVRiPg92f9KJtZ9d1YAmlwnzxIjg80VlyUAcOOjb0xhfMNkkDcG+v+iLVaqkgIY7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@flamefrontend/sse-runtime-core": "0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^18.3.0 || ^19.0.0"
+ }
+ },
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
diff --git a/package.json b/package.json
index 5c3a60c4..324dce7e 100644
--- a/package.json
+++ b/package.json
@@ -153,6 +153,7 @@
"@emotion/react": "^11.14.0",
"@eslint/js": "^10.0.1",
"@faker-js/faker": "^9.0.1",
+ "@flamefrontend/sse-runtime-react": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.6",
diff --git a/src/backend/server/api.ts b/src/backend/server/api.ts
index fb6ac4cd..34abd02e 100644
--- a/src/backend/server/api.ts
+++ b/src/backend/server/api.ts
@@ -147,15 +147,31 @@ export const setupApi = (app: Express, logger: Logger, appLoggerStream: PassThro
});
app.get('/api/events', async (req, res) => {
+ const {
+ query: {
+ next: nextQs
+ }
+ } = req;
+
+ const isNextapi = nextQs === 'true';
+
const session = await bsseDef.createSession(req, res);
scrobbleSources.emitter.on('*', (payload: any, eventName: string) => {
if(payload.from !== undefined) {
- session.push({event: eventName, ...payload}, payload.from);
+ if(isNextapi) {
+ session.push({event: eventName, ...payload}, eventName);
+ } else {
+ session.push({event: eventName, ...payload}, payload.from);
+ }
}
});
scrobbleClients.emitter.on('*', (payload: any, eventName: string) => {
if(payload.from !== undefined) {
- session.push({event: eventName, ...payload}, payload.from);
+ if(isNextapi) {
+ session.push({event: eventName, ...payload}, eventName);
+ } else {
+ session.push({event: eventName, ...payload}, payload.from);
+ }
}
});
});
diff --git a/src/client/AppNext.tsx b/src/client/AppNext.tsx
index eb3e0448..1a9b9081 100644
--- a/src/client/AppNext.tsx
+++ b/src/client/AppNext.tsx
@@ -13,14 +13,12 @@ import Dashboard from "./dashboard/dashboard";
import RecentPage from "./recent/RecentPage";
import ScrobbledPage from "./scrobbled/ScrobbledPage";
import DeadPage from "./deadLetter/DeadPage";
-import {clientUpdate, sourceUpdate} from "./status/ducks";
-import {useEventSource, useEventSourceListener} from "@react-nano/use-event-source";
import Version from "./Version";
import { MSComponentList, MSComponentListFetchable } from './components/msComponent/MSComponentList';
import { Provider } from './components/Provider';
-import { Container } from '@chakra-ui/react';
-import { useQueryClient } from '@tanstack/react-query'
-import { ComponentsApiJson, ComponentSourceApiJson } from '../core/Api';
+import { Container, Box, Center } from '@chakra-ui/react';
+import { MsSseEvent } from '../core/Api';
+import { SSEProvider } from "@flamefrontend/sse-runtime-react";
function NoMatch() {
const location = useLocation();
@@ -61,7 +59,7 @@ function MissingDocs() {
const routes: RouteObject[] = [
{
path: "/next",
- element: ,
+ element:
,
},
{
path: "/docs",
@@ -80,84 +78,18 @@ const genRouter = () => {
const router = genRouter();
-// const mapDispatchToProps = (dispatch) => {
-// return {
-// updateSource: (payload) => dispatch(sourceUpdate(payload)),
-// updateClient: (payload) => dispatch(clientUpdate(payload))
-// }
-// }
-
-// const connector = connect(null, mapDispatchToProps);
-
-// type PropsFromRedux = ConnectedProps;
-
-// const Global = (props: PropsFromRedux) => {
-// const {
-// updateSource,
-// updateClient
-// } = props;
-
-// const [sourceEventSource, eventSourceStatus] = useEventSource("api/events", false);
-// useEventSourceListener(sourceEventSource, ['source', 'client'], evt => {
-// const data = JSON.parse(evt.data);
-// if(data.from === 'source') {
-// updateSource(data);
-// } else if(data.from === 'client') {
-// updateClient(data);
-// }
-// }, [updateSource, updateClient]);
-
-// return ;
-// }
-
-// const ConnectedGlobal = connector(Global);
-
-const Global = (props) => {
-
- const queryClient = useQueryClient();
- const [sourceEventSource, eventSourceStatus] = useEventSource("/api/events", false);
- useEventSourceListener(sourceEventSource, ['source', 'client'], evt => {
- const data = JSON.parse(evt.data);
- if(data.event === 'playerUpdate') {
-
- console.log(`Updated => Component ${data.componentId} Player ${data.data.platformId}`);
- queryClient.setQueryData(['components', data.componentId, 'players', data.data.platformId], data.data);
-
- // queryClient.setQueryData(['components'], (old: ComponentsApiJson[]) => {
- // const componentIndex = old.findIndex(x => x.id === data.componentId);
- // if(componentIndex !== -1) {
- // console.log(`Updated => Component ${data.componentId} Player ${data.data.platformId}`);
- // //(old[componentIndex] as ComponentSourceApiJson).players[data.data.platformId] = data.data;
- // const newData = [...old];
- // newData[componentIndex] = {
- // ...old[componentIndex],
- // players: {
- // // @ts-ignore
- // ...old[componentIndex].players,
- // [data.data.platformId]: data.data
- // }
- // }
- // return newData;
- // }
- // //console.log(old);
- // });
- }
- if(data.from === 'source') {
- console.log(data);
- } else if(data.from === 'client') {
- console.log(data);
- }
- }, []);
-
- return ;
+const sseProviderOptions = {
+ key: ['events'],
+ url: '/api/events?next=true'
}
function App() {
return (
-
+ options={sseProviderOptions}>
+
);
diff --git a/src/client/components/chakraPlayer/Player.tsx b/src/client/components/chakraPlayer/Player.tsx
index 5b8039f2..99bc7641 100644
--- a/src/client/components/chakraPlayer/Player.tsx
+++ b/src/client/components/chakraPlayer/Player.tsx
@@ -8,6 +8,11 @@ import { QueryFunctionContext, queryOptions, useQuery, useQueryClient } from '@t
import { ErrorAlert } from "../ErrorAlert";
import ky from 'ky';
import { baseUrl } from "../../utils";
+import {
+ useSSEContext,
+ useSSEEvent,
+} from "@flamefrontend/sse-runtime-react";
+import { MsSseEvent, MsSseEventPayload } from "../../../core/Api";
export interface PlayerProps {
data: SourcePlayerJson
@@ -102,11 +107,19 @@ export const ChakraPlayerFetchable = (props: ChakraPlayerFetchableProps) => {
sot
} = props;
const queryClient = useQueryClient();
+ const qKey = ['components', componentId, 'players', platformId];
useEffect(() => {
- if(initData !== undefined) {
+ if (initData !== undefined && queryClient.getQueryData(qKey) === undefined) {
queryClient.setQueryData(['components', componentId, 'players', platformId], initData);
}
- },[initData]);
+ }, [initData]);
+
+ const client = useSSEContext>>();
+ useSSEEvent(client, "playerUpdate", (payload) => {
+ if (payload.componentId === componentId && payload.data.platformId === platformId) {
+ queryClient.setQueryData(['components', componentId, 'players', platformId], payload.data);
+ }
+ });
const { isPending, isError, data, error } = useQuery({
queryKey: ['components', componentId, 'players', platformId],
@@ -115,14 +128,12 @@ export const ChakraPlayerFetchable = (props: ChakraPlayerFetchableProps) => {
staleTime: Infinity,
});
- if(isError) {
- return
+ if (isError) {
+ return
}
- console.log('rendering player');
-
- if(!isPending) {
- return
+ if (!isPending) {
+ return
}
}
diff --git a/src/client/components/msComponent/MSComponentList.tsx b/src/client/components/msComponent/MSComponentList.tsx
index dc9cdd6d..c3e3dcaa 100644
--- a/src/client/components/msComponent/MSComponentList.tsx
+++ b/src/client/components/msComponent/MSComponentList.tsx
@@ -1,7 +1,7 @@
import React, { ComponentProps, useMemo, useState, forwardRef, Fragment } from "react"
import { Stack, SegmentGroup, Text, Box, Center, Card, SkeletonText } from '@chakra-ui/react';
import { ComponentsApiJson } from "../../../core/Api";
-import { MSComponentSummary } from "./MSComponentSummary";
+import { MSComponentSummary, MSComponentSummaryFetchable } from "./MSComponentSummary";
import { QueryFunctionContext, queryOptions, useQuery } from '@tanstack/react-query';
import ky from 'ky';
import { baseUrl } from "../../utils";
@@ -9,6 +9,7 @@ import { ErrorAlert } from "../ErrorAlert";
export interface ComponentListProps {
components: ComponentsApiJson[]
+ fetchable?: boolean
}
export const MSComponentList = (props: ComponentListProps) => {
@@ -31,7 +32,7 @@ export const MSComponentList = (props: ComponentListProps) => {
return x.mode === 'source';
}
return x.mode === 'client';
- }).map(x => )}
+ }).map(x => props.fetchable ? : )}
)
@@ -56,7 +57,7 @@ export const MSComponentListFetchable = () => {
return
}
- return
+ return
}
type ComponentListQueryKey = ['components'];
diff --git a/src/client/components/msComponent/MSComponentSummary.tsx b/src/client/components/msComponent/MSComponentSummary.tsx
index 31701d31..8a3380c4 100644
--- a/src/client/components/msComponent/MSComponentSummary.tsx
+++ b/src/client/components/msComponent/MSComponentSummary.tsx
@@ -1,6 +1,6 @@
-import React, { ComponentProps, useMemo, forwardRef, Fragment } from "react"
+import React, { ComponentProps, useMemo, forwardRef, Fragment, useEffect } from "react"
import { Accordion, For, Span, Stack, Text, Box, Heading, AbsoluteCenter, Button, Separator, HStack, Flex, Badge, IconButton, Container, Collapsible, Card, LinkOverlay, LinkBox } from '@chakra-ui/react';
-import { ComponentCommonApi, ComponentCommonApiJson, isComponentClientApiJson, isComponentSourceApiJson } from "../../../core/Api.js";
+import { ComponentClientApiJson, ComponentCommonApi, ComponentCommonApiJson, ComponentSourceApiJson, isComponentClientApiJson, isComponentSourceApiJson, MsSseEvent } from "../../../core/Api.js";
import { TextMuted } from "../TextMuted.js";
import { isClientType } from "../../../backend/common/infrastructure/Atomic.js";
import { capitalize } from "../../../core/StringUtils.js";
@@ -8,10 +8,20 @@ import { ShortDateDisplay } from "../DateDisplay.js";
import { ChevronRightButton } from "../icons/ChakraIcons.js";
import { ChakraPlayer, ChakraPlayerFetchable } from "../chakraPlayer/Player.js";
import { InfoTip } from "../ToggleTip.js";
+import { QueryFunctionContext, queryOptions, useQuery, useQueryClient } from '@tanstack/react-query';
+import { ErrorAlert } from "../ErrorAlert";
+import ky from 'ky';
+import { baseUrl } from "../../utils";
+import {
+ useSSEContext,
+ useSSEEvent,
+ useSSEAnyEvent
+} from "@flamefrontend/sse-runtime-react";
-export const MSComponentSummary = (props: { data: ComponentCommonApiJson }) => {
+export const MSComponentSummary = (props: { data: ComponentCommonApiJson, fetchable?: boolean }) => {
const {
- data
+ data,
+ fetchable
} = props;
const isClient = isClientType(data.type);
@@ -27,7 +37,11 @@ export const MSComponentSummary = (props: { data: ComponentCommonApiJson }) => {
body = (
{
- Object.entries(players).map(([key, x]) => )
+ Object.entries(players).map(([key, x]) => (
+
+ {fetchable ? : }
+
+ ))
}
);
@@ -120,4 +134,77 @@ const StateBadge = (props: ComponentProps & { data: ComponentCommo
}
return {badgeText}
+}
+
+
+
+export const MSComponentSummaryFetchable = (props: {componentId: number, data: ComponentCommonApiJson}) => {
+ const {
+ componentId,
+ data: initData
+ } = props;
+ const queryClient = useQueryClient();
+ const qKey = ['components', componentId, 'summary'];
+ useEffect(() => {
+ if (initData !== undefined && queryClient.getQueryData(qKey) === undefined) {
+ queryClient.setQueryData(['components', componentId, 'summary'], initData);
+ }
+ }, [initData]);
+
+ const client = useSSEContext();
+ useSSEAnyEvent(client, (payload) => {
+ if('componentId' in (payload.data as object) && (payload.data as Record).componentId === componentId) {
+ switch(payload.type) {
+ case 'discovered':
+ queryClient.setQueryData(['components', componentId, 'summary'], (old: ComponentSourceApiJson) => {
+ return {
+ ...old,
+ tracksDiscovered: old.tracksDiscovered + 1
+ }
+ });
+ break;
+ case 'scrobbleQueued':
+ queryClient.setQueryData(['components', componentId, 'summary'], (old: ComponentClientApiJson) => {
+ let newData: ComponentClientApiJson = {...old};
+ newData.queued = old.queued + 1;
+ return newData;
+ });
+ break;
+ case 'scrobbleDequeued':
+ queryClient.setQueryData(['components', componentId, 'summary'], (old: ComponentClientApiJson) => {
+ let newData: ComponentClientApiJson = {...old};
+ newData.queued = old.queued - 1;
+ return newData;
+ });
+ break;
+ case 'scrobble':
+ queryClient.setQueryData(['components', componentId, 'summary'], (old: ComponentClientApiJson) => {
+ let newData: ComponentClientApiJson = {...old};
+ newData.countLive = old.countLive + 1;
+ return newData;
+ });
+ break;
+ }
+ }
+ });
+
+ const { isPending, isError, data, error } = useQuery({
+ queryKey: ['components', componentId, 'summary'],
+ queryFn: queryFn,
+ structuralSharing: false,
+ staleTime: Infinity,
+ });
+
+ if (isError) {
+ return
+ }
+
+ if(!isPending) {
+ return
+ }
+}
+
+type ComponentSummaryQueryKey = ['components', number, 'summary'];
+const queryFn = async (context: QueryFunctionContext) => {
+ return {} as ComponentCommonApiJson;
}
\ No newline at end of file
diff --git a/src/core/Api.ts b/src/core/Api.ts
index bb64fce8..56a3647b 100644
--- a/src/core/Api.ts
+++ b/src/core/Api.ts
@@ -89,4 +89,21 @@ export const isComponentSourceApiJson = (data: ComponentCommonApiJson): data is
export const isComponentClientApiJson = (data: ComponentCommonApiJson): data is ComponentClientApiJson => {
return data.mode === 'client';
+}
+
+export type MsSseEventPayload> = {
+ type: SourceType | ClientType
+ name: string
+ componentId: number
+ from: 'source' | 'client'
+ data: T
+ event: string
+}
+
+export type MsSseEvent> = {
+ playerUpdate: MsSseEventPayload
+ discovered: MsSseEventPayload
+ scrobbleQueued: MsSseEventPayload
+ scrobbleDequeued: MsSseEventPayload
+ client: T
}
\ No newline at end of file