timeline-state-resolver
Version:
Have timeline, control stuff
174 lines • 8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StateHandler = void 0;
const lib_1 = require("../lib");
const measure_1 = require("./measure");
const commandExecutor_1 = require("./commandExecutor");
const CLOCK_INTERVAL = 20;
class StateHandler {
constructor(context, config, device) {
this.context = context;
this.config = config;
this.device = device;
this.stateQueue = [];
/** Semaphore, to ensure that .executeNextStateChange() is only executed one at a time */
this._executingStateChange = false;
this.logger = context.logger;
this.setCurrentState(undefined).catch((e) => {
this.logger.error('Error while creating new StateHandler', e);
});
this._commandExecutor = new commandExecutor_1.CommandExecutor(context.logger, this.config.executionType, async (c) => device.sendCommand(c));
this.clock = setInterval(() => {
const t = context.getCurrentTime();
// main clock to check if next state needs to be sent out
for (const state of this.stateQueue) {
const nextTime = Math.max(0, state?.state.time - (state?.preliminary ?? 0) - t);
if (nextTime > CLOCK_INTERVAL)
break;
// schedule any states between now and the next tick
setTimeout(() => {
if (!this._executingStateChange && this.stateQueue[0] === state) {
// if this is the next state, execute it
this.executeNextStateChange().catch((e) => {
this.logger.error('Error while executing next state change', e);
});
}
}, nextTime);
}
}, CLOCK_INTERVAL);
}
async terminate() {
clearInterval(this.clock);
this.stateQueue = [];
}
async clearFutureStates() {
this.stateQueue = [];
}
async handleState(state, mappings) {
const nextState = this.stateQueue[0];
const trace = (0, lib_1.startTrace)('device:convertTimelineStateToDeviceState', { deviceId: this.context.deviceId });
const deviceState = this.device.convertTimelineStateToDeviceState(state, mappings);
this.context.emitTimeTrace((0, lib_1.endTrace)(trace));
// Discard any states that comes after this one,
// and append this one to the end:
this.stateQueue = [
...this.stateQueue.filter((s) => s.state.time < state.time),
{
deviceState,
state,
mappings,
measurement: new measure_1.Measurement(state.time),
},
];
if (nextState !== this.stateQueue[0]) {
// the next state changed
if (nextState)
nextState.commands = undefined;
this.calculateNextStateChange().catch((e) => {
this.logger.error('Error while calculating next state change', e);
});
}
}
/**
* Sets the current state and makes sure the commands to get to the next state are still corrects
**/
async setCurrentState(state) {
this.currentState = {
commands: [],
deviceState: state,
state: this.currentState?.state || { time: this.context.getCurrentTime(), layers: {}, nextEvents: [] },
mappings: this.currentState?.mappings || {},
};
await this.calculateNextStateChange();
}
/**
* This takes in a DeviceState and then updates the commands such that the device
* will be put back into its intended state as designated by the timeline
* @todo: this may need to be tied into _executingStateChange variable
*/
async updateStateFromDeviceState(state) {
// update the current state to the state we received
const timelineState = this.currentState?.state || {
time: this.context.getCurrentTime(),
layers: {},
nextEvents: [],
};
const currentMappings = this.currentState?.mappings || {};
this.currentState = {
commands: [],
deviceState: state,
state: timelineState,
mappings: currentMappings,
};
// calculate how to get to the timeline state (because the device state may have changed based on device config changes or something)
const trace = (0, lib_1.startTrace)('device:convertTimelineStateToDeviceState', { deviceId: this.context.deviceId });
const deviceState = this.device.convertTimelineStateToDeviceState(timelineState, currentMappings); // @todo - we should probably be recalculating all of these :x
this.context.emitTimeTrace((0, lib_1.endTrace)(trace));
// push a new state
this.stateQueue.unshift({
deviceState: deviceState,
state: this.currentState?.state || { time: this.context.getCurrentTime(), layers: {}, nextEvents: [] },
mappings: this.currentState?.mappings || {},
});
// now we let it calculate commands to get into the right state, which should be executed immediately given this state is from the past
await this.calculateNextStateChange();
}
clearFutureAfterTimestamp(t) {
this.stateQueue = this.stateQueue.filter((s) => s.state.time <= t);
}
async calculateNextStateChange() {
if (!this.currentState)
return; // a change is currently being executed, we'll be called again once it's done
const nextState = this.stateQueue[0];
if (!nextState)
return;
try {
const trace = (0, lib_1.startTrace)('device:diffDeviceStates', { deviceId: this.context.deviceId });
nextState.commands = this.device.diffStates(this.currentState?.deviceState, nextState.deviceState, nextState.mappings, this.context.getCurrentTime());
nextState.preliminary = Math.max(0, ...nextState.commands.map((c) => c.preliminary ?? 0));
this.context.emitTimeTrace((0, lib_1.endTrace)(trace));
}
catch (e) {
// todo - log an error
this.logger.error('diffDeviceState failed, t = ' + nextState.state.time, e);
// we don't want to get stuck, so we should act as if this can be executed anyway
nextState.commands = [];
}
if (nextState.state.time - (nextState.preliminary ?? 0) <= this.context.getCurrentTime() && this.currentState) {
await this.executeNextStateChange();
}
}
async executeNextStateChange() {
if (!this.stateQueue[0] || this._executingStateChange) {
// there is no next to execute - or we are currently executing something
return;
}
this._executingStateChange = true;
if (!this.stateQueue[0].commands) {
await this.calculateNextStateChange();
}
const newState = this.stateQueue.shift();
if (!newState || !newState.commands) {
// this should not be possible given our previous guard?
return;
}
newState.measurement?.executeState();
this.currentState = undefined;
this._commandExecutor
.executeCommands(newState.commands, newState.measurement)
.then(() => {
if (newState.measurement)
this.context.reportStateChangeMeasurement(newState.measurement.report());
})
.catch((e) => {
this.logger.error('Error while executing next state change', e);
});
this.currentState = newState;
this._executingStateChange = false;
this.calculateNextStateChange().catch((e) => {
this.logger.error('Error while executing next state change', e);
});
}
}
exports.StateHandler = StateHandler;
//# sourceMappingURL=stateHandler.js.map