refactor: remove legacy service

This commit is contained in:
Carlos Valente
2025-01-11 16:06:43 +01:00
committed by Carlos Valente
parent 1f71d4578c
commit 2cc434b0e9
50 changed files with 60 additions and 1851 deletions
@@ -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';