diff --git a/apps/client/src/features/event-editor/EventEditor.tsx b/apps/client/src/features/event-editor/EventEditor.tsx
index 3068d2e512..2c5e185690 100644
--- a/apps/client/src/features/event-editor/EventEditor.tsx
+++ b/apps/client/src/features/event-editor/EventEditor.tsx
@@ -3,6 +3,7 @@ import { Button, Select, Switch } from '@chakra-ui/react';
import { IoBan } from '@react-icons/all-files/io5/IoBan';
import { useAtom } from 'jotai';
import { OntimeEvent, TimerType } from 'ontime-types';
+import { millisToString } from 'ontime-utils';
import { editorEventId } from '../../common/atoms/LocalEventSettings';
import CopyTag from '../../common/components/copy-tag/CopyTag';
@@ -14,7 +15,6 @@ import useRundown from '../../common/hooks-query/useRundown';
import { useEmitLog } from '../../common/stores/logger';
import { millisToMinutes } from '../../common/utils/dateConfig';
import getDelayTo from '../../common/utils/getDelayTo';
-import { stringFromMillis } from '../../common/utils/time';
import { calculateDuration, TimeEntryField, validateEntry } from '../../common/utils/timesManager';
import style from './EventEditor.module.scss';
@@ -121,8 +121,8 @@ export default function EventEditor() {
const delayed = delay !== 0;
const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null;
- const newStart = delayed ? `New start ${stringFromMillis(event.timeStart + delay)}` : null;
- const newEnd = delayed ? `New end ${stringFromMillis(event.timeEnd + delay)}` : null;
+ const newStart = delayed ? `New start ${millisToString(event.timeStart + delay)}` : null;
+ const newEnd = delayed ? `New end ${millisToString(event.timeEnd + delay)}` : null;
return (
diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx
index cf5c2a3cca..0d2c24f612 100644
--- a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx
+++ b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx
@@ -1,11 +1,11 @@
import { useCallback } from 'react';
+import { millisToString } from 'ontime-utils';
import PropTypes from 'prop-types';
import { useEmitLog } from '@/common/stores/logger';
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import { millisToMinutes } from '../../../../common/utils/dateConfig';
-import { stringFromMillis } from '../../../../common/utils/time';
import { validateEntry } from '../../../../common/utils/timesManager';
import style from '../EventBlock.module.scss';
@@ -15,7 +15,7 @@ export default function EventBlockTimers(props) {
const { emitWarning } = useEmitLog();
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
- const newTime = stringFromMillis(timeStart + delay);
+ const newTime = millisToString(timeStart + delay);
/**
* @description Validates a time input against its pair
diff --git a/apps/client/src/features/table/columns.jsx b/apps/client/src/features/table/columns.jsx
index 52792e9956..081a01a3b4 100644
--- a/apps/client/src/features/table/columns.jsx
+++ b/apps/client/src/features/table/columns.jsx
@@ -1,10 +1,9 @@
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
-import { stringFromMillis } from '../../common/utils/time.js';
-
import EditableCell from './tableElements/EditableCell';
import style from './Table.module.scss';
+import { millisToString } from 'ontime-utils';
/**
* React - Table column object
@@ -22,19 +21,19 @@ export const makeColumns = (sizes, userFields) => {
{
Header: 'Start',
accessor: 'timeStart',
- Cell: ({ cell: { value, delayed } }) => stringFromMillis(delayed || value),
+ Cell: ({ cell: { value, delayed } }) => millisToString(delayed || value),
width: sizes?.timeStart || 90,
},
{
Header: 'End',
accessor: 'timeEnd',
- Cell: ({ cell: { value, delayed } }) => stringFromMillis(delayed || value),
+ Cell: ({ cell: { value, delayed } }) => millisToString(delayed || value),
width: sizes?.timeEnd || 90,
},
{
Header: 'Duration',
accessor: 'duration',
- Cell: ({ cell: { value } }) => stringFromMillis(value),
+ Cell: ({ cell: { value } }) => millisToString(value),
width: sizes?.duration || 90,
},
{ Header: 'Title', accessor: 'title', width: sizes?.title || 400 },
diff --git a/apps/client/src/features/table/utils.js b/apps/client/src/features/table/utils.js
index 28d9c5cd54..1d9968e17d 100644
--- a/apps/client/src/features/table/utils.js
+++ b/apps/client/src/features/table/utils.js
@@ -1,4 +1,5 @@
import { stringify } from 'csv-stringify/browser/esm/sync';
+import { millisToString } from 'ontime-utils';
/**
* @description parses a field for export
@@ -6,14 +7,13 @@ import { stringify } from 'csv-stringify/browser/esm/sync';
* @param {*} data
* @return {string}
*/
-import { stringFromMillis } from '../../common/utils/time';
export const parseField = (field, data) => {
let val;
switch (field) {
case 'timeStart':
case 'timeEnd':
- val = stringFromMillis(data);
+ val = millisToString(data);
break;
case 'isPublic':
val = data ? 'x' : '';
diff --git a/apps/client/src/features/viewers/studio/StudioClock.jsx b/apps/client/src/features/viewers/studio/StudioClock.jsx
index fc64b2c9c0..35e323cf1d 100644
--- a/apps/client/src/features/viewers/studio/StudioClock.jsx
+++ b/apps/client/src/features/viewers/studio/StudioClock.jsx
@@ -10,9 +10,10 @@ import useFitText from '../../../common/hooks/useFitText';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { formatDisplay } from '../../../common/utils/dateConfig';
import { formatEventList, getEventsWithDelay, trimEventlist } from '../../../common/utils/eventsManager';
-import { formatTime, stringFromMillis } from '../../../common/utils/time';
+import { formatTime } from '../../../common/utils/time';
import './StudioClock.scss';
+import { millisToString } from 'ontime-utils';
const formatOptions = {
showSeconds: false,
@@ -68,7 +69,7 @@ export default function StudioClock(props) {
}, [backstageEvents, nextId, selectedId]);
const clock = formatTime(time.clock, formatOptions);
- const [, , secondsNow] = stringFromMillis(time.clock).split(':');
+ const [, , secondsNow] = millisToString(time.clock).split(':');
const isNegative = (time.current ?? 0) < 0;
return (
diff --git a/apps/server/src/utils/time.js b/apps/server/src/utils/time.js
index e4cedadd35..0ad2124517 100644
--- a/apps/server/src/utils/time.js
+++ b/apps/server/src/utils/time.js
@@ -26,37 +26,6 @@ export const isTimeString = (string) => {
return regex.test(string);
};
-/**
- * @description Converts milliseconds to string representing time
- * @param {number} ms - time in milliseconds
- * @param {boolean} showSeconds - weather to show the seconds
- * @param {string} delim - character between HH MM SS
- * @param {string} ifNull - what to return if value is null
- * @returns {string} String representing time 00:12:02
- */
-
-export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '...') => {
- if (ms == null || isNaN(ms)) return ifNull;
- const isNegative = ms < 0 ? '-' : '';
- const millis = Math.abs(ms);
-
- /**
- * @description ensures value is double digit
- * @param value
- * @return {string|*}
- */
- const showWith0 = (value) => (value < 10 ? `0${value}` : value);
- const hours = showWith0(Math.floor(((millis / mth) % 60) % 24));
- const minutes = showWith0(Math.floor((millis / mtm) % 60));
- const seconds = showWith0(Math.floor((millis / mts) % 60));
-
- return showSeconds
- ? `${isNegative}${
- parseInt(hours, 10) ? `${hours}${delim}` : `00${delim}`
- }${minutes}${delim}${seconds}`
- : `${isNegative}${parseInt(hours, 10) ? `${hours}` : '00'}${delim}${minutes}`;
-};
-
/**
* @description Converts an excel date to milliseconds
* @argument {string} date - excel string date
diff --git a/packages/utils/index.ts b/packages/utils/index.ts
index f7ea126caa..dbf9e00edf 100644
--- a/packages/utils/index.ts
+++ b/packages/utils/index.ts
@@ -1 +1,2 @@
+export { millisToString } from './src/date-utils/millisToString.js';
export { generateId } from './src/generate-id/generateId.js';
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 84ca4d9c04..02774b23e7 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -12,9 +12,11 @@
"cleanup": "rm -rf .turbo && rm -rf node_modules"
},
"dependencies": {
+ "luxon": "^3.3.0",
"nanoid": "^4.0.0"
},
"devDependencies": {
+ "@types/luxon": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.31.0",
diff --git a/packages/utils/src/date-utils/millisToString.test.ts b/packages/utils/src/date-utils/millisToString.test.ts
new file mode 100644
index 0000000000..915d6c71aa
--- /dev/null
+++ b/packages/utils/src/date-utils/millisToString.test.ts
@@ -0,0 +1,71 @@
+import { expect } from 'vitest';
+
+import { millisToString } from './millisToString';
+
+describe('millisToString()', () => {
+ it('returns fallback if millis is null', () => {
+ const fallback = 'testFallback';
+ expect(millisToString(null, true, fallback)).toBe(fallback);
+ });
+
+ it('returns 00:00:00 if 0 is passed', () => {
+ expect(millisToString(0)).toBe('00:00:00');
+ });
+
+ it('shows negative timers', () => {
+ const testScenarios = [
+ { millis: -300, expected: '-00:00:00' },
+ { millis: -1000, expected: '-00:00:01' },
+ { millis: -1500, expected: '-00:00:01' },
+ { millis: -60000, expected: '-00:01:00' },
+ { millis: -600000, expected: '-00:10:00' },
+ { millis: -3600000, expected: '-01:00:00' },
+ { millis: -36000000, expected: '-10:00:00' },
+ { millis: -86399000, expected: '-23:59:59' },
+ { millis: -86400000, expected: '-00:00:00' },
+ { millis: -86401000, expected: '-00:00:01' },
+ ];
+
+ testScenarios.forEach((scenario) => {
+ expect(millisToString(scenario.millis)).toBe(scenario.expected);
+ });
+ });
+
+ test('random properties', () => {
+ const testScenarios = [
+ { millis: 300, expected: '00:00:00' },
+ { millis: 1000, expected: '00:00:01' },
+ { millis: 1500, expected: '00:00:01' },
+ { millis: 60000, expected: '00:01:00' },
+ { millis: 600000, expected: '00:10:00' },
+ { millis: 3600000, expected: '01:00:00' },
+ { millis: 36000000, expected: '10:00:00' },
+ { millis: 86399000, expected: '23:59:59' },
+ { millis: 86400000, expected: '00:00:00' },
+ { millis: 86401000, expected: '00:00:01' },
+ ];
+
+ testScenarios.forEach((scenario) => {
+ expect(millisToString(scenario.millis)).toBe(scenario.expected);
+ });
+ });
+
+ test('random properties without seconds', () => {
+ const testScenarios = [
+ { millis: 300, expected: '00:00' },
+ { millis: 1000, expected: '00:00' },
+ { millis: 1500, expected: '00:00' },
+ { millis: 60000, expected: '00:01' },
+ { millis: 600000, expected: '00:10' },
+ { millis: 3600000, expected: '01:00' },
+ { millis: 36000000, expected: '10:00' },
+ { millis: 86399000, expected: '23:59' },
+ { millis: 86400000, expected: '00:00' },
+ { millis: 86401000, expected: '00:00' },
+ ];
+
+ testScenarios.forEach((scenario) => {
+ expect(millisToString(scenario.millis, false)).toBe(scenario.expected);
+ });
+ });
+});
diff --git a/packages/utils/src/date-utils/millisToString.ts b/packages/utils/src/date-utils/millisToString.ts
new file mode 100644
index 0000000000..7820eba17c
--- /dev/null
+++ b/packages/utils/src/date-utils/millisToString.ts
@@ -0,0 +1,19 @@
+import { DateTime } from 'luxon';
+
+/**
+ * @description Converts milliseconds to string representing time
+ * @param {number | null} millis - time in milliseconds
+ * @param {boolean} showSeconds - weather to show the seconds
+ * @param {string} fallback - what to return if value is null
+ * @returns {string} String representing time 00:12:02
+ */
+export function millisToString(millis: number | null, showSeconds = true, fallback = '...') {
+ if (millis === null) {
+ return fallback;
+ }
+
+ const isNegative = millis < 0;
+
+ const format = `HH:mm${showSeconds ? ':ss' : ''}`;
+ return `${isNegative ? '-' : ''}${DateTime.fromMillis(Math.abs(millis)).toUTC().toFormat(format)}`;
+}
From 93783c49aa6ca5a30f42612a7b67eaff7f7aeafe Mon Sep 17 00:00:00 2001
From: cv <34649812+cpvalente@users.noreply.github.com>
Date: Sat, 11 Mar 2023 21:56:48 +0100
Subject: [PATCH 14/20] wip: playback
---
.../src/features/control/playback/PlaybackControl.tsx | 2 +-
.../src/features/control/playback/PlaybackDisplay.tsx | 7 ++++++-
apps/server/src/services/PlaybackService.ts | 10 +++++-----
3 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/apps/client/src/features/control/playback/PlaybackControl.tsx b/apps/client/src/features/control/playback/PlaybackControl.tsx
index 398111c7f0..4277e16187 100644
--- a/apps/client/src/features/control/playback/PlaybackControl.tsx
+++ b/apps/client/src/features/control/playback/PlaybackControl.tsx
@@ -8,7 +8,7 @@ import PlaybackTimer from './PlaybackTimer';
import style from './PlaybackControl.module.scss';
export default function PlaybackControl() {
- const { data } = usePlaybackControl();
+ const data = usePlaybackControl();
return (
diff --git a/apps/client/src/features/control/playback/PlaybackDisplay.tsx b/apps/client/src/features/control/playback/PlaybackDisplay.tsx
index 01aec4dfeb..d2000022ac 100644
--- a/apps/client/src/features/control/playback/PlaybackDisplay.tsx
+++ b/apps/client/src/features/control/playback/PlaybackDisplay.tsx
@@ -42,7 +42,12 @@ export default function PlaybackDisplay(props: PlaybackProps) {
- setPlayback.roll()} disabled={noEvents} theme={Playback.Roll} active={isRolling}>
+ setPlayback.roll()}
+ disabled={!isStopped || noEvents}
+ theme={Playback.Roll}
+ active={isRolling}
+ >
diff --git a/apps/server/src/services/PlaybackService.ts b/apps/server/src/services/PlaybackService.ts
index 0e1705d1b2..ca3929afd7 100644
--- a/apps/server/src/services/PlaybackService.ts
+++ b/apps/server/src/services/PlaybackService.ts
@@ -119,7 +119,7 @@ export class PlaybackService {
* Starts playback on selected event
*/
static start() {
- if (eventTimer.loadedTimerId) {
+ if (eventTimer.playback === Playback.Armed || eventTimer.playback === Playback.Pause) {
eventTimer.start();
const newState = eventTimer.playback;
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
@@ -130,7 +130,7 @@ export class PlaybackService {
* Pauses playback on selected event
*/
static pause() {
- if (eventTimer.loadedTimerId) {
+ if (eventTimer.playback === Playback.Play) {
eventTimer.pause();
const newState = eventTimer.playback;
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
@@ -141,7 +141,7 @@ export class PlaybackService {
* Stops timer and unloads any events
*/
static stop() {
- if (eventTimer.loadedTimerId || eventTimer.playback === Playback.Roll) {
+ if (eventTimer.playback !== Playback.Stop) {
eventLoader.reset();
eventTimer.stop();
const newState = eventTimer.playback;
@@ -167,14 +167,14 @@ export class PlaybackService {
// nothing to play
if (rollTimers === null) {
- logger.error('SERVER', 'Roll: no events found');
+ logger.warning('SERVER', 'Roll: no events found');
PlaybackService.stop();
return;
}
const { currentEvent, nextEvent, timers } = rollTimers;
if (!currentEvent && !nextEvent) {
- logger.error('SERVER', 'Roll: no events found');
+ logger.warning('SERVER', 'Roll: no events found');
PlaybackService.stop();
return;
}
From 1809e65fa7592d52c04a5fb035a9de0923ab9874 Mon Sep 17 00:00:00 2001
From: cv <34649812+cpvalente@users.noreply.github.com>
Date: Sat, 11 Mar 2023 22:10:51 +0100
Subject: [PATCH 15/20] feat: implement websocket
---
apps/client/package.json | 4 +-
apps/client/src/App.tsx | 5 +-
apps/client/src/common/api/apiConstants.ts | 11 +-
apps/client/src/common/hooks/useSocket.ts | 185 ++++++++----------
.../src/common/hooks/useSubscription.tsx | 22 ---
apps/client/src/common/stores/createStore.ts | 1 +
apps/client/src/common/stores/runtime.ts | 77 ++++++++
apps/client/src/common/utils/socket.ts | 151 ++++++++++++--
apps/client/src/common/utils/wss.ts | 15 --
apps/client/src/features/rundown/Rundown.tsx | 2 +-
apps/server/src/adapters/WebsocketAdapter.ts | 16 +-
apps/server/src/app.ts | 18 +-
apps/server/src/classes/Logger.ts | 39 ++--
.../src/controllers/integrationController.ts | 32 +--
apps/server/src/stores/EventStore.ts | 14 +-
pnpm-lock.yaml | 25 ++-
16 files changed, 381 insertions(+), 236 deletions(-)
delete mode 100644 apps/client/src/common/hooks/useSubscription.tsx
create mode 100644 apps/client/src/common/stores/runtime.ts
delete mode 100644 apps/client/src/common/utils/wss.ts
diff --git a/apps/client/package.json b/apps/client/package.json
index 5014c95ab3..cc27befce2 100644
--- a/apps/client/package.json
+++ b/apps/client/package.json
@@ -18,9 +18,10 @@
"axios": "^1.2.0",
"color": "^4.2.3",
"csv-stringify": "^6.2.3",
+ "deepmerge": "^4.3.0",
"framer-motion": "^8.0.2",
"jotai": "^1.10.0",
- "luxon": "^3.1.0",
+ "luxon": "^3.3.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0",
@@ -63,6 +64,7 @@
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^14.1.1",
"@types/color": "^3.0.3",
+ "@types/luxon": "^3.2.0",
"@types/prop-types": "^15.7.5",
"@types/react": "^18.0.26",
"@types/react-beautiful-dnd": "^13.1.3",
diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx
index 1a6a6a3883..d2b0c1daa6 100644
--- a/apps/client/src/App.tsx
+++ b/apps/client/src/App.tsx
@@ -8,17 +8,18 @@ import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
import { AppContextProvider } from './common/context/AppContext';
import useElectronEvent from './common/hooks/useElectronEvent';
import { ontimeQueryClient } from './common/queryClient';
+import { connectSocket } from './common/utils/socket';
import theme from './theme/theme';
import AppRouter from './AppRouter';
-// import { useSyncExternalLogger } from './common/stores/logger';
// Load Open Sans typeface
// @ts-expect-error no types from font import
import('typeface-open-sans');
+connectSocket();
+
function App() {
const { isElectron, sendToElectron } = useElectronEvent();
- // useSyncExternalLogger();
const handleKeyPress = (event: KeyboardEvent) => {
// handle held key
diff --git a/apps/client/src/common/api/apiConstants.ts b/apps/client/src/common/api/apiConstants.ts
index 287e4d8009..bb77f60913 100644
--- a/apps/client/src/common/api/apiConstants.ts
+++ b/apps/client/src/common/api/apiConstants.ts
@@ -10,14 +10,7 @@ export const APP_INFO = ['appinfo'];
export const OSC_SETTINGS = ['oscSettings'];
export const APP_SETTINGS = ['appSettings'];
export const VIEW_SETTINGS = ['viewSettings'];
-
-// websocket stuff
-export const FEAT_CUESHEET = 'feat-cuesheet';
-export const FEAT_INFO = 'feat-info';
-export const FEAT_MESSAGECONTROL = 'feat-messagecontrol';
-export const FEAT_PLAYBACKCONTROL = 'feat-playbackcontrol';
-export const FEAT_RUNDOWN = 'feat-rundown';
-export const TIMER = 'timer';
+export const RUNTIME = ['runtimeStore'];
/**
* @description finds server path given the current location, it
@@ -26,6 +19,8 @@ export const TIMER = 'timer';
export const calculateServer = () => (import.meta.env.DEV ? `http://localhost:${STATIC_PORT}` : window.location.origin);
export const serverURL = calculateServer();
+export const websocketUrl = `ws://${window.location.hostname}:${STATIC_PORT}/ws`;
+
export const eventURL = `${serverURL}/eventdata`;
export const rundownURL = `${serverURL}/eventlist`;
export const ontimeURL = `${serverURL}/ontime`;
diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts
index 75b70f787f..56b354fc73 100644
--- a/apps/client/src/common/hooks/useSocket.ts
+++ b/apps/client/src/common/hooks/useSocket.ts
@@ -1,141 +1,110 @@
-import { useQuery } from '@tanstack/react-query';
-import { Playback } from 'ontime-types';
+import { useMemo } from 'react';
-import {
- FEAT_CUESHEET,
- FEAT_INFO,
- FEAT_MESSAGECONTROL,
- FEAT_PLAYBACKCONTROL,
- FEAT_RUNDOWN,
- TIMER,
-} from '../api/apiConstants';
-import { ontimeQueryClient as queryClient } from '../queryClient';
-import socket, { subscribeOnce } from '../utils/socket';
+import { useRuntimeStore } from '../stores/runtime';
+import { socketSendJson } from '../utils/socket';
-function createSocketHook
(key: string, defaultValue: T | null = null) {
- subscribeOnce(key, (data) => queryClient.setQueryData([key], data));
+export const useRundownEditor = () => {
+ const state = useRuntimeStore();
- // retrieves data from the cache or null if non-existent
- // we need the null because useQuery can't receive undefined
- const fetcher = () => (queryClient.getQueryData([key]) ?? defaultValue) as T | null;
-
- return () => useQuery({ queryKey: [key], queryFn: fetcher, placeholderData: defaultValue });
-}
-
-interface IRundown {
- selectedEventId: string | null;
- nextEventId: string | null;
- playback: Playback | null;
-}
-
-const emptyRundown: IRundown = {
- selectedEventId: null,
- nextEventId: null,
- playback: null,
+ return useMemo(() => {
+ return {
+ selectedEventId: state.loaded.selectedEventId,
+ nextEventId: state.loaded.nextEventId,
+ playback: state.playback,
+ };
+ }, [state.loaded.selectedEventId, state.loaded.nextEventId, state.playback]);
};
-export const useRundownEditor = createSocketHook(FEAT_RUNDOWN, emptyRundown);
-
-const emptyMessageControl = {
- messages: {
- presenter: {
- text: '',
- visible: false,
- },
- public: {
- text: '',
- visible: false,
- },
- lower: {
- text: '',
- visible: false,
- },
- },
- onAir: false,
+export const useMessageControl = () => {
+ const state = useRuntimeStore();
+ return useMemo(() => {
+ return {
+ timerMessage: state.timerMessage,
+ publicMessage: state.publicMessage,
+ lowerMessage: state.lowerMessage,
+ onAir: state.onAir,
+ };
+ }, [state.timerMessage, state.publicMessage, state.lowerMessage, state.onAir]);
};
-export const useMessageControl = createSocketHook(FEAT_MESSAGECONTROL, emptyMessageControl);
export const setMessage = {
- presenterText: (payload: string) => socket.emit('set-timer-message-text', payload),
- presenterVisible: (payload: boolean) => socket.emit('set-timer-message-visible', payload),
- publicText: (payload: string) => socket.emit('set-public-message-text', payload),
- publicVisible: (payload: boolean) => socket.emit('set-public-message-visible', payload),
- lowerText: (payload: string) => socket.emit('set-lower-message-text', payload),
- lowerVisible: (payload: boolean) => socket.emit('set-lower-message-visible', payload),
- onAir: (payload: boolean) => socket.emit('set-onAir', payload),
+ presenterText: (payload: string) => socketSendJson('set-timer-message-text', payload),
+ presenterVisible: (payload: boolean) => socketSendJson('set-timer-message-visible', payload),
+ publicText: (payload: string) => socketSendJson('set-public-message-text', payload),
+ publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload),
+ lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload),
+ lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload),
+ onAir: (payload: boolean) => socketSendJson('set-onAir', payload),
};
-export const emptyPlaybackControl = {
- playback: 'stop',
- numEvents: 0,
-};
-export const usePlaybackControl = createSocketHook(FEAT_PLAYBACKCONTROL, emptyPlaybackControl);
-export const resetPlayback = () => {
- const cacheData = queryClient.getQueryData([FEAT_PLAYBACKCONTROL]) as Record;
- queryClient.setQueryData([FEAT_PLAYBACKCONTROL], {
- ...cacheData,
- playback: 'stop',
- });
+export const usePlaybackControl = () => {
+ const state = useRuntimeStore();
+
+ return useMemo(() => {
+ return {
+ playback: state.playback,
+ numEvents: state.loaded.numEvents,
+ };
+ }, [state.playback, state.loaded.numEvents]);
};
+
export const setPlayback = {
- start: () => socket.emit('set-start'),
- pause: () => socket.emit('set-pause'),
- roll: () => socket.emit('set-roll'),
+ start: () => socketSendJson('start'),
+ pause: () => socketSendJson('pause'),
+ roll: () => socketSendJson('roll'),
previous: () => {
- socket.emit('set-previous');
+ socketSendJson('previous');
},
next: () => {
- socket.emit('set-next');
+ socketSendJson('next');
},
stop: () => {
- socket.emit('set-stop');
+ socketSendJson('stop');
},
reload: () => {
- socket.emit('set-reload');
+ socketSendJson('reload');
},
delay: (amount: number) => {
- socket.emit('set-delay', amount);
+ socketSendJson('delay', amount);
},
};
-export const emptyInfo = {
- titles: {
- titleNow: '',
- subtitleNow: '',
- presenterNow: '',
- noteNow: '',
- titleNext: '',
- subtitleNext: '',
- presenterNext: '',
- noteNext: '',
- },
- playback: 'stop',
- selectedEventIndex: null,
- numEvents: 0,
+export const useInfoPanel = () => {
+ const state = useRuntimeStore();
+
+ return useMemo(() => {
+ return {
+ titles: state.titles,
+ playback: state.playback,
+ selectedEventIndex: state.loaded.selectedEventIndex,
+ numEvents: state.loaded.numEvents,
+ };
+ }, [state.titles, state.playback, state.loaded.selectedEventIndex, state.loaded.numEvents]);
};
-export const useInfoPanel = createSocketHook(FEAT_INFO, emptyInfo);
+export const useCuesheet = () => {
+ const state = useRuntimeStore();
-export const emptyCuesheet = {
- selectedEventId: null,
- titleNow: '',
+ return useMemo(() => {
+ return {
+ selectedEventIndex: state.loaded.selectedEventId,
+ titleNow: state.titles.titleNow,
+ };
+ }, [state.loaded.selectedEventId, state.titles.titleNow]);
};
-export const useCuesheet = createSocketHook(FEAT_CUESHEET, emptyCuesheet);
-
export const setEventPlayback = {
- loadEvent: (eventId: string) => socket.emit('set-loadid', eventId),
- startEvent: (eventId: string) => socket.emit('set-startid', eventId),
- pause: () => socket.emit('set-pause'),
+ loadEvent: (eventId: string) => socketSendJson('loadid', eventId),
+ startEvent: (eventId: string) => socketSendJson('startid', eventId),
+ pause: () => socketSendJson('pause'),
};
-const emptyTimer = {
- clock: 0,
- current: 0,
- secondaryTimer: null,
- duration: null,
- startedAt: null,
- expectedFinish: null,
-};
+export const useTimer = () => {
+ const state = useRuntimeStore();
-export const useTimer = createSocketHook(TIMER, emptyTimer);
+ return useMemo(() => {
+ return {
+ timer: state.timer,
+ };
+ }, [state.timer]);
+};
diff --git a/apps/client/src/common/hooks/useSubscription.tsx b/apps/client/src/common/hooks/useSubscription.tsx
deleted file mode 100644
index cda26fa183..0000000000
--- a/apps/client/src/common/hooks/useSubscription.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import socket from '../utils/socket';
-
-export default function useSubscription(topic: string, initialState: T, requestString?: string) {
- const [state, setState] = useState(initialState);
-
- useEffect(() => {
- if (requestString) {
- socket.emit(requestString);
- } else {
- socket.emit(`get-${topic}`);
- }
- socket.on(topic, setState);
-
- return () => {
- socket.off(topic);
- };
- }, [requestString, topic]);
-
- return [state, setState] as const;
-};
diff --git a/apps/client/src/common/stores/createStore.ts b/apps/client/src/common/stores/createStore.ts
index fe5d4136a3..10df84379e 100644
--- a/apps/client/src/common/stores/createStore.ts
+++ b/apps/client/src/common/stores/createStore.ts
@@ -6,6 +6,7 @@ export default function createStore(initialState: T) {
get: () => currentState,
set: (newState: T) => {
currentState = newState;
+ listeners.forEach((listener) => listener(currentState));
},
subscribe: (listener: (state: T) => void) => {
listeners.add(listener);
diff --git a/apps/client/src/common/stores/runtime.ts b/apps/client/src/common/stores/runtime.ts
new file mode 100644
index 0000000000..542dce8006
--- /dev/null
+++ b/apps/client/src/common/stores/runtime.ts
@@ -0,0 +1,77 @@
+import { useSyncExternalStore } from 'react';
+import { Playback, RuntimeStore } from 'ontime-types';
+
+import { RUNTIME } from '../api/apiConstants';
+import { ontimeQueryClient } from '../queryClient';
+
+import createStore from './createStore';
+
+export const runtimeStorePlaceholder = {
+ timer: {
+ clock: 0,
+ current: null,
+ elapsed: null,
+ expectedFinish: null,
+ addedTime: 0,
+ startedAt: null,
+ finishedAt: null,
+ secondaryTimer: null,
+ selectedEventId: null,
+ duration: null,
+ timerType: null,
+ },
+ playback: Playback.Stop,
+ timerMessage: {
+ text: '',
+ visible: false,
+ },
+ publicMessage: {
+ text: '',
+ visible: false,
+ },
+ lowerMessage: {
+ text: '',
+ visible: false,
+ },
+ onAir: false,
+ loaded: {
+ numEvents: 0,
+ selectedEventIndex: null,
+ selectedEventId: null,
+ selectedPublicEventId: null,
+ nextEventId: null,
+ nextPublicEventId: null,
+ },
+ titles: {
+ titleNow: null,
+ subtitleNow: null,
+ presenterNow: null,
+ noteNow: null,
+ titleNext: null,
+ subtitleNext: null,
+ presenterNext: null,
+ noteNext: null,
+ },
+ titlesPublic: {
+ titleNow: null,
+ subtitleNow: null,
+ presenterNow: null,
+ noteNow: null,
+ titleNext: null,
+ subtitleNext: null,
+ presenterNext: null,
+ noteNext: null,
+ },
+};
+
+export const runtime = createStore(runtimeStorePlaceholder);
+
+export const useRuntimeStore = () => {
+ const data = useSyncExternalStore(runtime.subscribe, runtime.get);
+
+ // inject the data to react query to leverage dev tools for debugging
+ if (import.meta.env.DEV) {
+ ontimeQueryClient.setQueryData(RUNTIME, data);
+ }
+ return data;
+};
diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts
index 4fff1ad5d3..fb1144270f 100644
--- a/apps/client/src/common/utils/socket.ts
+++ b/apps/client/src/common/utils/socket.ts
@@ -1,14 +1,143 @@
-const socket = new WebSocket('ws://localhost:4001/ws');
-const subscriptions = new Set();
+import deepmerge from 'deepmerge';
+import { Log } from 'ontime-types';
-export function subscribeOnce(key: string, callback: (data: T) => void, requestString?: string) {
- if (subscriptions.has(key)) {
- return;
- }
- subscriptions.add(key);
+import { websocketUrl } from '../api/apiConstants';
+import { logger, LOGGER_MAX_MESSAGES } from '../stores/logger';
+import { runtime } from '../stores/runtime';
+
+export let websocket: WebSocket | null = null;
+let reconnectTimeout: NodeJS.Timeout | null = null;
+const reconnectInterval = 1000;
+let shouldReconnect = true;
+
+export const connectSocket = () => {
+ websocket = new WebSocket(websocketUrl);
+
+ websocket.onopen = () => {
+ clearTimeout(reconnectTimeout as NodeJS.Timeout);
+ };
+
+ websocket.onclose = () => {
+ console.warn('WebSocket disconnected');
+ if (shouldReconnect) {
+ reconnectTimeout = setTimeout(() => {
+ console.warn('WebSocket: attempting reconnect');
+ if (websocket && websocket.readyState === WebSocket.CLOSED) {
+ connectSocket();
+ }
+ }, reconnectInterval);
+ }
+ };
+
+ websocket.onerror = (error) => {
+ console.error('WebSocket error:', error);
+ };
+
+ websocket.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
- // requestString ? socket.send(requestString) : socket.send(`get-${key}`);
- // socket.on(key, callback);
-}
+ const { type, payload } = data;
+
+ if (!type) {
+ return;
+ }
+
+ // TODO: implement partial store updates
+ switch (type) {
+ case 'ontime-log': {
+ const state = logger.get();
+ state.unshift(payload as Log);
+
+ if (state.length > LOGGER_MAX_MESSAGES) {
+ state.splice(LOGGER_MAX_MESSAGES + 1, state.length - LOGGER_MAX_MESSAGES - 1);
+ }
+ logger.set(state);
+ break;
+ }
+ case 'ontime': {
+ const storeState = runtime.get();
+ const newState = deepmerge(storeState, payload);
+ runtime.set(newState);
+ break;
+ }
+ case 'ontime-playback': {
+ const state = runtime.get();
+ state.playback = payload;
+ runtime.set(state);
+ break;
+ }
+ case 'ontime-timer': {
+ const state = runtime.get();
+ state.timer = payload;
+ runtime.set(state);
+ break;
+ }
+ case 'ontime-loaded': {
+ const state = runtime.get();
+ state.loaded = payload;
+ runtime.set(state);
+ break;
+ }
+ case 'ontime-titles': {
+ const state = runtime.get();
+ state.titles = payload;
+ runtime.set(state);
+ break;
+ }
+ case 'ontime-titlesPublic': {
+ const state = runtime.get();
+ state.titlesPublic = payload;
+ runtime.set(state);
+ break;
+ }
+ case 'ontime-timerMessage': {
+ const state = runtime.get();
+ state.timerMessage = payload;
+ runtime.set(state);
+ break;
+ }
+ case 'ontime-publicMessage': {
+ const state = runtime.get();
+ state.publicMessage = payload;
+ runtime.set(state);
+ break;
+ }
+ case 'ontime-lowerMessage': {
+ const state = runtime.get();
+ state.lowerMessage = payload;
+ runtime.set(state);
+ break;
+ }
+ case 'ontime-onAir': {
+ const state = runtime.get();
+ state.onAir = payload;
+ runtime.set(state);
+ break;
+ }
+ }
+ } catch (_) {
+ // ignore unhandled
+ }
+ };
+};
+
+export const disconnectSocket = () => {
+ shouldReconnect = false;
+ websocket?.close();
+};
+
+export const socketSend = (message: any) => {
+ if (websocket && websocket.readyState === WebSocket.OPEN) {
+ websocket.send(message);
+ }
+};
-export default socket;
+export const socketSendJson = (type: string, payload?: any) => {
+ socketSend(
+ JSON.stringify({
+ type,
+ payload,
+ }),
+ );
+};
diff --git a/apps/client/src/common/utils/wss.ts b/apps/client/src/common/utils/wss.ts
deleted file mode 100644
index 64848291d2..0000000000
--- a/apps/client/src/common/utils/wss.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import useWebSocket from 'react-use-websocket';
-
-export default function useSocketClient() {
- const { sendJsonMessage, lastMessage, lastJsonMessage, readyState } = useWebSocket('_emit:4001', {
- share: true,
- shouldReconnect: () => true,
- onClose: () => console.log('closed socket connection'),
- onError: () => console.log('error in socket connection'),
- });
-
- console.log('debug messages', lastMessage);
- console.log('debug JSON messages', lastJsonMessage);
-
- return { sendJsonMessage, lastJsonMessage, lastMessage, readyState };
-}
diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx
index 862341f837..355ef1b1c3 100644
--- a/apps/client/src/features/rundown/Rundown.tsx
+++ b/apps/client/src/features/rundown/Rundown.tsx
@@ -23,7 +23,7 @@ interface RundownProps {
export default function Rundown(props: RundownProps) {
const { entries } = props;
- const { data } = useRundownEditor();
+ const data = useRundownEditor();
const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
diff --git a/apps/server/src/adapters/WebsocketAdapter.ts b/apps/server/src/adapters/WebsocketAdapter.ts
index 7ae071a1d1..04c0531712 100644
--- a/apps/server/src/adapters/WebsocketAdapter.ts
+++ b/apps/server/src/adapters/WebsocketAdapter.ts
@@ -30,7 +30,7 @@ export class SocketServer implements IAdapter {
private wss: WebSocketServer | null;
private clientIds: Set;
- constructor(server) {
+ constructor() {
if (instance) {
throw new Error('There can be only one');
}
@@ -38,14 +38,16 @@ export class SocketServer implements IAdapter {
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
instance = this;
this.clientIds = new Set();
+ this.wss = null;
+ }
+ init(server) {
this.wss = new WebSocketServer({ path: '/ws', server });
this.wss.on('connection', (ws) => {
const clientId = getRandomName();
this.clientIds.add(clientId);
logger.info('RX', `${this.wss.clients.size} Connections with new: ${clientId}`);
- console.log('DEBUG OPENED', this.wss.clients.size);
// send store payload on connect
ws.send(
@@ -99,7 +101,7 @@ export class SocketServer implements IAdapter {
logger.error('RX', `WS IN: ${error}`);
}
} catch (_) {
- return;
+ // we ignore unknown
}
});
});
@@ -107,7 +109,6 @@ export class SocketServer implements IAdapter {
// message is any serializable value
send(message: any) {
- console.log('DEBUG SEND', this.wss);
this.wss?.clients.forEach((client) => {
if (client !== this.wss && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
@@ -115,6 +116,9 @@ export class SocketServer implements IAdapter {
});
}
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- shutdown() {}
+ shutdown() {
+ this.wss?.close();
+ }
}
+
+export const socket = new SocketServer();
diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts
index 2d0689b724..6900f4c164 100644
--- a/apps/server/src/app.ts
+++ b/apps/server/src/app.ts
@@ -6,6 +6,7 @@ import cors from 'cors';
// import utils
import { join, resolve } from 'path';
+import { initSentry, reportSentryException } from './modules/sentry.js';
import { currentDirectory, environment, isProduction, resolvedPath } from './setup.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { OSCSettings } from 'ontime-types';
@@ -18,7 +19,7 @@ import { router as playbackRouter } from './routes/playbackRouter.js';
// Import adapters
import { OscServer } from './adapters/OscAdapter.js';
-import { SocketServer } from './adapters/WebsocketAdapter.js';
+import { socket } from './adapters/WebsocketAdapter.js';
import { DataProvider } from './classes/data-provider/DataProvider.js';
import { dbLoadingProcess } from './modules/loadDb.js';
@@ -27,6 +28,7 @@ import { eventTimer } from './services/TimerService.js';
import { integrationService } from './services/integration-service/IntegrationService.js';
import { OscIntegration } from './services/integration-service/OscIntegration.js';
import { logger } from './classes/Logger.js';
+import { eventLoader } from './classes/event-loader/EventLoader.js';
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
@@ -130,18 +132,11 @@ export const startServer = async () => {
expressServer = http.createServer(app);
- const socket = new SocketServer(expressServer);
+ socket.init(expressServer);
+ eventLoader.init();
expressServer.listen(serverPort, '0.0.0.0');
- logger.init(socket.send);
-
- let i = 1;
- setInterval(() => {
- logger.info('RX', `TESTING ${i}`);
- i++;
- }, 2000);
-
return returnMessage;
};
@@ -204,8 +199,9 @@ export const shutdown = async (exitCode = 0) => {
expressServer?.close();
oscServer?.shutdown();
eventTimer.shutdown();
- logger.shutdown();
integrationService.shutdown();
+ logger.shutdown();
+ socket.shutdown();
process.exit(exitCode);
};
diff --git a/apps/server/src/classes/Logger.ts b/apps/server/src/classes/Logger.ts
index a8a07b4697..d4318a1271 100644
--- a/apps/server/src/classes/Logger.ts
+++ b/apps/server/src/classes/Logger.ts
@@ -1,30 +1,21 @@
import { Log, LogLevel } from 'ontime-types';
-import { generateId } from 'ontime-utils';
+import { generateId, millisToString } from 'ontime-utils';
-import { stringFromMillis } from '../utils/time.js';
import { clock } from '../services/Clock.js';
import { isProduction } from '../setup.js';
-
-type LogMessage = {
- type: 'ontime-log';
- payload: Log;
-};
+import { socket } from '../adapters/WebsocketAdapter.js';
class Logger {
- private push: (log: LogMessage) => void | null;
private queue: Log[];
- constructor(emitCallback?: (message) => void) {
- this.push = emitCallback;
+ constructor() {
this.queue = [];
}
/**
* Enabling setup logger after init
- * @param emitCallback
*/
- init(emitCallback: (message) => void) {
- this.push = emitCallback;
+ init() {
this.queue.forEach((log) => {
this._push(log);
});
@@ -47,19 +38,12 @@ class Logger {
console.log(`[${log.level}] \t ${log.origin} \t ${log.text}`);
}
- if (this.push) {
- try {
- this.push({
- type: 'ontime-log',
- payload: log,
- });
- console.log('DEBUG FAILED LOGGER SHOULDVE SENT')
-
- } catch (_e) {
- console.log('DEBUG FAILED LOGGER SEND', _e)
- this.addToQueue(log);
- }
- } else {
+ try {
+ socket.send({
+ type: 'ontime-log',
+ payload: log,
+ });
+ } catch (_e) {
this.addToQueue(log);
}
}
@@ -76,7 +60,7 @@ class Logger {
level,
origin,
text,
- time: stringFromMillis(clock.getSystemTime() || 0),
+ time: millisToString(clock.getSystemTime() || 0),
};
this._push(log);
}
@@ -113,7 +97,6 @@ class Logger {
*/
shutdown() {
console.log('Shutting down logger');
- this.push = null;
this.queue = [];
}
}
diff --git a/apps/server/src/controllers/integrationController.ts b/apps/server/src/controllers/integrationController.ts
index f75e67c0df..0ad2dc5139 100644
--- a/apps/server/src/controllers/integrationController.ts
+++ b/apps/server/src/controllers/integrationController.ts
@@ -16,7 +16,7 @@ export function dispatchFromAdapter(type: string, payload: unknown, source?: 'os
}
case 'set-onair': {
- if (payload) {
+ if (typeof payload !== 'undefined') {
messageService.setOnAir(Boolean(payload));
}
break;
@@ -32,46 +32,46 @@ export function dispatchFromAdapter(type: string, payload: unknown, source?: 'os
break;
}
- case 'timer-message-text': {
+ case 'set-timer-message-text': {
if (typeof payload !== 'string') {
- throw new Error('Unable to parse payload');
+ throw new Error(`Unable to parse payload: ${payload}`);
}
messageService.setTimerText(payload);
break;
}
- case 'timer-message-visibility': {
- if (!payload) {
- throw new Error('Unable to parse payload');
+ case 'set-timer-message-visible': {
+ if (typeof payload === 'undefined') {
+ throw new Error(`Unable to parse payload: ${payload}`);
}
messageService.setTimerVisibility(Boolean(payload));
break;
}
- case 'public-message-text': {
+ case 'set-public-message-text': {
if (typeof payload !== 'string') {
- throw new Error('Unable to parse payload');
+ throw new Error(`Unable to parse payload: ${payload}`);
}
messageService.setPublicText(payload);
break;
}
- case 'public-message-visibility': {
- if (!payload) {
- throw new Error('Unable to parse payload');
+ case 'set-public-message-visible': {
+ if (typeof payload === 'undefined') {
+ throw new Error(`Unable to parse payload: ${payload}`);
}
messageService.setPublicVisibility(Boolean(payload));
break;
}
- case 'lower-message-text': {
+ case 'set-lower-message-text': {
if (typeof payload !== 'string') {
- throw new Error('Unable to parse payload');
+ throw new Error(`Unable to parse payload: ${payload}`);
}
messageService.setLowerText(payload);
break;
}
- case 'lower-message-visibility': {
- if (!payload) {
- throw new Error('Unable to parse payload');
+ case 'set-lower-message-visible': {
+ if (typeof payload === 'undefined') {
+ throw new Error(`Unable to parse payload: ${payload}`);
}
messageService.setLowerVisibility(Boolean(payload));
break;
diff --git a/apps/server/src/stores/EventStore.ts b/apps/server/src/stores/EventStore.ts
index 71b01204b6..f6ba159835 100644
--- a/apps/server/src/stores/EventStore.ts
+++ b/apps/server/src/stores/EventStore.ts
@@ -1,23 +1,31 @@
import { RuntimeStore } from 'ontime-types';
+import { socket } from '../adapters/WebsocketAdapter.js';
const store: Partial = {};
/**
* A runtime store that broadcasts its payload
*/
-// TODO: misses callback to send stuff
export const eventStore = {
get(key: T) {
return store[key];
},
set(key: T, value: RuntimeStore[T]) {
store[key] = value;
- // socketProvider.send(key, value);
+ // TODO: Partial updates seems to cause issues on the client
+ // socket.send({
+ // type: `ontime-${key}`,
+ // payload: value,
+ // });
+ this.broadcast();
},
poll() {
return store;
},
broadcast() {
- // socketProvider.send(store);
+ socket.send({
+ type: 'ontime',
+ payload: store,
+ });
},
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3909684320..f14fd6babd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -47,6 +47,7 @@ importers:
'@testing-library/react': ^13.1.1
'@testing-library/user-event': ^14.1.1
'@types/color': ^3.0.3
+ '@types/luxon': ^3.2.0
'@types/prop-types': ^15.7.5
'@types/react': ^18.0.26
'@types/react-beautiful-dnd': ^13.1.3
@@ -59,6 +60,7 @@ importers:
axios: ^1.2.0
color: ^4.2.3
csv-stringify: ^6.2.3
+ deepmerge: ^4.3.0
eslint: ^8.31.0
eslint-config-prettier: ^8.6.0
eslint-plugin-jest: ^27.1.7
@@ -70,7 +72,7 @@ importers:
framer-motion: ^8.0.2
jotai: ^1.10.0
jsdom: ^21.1.0
- luxon: ^3.1.0
+ luxon: ^3.3.0
ontime-types: workspace:*
ontime-utils: workspace:*
prettier: ^2.8.3
@@ -110,9 +112,10 @@ importers:
axios: 1.2.2
color: 4.2.3
csv-stringify: 6.2.3
+ deepmerge: 4.3.0
framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y
jotai: 1.13.0_react@18.2.0
- luxon: 3.2.1
+ luxon: 3.3.0
react: 18.2.0
react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y
react-dom: 18.2.0_react@18.2.0
@@ -131,6 +134,7 @@ importers:
'@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y
'@testing-library/user-event': 14.4.3
'@types/color': 3.0.3
+ '@types/luxon': 3.2.0
'@types/prop-types': 15.7.5
'@types/react': 18.0.26
'@types/react-beautiful-dnd': 13.1.3
@@ -260,19 +264,23 @@ importers:
packages/utils:
specifiers:
+ '@types/luxon': ^3.2.0
'@typescript-eslint/eslint-plugin': ^5.48.1
'@typescript-eslint/parser': ^5.48.1
eslint: ^8.31.0
eslint-config-prettier: ^8.6.0
eslint-plugin-prettier: ^4.2.1
eslint-plugin-simple-import-sort: ^8.0.0
+ luxon: ^3.3.0
nanoid: ^4.0.0
prettier: ^2.8.3
typescript: ^4.9.4
vitest: ^0.27.1
dependencies:
+ luxon: 3.3.0
nanoid: 4.0.0
devDependencies:
+ '@types/luxon': 3.2.0
'@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse
'@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
eslint: 8.31.0
@@ -2996,6 +3004,10 @@ packages:
resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==}
dev: false
+ /@types/luxon/3.2.0:
+ resolution: {integrity: sha512-lGmaGFoaXHuOLXFvuju2bfvZRqxAqkHPx9Y9IQdQABrinJJshJwfNCKV+u7rR3kJbiqfTF/NhOkcxxAFrObyaA==}
+ dev: true
+
/@types/mime/3.0.1:
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
dev: true
@@ -4413,6 +4425,11 @@ packages:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true
+ /deepmerge/4.3.0:
+ resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/defer-to-connect/1.1.3:
resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==}
dev: true
@@ -6485,8 +6502,8 @@ packages:
/lru_map/0.3.3:
resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==}
- /luxon/3.2.1:
- resolution: {integrity: sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==}
+ /luxon/3.3.0:
+ resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==}
engines: {node: '>=12'}
dev: false
From 8dbfc667f095115ed77cf4d843e55686959c9ced Mon Sep 17 00:00:00 2001
From: cv <34649812+cpvalente@users.noreply.github.com>
Date: Sat, 11 Mar 2023 22:31:02 +0100
Subject: [PATCH 16/20] refactor: migrate viewwrapper
---
apps/client/src/AppRouter.tsx | 18 ++--
.../{ViewWrapper.jsx => ViewWrapper.tsx} | 84 +++++--------------
2 files changed, 32 insertions(+), 70 deletions(-)
rename apps/client/src/features/viewers/{ViewWrapper.jsx => ViewWrapper.tsx} (54%)
diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx
index 846ed2fd02..d17a4da4fc 100644
--- a/apps/client/src/AppRouter.tsx
+++ b/apps/client/src/AppRouter.tsx
@@ -2,7 +2,7 @@ import { lazy, useEffect } from 'react';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import useAliases from './common/hooks-query/useAliases';
-import withSocket from './features/viewers/ViewWrapper';
+import withData from './features/viewers/ViewWrapper';
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
const Table = lazy(() => import('./features/table/ProtectedTable'));
@@ -17,14 +17,14 @@ const Public = lazy(() => import('./features/viewers/public/Public'));
const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerWrapper'));
const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock'));
-const STimer = withSocket(TimerView);
-const SMinimalTimer = withSocket(MinimalTimerView);
-const SClock = withSocket(ClockView);
-const SCountdown = withSocket(Countdown);
-const SBackstage = withSocket(Backstage);
-const SPublic = withSocket(Public);
-const SLowerThird = withSocket(Lower);
-const SStudio = withSocket(StudioClock);
+const STimer = withData(TimerView);
+const SMinimalTimer = withData(MinimalTimerView);
+const SClock = withData(ClockView);
+const SCountdown = withData(Countdown);
+const SBackstage = withData(Backstage);
+const SPublic = withData(Public);
+const SLowerThird = withData(Lower);
+const SStudio = withData(StudioClock);
const FeatureWrapper = lazy(() => import('./features/FeatureWrapper'));
const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
diff --git a/apps/client/src/features/viewers/ViewWrapper.jsx b/apps/client/src/features/viewers/ViewWrapper.tsx
similarity index 54%
rename from apps/client/src/features/viewers/ViewWrapper.jsx
rename to apps/client/src/features/viewers/ViewWrapper.tsx
index 3c01454ae8..88419f3614 100644
--- a/apps/client/src/features/viewers/ViewWrapper.jsx
+++ b/apps/client/src/features/viewers/ViewWrapper.tsx
@@ -1,68 +1,33 @@
-/* eslint-disable react/display-name */
-import { useEffect, useMemo, useState } from 'react';
+import { ReactNode, useMemo } from 'react';
+import { Playback } from 'ontime-types';
-import { useMessageControl } from '../../common/hooks/useSocket';
-import useSubscription from '../../common/hooks/useSubscription';
import useEventData from '../../common/hooks-query/useEventData';
import useRundown from '../../common/hooks-query/useRundown';
import useViewSettings from '../../common/hooks-query/useViewSettings';
-import socket from '../../common/utils/socket';
+import { useRuntimeStore } from '../../common/stores/runtime';
-const withSocket = (Component) => {
+const withData = (Component: ReactNode) => {
return (props) => {
+
+ // HTTP API data
const { data: eventsData } = useRundown();
const { data: genData } = useEventData();
const { data: viewSettings } = useViewSettings();
- const { data: messageControl } = useMessageControl();
-
- const [publicSelectedId, setPublicSelectedId] = useState(null);
-
- const [timer] = useSubscription('timer', {
- clock: null,
- current: null,
- elapsed: null ,
- expectedFinish: null,
- addedTime: 0,
- startedAt: null,
- finishedAt: null,
- secondaryTimer: null,
- });
- const [titles] = useSubscription('titles', {
- titleNow: '',
- subtitleNow: '',
- presenterNow: '',
- titleNext: '',
- subtitleNext: '',
- presenterNext: '',
- });
- const [publicTitles] = useSubscription('titlesPublic', {
- titleNow: '',
- subtitleNow: '',
- presenterNow: '',
- titleNext: '',
- subtitleNext: '',
- presenterNext: '',
- });
- const [selectedId] = useSubscription('selected-id', null);
- const [nextId] = useSubscription('next-id', null);
- const [playback] = useSubscription('playback', null);
-
- // Ask for update on load
- useEffect(() => {
- // todo: remove
- socket.on('publicselected-id', (data) => {
- setPublicSelectedId(data);
- });
- }, []);
-
const publicEvents = useMemo(() => {
if (Array.isArray(eventsData)) {
- return eventsData.filter((d) => d.type === 'event' && d.title !== '' && d.isPublic);
+ return eventsData.filter((e) => e.type === 'event' && e.title && e.isPublic);
}
return [];
}, [eventsData]);
+ // websocket data
+ const data = useRuntimeStore();
+ const { timer, titles, titlesPublic, publicMessage, timerMessage, lowerMessage, playback, onAir } = data;
+ const publicSelectedId = data.loaded.selectedPublicEventId;
+ const selectedId = data.loaded.selectedEventId;
+ const nextId = data.loaded.nextEventId;
+
/********************************************/
/*** + titleManager ***/
/*** WRAP INFORMATION RELATED TO TITLES ***/
@@ -85,16 +50,14 @@ const withSocket = (Component) => {
/********************************************/
// is there a now field?
let showPublicNow = true;
- if (!publicTitles.titleNow && !publicTitles.subtitleNow && !publicTitles.presenterNow)
- showPublicNow = false;
+ if (!titlesPublic.titleNow && !titlesPublic.subtitleNow && !titlesPublic.presenterNow) showPublicNow = false;
// is there a next field?
let showPublicNext = true;
- if (!publicTitles.titleNext && !publicTitles.subtitleNext && !publicTitles.presenterNext)
- showPublicNext = false;
+ if (!titlesPublic.titleNext && !titlesPublic.subtitleNext && !titlesPublic.presenterNext) showPublicNext = false;
const publicTitleManager = {
- ...publicTitles,
+ ...titlesPublic,
showNow: showPublicNow,
showNext: showPublicNext,
};
@@ -110,7 +73,7 @@ const withSocket = (Component) => {
// get clock string
const TimeManagerType = {
...timer,
- finished: playback === 'play' && timer.current < 0 && timer.startedAt,
+ finished: playback === Playback.Play && (timer.current ?? 0) < 0 && timer.startedAt,
playback,
};
@@ -119,13 +82,12 @@ const withSocket = (Component) => {
return null;
}
- Component.displayName = 'ComponentWithData';
return (
{
viewSettings={viewSettings}
nextId={nextId}
general={genData}
- onAir={messageControl.onAir}
+ onAir={onAir}
/>
);
};
};
-export default withSocket;
+export default withData;
From e052b60f91d4c6ec716cc3b8cb757f72a8e06591 Mon Sep 17 00:00:00 2001
From: cv <34649812+cpvalente@users.noreply.github.com>
Date: Sat, 11 Mar 2023 22:35:57 +0100
Subject: [PATCH 17/20] fix: update migration
---
apps/server/src/controllers/playbackController.js | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/apps/server/src/controllers/playbackController.js b/apps/server/src/controllers/playbackController.js
index e9f76e68e8..fe71f19c17 100644
--- a/apps/server/src/controllers/playbackController.js
+++ b/apps/server/src/controllers/playbackController.js
@@ -1,11 +1,10 @@
-// Create controller for GET request to '/playback'
-// Returns ACK message
import { PlaybackService } from '../services/PlaybackService.js';
+import { eventStore } from '../stores/EventStore.js';
// Create controller for POST request to '/playback'
// Returns playback state
export const pbGet = async (req, res) => {
- res.send({ playback: global.timer.state });
+ res.send({ playback: eventStore.get('playback') });
};
// Create controller for POST request to '/playback/start'
From 3d3855a76617e5aa90fb84227d9a3bfc5ce44425 Mon Sep 17 00:00:00 2001
From: cv <34649812+cpvalente@users.noreply.github.com>
Date: Sun, 12 Mar 2023 22:15:42 +0100
Subject: [PATCH 18/20] feat: zustand to manage stores
---
apps/client/package.json | 3 +-
apps/client/src/common/hooks/useSocket.ts | 93 ++++++++++-------------
apps/client/src/common/stores/runtime.ts | 26 +++----
apps/client/src/common/utils/socket.ts | 57 +++++++-------
pnpm-lock.yaml | 18 +++++
5 files changed, 98 insertions(+), 99 deletions(-)
diff --git a/apps/client/package.json b/apps/client/package.json
index cc27befce2..9a9ca8d298 100644
--- a/apps/client/package.json
+++ b/apps/client/package.json
@@ -32,7 +32,8 @@
"react-table": "^7.7.0",
"react-use-websocket": "^4.3.1",
"typeface-open-sans": "^1.1.13",
- "web-vitals": "^3.1.1"
+ "web-vitals": "^3.1.1",
+ "zustand": "^4.3.6"
},
"scripts": {
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts
index 56b354fc73..51c31c0324 100644
--- a/apps/client/src/common/hooks/useSocket.ts
+++ b/apps/client/src/common/hooks/useSocket.ts
@@ -1,30 +1,27 @@
-import { useMemo } from 'react';
+import { RuntimeStore } from 'ontime-types';
-import { useRuntimeStore } from '../stores/runtime';
+import { deepCompare, useRuntimeStore } from '../stores/runtime';
import { socketSendJson } from '../utils/socket';
export const useRundownEditor = () => {
- const state = useRuntimeStore();
-
- return useMemo(() => {
- return {
- selectedEventId: state.loaded.selectedEventId,
- nextEventId: state.loaded.nextEventId,
- playback: state.playback,
- };
- }, [state.loaded.selectedEventId, state.loaded.nextEventId, state.playback]);
+ const featureSelector = (state: RuntimeStore) => ({
+ playback: state.playback,
+ selectedEventId: state.loaded.selectedEventId,
+ nextEventId: state.loaded.nextEventId,
+ });
+
+ return useRuntimeStore(featureSelector, deepCompare);
};
export const useMessageControl = () => {
- const state = useRuntimeStore();
- return useMemo(() => {
- return {
- timerMessage: state.timerMessage,
- publicMessage: state.publicMessage,
- lowerMessage: state.lowerMessage,
- onAir: state.onAir,
- };
- }, [state.timerMessage, state.publicMessage, state.lowerMessage, state.onAir]);
+ const featureSelector = (state: RuntimeStore) => ({
+ timerMessage: state.timerMessage,
+ publicMessage: state.publicMessage,
+ lowerMessage: state.lowerMessage,
+ onAir: state.onAir,
+ });
+
+ return useRuntimeStore(featureSelector, deepCompare);
};
export const setMessage = {
@@ -38,14 +35,12 @@ export const setMessage = {
};
export const usePlaybackControl = () => {
- const state = useRuntimeStore();
-
- return useMemo(() => {
- return {
- playback: state.playback,
- numEvents: state.loaded.numEvents,
- };
- }, [state.playback, state.loaded.numEvents]);
+ const featureSelector = (state: RuntimeStore) => ({
+ playback: state.playback,
+ numEvents: state.loaded.numEvents,
+ });
+
+ return useRuntimeStore(featureSelector, deepCompare);
};
export const setPlayback = {
@@ -70,27 +65,23 @@ export const setPlayback = {
};
export const useInfoPanel = () => {
- const state = useRuntimeStore();
-
- return useMemo(() => {
- return {
- titles: state.titles,
- playback: state.playback,
- selectedEventIndex: state.loaded.selectedEventIndex,
- numEvents: state.loaded.numEvents,
- };
- }, [state.titles, state.playback, state.loaded.selectedEventIndex, state.loaded.numEvents]);
+ const featureSelector = (state: RuntimeStore) => ({
+ titles: state.titles,
+ playback: state.playback,
+ selectedEventIndex: state.loaded.selectedEventIndex,
+ numEvents: state.loaded.numEvents,
+ });
+
+ return useRuntimeStore(featureSelector, deepCompare);
};
export const useCuesheet = () => {
- const state = useRuntimeStore();
-
- return useMemo(() => {
- return {
- selectedEventIndex: state.loaded.selectedEventId,
- titleNow: state.titles.titleNow,
- };
- }, [state.loaded.selectedEventId, state.titles.titleNow]);
+ const featureSelector = (state: RuntimeStore) => ({
+ selectedEventIndex: state.loaded.selectedEventId,
+ titleNow: state.titles.titleNow,
+ });
+
+ return useRuntimeStore(featureSelector, deepCompare);
};
export const setEventPlayback = {
@@ -100,11 +91,9 @@ export const setEventPlayback = {
};
export const useTimer = () => {
- const state = useRuntimeStore();
+ const featureSelector = (state: RuntimeStore) => ({
+ timer: state.timer,
+ });
- return useMemo(() => {
- return {
- timer: state.timer,
- };
- }, [state.timer]);
+ return useRuntimeStore(featureSelector, deepCompare);
};
diff --git a/apps/client/src/common/stores/runtime.ts b/apps/client/src/common/stores/runtime.ts
index 542dce8006..bf2f0767b2 100644
--- a/apps/client/src/common/stores/runtime.ts
+++ b/apps/client/src/common/stores/runtime.ts
@@ -1,10 +1,7 @@
-import { useSyncExternalStore } from 'react';
+import isEqual from 'react-fast-compare';
import { Playback, RuntimeStore } from 'ontime-types';
-
-import { RUNTIME } from '../api/apiConstants';
-import { ontimeQueryClient } from '../queryClient';
-
-import createStore from './createStore';
+import { useStore } from 'zustand';
+import { createStore } from 'zustand/vanilla';
export const runtimeStorePlaceholder = {
timer: {
@@ -64,14 +61,13 @@ export const runtimeStorePlaceholder = {
},
};
-export const runtime = createStore(runtimeStorePlaceholder);
+export const runtime = createStore(() => ({
+ ...runtimeStorePlaceholder,
+}));
-export const useRuntimeStore = () => {
- const data = useSyncExternalStore(runtime.subscribe, runtime.get);
+export const deepCompare = (a: T, b: T) => isEqual(a, b);
- // inject the data to react query to leverage dev tools for debugging
- if (import.meta.env.DEV) {
- ontimeQueryClient.setQueryData(RUNTIME, data);
- }
- return data;
-};
+export const useRuntimeStore = (
+ selector: (state: RuntimeStore) => T,
+ equalityFn?: (a: unknown, b: unknown) => boolean,
+) => useStore(runtime, selector, equalityFn);
diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts
index fb1144270f..437f3660f8 100644
--- a/apps/client/src/common/utils/socket.ts
+++ b/apps/client/src/common/utils/socket.ts
@@ -1,8 +1,8 @@
-import deepmerge from 'deepmerge';
import { Log } from 'ontime-types';
-import { websocketUrl } from '../api/apiConstants';
-import { logger, LOGGER_MAX_MESSAGES } from '../stores/logger';
+import { RUNTIME, websocketUrl } from '../api/apiConstants';
+import { ontimeQueryClient } from '../queryClient';
+import { addLog } from '../stores/logger';
import { runtime } from '../stores/runtime';
export let websocket: WebSocket | null = null;
@@ -46,73 +46,68 @@ export const connectSocket = () => {
// TODO: implement partial store updates
switch (type) {
case 'ontime-log': {
- const state = logger.get();
- state.unshift(payload as Log);
-
- if (state.length > LOGGER_MAX_MESSAGES) {
- state.splice(LOGGER_MAX_MESSAGES + 1, state.length - LOGGER_MAX_MESSAGES - 1);
- }
- logger.set(state);
+ addLog(payload as Log);
break;
}
case 'ontime': {
- const storeState = runtime.get();
- const newState = deepmerge(storeState, payload);
- runtime.set(newState);
+ runtime.setState(payload);
+ if (import.meta.env.DEV) {
+ ontimeQueryClient.setQueryData(RUNTIME, data.payload);
+ }
break;
}
case 'ontime-playback': {
- const state = runtime.get();
+ const state = runtime.getState();
state.playback = payload;
- runtime.set(state);
+ runtime.setState(state);
break;
}
case 'ontime-timer': {
- const state = runtime.get();
+ const state = runtime.getState();
state.timer = payload;
- runtime.set(state);
+ runtime.setState(state);
break;
}
case 'ontime-loaded': {
- const state = runtime.get();
+ const state = runtime.getState();
state.loaded = payload;
- runtime.set(state);
+ runtime.setState(state);
break;
}
case 'ontime-titles': {
- const state = runtime.get();
+ const state = runtime.getState();
state.titles = payload;
- runtime.set(state);
+ runtime.setState(state);
break;
}
case 'ontime-titlesPublic': {
- const state = runtime.get();
+ const state = runtime.getState();
state.titlesPublic = payload;
- runtime.set(state);
+ runtime.setState(state);
break;
}
case 'ontime-timerMessage': {
- const state = runtime.get();
+ const state = runtime.getState();
state.timerMessage = payload;
- runtime.set(state);
+ runtime.setState(state);
break;
}
case 'ontime-publicMessage': {
- const state = runtime.get();
+ const state = runtime.getState();
state.publicMessage = payload;
- runtime.set(state);
+ runtime.setState(state);
break;
}
case 'ontime-lowerMessage': {
- const state = runtime.get();
+ const state = runtime.getState();
state.lowerMessage = payload;
- runtime.set(state);
+ runtime.setState(state);
break;
}
case 'ontime-onAir': {
- const state = runtime.get();
+ const state = runtime.getState();
state.onAir = payload;
- runtime.set(state);
+ runtime.setState(state);
break;
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f14fd6babd..3247152526 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -96,6 +96,7 @@ importers:
vite-plugin-svgr: ^2.4.0
vite-tsconfig-paths: ^4.0.3
web-vitals: ^3.1.1
+ zustand: ^4.3.6
dependencies:
'@chakra-ui/react': 2.4.8_loo4skotrnm7icurwgkplqpnwq
'@dnd-kit/core': 6.0.7_biqbaboplfbrettd7655fr4n2y
@@ -127,6 +128,7 @@ importers:
react-use-websocket: 4.3.1_biqbaboplfbrettd7655fr4n2y
typeface-open-sans: 1.1.13
web-vitals: 3.1.1
+ zustand: 4.3.6_react@18.2.0
devDependencies:
'@sentry/vite-plugin': 0.3.0
'@tanstack/eslint-plugin-query': 4.21.0
@@ -9344,3 +9346,19 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
dev: true
+
+ /zustand/4.3.6_react@18.2.0:
+ resolution: {integrity: sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ immer: '>=9.0'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ immer:
+ optional: true
+ react:
+ optional: true
+ dependencies:
+ react: 18.2.0
+ use-sync-external-store: 1.2.0_react@18.2.0
+ dev: false
From 76ead2037815ea9de2cab4914481344958a3e7ab Mon Sep 17 00:00:00 2001
From: cv <34649812+cpvalente@users.noreply.github.com>
Date: Wed, 15 Mar 2023 20:56:28 +0100
Subject: [PATCH 19/20] refactor: cleanup logging store
---
apps/client/src/common/stores/logger.ts | 46 ++--
.../src/features/info/CollapsableInfo.tsx | 35 +--
apps/client/src/features/info/Info.tsx | 21 +-
.../src/features/info/InfoLogger.module.scss | 8 +-
apps/client/src/features/info/InfoLogger.tsx | 220 +++++++++---------
apps/client/src/features/info/InfoNif.tsx | 22 +-
apps/client/src/features/info/InfoTitles.tsx | 36 +++
7 files changed, 189 insertions(+), 199 deletions(-)
create mode 100644 apps/client/src/features/info/InfoTitles.tsx
diff --git a/apps/client/src/common/stores/logger.ts b/apps/client/src/common/stores/logger.ts
index 456cff7f7e..dd57139c19 100644
--- a/apps/client/src/common/stores/logger.ts
+++ b/apps/client/src/common/stores/logger.ts
@@ -1,32 +1,37 @@
-import { useCallback, useSyncExternalStore } from 'react';
+import { useCallback } from 'react';
import { Log, LogLevel } from 'ontime-types';
import { generateId, millisToString } from 'ontime-utils';
+import { useStore } from 'zustand';
+import { createStore } from 'zustand/vanilla';
import { socketSendJson } from '../utils/socket';
import { nowInMillis } from '../utils/time';
-import createStore from './createStore';
+type LogStore = {
+ logs: Log[];
+};
-export const logger = createStore([]);
-export const LOGGER_MAX_MESSAGES = 100;
+export const logger = createStore(() => ({
+ logs: [],
+}));
-export function useEmitLog() {
- const _addToLogger = (log: Log) => {
- const state = logger.get();
- state.push(log);
- if (state.length > LOGGER_MAX_MESSAGES) {
- state.slice(1);
- }
- logger.set(state);
- };
+export const useLogData = () => useStore(logger);
+export const addLog = (log: Log) =>
+ logger.setState((state) => ({
+ logs: [...state.logs, log],
+ }));
+
+export const clearLogs = () => logger.setState({ logs: [] });
+
+export function useEmitLog() {
/**
* Utility function sends message over socket
* @param text
* @param level
* @private
*/
- const _emit = (text: string, level: LogLevel) => {
+ const _emit = useCallback((text: string, level: LogLevel) => {
const log = {
id: generateId(),
origin: 'CLIENT',
@@ -35,9 +40,9 @@ export function useEmitLog() {
text,
};
- _addToLogger(log);
+ addLog(log);
socketSendJson('ontime-log', log);
- };
+ }, []);
/**
* Sends a message with level INFO
@@ -72,18 +77,9 @@ export function useEmitLog() {
[_emit],
);
- const clearLog = useCallback(() => {
- logger.set([]);
- }, []);
-
return {
emitInfo,
emitWarning,
emitError,
- clearLog,
};
}
-
-export const useLogData = () => {
- return useSyncExternalStore(logger.subscribe, () => logger.get());
-};
diff --git a/apps/client/src/features/info/CollapsableInfo.tsx b/apps/client/src/features/info/CollapsableInfo.tsx
index f076a6343b..207052c4b4 100644
--- a/apps/client/src/features/info/CollapsableInfo.tsx
+++ b/apps/client/src/features/info/CollapsableInfo.tsx
@@ -1,48 +1,21 @@
-import { useState } from 'react';
+import { PropsWithChildren, useState } from 'react';
import CollapseBar from '../../common/components/collapse-bar/CollapseBar';
import style from './Info.module.scss';
-type TitleShape = {
- title: string;
- presenter: string;
- subtitle: string;
- note: string;
-};
-
interface CollapsableInfoProps {
title: string;
- data: TitleShape;
}
-export default function CollapsableInfo(props: CollapsableInfoProps) {
- const { title, data } = props;
+export default function CollapsableInfo(props: PropsWithChildren) {
+ const { title, children } = props;
const [collapsed, setCollapsed] = useState(false);
return (
setCollapsed((prev) => !prev)} />
- {!collapsed && (
-
-
- Title:
- {data.title}
-
-
- Presenter:
- {data.presenter}
-
-
- Subtitle:
- {data.subtitle}
-
-
- Note:
- {data.note}
-
-
- )}
+ {!collapsed && children}
);
}
diff --git a/apps/client/src/features/info/Info.tsx b/apps/client/src/features/info/Info.tsx
index 7201dea9ff..406ed85c6a 100644
--- a/apps/client/src/features/info/Info.tsx
+++ b/apps/client/src/features/info/Info.tsx
@@ -1,8 +1,9 @@
import { useInfoPanel } from '../../common/hooks/useSocket';
-import InfoTitle from './CollapsableInfo';
+import CollapsableInfo from './CollapsableInfo';
import InfoLogger from './InfoLogger';
import InfoNif from './InfoNif';
+import InfoTitles from './InfoTitles';
import style from './Info.module.scss';
@@ -25,7 +26,7 @@ export default function Info() {
const selected = !data.numEvents
? 'No events'
- : `Event ${data.selectedEventIndex != null ? data.selectedEventIndex + 1 : '-'} / ${
+ : `Event ${data.selectedEventIndex !== null ? data.selectedEventIndex + 1 : '-'} / ${
data.numEvents ? data.numEvents : '-'
}`;
@@ -35,10 +36,18 @@ export default function Info() {
Ontime running on port 4001
{selected}
>
);
}
diff --git a/apps/client/src/features/info/InfoLogger.module.scss b/apps/client/src/features/info/InfoLogger.module.scss
index d1e86e4a9a..33ba388606 100644
--- a/apps/client/src/features/info/InfoLogger.module.scss
+++ b/apps/client/src/features/info/InfoLogger.module.scss
@@ -6,12 +6,7 @@ $info-hover: $section-white;
.infoLoggerContainer {
max-height: 80%;
- margin-top: 32px;
-
- &.expanded {
- min-height: 50%;
- height: 100%
- }
+ height: 100%
}
.log {
@@ -24,6 +19,7 @@ $info-hover: $section-white;
.logEntry {
display: flex;
margin-bottom: 2px;
+
&.INFO {
color: $info-gray;
}
diff --git a/apps/client/src/features/info/InfoLogger.tsx b/apps/client/src/features/info/InfoLogger.tsx
index e7d8d640f2..5abab2adab 100644
--- a/apps/client/src/features/info/InfoLogger.tsx
+++ b/apps/client/src/features/info/InfoLogger.tsx
@@ -1,29 +1,22 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useState } from 'react';
import { Button } from '@chakra-ui/react';
-import { Log } from 'ontime-types';
-import CollapseBar from '../../common/components/collapse-bar/CollapseBar';
-import { useEmitLog, useLogData } from '../../common/stores/logger';
+import { clearLogs, useLogData } from '../../common/stores/logger';
import style from './InfoLogger.module.scss';
-enum LOG_FILTER {
- USER = 'USER',
- CLIENT = 'CLIENT',
- SERVER = 'SERVER',
+enum LogFilter {
+ User = 'USER',
+ Client = 'CLIENT',
+ Server = 'SERVER',
RX = 'RX',
TX = 'TX',
- PLAYBACK = 'PLAYBACK',
+ Playback = 'PLAYBACK',
}
export default function InfoLogger() {
- const logData = useLogData();
- const { clearLog } = useEmitLog();
- // const { logData, clearLog } = useContext(LoggingContext);
- // TODO: derived data shouldn't be in state
- const data: Log[] = [];
- const setData = (newData) => console.log('tried setting data');
- const [collapsed, setCollapsed] = useState(false);
+ const { logs: logData } = useLogData();
+
const [showClient, setShowClient] = useState(true);
const [showServer, setShowServer] = useState(true);
const [showRx, setShowRx] = useState(true);
@@ -31,112 +24,107 @@ export default function InfoLogger() {
const [showPlayback, setShowPlayback] = useState(true);
const [showUser, setShowUser] = useState(true);
- const matchers: LOG_FILTER[] = [];
- if (showUser) {
- matchers.push(LOG_FILTER.USER);
- }
- if (showClient) {
- matchers.push(LOG_FILTER.CLIENT);
- }
- if (showServer) {
- matchers.push(LOG_FILTER.SERVER);
- }
- if (showRx) {
- matchers.push(LOG_FILTER.RX);
- }
- if (showTx) {
- matchers.push(LOG_FILTER.TX);
- }
- if (showPlayback) {
- matchers.push(LOG_FILTER.PLAYBACK);
- }
+ const matchers: LogFilter[] = [];
+ if (showUser) {
+ matchers.push(LogFilter.User);
+ }
+ if (showClient) {
+ matchers.push(LogFilter.Client);
+ }
+ if (showServer) {
+ matchers.push(LogFilter.Server);
+ }
+ if (showRx) {
+ matchers.push(LogFilter.RX);
+ }
+ if (showTx) {
+ matchers.push(LogFilter.TX);
+ }
+ if (showPlayback) {
+ matchers.push(LogFilter.Playback);
+ }
const filteredData = logData.filter((entry) => matchers.some((match) => entry.origin === match));
- const disableOthers = useCallback((toEnable: LOG_FILTER) => {
- toEnable === LOG_FILTER.USER ? setShowUser(true) : setShowUser(false);
- toEnable === LOG_FILTER.CLIENT ? setShowClient(true) : setShowClient(false);
- toEnable === LOG_FILTER.SERVER ? setShowServer(true) : setShowServer(false);
- toEnable === LOG_FILTER.RX ? setShowRx(true) : setShowRx(false);
- toEnable === LOG_FILTER.TX ? setShowTx(true) : setShowTx(false);
- toEnable === LOG_FILTER.PLAYBACK ? setShowPlayback(true) : setShowPlayback(false);
+ const disableOthers = useCallback((toEnable: LogFilter) => {
+ toEnable === LogFilter.User ? setShowUser(true) : setShowUser(false);
+ toEnable === LogFilter.Client ? setShowClient(true) : setShowClient(false);
+ toEnable === LogFilter.Server ? setShowServer(true) : setShowServer(false);
+ toEnable === LogFilter.RX ? setShowRx(true) : setShowRx(false);
+ toEnable === LogFilter.TX ? setShowTx(true) : setShowTx(false);
+ toEnable === LogFilter.Playback ? setShowPlayback(true) : setShowPlayback(false);
}, []);
return (
-