refactor: WS (#1622)

* refactor: WS

* refactor refetch

extract refetch keys to package/types

auto invalidata all keys

use refetch keys for project data

no need for constant update of info, just fetch when looking at it

split refetch and query keys

rename types

* refactor viewSettings

* fixup! refactor: WS

* rearange files and make types for api calls

* cleanup viewsettings

* switch exhaustiveCheck

* combine send socket function

* exhaustive check

* rename type to tag

* fixup! fixup! refactor: WS

* remove custom etag solution

* get errors from socket

* lint

* small change

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
Alex Christoffer Rasmussen
2025-06-21 12:18:59 +02:00
committed by GitHub
parent c8fe0bbcba
commit 31d47ed1e5
28 changed files with 377 additions and 333 deletions
+6
View File
@@ -1,3 +1,5 @@
import axios from 'axios';
import { serverURL } from '../../externals'; import { serverURL } from '../../externals';
// keys in tanstack store // keys in tanstack store
@@ -23,3 +25,7 @@ const cssOverridePath = 'styles/override.css';
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`; export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`; export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
axios.defaults.validateStatus = (status) => {
return (status >= 200 && status < 300) || status === 304;
};
+5 -11
View File
@@ -1,21 +1,15 @@
import axios from 'axios'; import axios from 'axios';
import { ViewSettings } from 'ontime-types'; import type { ViewSettings } from 'ontime-types';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
const viewSettingsPath = apiEntryUrl + '/view-settings';
const viewSettingsPath = `${apiEntryUrl}/view-settings`; export async function getViewSettings() {
/**
* HTTP request to retrieve view settings
*/
export async function getView(): Promise<ViewSettings> {
const res = await axios.get(viewSettingsPath); const res = await axios.get(viewSettingsPath);
return res.data; return res.data;
} }
/**
* HTTP request to mutate view settings
*/
export async function postViewSettings(data: ViewSettings) { export async function postViewSettings(data: ViewSettings) {
return axios.post(viewSettingsPath, data); const res = await axios.post(viewSettingsPath, data);
return res.data as ViewSettings;
} }
@@ -1,20 +1,28 @@
import { useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { MILLIS_PER_HOUR } from 'ontime-utils';
import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { getViewSettings, postViewSettings } from '../../common/api/viewSettings';
import { ontimeQueryClient } from '../../common/queryClient';
import { VIEW_SETTINGS } from '../api/constants'; import { VIEW_SETTINGS } from '../api/constants';
import { getView } from '../api/viewSettings';
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type'; import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
export default function useViewSettings() { export default function useViewSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({ const { data, isPending } = useQuery({
queryKey: VIEW_SETTINGS, queryKey: VIEW_SETTINGS,
queryFn: getView, queryFn: getViewSettings,
placeholderData: (previousData, _previousQuery) => previousData, placeholderData: (previousData, _previousQuery) => previousData,
retry: 5, staleTime: MILLIS_PER_HOUR,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
networkMode: 'always',
}); });
return { data: data ?? viewsSettingsPlaceholder, status, isError, refetch, isFetching }; const { mutateAsync } = useMutation({
mutationFn: postViewSettings,
onMutate: () => {
ontimeQueryClient.cancelQueries({ queryKey: VIEW_SETTINGS });
},
onSuccess: (data) => {
ontimeQueryClient.setQueryData(VIEW_SETTINGS, data);
},
});
return { data: data ?? viewsSettingsPlaceholder, mutateAsync, isPending };
} }
@@ -1,9 +1,10 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { MessageTag } from 'ontime-types';
import { useShallow } from 'zustand/shallow'; import { useShallow } from 'zustand/shallow';
import { useClientStore } from '../stores/clientStore'; import { useClientStore } from '../stores/clientStore';
import { socketSendJson } from '../utils/socket'; import { sendSocket } from '../utils/socket';
import { useIsOnline } from './useSocket'; import { useIsOnline } from './useSocket';
@@ -22,7 +23,7 @@ export const useClientPath = () => {
useEffect(() => { useEffect(() => {
if (!isOnline) return; if (!isOnline) return;
socketSendJson('set-client-path', pathname + search); sendSocket(MessageTag.ClientSetPath, pathname + search);
}, [pathname, search, isOnline]); }, [pathname, search, isOnline]);
// navigate to new path when received from server // navigate to new path when received from server
+29 -29
View File
@@ -1,7 +1,7 @@
import { OffsetMode, RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ontime-types'; import { OffsetMode, RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ontime-types';
import { useRuntimeStore } from '../stores/runtime'; import { useRuntimeStore } from '../stores/runtime';
import { socketSendJson } from '../utils/socket'; import { sendSocket } from '../utils/socket';
const createSelector = const createSelector =
<T>(selector: (state: RuntimeStore) => T) => <T>(selector: (state: RuntimeStore) => T) =>
@@ -9,9 +9,9 @@ const createSelector =
useRuntimeStore(selector); useRuntimeStore(selector);
export const setClientRemote = { export const setClientRemote = {
setIdentify: (payload: { target: string; identify: boolean }) => socketSendJson('client', payload), setIdentify: (payload: { target: string; identify: boolean }) => sendSocket('client', payload),
setRedirect: (payload: { target: string; redirect: string }) => socketSendJson('client', payload), setRedirect: (payload: { target: string; redirect: string }) => sendSocket('client', payload),
setClientName: (payload: { target: string; rename: string }) => socketSendJson('client', payload), setClientName: (payload: { target: string; rename: string }) => sendSocket('client', payload),
}; };
export const useRundownEditor = createSelector((state: RuntimeStore) => ({ export const useRundownEditor = createSelector((state: RuntimeStore) => ({
@@ -53,13 +53,13 @@ export const useMessagePreview = createSelector((state: RuntimeStore) => ({
})); }));
export const setMessage = { export const setMessage = {
timerText: (payload: string) => socketSendJson('message', { timer: { text: payload } }), timerText: (payload: string) => sendSocket('message', { timer: { text: payload } }),
timerVisible: (payload: boolean) => socketSendJson('message', { timer: { visible: payload } }), timerVisible: (payload: boolean) => sendSocket('message', { timer: { visible: payload } }),
externalText: (payload: string) => socketSendJson('message', { external: payload }), externalText: (payload: string) => sendSocket('message', { external: payload }),
timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }), timerBlink: (payload: boolean) => sendSocket('message', { timer: { blink: payload } }),
timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }), timerBlackout: (payload: boolean) => sendSocket('message', { timer: { blackout: payload } }),
timerSecondary: (payload: TimerMessage['secondarySource']) => timerSecondary: (payload: TimerMessage['secondarySource']) =>
socketSendJson('message', { timer: { secondarySource: payload } }), sendSocket('message', { timer: { secondarySource: payload } }),
}; };
export const usePlaybackControl = createSelector((state: RuntimeStore) => ({ export const usePlaybackControl = createSelector((state: RuntimeStore) => ({
@@ -70,24 +70,24 @@ export const usePlaybackControl = createSelector((state: RuntimeStore) => ({
})); }));
export const setPlayback = { export const setPlayback = {
start: () => socketSendJson('start'), start: () => sendSocket('start', undefined),
pause: () => socketSendJson('pause'), pause: () => sendSocket('pause', undefined),
roll: () => socketSendJson('roll'), roll: () => sendSocket('roll', undefined),
startNext: () => socketSendJson('start', 'next'), startNext: () => sendSocket('start', 'next'),
previous: () => { previous: () => {
socketSendJson('load', 'previous'); sendSocket('load', 'previous');
}, },
next: () => { next: () => {
socketSendJson('load', 'next'); sendSocket('load', 'next');
}, },
stop: () => { stop: () => {
socketSendJson('stop'); sendSocket('stop', undefined);
}, },
reload: () => { reload: () => {
socketSendJson('reload'); sendSocket('reload', undefined);
}, },
addTime: (amount: number) => { addTime: (amount: number) => {
socketSendJson('addtime', amount); sendSocket('addtime', amount);
}, },
}; };
@@ -99,11 +99,11 @@ export const useAuxTimerControl = createSelector((state: RuntimeStore) => ({
})); }));
export const setAuxTimer = { export const setAuxTimer = {
start: () => socketSendJson('auxtimer', { '1': SimplePlayback.Start }), start: () => sendSocket('auxtimer', { '1': SimplePlayback.Start }),
pause: () => socketSendJson('auxtimer', { '1': SimplePlayback.Pause }), pause: () => sendSocket('auxtimer', { '1': SimplePlayback.Pause }),
stop: () => socketSendJson('auxtimer', { '1': SimplePlayback.Stop }), stop: () => sendSocket('auxtimer', { '1': SimplePlayback.Stop }),
setDirection: (direction: SimpleDirection) => socketSendJson('auxtimer', { '1': { direction } }), setDirection: (direction: SimpleDirection) => sendSocket('auxtimer', { '1': { direction } }),
setDuration: (time: number) => socketSendJson('auxtimer', { '1': { duration: time } }), setDuration: (time: number) => sendSocket('auxtimer', { '1': { duration: time } }),
}; };
export const useSelectedEventId = createSelector((state: RuntimeStore) => ({ export const useSelectedEventId = createSelector((state: RuntimeStore) => ({
@@ -115,10 +115,10 @@ export const useCurrentBlockId = createSelector((state: RuntimeStore) => ({
})); }));
export const setEventPlayback = { export const setEventPlayback = {
loadEvent: (id: string) => socketSendJson('load', { id }), loadEvent: (id: string) => sendSocket('load', { id }),
startEvent: (id: string) => socketSendJson('start', { id }), startEvent: (id: string) => sendSocket('start', { id }),
start: () => socketSendJson('start'), start: () => sendSocket('start', undefined),
pause: () => socketSendJson('pause'), pause: () => sendSocket('pause', undefined),
}; };
export const useTimer = createSelector((state: RuntimeStore) => ({ export const useTimer = createSelector((state: RuntimeStore) => ({
@@ -190,7 +190,7 @@ export const useOffsetMode = createSelector((state: RuntimeStore) => ({
offsetMode: state.runtime.offsetMode, offsetMode: state.runtime.offsetMode,
})); }));
export const setOffsetMode = (payload: OffsetMode) => socketSendJson('offsetmode', payload); export const setOffsetMode = (payload: OffsetMode) => sendSocket('offsetmode', payload);
export const usePlayback = () => { export const usePlayback = () => {
const featureSelector = (state: RuntimeStore) => ({ const featureSelector = (state: RuntimeStore) => ({
+7 -1
View File
@@ -1,11 +1,17 @@
import { QueryClient } from '@tanstack/react-query'; import { QueryClient } from '@tanstack/react-query';
import { MILLIS_PER_MINUTE } from 'ontime-utils';
import { isOntimeCloud } from '../externals'; import { isOntimeCloud } from '../externals';
export const ontimeQueryClient = new QueryClient({ export const ontimeQueryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
gcTime: 1000 * 60 * 10, // 10 min gcTime: 10 * MILLIS_PER_MINUTE,
// staleTime: MILLIS_PER_HOUR, //TODO: when all routes have implemented refetch signal from server, we can the assume that the data is not stale until we get the signal
networkMode: 'always',
refetchOnWindowFocus: false,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
}, },
mutations: { mutations: {
/** /**
+3 -3
View File
@@ -1,10 +1,10 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Log, LogLevel, LogOrigin } from 'ontime-types'; import { Log, LogLevel, LogOrigin, MessageTag } from 'ontime-types';
import { generateId, millisToString } from 'ontime-utils'; import { generateId, millisToString } from 'ontime-utils';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { createStore } from 'zustand/vanilla'; import { createStore } from 'zustand/vanilla';
import { socketSendJson } from '../utils/socket'; import { sendSocket } from '../utils/socket';
import { nowInMillis } from '../utils/time'; import { nowInMillis } from '../utils/time';
type LogStore = { type LogStore = {
@@ -40,7 +40,7 @@ export function useEmitLog() {
text, text,
}; };
socketSendJson('ontime-log', log); sendSocket(MessageTag.Log, log);
}, []); }, []);
/** /**
+64 -64
View File
@@ -1,7 +1,16 @@
import { Log, Rundown, RuntimeStore } from 'ontime-types'; import {
ApiAction,
Log,
MessageTag,
RefetchKey,
Rundown,
RuntimeStore,
WsPacketToClient,
WsPacketToServer,
} from 'ontime-types';
import { isProduction, websocketUrl } from '../../externals'; import { isProduction, websocketUrl } from '../../externals';
import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME } from '../api/constants'; import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME, VIEW_SETTINGS } from '../api/constants';
import { invalidateAllCaches } from '../api/utils'; import { invalidateAllCaches } from '../api/utils';
import { ontimeQueryClient } from '../queryClient'; import { ontimeQueryClient } from '../queryClient';
import { import {
@@ -33,16 +42,14 @@ export const connectSocket = () => {
hasConnected = true; hasConnected = true;
reconnectAttempts = 0; reconnectAttempts = 0;
socketSendJson('set-client-patch', { sendSocket(MessageTag.ClientSet, {
type: 'ontime', type: 'ontime',
origin: window.location.origin, origin: window.location.origin,
path: window.location.pathname + window.location.search, path: window.location.pathname + window.location.search,
name: preferredClientName,
}); });
invalidateAllCaches(); // assume all data to be stale after a reconnect
setOnlineStatus(true); setOnlineStatus(true);
if (preferredClientName) {
socketSendJson('set-client-name', preferredClientName);
}
}; };
websocket.onclose = () => { websocket.onclose = () => {
@@ -65,56 +72,48 @@ export const connectSocket = () => {
console.error('WebSocket error:', error); console.error('WebSocket error:', error);
}; };
websocket.onmessage = (event) => { websocket.onmessage = async (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data) as WsPacketToClient;
const { type, payload } = data; const { tag, payload } = data;
if (!type) { if (!tag) {
return; return;
} }
switch (type) { switch (tag) {
case 'pong': { case MessageTag.Pong: {
const offset = (new Date().getTime() - new Date(payload).getTime()) * 0.5; const offset = (new Date().getTime() - new Date(payload).getTime()) * 0.5;
patchRuntimeProperty('ping', offset); patchRuntimeProperty('ping', offset);
updateDevTools({ ping: offset }); updateDevTools({ ping: offset });
break; break;
} }
case 'client': { case MessageTag.ClientInit: {
if (typeof payload === 'object' || payload !== null) { setClientId(payload.clientId);
if (payload.clientId && payload.clientName) { if (!preferredClientName) {
setClientId(payload.clientId); setClientName(payload.clientName);
if (!preferredClientName) {
setClientName(payload.clientName);
}
}
} }
break; break;
} }
case 'client-rename': { case MessageTag.ClientRename: {
if (typeof payload === 'object') { const id = getClientId();
const id = getClientId(); if (payload.target === id) {
if (payload.target && payload.target === id) { setClientName(payload.name);
setClientName(payload.name);
}
} }
break; break;
} }
case 'client-redirect': { case MessageTag.ClientRedirect: {
if (typeof payload === 'object') { const id = getClientId();
const id = getClientId(); if (payload.target === id) {
if (payload.target && payload.target === id) { setClientRedirect(payload.path);
setClientRedirect(payload.path);
}
} }
break; break;
} }
case 'client-list': { case MessageTag.ClientList: {
setClients(payload); setClients(payload);
if (!isProduction) { if (!isProduction) {
ontimeQueryClient.setQueryData(CLIENT_LIST, payload); ontimeQueryClient.setQueryData(CLIENT_LIST, payload);
@@ -122,50 +121,57 @@ export const connectSocket = () => {
break; break;
} }
case 'dialog': { case MessageTag.Dialog: {
if (payload.dialog === 'welcome') { if (payload.dialog === 'welcome') {
addDialog('welcome'); addDialog('welcome');
} }
break; break;
} }
case 'ontime-log': { case MessageTag.Log: {
addLog(payload as Log); addLog(payload as Log);
break; break;
} }
case 'ontime': { case MessageTag.RuntimeData: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- removing the key from the payload // eslint-disable-next-line @typescript-eslint/no-unused-vars -- removing the key from the payload
const { ping, ...serverPayload } = payload as Partial<RuntimeStore>; const { ping, ...serverPayload } = payload;
patchRuntime(serverPayload); patchRuntime(serverPayload);
updateDevTools(serverPayload); updateDevTools(serverPayload);
break; break;
} }
case 'ontime-patch': { case MessageTag.RuntimePatch: {
const patch = payload as Partial<RuntimeStore>; const patch = payload;
patchRuntime(patch); patchRuntime(patch);
updateDevTools(patch); updateDevTools(patch);
break; break;
} }
case 'ontime-refetch': { case MessageTag.Refetch: {
// the refetch message signals that the rundown has changed in the server side // the refetch message signals that the rundown has changed in the server side
const { reload, target } = payload; const { target, revision } = payload;
if (reload) { switch (target) {
invalidateAllCaches(); case RefetchKey.All:
} else if (target === 'RUNDOWN') { invalidateAllCaches();
const { revision } = payload; break;
const currentRevision = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN)?.revision ?? -1; case RefetchKey.Rundown:
if (revision > currentRevision) { if (revision === (ontimeQueryClient.getQueryData(RUNDOWN) as Rundown).revision) break;
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN }); ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS }); ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
break;
case RefetchKey.ViewSettings:
ontimeQueryClient.invalidateQueries({ queryKey: VIEW_SETTINGS });
break;
case RefetchKey.Report:
ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
break;
default: {
target satisfies never;
break;
} }
} else if (target === 'REPORT') {
ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
} }
break; break;
} }
default: { default: {
console.log('unknown WS message', type); tag satisfies never;
break; break;
} }
} }
@@ -175,20 +181,14 @@ export const connectSocket = () => {
}; };
}; };
export const socketSend = (message: any) => { export function sendSocket<T extends MessageTag | ApiAction>(
tag: T,
payload: T extends MessageTag ? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload'] : unknown,
): void {
if (websocket && websocket.readyState === WebSocket.OPEN) { if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send(message); websocket.send(JSON.stringify({ tag, payload }));
} }
}; }
export const socketSendJson = (type: string, payload?: unknown) => {
socketSend(
JSON.stringify({
type,
payload,
}),
);
};
function updateDevTools(newData: Partial<RuntimeStore>) { function updateDevTools(newData: Partial<RuntimeStore>) {
if (!isProduction) { if (!isProduction) {
@@ -4,7 +4,6 @@ import { Button, Input, Switch, useDisclosure } from '@chakra-ui/react';
import { ViewSettings } from 'ontime-types'; import { ViewSettings } from 'ontime-types';
import { maybeAxiosError } from '../../../../common/api/utils'; import { maybeAxiosError } from '../../../../common/api/utils';
import { postViewSettings } from '../../../../common/api/viewSettings';
import Info from '../../../../common/components/info/Info'; import Info from '../../../../common/components/info/Info';
import { SwatchPickerRHF } from '../../../../common/components/input/colour-input/SwatchPicker'; import { SwatchPickerRHF } from '../../../../common/components/input/colour-input/SwatchPicker';
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink'; import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
@@ -19,17 +18,17 @@ import CodeEditorModal from './StyleEditorModal';
const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/'; const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/';
export default function ViewSettingsForm() { export default function ViewSettingsForm() {
const { data, status, refetch } = useViewSettings(); const { data, isPending, mutateAsync } = useViewSettings();
const { data: info, status: infoStatus } = useInfo(); const { data: info, status: infoStatus } = useInfo();
const { isOpen: isCodeEditorOpen, onOpen: onCodeEditorOpen, onClose: onCodeEditorClose } = useDisclosure(); const { isOpen: isCodeEditorOpen, onOpen: onCodeEditorOpen, onClose: onCodeEditorClose } = useDisclosure();
const { const {
control, control,
handleSubmit, handleSubmit,
setError,
register, register,
reset, reset,
setError, formState: { isSubmitting, isDirty, errors },
formState: { isSubmitting, isDirty },
} = useForm<ViewSettings>({ } = useForm<ViewSettings>({
defaultValues: data, defaultValues: data,
values: data, values: data,
@@ -46,17 +45,11 @@ export default function ViewSettingsForm() {
}, [data, reset]); }, [data, reset]);
const onSubmit = async (formData: ViewSettings) => { const onSubmit = async (formData: ViewSettings) => {
const newData = {
...formData,
};
try { try {
await postViewSettings(newData); mutateAsync(formData);
} catch (error) { } catch (error) {
const message = maybeAxiosError(error); const message = maybeAxiosError(error);
setError('root', { message }); setError('root', { message });
} finally {
await refetch();
} }
}; };
@@ -68,7 +61,7 @@ export default function ViewSettingsForm() {
return null; return null;
} }
const isLoading = status === 'pending' || infoStatus === 'pending'; const isLoading = isPending || infoStatus === 'pending';
return ( return (
<Panel.Section <Panel.Section
@@ -105,6 +98,7 @@ export default function ViewSettingsForm() {
</Info> </Info>
<Panel.Section> <Panel.Section>
<Panel.Loader isLoading={isLoading} /> <Panel.Loader isLoading={isLoading} />
<Panel.Error>{errors.root?.message}</Panel.Error>
<Panel.ListGroup> <Panel.ListGroup>
<CodeEditorModal isOpen={isCodeEditorOpen} onClose={onCodeEditorClose} /> <CodeEditorModal isOpen={isCodeEditorOpen} onClose={onCodeEditorClose} />
<Panel.ListItem> <Panel.ListItem>
@@ -1,8 +1,9 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { MessageTag } from 'ontime-types';
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView'; import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { usePing } from '../../../../common/hooks/useSocket'; import { usePing } from '../../../../common/hooks/useSocket';
import { socketSendJson } from '../../../../common/utils/socket'; import { sendSocket } from '../../../../common/utils/socket';
import { isDockerImage, isOntimeCloud } from '../../../../externals'; import { isDockerImage, isOntimeCloud } from '../../../../externals';
import type { PanelBaseProps } from '../../panel-list/PanelList'; import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils'; import * as Panel from '../../panel-utils/PanelUtils';
@@ -57,10 +58,10 @@ function OntimeCloudStats() {
* Send immediate ping request, and keep sending on an interval * Send immediate ping request, and keep sending on an interval
*/ */
useEffect(() => { useEffect(() => {
socketSendJson('ping', new Date()); sendSocket(MessageTag.Ping, new Date());
const doPing = setInterval(() => { const doPing = setInterval(() => {
socketSendJson('ping', new Date()); sendSocket(MessageTag.Ping, new Date());
}, 5000); }, 5000);
return () => { return () => {
@@ -158,8 +158,10 @@ export default function RundownEntry(props: RundownEntryProps) {
return emitError(`Unknown field: ${field}`); return emitError(`Unknown field: ${field}`);
} }
default: default: {
action satisfies never;
throw new Error(`Unhandled event ${action}`); throw new Error(`Unhandled event ${action}`);
}
} }
}); });
@@ -37,8 +37,10 @@ export function getTimerByType(
return timerObject.clock; return timerObject.clock;
case TimerType.None: case TimerType.None:
return null; return null;
default: default: {
viewTimerType satisfies never;
return null; return null;
}
} }
} }
+63 -109
View File
@@ -6,7 +6,7 @@
* *
* Messages should be in JSON format with two top level objects * Messages should be in JSON format with two top level objects
* { * {
* type: ... * tag: ...
* payload: ... * payload: ...
* } * }
* *
@@ -14,7 +14,15 @@
* Payload: adds necessary payload for the request to be completed * Payload: adds necessary payload for the request to be completed
*/ */
import { Client, LogOrigin } from 'ontime-types'; import {
Client,
LogOrigin,
WsPacketToClient,
WsPacketToServer,
MessageTag,
RefetchKey,
MaybeNumber,
} from 'ontime-types';
import { WebSocket, WebSocketServer } from 'ws'; import { WebSocket, WebSocketServer } from 'ws';
import type { Server } from 'http'; import type { Server } from 'http';
@@ -60,6 +68,12 @@ class SocketServer implements IAdapter {
}); });
const clientId = generateId(); const clientId = generateId();
const clientName = getRandomName(); const clientName = getRandomName();
function sendPacket<T extends MessageTag>(
tag: T,
payload: Pick<WsPacketToClient & { tag: T }, 'payload'>['payload'],
) {
ws.send(JSON.stringify({ tag, payload }));
}
this.clients.set(clientId, { this.clients.set(clientId, {
type: 'unknown', type: 'unknown',
@@ -72,25 +86,12 @@ class SocketServer implements IAdapter {
this.lastConnection = new Date(); this.lastConnection = new Date();
logger.info(LogOrigin.Client, `${this.clients.size} Connections with new: ${clientId}`); logger.info(LogOrigin.Client, `${this.clients.size} Connections with new: ${clientId}`);
ws.send( sendPacket(MessageTag.ClientInit, { clientId, clientName });
JSON.stringify({
type: 'client',
payload: {
clientId,
clientName,
},
}),
);
this.sendClientList(); this.sendClientList();
// send store payload on connect // send store payload on connect
ws.send( sendPacket(MessageTag.RuntimeData, eventStore.poll());
JSON.stringify({
type: 'ontime',
payload: eventStore.poll(),
}),
);
ws.on('error', console.error); ws.on('error', console.error);
@@ -102,104 +103,53 @@ class SocketServer implements IAdapter {
ws.on('message', (data) => { ws.on('message', (data) => {
try { try {
// @ts-expect-error -- this works fine const message = JSON.parse(data.toString()) as WsPacketToServer;
const message = JSON.parse(data); const { tag, payload } = message;
const { type, payload } = message;
if (type === 'ping') { switch (tag) {
ws.send( case MessageTag.Ping: {
JSON.stringify({ sendPacket(MessageTag.Pong, payload);
type: 'pong', break;
payload,
}),
);
return;
}
if (type === 'get-client-name') {
ws.send(
JSON.stringify({
type: 'client-name',
payload: this.getOrCreateClient(clientId),
}),
);
return;
}
if (type === 'set-client-patch') {
if (payload && typeof payload == 'object') {
this.clients.set(clientId, { ...this.clients.get(clientId), ...payload });
} }
this.sendClientList(); case MessageTag.ClientSet: {
return;
}
if (type === 'set-client-type') {
if (payload && typeof payload == 'string') {
const previousData = this.getOrCreateClient(clientId); const previousData = this.getOrCreateClient(clientId);
this.clients.set(clientId, { ...previousData, type: payload }); this.clients.set(clientId, { ...previousData, ...payload });
this.sendClientList();
break;
} }
this.sendClientList(); case MessageTag.ClientSetPath: {
return;
}
if (type === 'set-client-path') {
if (payload && typeof payload == 'string') {
const previousData = this.getOrCreateClient(clientId); const previousData = this.getOrCreateClient(clientId);
previousData.path = payload; previousData.path = payload;
this.clients.set(clientId, previousData); this.clients.set(clientId, previousData);
if (payload.includes('editor') && this.shouldShowWelcome) { if (payload.includes('editor') && this.shouldShowWelcome) {
this.shouldShowWelcome = false; this.shouldShowWelcome = false;
ws.send( sendPacket(MessageTag.Dialog, { dialog: 'welcome' });
JSON.stringify({
type: 'dialog',
payload: { dialog: 'welcome' },
}),
);
} }
this.sendClientList();
break;
} }
case MessageTag.Log: {
this.sendClientList();
return;
}
if (type === 'set-client-name') {
if (payload) {
const previousData = this.getOrCreateClient(clientId);
logger.info(LogOrigin.Client, `Client ${previousData.name} renamed to ${payload}`);
this.clients.set(clientId, { ...previousData, name: payload });
ws.send(
JSON.stringify({
type: 'client-name',
payload: this.getOrCreateClient(clientId).name,
}),
);
}
this.sendClientList();
return;
}
if (type === 'ontime-log') {
if (payload.level && payload.origin && payload.text) {
logger.emit(payload.level, payload.origin, payload.text); logger.emit(payload.level, payload.origin, payload.text);
break;
} }
return; default: {
} tag satisfies never;
// Protocol specific stuff handled above
// Protocol specific stuff handled above try {
try { const reply = dispatchFromAdapter(tag, payload, 'ws');
const reply = dispatchFromAdapter(type, payload, 'ws'); if (reply) {
if (reply) { ws.send(
ws.send( JSON.stringify({
JSON.stringify({ type: tag,
type, payload: reply.payload,
payload: reply.payload, }),
}), );
); }
} catch (error) {
logger.error(LogOrigin.Rx, `WS IN: ${error}`);
}
break;
} }
} catch (error) {
logger.error(LogOrigin.Rx, `WS IN: ${error}`);
} }
} catch (_) { } catch (_) {
// we ignore unknown // we ignore unknown
@@ -230,7 +180,7 @@ class SocketServer implements IAdapter {
private sendClientList(): void { private sendClientList(): void {
const payload = Object.fromEntries(this.clients.entries()); const payload = Object.fromEntries(this.clients.entries());
this.sendAsJson({ type: 'client-list', payload }); this.sendAsJson(MessageTag.ClientList, payload);
} }
public getClientList(): string[] { public getClientList(): string[] {
@@ -244,10 +194,7 @@ class SocketServer implements IAdapter {
} }
logger.info(LogOrigin.Client, `Client ${previousData.name} renamed to ${name}`); logger.info(LogOrigin.Client, `Client ${previousData.name} renamed to ${name}`);
this.clients.set(target, { ...previousData, name }); this.clients.set(target, { ...previousData, name });
this.sendAsJson({ this.sendAsJson(MessageTag.ClientRename, { name, target });
type: 'client-rename',
payload: { name, target },
});
this.sendClientList(); this.sendClientList();
} }
@@ -256,7 +203,7 @@ class SocketServer implements IAdapter {
if (!previousData) { if (!previousData) {
throw new Error(`Client "${target}" not found`); throw new Error(`Client "${target}" not found`);
} }
this.sendAsJson({ type: 'client-redirect', payload: { target, path } }); this.sendAsJson(MessageTag.ClientRedirect, { target, path });
} }
public identifyClient(target: string, identify: boolean) { public identifyClient(target: string, identify: boolean) {
@@ -269,9 +216,9 @@ class SocketServer implements IAdapter {
} }
// message is any serializable value // message is any serializable value
public sendAsJson(message: unknown) { public sendAsJson<T extends MessageTag>(tag: T, payload: Pick<WsPacketToClient & { tag: T }, 'payload'>['payload']) {
try { try {
const stringifiedMessage = JSON.stringify(message); const stringifiedMessage = JSON.stringify({ tag, payload });
this.wss?.clients.forEach((client) => { this.wss?.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) {
client.send(stringifiedMessage); client.send(stringifiedMessage);
@@ -288,3 +235,10 @@ class SocketServer implements IAdapter {
} }
export const socket = new SocketServer(); export const socket = new SocketServer();
/**
* Utility function to notify clients that the REST data is stale
*/
export function sendRefetch(target: RefetchKey, revision: MaybeNumber = null) {
socket.sendAsJson(MessageTag.Refetch, { target, revision });
}
-17
View File
@@ -1,17 +0,0 @@
import { socket } from './WebsocketAdapter.js';
export enum RefetchTargets {
Rundown = 'rundown',
Report = 'report',
}
/**
* Utility function to notify clients that the REST data is stale
* @param payload -- possible patch payload
*/
export function sendRefetch(payload: unknown = null) {
socket.sendAsJson({
type: 'ontime-refetch',
payload,
});
}
@@ -115,8 +115,10 @@ export function testConditions(
return typeof fieldValue === 'string' && fieldValue.includes(value); return typeof fieldValue === 'string' && fieldValue.includes(value);
case 'not_contains': case 'not_contains':
return typeof fieldValue === 'string' && !fieldValue.includes(value); return typeof fieldValue === 'string' && !fieldValue.includes(value);
default: default: {
operator satisfies never;
return false; return false;
}
} }
} }
} }
@@ -5,7 +5,8 @@ import { auxTimerService } from '../../../services/aux-timer-service/AuxTimerSer
import * as messageService from '../../../services/message-service/MessageService.js'; import * as messageService from '../../../services/message-service/MessageService.js';
export function toOntimeAction(action: OntimeAction) { export function toOntimeAction(action: OntimeAction) {
switch (action.action) { const actionType = action.action;
switch (actionType) {
// Aux timer actions // Aux timer actions
case 'aux-start': case 'aux-start':
auxTimerService.start(); auxTimerService.start();
@@ -40,9 +41,10 @@ export function toOntimeAction(action: OntimeAction) {
break; break;
} }
default: default: {
// @ts-expect-error -- this guard checks that we handled all the cases, but we still want to log just in case actionType satisfies never;
logger.warning(LogOrigin.Tx, `Unknown action type: ${action.type}`); logger.warning(LogOrigin.Tx, `Unknown action type: ${actionType}`);
break; break;
}
} }
} }
@@ -1,7 +1,7 @@
import { OntimeReport, OntimeEventReport, TimerLifeCycle } from 'ontime-types'; import { OntimeReport, OntimeEventReport, TimerLifeCycle, RefetchKey } from 'ontime-types';
import { RuntimeState } from '../../stores/runtimeState.js'; import { RuntimeState } from '../../stores/runtimeState.js';
import { RefetchTargets, sendRefetch } from '../../adapters/websocketAux.js';
import { DeepReadonly } from 'ts-essentials'; import { DeepReadonly } from 'ts-essentials';
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
const report = new Map<string, OntimeEventReport>(); const report = new Map<string, OntimeEventReport>();
@@ -57,8 +57,6 @@ export function triggerReportEntry(
const startedAt = report.get(eventId)?.startedAt ?? null; const startedAt = report.get(eventId)?.startedAt ?? null;
report.set(eventId, { startedAt, endedAt: state.clock }); report.set(eventId, { startedAt, endedAt: state.clock });
formattedReport = null; formattedReport = null;
sendRefetch({ sendRefetch(RefetchKey.Report);
target: RefetchTargets.Report,
});
} }
} }
@@ -10,17 +10,18 @@ import {
OntimeEntry, OntimeEntry,
OntimeEvent, OntimeEvent,
PatchWithId, PatchWithId,
RefetchKey,
Rundown, Rundown,
} from 'ontime-types'; } from 'ontime-types';
import { customFieldLabelToKey } from 'ontime-utils'; import { customFieldLabelToKey } from 'ontime-utils';
import { updateRundownData } from '../../stores/runtimeState.js'; import { updateRundownData } from '../../stores/runtimeState.js';
import { sendRefetch } from '../../adapters/websocketAux.js';
import { runtimeService } from '../../services/runtime-service/RuntimeService.js'; import { runtimeService } from '../../services/runtime-service/RuntimeService.js';
import { createTransaction, customFieldMutation, rundownCache, rundownMutation } from './rundown.dao.js'; import { createTransaction, customFieldMutation, rundownCache, rundownMutation } from './rundown.dao.js';
import type { RundownMetadata } from './rundown.types.js'; import type { RundownMetadata } from './rundown.types.js';
import { generateEvent, getInsertAfterId, hasChanges } from './rundown.utils.js'; import { generateEvent, getInsertAfterId, hasChanges } from './rundown.utils.js';
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
/** /**
* creates a new entry with given data * creates a new entry with given data
@@ -553,13 +554,10 @@ export function notifyChanges(rundownMetadata: RundownMetadata, revision: number
} }
// notify external services of changes // notify external services of changes
if (options.external) { if (options.reload) {
const payload = { sendRefetch(RefetchKey.All);
target: 'RUNDOWN', } else if (options.external) {
reload: options.reload, sendRefetch(RefetchKey.Rundown, revision);
revision,
};
sendRefetch(payload);
} }
} }
@@ -1,29 +0,0 @@
import type { ErrorResponse, ViewSettings } from 'ontime-types';
import { getErrorMessage } from 'ontime-utils';
import type { Request, Response } from 'express';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
export async function getViewSettings(_req: Request, res: Response<ViewSettings>) {
const views = getDataProvider().getViewSettings();
res.status(200).send(views);
}
export async function postViewSettings(req: Request, res: Response<ViewSettings | ErrorResponse>) {
try {
const newData = {
dangerColor: req.body.dangerColor,
endMessage: req.body.endMessage,
freezeEnd: req.body.freezeEnd,
normalColor: req.body.normalColor,
overrideStyles: req.body.overrideStyles,
warningColor: req.body.warningColor,
} as ViewSettings;
await getDataProvider().setViewSettings(newData);
res.status(200).send(newData);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
}
@@ -1,9 +1,36 @@
import express from 'express'; import express from 'express';
import type { Request, Response } from 'express';
import { RefetchKey, type ErrorResponse, type ViewSettings } from 'ontime-types';
import { getErrorMessage } from 'ontime-utils';
import { validateViewSettings } from './viewSettings.validation.js'; import { validateViewSettings } from './viewSettings.validation.js';
import { getViewSettings, postViewSettings } from './viewSettings.controller.js'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
export const router = express.Router(); export const router = express.Router();
router.get('/', getViewSettings); router.get('/', (_req: Request, res: Response<ViewSettings>) => {
router.post('/', validateViewSettings, postViewSettings); const views = getDataProvider().getViewSettings();
res.status(200).send(views);
});
router.post('/', validateViewSettings, async (req: Request, res: Response<ViewSettings | ErrorResponse>) => {
try {
const newData = {
dangerColor: req.body.dangerColor,
endMessage: req.body.endMessage,
freezeEnd: req.body.freezeEnd,
normalColor: req.body.normalColor,
overrideStyles: req.body.overrideStyles,
warningColor: req.body.warningColor,
} as ViewSettings;
await getDataProvider().setViewSettings(newData);
res.status(200).send(newData);
setImmediate(() => {
sendRefetch(RefetchKey.ViewSettings);
});
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
@@ -1,4 +1,12 @@
import { MessageState, OffsetMode, OntimeEvent, PatchWithId, SimpleDirection, SimplePlayback } from 'ontime-types'; import {
ApiAction,
MessageState,
OffsetMode,
OntimeEvent,
PatchWithId,
SimpleDirection,
SimplePlayback,
} from 'ontime-types';
import { MILLIS_PER_HOUR } from 'ontime-utils'; import { MILLIS_PER_HOUR } from 'ontime-utils';
import { DeepPartial } from 'ts-essentials'; import { DeepPartial } from 'ts-essentials';
@@ -21,15 +29,15 @@ import { willCauseRegeneration } from '../api-data/rundown/rundown.utils.js';
const throttledEditEvent = throttle(editEntry, 20); const throttledEditEvent = throttle(editEntry, 20);
let lastRequest: Date | null = null; let lastRequest: Date | null = null;
export function dispatchFromAdapter(type: string, payload: unknown, _source?: 'osc' | 'ws' | 'http') { export function dispatchFromAdapter(tag: string, payload: unknown, _source?: 'osc' | 'ws' | 'http') {
const action = type.toLowerCase(); const action = tag.toLowerCase();
const handler = actionHandlers[action]; const handler = actionHandlers[action as ApiAction];
lastRequest = new Date(); lastRequest = new Date();
if (handler) { if (handler) {
return handler(payload); return handler(payload);
} else { } else {
throw new Error(`Unhandled message ${type}`); throw new Error(`Unhandled message ${tag}`);
} }
} }
@@ -39,7 +47,7 @@ export function getLastRequest() {
type ActionHandler = (payload: unknown) => { payload: unknown }; type ActionHandler = (payload: unknown) => { payload: unknown };
const actionHandlers: Record<string, ActionHandler> = { const actionHandlers: Record<ApiAction, ActionHandler> = {
/* General */ /* General */
version: () => ({ payload: ONTIME_VERSION }), version: () => ({ payload: ONTIME_VERSION }),
poll: () => ({ poll: () => ({
+3 -2
View File
@@ -73,6 +73,7 @@ if (!isProduction) {
app.use(serverTiming()); app.use(serverTiming());
} }
app.disable('x-powered-by'); app.disable('x-powered-by');
app.enable('etag');
// Implement middleware // Implement middleware
app.use(cors()); // setup cors for all routes app.use(cors()); // setup cors for all routes
@@ -88,12 +89,12 @@ app.use(`${prefix}/data`, authenticate, appRouter); // router for application da
app.use(`${prefix}/api`, authenticate, integrationRouter); // router for integrations app.use(`${prefix}/api`, authenticate, integrationRouter); // router for integrations
// serve static external files // serve static external files
app.use(`${prefix}/external`, express.static(publicDir.externalDir)); app.use(`${prefix}/external`, express.static(publicDir.externalDir, { etag: false, lastModified: true }));
app.use(`${prefix}/external`, (req, res) => { app.use(`${prefix}/external`, (req, res) => {
// if the user reaches to the root, we show a 404 // if the user reaches to the root, we show a 404
res.status(404).send(`${req.originalUrl} not found`); res.status(404).send(`${req.originalUrl} not found`);
}); });
app.use(`${prefix}/user`, express.static(publicDir.userDir)); app.use(`${prefix}/user`, express.static(publicDir.userDir, { etag: false, lastModified: true }));
// Base route for static files // Base route for static files
app.use(`${prefix}`, authenticateAndRedirect, compressedStatic); app.use(`${prefix}`, authenticateAndRedirect, compressedStatic);
+2 -5
View File
@@ -1,4 +1,4 @@
import { Log, LogLevel } from 'ontime-types'; import { Log, LogLevel, MessageTag } from 'ontime-types';
import { generateId, millisToString } from 'ontime-utils'; import { generateId, millisToString } from 'ontime-utils';
import { socket } from '../adapters/WebsocketAdapter.js'; import { socket } from '../adapters/WebsocketAdapter.js';
@@ -54,10 +54,7 @@ class Logger {
} }
try { try {
socket.sendAsJson({ socket.sendAsJson(MessageTag.Log, log);
type: 'ontime-log',
payload: log,
});
} catch (_e) { } catch (_e) {
this.addToQueue(log); this.addToQueue(log);
} }
+7 -7
View File
@@ -1,4 +1,4 @@
import { RuntimeStore } from 'ontime-types'; import { RuntimeStore, MessageTag } from 'ontime-types';
import { socket } from '../adapters/WebsocketAdapter.js'; import { socket } from '../adapters/WebsocketAdapter.js';
import { isEmptyObject } from '../utils/parserUtils.js'; import { isEmptyObject } from '../utils/parserUtils.js';
@@ -23,7 +23,7 @@ export const eventStore = {
}, },
set<T extends keyof RuntimeStore>(key: T, value: RuntimeStore[T]) { set<T extends keyof RuntimeStore>(key: T, value: RuntimeStore[T]) {
store[key] = value; store[key] = value;
socket.sendAsJson({ type: 'ontime-patch', payload: { [key]: value } }); socket.sendAsJson(MessageTag.RuntimePatch, { [key]: value });
}, },
createBatch() { createBatch() {
const patch: Partial<RuntimeStore> = {}; const patch: Partial<RuntimeStore> = {};
@@ -34,7 +34,7 @@ export const eventStore = {
send() { send() {
if (isEmptyObject(patch)) return; if (isEmptyObject(patch)) return;
store = { ...store, ...patch }; store = { ...store, ...patch };
socket.sendAsJson({ type: 'ontime-patch', payload: patch }); socket.sendAsJson(MessageTag.RuntimePatch, patch);
}, },
}; };
}, },
@@ -42,9 +42,9 @@ export const eventStore = {
return store as RuntimeStore; return store as RuntimeStore;
}, },
broadcast() { broadcast() {
socket.sendAsJson({ socket.sendAsJson(
type: 'ontime', MessageTag.RuntimeData,
payload: store, store as RuntimeStore, // We assume that it has been initialized at this point
}); );
}, },
}; };
@@ -0,0 +1,15 @@
export type ApiAction =
| 'version'
| 'poll'
| 'change'
| 'message'
| 'start'
| 'pause'
| 'stop'
| 'reload'
| 'roll'
| 'load'
| 'addtime'
| 'auxtimer'
| 'client'
| 'offsetmode';
@@ -0,0 +1,63 @@
import type { Client } from '../../definitions/Clients.type.js';
import type { Log } from '../../definitions/runtime/Logger.type.js';
import type { RuntimeStore } from '../../definitions/runtime/RuntimeStore.type.js';
import type { MaybeNumber } from '../../utils/utils.type.js';
import type { RefetchKey } from './refetch.type.js';
export enum MessageTag {
Ping = 'ping',
Pong = 'pong',
ClientInit = 'client-init',
ClientSet = 'client-set',
ClientSetPath = 'client-set-path',
ClientRename = 'client-rename',
ClientRedirect = 'client-redirect',
ClientList = 'client-list',
Dialog = 'dialog',
Log = 'log',
RuntimeData = 'runtime-data',
RuntimePatch = 'runtime-patch',
Refetch = 'refetch',
}
//CLIENT TO SERVER
type PingPacket = { tag: MessageTag.Ping; payload: Date };
type SetClientPacket = { tag: MessageTag.ClientSetPath; payload: string };
type SetClientPathPacket = { tag: MessageTag.ClientSet; payload: Partial<Client> };
// SERVER TO CLIENT
type PongPacket = { tag: MessageTag.Pong; payload: Date };
type InitClientPacket = { tag: MessageTag.ClientInit; payload: { clientId: string; clientName: string } };
type RenameClientPacket = { tag: MessageTag.ClientRename; payload: { target: string; name: string } };
type RedirectClientPacket = { tag: MessageTag.ClientRedirect; payload: { target: string; path: string } };
type DialogPacket = { tag: MessageTag.Dialog; payload: { dialog: string } };
type ListClientPacket = {
tag: MessageTag.ClientList;
payload: Record<string, Client>;
};
type RuntimePacket = { tag: MessageTag.RuntimeData; payload: RuntimeStore };
type RuntimePatchPacket = { tag: MessageTag.RuntimePatch; payload: Partial<RuntimeStore> };
type RefetchPacket = {
tag: MessageTag.Refetch;
payload: {
target: RefetchKey;
revision: MaybeNumber;
};
};
// SHARED
type LogPacket = { tag: MessageTag.Log; payload: Log };
export type WsPacketToServer = PingPacket | SetClientPacket | SetClientPathPacket | LogPacket;
export type WsPacketToClient =
| PongPacket
| InitClientPacket
| RenameClientPacket
| RedirectClientPacket
| DialogPacket
| LogPacket
| ListClientPacket
| RuntimePacket
| RuntimePatchPacket
| RefetchPacket;
@@ -0,0 +1,6 @@
export enum RefetchKey {
All = 'all',
Report = 'report',
Rundown = 'rundown',
ViewSettings = 'view-settings',
}
+5
View File
@@ -85,6 +85,11 @@ export type {
TransientEventPayload, TransientEventPayload,
} from './api/rundown-controller/BackendResponse.type.js'; } from './api/rundown-controller/BackendResponse.type.js';
// web socket
export { MessageTag } from './api/websocket/data.type.js';
export type { WsPacketToServer, WsPacketToClient } from './api/websocket/data.type.js';
export { RefetchKey } from './api/websocket/refetch.type.js';
export type { ApiAction } from './api/websocket/api.type.js';
// SERVER RUNTIME // SERVER RUNTIME
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js'; export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
export { Playback } from './definitions/runtime/Playback.type.js'; export { Playback } from './definitions/runtime/Playback.type.js';