iobroker.statechart 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +59 -438
- package/admin/build/tab.js +133 -130
- package/admin/statechart.png +0 -0
- package/admin/{style.css → tab.css} +0 -2
- package/admin/tab_m.html +1 -1
- package/build/lib/chart-analysis.js +367 -0
- package/build/lib/chart-analysis.js.map +7 -0
- package/build/lib/chart-codegen.js +405 -0
- package/build/lib/chart-codegen.js.map +7 -0
- package/build/lib/chart-helpers.js +102 -0
- package/build/lib/chart-helpers.js.map +7 -0
- package/build/lib/chart-model.js +477 -0
- package/build/lib/chart-model.js.map +7 -0
- package/build/lib/chart-runtime.js +1521 -0
- package/build/lib/chart-runtime.js.map +7 -0
- package/build/lib/chart-structure.js +172 -0
- package/build/lib/chart-structure.js.map +7 -0
- package/build/lib/chart-validation.js +372 -0
- package/build/lib/chart-validation.js.map +7 -0
- package/build/main.js +638 -0
- package/build/main.js.map +7 -0
- package/io-package.json +31 -34
- package/package.json +21 -17
- package/admin/i18n/de.json +0 -4
- package/admin/i18n/en.json +0 -4
- package/admin/i18n/es.json +0 -4
- package/admin/i18n/fr.json +0 -4
- package/admin/i18n/it.json +0 -4
- package/admin/i18n/nl.json +0 -4
- package/admin/i18n/pl.json +0 -4
- package/admin/i18n/pt.json +0 -4
- package/admin/i18n/ru.json +0 -4
- package/admin/i18n/uk.json +0 -4
- package/admin/i18n/zh-cn.json +0 -4
- package/admin/jsonConfig.json +0 -19
- package/admin/statechart.svg +0 -38
- package/lib/adapter-config.d.ts +0 -19
- package/lib/chart-codegen.js +0 -436
- package/lib/chart-codegen.test.js +0 -132
- package/lib/chart-helpers.js +0 -74
- package/lib/chart-helpers.test.js +0 -38
- package/lib/chart-model.js +0 -577
- package/lib/chart-model.test.js +0 -189
- package/lib/chart-runtime.js +0 -797
- package/lib/chart-runtime.test.js +0 -699
- package/lib/chart-structure.js +0 -106
- package/lib/chart-validation.js +0 -328
- package/lib/chart-validation.test.js +0 -149
- package/main.js +0 -279
|
@@ -0,0 +1,1521 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var chart_runtime_exports = {};
|
|
30
|
+
__export(chart_runtime_exports, {
|
|
31
|
+
ChartRuntime: () => ChartRuntime,
|
|
32
|
+
getChartHash: () => getChartHash
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(chart_runtime_exports);
|
|
35
|
+
var import_node_crypto = __toESM(require("node:crypto"));
|
|
36
|
+
var import_node_util = require("node:util");
|
|
37
|
+
var import_node_vm = __toESM(require("node:vm"));
|
|
38
|
+
var import_suncalc = __toESM(require("suncalc"));
|
|
39
|
+
var import_xstate = require("xstate");
|
|
40
|
+
var import_chart_helpers = require("./chart-helpers");
|
|
41
|
+
var import_chart_model = require("./chart-model");
|
|
42
|
+
var import_chart_structure = require("./chart-structure");
|
|
43
|
+
var import_chart_validation = require("./chart-validation");
|
|
44
|
+
const SCRIPT_TIMEOUT_MS = 1e3;
|
|
45
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
46
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
47
|
+
const MAX_ADVANCE_TIMERS = 1e4;
|
|
48
|
+
const PERSISTED_VERSION = 1;
|
|
49
|
+
function getChartHash(chart) {
|
|
50
|
+
const logic = {
|
|
51
|
+
initial: chart.initial,
|
|
52
|
+
symbols: chart.symbols,
|
|
53
|
+
// comments, notes and colors are only documentation
|
|
54
|
+
states: chart.states.map(({ x: _x, y: _y, width: _w, height: _h, comment: _c, color: _co, ...state }) => state),
|
|
55
|
+
transitions: chart.transitions.map(
|
|
56
|
+
({
|
|
57
|
+
sourceHandle: _s,
|
|
58
|
+
targetHandle: _t,
|
|
59
|
+
sourceAnchor: _sa,
|
|
60
|
+
targetAnchor: _ta,
|
|
61
|
+
labelOffset: _lo,
|
|
62
|
+
comment: _c,
|
|
63
|
+
...transition
|
|
64
|
+
}) => transition
|
|
65
|
+
),
|
|
66
|
+
functions: (chart.functions || []).map((fn) => fn.code)
|
|
67
|
+
};
|
|
68
|
+
return import_node_crypto.default.createHash("sha1").update(JSON.stringify(logic)).digest("hex");
|
|
69
|
+
}
|
|
70
|
+
function toSnapshot(state) {
|
|
71
|
+
return state ? { val: state.val, ack: !!state.ack, ts: state.ts, lc: state.lc } : null;
|
|
72
|
+
}
|
|
73
|
+
class ChartRuntime {
|
|
74
|
+
/**
|
|
75
|
+
* @param chartId ID of the chart object
|
|
76
|
+
* @param chart The chart
|
|
77
|
+
* @param host The connection to the outside world
|
|
78
|
+
* @param simulated Simulation: the inputs are only changed by simulateInput(), outputs, setState() etc. are not
|
|
79
|
+
* written but recorded in the history, nothing is persisted
|
|
80
|
+
*/
|
|
81
|
+
constructor(chartId, chart, host, simulated = false) {
|
|
82
|
+
this.chartId = chartId;
|
|
83
|
+
this.chart = chart;
|
|
84
|
+
this.host = host;
|
|
85
|
+
this.simulated = simulated;
|
|
86
|
+
this.structure = new import_chart_structure.ChartStructure(chart);
|
|
87
|
+
this.statesById = this.structure.statesById;
|
|
88
|
+
this.sandbox = import_node_vm.default.createContext({ ...this.createHelpers(), ...import_chart_model.TIME_UNITS });
|
|
89
|
+
this.hash = getChartHash(chart);
|
|
90
|
+
this.prepareAllCode();
|
|
91
|
+
}
|
|
92
|
+
structure;
|
|
93
|
+
statesById;
|
|
94
|
+
/** timers of setStateDelayed() */
|
|
95
|
+
delayedWrites = /* @__PURE__ */ new Set();
|
|
96
|
+
/** values of all symbols, this is also the global scope of conditions and actions */
|
|
97
|
+
sandbox;
|
|
98
|
+
/** last value written to (or read from) each output */
|
|
99
|
+
outputValues = /* @__PURE__ */ new Map();
|
|
100
|
+
/** transitions which leave the source state, by source, sorted */
|
|
101
|
+
outerTransitions = /* @__PURE__ */ new Map();
|
|
102
|
+
/** transitions to children of the source state, by source, sorted */
|
|
103
|
+
innerTransitions = /* @__PURE__ */ new Map();
|
|
104
|
+
/** temporal transitions by source */
|
|
105
|
+
temporalTransitions = /* @__PURE__ */ new Map();
|
|
106
|
+
/** all active states, parents before children */
|
|
107
|
+
activeIds = [];
|
|
108
|
+
/** time each active state was entered */
|
|
109
|
+
enteredAt = /* @__PURE__ */ new Map();
|
|
110
|
+
/** how often each state was entered, to notice that a parallel state was left and entered again */
|
|
111
|
+
entryCount = /* @__PURE__ */ new Map();
|
|
112
|
+
/** timers of the temporal transitions by state */
|
|
113
|
+
timers = /* @__PURE__ */ new Map();
|
|
114
|
+
/** after(n, unit) transitions whose time has come */
|
|
115
|
+
expired = /* @__PURE__ */ new Set();
|
|
116
|
+
/** at() and every() transitions whose timer caused the current wakeup */
|
|
117
|
+
firedTimers = /* @__PURE__ */ new Set();
|
|
118
|
+
/** wakeups by each input since each active state was entered, by state */
|
|
119
|
+
inputCounts = /* @__PURE__ */ new Map();
|
|
120
|
+
/** since when the condition of each after(n, unit, condition) of the active states is true, by transition */
|
|
121
|
+
trueSince = /* @__PURE__ */ new Map();
|
|
122
|
+
/** timers of after(n, unit, condition), by transition */
|
|
123
|
+
durationTimers = /* @__PURE__ */ new Map();
|
|
124
|
+
/** for duration(): since when the value of each call is true, by call and state */
|
|
125
|
+
durationCalls = /* @__PURE__ */ new Map();
|
|
126
|
+
/** number of duration() calls found in the code, to give each of them an ID */
|
|
127
|
+
durationCallCount = 0;
|
|
128
|
+
/** the state whose condition or action is executed, used by elapsed() and duration() */
|
|
129
|
+
context = null;
|
|
130
|
+
/** timers of inputs with debounce, by variable name */
|
|
131
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
132
|
+
/** JSON of the last persisted data, to persist only changes */
|
|
133
|
+
lastPersisted = "";
|
|
134
|
+
/** name paths of the active leaf states, like the "activeState" state */
|
|
135
|
+
activeNames = "";
|
|
136
|
+
/** events which are not yet passed to host.onHistory() */
|
|
137
|
+
pendingHistory = [];
|
|
138
|
+
/** JSON of the last values passed to host.onValues(), to pass only changes */
|
|
139
|
+
lastValues = "";
|
|
140
|
+
/** what caused the current wakeup, for the history */
|
|
141
|
+
wakeupCause = "input";
|
|
142
|
+
/** name of the input which caused the current wakeup, for the history */
|
|
143
|
+
wakeupInput = null;
|
|
144
|
+
/** the code of conditions, actions and function blocks prepared for the change detection, by kind and code */
|
|
145
|
+
preparedCode = /* @__PURE__ */ new Map();
|
|
146
|
+
/** the variables used by hasChanged() etc. */
|
|
147
|
+
changeVariables = /* @__PURE__ */ new Set();
|
|
148
|
+
/** the IDs of the states read by getState(), they are read at the start and then kept up to date */
|
|
149
|
+
readStateIds = /* @__PURE__ */ new Set();
|
|
150
|
+
/** the states read by getState(), null if a state does not exist */
|
|
151
|
+
stateCache = /* @__PURE__ */ new Map();
|
|
152
|
+
/** their values at the start of the previous wakeup, null before the second wakeup */
|
|
153
|
+
previousValues = null;
|
|
154
|
+
/** their values at the start of the current wakeup, null before the first one */
|
|
155
|
+
currentValues = null;
|
|
156
|
+
hash;
|
|
157
|
+
actor = null;
|
|
158
|
+
/** true while a wakeup is processed, effects outside of wakeups are published right away */
|
|
159
|
+
wakingUp = false;
|
|
160
|
+
/** all running timers, to fire them early when the time of a simulation is advanced */
|
|
161
|
+
scheduled = /* @__PURE__ */ new Set();
|
|
162
|
+
/** simulation: added to the real time, see advanceTime() */
|
|
163
|
+
timeOffset = 0;
|
|
164
|
+
/** simulation: paused, the wakeups wait in the queue */
|
|
165
|
+
paused = false;
|
|
166
|
+
/** simulation: the wakeups which wait while paused */
|
|
167
|
+
queue = [];
|
|
168
|
+
/** simulation: IDs of the states and transitions with a breakpoint */
|
|
169
|
+
breakpoints = /* @__PURE__ */ new Set();
|
|
170
|
+
/** simulation: set when a breakpoint is reached during a wakeup, it pauses after the step */
|
|
171
|
+
pendingBreak = null;
|
|
172
|
+
/** simulation: where it stopped at a breakpoint, until it continues */
|
|
173
|
+
breakpointHit = null;
|
|
174
|
+
/** JSON of the last status passed to host.onSimulationStatus() */
|
|
175
|
+
lastStatus = "";
|
|
176
|
+
/** the current time, can be replaced in tests. A simulation can be ahead of the real time. */
|
|
177
|
+
now = () => Date.now() + this.timeOffset;
|
|
178
|
+
/**
|
|
179
|
+
* Prepares all code of the chart for the change detection, so its variables are known from the first wakeup on
|
|
180
|
+
*/
|
|
181
|
+
prepareAllCode() {
|
|
182
|
+
for (const state of this.chart.states) {
|
|
183
|
+
for (const code of [state.entry, state.during, state.exit]) {
|
|
184
|
+
this.prepareCode(code, "statements");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const transition of this.chart.transitions) {
|
|
188
|
+
try {
|
|
189
|
+
const label = (0, import_chart_model.parseTransitionLabel)(transition.label);
|
|
190
|
+
this.prepareCode(label.temporal?.condition, "expression");
|
|
191
|
+
this.prepareCode(label.condition, "expression");
|
|
192
|
+
this.prepareCode(label.action, "statements");
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const fn of this.chart.functions || []) {
|
|
197
|
+
this.prepareCode(fn.code, "statements");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* @param code A condition, action or function block
|
|
202
|
+
* @param kind Expression or statements
|
|
203
|
+
* @returns the code with the names of the variables in hasChanged() etc. and the IDs of the duration() calls,
|
|
204
|
+
* see prepareHelperCalls()
|
|
205
|
+
*/
|
|
206
|
+
prepareCode(code, kind) {
|
|
207
|
+
if (!code) {
|
|
208
|
+
return "";
|
|
209
|
+
}
|
|
210
|
+
const key = `${kind}:${code}`;
|
|
211
|
+
let prepared = this.preparedCode.get(key);
|
|
212
|
+
if (prepared === void 0) {
|
|
213
|
+
const quoted = (0, import_chart_validation.prepareHelperCalls)(code, kind, () => `d${++this.durationCallCount}`);
|
|
214
|
+
prepared = quoted.code;
|
|
215
|
+
for (const name of quoted.names) {
|
|
216
|
+
this.changeVariables.add(name);
|
|
217
|
+
}
|
|
218
|
+
for (const id of quoted.stateIds) {
|
|
219
|
+
this.readStateIds.add(id);
|
|
220
|
+
}
|
|
221
|
+
this.preparedCode.set(key, prepared);
|
|
222
|
+
}
|
|
223
|
+
return prepared;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* IDs of all states this chart reads
|
|
227
|
+
*/
|
|
228
|
+
get inputStateIds() {
|
|
229
|
+
return this.chart.symbols.filter((s) => s.scope === "input" && s.stateId).map((s) => s.stateId);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* IDs of all states whose changes must be passed to handleStateChange(): the inputs and the states read by
|
|
233
|
+
* getState()
|
|
234
|
+
*/
|
|
235
|
+
get subscribedStateIds() {
|
|
236
|
+
return [.../* @__PURE__ */ new Set([...this.simulated ? [] : this.inputStateIds, ...this.readStateIds])];
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* @param options Start options
|
|
240
|
+
* @param options.restore Continue with the persisted state, if the chart was not changed since
|
|
241
|
+
*/
|
|
242
|
+
async start(options = {}) {
|
|
243
|
+
this.logProblems();
|
|
244
|
+
for (const id of this.readStateIds) {
|
|
245
|
+
try {
|
|
246
|
+
let state;
|
|
247
|
+
if (this.host.getState) {
|
|
248
|
+
state = await this.host.getState(id);
|
|
249
|
+
} else {
|
|
250
|
+
const val = await this.host.readState(id);
|
|
251
|
+
state = val === void 0 ? null : { val, ack: true, ts: this.now(), lc: this.now() };
|
|
252
|
+
}
|
|
253
|
+
this.stateCache.set(id, toSnapshot(state));
|
|
254
|
+
} catch (err) {
|
|
255
|
+
this.host.log.warn(`[${this.chart.name}] Cannot read ${id}: ${err.message}`);
|
|
256
|
+
this.stateCache.set(id, null);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const symbol of this.chart.symbols) {
|
|
260
|
+
if (symbol.scope === "local") {
|
|
261
|
+
this.sandbox[symbol.name] = structuredClone(symbol.initial);
|
|
262
|
+
} else if (symbol.stateId) {
|
|
263
|
+
const val = await this.host.readState(symbol.stateId);
|
|
264
|
+
this.sandbox[symbol.name] = val;
|
|
265
|
+
if (symbol.scope === "output") {
|
|
266
|
+
this.outputValues.set(symbol.name, val);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const fn of this.chart.functions || []) {
|
|
271
|
+
const where = `function block ${(0, import_chart_validation.getDeclarations)(fn.code).map((d) => d.name).join(", ")}`;
|
|
272
|
+
this.run(this.compile(this.prepareCode(fn.code, "statements"), where), where);
|
|
273
|
+
}
|
|
274
|
+
this.writeChangedOutputs();
|
|
275
|
+
this.wakingUp = true;
|
|
276
|
+
const machine = (0, import_xstate.createMachine)(this.buildMachineConfig());
|
|
277
|
+
const persisted = options.restore && this.chart.restoreState !== false ? await this.loadPersisted() : null;
|
|
278
|
+
let restored = false;
|
|
279
|
+
if (persisted) {
|
|
280
|
+
try {
|
|
281
|
+
this.restore(machine, persisted);
|
|
282
|
+
restored = true;
|
|
283
|
+
this.host.log.info(`[${this.chart.name}] Restored the state from before the restart`);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
this.host.log.warn(`[${this.chart.name}] Cannot restore the state, starting anew: ${err.message}`);
|
|
286
|
+
this.stop();
|
|
287
|
+
this.startActor(machine);
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
this.startActor(machine);
|
|
291
|
+
}
|
|
292
|
+
this.pendingHistory.unshift({ ts: this.now(), kind: "start", restored, active: this.activeNames });
|
|
293
|
+
this.wakingUp = false;
|
|
294
|
+
this.wakeup(null, "start");
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Starts the chart in its initial state, the entry actions are executed
|
|
298
|
+
*
|
|
299
|
+
* @param machine The XState machine of the chart
|
|
300
|
+
*/
|
|
301
|
+
startActor(machine) {
|
|
302
|
+
this.activeIds = [];
|
|
303
|
+
this.enteredAt.clear();
|
|
304
|
+
this.actor = (0, import_xstate.createActor)(machine);
|
|
305
|
+
this.subscribe();
|
|
306
|
+
this.actor.start();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Continues with a persisted state: the entry actions are not executed again,
|
|
310
|
+
* the timers of the temporal transitions continue with their remaining time
|
|
311
|
+
*
|
|
312
|
+
* @param machine The XState machine of the chart
|
|
313
|
+
* @param persisted The stored state
|
|
314
|
+
*/
|
|
315
|
+
restore(machine, persisted) {
|
|
316
|
+
for (const symbol of this.chart.symbols) {
|
|
317
|
+
if (symbol.scope === "local" && symbol.name in persisted.locals) {
|
|
318
|
+
this.sandbox[symbol.name] = persisted.locals[symbol.name];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.actor = (0, import_xstate.createActor)(machine, { snapshot: persisted.machine });
|
|
322
|
+
this.subscribe();
|
|
323
|
+
this.actor.start();
|
|
324
|
+
this.publishActiveStates(this.actor.getSnapshot().value);
|
|
325
|
+
for (const id of this.activeIds) {
|
|
326
|
+
this.enteredAt.set(id, persisted.entered[id] ?? this.now());
|
|
327
|
+
this.inputCounts.set(id, new Map(Object.entries(persisted.counts?.[id] ?? {})));
|
|
328
|
+
for (const transition of this.temporalTransitions.get(id) || []) {
|
|
329
|
+
const since = persisted.trueSince?.[transition.id];
|
|
330
|
+
if (since !== void 0) {
|
|
331
|
+
this.trueSince.set(transition.id, since);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
this.startTimers(id);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Publishes the active states after each transition, and logs errors of the state machine
|
|
339
|
+
*/
|
|
340
|
+
subscribe() {
|
|
341
|
+
this.actor?.subscribe({
|
|
342
|
+
next: (snapshot) => this.publishActiveStates(snapshot.value),
|
|
343
|
+
error: (err) => this.host.log.error(`[${this.chart.name}] ${String(err)}`)
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* @returns the persisted data, if it fits to this chart
|
|
348
|
+
*/
|
|
349
|
+
async loadPersisted() {
|
|
350
|
+
let persisted;
|
|
351
|
+
try {
|
|
352
|
+
persisted = await this.host.loadPersisted?.();
|
|
353
|
+
} catch (err) {
|
|
354
|
+
this.host.log.warn(`[${this.chart.name}] Cannot load the stored state: ${err.message}`);
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
if (!persisted?.machine || persisted.version !== PERSISTED_VERSION) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
if (persisted.hash !== this.hash) {
|
|
361
|
+
this.host.log.info(`[${this.chart.name}] The chart was changed, starting in the initial state`);
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
...persisted,
|
|
366
|
+
entered: persisted.entered || {},
|
|
367
|
+
locals: persisted.locals || {},
|
|
368
|
+
counts: persisted.counts || {},
|
|
369
|
+
trueSince: persisted.trueSince || {}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Stores the current state of the chart, if it changed since the last time
|
|
374
|
+
*/
|
|
375
|
+
persist() {
|
|
376
|
+
if (!this.actor || !this.host.persist || this.simulated) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const data = {
|
|
380
|
+
version: PERSISTED_VERSION,
|
|
381
|
+
hash: this.hash,
|
|
382
|
+
machine: this.actor.getPersistedSnapshot(),
|
|
383
|
+
entered: Object.fromEntries(this.enteredAt),
|
|
384
|
+
locals: Object.fromEntries(
|
|
385
|
+
this.chart.symbols.filter((s) => s.scope === "local").map((s) => [s.name, this.sandbox[s.name]])
|
|
386
|
+
),
|
|
387
|
+
counts: Object.fromEntries(
|
|
388
|
+
[...this.inputCounts].filter(([, counts]) => counts.size).map(([id, counts]) => [id, Object.fromEntries(counts)])
|
|
389
|
+
),
|
|
390
|
+
trueSince: Object.fromEntries(this.trueSince)
|
|
391
|
+
};
|
|
392
|
+
let json;
|
|
393
|
+
try {
|
|
394
|
+
json = JSON.stringify(data);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
this.host.log.warn(`[${this.chart.name}] Cannot store the state: ${err.message}`);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (json !== this.lastPersisted) {
|
|
400
|
+
this.lastPersisted = json;
|
|
401
|
+
this.host.persist(data);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Called by XState before the entry action of a state
|
|
406
|
+
*
|
|
407
|
+
* @param id ID of the state
|
|
408
|
+
*/
|
|
409
|
+
onEnter(id) {
|
|
410
|
+
if (this.simulated && this.breakpoints.has(id)) {
|
|
411
|
+
this.pendingBreak ??= this.statesById.get(id)?.name ?? id;
|
|
412
|
+
}
|
|
413
|
+
this.enteredAt.set(id, this.now());
|
|
414
|
+
this.entryCount.set(id, (this.entryCount.get(id) ?? 0) + 1);
|
|
415
|
+
this.inputCounts.set(id, /* @__PURE__ */ new Map());
|
|
416
|
+
this.startTimers(id);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Called by XState after the exit action of a state
|
|
420
|
+
*
|
|
421
|
+
* @param id ID of the state
|
|
422
|
+
*/
|
|
423
|
+
onExit(id) {
|
|
424
|
+
this.enteredAt.delete(id);
|
|
425
|
+
for (const timer of this.timers.get(id) || []) {
|
|
426
|
+
this.cancelTimer(timer);
|
|
427
|
+
}
|
|
428
|
+
this.timers.delete(id);
|
|
429
|
+
this.inputCounts.delete(id);
|
|
430
|
+
for (const transition of this.temporalTransitions.get(id) || []) {
|
|
431
|
+
this.expired.delete(transition.id);
|
|
432
|
+
this.stopDuration(transition.id);
|
|
433
|
+
}
|
|
434
|
+
for (const [key, call] of this.durationCalls) {
|
|
435
|
+
if (call.state === id) {
|
|
436
|
+
this.durationCalls.delete(key);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Stops measuring how long the condition of an after(n, unit, condition) is true
|
|
442
|
+
*
|
|
443
|
+
* @param id ID of the transition
|
|
444
|
+
*/
|
|
445
|
+
stopDuration(id) {
|
|
446
|
+
this.trueSince.delete(id);
|
|
447
|
+
this.cancelTimer(this.durationTimers.get(id));
|
|
448
|
+
this.durationTimers.delete(id);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Starts a timer with the functions of the host, so the adapter can clear it when it is unloaded
|
|
452
|
+
*
|
|
453
|
+
* @param callback Called when the time has come
|
|
454
|
+
* @param ms The delay
|
|
455
|
+
* @returns the handle to stop it
|
|
456
|
+
*/
|
|
457
|
+
startTimeout(callback, ms) {
|
|
458
|
+
return this.host.setTimeout ? this.host.setTimeout(callback, ms) : globalThis.setTimeout(callback, ms);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* @param handle The handle of a timer started by startTimeout(), may be undefined
|
|
462
|
+
*/
|
|
463
|
+
stopTimeout(handle) {
|
|
464
|
+
if (handle === void 0) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (this.host.clearTimeout) {
|
|
468
|
+
this.host.clearTimeout(handle);
|
|
469
|
+
} else {
|
|
470
|
+
globalThis.clearTimeout(handle);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* @param timer The timer to stop, may be undefined
|
|
475
|
+
*/
|
|
476
|
+
cancelTimer(timer) {
|
|
477
|
+
if (timer) {
|
|
478
|
+
this.stopTimeout(timer.handle);
|
|
479
|
+
this.scheduled.delete(timer);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Wakes up the chart for a timer. A paused simulation queues the wakeup.
|
|
484
|
+
*
|
|
485
|
+
* @param fired ID of the at() or every() transition whose timer fired, they are only valid in this wakeup
|
|
486
|
+
*/
|
|
487
|
+
timerWakeup(fired) {
|
|
488
|
+
if (this.simulated && this.paused) {
|
|
489
|
+
this.queue.push({ kind: "timer", fired });
|
|
490
|
+
this.publishLive();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (fired) {
|
|
494
|
+
this.firedTimers.add(fired);
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
this.wakeup(null);
|
|
498
|
+
} finally {
|
|
499
|
+
if (fired) {
|
|
500
|
+
this.firedTimers.delete(fired);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Calls the callback at the given time, also after delays which are too long for setTimeout()
|
|
506
|
+
*
|
|
507
|
+
* @param due The time (ms since 1970), the callback is called right away if it has passed
|
|
508
|
+
* @param timer Gets the handle of the timeout, to clear it
|
|
509
|
+
* @param callback Called at the time
|
|
510
|
+
*/
|
|
511
|
+
scheduleAt(due, timer, callback) {
|
|
512
|
+
this.stopTimeout(timer.handle);
|
|
513
|
+
timer.due = due;
|
|
514
|
+
timer.callback = callback;
|
|
515
|
+
this.scheduled.add(timer);
|
|
516
|
+
const remaining = due - this.now();
|
|
517
|
+
timer.handle = this.startTimeout(
|
|
518
|
+
() => {
|
|
519
|
+
if (remaining > MAX_TIMEOUT_MS && due - this.now() > 0) {
|
|
520
|
+
this.scheduleAt(due, timer, callback);
|
|
521
|
+
} else {
|
|
522
|
+
this.scheduled.delete(timer);
|
|
523
|
+
callback();
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
Math.min(Math.max(remaining, 0), MAX_TIMEOUT_MS)
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Starts a timer for every temporal transition of a state, which wakes up the chart when its time has come
|
|
531
|
+
*
|
|
532
|
+
* @param id ID of the state
|
|
533
|
+
*/
|
|
534
|
+
startTimers(id) {
|
|
535
|
+
const entered = this.enteredAt.get(id) ?? this.now();
|
|
536
|
+
const timers = [];
|
|
537
|
+
this.timers.set(id, timers);
|
|
538
|
+
for (const transition of this.temporalTransitions.get(id) || []) {
|
|
539
|
+
const { operator, ms, condition, time } = transition.temporal;
|
|
540
|
+
if (time !== void 0) {
|
|
541
|
+
const timer2 = { label: transition.where };
|
|
542
|
+
timers.push(timer2);
|
|
543
|
+
this.scheduleDaily(id, transition, timer2, this.now());
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (ms === void 0 || operator === "before" || condition !== void 0) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const timer = { label: transition.where };
|
|
550
|
+
timers.push(timer);
|
|
551
|
+
const fire = (due2) => {
|
|
552
|
+
if (operator === "after") {
|
|
553
|
+
this.expired.add(transition.id);
|
|
554
|
+
this.timerWakeup(null);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
this.timerWakeup(transition.id);
|
|
558
|
+
if (operator === "every" && this.timers.get(id)?.includes(timer)) {
|
|
559
|
+
this.scheduleAt(due2 + ms, timer, () => fire(due2 + ms));
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
let due = entered + ms;
|
|
563
|
+
if (operator === "every" && due < this.now()) {
|
|
564
|
+
due = entered + Math.ceil((this.now() - entered) / ms) * ms;
|
|
565
|
+
}
|
|
566
|
+
this.scheduleAt(due, timer, () => fire(due));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Starts the timer of at("sunset", offset) or at("22:30") for the next time, which wakes up the chart
|
|
571
|
+
*
|
|
572
|
+
* @param stateId ID of the source state
|
|
573
|
+
* @param transition The transition
|
|
574
|
+
* @param timer Gets the handle of the timeout
|
|
575
|
+
* @param after The timer is started for the next time after this one (ms since 1970)
|
|
576
|
+
*/
|
|
577
|
+
scheduleDaily(stateId, transition, timer, after) {
|
|
578
|
+
let due;
|
|
579
|
+
try {
|
|
580
|
+
due = this.getNextDailyTime(transition.temporal, after);
|
|
581
|
+
} catch (err) {
|
|
582
|
+
this.host.log.warn(`[${this.chart.name}] ${transition.where}: ${err.message}`);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (due === null) {
|
|
586
|
+
this.scheduleAt(
|
|
587
|
+
after + DAY_MS,
|
|
588
|
+
timer,
|
|
589
|
+
() => this.scheduleDaily(stateId, transition, timer, after + DAY_MS)
|
|
590
|
+
);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
this.scheduleAt(due, timer, () => {
|
|
594
|
+
this.timerWakeup(transition.id);
|
|
595
|
+
if (this.timers.get(stateId)?.includes(timer)) {
|
|
596
|
+
this.scheduleDaily(stateId, transition, timer, Math.max(due, this.now()));
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* @param temporal at("sunset", offset) or at("22:30")
|
|
602
|
+
* @param after The time is searched after this one (ms since 1970)
|
|
603
|
+
* @returns the next time (ms since 1970), null if there is none in the next week (astro events in polar regions)
|
|
604
|
+
* @throws {Error} for astro events if no location is configured
|
|
605
|
+
*/
|
|
606
|
+
getNextDailyTime(temporal, after) {
|
|
607
|
+
const time = temporal.time;
|
|
608
|
+
const offset = (temporal.offsetMinutes ?? 0) * 6e4;
|
|
609
|
+
const day = new Date(after);
|
|
610
|
+
day.setDate(day.getDate() - 1);
|
|
611
|
+
for (let i = 0; i < 9; i++) {
|
|
612
|
+
let at;
|
|
613
|
+
if (import_chart_model.TIME_OF_DAY.test(time)) {
|
|
614
|
+
const [hours, minutes, seconds = 0] = time.split(":").map(Number);
|
|
615
|
+
at = new Date(day).setHours(hours, minutes, seconds, 0);
|
|
616
|
+
} else {
|
|
617
|
+
const event = this.getSunTimes(new Date(new Date(day).setHours(12, 0, 0, 0)))[time];
|
|
618
|
+
at = event instanceof Date ? event.getTime() : null;
|
|
619
|
+
}
|
|
620
|
+
if (at !== null && at + offset > after) {
|
|
621
|
+
return at + offset;
|
|
622
|
+
}
|
|
623
|
+
day.setDate(day.getDate() + 1);
|
|
624
|
+
}
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Logs the problems found by the validation, the chart is started anyway
|
|
629
|
+
*/
|
|
630
|
+
logProblems() {
|
|
631
|
+
for (const { kind, id, field, diagnostic } of (0, import_chart_validation.validateChart)(this.chart)) {
|
|
632
|
+
let where;
|
|
633
|
+
if (kind === "state") {
|
|
634
|
+
where = `${this.statesById.get(id)?.name ?? id}${field ? `/${field}` : ""}`;
|
|
635
|
+
} else if (kind === "symbol") {
|
|
636
|
+
where = `Variable ${id}`;
|
|
637
|
+
} else if (kind === "function") {
|
|
638
|
+
const fn = this.chart.functions?.find((f) => f.id === id);
|
|
639
|
+
where = `Function block ${(0, import_chart_validation.getDeclarations)(fn?.code).map((d) => d.name).join(", ")}`;
|
|
640
|
+
} else {
|
|
641
|
+
const transition = this.chart.transitions.find((t) => t.id === id);
|
|
642
|
+
const source = this.statesById.get(transition?.source ?? "")?.name;
|
|
643
|
+
const target = this.statesById.get(transition?.target ?? "")?.name;
|
|
644
|
+
where = `Transition ${source} -> ${target}`;
|
|
645
|
+
}
|
|
646
|
+
const log = { error: this.host.log.error, warning: this.host.log.warn, info: this.host.log.debug }[diagnostic.severity];
|
|
647
|
+
log(`[${this.chart.name}] ${where}: ${diagnostic.message}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Stops the state machine and all timers, delayed writes are discarded
|
|
652
|
+
*/
|
|
653
|
+
stop() {
|
|
654
|
+
this.actor?.stop();
|
|
655
|
+
this.actor = null;
|
|
656
|
+
for (const timer of this.scheduled) {
|
|
657
|
+
this.stopTimeout(timer.handle);
|
|
658
|
+
}
|
|
659
|
+
this.scheduled.clear();
|
|
660
|
+
this.timers.clear();
|
|
661
|
+
this.expired.clear();
|
|
662
|
+
this.durationTimers.clear();
|
|
663
|
+
for (const timer of this.debounceTimers.values()) {
|
|
664
|
+
this.stopTimeout(timer);
|
|
665
|
+
}
|
|
666
|
+
this.debounceTimers.clear();
|
|
667
|
+
this.delayedWrites.clear();
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* The helper functions which can be used in conditions and actions, see chart-helpers.ts
|
|
671
|
+
*/
|
|
672
|
+
createHelpers() {
|
|
673
|
+
const prefix = `[${this.chart.name}]`;
|
|
674
|
+
const write = (id, value, ack) => this.host.writeState(String(id), value, !!ack).catch((err) => this.host.log.warn(`${prefix} Cannot write ${id}: ${err}`));
|
|
675
|
+
const describe = (name, args) => `${name}(${args.map((arg) => arg === void 0 ? "undefined" : JSON.stringify(arg)).join(", ")})`;
|
|
676
|
+
return {
|
|
677
|
+
log: (message, level = "info") => {
|
|
678
|
+
const method = ["debug", "info", "warn", "error"].includes(level) ? level : "info";
|
|
679
|
+
this.host.log[method](`${prefix} ${message}`);
|
|
680
|
+
},
|
|
681
|
+
setState: (id, value, ack = false) => {
|
|
682
|
+
if (this.simulated) {
|
|
683
|
+
this.recordEffect(describe("setState", [id, value, ...ack ? [true] : []]));
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
void write(id, value, ack);
|
|
687
|
+
},
|
|
688
|
+
setStateDelayed: (id, value, delayMs, ack = false) => {
|
|
689
|
+
const timer = { label: describe("setStateDelayed", [id, value]) };
|
|
690
|
+
this.delayedWrites.add(timer);
|
|
691
|
+
this.scheduleAt(this.now() + Math.max(Number(delayMs) || 0, 0), timer, () => {
|
|
692
|
+
this.delayedWrites.delete(timer);
|
|
693
|
+
if (this.simulated) {
|
|
694
|
+
this.recordEffect(describe("setState", [id, value, ...ack ? [true] : []]));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
void write(id, value, ack);
|
|
698
|
+
});
|
|
699
|
+
},
|
|
700
|
+
sendTo: (instance, command, message) => {
|
|
701
|
+
if (this.simulated) {
|
|
702
|
+
this.recordEffect(describe("sendTo", [instance, command, message]));
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (!this.host.sendTo) {
|
|
706
|
+
throw new Error("sendTo() is not available");
|
|
707
|
+
}
|
|
708
|
+
this.host.sendTo(String(instance), String(command), message);
|
|
709
|
+
},
|
|
710
|
+
isTimeBetween: (start, end) => (0, import_chart_helpers.isTimeBetween)(start, end, new Date(this.now())),
|
|
711
|
+
isDaytime: () => {
|
|
712
|
+
const now = new Date(this.now());
|
|
713
|
+
const { sunrise, sunset, alwaysUp } = this.getSunTimes(now);
|
|
714
|
+
if (!sunrise || !sunset) {
|
|
715
|
+
return !!alwaysUp;
|
|
716
|
+
}
|
|
717
|
+
return now >= sunrise && now < sunset;
|
|
718
|
+
},
|
|
719
|
+
astroTime: (name, offsetMinutes = 0) => {
|
|
720
|
+
const times = this.getSunTimes(new Date(this.now()));
|
|
721
|
+
const time = times[name];
|
|
722
|
+
if (!(time instanceof Date)) {
|
|
723
|
+
const known = name in times && name !== "alwaysUp" && name !== "alwaysDown";
|
|
724
|
+
throw new Error(
|
|
725
|
+
known ? `There is no "${name}" today at the location of the system settings` : `Unknown astro event "${name}"`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
return new Date(time.getTime() + (Number(offsetMinutes) || 0) * 6e4);
|
|
729
|
+
},
|
|
730
|
+
getState: (id) => {
|
|
731
|
+
if (!this.stateCache.has(id)) {
|
|
732
|
+
throw new Error(
|
|
733
|
+
`getState() can only read states whose ID is written as text, e.g. getState("${id}")`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
const state = this.stateCache.get(id);
|
|
737
|
+
return state ? { ...state } : null;
|
|
738
|
+
},
|
|
739
|
+
elapsed: (unit = import_chart_model.TIME_UNITS.sec) => {
|
|
740
|
+
const entered = this.context === null ? void 0 : this.enteredAt.get(this.context);
|
|
741
|
+
if (entered === void 0) {
|
|
742
|
+
throw new Error("elapsed() can only be used in conditions and in the actions of states");
|
|
743
|
+
}
|
|
744
|
+
return (this.now() - entered) / unit;
|
|
745
|
+
},
|
|
746
|
+
// the ID of the call is added by prepareHelperCalls()
|
|
747
|
+
duration: (call, value, unit = import_chart_model.TIME_UNITS.sec) => {
|
|
748
|
+
const key = `${call}@${this.context}`;
|
|
749
|
+
if (!value) {
|
|
750
|
+
this.durationCalls.delete(key);
|
|
751
|
+
return 0;
|
|
752
|
+
}
|
|
753
|
+
const since = this.durationCalls.get(key)?.since ?? this.now();
|
|
754
|
+
this.durationCalls.set(key, { since, state: this.context });
|
|
755
|
+
return (this.now() - since) / unit;
|
|
756
|
+
},
|
|
757
|
+
hasChanged: (name) => this.getChange(name, "hasChanged").changed,
|
|
758
|
+
hasChangedFrom: (name, value) => {
|
|
759
|
+
const { changed, previous } = this.getChange(name, "hasChangedFrom");
|
|
760
|
+
return changed && (0, import_node_util.isDeepStrictEqual)(previous, value);
|
|
761
|
+
},
|
|
762
|
+
hasChangedTo: (name, value) => {
|
|
763
|
+
const { changed, current } = this.getChange(name, "hasChangedTo");
|
|
764
|
+
return changed && (0, import_node_util.isDeepStrictEqual)(current, value);
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* @param date The day
|
|
770
|
+
* @returns the astro events of the day, null for events which do not happen on that day (polar regions)
|
|
771
|
+
* @throws {Error} if no location is configured
|
|
772
|
+
*/
|
|
773
|
+
getSunTimes(date) {
|
|
774
|
+
const location = this.host.getLocation?.();
|
|
775
|
+
if (!location) {
|
|
776
|
+
throw new Error("No location (latitude/longitude) configured in the ioBroker system settings");
|
|
777
|
+
}
|
|
778
|
+
return import_suncalc.default.getTimes(date, location.latitude, location.longitude);
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Must be called when one of the subscribedStateIds is updated
|
|
782
|
+
*
|
|
783
|
+
* @param stateId ID of the updated state
|
|
784
|
+
* @param state The new state
|
|
785
|
+
*/
|
|
786
|
+
handleStateChange(stateId, state) {
|
|
787
|
+
if (this.readStateIds.has(stateId)) {
|
|
788
|
+
this.stateCache.set(stateId, toSnapshot(state));
|
|
789
|
+
}
|
|
790
|
+
if (!this.simulated) {
|
|
791
|
+
this.handleInput(stateId, state.val, { ack: state.ack });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Simulation: sets the value of an input and wakes up the chart, like a change of the input
|
|
796
|
+
*
|
|
797
|
+
* @param name Name of the input
|
|
798
|
+
* @param val The new value
|
|
799
|
+
* @returns false if the chart has no input with this name
|
|
800
|
+
*/
|
|
801
|
+
simulateInput(name, val) {
|
|
802
|
+
const symbol = this.chart.symbols.find((s) => s.scope === "input" && s.name === name);
|
|
803
|
+
if (!symbol || !this.actor) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
if (this.paused) {
|
|
807
|
+
this.queue.push({ kind: "input", name, value: val });
|
|
808
|
+
this.publishLive();
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
this.applyInput(symbol, val);
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Simulation: pauses, the following wakeups wait in a queue
|
|
816
|
+
*/
|
|
817
|
+
pauseSimulation() {
|
|
818
|
+
this.paused = true;
|
|
819
|
+
this.publishLive();
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Simulation: processes the queued wakeups until a breakpoint is reached, then continues normally
|
|
823
|
+
*/
|
|
824
|
+
resumeSimulation() {
|
|
825
|
+
this.paused = false;
|
|
826
|
+
this.breakpointHit = null;
|
|
827
|
+
while (!this.paused && this.queue.length) {
|
|
828
|
+
this.processQueued(this.queue.shift());
|
|
829
|
+
}
|
|
830
|
+
this.publishLive();
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Simulation: processes the next queued wakeup, and stays paused
|
|
834
|
+
*
|
|
835
|
+
* @returns false if no wakeup was queued
|
|
836
|
+
*/
|
|
837
|
+
stepSimulation() {
|
|
838
|
+
const next = this.queue.shift();
|
|
839
|
+
if (!next) {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
this.breakpointHit = null;
|
|
843
|
+
this.paused = false;
|
|
844
|
+
try {
|
|
845
|
+
this.processQueued(next);
|
|
846
|
+
} finally {
|
|
847
|
+
this.paused = true;
|
|
848
|
+
}
|
|
849
|
+
this.publishLive();
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* @param wakeup A wakeup which waited while the simulation was paused
|
|
854
|
+
*/
|
|
855
|
+
processQueued(wakeup) {
|
|
856
|
+
if (wakeup.kind === "input") {
|
|
857
|
+
const symbol = this.chart.symbols.find((s) => s.scope === "input" && s.name === wakeup.name);
|
|
858
|
+
if (symbol) {
|
|
859
|
+
this.applyInput(symbol, wakeup.value);
|
|
860
|
+
}
|
|
861
|
+
} else if (wakeup.kind === "event") {
|
|
862
|
+
this.wakeup(wakeup.name);
|
|
863
|
+
} else if (wakeup.kind === "timer") {
|
|
864
|
+
this.timerWakeup(wakeup.fired);
|
|
865
|
+
} else {
|
|
866
|
+
this.wakeup(null);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Simulation: lets time pass. The timers which are due meanwhile fire in the order of their time, a paused
|
|
871
|
+
* simulation queues their wakeups.
|
|
872
|
+
*
|
|
873
|
+
* @param ms The time to advance
|
|
874
|
+
*/
|
|
875
|
+
advanceTime(ms) {
|
|
876
|
+
const target = this.now() + Math.max(0, ms);
|
|
877
|
+
for (let fired = 0; ; fired++) {
|
|
878
|
+
let next;
|
|
879
|
+
for (const timer of this.scheduled) {
|
|
880
|
+
if (timer.due <= target && (!next || timer.due < next.due)) {
|
|
881
|
+
next = timer;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (!next) {
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
if (fired >= MAX_ADVANCE_TIMERS) {
|
|
888
|
+
this.host.log.warn(`[${this.chart.name}] Stopped advancing the time after ${fired} timers`);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
this.timeOffset += Math.max(0, next.due - this.now());
|
|
892
|
+
this.cancelTimer(next);
|
|
893
|
+
next.callback();
|
|
894
|
+
}
|
|
895
|
+
this.timeOffset += Math.max(0, target - this.now());
|
|
896
|
+
for (const timer of [...this.scheduled]) {
|
|
897
|
+
this.scheduleAt(timer.due, timer, timer.callback);
|
|
898
|
+
}
|
|
899
|
+
this.publishLive();
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Simulation: advances the time to the next timer, see advanceTime()
|
|
903
|
+
*
|
|
904
|
+
* @returns false if there is no timer
|
|
905
|
+
*/
|
|
906
|
+
advanceToNextTimer() {
|
|
907
|
+
const next = this.getNextTimer();
|
|
908
|
+
if (!next) {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
this.advanceTime(next.due - this.now());
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* @returns the timer which fires next, null if there is none
|
|
916
|
+
*/
|
|
917
|
+
getNextTimer() {
|
|
918
|
+
let next;
|
|
919
|
+
for (const timer of this.scheduled) {
|
|
920
|
+
if (!next || timer.due < next.due) {
|
|
921
|
+
next = timer;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return next ? { due: next.due, label: next.label ?? "" } : null;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Simulation: records something which was not executed, shown in the history
|
|
928
|
+
*
|
|
929
|
+
* @param text What was not executed
|
|
930
|
+
*/
|
|
931
|
+
recordEffect(text) {
|
|
932
|
+
this.pendingHistory.push({ ts: this.now(), kind: "effect", text, active: this.activeNames });
|
|
933
|
+
if (!this.wakingUp) {
|
|
934
|
+
this.publishLive();
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Must be called when an input state is updated
|
|
939
|
+
*
|
|
940
|
+
* @param stateId ID of the updated state
|
|
941
|
+
* @param val The new value
|
|
942
|
+
* @param options The ack flag of the update
|
|
943
|
+
* @param options.ack The ack flag of the update
|
|
944
|
+
*/
|
|
945
|
+
handleInput(stateId, val, options = {}) {
|
|
946
|
+
if (!this.actor) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
for (const symbol of this.chart.symbols) {
|
|
950
|
+
if (symbol.scope !== "input" || symbol.stateId !== stateId) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (symbol.ack === "ack" && options.ack !== true || symbol.ack === "command" && options.ack !== false) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const debounceMs = symbol.debounceMs || 0;
|
|
957
|
+
if (debounceMs > 0) {
|
|
958
|
+
this.stopTimeout(this.debounceTimers.get(symbol.name));
|
|
959
|
+
this.debounceTimers.set(
|
|
960
|
+
symbol.name,
|
|
961
|
+
this.startTimeout(() => {
|
|
962
|
+
this.debounceTimers.delete(symbol.name);
|
|
963
|
+
this.applyInput(symbol, val);
|
|
964
|
+
}, debounceMs)
|
|
965
|
+
);
|
|
966
|
+
} else {
|
|
967
|
+
this.applyInput(symbol, val);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Wakes up the chart with the event of an input, without changing the value of the input
|
|
973
|
+
* (state charts.<chart>.event)
|
|
974
|
+
*
|
|
975
|
+
* @param name Name of the input
|
|
976
|
+
* @returns false if the chart has no input with this name
|
|
977
|
+
*/
|
|
978
|
+
handleEvent(name) {
|
|
979
|
+
if (!this.chart.symbols.some((s) => s.scope === "input" && s.name === name)) {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
if (this.simulated && this.paused) {
|
|
983
|
+
this.queue.push({ kind: "event", name });
|
|
984
|
+
this.publishLive();
|
|
985
|
+
} else {
|
|
986
|
+
this.wakeup(name);
|
|
987
|
+
}
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Sets the variable of an input and wakes up the chart
|
|
992
|
+
*
|
|
993
|
+
* @param symbol The input
|
|
994
|
+
* @param val The new value
|
|
995
|
+
*/
|
|
996
|
+
applyInput(symbol, val) {
|
|
997
|
+
if (symbol.trigger === "change" && val === this.sandbox[symbol.name]) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
this.sandbox[symbol.name] = val;
|
|
1001
|
+
this.wakeup(symbol.name);
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* @param input Name of the input which caused the wakeup, null for timers and the start
|
|
1005
|
+
* @param cause What caused the wakeup
|
|
1006
|
+
*/
|
|
1007
|
+
wakeup(input, cause = input === null ? "timer" : "input") {
|
|
1008
|
+
if (!this.actor) {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
this.wakeupCause = cause;
|
|
1012
|
+
this.wakeupInput = input;
|
|
1013
|
+
this.wakingUp = true;
|
|
1014
|
+
try {
|
|
1015
|
+
this.processWakeup(input);
|
|
1016
|
+
this.writeChangedOutputs();
|
|
1017
|
+
} finally {
|
|
1018
|
+
this.wakingUp = false;
|
|
1019
|
+
}
|
|
1020
|
+
if (this.pendingBreak) {
|
|
1021
|
+
this.paused = true;
|
|
1022
|
+
this.breakpointHit = this.pendingBreak;
|
|
1023
|
+
this.pendingHistory.push({
|
|
1024
|
+
ts: this.now(),
|
|
1025
|
+
kind: "break",
|
|
1026
|
+
text: this.pendingBreak,
|
|
1027
|
+
active: this.activeNames
|
|
1028
|
+
});
|
|
1029
|
+
this.pendingBreak = null;
|
|
1030
|
+
}
|
|
1031
|
+
this.persist();
|
|
1032
|
+
this.publishLive();
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* @param input Name of the input which caused the wakeup, null for timers and the start
|
|
1036
|
+
*/
|
|
1037
|
+
processWakeup(input) {
|
|
1038
|
+
this.rememberValues();
|
|
1039
|
+
if (input !== null) {
|
|
1040
|
+
for (const [id, counts] of this.inputCounts) {
|
|
1041
|
+
if (this.temporalTransitions.get(id)?.some((t) => t.temporal?.event === input)) {
|
|
1042
|
+
counts.set(input, (counts.get(input) ?? 0) + 1);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
this.updateDurations();
|
|
1047
|
+
if (!this.chart.superStep) {
|
|
1048
|
+
this.step(input);
|
|
1049
|
+
} else {
|
|
1050
|
+
let steps = 0;
|
|
1051
|
+
let stepInput = input;
|
|
1052
|
+
while (steps < import_chart_model.MAX_SUPER_STEPS && this.step(stepInput)) {
|
|
1053
|
+
steps++;
|
|
1054
|
+
stepInput = null;
|
|
1055
|
+
if (this.pendingBreak) {
|
|
1056
|
+
this.queue.unshift({ kind: "continue" });
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (steps >= import_chart_model.MAX_SUPER_STEPS) {
|
|
1061
|
+
this.host.log.warn(
|
|
1062
|
+
`[${this.chart.name}] Stopped after ${import_chart_model.MAX_SUPER_STEPS} transitions in one wakeup, the transitions probably form a loop. The chart continues on the next wakeup.`
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
this.updateDurations();
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Checks the conditions of after(n, unit, condition) of the active states, and starts a timer which wakes up the
|
|
1070
|
+
* chart when a condition has been true for the time
|
|
1071
|
+
*/
|
|
1072
|
+
updateDurations() {
|
|
1073
|
+
for (const id of this.activeIds) {
|
|
1074
|
+
for (const transition of this.temporalTransitions.get(id) || []) {
|
|
1075
|
+
const temporal = transition.temporal;
|
|
1076
|
+
if (temporal.condition === void 0) {
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (!this.run(transition.temporalCondition, `${transition.where}/after condition`, id)) {
|
|
1080
|
+
this.stopDuration(transition.id);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
const since = this.trueSince.get(transition.id) ?? this.now();
|
|
1084
|
+
this.trueSince.set(transition.id, since);
|
|
1085
|
+
if (!this.durationTimers.has(transition.id)) {
|
|
1086
|
+
const timer = { label: transition.where };
|
|
1087
|
+
this.durationTimers.set(transition.id, timer);
|
|
1088
|
+
this.scheduleAt(since + temporal.ms, timer, () => {
|
|
1089
|
+
this.durationTimers.delete(transition.id);
|
|
1090
|
+
this.timerWakeup(null);
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Stores the values of the variables used by the change detection at the start of a wakeup
|
|
1098
|
+
*/
|
|
1099
|
+
rememberValues() {
|
|
1100
|
+
if (!this.changeVariables.size) {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
this.previousValues = this.currentValues;
|
|
1104
|
+
this.currentValues = /* @__PURE__ */ new Map();
|
|
1105
|
+
for (const name of this.changeVariables) {
|
|
1106
|
+
const value = this.sandbox[name];
|
|
1107
|
+
let copy;
|
|
1108
|
+
try {
|
|
1109
|
+
copy = structuredClone(value);
|
|
1110
|
+
} catch {
|
|
1111
|
+
copy = value;
|
|
1112
|
+
}
|
|
1113
|
+
this.currentValues.set(name, copy);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* @param name Name of the variable
|
|
1118
|
+
* @param helper Name of the helper, for the error message
|
|
1119
|
+
* @returns whether the variable has changed between the start of the previous and of the current wakeup
|
|
1120
|
+
* @throws {Error} if the variable is unknown
|
|
1121
|
+
*/
|
|
1122
|
+
getChange(name, helper) {
|
|
1123
|
+
if (!this.changeVariables.has(name) || !this.chart.symbols.some((s) => s.name === name)) {
|
|
1124
|
+
throw new Error(`${helper}() needs a variable of the chart as first argument`);
|
|
1125
|
+
}
|
|
1126
|
+
if (!this.previousValues || !this.currentValues) {
|
|
1127
|
+
return { changed: false };
|
|
1128
|
+
}
|
|
1129
|
+
const previous = this.previousValues.get(name);
|
|
1130
|
+
const current = this.currentValues.get(name);
|
|
1131
|
+
return { changed: !(0, import_node_util.isDeepStrictEqual)(previous, current), previous, current };
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Processes the active states once, see the class description
|
|
1135
|
+
*
|
|
1136
|
+
* @param input Name of the input which caused the wakeup, null if the step has no event
|
|
1137
|
+
* @returns true if a transition was taken
|
|
1138
|
+
*/
|
|
1139
|
+
step(input) {
|
|
1140
|
+
return this.processChildren(null, input);
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Passes the events since the last call and the changed values of the variables to the host
|
|
1144
|
+
*/
|
|
1145
|
+
publishLive() {
|
|
1146
|
+
if (this.pendingHistory.length) {
|
|
1147
|
+
const entries = this.simulated ? this.pendingHistory.map((entry) => ({ ...entry, simulated: true })) : this.pendingHistory;
|
|
1148
|
+
this.pendingHistory = [];
|
|
1149
|
+
this.host.onHistory?.(entries);
|
|
1150
|
+
}
|
|
1151
|
+
if (this.simulated && this.host.onSimulationStatus) {
|
|
1152
|
+
const status = {
|
|
1153
|
+
paused: this.paused,
|
|
1154
|
+
timeOffset: this.timeOffset,
|
|
1155
|
+
queued: this.queue.length,
|
|
1156
|
+
nextTimer: this.getNextTimer(),
|
|
1157
|
+
breakpoint: this.breakpointHit
|
|
1158
|
+
};
|
|
1159
|
+
const json2 = JSON.stringify(status);
|
|
1160
|
+
if (json2 !== this.lastStatus) {
|
|
1161
|
+
this.lastStatus = json2;
|
|
1162
|
+
this.host.onSimulationStatus(status);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (!this.host.onValues) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const values = Object.fromEntries(this.chart.symbols.map((s) => [s.name, this.sandbox[s.name]]));
|
|
1169
|
+
let json;
|
|
1170
|
+
try {
|
|
1171
|
+
json = JSON.stringify(values);
|
|
1172
|
+
} catch {
|
|
1173
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1174
|
+
try {
|
|
1175
|
+
JSON.stringify(value);
|
|
1176
|
+
} catch {
|
|
1177
|
+
values[name] = String(value);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
json = JSON.stringify(values);
|
|
1181
|
+
}
|
|
1182
|
+
if (json !== this.lastValues) {
|
|
1183
|
+
this.lastValues = json;
|
|
1184
|
+
this.host.onValues(values);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Processes an active state and its active children, see the class description
|
|
1189
|
+
*
|
|
1190
|
+
* @param id ID of the state
|
|
1191
|
+
* @param input Name of the input which caused the wakeup, null for timers
|
|
1192
|
+
* @returns true if a transition was taken
|
|
1193
|
+
*/
|
|
1194
|
+
processState(id, input) {
|
|
1195
|
+
if (this.takeFirstValid(this.outerTransitions.get(id), input)) {
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
1198
|
+
const state = this.statesById.get(id);
|
|
1199
|
+
if (state?.during) {
|
|
1200
|
+
this.runAction(state.during, `${state.name}/during`, id);
|
|
1201
|
+
}
|
|
1202
|
+
if (this.takeFirstValid(this.innerTransitions.get(id), input)) {
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
1205
|
+
return this.processChildren(id, input);
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Processes the active child of a state, or all children of a parallel state, see the class description
|
|
1209
|
+
*
|
|
1210
|
+
* @param parent ID of the state, null for the top level
|
|
1211
|
+
* @param input Name of the input which caused the wakeup, null for timers
|
|
1212
|
+
* @returns true if a transition was taken
|
|
1213
|
+
*/
|
|
1214
|
+
processChildren(parent, input) {
|
|
1215
|
+
if (!this.structure.isParallel(parent)) {
|
|
1216
|
+
for (const child of this.getActiveChildren(parent)) {
|
|
1217
|
+
if (this.processState(child, input)) {
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
const entries = parent && this.entryCount.get(parent);
|
|
1224
|
+
let taken = false;
|
|
1225
|
+
for (const child of this.structure.getChildren(parent)) {
|
|
1226
|
+
if (parent && (!this.activeIds.includes(parent) || this.entryCount.get(parent) !== entries)) {
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
if (this.activeIds.includes(child.id) && this.processState(child.id, input)) {
|
|
1230
|
+
taken = true;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return taken;
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* @param parent null for the top level
|
|
1237
|
+
*/
|
|
1238
|
+
getActiveChildren(parent) {
|
|
1239
|
+
const children = new Set(this.structure.getChildren(parent).map((s) => s.id));
|
|
1240
|
+
return this.activeIds.filter((id) => children.has(id));
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Takes the first transition whose event and condition match
|
|
1244
|
+
*
|
|
1245
|
+
* @param transitions The transitions to check, sorted
|
|
1246
|
+
* @param input Name of the input which caused the wakeup, null for timers
|
|
1247
|
+
* @returns true if a transition was taken
|
|
1248
|
+
*/
|
|
1249
|
+
takeFirstValid(transitions, input) {
|
|
1250
|
+
for (const transition of transitions || []) {
|
|
1251
|
+
if (!this.isTriggered(transition, input)) {
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
if (transition.condition && !this.run(transition.condition, `${transition.where}/condition`, transition.source)) {
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const at = this.pendingHistory.length;
|
|
1258
|
+
if (this.simulated && this.breakpoints.has(transition.id)) {
|
|
1259
|
+
this.pendingBreak ??= `${transition.from} \u2192 ${transition.to}`;
|
|
1260
|
+
}
|
|
1261
|
+
this.actor?.send({ type: `T.${transition.id}` });
|
|
1262
|
+
this.pendingHistory.splice(at, 0, {
|
|
1263
|
+
ts: this.now(),
|
|
1264
|
+
kind: "transition",
|
|
1265
|
+
transition: transition.id,
|
|
1266
|
+
from: transition.from,
|
|
1267
|
+
to: transition.to,
|
|
1268
|
+
// in a super step, all transitions of the wakeup were caused by its input
|
|
1269
|
+
trigger: this.wakeupInput,
|
|
1270
|
+
cause: this.wakeupCause,
|
|
1271
|
+
active: this.activeNames
|
|
1272
|
+
});
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* @param transition The transition
|
|
1279
|
+
* @param input Name of the input which caused the wakeup, null for timers
|
|
1280
|
+
* @returns true if its event and its temporal expression allow it in this wakeup (the condition is not checked)
|
|
1281
|
+
*/
|
|
1282
|
+
isTriggered(transition, input) {
|
|
1283
|
+
if (transition.event && transition.event !== input) {
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
const temporal = transition.temporal;
|
|
1287
|
+
if (!temporal) {
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
if (temporal.event !== void 0) {
|
|
1291
|
+
if (temporal.event !== input) {
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
const count = this.inputCounts.get(transition.source)?.get(temporal.event) ?? 0;
|
|
1295
|
+
const n = temporal.count;
|
|
1296
|
+
switch (temporal.operator) {
|
|
1297
|
+
case "after":
|
|
1298
|
+
return count >= n;
|
|
1299
|
+
case "before":
|
|
1300
|
+
return count < n;
|
|
1301
|
+
case "at":
|
|
1302
|
+
return count === n;
|
|
1303
|
+
case "every":
|
|
1304
|
+
return count % n === 0;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const ms = temporal.ms;
|
|
1308
|
+
if (temporal.condition !== void 0) {
|
|
1309
|
+
const since = this.trueSince.get(transition.id);
|
|
1310
|
+
return since !== void 0 && this.now() - since >= ms;
|
|
1311
|
+
}
|
|
1312
|
+
const entered = this.enteredAt.get(transition.source);
|
|
1313
|
+
switch (temporal.operator) {
|
|
1314
|
+
case "after":
|
|
1315
|
+
return this.expired.has(transition.id) || entered !== void 0 && this.now() - entered >= ms;
|
|
1316
|
+
case "before":
|
|
1317
|
+
return entered !== void 0 && this.now() - entered < ms;
|
|
1318
|
+
default:
|
|
1319
|
+
return this.firedTimers.has(transition.id);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* @returns the configuration of the XState machine: the states with their entry and exit actions, the
|
|
1324
|
+
* transitions as guarded events
|
|
1325
|
+
*/
|
|
1326
|
+
buildMachineConfig() {
|
|
1327
|
+
const structure = this.structure;
|
|
1328
|
+
const buildState = (state) => {
|
|
1329
|
+
const node = {
|
|
1330
|
+
id: state.id,
|
|
1331
|
+
entry: [
|
|
1332
|
+
() => this.onEnter(state.id),
|
|
1333
|
+
...state.entry ? [() => this.runAction(state.entry, `${state.name}/entry`, state.id)] : []
|
|
1334
|
+
],
|
|
1335
|
+
exit: [
|
|
1336
|
+
...state.exit ? [() => this.runAction(state.exit, `${state.name}/exit`, state.id)] : [],
|
|
1337
|
+
() => this.onExit(state.id)
|
|
1338
|
+
],
|
|
1339
|
+
on: {}
|
|
1340
|
+
};
|
|
1341
|
+
const outer = [];
|
|
1342
|
+
const inner = [];
|
|
1343
|
+
for (const transition of structure.getTransitions(state.id)) {
|
|
1344
|
+
const built = this.buildTransition(transition, state);
|
|
1345
|
+
if (!built) {
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
node.on[`T.${transition.id}`] = built.config;
|
|
1349
|
+
(built.inner ? inner : outer).push(built.checked);
|
|
1350
|
+
}
|
|
1351
|
+
this.outerTransitions.set(state.id, outer);
|
|
1352
|
+
this.innerTransitions.set(state.id, inner);
|
|
1353
|
+
this.temporalTransitions.set(
|
|
1354
|
+
state.id,
|
|
1355
|
+
[...outer, ...inner].filter((t) => t.temporal !== void 0)
|
|
1356
|
+
);
|
|
1357
|
+
const subStates = structure.getChildren(state.id);
|
|
1358
|
+
if (subStates.length) {
|
|
1359
|
+
if (structure.isParallel(state.id)) {
|
|
1360
|
+
node.type = "parallel";
|
|
1361
|
+
} else {
|
|
1362
|
+
node.initial = structure.getInitialChild(state.id);
|
|
1363
|
+
}
|
|
1364
|
+
node.states = Object.fromEntries(subStates.map((s) => [s.id, buildState(s)]));
|
|
1365
|
+
if (structure.hasHistory(state.id)) {
|
|
1366
|
+
node.states[import_chart_structure.HISTORY_SUFFIX] = { id: `${state.id}${import_chart_structure.HISTORY_SUFFIX}`, type: "history" };
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return node;
|
|
1370
|
+
};
|
|
1371
|
+
const topLevel = structure.getChildren(null);
|
|
1372
|
+
if (!topLevel.length) {
|
|
1373
|
+
throw new Error("The chart has no states");
|
|
1374
|
+
}
|
|
1375
|
+
return {
|
|
1376
|
+
id: `chart_${this.chartId}`,
|
|
1377
|
+
...structure.isParallel(null) ? { type: "parallel" } : { initial: structure.getInitialChild(null) },
|
|
1378
|
+
states: Object.fromEntries(topLevel.map((s) => [s.id, buildState(s)]))
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* @param transition The transition
|
|
1383
|
+
* @param source Its source state
|
|
1384
|
+
*/
|
|
1385
|
+
buildTransition(transition, source) {
|
|
1386
|
+
const target = this.statesById.get(transition.target);
|
|
1387
|
+
const where = `${source.name} -> ${target?.name ?? transition.target}`;
|
|
1388
|
+
if (!target) {
|
|
1389
|
+
this.host.log.warn(`[${this.chart.name}] Transition ${where}: unknown target, ignored`);
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
if (this.structure.getCrossedParallel(transition) !== void 0) {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
let label;
|
|
1396
|
+
try {
|
|
1397
|
+
label = (0, import_chart_model.parseTransitionLabel)(transition.label);
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
this.host.log.error(`[${this.chart.name}] Transition ${where}: ${err.message}, ignored`);
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
const condition = label.condition ? this.compile(`(${this.prepareCode(label.condition, "expression")})`, `${where}/condition`) : null;
|
|
1403
|
+
const temporalCondition = label.temporal?.condition ? this.compile(`(${this.prepareCode(label.temporal.condition, "expression")})`, `${where}/after condition`) : null;
|
|
1404
|
+
const { inner, reenter } = this.structure.getTransitionKind(transition);
|
|
1405
|
+
const config = {
|
|
1406
|
+
target: `#${this.structure.getTargetId(transition)}`,
|
|
1407
|
+
reenter,
|
|
1408
|
+
actions: label.action ? [() => this.runAction(label.action, `${where}/action`, source.id)] : []
|
|
1409
|
+
};
|
|
1410
|
+
return {
|
|
1411
|
+
inner,
|
|
1412
|
+
config,
|
|
1413
|
+
checked: {
|
|
1414
|
+
id: transition.id,
|
|
1415
|
+
source: source.id,
|
|
1416
|
+
where,
|
|
1417
|
+
from: source.name,
|
|
1418
|
+
to: target.name,
|
|
1419
|
+
event: label.event,
|
|
1420
|
+
temporal: label.temporal,
|
|
1421
|
+
temporalCondition,
|
|
1422
|
+
condition
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* @param code The code
|
|
1428
|
+
* @param where Used in error messages
|
|
1429
|
+
*/
|
|
1430
|
+
compile(code, where) {
|
|
1431
|
+
try {
|
|
1432
|
+
return new import_node_vm.default.Script(code, { filename: `${this.chart.name}/${where}` });
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
this.host.log.error(`[${this.chart.name}] ${where}: ${err.message}`);
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* @param script The compiled code
|
|
1440
|
+
* @param where Used in error messages
|
|
1441
|
+
* @param context The state whose condition or action it is, for elapsed() and duration()
|
|
1442
|
+
*/
|
|
1443
|
+
run(script, where, context = this.context) {
|
|
1444
|
+
if (!script) {
|
|
1445
|
+
return void 0;
|
|
1446
|
+
}
|
|
1447
|
+
const previous = this.context;
|
|
1448
|
+
this.context = context;
|
|
1449
|
+
try {
|
|
1450
|
+
return script.runInContext(this.sandbox, { timeout: SCRIPT_TIMEOUT_MS });
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
this.host.log.warn(`[${this.chart.name}] ${where}: ${err.message}`);
|
|
1453
|
+
return void 0;
|
|
1454
|
+
} finally {
|
|
1455
|
+
this.context = previous;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Executes an action and writes all changed outputs
|
|
1460
|
+
*
|
|
1461
|
+
* @param code The action
|
|
1462
|
+
* @param where Used in error messages
|
|
1463
|
+
* @param context The state whose action it is (the source state for transition actions)
|
|
1464
|
+
*/
|
|
1465
|
+
runAction(code, where, context) {
|
|
1466
|
+
this.run(this.compile(`{
|
|
1467
|
+
${this.prepareCode(code, "statements")}
|
|
1468
|
+
}`, where), where, context);
|
|
1469
|
+
this.writeChangedOutputs();
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Writes the outputs whose values were changed by actions to their states, in a simulation only records them
|
|
1473
|
+
*/
|
|
1474
|
+
writeChangedOutputs() {
|
|
1475
|
+
for (const symbol of this.chart.symbols) {
|
|
1476
|
+
if (symbol.scope !== "output" || !symbol.stateId) {
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
const val = this.sandbox[symbol.name];
|
|
1480
|
+
if (val !== this.outputValues.get(symbol.name)) {
|
|
1481
|
+
this.outputValues.set(symbol.name, val);
|
|
1482
|
+
if (this.simulated) {
|
|
1483
|
+
this.recordEffect(`${symbol.name} = ${val === void 0 ? "undefined" : JSON.stringify(val)}`);
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
this.host.writeState(symbol.stateId, val).catch((err) => this.host.log.warn(`[${this.chart.name}] Cannot write ${symbol.stateId}: ${err}`));
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* @param value XState state value
|
|
1492
|
+
*/
|
|
1493
|
+
publishActiveStates(value) {
|
|
1494
|
+
const ids = [];
|
|
1495
|
+
const names = [];
|
|
1496
|
+
const walk = (val, path) => {
|
|
1497
|
+
const keys = typeof val === "string" ? [val] : Object.keys(val || {});
|
|
1498
|
+
for (const key of keys) {
|
|
1499
|
+
const name = [...path, this.statesById.get(key)?.name ?? key];
|
|
1500
|
+
ids.push(key);
|
|
1501
|
+
if (typeof val === "string" || !val[key] || !Object.keys(val[key]).length) {
|
|
1502
|
+
names.push(name.join("."));
|
|
1503
|
+
} else {
|
|
1504
|
+
walk(val[key], name);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
walk(value, []);
|
|
1509
|
+
if (ids.join() !== this.activeIds.join()) {
|
|
1510
|
+
this.activeIds = ids;
|
|
1511
|
+
this.activeNames = names.join(", ");
|
|
1512
|
+
this.host.onActiveChange({ names: this.activeNames, ids });
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1517
|
+
0 && (module.exports = {
|
|
1518
|
+
ChartRuntime,
|
|
1519
|
+
getChartHash
|
|
1520
|
+
});
|
|
1521
|
+
//# sourceMappingURL=chart-runtime.js.map
|