mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 02:13:48 +00:00
refactor: remove legacy service
This commit is contained in:
committed by
Carlos Valente
parent
1f71d4578c
commit
2cc434b0e9
@@ -1,80 +0,0 @@
|
||||
import got from 'got';
|
||||
|
||||
import { HttpSettings, HttpSubscription, LogOrigin } from 'ontime-types';
|
||||
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { parseTemplateNested } from './integrationUtils.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing HTTP communications
|
||||
* @class
|
||||
*/
|
||||
export class HttpIntegration implements IIntegration<HttpSubscription, HttpSettings> {
|
||||
subscriptions: HttpSubscription[];
|
||||
enabled: boolean;
|
||||
|
||||
constructor() {
|
||||
this.subscriptions = [];
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes httpClient
|
||||
*/
|
||||
init(config: HttpSettings) {
|
||||
const { subscriptions, enabledOut } = config;
|
||||
this.initSubscriptions(subscriptions);
|
||||
this.enabled = enabledOut;
|
||||
}
|
||||
|
||||
initSubscriptions(subscriptions: HttpSubscription[]) {
|
||||
this.subscriptions = subscriptions;
|
||||
}
|
||||
|
||||
dispatch(action: TimerLifeCycleKey, state?: object) {
|
||||
// noop
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||
const { cycle, message, enabled } = this.subscriptions[i];
|
||||
if (cycle !== action || !enabled || !message) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsedMessage = parseTemplateNested(message, state || {});
|
||||
this.emit(parsedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
emit(path: string) {
|
||||
got.get(path, { retry: { limit: 0 } }).catch((err) => {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: ${err.message}`);
|
||||
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' The server refused the connection`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.code === 'ENOTFOUND') {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' DNS lookup failed`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.code === 'ETIMEDOUT') {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' The connection timed out`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: ${err.code}`);
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
/** shutdown is a no-op here*/
|
||||
}
|
||||
}
|
||||
|
||||
export const httpIntegration = new HttpIntegration();
|
||||
@@ -1,11 +0,0 @@
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
|
||||
|
||||
export default interface IIntegration<T, C> {
|
||||
subscriptions: T[];
|
||||
init: (config: C) => void;
|
||||
dispatch: (action: TimerLifeCycleKey, state?: object) => void;
|
||||
emit: (...args: never[]) => unknown;
|
||||
shutdown: () => void;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { LogOrigin } from 'ontime-types';
|
||||
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
|
||||
class IntegrationService {
|
||||
private integrations: IIntegration<unknown, unknown>[];
|
||||
|
||||
constructor() {
|
||||
this.integrations = [];
|
||||
}
|
||||
|
||||
register(integrationService: IIntegration<unknown, unknown>) {
|
||||
this.integrations.push(integrationService);
|
||||
}
|
||||
|
||||
unregister(integrationService: IIntegration<unknown, unknown>) {
|
||||
this.integrations = this.integrations.filter((int) => int !== integrationService);
|
||||
}
|
||||
|
||||
dispatch(action: TimerLifeCycleKey) {
|
||||
const state = eventStore.poll();
|
||||
this.integrations.forEach((integration) => {
|
||||
integration.dispatch(action, state);
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
logger.info(LogOrigin.Tx, 'Shutdown Integrations');
|
||||
this.integrations.forEach((integration) => {
|
||||
integration.shutdown();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const integrationService = new IntegrationService();
|
||||
@@ -1,147 +0,0 @@
|
||||
import { ArgumentType, Client, Message } from 'node-osc';
|
||||
import { LogOrigin, MaybeNumber, MaybeString, OSCSettings, OscSubscription } from 'ontime-types';
|
||||
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { parseTemplateNested } from './integrationUtils.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { OscServer } from '../../adapters/OscAdapter.js';
|
||||
import { stringToOSCArgs } from '../../utils/oscArgParser.js';
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing OSC communications
|
||||
* @class
|
||||
*/
|
||||
export class OscIntegration implements IIntegration<OscSubscription, OSCSettings> {
|
||||
protected oscClient: null | Client;
|
||||
protected oscServer: OscServer | null = null;
|
||||
|
||||
subscriptions: OscSubscription[];
|
||||
targetIP: MaybeString;
|
||||
portOut: MaybeNumber;
|
||||
portIn: MaybeNumber;
|
||||
enabledOut: boolean;
|
||||
enabledIn: boolean;
|
||||
|
||||
constructor() {
|
||||
this.oscClient = null;
|
||||
this.subscriptions = [];
|
||||
this.targetIP = null;
|
||||
this.portOut = null;
|
||||
this.portIn = null;
|
||||
this.enabledOut = false;
|
||||
this.enabledIn = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes oscClient
|
||||
*/
|
||||
init(config: OSCSettings) {
|
||||
const { targetIP, portOut, subscriptions, enabledOut, enabledIn, portIn } = config;
|
||||
|
||||
this.initTX(enabledOut, targetIP, portOut, subscriptions);
|
||||
this.initRX(enabledIn, portIn);
|
||||
// return `OSC integration client connected to ${targetIP}:${portOut}`;
|
||||
}
|
||||
|
||||
private initSubscriptions(subscriptions: OscSubscription[]) {
|
||||
this.subscriptions = subscriptions;
|
||||
}
|
||||
|
||||
dispatch(action: TimerLifeCycleKey, state?: object) {
|
||||
// noop
|
||||
if (!this.oscClient || !this.enabledOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||
const { cycle, address, payload, enabled } = this.subscriptions[i];
|
||||
if (cycle !== action || !enabled || !address) {
|
||||
continue;
|
||||
}
|
||||
const parsedAddress = parseTemplateNested(address, state || {});
|
||||
const parsedPayload = payload ? parseTemplateNested(payload, state || {}) : undefined;
|
||||
const parsedArguments = stringToOSCArgs(parsedPayload);
|
||||
|
||||
try {
|
||||
this.emit(parsedAddress, parsedArguments);
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Tx, `OSC Integration: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(address: string, args: ArgumentType[]) {
|
||||
if (!this.oscClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO: Look into using bundles
|
||||
const message = new Message(address);
|
||||
message.append(args);
|
||||
|
||||
this.oscClient.send(message);
|
||||
}
|
||||
|
||||
private initTX(enabledOut: boolean, targetIP: string, portOut: number, subscriptions: OscSubscription[]) {
|
||||
this.initSubscriptions(subscriptions);
|
||||
|
||||
if (!enabledOut) {
|
||||
this.targetIP = targetIP;
|
||||
this.portOut = portOut;
|
||||
this.enabledOut = enabledOut;
|
||||
this.shutdownTX();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.oscClient && targetIP === this.targetIP && portOut === this.portOut) {
|
||||
// nothing changed that would mean we need a new client
|
||||
return;
|
||||
}
|
||||
|
||||
this.targetIP = targetIP;
|
||||
this.portOut = portOut;
|
||||
this.enabledOut = enabledOut;
|
||||
|
||||
try {
|
||||
this.oscClient = new Client(targetIP, portOut);
|
||||
logger.info(LogOrigin.Tx, `Starting OSC Clint on port: ${portOut}`);
|
||||
} catch (error) {
|
||||
this.oscClient = null;
|
||||
throw new Error(`Failed initialising OSC client: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private initRX(enabledIn: boolean, portIn: number) {
|
||||
if (!enabledIn) {
|
||||
this.shutdownRX();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start OSC Server
|
||||
logger.info(LogOrigin.Rx, `Starting OSC Server on port: ${portIn}`);
|
||||
this.oscServer = new OscServer(portIn);
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
this.shutdownTX();
|
||||
this.shutdownRX();
|
||||
}
|
||||
|
||||
private shutdownTX() {
|
||||
if (this.oscClient) {
|
||||
logger.info(LogOrigin.Tx, 'Shutting down OSC integration');
|
||||
this.oscClient?.close();
|
||||
this.oscClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
private shutdownRX() {
|
||||
if (this.oscServer) {
|
||||
logger.info(LogOrigin.Rx, 'Shutting down OSC integration');
|
||||
this.oscServer?.shutdown();
|
||||
this.oscServer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const oscIntegration = new OscIntegration();
|
||||
@@ -1,169 +0,0 @@
|
||||
import { stringToOSCArgs } from '../../utils/oscArgParser.js';
|
||||
import { parseTemplateNested } from './integrationUtils.js';
|
||||
|
||||
describe('parseTemplateNested()', () => {
|
||||
it('parses string with a single-level variable name', () => {
|
||||
const store = { timer: 10 };
|
||||
const templateString = '/test/{{timer}}';
|
||||
const result = parseTemplateNested(templateString, store);
|
||||
expect(result).toEqual('/test/10');
|
||||
});
|
||||
|
||||
it('parses string with a nested variable name', () => {
|
||||
const store = { timer: { clock: 10 } };
|
||||
const templateString = '/timer/{{timer.clock}}';
|
||||
const result = parseTemplateNested(templateString, store);
|
||||
expect(result).toEqual('/timer/10');
|
||||
});
|
||||
|
||||
it('parses string with multiple variables', () => {
|
||||
const mockState = { test1: 'that', test2: 'this' };
|
||||
const testString = '{{test1}} should replace {{test2}}';
|
||||
const expected = `${mockState.test1} should replace ${mockState.test2}`;
|
||||
|
||||
const result = parseTemplateNested(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('correctly parses a string without templates', () => {
|
||||
const testString = 'That should replace {test}';
|
||||
|
||||
const result = parseTemplateNested(testString, {});
|
||||
expect(result).toStrictEqual(testString);
|
||||
});
|
||||
|
||||
it('handles scenarios with missing variables', () => {
|
||||
// by failing to provide a value, we give visibility to
|
||||
// potential issues in the given string
|
||||
const mockState = { test1: 'that', test2: 'this' };
|
||||
const testString = '{{test1}} should replace {{test2}}, but not {{test3}}';
|
||||
const expected = `${mockState.test1} should replace ${mockState.test2}, but not {{test3}}`;
|
||||
|
||||
const result = parseTemplateNested(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseNestedTemplate() -> resolveAliasData()', () => {
|
||||
it('resolves data through callback', () => {
|
||||
const data = {
|
||||
not: {
|
||||
so: {
|
||||
easy: '3',
|
||||
},
|
||||
},
|
||||
};
|
||||
const aliases = {
|
||||
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
|
||||
};
|
||||
|
||||
const easyParse = parseTemplateNested('{{human.easy}}', data, aliases);
|
||||
expect(easyParse).toBe('testing-3');
|
||||
});
|
||||
it('handles a mixed operation', () => {
|
||||
const data = {
|
||||
not: {
|
||||
so: {
|
||||
easy: '3',
|
||||
},
|
||||
},
|
||||
other: {
|
||||
value: 42,
|
||||
},
|
||||
};
|
||||
const aliases = {
|
||||
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
|
||||
};
|
||||
|
||||
const easyParse = parseTemplateNested('{{other.value}} to {{human.easy}}', data, aliases);
|
||||
expect(easyParse).toBe('42 to testing-3');
|
||||
});
|
||||
it('returns given key when not found', () => {
|
||||
const data = {
|
||||
not: {
|
||||
so: {
|
||||
easy: '3',
|
||||
},
|
||||
},
|
||||
other: {
|
||||
value: 5,
|
||||
},
|
||||
};
|
||||
const aliases = {
|
||||
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
|
||||
};
|
||||
|
||||
const easyParse = parseTemplateNested('{{other.value}} to {{human.easy}} {{human.not.found}}', data, aliases);
|
||||
expect(easyParse).toBe('5 to testing-3 {{human.not.found}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseNestedTemplate() -> stringToOSCArgs()', () => {
|
||||
it('specific osc requirements', () => {
|
||||
const data = {
|
||||
not: {
|
||||
so: {
|
||||
easy: 'data with space',
|
||||
empty: '',
|
||||
number: 1234,
|
||||
stringNumber: '1234',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const payloads = [
|
||||
{
|
||||
test: '"string with space and {{not.so.easy}}"',
|
||||
expect: [{ type: 'string', value: 'string with space and data with space' }],
|
||||
},
|
||||
{
|
||||
test: '',
|
||||
expect: [],
|
||||
},
|
||||
{
|
||||
test: ' ',
|
||||
expect: [],
|
||||
},
|
||||
{
|
||||
test: '""',
|
||||
expect: [{ type: 'string', value: '' }],
|
||||
},
|
||||
{
|
||||
test: '"string with space and {{not.so.empty}}"',
|
||||
expect: [{ type: 'string', value: 'string with space and ' }],
|
||||
},
|
||||
{
|
||||
test: '"string with space and {{not.so.number}}"',
|
||||
expect: [{ type: 'string', value: 'string with space and 1234' }],
|
||||
},
|
||||
{
|
||||
test: '"string with space and {{not.so.stringNumber}}"',
|
||||
expect: [{ type: 'string', value: 'string with space and 1234' }],
|
||||
},
|
||||
{
|
||||
test: '"{{not.so.easy}}" 1',
|
||||
expect: [
|
||||
{ type: 'string', value: 'data with space' },
|
||||
{ type: 'integer', value: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
test: '"{{not.so.empty}}" 1',
|
||||
expect: [
|
||||
{ type: 'string', value: '' },
|
||||
{ type: 'integer', value: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
test: '',
|
||||
expect: [],
|
||||
},
|
||||
];
|
||||
|
||||
payloads.forEach((payload) => {
|
||||
const parsedPayload = parseTemplateNested(payload.test, data);
|
||||
const parsedArguments = stringToOSCArgs(parsedPayload);
|
||||
expect(parsedArguments).toStrictEqual(payload.expect);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
||||
|
||||
// any value inside double curly braces {{val}}
|
||||
const placeholderRegex = /{{(.*?)}}/g;
|
||||
|
||||
function formatDisplayFromString(value: string, hideZero = false): string {
|
||||
let valueInNumber: MaybeNumber = null;
|
||||
|
||||
if (value !== 'null') {
|
||||
const parsedValue = Number(value);
|
||||
if (!Number.isNaN(parsedValue)) {
|
||||
valueInNumber = parsedValue;
|
||||
}
|
||||
}
|
||||
let formatted = millisToString(valueInNumber, { fallback: hideZero ? '00:00' : '00:00:00' });
|
||||
if (hideZero) {
|
||||
formatted = removeLeadingZero(formatted);
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
type AliasesDefinition = Record<string, { key: string; cb: (value: string) => string }>;
|
||||
const quickAliases: AliasesDefinition = {
|
||||
clock: { key: 'clock', cb: (value: string) => formatDisplayFromString(value) },
|
||||
duration: { key: 'timer.duration', cb: (value: string) => formatDisplayFromString(value, true) },
|
||||
expectedEnd: {
|
||||
key: 'timer.expectedFinish',
|
||||
cb: (value: string) => formatDisplayFromString(value),
|
||||
},
|
||||
runningTimer: {
|
||||
key: 'timer.current',
|
||||
cb: (value: string) => formatDisplayFromString(value, true),
|
||||
},
|
||||
elapsedTime: {
|
||||
key: 'timer.elapsed',
|
||||
cb: (value: string) => formatDisplayFromString(value, true),
|
||||
},
|
||||
startedAt: { key: 'timer.startedAt', cb: (value: string) => formatDisplayFromString(value) },
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a templated string to values in a nested object
|
||||
*/
|
||||
export function parseTemplateNested(template: string, state: object, humanReadable = quickAliases): string {
|
||||
let parsedTemplate = template;
|
||||
const matches = Array.from(parsedTemplate.matchAll(placeholderRegex));
|
||||
|
||||
for (const match of matches) {
|
||||
const variableName = match[1];
|
||||
const variableParts = variableName.split('.');
|
||||
let value: string | undefined = undefined;
|
||||
|
||||
if (variableParts[0] === 'human') {
|
||||
const lookupKey = variableParts[1];
|
||||
if (lookupKey in humanReadable) {
|
||||
const newTemplate = `{{${humanReadable[lookupKey].key}}}`;
|
||||
const parsed = parseTemplateNested(newTemplate, state, humanReadable);
|
||||
value = humanReadable[lookupKey].cb(parsed);
|
||||
} else {
|
||||
value = undefined;
|
||||
}
|
||||
} else {
|
||||
// iterate through variable parts, and look for the property in the state object
|
||||
value = variableParts.reduce((obj, key) => obj?.[key], state);
|
||||
}
|
||||
if (value !== undefined) {
|
||||
parsedTemplate = parsedTemplate.replace(match[0], value);
|
||||
}
|
||||
}
|
||||
|
||||
return parsedTemplate;
|
||||
}
|
||||
@@ -28,8 +28,6 @@ import {
|
||||
setLastLoadedProject,
|
||||
} from '../app-state-service/AppStateService.js';
|
||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
import { oscIntegration } from '../integration-service/OscIntegration.js';
|
||||
import { httpIntegration } from '../integration-service/HttpIntegration.js';
|
||||
|
||||
import {
|
||||
copyCorruptFile,
|
||||
@@ -176,14 +174,10 @@ export async function loadProjectFile(name: string) {
|
||||
// apply data model
|
||||
runtimeService.stop();
|
||||
|
||||
const { rundown, customFields, osc, http } = result.data;
|
||||
const { rundown, customFields } = result.data;
|
||||
|
||||
// apply the rundown
|
||||
await initRundown(rundown, customFields);
|
||||
|
||||
// apply integrations
|
||||
oscIntegration.init(osc);
|
||||
httpIntegration.init(http);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,14 +244,10 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
||||
// apply data model
|
||||
runtimeService.stop();
|
||||
|
||||
const { rundown, customFields, osc, http } = result.data;
|
||||
const { rundown, customFields } = result.data;
|
||||
|
||||
// apply the rundown
|
||||
await initRundown(rundown, customFields);
|
||||
|
||||
// apply integrations
|
||||
oscIntegration.init(osc);
|
||||
httpIntegration.init(http);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
getRundown,
|
||||
getTimedEvents,
|
||||
} from '../rundown-service/rundownUtils.js';
|
||||
import { integrationService } from '../integration-service/IntegrationService.js';
|
||||
|
||||
import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js';
|
||||
import { skippedOutOfEvent } from '../timerUtils.js';
|
||||
|
||||
Reference in New Issue
Block a user