mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 10:23:54 +00:00
Merge branch 'master' into port-colision
This commit is contained in:
@@ -47,8 +47,8 @@ export class SocketServer implements IAdapter {
|
||||
this.wss = null;
|
||||
}
|
||||
|
||||
init(server: Server) {
|
||||
this.wss = new WebSocketServer({ path: '/ws', server, maxPayload: this.MAX_PAYLOAD });
|
||||
init(server: Server, prefix?: string) {
|
||||
this.wss = new WebSocketServer({ path: `${prefix}/ws`, server, maxPayload: this.MAX_PAYLOAD });
|
||||
|
||||
this.wss.on('connection', (ws) => {
|
||||
const clientId = generateId();
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
deleteEvent,
|
||||
editEvent,
|
||||
reorderEvent,
|
||||
setFrozenState,
|
||||
swapEvents,
|
||||
} from '../../services/rundown-service/RundownService.js';
|
||||
import {
|
||||
@@ -127,17 +126,6 @@ export async function rundownBatchPut(req: Request, res: Response<MessageRespons
|
||||
}
|
||||
}
|
||||
|
||||
export async function rundownFrozenPost(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||
try {
|
||||
const { frozen } = req.body;
|
||||
setFrozenState(frozen);
|
||||
res.status(200).send({ message: 'Rundown frozen state updated.' });
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(400).send({ message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function rundownReorder(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
return;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
|
||||
export const preventIfFrozen = function (req, res, next) {
|
||||
if (eventStore.get('frozen')) {
|
||||
res.status(403).send({ message: 'Rundown is frozen' });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
rundownApplyDelay,
|
||||
rundownBatchPut,
|
||||
rundownDelete,
|
||||
rundownFrozenPost,
|
||||
rundownGetAll,
|
||||
rundownGetById,
|
||||
rundownGetNormalised,
|
||||
@@ -19,14 +18,12 @@ import {
|
||||
paramsMustHaveEventId,
|
||||
rundownArrayOfIds,
|
||||
rundownBatchPutValidator,
|
||||
rundownFrozenPostValidator,
|
||||
rundownGetPaginatedQueryParams,
|
||||
rundownPostValidator,
|
||||
rundownPutValidator,
|
||||
rundownReorderValidator,
|
||||
rundownSwapValidator,
|
||||
} from './rundown.validation.js';
|
||||
import { preventIfFrozen } from './rundown.middleware.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
@@ -36,14 +33,13 @@ router.get('/normalised', rundownGetNormalised);
|
||||
router.get('/:eventId', paramsMustHaveEventId, rundownGetById); // not used in Ontime frontend
|
||||
|
||||
router.post('/', rundownPostValidator, rundownPost);
|
||||
router.post('/frozen', rundownFrozenPostValidator, rundownFrozenPost);
|
||||
|
||||
router.put('/', rundownPutValidator, rundownPut);
|
||||
router.put('/batch', rundownBatchPutValidator, rundownBatchPut);
|
||||
|
||||
router.patch('/reorder/', rundownReorderValidator, preventIfFrozen, rundownReorder);
|
||||
router.patch('/swap', rundownSwapValidator, preventIfFrozen, rundownSwap);
|
||||
router.patch('/reorder/', rundownReorderValidator, rundownReorder);
|
||||
router.patch('/swap', rundownSwapValidator, rundownSwap);
|
||||
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
|
||||
|
||||
router.delete('/', rundownArrayOfIds, preventIfFrozen, deletesEventById);
|
||||
router.delete('/all', preventIfFrozen, rundownDelete);
|
||||
router.delete('/', rundownArrayOfIds, deletesEventById);
|
||||
router.delete('/all', rundownDelete);
|
||||
|
||||
@@ -21,15 +21,6 @@ export const rundownPutValidator = [
|
||||
},
|
||||
];
|
||||
|
||||
export const rundownFrozenPostValidator = [
|
||||
body('frozen').isBoolean().exists(),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
]
|
||||
|
||||
export const rundownBatchPutValidator = [
|
||||
body('data').isObject().exists(),
|
||||
body('ids').isArray().exists(),
|
||||
|
||||
@@ -241,6 +241,11 @@ const actionHandlers: Record<string, ActionHandler> = {
|
||||
const timeInMs = numberOrError(command.duration) * 1000;
|
||||
reply.payload = auxTimerService.setTime(timeInMs);
|
||||
}
|
||||
if ('addtime' in command) {
|
||||
// convert addTime in seconds to ms
|
||||
const timeInMs = numberOrError(command.addtime) * 1000;
|
||||
reply.payload = auxTimerService.addTime(timeInMs);
|
||||
}
|
||||
if ('direction' in command) {
|
||||
if (command.direction === SimpleDirection.CountUp || command.direction === SimpleDirection.CountDown) {
|
||||
reply.payload = auxTimerService.setDirection(command.direction);
|
||||
|
||||
+38
-28
@@ -6,11 +6,11 @@ import expressStaticGzip from 'express-static-gzip';
|
||||
import http, { Server } from 'http';
|
||||
import cors from 'cors';
|
||||
import serverTiming from 'server-timing';
|
||||
import { extname, resolve } from 'path';
|
||||
import { extname } from 'node:path';
|
||||
|
||||
// import utils
|
||||
import { publicDir, srcDir } from './setup/index.js';
|
||||
import { environment, isProduction, updateRouterPrefix } from './externals.js';
|
||||
import { publicDir, srcDir, srcFiles } from './setup/index.js';
|
||||
import { environment, isOntimeCloud, isProduction, updateRouterPrefix } from './externals.js';
|
||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
|
||||
|
||||
@@ -53,8 +53,14 @@ if (!canLog) {
|
||||
console.log(`Ontime public directory at ${publicDir.root} `);
|
||||
}
|
||||
|
||||
// calls an update to the client router prefix
|
||||
updateRouterPrefix();
|
||||
/**
|
||||
* When running in Ontime cloud, the client is not at the root segment
|
||||
* ie: https://cloud.getontime.com/client-hash/timer
|
||||
* This means:
|
||||
* - changing the base path in the index.html file
|
||||
* - prepending all express routes with the given prefix
|
||||
*/
|
||||
const prefix = updateRouterPrefix();
|
||||
|
||||
// Create express APP
|
||||
const app = express();
|
||||
@@ -75,20 +81,20 @@ app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
|
||||
// Implement route endpoints
|
||||
app.use('/data', appRouter); // router for application data
|
||||
app.use('/api', integrationRouter); // router for integrations
|
||||
app.use(`${prefix}/data`, appRouter); // router for application data
|
||||
app.use(`${prefix}/api`, integrationRouter); // router for integrations
|
||||
|
||||
// serve static external files
|
||||
app.use('/external', express.static(publicDir.externalDir));
|
||||
app.use('/user', express.static(publicDir.userDir));
|
||||
|
||||
// if the user reaches to the root, we show a 404
|
||||
app.use('/external', (req, res) => {
|
||||
app.use(`${prefix}/external`, express.static(publicDir.externalDir));
|
||||
app.use(`${prefix}/external`, (req, res) => {
|
||||
// if the user reaches to the root, we show a 404
|
||||
res.status(404).send(`${req.originalUrl} not found`);
|
||||
});
|
||||
app.use(`${prefix}/user`, express.static(publicDir.userDir));
|
||||
|
||||
// serve static - react, in dev/test mode we fetch the React app from module
|
||||
app.use(
|
||||
prefix,
|
||||
expressStaticGzip(srcDir.clientDir, {
|
||||
enableBrotli: true,
|
||||
orderPreference: ['br'],
|
||||
@@ -111,8 +117,8 @@ app.use(
|
||||
}),
|
||||
);
|
||||
|
||||
app.get('*', (_req, res) => {
|
||||
res.sendFile(resolve(srcDir.clientDir, 'index.html'));
|
||||
app.get(`${prefix}/*`, (_req, res) => {
|
||||
res.sendFile(srcFiles.clientIndexHtml);
|
||||
});
|
||||
|
||||
// Implement catch all
|
||||
@@ -183,7 +189,7 @@ export const startServer = async (
|
||||
const portError = resultPort !== desiredPort;
|
||||
await getDataProvider().setSettings({ ...settings, serverPort: resultPort });
|
||||
|
||||
socket.init(expressServer);
|
||||
socket.init(expressServer, prefix);
|
||||
|
||||
/**
|
||||
* Module initialises the services and provides initial payload for the store
|
||||
@@ -209,7 +215,6 @@ export const startServer = async (
|
||||
playback: SimplePlayback.Stop,
|
||||
direction: SimpleDirection.CountDown,
|
||||
},
|
||||
frozen: false,
|
||||
ping: -1,
|
||||
});
|
||||
|
||||
@@ -228,10 +233,10 @@ export const startServer = async (
|
||||
runtimeService.init(maybeRestorePoint);
|
||||
|
||||
const nif = getNetworkInterfaces();
|
||||
consoleSuccess(`Local: http://localhost:${resultPort}/editor`);
|
||||
consoleSuccess(`Local: http://localhost:${resultPort}${prefix}/editor`);
|
||||
for (const key in nif) {
|
||||
const address = nif[key].address;
|
||||
consoleSuccess(`Network: http://${address}:${resultPort}/editor`);
|
||||
consoleSuccess(`Network: http://${address}:${resultPort}${prefix}/editor`);
|
||||
}
|
||||
|
||||
const returnMessage = `Ontime is listening on port ${resultPort}`;
|
||||
@@ -253,16 +258,6 @@ export const startIntegrations = async () => {
|
||||
// if a config is not provided, we use the persisted one
|
||||
const { osc, http } = getDataProvider().getData();
|
||||
|
||||
if (osc) {
|
||||
logger.info(LogOrigin.Tx, 'Initialising OSC Integration...');
|
||||
try {
|
||||
oscIntegration.init(osc);
|
||||
integrationService.register(oscIntegration);
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Tx, 'OSC Integration initialisation failed');
|
||||
}
|
||||
}
|
||||
|
||||
if (http) {
|
||||
logger.info(LogOrigin.Tx, 'Initialising HTTP Integration...');
|
||||
try {
|
||||
@@ -272,6 +267,21 @@ export const startIntegrations = async () => {
|
||||
logger.error(LogOrigin.Tx, `HTTP Integration initialisation failed: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isOntimeCloud) {
|
||||
logger.info(LogOrigin.Tx, 'Skipping OSC in Cloud environment...');
|
||||
return;
|
||||
}
|
||||
|
||||
if (osc) {
|
||||
logger.info(LogOrigin.Tx, 'Initialising OSC Integration...');
|
||||
try {
|
||||
oscIntegration.init(osc);
|
||||
integrationService.register(oscIntegration);
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Tx, 'OSC Integration initialisation failed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,14 @@ export class SimpleTimer {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public addTime(millis: number): SimpleTimerState {
|
||||
this.state.duration += millis;
|
||||
// the value of current will be overridden when update is called,
|
||||
// but if we are in pause or stop state it will not be changed so we do it here
|
||||
this.state.current += millis;
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public setDirection(direction: SimpleDirection, timeNow: number): SimpleTimerState {
|
||||
// if we are playing, we need to reset the targets
|
||||
if (this.state.playback === SimplePlayback.Start) {
|
||||
|
||||
@@ -177,5 +177,60 @@ describe('SimpleTimer count-down', () => {
|
||||
playback: SimplePlayback.Start,
|
||||
});
|
||||
});
|
||||
|
||||
test('adding time affects final result', () => {
|
||||
timer.reset();
|
||||
|
||||
timer.setTime(1000);
|
||||
timer.start(0);
|
||||
timer.update(100);
|
||||
expect(timer.state).toMatchObject({ current: 900, duration: 1000 });
|
||||
|
||||
timer.addTime(1000);
|
||||
timer.update(200);
|
||||
expect(timer.state).toMatchObject({ current: 1800, duration: 2000 });
|
||||
|
||||
timer.update(300);
|
||||
expect(timer.state).toMatchObject({ current: 1700, duration: 2000 });
|
||||
|
||||
timer.stop();
|
||||
expect(timer.state).toMatchObject({ current: 1000, duration: 1000 });
|
||||
});
|
||||
|
||||
test('adding time affects paused timer', () => {
|
||||
timer.reset();
|
||||
|
||||
timer.setTime(1000);
|
||||
timer.start(0);
|
||||
timer.update(100);
|
||||
expect(timer.state).toMatchObject({ current: 900, duration: 1000 });
|
||||
|
||||
timer.pause(200);
|
||||
expect(timer.state).toMatchObject({ current: 900, duration: 1000 });
|
||||
|
||||
timer.addTime(1000);
|
||||
timer.update(200);
|
||||
expect(timer.state).toMatchObject({ current: 1900, duration: 2000 });
|
||||
|
||||
timer.start(300);
|
||||
expect(timer.state).toMatchObject({ current: 1800, duration: 2000 });
|
||||
});
|
||||
|
||||
test('adding time affects stopped timer, but returns to initial valuses when stopped again', () => {
|
||||
timer.reset();
|
||||
|
||||
timer.setTime(1000);
|
||||
expect(timer.state).toMatchObject({ current: 1000, duration: 1000 });
|
||||
|
||||
timer.addTime(1000);
|
||||
expect(timer.state).toMatchObject({ current: 2000, duration: 2000 });
|
||||
|
||||
timer.start(0);
|
||||
timer.update(100);
|
||||
expect(timer.state).toMatchObject({ current: 1900, duration: 2000 });
|
||||
|
||||
timer.stop();
|
||||
expect(timer.state).toMatchObject({ current: 1000, duration: 1000 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
export const timerConfig = {
|
||||
skipLimit: 1000, // threshold of skip for recalculating
|
||||
skipLimit: 1000, // threshold of skip for recalculating, values lower than updateRate can cause issues with rolling over midnight
|
||||
updateRate: 32, // how often do we update the timer
|
||||
notificationRate: 1000, // how often do we notify clients and integrations
|
||||
triggerAhead: 10, // how far ahead do we trigger the end event
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { srcFiles } from './setup/index.js';
|
||||
|
||||
// =================================================
|
||||
// resolve running environment
|
||||
@@ -13,25 +14,26 @@ export const isTest = Boolean(process.env.IS_TEST);
|
||||
export const environment = isTest ? 'test' : env;
|
||||
export const isDocker = env === 'docker';
|
||||
export const isProduction = isDocker || (env === 'production' && !isTest);
|
||||
|
||||
export const isOntimeCloud = Boolean(process.env.IS_CLOUD);
|
||||
/**
|
||||
* Updates the router prefix in the index.html file
|
||||
* This is only needed in the cloud environment where the client is not at the root segment
|
||||
* ie: https://cloud.getontime.com/client-hash/timer
|
||||
*/
|
||||
export function updateRouterPrefix(prefix: string | undefined = process.env.ROUTER_PREFIX) {
|
||||
export function updateRouterPrefix(prefix: string | undefined = process.env.ROUTER_PREFIX): string {
|
||||
if (!prefix) {
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
|
||||
const indexFile = resolve('.', 'client', 'index.html');
|
||||
try {
|
||||
const data = readFileSync(indexFile, { encoding: 'utf-8', flag: 'r' }).replace(
|
||||
/<base href="[^"]*">/g,
|
||||
`<base href="${prefix}>"`,
|
||||
const data = readFileSync(srcFiles.clientIndexHtml, { encoding: 'utf-8', flag: 'r' }).replace(
|
||||
'<base href="/" />',
|
||||
`<base href="/${prefix}/" />`,
|
||||
);
|
||||
writeFileSync(indexFile, data, { encoding: 'utf-8', flag: 'w' });
|
||||
writeFileSync(srcFiles.clientIndexHtml, data, { encoding: 'utf-8', flag: 'w' });
|
||||
} catch (_error) {
|
||||
/** unhandled */
|
||||
}
|
||||
|
||||
return `/${prefix}`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SimpleDirection, SimpleTimerState } from 'ontime-types';
|
||||
import { SimpleDirection, SimplePlayback, SimpleTimerState } from 'ontime-types';
|
||||
|
||||
import { SimpleTimer } from '../../classes/simple-timer/SimpleTimer.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
@@ -57,6 +57,15 @@ export class AuxTimerService {
|
||||
return this.timer.setTime(duration);
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
addTime(millis: number) {
|
||||
if (this.timer.state.playback === SimplePlayback.Start) {
|
||||
this.timer.addTime(millis);
|
||||
return this.timer.update(this.getTime());
|
||||
}
|
||||
return this.timer.addTime(millis);
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
private update() {
|
||||
return this.timer.update(this.getTime());
|
||||
|
||||
@@ -128,18 +128,18 @@ export class OscIntegration implements IIntegration<OscSubscription, OSCSettings
|
||||
}
|
||||
|
||||
private shutdownTX() {
|
||||
logger.info(LogOrigin.Rx, 'Shutting down OSC integration');
|
||||
if (this.oscServer) {
|
||||
this.oscServer?.shutdown();
|
||||
this.oscServer = null;
|
||||
if (this.oscClient) {
|
||||
logger.info(LogOrigin.Tx, 'Shutting down OSC integration');
|
||||
this.oscClient?.close();
|
||||
this.oscClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
private shutdownRX() {
|
||||
logger.info(LogOrigin.Tx, 'Shutting down OSC integration');
|
||||
if (this.oscClient) {
|
||||
this.oscClient?.close();
|
||||
this.oscClient = null;
|
||||
if (this.oscServer) {
|
||||
logger.info(LogOrigin.Rx, 'Shutting down OSC integration');
|
||||
this.oscServer?.shutdown();
|
||||
this.oscServer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { DatabaseModel, LogOrigin, ProjectFileListResponse } from 'ontime-types';
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
|
||||
import { copyFile, rename } from 'fs/promises';
|
||||
import { copyFile } from 'fs/promises';
|
||||
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { publicDir } from '../../setup/index.js';
|
||||
import {
|
||||
appendToName,
|
||||
dockerSafeRename,
|
||||
ensureDirectory,
|
||||
generateUniqueFileName,
|
||||
getFileNameFromPath,
|
||||
@@ -93,7 +94,7 @@ async function handleCorruptedFile(filePath: string, fileName: string): Promise<
|
||||
|
||||
// and make a new file with the recovered data
|
||||
const newPath = appendToName(filePath, '(recovered)');
|
||||
await rename(filePath, newPath);
|
||||
await dockerSafeRename(filePath, newPath);
|
||||
return getFileNameFromPath(newPath);
|
||||
}
|
||||
|
||||
@@ -231,7 +232,7 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
||||
}
|
||||
|
||||
const pathToRenamed = getPathToProject(newFilename);
|
||||
await rename(projectFilePath, pathToRenamed);
|
||||
await dockerSafeRename(projectFilePath, pathToRenamed);
|
||||
|
||||
// Update the last loaded project config if current loaded project is the one being renamed
|
||||
const isLoaded = await isLastLoadedProject(originalFile);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { DatabaseModel, MaybeString, ProjectFile } from 'ontime-types';
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import { copyFile, readFile, rename, stat } from 'fs/promises';
|
||||
import { copyFile, readFile, stat } from 'fs/promises';
|
||||
import { extname, join } from 'path';
|
||||
|
||||
import { publicDir } from '../../setup/index.js';
|
||||
import { ensureDirectory, getFilesFromFolder, removeFileExtension } from '../../utils/fileManagement.js';
|
||||
import {
|
||||
dockerSafeRename,
|
||||
ensureDirectory,
|
||||
getFilesFromFolder,
|
||||
removeFileExtension,
|
||||
} from '../../utils/fileManagement.js';
|
||||
|
||||
/**
|
||||
* Handles the upload of a new project file
|
||||
@@ -14,13 +19,13 @@ import { ensureDirectory, getFilesFromFolder, removeFileExtension } from '../../
|
||||
*/
|
||||
export async function handleUploaded(filePath: string, name: string) {
|
||||
const newFilePath = join(publicDir.projectsDir, name);
|
||||
await rename(filePath, newFilePath);
|
||||
await dockerSafeRename(filePath, newFilePath);
|
||||
}
|
||||
|
||||
export async function handleImageUpload(filePath: string, name: string): Promise<string> {
|
||||
ensureDirectory(publicDir.logoDir);
|
||||
const newFilePath = join(publicDir.logoDir, name);
|
||||
await rename(filePath, newFilePath);
|
||||
await dockerSafeRename(filePath, newFilePath);
|
||||
|
||||
return name;
|
||||
}
|
||||
@@ -86,7 +91,7 @@ export async function copyCorruptFile(filePath: string, name: string): Promise<v
|
||||
*/
|
||||
export async function moveCorruptFile(filePath: string, name: string): Promise<void> {
|
||||
const newPath = join(publicDir.corruptDir, name);
|
||||
return rename(filePath, newPath);
|
||||
return dockerSafeRename(filePath, newPath);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,6 @@ import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
import { getPlayableEvents, getTimedEvents } from './rundownUtils.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
|
||||
type PatchWithId = (Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) & { id: string };
|
||||
|
||||
@@ -275,7 +274,3 @@ export async function initRundown(rundown: Readonly<OntimeRundown>, customFields
|
||||
// notify timer of change
|
||||
notifyChanges({ timer: true, external: true, reload: true });
|
||||
}
|
||||
|
||||
export async function setFrozenState(state: boolean) {
|
||||
eventStore.set('frozen', state);
|
||||
}
|
||||
|
||||
@@ -99,10 +99,15 @@ class RuntimeService {
|
||||
});
|
||||
this.handleLoadNext();
|
||||
this.rollLoaded(keepOffset);
|
||||
} else if (skippedOutOfEvent(newState, this.lastIntegrationClockUpdate, timerConfig.skipLimit)) {
|
||||
} else if (
|
||||
// if there is no previous clock, we could not have skipped
|
||||
RuntimeService.previousState?.clock &&
|
||||
skippedOutOfEvent(newState, RuntimeService.previousState.clock, timerConfig.skipLimit)
|
||||
) {
|
||||
// if we have skipped out of the event, we will recall roll
|
||||
// to push the playback to the right place
|
||||
// this comes with the caveat that we will lose our runtime data
|
||||
logger.warning(LogOrigin.Playback, 'Time skip detected, reloading roll');
|
||||
this.roll(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ export const srcDir = {
|
||||
} as const;
|
||||
|
||||
export const srcFiles = {
|
||||
/** Path to start index.html */
|
||||
clientIndexHtml: join(srcDir.clientDir, 'index.html'),
|
||||
/** Path to bundled CSS */
|
||||
cssOverride: join(srcDir.root, config.user, config.styles.directory, config.styles.filename),
|
||||
/** Path to bundled external readme */
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { cleanURL } from '../url.js';
|
||||
|
||||
describe('url is correctly formatted', () => {
|
||||
it('has no leading spaces', () => {
|
||||
const test = ' http://testing';
|
||||
const expected = 'http://testing';
|
||||
expect(cleanURL(test)).toBe(expected);
|
||||
});
|
||||
|
||||
it('has no trailing spaces', () => {
|
||||
const test = 'http://testing ';
|
||||
const expected = 'http://testing';
|
||||
expect(cleanURL(test)).toBe(expected);
|
||||
});
|
||||
|
||||
it('doesnt contain spaces', () => {
|
||||
const test = 'http://t e s t i n g';
|
||||
const expected = 'http://t%20e%20s%20t%20i%20n%20g';
|
||||
expect(cleanURL(test)).toBe(expected);
|
||||
});
|
||||
|
||||
it('only contains allowed characters', () => {
|
||||
const test = 'http://<>[]{}|^';
|
||||
const expected = 'http://';
|
||||
expect(cleanURL(test)).toBe(expected);
|
||||
});
|
||||
|
||||
it('begins with http://', () => {
|
||||
const test = 'ontime.com';
|
||||
const expected = 'http://ontime.com';
|
||||
expect(cleanURL(test)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { readdir, copyFile } from 'fs/promises';
|
||||
import { existsSync, mkdirSync, PathLike } from 'fs';
|
||||
import { readdir, copyFile, unlink } from 'fs/promises';
|
||||
import { basename, extname, join, parse } from 'path';
|
||||
|
||||
/**
|
||||
@@ -105,3 +105,12 @@ export async function copyDirectory(src: string, dest: string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* workaround avoids origin errors in docker deployments
|
||||
* EXDEV cross-device link not permitted
|
||||
*/
|
||||
export async function dockerSafeRename(oldPath: PathLike, newPath: PathLike) {
|
||||
await copyFile(oldPath, newPath);
|
||||
await unlink(oldPath);
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* @description Cleans given url
|
||||
* @param {string} url - URL to be checked
|
||||
* @returns {string} Sanitized url
|
||||
*/
|
||||
export const cleanURL = (url: string): string => {
|
||||
// trim whitespaces
|
||||
let sanitised = url.trim();
|
||||
|
||||
// clear any whitespaces
|
||||
sanitised = sanitised.split(' ').join('%20');
|
||||
|
||||
// contain only allowed characters
|
||||
sanitised = sanitised.replace(/([@\s<>[\]{}|\\^])+/g, '');
|
||||
|
||||
// starts with http://
|
||||
if (!sanitised.startsWith('http://')) sanitised = `http://${sanitised}`;
|
||||
|
||||
return sanitised;
|
||||
};
|
||||
Reference in New Issue
Block a user