feat: many timers (#706)

* wip: create simple timer class

* refactor: service to manage extra timers

* wip:ui for controlling the extra timer

---------

Co-authored-by: arc-alex <ac@omnivox.dk>
This commit is contained in:
Carlos Valente
2024-01-25 16:51:20 +01:00
committed by GitHub
parent 74e1ae609c
commit 2df3d376ad
17 changed files with 465 additions and 7 deletions
+7 -1
View File
@@ -1,4 +1,4 @@
import { HttpSettings, LogOrigin, OSCSettings, Playback } from 'ontime-types';
import { HttpSettings, LogOrigin, OSCSettings, Playback, SimpleDirection, SimplePlayback } from 'ontime-types';
import 'dotenv/config';
import express from 'express';
@@ -168,6 +168,12 @@ export const startServer = async () => {
publicEventNow: state.publicEventNow,
eventNext: state.eventNext,
publicEventNext: state.publicEventNext,
timer1: {
duration: null,
current: null,
playback: SimplePlayback.Stop,
direction: SimpleDirection.CountDown,
},
});
// load restore point if it exists
@@ -0,0 +1,78 @@
import { SimpleDirection, SimplePlayback, SimpleTimerState } from 'ontime-types';
export class SimpleTimer {
state: SimpleTimerState = {
duration: 0,
current: 0,
playback: SimplePlayback.Stop,
direction: SimpleDirection.CountDown,
};
private startedAt: number | null = null;
private pausedAt: number | null = null;
constructor() {}
public reset() {
this.state = {
duration: 0,
current: 0,
playback: SimplePlayback.Stop,
direction: SimpleDirection.CountDown,
};
}
/**
* Sets the duration of the timer
* @param time - time in milliseconds
*/
public setTime(time: number): SimpleTimerState {
this.state.duration = time;
this.state.current = time;
return this.state;
}
public setDirection(direction: SimpleDirection): SimpleTimerState {
this.state.playback = SimplePlayback.Stop;
this.state.current = this.state.duration;
this.state.direction = direction;
return this.state;
}
public start(timeNow: number): SimpleTimerState {
if (this.state.playback === SimplePlayback.Pause) {
const elapsedSincePause = this.pausedAt - this.startedAt;
this.startedAt = timeNow - elapsedSincePause;
} else if (this.state.playback === SimplePlayback.Stop) {
this.startedAt = timeNow;
}
this.state.playback = SimplePlayback.Start;
return this.update(timeNow);
}
public pause(timeNow: number): SimpleTimerState {
if (this.state.playback !== SimplePlayback.Start) return this.state;
this.state.playback = SimplePlayback.Pause;
this.pausedAt = timeNow;
return this.state;
}
public stop(): SimpleTimerState {
this.state.playback = SimplePlayback.Stop;
this.state.current = this.state.duration;
this.startedAt = null;
return this.state;
}
public update(timeNow: number): SimpleTimerState {
if (this.state.playback === SimplePlayback.Start) {
const elapsed = timeNow - this.startedAt;
if (this.state.direction === SimpleDirection.CountDown) {
this.state.current = this.state.duration - elapsed;
} else if (this.state.direction === SimpleDirection.CountUp) {
this.state.current = this.state.duration + elapsed;
}
}
return this.state;
}
}
@@ -0,0 +1,128 @@
import { SimpleDirection, SimplePlayback, SimpleTimerState } from 'ontime-types';
import { SimpleTimer } from '../SimpleTimer.js';
describe('SimpleTimer count-down', () => {
let timer: SimpleTimer;
describe('normal timer flow', () => {
const initialTime = 1000;
timer = new SimpleTimer();
test('setting the timer duration', () => {
const newState = timer.setTime(initialTime);
const expected: SimpleTimerState = {
duration: initialTime,
current: initialTime,
direction: SimpleDirection.CountDown,
playback: SimplePlayback.Stop,
};
expect(newState).toStrictEqual(expected);
});
test('setting the timer to play', () => {
const newState = timer.start(0);
const expected: SimpleTimerState = {
duration: initialTime,
current: initialTime,
direction: SimpleDirection.CountDown,
playback: SimplePlayback.Start,
};
expect(newState).toStrictEqual(expected);
});
test('updating the timer', () => {
let newState = timer.update(100);
const expected: SimpleTimerState = {
duration: initialTime,
current: initialTime - 100,
direction: SimpleDirection.CountDown,
playback: SimplePlayback.Start,
};
expect(newState).toStrictEqual(expected);
newState = timer.update(500);
expected.current = initialTime - 500;
expect(newState).toStrictEqual(expected);
newState = timer.update(1500);
expected.current = initialTime - 1500;
expect(newState).toStrictEqual(expected);
});
test('pausing the time doesnt affect the current', () => {
const pausedTime = 200;
let newState = timer.pause(1500);
const expected: SimpleTimerState = {
duration: initialTime,
current: initialTime - 1500,
direction: SimpleDirection.CountDown,
playback: SimplePlayback.Pause,
};
expect(newState).toStrictEqual(expected);
newState = timer.update(1600);
expect(newState).toStrictEqual(expected);
newState = timer.update(1700);
expect(newState).toStrictEqual(expected);
newState = timer.start(1700);
expected.playback = SimplePlayback.Start;
expect(newState).toStrictEqual(expected);
newState = timer.update(1800);
(expected.current = initialTime - 1800 + pausedTime), expect(newState).toStrictEqual(expected);
});
test('stopping the timer clears the running data', () => {
const newState = timer.stop();
const expected: SimpleTimerState = {
duration: initialTime,
current: initialTime,
direction: SimpleDirection.CountDown,
playback: SimplePlayback.Stop,
};
expect(newState).toStrictEqual(expected);
});
test('count-up mode', () => {
const initialState = timer.setDirection(SimpleDirection.CountUp);
expect(initialState.current).toBe(initialTime);
let newState = timer.start(0);
const expected: SimpleTimerState = {
duration: initialTime,
current: initialTime,
direction: SimpleDirection.CountUp,
playback: SimplePlayback.Start,
};
newState = timer.update(100);
expected.current = initialTime + 100;
expect(newState).toStrictEqual(expected);
expect(newState).toStrictEqual(expected);
newState = timer.update(500);
expected.current = initialTime + 500;
expect(newState).toStrictEqual(expected);
newState = timer.update(1500);
expected.current = initialTime + 1500;
expect(newState).toStrictEqual(expected);
});
test('changing direction stops the timer', () => {
const newState = timer.setDirection(SimpleDirection.CountDown);
const expected: SimpleTimerState = {
duration: initialTime,
current: initialTime,
direction: SimpleDirection.CountDown,
playback: SimplePlayback.Stop,
};
expect(newState).toStrictEqual(expected);
});
});
});
@@ -1,4 +1,4 @@
import { DeepPartial, MessageState } from 'ontime-types';
import { DeepPartial, MessageState, SimpleDirection, SimplePlayback } from 'ontime-types';
// skipcq: JS-C1003 - we like the API
import * as assert from '../utils/assert.js';
@@ -8,6 +8,7 @@ import { messageService } from '../services/message-service/MessageService.js';
import { runtimeService } from '../services/runtime-service/RuntimeService.js';
import { eventStore } from '../stores/EventStore.js';
import { parse, updateEvent } from './integrationController.config.js';
import { extraTimerService } from '../services/extra-timer-service/ExtraTimerService.js';
import { validateMessage, validateTimerMessage } from '../services/message-service/messageUtils.js';
export type ChangeOptions = {
@@ -152,6 +153,40 @@ const actionHandlers: Record<string, ActionHandler> = {
runtimeService.addTime(time * 1000);
return { payload: 'success' };
},
/* Extra timers */
extratimer: (payload) => {
if (payload && typeof payload === 'string') {
if (payload === SimplePlayback.Start) {
const reply = extraTimerService.start();
return { payload: reply };
}
if (payload === SimplePlayback.Pause) {
const reply = extraTimerService.pause();
return { payload: reply };
}
if (payload === SimplePlayback.Stop) {
const reply = extraTimerService.stop();
return { payload: reply };
}
}
if (payload && typeof payload === 'object') {
if ('settime' in payload) {
const time = numberOrError(payload.settime);
const reply = extraTimerService.setTime(time);
return { payload: reply };
}
if ('direction' in payload) {
if (payload.direction === SimpleDirection.CountUp || payload.direction === SimpleDirection.CountDown) {
const reply = extraTimerService.setDirection(payload.direction);
return { payload: reply };
} else {
throw new Error('Invalid direction payload');
}
}
}
throw new Error('Invalid extratimer payload');
},
};
/**
* Returns a value of type number, converting if necessary
@@ -0,0 +1,77 @@
import { SimpleDirection, SimpleTimerState } from 'ontime-types';
import { SimpleTimer } from '../../classes/simple-timer/SimpleTimer.js';
import { eventStore } from '../../stores/EventStore.js';
export type EmitFn = (state: SimpleTimerState) => void;
export type GetTimeFn = () => number;
export class ExtraTimerService {
private timer: SimpleTimer;
private interval: NodeJS.Timer;
private emit: EmitFn;
private getTime: GetTimeFn;
constructor(emit: EmitFn, getTime: GetTimeFn) {
this.timer = new SimpleTimer();
this.emit = emit;
this.getTime = getTime;
}
private startInterval() {
this.interval = setInterval(this.update.bind(this), 500);
}
private stopInterval() {
clearInterval(this.interval);
}
@broadcastReturn
setDirection(direction: SimpleDirection) {
return this.timer.setDirection(direction);
}
@broadcastReturn
start() {
this.startInterval();
return this.timer.start(this.getTime());
}
@broadcastReturn
pause() {
return this.timer.pause(this.getTime());
}
@broadcastReturn
stop() {
this.stopInterval();
return this.timer.stop();
}
@broadcastReturn
setTime(duration: number) {
return this.timer.setTime(duration);
}
@broadcastReturn
private update() {
return this.timer.update(this.getTime());
}
}
function broadcastReturn(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const result = originalMethod.apply(this, args);
this.emit(result);
return result;
};
return descriptor;
}
const emit = (state: SimpleTimerState) => eventStore.set('timer1', state);
const timeNow = () => Date.now();
export const extraTimerService = new ExtraTimerService(emit, timeNow);