mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-08 17:03:53 +00:00
refactor: improve typing in backend (#748)
This commit is contained in:
@@ -13,8 +13,8 @@
|
||||
"express-session": "^1.17.3",
|
||||
"express-static-gzip": "^2.1.7",
|
||||
"express-validator": "^6.14.2",
|
||||
"got": "^14.0.0",
|
||||
"google-auth-library": "^9.4.2",
|
||||
"got": "^14.0.0",
|
||||
"lowdb": "^7.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-osc": "^9.0.2",
|
||||
@@ -31,6 +31,7 @@
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-osc": "^6.0.2",
|
||||
"@types/websocket": "^1.0.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"esbuild": "^0.19.10",
|
||||
|
||||
@@ -45,7 +45,7 @@ export class SocketServer implements IAdapter {
|
||||
}
|
||||
|
||||
init(server: Server) {
|
||||
this.wss = new WebSocketServer({ path: '/ws', server });
|
||||
this.wss = new WebSocketServer({ path: '/ws', server, maxPayload: this.MAX_PAYLOAD });
|
||||
|
||||
this.wss.on('connection', (ws) => {
|
||||
let clientId = getRandomName();
|
||||
@@ -75,11 +75,8 @@ export class SocketServer implements IAdapter {
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
if (data.length > this.MAX_PAYLOAD) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-expect-error -- ??
|
||||
const message = JSON.parse(data);
|
||||
const { type, payload } = message;
|
||||
|
||||
@@ -110,11 +107,6 @@ export class SocketServer implements IAdapter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'hello') {
|
||||
ws.send('hi');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'ontime-log') {
|
||||
if (payload.level && payload.origin && payload.text) {
|
||||
logger.emit(payload.level, payload.origin, payload.text);
|
||||
@@ -148,8 +140,12 @@ export class SocketServer implements IAdapter {
|
||||
// message is any serializable value
|
||||
sendAsJson(message: unknown) {
|
||||
this.wss?.clients.forEach((client) => {
|
||||
if (client !== this.wss && client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
try {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
} catch (_) {
|
||||
/** We do not handle this error */
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class Logger {
|
||||
* @param origin
|
||||
* @param text
|
||||
*/
|
||||
emit(level, origin: string, text: string) {
|
||||
emit(level: LogLevel, origin: string, text: string) {
|
||||
const log = {
|
||||
id: generateId(),
|
||||
level,
|
||||
|
||||
@@ -74,12 +74,12 @@ export class EventLoader {
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!currentEventId) {
|
||||
return timedEvents.at(0);
|
||||
return timedEvents.at(0) ?? null;
|
||||
}
|
||||
|
||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||
const newIndex = Math.max(currentIndex - 1, 0);
|
||||
const previousEvent = timedEvents.at(newIndex);
|
||||
const previousEvent = timedEvents.at(newIndex) ?? null;
|
||||
return previousEvent;
|
||||
}
|
||||
|
||||
@@ -95,12 +95,12 @@ export class EventLoader {
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!currentEventId) {
|
||||
return timedEvents.at(0);
|
||||
return timedEvents.at(0) ?? null;
|
||||
}
|
||||
|
||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||
const newIndex = (currentIndex + 1) % timedEvents.length;
|
||||
const nextEvent = timedEvents.at(newIndex);
|
||||
return nextEvent;
|
||||
return nextEvent ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { body, check, validationResult } from 'express-validator';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
import {
|
||||
validateHttpSubscriptionObject,
|
||||
@@ -20,7 +21,7 @@ export const viewValidator = [
|
||||
check('dangerColor').isString().trim().withMessage('dangerColor value must be string'),
|
||||
check('warningThreshold').isNumeric().withMessage('warningThreshold value must be a number'),
|
||||
check('dangerThreshold').isNumeric().withMessage('dangerThreshold value must a number'),
|
||||
(req, res, next) => {
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -35,7 +36,8 @@ export const validateAliases = [
|
||||
body('*.enabled').isBoolean(),
|
||||
body('*.alias').isString().trim(),
|
||||
body('*.pathAndParams').isString().trim(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -56,7 +58,8 @@ export const validateUserFields = [
|
||||
body('user7').exists().isString().trim(),
|
||||
body('user8').exists().isString().trim(),
|
||||
body('user9').exists().isString().trim(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -72,7 +75,8 @@ export const validateSettings = [
|
||||
body('timeFormat').isString().isIn(['12', '24']),
|
||||
body('language').isString(),
|
||||
body('serverPort').isPort().optional(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -91,7 +95,7 @@ export const validateOSC = [
|
||||
body('subscriptions')
|
||||
.isObject()
|
||||
.custom((value) => validateOscSubscriptionObject(value)),
|
||||
(req, res, next) => {
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -106,7 +110,8 @@ export const validateHTTP = [
|
||||
body('subscriptions')
|
||||
.isObject()
|
||||
.custom((value) => validateHttpSubscriptionObject(value)),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -135,7 +140,8 @@ export const validateOscSubscription = [
|
||||
body('onFinish')
|
||||
.isArray()
|
||||
.custom((value) => validateOscSubscriptionCycle(value)),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -150,7 +156,8 @@ export const validatePatchProjectFile = [
|
||||
body('aliases').isArray().optional({ nullable: false }),
|
||||
body('userFields').isObject().optional({ nullable: false }),
|
||||
body('osc').isObject().optional({ nullable: false }),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -162,7 +169,8 @@ export const validatePatchProjectFile = [
|
||||
*/
|
||||
export const validateLoadProjectFile = [
|
||||
body('filename').exists().withMessage('Filename is required').isString().withMessage('Filename must be a string'),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
@@ -183,7 +191,7 @@ export const validateProjectDuplicate = [
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('New project filename must be between 1 and 255 characters'),
|
||||
|
||||
(req, res, next) => {
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
@@ -205,7 +213,7 @@ export const validateProjectRename = [
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Duplicate project filename must be between 1 and 255 characters'),
|
||||
|
||||
(req, res, next) => {
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
@@ -226,7 +234,8 @@ export const validateProjectCreate = [
|
||||
.withMessage('Filename must be a string')
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Filename must be between 1 and 255 characters'),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
@@ -269,7 +278,8 @@ export const validateProjectFiles = (projectFiles: { filename?: string; newFilen
|
||||
|
||||
export const validateSheetid = [
|
||||
body('id').exists().isString(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -279,7 +289,8 @@ export const validateSheetid = [
|
||||
export const validateWorksheet = [
|
||||
body('id').exists().isString(),
|
||||
body('worksheet').exists().isString(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -289,7 +300,8 @@ export const validateWorksheet = [
|
||||
export const validateSheetOptions = [
|
||||
body('id').exists().isString(),
|
||||
// body('options').exists().isObject(), TODO:
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { body, validationResult } from 'express-validator';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export const projectSanitiser = [
|
||||
body('title').optional().isString().trim(),
|
||||
@@ -8,7 +9,8 @@ export const projectSanitiser = [
|
||||
body('backstageUrl').optional().isString().trim(),
|
||||
body('backstageInfo').optional().isString().trim(),
|
||||
body('endMessage').optional().isString().trim(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { body, param, validationResult } from 'express-validator';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export const rundownPostValidator = [
|
||||
body('type').isString().exists().isIn(['event', 'delay', 'block']),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -11,7 +13,8 @@ export const rundownPostValidator = [
|
||||
|
||||
export const rundownPutValidator = [
|
||||
body('id').isString().exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -21,7 +24,8 @@ export const rundownPutValidator = [
|
||||
export const rundownBatchPutValidator = [
|
||||
body('data').isObject().exists(),
|
||||
body('ids').isArray().exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -32,7 +36,8 @@ export const rundownReorderValidator = [
|
||||
body('eventId').isString().exists(),
|
||||
body('from').isNumeric().exists(),
|
||||
body('to').isNumeric().exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -42,7 +47,8 @@ export const rundownReorderValidator = [
|
||||
export const rundownSwapValidator = [
|
||||
body('from').isString().exists(),
|
||||
body('to').isString().exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -51,7 +57,8 @@ export const rundownSwapValidator = [
|
||||
|
||||
export const paramsMustHaveEventId = [
|
||||
param('eventId').exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
|
||||
@@ -57,7 +57,7 @@ async function loadDb() {
|
||||
|
||||
const data = await parseDb(dbInDisk, db);
|
||||
if (data === null) {
|
||||
console.log('ERROR: Invalid JSON format');
|
||||
console.error('ERROR: Invalid JSON format');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Playback } from 'ontime-types';
|
||||
import { MaybeNumber, MaybeString, Playback } from 'ontime-types';
|
||||
|
||||
import { JSONFile } from 'lowdb/node';
|
||||
import { resolveRestoreFile } from '../setup.js';
|
||||
|
||||
export type RestorePoint = {
|
||||
playback: Playback;
|
||||
selectedEventId: string | null;
|
||||
startedAt: number | null;
|
||||
addedTime: number | null;
|
||||
pausedAt: number | null;
|
||||
selectedEventId: MaybeString;
|
||||
startedAt: MaybeNumber;
|
||||
addedTime: number;
|
||||
pausedAt: MaybeNumber;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ export function isRestorePoint(obj: unknown): obj is RestorePoint {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof restorePoint.addedTime !== 'number' && restorePoint.addedTime !== null) {
|
||||
if (typeof restorePoint.addedTime !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ export function isRestorePoint(obj: unknown): obj is RestorePoint {
|
||||
* that can then be restored when reopening
|
||||
*/
|
||||
export class RestoreService {
|
||||
private readonly filePath: string | null;
|
||||
private readonly filePath: MaybeString;
|
||||
private readonly file: JSONFile<RestorePoint | null>;
|
||||
private lastStore: string | null;
|
||||
private lastStore: MaybeString;
|
||||
private failedCreateAttempts: number;
|
||||
|
||||
constructor(filePath: string) {
|
||||
@@ -128,7 +128,7 @@ export class RestoreService {
|
||||
*/
|
||||
async clear() {
|
||||
try {
|
||||
await this.write(null);
|
||||
await this.file.write(null);
|
||||
} catch (_error) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { isRestorePoint, RestorePoint, RestoreService } from '../RestoreService.
|
||||
|
||||
describe('isRestorePoint()', () => {
|
||||
it('validates a well defined object', () => {
|
||||
let restorePoint = {
|
||||
playback: 'play',
|
||||
let restorePoint: RestorePoint = {
|
||||
playback: Playback.Roll,
|
||||
selectedEventId: '123',
|
||||
startedAt: 1,
|
||||
addedTime: 2,
|
||||
@@ -17,10 +17,10 @@ describe('isRestorePoint()', () => {
|
||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||
|
||||
restorePoint = {
|
||||
playback: 'roll',
|
||||
playback: Playback.Roll,
|
||||
selectedEventId: '123',
|
||||
startedAt: null,
|
||||
addedTime: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||
@@ -32,7 +32,7 @@ describe('isRestorePoint()', () => {
|
||||
playback: 'unknown',
|
||||
selectedEventId: '123',
|
||||
startedAt: null,
|
||||
addedTime: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||
@@ -41,17 +41,17 @@ describe('isRestorePoint()', () => {
|
||||
const restorePoint = {
|
||||
selectedEventId: '123',
|
||||
startedAt: null,
|
||||
addedTime: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||
});
|
||||
it('with incorrect value', () => {
|
||||
const restorePoint = {
|
||||
playback: 'roll',
|
||||
playback: Playback.Roll,
|
||||
selectedEventId: '123',
|
||||
startedAt: 'testing',
|
||||
addedTime: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||
@@ -62,7 +62,7 @@ describe('isRestorePoint()', () => {
|
||||
describe('RestoreService()', () => {
|
||||
describe('load()', () => {
|
||||
it('loads working file with times', async () => {
|
||||
const expected = {
|
||||
const expected: RestorePoint = {
|
||||
playback: Playback.Play,
|
||||
selectedEventId: 'da5b4',
|
||||
startedAt: 1234,
|
||||
@@ -78,11 +78,11 @@ describe('RestoreService()', () => {
|
||||
});
|
||||
|
||||
it('loads working file without times', async () => {
|
||||
const expected = {
|
||||
const expected: RestorePoint = {
|
||||
playback: Playback.Stop,
|
||||
selectedEventId: null,
|
||||
startedAt: null,
|
||||
addedTime: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isOntimeBlock, isOntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown } from 'ontime-types';
|
||||
|
||||
import { deleteAtIndex } from '../utils/arrayUtils.js';
|
||||
|
||||
@@ -6,7 +6,11 @@ export function _applyDelay(eventId: string, rundown: OntimeRundown): OntimeRund
|
||||
const delayIndex = rundown.findIndex((event) => event.id === eventId);
|
||||
const delayEvent = rundown.at(delayIndex);
|
||||
|
||||
if (delayEvent.type !== SupportedEvent.Delay) {
|
||||
if (!delayEvent) {
|
||||
throw new Error('Given event ID not found');
|
||||
}
|
||||
|
||||
if (!isOntimeDelay(delayEvent)) {
|
||||
throw new Error('Given event ID is not a delay');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export type GetTimeFn = () => number;
|
||||
|
||||
export class ExtraTimerService {
|
||||
private timer: SimpleTimer;
|
||||
private interval: NodeJS.Timer;
|
||||
private interval: NodeJS.Timer | null = null;
|
||||
private emit: EmitFn;
|
||||
private getTime: GetTimeFn;
|
||||
|
||||
@@ -23,7 +23,9 @@ export class ExtraTimerService {
|
||||
}
|
||||
|
||||
private stopInterval() {
|
||||
clearInterval(this.interval);
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
|
||||
@@ -19,7 +19,7 @@ function formatDisplayFromString(value: string, hideZero = false): string {
|
||||
return formatted;
|
||||
}
|
||||
|
||||
type AliasesDefinition = Record<string, { key: string; cb: (value: unknown) => string }>;
|
||||
type AliasesDefinition = Record<string, { key: string; cb: (value: string) => string }>;
|
||||
const quickAliases: AliasesDefinition = {
|
||||
clock: { key: 'timer.clock', cb: (value: string) => formatDisplayFromString(value) },
|
||||
duration: { key: 'timer.duration', cb: (value: string) => formatDisplayFromString(value, true) },
|
||||
|
||||
@@ -88,6 +88,9 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
||||
}
|
||||
|
||||
export async function editEvent(eventData: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
|
||||
if (!eventData?.id) {
|
||||
throw new Error('Event misses ID');
|
||||
}
|
||||
if (isOntimeEvent(eventData) && eventData?.cue === '') {
|
||||
throw new Error('Cue value invalid');
|
||||
}
|
||||
@@ -114,7 +117,7 @@ export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>)
|
||||
* @param eventId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deleteEvent(eventId) {
|
||||
export async function deleteEvent(eventId: string) {
|
||||
await cachedDelete(eventId);
|
||||
|
||||
notifyChanges({ timer: [eventId], external: true });
|
||||
|
||||
@@ -12,7 +12,7 @@ import { state, stateMutations } from '../../state.js';
|
||||
* Coordinating with necessary services
|
||||
*/
|
||||
class RuntimeService {
|
||||
private eventTimer: TimerService;
|
||||
private eventTimer: TimerService | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -28,8 +28,10 @@ class RuntimeService {
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
logger.info(LogOrigin.Server, 'Runtime service shutting down');
|
||||
this.eventTimer.shutdown();
|
||||
if (this.eventTimer) {
|
||||
logger.info(LogOrigin.Server, 'Runtime service shutting down');
|
||||
this.eventTimer.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,9 +15,8 @@ export const normaliseEndTime = (start: number, end: number) => (end < start ? e
|
||||
*/
|
||||
export function getExpectedFinish(state: TState): MaybeNumber {
|
||||
const { startedAt, finishedAt, duration, addedTime } = state.timer;
|
||||
const { timerType } = state.eventNow;
|
||||
const { timerType, timeEnd } = state.eventNow;
|
||||
const { pausedAt } = state._timer;
|
||||
const { timeEnd } = state.eventNow;
|
||||
const { clock } = state;
|
||||
|
||||
if (startedAt === null) {
|
||||
@@ -51,9 +50,8 @@ export function getExpectedFinish(state: TState): MaybeNumber {
|
||||
*/
|
||||
export function getCurrent(state: TState): number {
|
||||
const { startedAt, duration, addedTime } = state.timer;
|
||||
const { timerType } = state.eventNow;
|
||||
const { timerType, timeEnd } = state.eventNow;
|
||||
const { pausedAt } = state._timer;
|
||||
const { timeEnd } = state.eventNow;
|
||||
const { clock } = state;
|
||||
|
||||
if (timerType === TimerType.TimeToEnd) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export function getCached<T>(key: string, callback: () => T): T {
|
||||
const data = callback();
|
||||
runtimeCache.set(key, { data });
|
||||
} catch (error) {
|
||||
console.log(`Failed retrieving data from callback: ${error}`);
|
||||
console.error(`Failed retrieving data from callback: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ export function getCached<T>(key: string, callback: () => T): T {
|
||||
|
||||
export function setCached<T>(key: string, value: T): T {
|
||||
runtimeCache.set(key, { data: value });
|
||||
return runtimeCache.get(key).data as T;
|
||||
return value;
|
||||
}
|
||||
|
||||
export function invalidate(key) {
|
||||
export function invalidate(key: string) {
|
||||
runtimeCache.delete(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
/* eslint-disable no-console -- we are mocking the console */
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
||||
import {
|
||||
DatabaseModel,
|
||||
EndAction,
|
||||
OntimeEvent,
|
||||
ProjectData,
|
||||
Settings,
|
||||
SupportedEvent,
|
||||
TimerType,
|
||||
ViewSettings,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { dbModel } from '../../models/dataModel.js';
|
||||
import { parseExcel, parseJson, createEvent } from '../parser.js';
|
||||
@@ -9,24 +18,24 @@ import { makeString } from '../parserUtils.js';
|
||||
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.js';
|
||||
|
||||
describe('test json parser with valid def', () => {
|
||||
const testData = {
|
||||
const testData: Partial<DatabaseModel> = {
|
||||
rundown: [
|
||||
{
|
||||
id: '4b31',
|
||||
cue: 'Guest Welcoming',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Guest Welcoming',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.PlayNext,
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 31500000,
|
||||
timeEnd: 32400000,
|
||||
timeType: 'start-end',
|
||||
duration: 32400000 - 31500000,
|
||||
isPublic: false,
|
||||
endAction: 'play-next',
|
||||
timerType: 'clock',
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '4b31',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -37,24 +46,26 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
},
|
||||
{
|
||||
id: 'f24d',
|
||||
cue: 'Good Morning',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Good Morning',
|
||||
subtitle: 'Days schedule',
|
||||
presenter: 'Carlos Valente',
|
||||
note: '',
|
||||
endAction: EndAction.PlayNext,
|
||||
timerType: TimerType.CountUp,
|
||||
timeStart: 32400000,
|
||||
timeEnd: 36000000,
|
||||
timeType: 'start-end',
|
||||
duration: 36000000 - 32400000,
|
||||
isPublic: true,
|
||||
endAction: 'play-next',
|
||||
timerType: 'count-up',
|
||||
skip: true,
|
||||
colour: 'red',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: 'f24d',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -65,23 +76,26 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
},
|
||||
{
|
||||
id: 'bbc5',
|
||||
cue: 'Stage 2 setup',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Stage 2 setup',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: 'wrong action' as EndAction, // testing
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 32400000,
|
||||
timeEnd: 37200000,
|
||||
timeType: 'start-end',
|
||||
duration: 37200000 - 32400000,
|
||||
isPublic: false,
|
||||
endAction: 'wrong action',
|
||||
timerType: 'clock',
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: 'bbc5',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -92,41 +106,47 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
},
|
||||
{
|
||||
// testing incomplete dataset
|
||||
id: '5b3e',
|
||||
cue: 'Working Procedures',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Working Procedures',
|
||||
subtitle: '',
|
||||
presenter: 'Filip Johansen',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 37200000,
|
||||
timeEnd: 39000000,
|
||||
timeType: 'start-end',
|
||||
duration: 39000000 - 37200000,
|
||||
isPublic: true,
|
||||
endAction: 'none',
|
||||
timerType: 'clock',
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '5b3e',
|
||||
},
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
id: '8e2c',
|
||||
cue: 'Lunch',
|
||||
title: 'Lunch',
|
||||
type: SupportedEvent.Event,
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 39600000,
|
||||
timeEnd: 45000000,
|
||||
timeType: 'start-end',
|
||||
duration: 37200000 - 32400000,
|
||||
isPublic: false,
|
||||
endAction: 'none',
|
||||
timerType: 'clock',
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '8e2c',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -137,23 +157,26 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
},
|
||||
{
|
||||
id: '08e9',
|
||||
cue: 'A day being carlos',
|
||||
title: 'A day being carlos',
|
||||
type: SupportedEvent.Event,
|
||||
subtitle: 'My life in a song',
|
||||
presenter: 'Carlos Valente',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 46800000,
|
||||
timeEnd: 50400000,
|
||||
timeType: 'start-end',
|
||||
duration: 37200000 - 32400000,
|
||||
isPublic: true,
|
||||
endAction: 'none',
|
||||
timerType: 'clock',
|
||||
skip: true,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '08e9',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -164,23 +187,26 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
},
|
||||
{
|
||||
// testing incomplete dataset
|
||||
id: 'e25a',
|
||||
cue: 'Hamburgers and Cheese',
|
||||
title: 'Hamburgers and Cheese',
|
||||
type: SupportedEvent.Event,
|
||||
subtitle: '... and other life questions',
|
||||
presenter: 'Filip Johansen',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 54000000,
|
||||
timeEnd: 57600000,
|
||||
timeType: 'start-end',
|
||||
duration: 37200000 - 32400000,
|
||||
isPublic: true,
|
||||
endAction: 'none',
|
||||
timerType: 'clock',
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: 'e25a',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -191,20 +217,23 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
},
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
} as OntimeEvent,
|
||||
],
|
||||
project: {
|
||||
title: 'This is a test definition',
|
||||
url: 'www.carlosvalente.com',
|
||||
backstageUrl: 'www.carlosvalente.com',
|
||||
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
|
||||
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
||||
},
|
||||
} as ProjectData,
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: '2.0.0',
|
||||
timeFormat: '24',
|
||||
},
|
||||
viewSettings: {},
|
||||
} as Settings,
|
||||
viewSettings: {} as ViewSettings,
|
||||
};
|
||||
|
||||
let parseResponse;
|
||||
@@ -283,6 +312,8 @@ describe('test parser edge cases', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parseResponse = await parseJson(testData);
|
||||
expect(typeof (parseResponse.rundown[0] as OntimeEvent).cue).toBe('string');
|
||||
expect(typeof (parseResponse.rundown[1] as OntimeEvent).cue).toBe('string');
|
||||
@@ -298,6 +329,7 @@ describe('test parser edge cases', () => {
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parseResponse = await parseJson(testData);
|
||||
expect(parseResponse.rundown[0].id).toBeDefined();
|
||||
});
|
||||
@@ -319,6 +351,7 @@ describe('test parser edge cases', () => {
|
||||
],
|
||||
};
|
||||
|
||||
//@ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parseResponse = await parseJson(testData);
|
||||
expect(console.log).toHaveBeenCalledWith('ERROR: ID collision on import, skipping');
|
||||
expect(parseResponse?.rundown.length).toBe(1);
|
||||
@@ -339,6 +372,7 @@ describe('test parser edge cases', () => {
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parseResponse = await parseJson(testData);
|
||||
expect(console.log).toHaveBeenCalledWith('ERROR: unkown event type, skipping');
|
||||
expect(parseResponse?.rundown.length).toBe(0);
|
||||
@@ -352,6 +386,7 @@ describe('test parser edge cases', () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
await parseJson(testData);
|
||||
expect(console.log).toHaveBeenCalledWith('ERROR: unknown app version, skipping');
|
||||
});
|
||||
@@ -378,9 +413,11 @@ describe('test corrupt data', () => {
|
||||
{},
|
||||
{},
|
||||
],
|
||||
event: {
|
||||
project: {
|
||||
title: 'All about Carlos demo event',
|
||||
url: 'www.carlosvalente.com',
|
||||
description: 'description',
|
||||
publicUrl: 'www.carlosvalente.com',
|
||||
backstageUrl: 'www.carlosvalente.com',
|
||||
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
|
||||
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
||||
endMessage: '',
|
||||
@@ -389,11 +426,11 @@ describe('test corrupt data', () => {
|
||||
app: 'ontime',
|
||||
version: '2.0.0',
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parsedDef = await parseJson(emptyEvents);
|
||||
expect(parsedDef.rundown.length).toBe(2);
|
||||
});
|
||||
@@ -401,9 +438,11 @@ describe('test corrupt data', () => {
|
||||
it('handles all empty events', async () => {
|
||||
const emptyEvents = {
|
||||
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
|
||||
event: {
|
||||
project: {
|
||||
title: 'All about Carlos demo event',
|
||||
url: 'www.carlosvalente.com',
|
||||
description: 'description',
|
||||
publicUrl: 'www.carlosvalente.com',
|
||||
backstageUrl: 'www.carlosvalente.com',
|
||||
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
|
||||
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
||||
endMessage: '',
|
||||
@@ -412,11 +451,11 @@ describe('test corrupt data', () => {
|
||||
app: 'ontime',
|
||||
version: '2.0.0',
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parsedDef = await parseJson(emptyEvents);
|
||||
expect(parsedDef.rundown.length).toBe(0);
|
||||
});
|
||||
@@ -434,6 +473,7 @@ describe('test corrupt data', () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parsedDef = await parseJson(emptyProjectData);
|
||||
expect(parsedDef.project).toStrictEqual(dbModel.project);
|
||||
});
|
||||
@@ -448,12 +488,15 @@ describe('test corrupt data', () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parsedDef = await parseJson(missingSettings);
|
||||
expect(parsedDef.settings).toStrictEqual(dbModel.settings);
|
||||
});
|
||||
|
||||
it('fails with invalid JSON', async () => {
|
||||
const invalidJSON = 'some random dataset';
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parsedDef = await parseJson(invalidJSON);
|
||||
expect(parsedDef).toBeNull();
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync } from 'fs';
|
||||
* @description Creates a directory if it doesn't exist
|
||||
* @param {string} directory - directory that should exist or will be created
|
||||
*/
|
||||
export function ensureDirectory(directory) {
|
||||
export function ensureDirectory(directory: string) {
|
||||
if (!existsSync(directory)) {
|
||||
try {
|
||||
mkdirSync(directory, { recursive: true });
|
||||
|
||||
@@ -316,7 +316,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
* @param {object} jsonData - project file to be parsed
|
||||
* @returns {object} - parsed object
|
||||
*/
|
||||
export const parseJson = async (jsonData): Promise<DatabaseModel | null> => {
|
||||
export const parseJson = async (jsonData: Partial<DatabaseModel>): Promise<DatabaseModel | null> => {
|
||||
if (!jsonData || typeof jsonData !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { ensureJsonExtension } from './ensureJsonExtension.js';
|
||||
|
||||
export const sanitizeProjectFilename = (req, _res, next) => {
|
||||
export const sanitizeProjectFilename = (req: Request, _res: Response, next: NextFunction) => {
|
||||
const { filename, newFilename } = req.body;
|
||||
const { filename: projectName } = req.params;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"skipLibCheck": true,
|
||||
"types": ["vitest/globals"],
|
||||
"outDir": "dist",
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
{
|
||||
"rundown": [
|
||||
{
|
||||
"id": "32d31",
|
||||
"type": "event",
|
||||
"title": "Albania",
|
||||
"subtitle": "Sekret",
|
||||
"presenter": "Ronela Hajati",
|
||||
"timeStart": 36000000,
|
||||
"timeEnd": 37200000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.01",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.01",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "21cd2",
|
||||
"type": "event",
|
||||
"title": "Latvia",
|
||||
"subtitle": "Eat Your Salad",
|
||||
"presenter": "Citi Zeni",
|
||||
"timeStart": 37500000,
|
||||
"timeEnd": 38700000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.02",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.02",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "0b371",
|
||||
"type": "event",
|
||||
"title": "Lithuania",
|
||||
"subtitle": "Sentimentai",
|
||||
"presenter": "Monika Liu",
|
||||
"timeStart": 39000000,
|
||||
"timeEnd": 40200000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.03",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.03",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "3cd28",
|
||||
"type": "event",
|
||||
"title": "Switzerland",
|
||||
"subtitle": "Boys Do Cry",
|
||||
"presenter": "Marius Bear",
|
||||
"timeStart": 40500000,
|
||||
"timeEnd": 41700000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.04",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.04",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "e457f",
|
||||
"type": "event",
|
||||
"title": "Slovenia",
|
||||
"subtitle": "Disko",
|
||||
"presenter": "LPS",
|
||||
"timeStart": 42000000,
|
||||
"timeEnd": 43200000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.05",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.05",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"title": "Lunch break",
|
||||
"type": "block",
|
||||
"id": "01e85"
|
||||
},
|
||||
{
|
||||
"id": "1c420",
|
||||
"type": "event",
|
||||
"title": "Ukraine",
|
||||
"subtitle": "Stefania",
|
||||
"presenter": "Kalush Orchestra",
|
||||
"timeStart": 47100000,
|
||||
"timeEnd": 48300000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.06",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.06",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "b7737",
|
||||
"type": "event",
|
||||
"title": "Bulgaria",
|
||||
"subtitle": "Intention",
|
||||
"presenter": "Intelligent Music Project",
|
||||
"timeStart": 48600000,
|
||||
"timeEnd": 49800000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.07",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.07",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "d3a80",
|
||||
"type": "event",
|
||||
"title": "Netherlands",
|
||||
"subtitle": "De Diepte",
|
||||
"presenter": "S10",
|
||||
"timeStart": 50100000,
|
||||
"timeEnd": 51300000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.08",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.08",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "8276c",
|
||||
"type": "event",
|
||||
"title": "Moldova",
|
||||
"subtitle": "Trenuletul",
|
||||
"presenter": "Zdob si Zdub",
|
||||
"timeStart": 51600000,
|
||||
"timeEnd": 52800000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.09",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.09",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "2340b",
|
||||
"type": "event",
|
||||
"title": "Portugal",
|
||||
"subtitle": "Saudade Saudade",
|
||||
"presenter": "Maro",
|
||||
"timeStart": 53100000,
|
||||
"timeEnd": 54300000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.10",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.10",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"title": "Afternoon break",
|
||||
"type": "block",
|
||||
"id": "cb90b"
|
||||
},
|
||||
{
|
||||
"id": "503c4",
|
||||
"type": "event",
|
||||
"title": "Croatia",
|
||||
"subtitle": "Guilty Pleasure",
|
||||
"presenter": "Mia Dimsic",
|
||||
"timeStart": 56100000,
|
||||
"timeEnd": 57300000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.11",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.11",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "5e965",
|
||||
"type": "event",
|
||||
"title": "Denmark",
|
||||
"subtitle": "The Show",
|
||||
"presenter": "Reddi",
|
||||
"timeStart": 57600000,
|
||||
"timeEnd": 58800000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.12",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.12",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "bab4a",
|
||||
"type": "event",
|
||||
"title": "Austria",
|
||||
"subtitle": "Halo",
|
||||
"presenter": "LUM!X & Pia Maria",
|
||||
"timeStart": 59100000,
|
||||
"timeEnd": 60300000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.13",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.13",
|
||||
"revision": 0
|
||||
},
|
||||
{
|
||||
"id": "d3eb1",
|
||||
"type": "event",
|
||||
"title": "Greece",
|
||||
"subtitle": "Die Together",
|
||||
"presenter": "Amanda Tenfjord",
|
||||
"timeStart": 60600000,
|
||||
"timeEnd": 61800000,
|
||||
"duration": 1200000,
|
||||
"endAction": "none",
|
||||
"timerType": "count-down",
|
||||
"isPublic": true,
|
||||
"skip": false,
|
||||
"note": "SF1.14",
|
||||
"user0": "",
|
||||
"user1": "",
|
||||
"user2": "",
|
||||
"user3": "",
|
||||
"user4": "",
|
||||
"user5": "",
|
||||
"user6": "",
|
||||
"user7": "",
|
||||
"user8": "",
|
||||
"user9": "",
|
||||
"colour": "",
|
||||
"cue": "SF1.14",
|
||||
"revision": 0
|
||||
}
|
||||
],
|
||||
"project": {
|
||||
"title": "Eurovision Song Contest",
|
||||
"description": "Turin 2022",
|
||||
"publicUrl": "www.getontime.no",
|
||||
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
||||
"backstageUrl": "www.github.com/cpvalente/ontime",
|
||||
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
|
||||
},
|
||||
"settings": {
|
||||
"app": "ontime",
|
||||
"version": "3.0.0-alpha",
|
||||
"serverPort": 4001,
|
||||
"editorKey": null,
|
||||
"operatorKey": null,
|
||||
"timeFormat": "24",
|
||||
"language": "en"
|
||||
},
|
||||
"viewSettings": {
|
||||
"overrideStyles": false,
|
||||
"normalColor": "#ffffffcc",
|
||||
"warningColor": "#FFAB33",
|
||||
"dangerColor": "#ED3333",
|
||||
"endMessage": ""
|
||||
},
|
||||
"aliases": [
|
||||
{
|
||||
"enabled": true,
|
||||
"alias": "test",
|
||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
||||
}
|
||||
],
|
||||
"userFields": {
|
||||
"user0": "user0",
|
||||
"user1": "user1",
|
||||
"user2": "user2",
|
||||
"user3": "user3",
|
||||
"user4": "user4",
|
||||
"user5": "user5",
|
||||
"user6": "user6",
|
||||
"user7": "user7",
|
||||
"user8": "user8",
|
||||
"user9": "user9"
|
||||
},
|
||||
"osc": {
|
||||
"portIn": 8888,
|
||||
"portOut": 9999,
|
||||
"targetIP": "127.0.0.1",
|
||||
"enabledIn": true,
|
||||
"enabledOut": true,
|
||||
"subscriptions": {
|
||||
"onLoad": [],
|
||||
"onStart": [],
|
||||
"onPause": [],
|
||||
"onStop": [],
|
||||
"onUpdate": [
|
||||
{
|
||||
"id": "10eea",
|
||||
"enabled": true,
|
||||
"message": "/ontime/update/{{timer.current}}"
|
||||
}
|
||||
],
|
||||
"onFinish": []
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"enabledOut": true,
|
||||
"subscriptions": {
|
||||
"onLoad": [],
|
||||
"onStart": [],
|
||||
"onPause": [],
|
||||
"onStop": [],
|
||||
"onUpdate": [],
|
||||
"onFinish": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,4 +67,4 @@ export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './defini
|
||||
|
||||
// TYPE UTILITIES
|
||||
export { isOntimeBlock, isOntimeDelay, isOntimeEvent, isKeyOfType } from './utils/guards.js';
|
||||
export type { MaybeNumber, DeepPartial } from './utils/utils.type.js';
|
||||
export type { DeepPartial, MaybeNumber, MaybeString } from './utils/utils.type.js';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type MaybeNumber = number | null;
|
||||
export type MaybeString = string | null;
|
||||
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||
|
||||
Generated
+13
-10
@@ -316,6 +316,9 @@ importers:
|
||||
'@types/websocket':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
'@types/ws':
|
||||
specifier: ^8.5.10
|
||||
version: 8.5.10
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^6.10.0
|
||||
version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2)
|
||||
@@ -2317,7 +2320,7 @@ packages:
|
||||
'@jest/schemas': 29.6.3
|
||||
'@types/istanbul-lib-coverage': 2.0.4
|
||||
'@types/istanbul-reports': 3.0.1
|
||||
'@types/node': 20.10.5
|
||||
'@types/node': 18.19.3
|
||||
'@types/yargs': 17.0.19
|
||||
chalk: 4.1.2
|
||||
dev: true
|
||||
@@ -3155,7 +3158,7 @@ packages:
|
||||
/@types/connect@3.4.35:
|
||||
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
||||
dependencies:
|
||||
'@types/node': 18.11.18
|
||||
'@types/node': 18.19.3
|
||||
dev: true
|
||||
|
||||
/@types/debug@4.1.12:
|
||||
@@ -3188,7 +3191,7 @@ packages:
|
||||
/@types/fs-extra@9.0.13:
|
||||
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
||||
dependencies:
|
||||
'@types/node': 20.10.5
|
||||
'@types/node': 18.19.3
|
||||
dev: true
|
||||
|
||||
/@types/http-cache-semantics@4.0.4:
|
||||
@@ -3265,12 +3268,6 @@ packages:
|
||||
undici-types: 5.26.5
|
||||
dev: true
|
||||
|
||||
/@types/node@20.10.5:
|
||||
resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==}
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
dev: true
|
||||
|
||||
/@types/parse-json@4.0.0:
|
||||
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
||||
dev: false
|
||||
@@ -3350,6 +3347,12 @@ packages:
|
||||
'@types/node': 18.11.18
|
||||
dev: true
|
||||
|
||||
/@types/ws@8.5.10:
|
||||
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
|
||||
dependencies:
|
||||
'@types/node': 18.19.3
|
||||
dev: true
|
||||
|
||||
/@types/yargs-parser@21.0.0:
|
||||
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
||||
dev: true
|
||||
@@ -6590,7 +6593,7 @@ packages:
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.3.1
|
||||
'@types/node': 20.10.5
|
||||
'@types/node': 18.19.3
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.7.1
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
Reference in New Issue
Block a user