iobroker.statechart 0.2.0 → 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 +42 -530
- package/admin/build/tab.js +118 -117
- 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 +82 -15
- package/build/lib/chart-codegen.js.map +3 -3
- package/build/lib/chart-helpers.js +12 -0
- package/build/lib/chart-helpers.js.map +2 -2
- package/build/lib/chart-model.js +138 -14
- package/build/lib/chart-model.js.map +2 -2
- package/build/lib/chart-runtime.js +732 -87
- package/build/lib/chart-runtime.js.map +3 -3
- package/build/lib/chart-structure.js +73 -2
- package/build/lib/chart-structure.js.map +2 -2
- package/build/lib/chart-validation.js +86 -17
- package/build/lib/chart-validation.js.map +3 -3
- package/build/main.js +330 -34
- package/build/main.js.map +2 -2
- package/io-package.json +18 -34
- package/package.json +2 -1
- 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
|
@@ -43,19 +43,22 @@ var import_chart_structure = require("./chart-structure");
|
|
|
43
43
|
var import_chart_validation = require("./chart-validation");
|
|
44
44
|
const SCRIPT_TIMEOUT_MS = 1e3;
|
|
45
45
|
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
46
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
47
|
+
const MAX_ADVANCE_TIMERS = 1e4;
|
|
46
48
|
const PERSISTED_VERSION = 1;
|
|
47
49
|
function getChartHash(chart) {
|
|
48
50
|
const logic = {
|
|
49
51
|
initial: chart.initial,
|
|
50
52
|
symbols: chart.symbols,
|
|
51
|
-
// comments and
|
|
52
|
-
states: chart.states.map(({ x: _x, y: _y, width: _w, height: _h, comment: _c, ...state }) => state),
|
|
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),
|
|
53
55
|
transitions: chart.transitions.map(
|
|
54
56
|
({
|
|
55
57
|
sourceHandle: _s,
|
|
56
58
|
targetHandle: _t,
|
|
57
59
|
sourceAnchor: _sa,
|
|
58
60
|
targetAnchor: _ta,
|
|
61
|
+
labelOffset: _lo,
|
|
59
62
|
comment: _c,
|
|
60
63
|
...transition
|
|
61
64
|
}) => transition
|
|
@@ -64,14 +67,25 @@ function getChartHash(chart) {
|
|
|
64
67
|
};
|
|
65
68
|
return import_node_crypto.default.createHash("sha1").update(JSON.stringify(logic)).digest("hex");
|
|
66
69
|
}
|
|
70
|
+
function toSnapshot(state) {
|
|
71
|
+
return state ? { val: state.val, ack: !!state.ack, ts: state.ts, lc: state.lc } : null;
|
|
72
|
+
}
|
|
67
73
|
class ChartRuntime {
|
|
68
|
-
|
|
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) {
|
|
69
82
|
this.chartId = chartId;
|
|
70
83
|
this.chart = chart;
|
|
71
84
|
this.host = host;
|
|
85
|
+
this.simulated = simulated;
|
|
72
86
|
this.structure = new import_chart_structure.ChartStructure(chart);
|
|
73
87
|
this.statesById = this.structure.statesById;
|
|
74
|
-
this.sandbox = import_node_vm.default.createContext(this.createHelpers());
|
|
88
|
+
this.sandbox = import_node_vm.default.createContext({ ...this.createHelpers(), ...import_chart_model.TIME_UNITS });
|
|
75
89
|
this.hash = getChartHash(chart);
|
|
76
90
|
this.prepareAllCode();
|
|
77
91
|
}
|
|
@@ -93,10 +107,26 @@ class ChartRuntime {
|
|
|
93
107
|
activeIds = [];
|
|
94
108
|
/** time each active state was entered */
|
|
95
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();
|
|
96
112
|
/** timers of the temporal transitions by state */
|
|
97
113
|
timers = /* @__PURE__ */ new Map();
|
|
98
|
-
/**
|
|
114
|
+
/** after(n, unit) transitions whose time has come */
|
|
99
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;
|
|
100
130
|
/** timers of inputs with debounce, by variable name */
|
|
101
131
|
debounceTimers = /* @__PURE__ */ new Map();
|
|
102
132
|
/** JSON of the last persisted data, to persist only changes */
|
|
@@ -115,14 +145,36 @@ class ChartRuntime {
|
|
|
115
145
|
preparedCode = /* @__PURE__ */ new Map();
|
|
116
146
|
/** the variables used by hasChanged() etc. */
|
|
117
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();
|
|
118
152
|
/** their values at the start of the previous wakeup, null before the second wakeup */
|
|
119
153
|
previousValues = null;
|
|
120
154
|
/** their values at the start of the current wakeup, null before the first one */
|
|
121
155
|
currentValues = null;
|
|
122
156
|
hash;
|
|
123
157
|
actor = null;
|
|
124
|
-
/**
|
|
125
|
-
|
|
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;
|
|
126
178
|
/**
|
|
127
179
|
* Prepares all code of the chart for the change detection, so its variables are known from the first wakeup on
|
|
128
180
|
*/
|
|
@@ -135,6 +187,7 @@ class ChartRuntime {
|
|
|
135
187
|
for (const transition of this.chart.transitions) {
|
|
136
188
|
try {
|
|
137
189
|
const label = (0, import_chart_model.parseTransitionLabel)(transition.label);
|
|
190
|
+
this.prepareCode(label.temporal?.condition, "expression");
|
|
138
191
|
this.prepareCode(label.condition, "expression");
|
|
139
192
|
this.prepareCode(label.action, "statements");
|
|
140
193
|
} catch {
|
|
@@ -147,7 +200,8 @@ class ChartRuntime {
|
|
|
147
200
|
/**
|
|
148
201
|
* @param code A condition, action or function block
|
|
149
202
|
* @param kind Expression or statements
|
|
150
|
-
* @returns the code with the names of the variables in hasChanged() etc
|
|
203
|
+
* @returns the code with the names of the variables in hasChanged() etc. and the IDs of the duration() calls,
|
|
204
|
+
* see prepareHelperCalls()
|
|
151
205
|
*/
|
|
152
206
|
prepareCode(code, kind) {
|
|
153
207
|
if (!code) {
|
|
@@ -156,11 +210,14 @@ class ChartRuntime {
|
|
|
156
210
|
const key = `${kind}:${code}`;
|
|
157
211
|
let prepared = this.preparedCode.get(key);
|
|
158
212
|
if (prepared === void 0) {
|
|
159
|
-
const quoted = (0, import_chart_validation.
|
|
213
|
+
const quoted = (0, import_chart_validation.prepareHelperCalls)(code, kind, () => `d${++this.durationCallCount}`);
|
|
160
214
|
prepared = quoted.code;
|
|
161
215
|
for (const name of quoted.names) {
|
|
162
216
|
this.changeVariables.add(name);
|
|
163
217
|
}
|
|
218
|
+
for (const id of quoted.stateIds) {
|
|
219
|
+
this.readStateIds.add(id);
|
|
220
|
+
}
|
|
164
221
|
this.preparedCode.set(key, prepared);
|
|
165
222
|
}
|
|
166
223
|
return prepared;
|
|
@@ -171,12 +228,34 @@ class ChartRuntime {
|
|
|
171
228
|
get inputStateIds() {
|
|
172
229
|
return this.chart.symbols.filter((s) => s.scope === "input" && s.stateId).map((s) => s.stateId);
|
|
173
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
|
+
}
|
|
174
238
|
/**
|
|
175
239
|
* @param options Start options
|
|
176
240
|
* @param options.restore Continue with the persisted state, if the chart was not changed since
|
|
177
241
|
*/
|
|
178
242
|
async start(options = {}) {
|
|
179
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
|
+
}
|
|
180
259
|
for (const symbol of this.chart.symbols) {
|
|
181
260
|
if (symbol.scope === "local") {
|
|
182
261
|
this.sandbox[symbol.name] = structuredClone(symbol.initial);
|
|
@@ -193,6 +272,7 @@ class ChartRuntime {
|
|
|
193
272
|
this.run(this.compile(this.prepareCode(fn.code, "statements"), where), where);
|
|
194
273
|
}
|
|
195
274
|
this.writeChangedOutputs();
|
|
275
|
+
this.wakingUp = true;
|
|
196
276
|
const machine = (0, import_xstate.createMachine)(this.buildMachineConfig());
|
|
197
277
|
const persisted = options.restore && this.chart.restoreState !== false ? await this.loadPersisted() : null;
|
|
198
278
|
let restored = false;
|
|
@@ -209,7 +289,8 @@ class ChartRuntime {
|
|
|
209
289
|
} else {
|
|
210
290
|
this.startActor(machine);
|
|
211
291
|
}
|
|
212
|
-
this.pendingHistory.
|
|
292
|
+
this.pendingHistory.unshift({ ts: this.now(), kind: "start", restored, active: this.activeNames });
|
|
293
|
+
this.wakingUp = false;
|
|
213
294
|
this.wakeup(null, "start");
|
|
214
295
|
}
|
|
215
296
|
/**
|
|
@@ -243,9 +324,19 @@ class ChartRuntime {
|
|
|
243
324
|
this.publishActiveStates(this.actor.getSnapshot().value);
|
|
244
325
|
for (const id of this.activeIds) {
|
|
245
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
|
+
}
|
|
246
334
|
this.startTimers(id);
|
|
247
335
|
}
|
|
248
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Publishes the active states after each transition, and logs errors of the state machine
|
|
339
|
+
*/
|
|
249
340
|
subscribe() {
|
|
250
341
|
this.actor?.subscribe({
|
|
251
342
|
next: (snapshot) => this.publishActiveStates(snapshot.value),
|
|
@@ -270,13 +361,19 @@ class ChartRuntime {
|
|
|
270
361
|
this.host.log.info(`[${this.chart.name}] The chart was changed, starting in the initial state`);
|
|
271
362
|
return null;
|
|
272
363
|
}
|
|
273
|
-
return {
|
|
364
|
+
return {
|
|
365
|
+
...persisted,
|
|
366
|
+
entered: persisted.entered || {},
|
|
367
|
+
locals: persisted.locals || {},
|
|
368
|
+
counts: persisted.counts || {},
|
|
369
|
+
trueSince: persisted.trueSince || {}
|
|
370
|
+
};
|
|
274
371
|
}
|
|
275
372
|
/**
|
|
276
373
|
* Stores the current state of the chart, if it changed since the last time
|
|
277
374
|
*/
|
|
278
375
|
persist() {
|
|
279
|
-
if (!this.actor || !this.host.persist) {
|
|
376
|
+
if (!this.actor || !this.host.persist || this.simulated) {
|
|
280
377
|
return;
|
|
281
378
|
}
|
|
282
379
|
const data = {
|
|
@@ -286,7 +383,11 @@ class ChartRuntime {
|
|
|
286
383
|
entered: Object.fromEntries(this.enteredAt),
|
|
287
384
|
locals: Object.fromEntries(
|
|
288
385
|
this.chart.symbols.filter((s) => s.scope === "local").map((s) => [s.name, this.sandbox[s.name]])
|
|
289
|
-
)
|
|
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)
|
|
290
391
|
};
|
|
291
392
|
let json;
|
|
292
393
|
try {
|
|
@@ -306,7 +407,12 @@ class ChartRuntime {
|
|
|
306
407
|
* @param id ID of the state
|
|
307
408
|
*/
|
|
308
409
|
onEnter(id) {
|
|
410
|
+
if (this.simulated && this.breakpoints.has(id)) {
|
|
411
|
+
this.pendingBreak ??= this.statesById.get(id)?.name ?? id;
|
|
412
|
+
}
|
|
309
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());
|
|
310
416
|
this.startTimers(id);
|
|
311
417
|
}
|
|
312
418
|
/**
|
|
@@ -317,13 +423,109 @@ class ChartRuntime {
|
|
|
317
423
|
onExit(id) {
|
|
318
424
|
this.enteredAt.delete(id);
|
|
319
425
|
for (const timer of this.timers.get(id) || []) {
|
|
320
|
-
|
|
426
|
+
this.cancelTimer(timer);
|
|
321
427
|
}
|
|
322
428
|
this.timers.delete(id);
|
|
429
|
+
this.inputCounts.delete(id);
|
|
323
430
|
for (const transition of this.temporalTransitions.get(id) || []) {
|
|
324
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);
|
|
325
480
|
}
|
|
326
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
|
+
}
|
|
327
529
|
/**
|
|
328
530
|
* Starts a timer for every temporal transition of a state, which wakes up the chart when its time has come
|
|
329
531
|
*
|
|
@@ -332,27 +534,95 @@ class ChartRuntime {
|
|
|
332
534
|
startTimers(id) {
|
|
333
535
|
const entered = this.enteredAt.get(id) ?? this.now();
|
|
334
536
|
const timers = [];
|
|
537
|
+
this.timers.set(id, timers);
|
|
335
538
|
for (const transition of this.temporalTransitions.get(id) || []) {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
this.wakeup(null);
|
|
348
|
-
},
|
|
349
|
-
Math.min(Math.max(remaining, 0), MAX_TIMEOUT_MS)
|
|
350
|
-
);
|
|
351
|
-
};
|
|
352
|
-
wait();
|
|
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 };
|
|
353
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));
|
|
354
567
|
}
|
|
355
|
-
|
|
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;
|
|
356
626
|
}
|
|
357
627
|
/**
|
|
358
628
|
* Logs the problems found by the validation, the chart is started anyway
|
|
@@ -361,7 +631,9 @@ class ChartRuntime {
|
|
|
361
631
|
for (const { kind, id, field, diagnostic } of (0, import_chart_validation.validateChart)(this.chart)) {
|
|
362
632
|
let where;
|
|
363
633
|
if (kind === "state") {
|
|
364
|
-
where = `${this.statesById.get(id)?.name ?? id}
|
|
634
|
+
where = `${this.statesById.get(id)?.name ?? id}${field ? `/${field}` : ""}`;
|
|
635
|
+
} else if (kind === "symbol") {
|
|
636
|
+
where = `Variable ${id}`;
|
|
365
637
|
} else if (kind === "function") {
|
|
366
638
|
const fn = this.chart.functions?.find((f) => f.id === id);
|
|
367
639
|
where = `Function block ${(0, import_chart_validation.getDeclarations)(fn?.code).map((d) => d.name).join(", ")}`;
|
|
@@ -371,27 +643,27 @@ class ChartRuntime {
|
|
|
371
643
|
const target = this.statesById.get(transition?.target ?? "")?.name;
|
|
372
644
|
where = `Transition ${source} -> ${target}`;
|
|
373
645
|
}
|
|
374
|
-
const log =
|
|
646
|
+
const log = { error: this.host.log.error, warning: this.host.log.warn, info: this.host.log.debug }[diagnostic.severity];
|
|
375
647
|
log(`[${this.chart.name}] ${where}: ${diagnostic.message}`);
|
|
376
648
|
}
|
|
377
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Stops the state machine and all timers, delayed writes are discarded
|
|
652
|
+
*/
|
|
378
653
|
stop() {
|
|
379
654
|
this.actor?.stop();
|
|
380
655
|
this.actor = null;
|
|
381
|
-
for (const
|
|
382
|
-
|
|
383
|
-
clearTimeout(timer.handle);
|
|
384
|
-
}
|
|
656
|
+
for (const timer of this.scheduled) {
|
|
657
|
+
this.stopTimeout(timer.handle);
|
|
385
658
|
}
|
|
659
|
+
this.scheduled.clear();
|
|
386
660
|
this.timers.clear();
|
|
387
661
|
this.expired.clear();
|
|
662
|
+
this.durationTimers.clear();
|
|
388
663
|
for (const timer of this.debounceTimers.values()) {
|
|
389
|
-
|
|
664
|
+
this.stopTimeout(timer);
|
|
390
665
|
}
|
|
391
666
|
this.debounceTimers.clear();
|
|
392
|
-
for (const timer of this.delayedWrites) {
|
|
393
|
-
clearTimeout(timer);
|
|
394
|
-
}
|
|
395
667
|
this.delayedWrites.clear();
|
|
396
668
|
}
|
|
397
669
|
/**
|
|
@@ -400,25 +672,36 @@ class ChartRuntime {
|
|
|
400
672
|
createHelpers() {
|
|
401
673
|
const prefix = `[${this.chart.name}]`;
|
|
402
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(", ")})`;
|
|
403
676
|
return {
|
|
404
677
|
log: (message, level = "info") => {
|
|
405
678
|
const method = ["debug", "info", "warn", "error"].includes(level) ? level : "info";
|
|
406
679
|
this.host.log[method](`${prefix} ${message}`);
|
|
407
680
|
},
|
|
408
681
|
setState: (id, value, ack = false) => {
|
|
682
|
+
if (this.simulated) {
|
|
683
|
+
this.recordEffect(describe("setState", [id, value, ...ack ? [true] : []]));
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
409
686
|
void write(id, value, ack);
|
|
410
687
|
},
|
|
411
688
|
setStateDelayed: (id, value, delayMs, ack = false) => {
|
|
412
|
-
const timer =
|
|
413
|
-
() => {
|
|
414
|
-
this.delayedWrites.delete(timer);
|
|
415
|
-
void write(id, value, ack);
|
|
416
|
-
},
|
|
417
|
-
Math.min(Math.max(Number(delayMs) || 0, 0), MAX_TIMEOUT_MS)
|
|
418
|
-
);
|
|
689
|
+
const timer = { label: describe("setStateDelayed", [id, value]) };
|
|
419
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
|
+
});
|
|
420
699
|
},
|
|
421
700
|
sendTo: (instance, command, message) => {
|
|
701
|
+
if (this.simulated) {
|
|
702
|
+
this.recordEffect(describe("sendTo", [instance, command, message]));
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
422
705
|
if (!this.host.sendTo) {
|
|
423
706
|
throw new Error("sendTo() is not available");
|
|
424
707
|
}
|
|
@@ -444,6 +727,33 @@ class ChartRuntime {
|
|
|
444
727
|
}
|
|
445
728
|
return new Date(time.getTime() + (Number(offsetMinutes) || 0) * 6e4);
|
|
446
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
|
+
},
|
|
447
757
|
hasChanged: (name) => this.getChange(name, "hasChanged").changed,
|
|
448
758
|
hasChangedFrom: (name, value) => {
|
|
449
759
|
const { changed, previous } = this.getChange(name, "hasChangedFrom");
|
|
@@ -467,6 +777,163 @@ class ChartRuntime {
|
|
|
467
777
|
}
|
|
468
778
|
return import_suncalc.default.getTimes(date, location.latitude, location.longitude);
|
|
469
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
|
+
}
|
|
470
937
|
/**
|
|
471
938
|
* Must be called when an input state is updated
|
|
472
939
|
*
|
|
@@ -488,10 +955,10 @@ class ChartRuntime {
|
|
|
488
955
|
}
|
|
489
956
|
const debounceMs = symbol.debounceMs || 0;
|
|
490
957
|
if (debounceMs > 0) {
|
|
491
|
-
|
|
958
|
+
this.stopTimeout(this.debounceTimers.get(symbol.name));
|
|
492
959
|
this.debounceTimers.set(
|
|
493
960
|
symbol.name,
|
|
494
|
-
|
|
961
|
+
this.startTimeout(() => {
|
|
495
962
|
this.debounceTimers.delete(symbol.name);
|
|
496
963
|
this.applyInput(symbol, val);
|
|
497
964
|
}, debounceMs)
|
|
@@ -501,6 +968,25 @@ class ChartRuntime {
|
|
|
501
968
|
}
|
|
502
969
|
}
|
|
503
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
|
+
}
|
|
504
990
|
/**
|
|
505
991
|
* Sets the variable of an input and wakes up the chart
|
|
506
992
|
*
|
|
@@ -524,7 +1010,40 @@ class ChartRuntime {
|
|
|
524
1010
|
}
|
|
525
1011
|
this.wakeupCause = cause;
|
|
526
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) {
|
|
527
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();
|
|
528
1047
|
if (!this.chart.superStep) {
|
|
529
1048
|
this.step(input);
|
|
530
1049
|
} else {
|
|
@@ -533,6 +1052,10 @@ class ChartRuntime {
|
|
|
533
1052
|
while (steps < import_chart_model.MAX_SUPER_STEPS && this.step(stepInput)) {
|
|
534
1053
|
steps++;
|
|
535
1054
|
stepInput = null;
|
|
1055
|
+
if (this.pendingBreak) {
|
|
1056
|
+
this.queue.unshift({ kind: "continue" });
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
536
1059
|
}
|
|
537
1060
|
if (steps >= import_chart_model.MAX_SUPER_STEPS) {
|
|
538
1061
|
this.host.log.warn(
|
|
@@ -540,9 +1063,35 @@ class ChartRuntime {
|
|
|
540
1063
|
);
|
|
541
1064
|
}
|
|
542
1065
|
}
|
|
543
|
-
this.
|
|
544
|
-
|
|
545
|
-
|
|
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
|
+
}
|
|
546
1095
|
}
|
|
547
1096
|
/**
|
|
548
1097
|
* Stores the values of the variables used by the change detection at the start of a wakeup
|
|
@@ -588,22 +1137,31 @@ class ChartRuntime {
|
|
|
588
1137
|
* @returns true if a transition was taken
|
|
589
1138
|
*/
|
|
590
1139
|
step(input) {
|
|
591
|
-
|
|
592
|
-
if (this.processState(id, input)) {
|
|
593
|
-
return true;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
return false;
|
|
1140
|
+
return this.processChildren(null, input);
|
|
597
1141
|
}
|
|
598
1142
|
/**
|
|
599
1143
|
* Passes the events since the last call and the changed values of the variables to the host
|
|
600
1144
|
*/
|
|
601
1145
|
publishLive() {
|
|
602
1146
|
if (this.pendingHistory.length) {
|
|
603
|
-
const entries = this.pendingHistory;
|
|
1147
|
+
const entries = this.simulated ? this.pendingHistory.map((entry) => ({ ...entry, simulated: true })) : this.pendingHistory;
|
|
604
1148
|
this.pendingHistory = [];
|
|
605
1149
|
this.host.onHistory?.(entries);
|
|
606
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
|
+
}
|
|
607
1165
|
if (!this.host.onValues) {
|
|
608
1166
|
return;
|
|
609
1167
|
}
|
|
@@ -639,17 +1197,40 @@ class ChartRuntime {
|
|
|
639
1197
|
}
|
|
640
1198
|
const state = this.statesById.get(id);
|
|
641
1199
|
if (state?.during) {
|
|
642
|
-
this.runAction(state.during, `${state.name}/during
|
|
1200
|
+
this.runAction(state.during, `${state.name}/during`, id);
|
|
643
1201
|
}
|
|
644
1202
|
if (this.takeFirstValid(this.innerTransitions.get(id), input)) {
|
|
645
1203
|
return true;
|
|
646
1204
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
+
}
|
|
650
1220
|
}
|
|
1221
|
+
return false;
|
|
651
1222
|
}
|
|
652
|
-
|
|
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;
|
|
653
1234
|
}
|
|
654
1235
|
/**
|
|
655
1236
|
* @param parent null for the top level
|
|
@@ -667,17 +1248,18 @@ class ChartRuntime {
|
|
|
667
1248
|
*/
|
|
668
1249
|
takeFirstValid(transitions, input) {
|
|
669
1250
|
for (const transition of transitions || []) {
|
|
670
|
-
if (
|
|
1251
|
+
if (!this.isTriggered(transition, input)) {
|
|
671
1252
|
continue;
|
|
672
1253
|
}
|
|
673
|
-
if (transition.
|
|
1254
|
+
if (transition.condition && !this.run(transition.condition, `${transition.where}/condition`, transition.source)) {
|
|
674
1255
|
continue;
|
|
675
1256
|
}
|
|
676
|
-
|
|
677
|
-
|
|
1257
|
+
const at = this.pendingHistory.length;
|
|
1258
|
+
if (this.simulated && this.breakpoints.has(transition.id)) {
|
|
1259
|
+
this.pendingBreak ??= `${transition.from} \u2192 ${transition.to}`;
|
|
678
1260
|
}
|
|
679
1261
|
this.actor?.send({ type: `T.${transition.id}` });
|
|
680
|
-
this.pendingHistory.
|
|
1262
|
+
this.pendingHistory.splice(at, 0, {
|
|
681
1263
|
ts: this.now(),
|
|
682
1264
|
kind: "transition",
|
|
683
1265
|
transition: transition.id,
|
|
@@ -693,16 +1275,54 @@ class ChartRuntime {
|
|
|
693
1275
|
return false;
|
|
694
1276
|
}
|
|
695
1277
|
/**
|
|
696
|
-
* @param transition
|
|
697
|
-
* @
|
|
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)
|
|
698
1281
|
*/
|
|
699
|
-
|
|
700
|
-
if (
|
|
1282
|
+
isTriggered(transition, input) {
|
|
1283
|
+
if (transition.event && transition.event !== input) {
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
const temporal = transition.temporal;
|
|
1287
|
+
if (!temporal) {
|
|
701
1288
|
return true;
|
|
702
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
|
+
}
|
|
703
1312
|
const entered = this.enteredAt.get(transition.source);
|
|
704
|
-
|
|
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
|
+
}
|
|
705
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
|
+
*/
|
|
706
1326
|
buildMachineConfig() {
|
|
707
1327
|
const structure = this.structure;
|
|
708
1328
|
const buildState = (state) => {
|
|
@@ -710,10 +1330,10 @@ class ChartRuntime {
|
|
|
710
1330
|
id: state.id,
|
|
711
1331
|
entry: [
|
|
712
1332
|
() => this.onEnter(state.id),
|
|
713
|
-
...state.entry ? [() => this.runAction(state.entry, `${state.name}/entry
|
|
1333
|
+
...state.entry ? [() => this.runAction(state.entry, `${state.name}/entry`, state.id)] : []
|
|
714
1334
|
],
|
|
715
1335
|
exit: [
|
|
716
|
-
...state.exit ? [() => this.runAction(state.exit, `${state.name}/exit
|
|
1336
|
+
...state.exit ? [() => this.runAction(state.exit, `${state.name}/exit`, state.id)] : [],
|
|
717
1337
|
() => this.onExit(state.id)
|
|
718
1338
|
],
|
|
719
1339
|
on: {}
|
|
@@ -732,12 +1352,19 @@ class ChartRuntime {
|
|
|
732
1352
|
this.innerTransitions.set(state.id, inner);
|
|
733
1353
|
this.temporalTransitions.set(
|
|
734
1354
|
state.id,
|
|
735
|
-
[...outer, ...inner].filter((t) => t.
|
|
1355
|
+
[...outer, ...inner].filter((t) => t.temporal !== void 0)
|
|
736
1356
|
);
|
|
737
1357
|
const subStates = structure.getChildren(state.id);
|
|
738
1358
|
if (subStates.length) {
|
|
739
|
-
|
|
1359
|
+
if (structure.isParallel(state.id)) {
|
|
1360
|
+
node.type = "parallel";
|
|
1361
|
+
} else {
|
|
1362
|
+
node.initial = structure.getInitialChild(state.id);
|
|
1363
|
+
}
|
|
740
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
|
+
}
|
|
741
1368
|
}
|
|
742
1369
|
return node;
|
|
743
1370
|
};
|
|
@@ -747,7 +1374,7 @@ class ChartRuntime {
|
|
|
747
1374
|
}
|
|
748
1375
|
return {
|
|
749
1376
|
id: `chart_${this.chartId}`,
|
|
750
|
-
initial: structure.getInitialChild(null),
|
|
1377
|
+
...structure.isParallel(null) ? { type: "parallel" } : { initial: structure.getInitialChild(null) },
|
|
751
1378
|
states: Object.fromEntries(topLevel.map((s) => [s.id, buildState(s)]))
|
|
752
1379
|
};
|
|
753
1380
|
}
|
|
@@ -762,6 +1389,9 @@ class ChartRuntime {
|
|
|
762
1389
|
this.host.log.warn(`[${this.chart.name}] Transition ${where}: unknown target, ignored`);
|
|
763
1390
|
return null;
|
|
764
1391
|
}
|
|
1392
|
+
if (this.structure.getCrossedParallel(transition) !== void 0) {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
765
1395
|
let label;
|
|
766
1396
|
try {
|
|
767
1397
|
label = (0, import_chart_model.parseTransitionLabel)(transition.label);
|
|
@@ -770,11 +1400,12 @@ class ChartRuntime {
|
|
|
770
1400
|
return null;
|
|
771
1401
|
}
|
|
772
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;
|
|
773
1404
|
const { inner, reenter } = this.structure.getTransitionKind(transition);
|
|
774
1405
|
const config = {
|
|
775
|
-
target: `#${
|
|
1406
|
+
target: `#${this.structure.getTargetId(transition)}`,
|
|
776
1407
|
reenter,
|
|
777
|
-
actions: label.action ? [() => this.runAction(label.action, `${where}/action
|
|
1408
|
+
actions: label.action ? [() => this.runAction(label.action, `${where}/action`, source.id)] : []
|
|
778
1409
|
};
|
|
779
1410
|
return {
|
|
780
1411
|
inner,
|
|
@@ -786,7 +1417,8 @@ class ChartRuntime {
|
|
|
786
1417
|
from: source.name,
|
|
787
1418
|
to: target.name,
|
|
788
1419
|
event: label.event,
|
|
789
|
-
|
|
1420
|
+
temporal: label.temporal,
|
|
1421
|
+
temporalCondition,
|
|
790
1422
|
condition
|
|
791
1423
|
}
|
|
792
1424
|
};
|
|
@@ -806,16 +1438,21 @@ class ChartRuntime {
|
|
|
806
1438
|
/**
|
|
807
1439
|
* @param script The compiled code
|
|
808
1440
|
* @param where Used in error messages
|
|
1441
|
+
* @param context The state whose condition or action it is, for elapsed() and duration()
|
|
809
1442
|
*/
|
|
810
|
-
run(script, where) {
|
|
1443
|
+
run(script, where, context = this.context) {
|
|
811
1444
|
if (!script) {
|
|
812
1445
|
return void 0;
|
|
813
1446
|
}
|
|
1447
|
+
const previous = this.context;
|
|
1448
|
+
this.context = context;
|
|
814
1449
|
try {
|
|
815
1450
|
return script.runInContext(this.sandbox, { timeout: SCRIPT_TIMEOUT_MS });
|
|
816
1451
|
} catch (err) {
|
|
817
1452
|
this.host.log.warn(`[${this.chart.name}] ${where}: ${err.message}`);
|
|
818
1453
|
return void 0;
|
|
1454
|
+
} finally {
|
|
1455
|
+
this.context = previous;
|
|
819
1456
|
}
|
|
820
1457
|
}
|
|
821
1458
|
/**
|
|
@@ -823,13 +1460,17 @@ class ChartRuntime {
|
|
|
823
1460
|
*
|
|
824
1461
|
* @param code The action
|
|
825
1462
|
* @param where Used in error messages
|
|
1463
|
+
* @param context The state whose action it is (the source state for transition actions)
|
|
826
1464
|
*/
|
|
827
|
-
runAction(code, where) {
|
|
1465
|
+
runAction(code, where, context) {
|
|
828
1466
|
this.run(this.compile(`{
|
|
829
1467
|
${this.prepareCode(code, "statements")}
|
|
830
|
-
}`, where), where);
|
|
1468
|
+
}`, where), where, context);
|
|
831
1469
|
this.writeChangedOutputs();
|
|
832
1470
|
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Writes the outputs whose values were changed by actions to their states, in a simulation only records them
|
|
1473
|
+
*/
|
|
833
1474
|
writeChangedOutputs() {
|
|
834
1475
|
for (const symbol of this.chart.symbols) {
|
|
835
1476
|
if (symbol.scope !== "output" || !symbol.stateId) {
|
|
@@ -838,6 +1479,10 @@ ${this.prepareCode(code, "statements")}
|
|
|
838
1479
|
const val = this.sandbox[symbol.name];
|
|
839
1480
|
if (val !== this.outputValues.get(symbol.name)) {
|
|
840
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
|
+
}
|
|
841
1486
|
this.host.writeState(symbol.stateId, val).catch((err) => this.host.log.warn(`[${this.chart.name}] Cannot write ${symbol.stateId}: ${err}`));
|
|
842
1487
|
}
|
|
843
1488
|
}
|