iobroker.statechart 0.1.1 → 0.2.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.
@@ -0,0 +1,876 @@
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 PERSISTED_VERSION = 1;
47
+ function getChartHash(chart) {
48
+ const logic = {
49
+ initial: chart.initial,
50
+ symbols: chart.symbols,
51
+ // comments and notes are only documentation
52
+ states: chart.states.map(({ x: _x, y: _y, width: _w, height: _h, comment: _c, ...state }) => state),
53
+ transitions: chart.transitions.map(
54
+ ({
55
+ sourceHandle: _s,
56
+ targetHandle: _t,
57
+ sourceAnchor: _sa,
58
+ targetAnchor: _ta,
59
+ comment: _c,
60
+ ...transition
61
+ }) => transition
62
+ ),
63
+ functions: (chart.functions || []).map((fn) => fn.code)
64
+ };
65
+ return import_node_crypto.default.createHash("sha1").update(JSON.stringify(logic)).digest("hex");
66
+ }
67
+ class ChartRuntime {
68
+ constructor(chartId, chart, host) {
69
+ this.chartId = chartId;
70
+ this.chart = chart;
71
+ this.host = host;
72
+ this.structure = new import_chart_structure.ChartStructure(chart);
73
+ this.statesById = this.structure.statesById;
74
+ this.sandbox = import_node_vm.default.createContext(this.createHelpers());
75
+ this.hash = getChartHash(chart);
76
+ this.prepareAllCode();
77
+ }
78
+ structure;
79
+ statesById;
80
+ /** timers of setStateDelayed() */
81
+ delayedWrites = /* @__PURE__ */ new Set();
82
+ /** values of all symbols, this is also the global scope of conditions and actions */
83
+ sandbox;
84
+ /** last value written to (or read from) each output */
85
+ outputValues = /* @__PURE__ */ new Map();
86
+ /** transitions which leave the source state, by source, sorted */
87
+ outerTransitions = /* @__PURE__ */ new Map();
88
+ /** transitions to children of the source state, by source, sorted */
89
+ innerTransitions = /* @__PURE__ */ new Map();
90
+ /** temporal transitions by source */
91
+ temporalTransitions = /* @__PURE__ */ new Map();
92
+ /** all active states, parents before children */
93
+ activeIds = [];
94
+ /** time each active state was entered */
95
+ enteredAt = /* @__PURE__ */ new Map();
96
+ /** timers of the temporal transitions by state */
97
+ timers = /* @__PURE__ */ new Map();
98
+ /** temporal transitions whose time has come */
99
+ expired = /* @__PURE__ */ new Set();
100
+ /** timers of inputs with debounce, by variable name */
101
+ debounceTimers = /* @__PURE__ */ new Map();
102
+ /** JSON of the last persisted data, to persist only changes */
103
+ lastPersisted = "";
104
+ /** name paths of the active leaf states, like the "activeState" state */
105
+ activeNames = "";
106
+ /** events which are not yet passed to host.onHistory() */
107
+ pendingHistory = [];
108
+ /** JSON of the last values passed to host.onValues(), to pass only changes */
109
+ lastValues = "";
110
+ /** what caused the current wakeup, for the history */
111
+ wakeupCause = "input";
112
+ /** name of the input which caused the current wakeup, for the history */
113
+ wakeupInput = null;
114
+ /** the code of conditions, actions and function blocks prepared for the change detection, by kind and code */
115
+ preparedCode = /* @__PURE__ */ new Map();
116
+ /** the variables used by hasChanged() etc. */
117
+ changeVariables = /* @__PURE__ */ new Set();
118
+ /** their values at the start of the previous wakeup, null before the second wakeup */
119
+ previousValues = null;
120
+ /** their values at the start of the current wakeup, null before the first one */
121
+ currentValues = null;
122
+ hash;
123
+ actor = null;
124
+ /** the current time, can be replaced in tests */
125
+ now = () => Date.now();
126
+ /**
127
+ * Prepares all code of the chart for the change detection, so its variables are known from the first wakeup on
128
+ */
129
+ prepareAllCode() {
130
+ for (const state of this.chart.states) {
131
+ for (const code of [state.entry, state.during, state.exit]) {
132
+ this.prepareCode(code, "statements");
133
+ }
134
+ }
135
+ for (const transition of this.chart.transitions) {
136
+ try {
137
+ const label = (0, import_chart_model.parseTransitionLabel)(transition.label);
138
+ this.prepareCode(label.condition, "expression");
139
+ this.prepareCode(label.action, "statements");
140
+ } catch {
141
+ }
142
+ }
143
+ for (const fn of this.chart.functions || []) {
144
+ this.prepareCode(fn.code, "statements");
145
+ }
146
+ }
147
+ /**
148
+ * @param code A condition, action or function block
149
+ * @param kind Expression or statements
150
+ * @returns the code with the names of the variables in hasChanged() etc., see quoteChangeArguments()
151
+ */
152
+ prepareCode(code, kind) {
153
+ if (!code) {
154
+ return "";
155
+ }
156
+ const key = `${kind}:${code}`;
157
+ let prepared = this.preparedCode.get(key);
158
+ if (prepared === void 0) {
159
+ const quoted = (0, import_chart_validation.quoteChangeArguments)(code, kind);
160
+ prepared = quoted.code;
161
+ for (const name of quoted.names) {
162
+ this.changeVariables.add(name);
163
+ }
164
+ this.preparedCode.set(key, prepared);
165
+ }
166
+ return prepared;
167
+ }
168
+ /**
169
+ * IDs of all states this chart reads
170
+ */
171
+ get inputStateIds() {
172
+ return this.chart.symbols.filter((s) => s.scope === "input" && s.stateId).map((s) => s.stateId);
173
+ }
174
+ /**
175
+ * @param options Start options
176
+ * @param options.restore Continue with the persisted state, if the chart was not changed since
177
+ */
178
+ async start(options = {}) {
179
+ this.logProblems();
180
+ for (const symbol of this.chart.symbols) {
181
+ if (symbol.scope === "local") {
182
+ this.sandbox[symbol.name] = structuredClone(symbol.initial);
183
+ } else if (symbol.stateId) {
184
+ const val = await this.host.readState(symbol.stateId);
185
+ this.sandbox[symbol.name] = val;
186
+ if (symbol.scope === "output") {
187
+ this.outputValues.set(symbol.name, val);
188
+ }
189
+ }
190
+ }
191
+ for (const fn of this.chart.functions || []) {
192
+ const where = `function block ${(0, import_chart_validation.getDeclarations)(fn.code).map((d) => d.name).join(", ")}`;
193
+ this.run(this.compile(this.prepareCode(fn.code, "statements"), where), where);
194
+ }
195
+ this.writeChangedOutputs();
196
+ const machine = (0, import_xstate.createMachine)(this.buildMachineConfig());
197
+ const persisted = options.restore && this.chart.restoreState !== false ? await this.loadPersisted() : null;
198
+ let restored = false;
199
+ if (persisted) {
200
+ try {
201
+ this.restore(machine, persisted);
202
+ restored = true;
203
+ this.host.log.info(`[${this.chart.name}] Restored the state from before the restart`);
204
+ } catch (err) {
205
+ this.host.log.warn(`[${this.chart.name}] Cannot restore the state, starting anew: ${err.message}`);
206
+ this.stop();
207
+ this.startActor(machine);
208
+ }
209
+ } else {
210
+ this.startActor(machine);
211
+ }
212
+ this.pendingHistory.push({ ts: this.now(), kind: "start", restored, active: this.activeNames });
213
+ this.wakeup(null, "start");
214
+ }
215
+ /**
216
+ * Starts the chart in its initial state, the entry actions are executed
217
+ *
218
+ * @param machine The XState machine of the chart
219
+ */
220
+ startActor(machine) {
221
+ this.activeIds = [];
222
+ this.enteredAt.clear();
223
+ this.actor = (0, import_xstate.createActor)(machine);
224
+ this.subscribe();
225
+ this.actor.start();
226
+ }
227
+ /**
228
+ * Continues with a persisted state: the entry actions are not executed again,
229
+ * the timers of the temporal transitions continue with their remaining time
230
+ *
231
+ * @param machine The XState machine of the chart
232
+ * @param persisted The stored state
233
+ */
234
+ restore(machine, persisted) {
235
+ for (const symbol of this.chart.symbols) {
236
+ if (symbol.scope === "local" && symbol.name in persisted.locals) {
237
+ this.sandbox[symbol.name] = persisted.locals[symbol.name];
238
+ }
239
+ }
240
+ this.actor = (0, import_xstate.createActor)(machine, { snapshot: persisted.machine });
241
+ this.subscribe();
242
+ this.actor.start();
243
+ this.publishActiveStates(this.actor.getSnapshot().value);
244
+ for (const id of this.activeIds) {
245
+ this.enteredAt.set(id, persisted.entered[id] ?? this.now());
246
+ this.startTimers(id);
247
+ }
248
+ }
249
+ subscribe() {
250
+ this.actor?.subscribe({
251
+ next: (snapshot) => this.publishActiveStates(snapshot.value),
252
+ error: (err) => this.host.log.error(`[${this.chart.name}] ${String(err)}`)
253
+ });
254
+ }
255
+ /**
256
+ * @returns the persisted data, if it fits to this chart
257
+ */
258
+ async loadPersisted() {
259
+ let persisted;
260
+ try {
261
+ persisted = await this.host.loadPersisted?.();
262
+ } catch (err) {
263
+ this.host.log.warn(`[${this.chart.name}] Cannot load the stored state: ${err.message}`);
264
+ return null;
265
+ }
266
+ if (!persisted?.machine || persisted.version !== PERSISTED_VERSION) {
267
+ return null;
268
+ }
269
+ if (persisted.hash !== this.hash) {
270
+ this.host.log.info(`[${this.chart.name}] The chart was changed, starting in the initial state`);
271
+ return null;
272
+ }
273
+ return { ...persisted, entered: persisted.entered || {}, locals: persisted.locals || {} };
274
+ }
275
+ /**
276
+ * Stores the current state of the chart, if it changed since the last time
277
+ */
278
+ persist() {
279
+ if (!this.actor || !this.host.persist) {
280
+ return;
281
+ }
282
+ const data = {
283
+ version: PERSISTED_VERSION,
284
+ hash: this.hash,
285
+ machine: this.actor.getPersistedSnapshot(),
286
+ entered: Object.fromEntries(this.enteredAt),
287
+ locals: Object.fromEntries(
288
+ this.chart.symbols.filter((s) => s.scope === "local").map((s) => [s.name, this.sandbox[s.name]])
289
+ )
290
+ };
291
+ let json;
292
+ try {
293
+ json = JSON.stringify(data);
294
+ } catch (err) {
295
+ this.host.log.warn(`[${this.chart.name}] Cannot store the state: ${err.message}`);
296
+ return;
297
+ }
298
+ if (json !== this.lastPersisted) {
299
+ this.lastPersisted = json;
300
+ this.host.persist(data);
301
+ }
302
+ }
303
+ /**
304
+ * Called by XState before the entry action of a state
305
+ *
306
+ * @param id ID of the state
307
+ */
308
+ onEnter(id) {
309
+ this.enteredAt.set(id, this.now());
310
+ this.startTimers(id);
311
+ }
312
+ /**
313
+ * Called by XState after the exit action of a state
314
+ *
315
+ * @param id ID of the state
316
+ */
317
+ onExit(id) {
318
+ this.enteredAt.delete(id);
319
+ for (const timer of this.timers.get(id) || []) {
320
+ clearTimeout(timer.handle);
321
+ }
322
+ this.timers.delete(id);
323
+ for (const transition of this.temporalTransitions.get(id) || []) {
324
+ this.expired.delete(transition.id);
325
+ }
326
+ }
327
+ /**
328
+ * Starts a timer for every temporal transition of a state, which wakes up the chart when its time has come
329
+ *
330
+ * @param id ID of the state
331
+ */
332
+ startTimers(id) {
333
+ const entered = this.enteredAt.get(id) ?? this.now();
334
+ const timers = [];
335
+ for (const transition of this.temporalTransitions.get(id) || []) {
336
+ const timer = {};
337
+ const due = entered + transition.afterMs;
338
+ const wait = () => {
339
+ const remaining = due - this.now();
340
+ timer.handle = setTimeout(
341
+ () => {
342
+ if (due - this.now() > 0 && remaining > MAX_TIMEOUT_MS) {
343
+ wait();
344
+ return;
345
+ }
346
+ this.expired.add(transition.id);
347
+ this.wakeup(null);
348
+ },
349
+ Math.min(Math.max(remaining, 0), MAX_TIMEOUT_MS)
350
+ );
351
+ };
352
+ wait();
353
+ timers.push(timer);
354
+ }
355
+ this.timers.set(id, timers);
356
+ }
357
+ /**
358
+ * Logs the problems found by the validation, the chart is started anyway
359
+ */
360
+ logProblems() {
361
+ for (const { kind, id, field, diagnostic } of (0, import_chart_validation.validateChart)(this.chart)) {
362
+ let where;
363
+ if (kind === "state") {
364
+ where = `${this.statesById.get(id)?.name ?? id}/${field}`;
365
+ } else if (kind === "function") {
366
+ const fn = this.chart.functions?.find((f) => f.id === id);
367
+ where = `Function block ${(0, import_chart_validation.getDeclarations)(fn?.code).map((d) => d.name).join(", ")}`;
368
+ } else {
369
+ const transition = this.chart.transitions.find((t) => t.id === id);
370
+ const source = this.statesById.get(transition?.source ?? "")?.name;
371
+ const target = this.statesById.get(transition?.target ?? "")?.name;
372
+ where = `Transition ${source} -> ${target}`;
373
+ }
374
+ const log = diagnostic.severity === "error" ? this.host.log.error : this.host.log.warn;
375
+ log(`[${this.chart.name}] ${where}: ${diagnostic.message}`);
376
+ }
377
+ }
378
+ stop() {
379
+ this.actor?.stop();
380
+ this.actor = null;
381
+ for (const timers of this.timers.values()) {
382
+ for (const timer of timers) {
383
+ clearTimeout(timer.handle);
384
+ }
385
+ }
386
+ this.timers.clear();
387
+ this.expired.clear();
388
+ for (const timer of this.debounceTimers.values()) {
389
+ clearTimeout(timer);
390
+ }
391
+ this.debounceTimers.clear();
392
+ for (const timer of this.delayedWrites) {
393
+ clearTimeout(timer);
394
+ }
395
+ this.delayedWrites.clear();
396
+ }
397
+ /**
398
+ * The helper functions which can be used in conditions and actions, see chart-helpers.ts
399
+ */
400
+ createHelpers() {
401
+ const prefix = `[${this.chart.name}]`;
402
+ const write = (id, value, ack) => this.host.writeState(String(id), value, !!ack).catch((err) => this.host.log.warn(`${prefix} Cannot write ${id}: ${err}`));
403
+ return {
404
+ log: (message, level = "info") => {
405
+ const method = ["debug", "info", "warn", "error"].includes(level) ? level : "info";
406
+ this.host.log[method](`${prefix} ${message}`);
407
+ },
408
+ setState: (id, value, ack = false) => {
409
+ void write(id, value, ack);
410
+ },
411
+ setStateDelayed: (id, value, delayMs, ack = false) => {
412
+ const timer = setTimeout(
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
+ );
419
+ this.delayedWrites.add(timer);
420
+ },
421
+ sendTo: (instance, command, message) => {
422
+ if (!this.host.sendTo) {
423
+ throw new Error("sendTo() is not available");
424
+ }
425
+ this.host.sendTo(String(instance), String(command), message);
426
+ },
427
+ isTimeBetween: (start, end) => (0, import_chart_helpers.isTimeBetween)(start, end, new Date(this.now())),
428
+ isDaytime: () => {
429
+ const now = new Date(this.now());
430
+ const { sunrise, sunset, alwaysUp } = this.getSunTimes(now);
431
+ if (!sunrise || !sunset) {
432
+ return !!alwaysUp;
433
+ }
434
+ return now >= sunrise && now < sunset;
435
+ },
436
+ astroTime: (name, offsetMinutes = 0) => {
437
+ const times = this.getSunTimes(new Date(this.now()));
438
+ const time = times[name];
439
+ if (!(time instanceof Date)) {
440
+ const known = name in times && name !== "alwaysUp" && name !== "alwaysDown";
441
+ throw new Error(
442
+ known ? `There is no "${name}" today at the location of the system settings` : `Unknown astro event "${name}"`
443
+ );
444
+ }
445
+ return new Date(time.getTime() + (Number(offsetMinutes) || 0) * 6e4);
446
+ },
447
+ hasChanged: (name) => this.getChange(name, "hasChanged").changed,
448
+ hasChangedFrom: (name, value) => {
449
+ const { changed, previous } = this.getChange(name, "hasChangedFrom");
450
+ return changed && (0, import_node_util.isDeepStrictEqual)(previous, value);
451
+ },
452
+ hasChangedTo: (name, value) => {
453
+ const { changed, current } = this.getChange(name, "hasChangedTo");
454
+ return changed && (0, import_node_util.isDeepStrictEqual)(current, value);
455
+ }
456
+ };
457
+ }
458
+ /**
459
+ * @param date The day
460
+ * @returns the astro events of the day, null for events which do not happen on that day (polar regions)
461
+ * @throws {Error} if no location is configured
462
+ */
463
+ getSunTimes(date) {
464
+ const location = this.host.getLocation?.();
465
+ if (!location) {
466
+ throw new Error("No location (latitude/longitude) configured in the ioBroker system settings");
467
+ }
468
+ return import_suncalc.default.getTimes(date, location.latitude, location.longitude);
469
+ }
470
+ /**
471
+ * Must be called when an input state is updated
472
+ *
473
+ * @param stateId ID of the updated state
474
+ * @param val The new value
475
+ * @param options The ack flag of the update
476
+ * @param options.ack The ack flag of the update
477
+ */
478
+ handleInput(stateId, val, options = {}) {
479
+ if (!this.actor) {
480
+ return;
481
+ }
482
+ for (const symbol of this.chart.symbols) {
483
+ if (symbol.scope !== "input" || symbol.stateId !== stateId) {
484
+ continue;
485
+ }
486
+ if (symbol.ack === "ack" && options.ack !== true || symbol.ack === "command" && options.ack !== false) {
487
+ continue;
488
+ }
489
+ const debounceMs = symbol.debounceMs || 0;
490
+ if (debounceMs > 0) {
491
+ clearTimeout(this.debounceTimers.get(symbol.name));
492
+ this.debounceTimers.set(
493
+ symbol.name,
494
+ setTimeout(() => {
495
+ this.debounceTimers.delete(symbol.name);
496
+ this.applyInput(symbol, val);
497
+ }, debounceMs)
498
+ );
499
+ } else {
500
+ this.applyInput(symbol, val);
501
+ }
502
+ }
503
+ }
504
+ /**
505
+ * Sets the variable of an input and wakes up the chart
506
+ *
507
+ * @param symbol The input
508
+ * @param val The new value
509
+ */
510
+ applyInput(symbol, val) {
511
+ if (symbol.trigger === "change" && val === this.sandbox[symbol.name]) {
512
+ return;
513
+ }
514
+ this.sandbox[symbol.name] = val;
515
+ this.wakeup(symbol.name);
516
+ }
517
+ /**
518
+ * @param input Name of the input which caused the wakeup, null for timers and the start
519
+ * @param cause What caused the wakeup
520
+ */
521
+ wakeup(input, cause = input === null ? "timer" : "input") {
522
+ if (!this.actor) {
523
+ return;
524
+ }
525
+ this.wakeupCause = cause;
526
+ this.wakeupInput = input;
527
+ this.rememberValues();
528
+ if (!this.chart.superStep) {
529
+ this.step(input);
530
+ } else {
531
+ let steps = 0;
532
+ let stepInput = input;
533
+ while (steps < import_chart_model.MAX_SUPER_STEPS && this.step(stepInput)) {
534
+ steps++;
535
+ stepInput = null;
536
+ }
537
+ if (steps >= import_chart_model.MAX_SUPER_STEPS) {
538
+ this.host.log.warn(
539
+ `[${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.`
540
+ );
541
+ }
542
+ }
543
+ this.writeChangedOutputs();
544
+ this.persist();
545
+ this.publishLive();
546
+ }
547
+ /**
548
+ * Stores the values of the variables used by the change detection at the start of a wakeup
549
+ */
550
+ rememberValues() {
551
+ if (!this.changeVariables.size) {
552
+ return;
553
+ }
554
+ this.previousValues = this.currentValues;
555
+ this.currentValues = /* @__PURE__ */ new Map();
556
+ for (const name of this.changeVariables) {
557
+ const value = this.sandbox[name];
558
+ let copy;
559
+ try {
560
+ copy = structuredClone(value);
561
+ } catch {
562
+ copy = value;
563
+ }
564
+ this.currentValues.set(name, copy);
565
+ }
566
+ }
567
+ /**
568
+ * @param name Name of the variable
569
+ * @param helper Name of the helper, for the error message
570
+ * @returns whether the variable has changed between the start of the previous and of the current wakeup
571
+ * @throws {Error} if the variable is unknown
572
+ */
573
+ getChange(name, helper) {
574
+ if (!this.changeVariables.has(name) || !this.chart.symbols.some((s) => s.name === name)) {
575
+ throw new Error(`${helper}() needs a variable of the chart as first argument`);
576
+ }
577
+ if (!this.previousValues || !this.currentValues) {
578
+ return { changed: false };
579
+ }
580
+ const previous = this.previousValues.get(name);
581
+ const current = this.currentValues.get(name);
582
+ return { changed: !(0, import_node_util.isDeepStrictEqual)(previous, current), previous, current };
583
+ }
584
+ /**
585
+ * Processes the active states once, see the class description
586
+ *
587
+ * @param input Name of the input which caused the wakeup, null if the step has no event
588
+ * @returns true if a transition was taken
589
+ */
590
+ step(input) {
591
+ for (const id of this.getActiveChildren(null)) {
592
+ if (this.processState(id, input)) {
593
+ return true;
594
+ }
595
+ }
596
+ return false;
597
+ }
598
+ /**
599
+ * Passes the events since the last call and the changed values of the variables to the host
600
+ */
601
+ publishLive() {
602
+ if (this.pendingHistory.length) {
603
+ const entries = this.pendingHistory;
604
+ this.pendingHistory = [];
605
+ this.host.onHistory?.(entries);
606
+ }
607
+ if (!this.host.onValues) {
608
+ return;
609
+ }
610
+ const values = Object.fromEntries(this.chart.symbols.map((s) => [s.name, this.sandbox[s.name]]));
611
+ let json;
612
+ try {
613
+ json = JSON.stringify(values);
614
+ } catch {
615
+ for (const [name, value] of Object.entries(values)) {
616
+ try {
617
+ JSON.stringify(value);
618
+ } catch {
619
+ values[name] = String(value);
620
+ }
621
+ }
622
+ json = JSON.stringify(values);
623
+ }
624
+ if (json !== this.lastValues) {
625
+ this.lastValues = json;
626
+ this.host.onValues(values);
627
+ }
628
+ }
629
+ /**
630
+ * Processes an active state and its active children, see the class description
631
+ *
632
+ * @param id ID of the state
633
+ * @param input Name of the input which caused the wakeup, null for timers
634
+ * @returns true if a transition was taken
635
+ */
636
+ processState(id, input) {
637
+ if (this.takeFirstValid(this.outerTransitions.get(id), input)) {
638
+ return true;
639
+ }
640
+ const state = this.statesById.get(id);
641
+ if (state?.during) {
642
+ this.runAction(state.during, `${state.name}/during`);
643
+ }
644
+ if (this.takeFirstValid(this.innerTransitions.get(id), input)) {
645
+ return true;
646
+ }
647
+ for (const child of this.getActiveChildren(id)) {
648
+ if (this.processState(child, input)) {
649
+ return true;
650
+ }
651
+ }
652
+ return false;
653
+ }
654
+ /**
655
+ * @param parent null for the top level
656
+ */
657
+ getActiveChildren(parent) {
658
+ const children = new Set(this.structure.getChildren(parent).map((s) => s.id));
659
+ return this.activeIds.filter((id) => children.has(id));
660
+ }
661
+ /**
662
+ * Takes the first transition whose event and condition match
663
+ *
664
+ * @param transitions The transitions to check, sorted
665
+ * @param input Name of the input which caused the wakeup, null for timers
666
+ * @returns true if a transition was taken
667
+ */
668
+ takeFirstValid(transitions, input) {
669
+ for (const transition of transitions || []) {
670
+ if (transition.event && transition.event !== input) {
671
+ continue;
672
+ }
673
+ if (transition.afterMs !== void 0 && !this.isExpired(transition)) {
674
+ continue;
675
+ }
676
+ if (transition.condition && !this.run(transition.condition, `${transition.where}/condition`)) {
677
+ continue;
678
+ }
679
+ this.actor?.send({ type: `T.${transition.id}` });
680
+ this.pendingHistory.push({
681
+ ts: this.now(),
682
+ kind: "transition",
683
+ transition: transition.id,
684
+ from: transition.from,
685
+ to: transition.to,
686
+ // in a super step, all transitions of the wakeup were caused by its input
687
+ trigger: this.wakeupInput,
688
+ cause: this.wakeupCause,
689
+ active: this.activeNames
690
+ });
691
+ return true;
692
+ }
693
+ return false;
694
+ }
695
+ /**
696
+ * @param transition A temporal transition
697
+ * @returns true if its source state is active for at least its delay
698
+ */
699
+ isExpired(transition) {
700
+ if (this.expired.has(transition.id)) {
701
+ return true;
702
+ }
703
+ const entered = this.enteredAt.get(transition.source);
704
+ return entered !== void 0 && this.now() - entered >= transition.afterMs;
705
+ }
706
+ buildMachineConfig() {
707
+ const structure = this.structure;
708
+ const buildState = (state) => {
709
+ const node = {
710
+ id: state.id,
711
+ entry: [
712
+ () => this.onEnter(state.id),
713
+ ...state.entry ? [() => this.runAction(state.entry, `${state.name}/entry`)] : []
714
+ ],
715
+ exit: [
716
+ ...state.exit ? [() => this.runAction(state.exit, `${state.name}/exit`)] : [],
717
+ () => this.onExit(state.id)
718
+ ],
719
+ on: {}
720
+ };
721
+ const outer = [];
722
+ const inner = [];
723
+ for (const transition of structure.getTransitions(state.id)) {
724
+ const built = this.buildTransition(transition, state);
725
+ if (!built) {
726
+ continue;
727
+ }
728
+ node.on[`T.${transition.id}`] = built.config;
729
+ (built.inner ? inner : outer).push(built.checked);
730
+ }
731
+ this.outerTransitions.set(state.id, outer);
732
+ this.innerTransitions.set(state.id, inner);
733
+ this.temporalTransitions.set(
734
+ state.id,
735
+ [...outer, ...inner].filter((t) => t.afterMs !== void 0)
736
+ );
737
+ const subStates = structure.getChildren(state.id);
738
+ if (subStates.length) {
739
+ node.initial = structure.getInitialChild(state.id);
740
+ node.states = Object.fromEntries(subStates.map((s) => [s.id, buildState(s)]));
741
+ }
742
+ return node;
743
+ };
744
+ const topLevel = structure.getChildren(null);
745
+ if (!topLevel.length) {
746
+ throw new Error("The chart has no states");
747
+ }
748
+ return {
749
+ id: `chart_${this.chartId}`,
750
+ initial: structure.getInitialChild(null),
751
+ states: Object.fromEntries(topLevel.map((s) => [s.id, buildState(s)]))
752
+ };
753
+ }
754
+ /**
755
+ * @param transition The transition
756
+ * @param source Its source state
757
+ */
758
+ buildTransition(transition, source) {
759
+ const target = this.statesById.get(transition.target);
760
+ const where = `${source.name} -> ${target?.name ?? transition.target}`;
761
+ if (!target) {
762
+ this.host.log.warn(`[${this.chart.name}] Transition ${where}: unknown target, ignored`);
763
+ return null;
764
+ }
765
+ let label;
766
+ try {
767
+ label = (0, import_chart_model.parseTransitionLabel)(transition.label);
768
+ } catch (err) {
769
+ this.host.log.error(`[${this.chart.name}] Transition ${where}: ${err.message}, ignored`);
770
+ return null;
771
+ }
772
+ const condition = label.condition ? this.compile(`(${this.prepareCode(label.condition, "expression")})`, `${where}/condition`) : null;
773
+ const { inner, reenter } = this.structure.getTransitionKind(transition);
774
+ const config = {
775
+ target: `#${target.id}`,
776
+ reenter,
777
+ actions: label.action ? [() => this.runAction(label.action, `${where}/action`)] : []
778
+ };
779
+ return {
780
+ inner,
781
+ config,
782
+ checked: {
783
+ id: transition.id,
784
+ source: source.id,
785
+ where,
786
+ from: source.name,
787
+ to: target.name,
788
+ event: label.event,
789
+ afterMs: label.afterMs,
790
+ condition
791
+ }
792
+ };
793
+ }
794
+ /**
795
+ * @param code The code
796
+ * @param where Used in error messages
797
+ */
798
+ compile(code, where) {
799
+ try {
800
+ return new import_node_vm.default.Script(code, { filename: `${this.chart.name}/${where}` });
801
+ } catch (err) {
802
+ this.host.log.error(`[${this.chart.name}] ${where}: ${err.message}`);
803
+ return null;
804
+ }
805
+ }
806
+ /**
807
+ * @param script The compiled code
808
+ * @param where Used in error messages
809
+ */
810
+ run(script, where) {
811
+ if (!script) {
812
+ return void 0;
813
+ }
814
+ try {
815
+ return script.runInContext(this.sandbox, { timeout: SCRIPT_TIMEOUT_MS });
816
+ } catch (err) {
817
+ this.host.log.warn(`[${this.chart.name}] ${where}: ${err.message}`);
818
+ return void 0;
819
+ }
820
+ }
821
+ /**
822
+ * Executes an action and writes all changed outputs
823
+ *
824
+ * @param code The action
825
+ * @param where Used in error messages
826
+ */
827
+ runAction(code, where) {
828
+ this.run(this.compile(`{
829
+ ${this.prepareCode(code, "statements")}
830
+ }`, where), where);
831
+ this.writeChangedOutputs();
832
+ }
833
+ writeChangedOutputs() {
834
+ for (const symbol of this.chart.symbols) {
835
+ if (symbol.scope !== "output" || !symbol.stateId) {
836
+ continue;
837
+ }
838
+ const val = this.sandbox[symbol.name];
839
+ if (val !== this.outputValues.get(symbol.name)) {
840
+ this.outputValues.set(symbol.name, val);
841
+ this.host.writeState(symbol.stateId, val).catch((err) => this.host.log.warn(`[${this.chart.name}] Cannot write ${symbol.stateId}: ${err}`));
842
+ }
843
+ }
844
+ }
845
+ /**
846
+ * @param value XState state value
847
+ */
848
+ publishActiveStates(value) {
849
+ const ids = [];
850
+ const names = [];
851
+ const walk = (val, path) => {
852
+ const keys = typeof val === "string" ? [val] : Object.keys(val || {});
853
+ for (const key of keys) {
854
+ const name = [...path, this.statesById.get(key)?.name ?? key];
855
+ ids.push(key);
856
+ if (typeof val === "string" || !val[key] || !Object.keys(val[key]).length) {
857
+ names.push(name.join("."));
858
+ } else {
859
+ walk(val[key], name);
860
+ }
861
+ }
862
+ };
863
+ walk(value, []);
864
+ if (ids.join() !== this.activeIds.join()) {
865
+ this.activeIds = ids;
866
+ this.activeNames = names.join(", ");
867
+ this.host.onActiveChange({ names: this.activeNames, ids });
868
+ }
869
+ }
870
+ }
871
+ // Annotate the CommonJS export names for ESM import in node:
872
+ 0 && (module.exports = {
873
+ ChartRuntime,
874
+ getChartHash
875
+ });
876
+ //# sourceMappingURL=chart-runtime.js.map