V2 ws store (#310)

* Update TimerService.ts

* refactor: message service publishes to store

* refactor: several type improvements

* V2 ws store wss (#309)

* refactor: shared logging types

* refactor: simplify message service consumption

* refactor: create discrete logging system

* refactor: move socket.io > websocket
This commit is contained in:
Carlos Valente
2023-03-15 22:06:47 +01:00
committed by GitHub
parent 11648ee546
commit 76c8f8a4d5
86 changed files with 1876 additions and 1936 deletions
+17 -14
View File
@@ -1,4 +1,4 @@
import { useCallback, useContext } from 'react';
import { useCallback } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios, { AxiosError } from 'axios';
import { useAtomValue } from 'jotai';
@@ -15,14 +15,14 @@ import {
requestReorderEvent,
} from '../api/eventsApi';
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../atoms/LocalEventSettings';
import { LoggingContext } from '../context/LoggingContext';
import { useEmitLog } from '../stores/logger';
/**
* @description Set of utilities for events
*/
export const useEventAction = () => {
const queryClient = useQueryClient();
const { emitError } = useContext(LoggingContext);
const { emitError } = useEmitLog();
const defaultPublic = useAtomValue(defaultPublicAtom);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
@@ -39,23 +39,24 @@ export const useEventAction = () => {
networkMode: 'always',
});
type AddOptions = {
defaultPublic?: boolean;
startTimeIsLastEnd?: boolean;
lastEventId?: string;
type BaseOptions = {
after?: string;
};
type EventOptions = BaseOptions & {
defaultPublic?: boolean;
lastEventId?: string;
startTimeIsLastEnd?: boolean;
};
/**
* Adds an event to rundown
*/
const addEvent = useCallback(
async (event: Partial<OntimeRundownEntry>, options?: AddOptions) => {
async (event: Partial<OntimeRundownEntry>, options?: EventOptions) => {
const newEvent: Partial<OntimeRundownEntry> = { ...event };
// ************* CHECK OPTIONS
// there is an option to pass an index of an array to use as start time
// only events have options
// ************* CHECK OPTIONS specific to events
if (newEvent.type === SupportedEvent.Event) {
const applicationOptions = {
defaultPublic: options?.defaultPublic ?? defaultPublic,
@@ -81,13 +82,15 @@ export const useEventAction = () => {
if (applicationOptions.defaultPublic) {
newEvent.isPublic = true;
}
}
if (applicationOptions?.after) {
newEvent.after = applicationOptions.after;
}
// handle adding options that concern all event type
if (options?.after) {
newEvent.after = options.after;
}
try {
// @ts-expect-error -- we know that the object is well formed now
await _addEventMutation.mutateAsync(newEvent);
} catch (error) {
if (!axios.isAxiosError(error)) {
+73 -118
View File
@@ -1,144 +1,99 @@
import { useQuery } from '@tanstack/react-query';
import { Playback } from 'ontime-types';
import { RuntimeStore } from 'ontime-types';
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 { deepCompare, useRuntimeStore } from '../stores/runtime';
import { socketSendJson } from '../utils/socket';
function createSocketHook<T>(key: string, defaultValue: T | null = null) {
subscribeOnce<T>(key, (data) => queryClient.setQueryData([key], data));
// 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,
};
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 = 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),
};
export const emptyPlaybackControl = {
playback: 'stop',
selectedEventId: null,
numEvents: 0,
};
export const usePlaybackControl = createSocketHook(FEAT_PLAYBACKCONTROL, emptyPlaybackControl);
export const resetPlayback = () => {
const cacheData = queryClient.getQueryData([FEAT_PLAYBACKCONTROL]) as Record<string, unknown>;
queryClient.setQueryData([FEAT_PLAYBACKCONTROL], {
...cacheData,
playback: 'stop',
selectedEventId: null,
export const useRundownEditor = () => {
const featureSelector = (state: RuntimeStore) => ({
playback: state.playback,
selectedEventId: state.loaded.selectedEventId,
nextEventId: state.loaded.nextEventId,
});
return useRuntimeStore(featureSelector, deepCompare);
};
export const useMessageControl = () => {
const featureSelector = (state: RuntimeStore) => ({
timerMessage: state.timerMessage,
publicMessage: state.publicMessage,
lowerMessage: state.lowerMessage,
onAir: state.onAir,
});
return useRuntimeStore(featureSelector, deepCompare);
};
export const setMessage = {
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 usePlaybackControl = () => {
const featureSelector = (state: RuntimeStore) => ({
playback: state.playback,
numEvents: state.loaded.numEvents,
});
return useRuntimeStore(featureSelector, deepCompare);
};
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',
selectedEventId: null,
selectedEventIndex: null,
numEvents: 0,
export const useInfoPanel = () => {
const featureSelector = (state: RuntimeStore) => ({
titles: state.titles,
playback: state.playback,
selectedEventIndex: state.loaded.selectedEventIndex,
numEvents: state.loaded.numEvents,
});
return useRuntimeStore(featureSelector, deepCompare);
};
export const useInfoPanel = createSocketHook(FEAT_INFO, emptyInfo);
export const useCuesheet = () => {
const featureSelector = (state: RuntimeStore) => ({
selectedEventIndex: state.loaded.selectedEventId,
titleNow: state.titles.titleNow,
});
export const emptyCuesheet = {
selectedEventId: null,
titleNow: '',
return useRuntimeStore(featureSelector, deepCompare);
};
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 featureSelector = (state: RuntimeStore) => ({
timer: state.timer,
});
export const useTimer = createSocketHook(TIMER, emptyTimer);
return useRuntimeStore(featureSelector, deepCompare);
};
@@ -1,22 +0,0 @@
import { useEffect, useState } from 'react';
import socket from '../utils/socket';
export default function useSubscription<T>(topic: string, initialState: T, requestString?: string) {
const [state, setState] = useState<T>(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;
};