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.
Files changed (50) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +59 -438
  3. package/admin/build/tab.js +133 -130
  4. package/admin/statechart.png +0 -0
  5. package/admin/{style.css → tab.css} +0 -2
  6. package/admin/tab_m.html +1 -1
  7. package/build/lib/chart-analysis.js +367 -0
  8. package/build/lib/chart-analysis.js.map +7 -0
  9. package/build/lib/chart-codegen.js +405 -0
  10. package/build/lib/chart-codegen.js.map +7 -0
  11. package/build/lib/chart-helpers.js +102 -0
  12. package/build/lib/chart-helpers.js.map +7 -0
  13. package/build/lib/chart-model.js +477 -0
  14. package/build/lib/chart-model.js.map +7 -0
  15. package/build/lib/chart-runtime.js +1521 -0
  16. package/build/lib/chart-runtime.js.map +7 -0
  17. package/build/lib/chart-structure.js +172 -0
  18. package/build/lib/chart-structure.js.map +7 -0
  19. package/build/lib/chart-validation.js +372 -0
  20. package/build/lib/chart-validation.js.map +7 -0
  21. package/build/main.js +638 -0
  22. package/build/main.js.map +7 -0
  23. package/io-package.json +31 -34
  24. package/package.json +21 -17
  25. package/admin/i18n/de.json +0 -4
  26. package/admin/i18n/en.json +0 -4
  27. package/admin/i18n/es.json +0 -4
  28. package/admin/i18n/fr.json +0 -4
  29. package/admin/i18n/it.json +0 -4
  30. package/admin/i18n/nl.json +0 -4
  31. package/admin/i18n/pl.json +0 -4
  32. package/admin/i18n/pt.json +0 -4
  33. package/admin/i18n/ru.json +0 -4
  34. package/admin/i18n/uk.json +0 -4
  35. package/admin/i18n/zh-cn.json +0 -4
  36. package/admin/jsonConfig.json +0 -19
  37. package/admin/statechart.svg +0 -38
  38. package/lib/adapter-config.d.ts +0 -19
  39. package/lib/chart-codegen.js +0 -436
  40. package/lib/chart-codegen.test.js +0 -132
  41. package/lib/chart-helpers.js +0 -74
  42. package/lib/chart-helpers.test.js +0 -38
  43. package/lib/chart-model.js +0 -577
  44. package/lib/chart-model.test.js +0 -189
  45. package/lib/chart-runtime.js +0 -797
  46. package/lib/chart-runtime.test.js +0 -699
  47. package/lib/chart-structure.js +0 -106
  48. package/lib/chart-validation.js +0 -328
  49. package/lib/chart-validation.test.js +0 -149
  50. 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